diff --git a/goldens/aria/listbox/testing/index.api.md b/goldens/aria/listbox/testing/index.api.md new file mode 100644 index 000000000000..088029b94111 --- /dev/null +++ b/goldens/aria/listbox/testing/index.api.md @@ -0,0 +1,60 @@ +## API Report File for "@angular/aria_listbox_testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseHarnessFilters } from '@angular/cdk/testing'; +import { ComponentHarness } from '@angular/cdk/testing'; +import { HarnessPredicate } from '@angular/cdk/testing'; + +// @public (undocumented) +export class ListboxHarness extends ComponentHarness { + blur(): Promise; + // (undocumented) + focus(): Promise; + // (undocumented) + getOptions(filters?: ListboxOptionHarnessFilters): Promise; + // (undocumented) + getOrientation(): Promise<'vertical' | 'horizontal'>; + // (undocumented) + static hostSelector: string; + // (undocumented) + isDisabled(): Promise; + // (undocumented) + isMulti(): Promise; + // (undocumented) + static with(options?: ListboxHarnessFilters): HarnessPredicate; +} + +// @public +export interface ListboxHarnessFilters extends BaseHarnessFilters { + disabled?: boolean; +} + +// @public (undocumented) +export class ListboxOptionHarness extends ComponentHarness { + // (undocumented) + click(): Promise; + // (undocumented) + getText(): Promise; + // (undocumented) + static hostSelector: string; + // (undocumented) + isDisabled(): Promise; + // (undocumented) + isSelected(): Promise; + // (undocumented) + static with(options?: ListboxOptionHarnessFilters): HarnessPredicate; +} + +// @public +export interface ListboxOptionHarnessFilters extends BaseHarnessFilters { + disabled?: boolean; + selected?: boolean; + text?: string | RegExp; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/src/aria/config.bzl b/src/aria/config.bzl index 291412b5a3fb..9b0a8e342880 100644 --- a/src/aria/config.bzl +++ b/src/aria/config.bzl @@ -4,6 +4,7 @@ ARIA_ENTRYPOINTS = [ "combobox", "grid", "listbox", + "listbox/testing", "menu", "tabs", "toolbar", diff --git a/src/aria/listbox/BUILD.bazel b/src/aria/listbox/BUILD.bazel index 4ba1e872d6eb..59719d4a9161 100644 --- a/src/aria/listbox/BUILD.bazel +++ b/src/aria/listbox/BUILD.bazel @@ -14,6 +14,7 @@ ng_project( "//src/aria/private", "//src/cdk/a11y", "//src/cdk/bidi", + "//src/cdk/testing", ], ) @@ -29,7 +30,9 @@ ng_project( "//:node_modules/@angular/core", "//:node_modules/@angular/platform-browser", "//:node_modules/axe-core", + "//src/cdk/testing", "//src/cdk/testing/private", + "//src/cdk/testing/testbed", ], ) diff --git a/src/aria/listbox/testing/BUILD.bazel b/src/aria/listbox/testing/BUILD.bazel new file mode 100644 index 000000000000..b3a3ebb2ab26 --- /dev/null +++ b/src/aria/listbox/testing/BUILD.bazel @@ -0,0 +1,42 @@ +load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//:node_modules/@angular/core", + "//src/cdk/testing", + ], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) + +ng_project( + name = "unit_tests_lib", + testonly = True, + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":testing", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//src/aria/listbox", + "//src/cdk/testing", + "//src/cdk/testing/private", + "//src/cdk/testing/testbed", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":unit_tests_lib", + ], +) diff --git a/src/aria/listbox/testing/index.ts b/src/aria/listbox/testing/index.ts new file mode 100644 index 000000000000..52b3c7a5156f --- /dev/null +++ b/src/aria/listbox/testing/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './public-api'; diff --git a/src/aria/listbox/testing/listbox-harness-filters.ts b/src/aria/listbox/testing/listbox-harness-filters.ts new file mode 100644 index 000000000000..5fb667c95a8d --- /dev/null +++ b/src/aria/listbox/testing/listbox-harness-filters.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {BaseHarnessFilters} from '@angular/cdk/testing'; + +/** Filters for locating a `ListboxOptionHarness`. */ +export interface ListboxOptionHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose text matches the given value. */ + text?: string | RegExp; + /** Only find instances whose selected state matches the given value. */ + selected?: boolean; + /** Only find instances whose disabled state matches the given value. */ + disabled?: boolean; +} + +/** Filters for locating a `ListboxHarness`. */ +export interface ListboxHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose disabled state matches the given value. */ + disabled?: boolean; +} diff --git a/src/aria/listbox/testing/listbox-harness.spec.ts b/src/aria/listbox/testing/listbox-harness.spec.ts new file mode 100644 index 000000000000..4d90e5bf09ac --- /dev/null +++ b/src/aria/listbox/testing/listbox-harness.spec.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; + +import {ListboxHarness, ListboxOptionHarness} from './listbox-harness'; +import {Listbox, Option} from '../index'; + +describe('Listbox Harness', () => { + let fixture: any; + let loader: any; + + @Component({ + imports: [Listbox, Option], + template: ` +
    +
  • Apple
  • +
  • Banana
  • +
    Inside Listbox
    +
+ `, + }) + class ListboxHarnessTestComponent {} + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ListboxHarnessTestComponent], + }); + fixture = TestBed.createComponent(ListboxHarnessTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('finds the listbox container harness', async () => { + const listbox = await loader.getHarness(ListboxHarness); + expect(listbox).toBeTruthy(); + }); + + it('returns all options scoped within the listbox', async () => { + const listbox = await loader.getHarness(ListboxHarness); + + const options = await listbox.getOptions(); + + expect(options.length).toBe(2); + }); + + it('filters options by exact text content', async () => { + const listbox = await loader.getHarness(ListboxHarness); + + const options = await listbox.getOptions({text: 'Apple'}); + + expect(options.length).toBe(1); + }); + + it('reports the disabled state of the listbox', async () => { + const listbox = await loader.getHarness(ListboxHarness); + + const isDisabled = await listbox.isDisabled(); + + expect(isDisabled).toBeFalse(); + }); + + it('reports the multi-selectable state of the listbox', async () => { + const listbox = await loader.getHarness(ListboxHarness); + + const isMulti = await listbox.isMulti(); + + expect(isMulti).toBeTrue(); + }); + + it('reports the orientation of the listbox', async () => { + const listbox = await loader.getHarness(ListboxHarness); + + const orientation = await listbox.getOrientation(); + + expect(orientation).toBe('horizontal'); + }); + + it('clicks an option inside the listbox', async () => { + const option = await loader.getHarness(ListboxOptionHarness.with({text: 'Apple'})); + + await option.click(); + + expect(await option.isSelected()).toBeTrue(); + }); +}); diff --git a/src/aria/listbox/testing/listbox-harness.ts b/src/aria/listbox/testing/listbox-harness.ts new file mode 100644 index 000000000000..9a07b7ed87ee --- /dev/null +++ b/src/aria/listbox/testing/listbox-harness.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {ListboxHarnessFilters, ListboxOptionHarnessFilters} from './listbox-harness-filters'; + +export class ListboxOptionHarness extends ComponentHarness { + static hostSelector = '[ngOption]'; + + static with(options: ListboxOptionHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(ListboxOptionHarness, options) + .addOption('text', options.text, (harness, text) => + HarnessPredicate.stringMatches(harness.getText(), text), + ) + .addOption( + 'selected', + options.selected, + async (harness, selected) => (await harness.isSelected()) === selected, + ) + .addOption( + 'disabled', + options.disabled, + async (harness, disabled) => (await harness.isDisabled()) === disabled, + ); + } + + async isSelected(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-selected')) === 'true'; + } + + async isDisabled(): Promise { + const host = await this.host(); + return ( + (await host.getAttribute('aria-disabled')) === 'true' || + (await host.getProperty('disabled')) === true + ); + } + + async getText(): Promise { + const host = await this.host(); + return host.text(); + } + + async click(): Promise { + const host = await this.host(); + return host.click(); + } +} + +export class ListboxHarness extends ComponentHarness { + static hostSelector = '[ngListbox]'; + + static with(options: ListboxHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(ListboxHarness, options).addOption( + 'disabled', + options.disabled, + async (harness, disabled) => (await harness.isDisabled()) === disabled, + ); + } + + async getOrientation(): Promise<'vertical' | 'horizontal'> { + const host = await this.host(); + const orientation = await host.getAttribute('aria-orientation'); + return orientation === 'horizontal' ? 'horizontal' : 'vertical'; + } + + async isMulti(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-multiselectable')) === 'true'; + } + + async isDisabled(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-disabled')) === 'true'; + } + + async getOptions(filters: ListboxOptionHarnessFilters = {}): Promise { + return this.locatorForAll(ListboxOptionHarness.with(filters))(); + } + + async focus(): Promise { + await (await this.host()).focus(); + } + + /** Blurs the listbox container. */ + async blur(): Promise { + await (await this.host()).blur(); + } +} diff --git a/src/aria/listbox/testing/public-api.ts b/src/aria/listbox/testing/public-api.ts new file mode 100644 index 000000000000..623e2559fd88 --- /dev/null +++ b/src/aria/listbox/testing/public-api.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './listbox-harness'; +export * from './listbox-harness-filters';