Skip to content

Commit 1885d35

Browse files
authored
feat(aria/listbox): introduce listbox harness (angular#33064)
* feat(aria/listbox): introduce listbox harness * fixup! feat(aria/listbox): introduce listbox harness
1 parent d91f46b commit 1885d35

9 files changed

Lines changed: 338 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## API Report File for "@angular/aria_listbox_testing"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { BaseHarnessFilters } from '@angular/cdk/testing';
8+
import { ComponentHarness } from '@angular/cdk/testing';
9+
import { HarnessPredicate } from '@angular/cdk/testing';
10+
11+
// @public (undocumented)
12+
export class ListboxHarness extends ComponentHarness {
13+
blur(): Promise<void>;
14+
// (undocumented)
15+
focus(): Promise<void>;
16+
// (undocumented)
17+
getOptions(filters?: ListboxOptionHarnessFilters): Promise<ListboxOptionHarness[]>;
18+
// (undocumented)
19+
getOrientation(): Promise<'vertical' | 'horizontal'>;
20+
// (undocumented)
21+
static hostSelector: string;
22+
// (undocumented)
23+
isDisabled(): Promise<boolean>;
24+
// (undocumented)
25+
isMulti(): Promise<boolean>;
26+
// (undocumented)
27+
static with(options?: ListboxHarnessFilters): HarnessPredicate<ListboxHarness>;
28+
}
29+
30+
// @public
31+
export interface ListboxHarnessFilters extends BaseHarnessFilters {
32+
disabled?: boolean;
33+
}
34+
35+
// @public (undocumented)
36+
export class ListboxOptionHarness extends ComponentHarness {
37+
// (undocumented)
38+
click(): Promise<void>;
39+
// (undocumented)
40+
getText(): Promise<string>;
41+
// (undocumented)
42+
static hostSelector: string;
43+
// (undocumented)
44+
isDisabled(): Promise<boolean>;
45+
// (undocumented)
46+
isSelected(): Promise<boolean>;
47+
// (undocumented)
48+
static with(options?: ListboxOptionHarnessFilters): HarnessPredicate<ListboxOptionHarness>;
49+
}
50+
51+
// @public
52+
export interface ListboxOptionHarnessFilters extends BaseHarnessFilters {
53+
disabled?: boolean;
54+
selected?: boolean;
55+
text?: string | RegExp;
56+
}
57+
58+
// (No @packageDocumentation comment for this package)
59+
60+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ARIA_ENTRYPOINTS = [
55
"combobox",
66
"grid",
77
"listbox",
8+
"listbox/testing",
89
"menu",
910
"tabs",
1011
"toolbar",

src/aria/listbox/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ng_project(
1414
"//src/aria/private",
1515
"//src/cdk/a11y",
1616
"//src/cdk/bidi",
17+
"//src/cdk/testing",
1718
],
1819
)
1920

@@ -29,7 +30,9 @@ ng_project(
2930
"//:node_modules/@angular/core",
3031
"//:node_modules/@angular/platform-browser",
3132
"//:node_modules/axe-core",
33+
"//src/cdk/testing",
3234
"//src/cdk/testing/private",
35+
"//src/cdk/testing/testbed",
3336
],
3437
)
3538

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_project(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src/cdk/testing",
14+
],
15+
)
16+
17+
filegroup(
18+
name = "source-files",
19+
srcs = glob(["**/*.ts"]),
20+
)
21+
22+
ng_project(
23+
name = "unit_tests_lib",
24+
testonly = True,
25+
srcs = glob(["**/*.spec.ts"]),
26+
deps = [
27+
":testing",
28+
"//:node_modules/@angular/core",
29+
"//:node_modules/@angular/platform-browser",
30+
"//src/aria/listbox",
31+
"//src/cdk/testing",
32+
"//src/cdk/testing/private",
33+
"//src/cdk/testing/testbed",
34+
],
35+
)
36+
37+
ng_web_test_suite(
38+
name = "unit_tests",
39+
deps = [
40+
":unit_tests_lib",
41+
],
42+
)

src/aria/listbox/testing/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
/** Filters for locating a `ListboxOptionHarness`. */
12+
export interface ListboxOptionHarnessFilters extends BaseHarnessFilters {
13+
/** Only find instances whose text matches the given value. */
14+
text?: string | RegExp;
15+
/** Only find instances whose selected state matches the given value. */
16+
selected?: boolean;
17+
/** Only find instances whose disabled state matches the given value. */
18+
disabled?: boolean;
19+
}
20+
21+
/** Filters for locating a `ListboxHarness`. */
22+
export interface ListboxHarnessFilters extends BaseHarnessFilters {
23+
/** Only find instances whose disabled state matches the given value. */
24+
disabled?: boolean;
25+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Component} from '@angular/core';
10+
import {TestBed} from '@angular/core/testing';
11+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
12+
13+
import {ListboxHarness, ListboxOptionHarness} from './listbox-harness';
14+
import {Listbox, Option} from '../index';
15+
16+
describe('Listbox Harness', () => {
17+
let fixture: any;
18+
let loader: any;
19+
20+
@Component({
21+
imports: [Listbox, Option],
22+
template: `
23+
<ul ngListbox [disabled]="false" [multi]="true" orientation="horizontal">
24+
<li ngOption [value]="1" label="Apple" aria-selected="true">Apple</li>
25+
<li ngOption [value]="2" label="Banana">Banana</li>
26+
<div class="test-item">Inside Listbox</div>
27+
</ul>
28+
`,
29+
})
30+
class ListboxHarnessTestComponent {}
31+
32+
beforeEach(() => {
33+
TestBed.configureTestingModule({
34+
imports: [ListboxHarnessTestComponent],
35+
});
36+
fixture = TestBed.createComponent(ListboxHarnessTestComponent);
37+
fixture.detectChanges();
38+
loader = TestbedHarnessEnvironment.loader(fixture);
39+
});
40+
41+
it('finds the listbox container harness', async () => {
42+
const listbox = await loader.getHarness(ListboxHarness);
43+
expect(listbox).toBeTruthy();
44+
});
45+
46+
it('returns all options scoped within the listbox', async () => {
47+
const listbox = await loader.getHarness(ListboxHarness);
48+
49+
const options = await listbox.getOptions();
50+
51+
expect(options.length).toBe(2);
52+
});
53+
54+
it('filters options by exact text content', async () => {
55+
const listbox = await loader.getHarness(ListboxHarness);
56+
57+
const options = await listbox.getOptions({text: 'Apple'});
58+
59+
expect(options.length).toBe(1);
60+
});
61+
62+
it('reports the disabled state of the listbox', async () => {
63+
const listbox = await loader.getHarness(ListboxHarness);
64+
65+
const isDisabled = await listbox.isDisabled();
66+
67+
expect(isDisabled).toBeFalse();
68+
});
69+
70+
it('reports the multi-selectable state of the listbox', async () => {
71+
const listbox = await loader.getHarness(ListboxHarness);
72+
73+
const isMulti = await listbox.isMulti();
74+
75+
expect(isMulti).toBeTrue();
76+
});
77+
78+
it('reports the orientation of the listbox', async () => {
79+
const listbox = await loader.getHarness(ListboxHarness);
80+
81+
const orientation = await listbox.getOrientation();
82+
83+
expect(orientation).toBe('horizontal');
84+
});
85+
86+
it('clicks an option inside the listbox', async () => {
87+
const option = await loader.getHarness(ListboxOptionHarness.with({text: 'Apple'}));
88+
89+
await option.click();
90+
91+
expect(await option.isSelected()).toBeTrue();
92+
});
93+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {ListboxHarnessFilters, ListboxOptionHarnessFilters} from './listbox-harness-filters';
11+
12+
export class ListboxOptionHarness extends ComponentHarness {
13+
static hostSelector = '[ngOption]';
14+
15+
static with(options: ListboxOptionHarnessFilters = {}): HarnessPredicate<ListboxOptionHarness> {
16+
return new HarnessPredicate(ListboxOptionHarness, options)
17+
.addOption('text', options.text, (harness, text) =>
18+
HarnessPredicate.stringMatches(harness.getText(), text),
19+
)
20+
.addOption(
21+
'selected',
22+
options.selected,
23+
async (harness, selected) => (await harness.isSelected()) === selected,
24+
)
25+
.addOption(
26+
'disabled',
27+
options.disabled,
28+
async (harness, disabled) => (await harness.isDisabled()) === disabled,
29+
);
30+
}
31+
32+
async isSelected(): Promise<boolean> {
33+
const host = await this.host();
34+
return (await host.getAttribute('aria-selected')) === 'true';
35+
}
36+
37+
async isDisabled(): Promise<boolean> {
38+
const host = await this.host();
39+
return (
40+
(await host.getAttribute('aria-disabled')) === 'true' ||
41+
(await host.getProperty('disabled')) === true
42+
);
43+
}
44+
45+
async getText(): Promise<string> {
46+
const host = await this.host();
47+
return host.text();
48+
}
49+
50+
async click(): Promise<void> {
51+
const host = await this.host();
52+
return host.click();
53+
}
54+
}
55+
56+
export class ListboxHarness extends ComponentHarness {
57+
static hostSelector = '[ngListbox]';
58+
59+
static with(options: ListboxHarnessFilters = {}): HarnessPredicate<ListboxHarness> {
60+
return new HarnessPredicate(ListboxHarness, options).addOption(
61+
'disabled',
62+
options.disabled,
63+
async (harness, disabled) => (await harness.isDisabled()) === disabled,
64+
);
65+
}
66+
67+
async getOrientation(): Promise<'vertical' | 'horizontal'> {
68+
const host = await this.host();
69+
const orientation = await host.getAttribute('aria-orientation');
70+
return orientation === 'horizontal' ? 'horizontal' : 'vertical';
71+
}
72+
73+
async isMulti(): Promise<boolean> {
74+
const host = await this.host();
75+
return (await host.getAttribute('aria-multiselectable')) === 'true';
76+
}
77+
78+
async isDisabled(): Promise<boolean> {
79+
const host = await this.host();
80+
return (await host.getAttribute('aria-disabled')) === 'true';
81+
}
82+
83+
async getOptions(filters: ListboxOptionHarnessFilters = {}): Promise<ListboxOptionHarness[]> {
84+
return this.locatorForAll(ListboxOptionHarness.with(filters))();
85+
}
86+
87+
async focus(): Promise<void> {
88+
await (await this.host()).focus();
89+
}
90+
91+
/** Blurs the listbox container. */
92+
async blur(): Promise<void> {
93+
await (await this.host()).blur();
94+
}
95+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './listbox-harness';
10+
export * from './listbox-harness-filters';

0 commit comments

Comments
 (0)