Skip to content

Commit e3d84f2

Browse files
authored
feat(aria/combobox): add test harnesses (#33194)
* feat(aria/combobox): add test harnesses * fixup! feat(aria/combobox): add test harnesses
1 parent c6e99f1 commit e3d84f2

8 files changed

Lines changed: 480 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## API Report File for "@angular/aria_simple-combobox_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 { ComponentHarnessConstructor } from '@angular/cdk/testing';
10+
import { ContentContainerComponentHarness } from '@angular/cdk/testing';
11+
import { HarnessLoader } from '@angular/cdk/testing';
12+
import { HarnessPredicate } from '@angular/cdk/testing';
13+
14+
// @public
15+
export class ComboboxHarness extends ContentContainerComponentHarness {
16+
blur(): Promise<void>;
17+
close(): Promise<void>;
18+
focus(): Promise<void>;
19+
getPlaceholder(): Promise<string | null>;
20+
getPopupLoader(): Promise<HarnessLoader>;
21+
getPopupWidget<T extends ComponentHarness>(type: ComponentHarnessConstructor<T> & {
22+
with: (options?: {
23+
selector?: string;
24+
}) => HarnessPredicate<T>;
25+
}): Promise<T>;
26+
protected getRootHarnessLoader(): Promise<HarnessLoader>;
27+
getValue(): Promise<string>;
28+
// (undocumented)
29+
static hostSelector: string;
30+
isDisabled(): Promise<boolean>;
31+
isFocused(): Promise<boolean>;
32+
isOpen(): Promise<boolean>;
33+
open(): Promise<void>;
34+
setValue(value: string): Promise<void>;
35+
static with(options?: ComboboxHarnessFilters): HarnessPredicate<ComboboxHarness>;
36+
}
37+
38+
// @public
39+
export interface ComboboxHarnessFilters extends BaseHarnessFilters {
40+
disabled?: boolean;
41+
placeholder?: string | RegExp;
42+
value?: string | RegExp;
43+
}
44+
45+
// (No @packageDocumentation comment for this package)
46+
47+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ARIA_ENTRYPOINTS = [
1010
"menu",
1111
"menu/testing",
1212
"simple-combobox",
13+
"simple-combobox/testing",
1314
"tabs",
1415
"tabs/testing",
1516
"toolbar",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
"//src/aria/listbox",
30+
"//src/aria/listbox/testing",
31+
"//src/aria/simple-combobox",
32+
"//src/cdk/overlay",
33+
"//src/cdk/testing",
34+
"//src/cdk/testing/testbed",
35+
],
36+
)
37+
38+
ng_web_test_suite(
39+
name = "unit_tests",
40+
deps = [
41+
":unit_tests_lib",
42+
],
43+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
/** A set of criteria that can be used to filter a list of `ComboboxHarness` instances. */
12+
export interface ComboboxHarnessFilters extends BaseHarnessFilters {
13+
/** Only find instances whose placeholder matches the given value. */
14+
placeholder?: string | RegExp;
15+
/** Only find instances whose value matches the given value. */
16+
value?: string | RegExp;
17+
/** Only find instances with the given disabled state. */
18+
disabled?: boolean;
19+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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, signal} from '@angular/core';
10+
import {ComponentFixture, TestBed} from '@angular/core/testing';
11+
import {HarnessLoader} from '@angular/cdk/testing';
12+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
13+
import {Combobox, ComboboxPopup, ComboboxWidget} from '../index';
14+
import {Listbox, Option} from '../../listbox';
15+
import {ListboxHarness, ListboxOptionHarness} from '../../listbox/testing/listbox-harness';
16+
import {ComboboxHarness} from './combobox-harness';
17+
import {OverlayModule} from '@angular/cdk/overlay';
18+
19+
describe('ComboboxHarness', () => {
20+
let fixture: ComponentFixture<any>;
21+
let loader: HarnessLoader;
22+
23+
function setupTest(component: any) {
24+
fixture = TestBed.createComponent(component);
25+
fixture.detectChanges();
26+
loader = TestbedHarnessEnvironment.loader(fixture);
27+
}
28+
29+
describe('Basic usage', () => {
30+
beforeEach(() => setupTest(ComboboxTestApp));
31+
32+
it('should load combobox harness', async () => {
33+
await expectAsync(loader.getHarness(ComboboxHarness)).toBeResolved();
34+
});
35+
36+
it('should get and set values', async () => {
37+
const combobox = await loader.getHarness(ComboboxHarness);
38+
await combobox.setValue('California');
39+
fixture.detectChanges();
40+
41+
expect(await combobox.getValue()).toBe('California');
42+
});
43+
44+
it('should correctly report disabled state', async () => {
45+
const combobox = await loader.getHarness(ComboboxHarness);
46+
expect(await combobox.isDisabled()).toBeFalse();
47+
48+
fixture.componentInstance.disabled.set(true);
49+
fixture.detectChanges();
50+
51+
expect(await combobox.isDisabled()).toBeTrue();
52+
});
53+
54+
it('should open and close the popup', async () => {
55+
const combobox = await loader.getHarness(ComboboxHarness);
56+
expect(await combobox.isOpen()).toBeFalse();
57+
58+
await combobox.open();
59+
fixture.detectChanges();
60+
expect(await combobox.isOpen()).toBeTrue();
61+
62+
await combobox.close();
63+
fixture.detectChanges();
64+
expect(await combobox.isOpen()).toBeFalse();
65+
});
66+
67+
it('should allow loading nested harnesses within the popup content via unified container API', async () => {
68+
const combobox = await loader.getHarness(ComboboxHarness);
69+
await combobox.open();
70+
fixture.detectChanges();
71+
72+
// We access the main widget harness using getPopupWidget.
73+
const listbox = await combobox.getPopupWidget(ListboxHarness);
74+
const options = await listbox.getOptions();
75+
expect(options.length).toBe(3);
76+
});
77+
78+
it('should fail to resolve nested items when closed', async () => {
79+
const combobox = await loader.getHarness(ComboboxHarness);
80+
// Popup isn't open yet, so getPopupWidget should fail.
81+
await expectAsync(combobox.getPopupWidget(ListboxHarness)).toBeRejectedWithError(
82+
/Cannot retrieve popup content because the combobox is closed/,
83+
);
84+
});
85+
86+
it('should support getting explicit popup loader for descendant matching', async () => {
87+
const combobox = await loader.getHarness(ComboboxHarness);
88+
await combobox.open();
89+
fixture.detectChanges();
90+
91+
const popupLoader = await combobox.getPopupLoader();
92+
// We are testing that the loader works for finding actual children (Options).
93+
const option = await popupLoader.getHarness(ListboxOptionHarness);
94+
expect(option).toBeDefined();
95+
});
96+
97+
it('should support focusing and blurring', async () => {
98+
const combobox = await loader.getHarness(ComboboxHarness);
99+
await combobox.focus();
100+
expect(await combobox.isFocused()).toBeTrue();
101+
102+
await combobox.blur();
103+
expect(await combobox.isFocused()).toBeFalse();
104+
});
105+
});
106+
107+
describe('Overlay and Popover integrations', () => {
108+
it('should find and resolve harnesses nested inside standard CdkOverlay', async () => {
109+
setupTest(ComboboxOverlayTestApp);
110+
const combobox = await loader.getHarness(ComboboxHarness);
111+
112+
await combobox.open();
113+
fixture.detectChanges();
114+
115+
// Should find listbox inside the dynamically attached cdk overlay root container
116+
const listbox = await combobox.getPopupWidget(ListboxHarness);
117+
expect(listbox).toBeDefined();
118+
expect((await listbox.getOptions()).length).toBe(2);
119+
});
120+
121+
it('should resolve nested harnesses when using Native Popover API', async () => {
122+
setupTest(ComboboxNativePopoverTestApp);
123+
const combobox = await loader.getHarness(ComboboxHarness);
124+
125+
await combobox.open();
126+
fixture.detectChanges();
127+
128+
const listbox = await combobox.getPopupWidget(ListboxHarness);
129+
expect(listbox).toBeDefined();
130+
expect((await listbox.getOptions()).length).toBe(2);
131+
});
132+
});
133+
});
134+
135+
@Component({
136+
template: `
137+
<div>
138+
<input
139+
ngCombobox
140+
#combobox="ngCombobox"
141+
placeholder="Search states"
142+
[disabled]="disabled()"
143+
/>
144+
145+
<ng-template ngComboboxPopup [combobox]="combobox">
146+
<div ngComboboxWidget #listbox="ngListbox" ngListbox id="listbox-widget" focusMode="activedescendant" [activeDescendant]="listbox.activeDescendant()">
147+
<div ngOption value="CA" label="California">California</div>
148+
<div ngOption value="WA" label="Washington">Washington</div>
149+
<div ngOption value="OR" label="Oregon">Oregon</div>
150+
</div>
151+
</ng-template>
152+
</div>
153+
`,
154+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option],
155+
})
156+
class ComboboxTestApp {
157+
disabled = signal(false);
158+
}
159+
160+
@Component({
161+
template: `
162+
<div #origin>
163+
<input ngCombobox #combobox="ngCombobox" [(expanded)]="popupExpanded" />
164+
</div>
165+
166+
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="origin" [cdkConnectedOverlayOpen]="popupExpanded()">
167+
<ng-template ngComboboxPopup [combobox]="combobox">
168+
<div ngComboboxWidget ngListbox id="overlay-listbox">
169+
<div ngOption value="A">A</div>
170+
<div ngOption value="B">B</div>
171+
</div>
172+
</ng-template>
173+
</ng-template>
174+
`,
175+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule],
176+
})
177+
class ComboboxOverlayTestApp {
178+
popupExpanded = signal(false);
179+
}
180+
181+
@Component({
182+
template: `
183+
<div #origin>
184+
<input ngCombobox #combobox="ngCombobox" [(expanded)]="popupExpanded" />
185+
</div>
186+
187+
<ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayOpen]="popupExpanded()">
188+
<ng-template ngComboboxPopup [combobox]="combobox">
189+
<div ngComboboxWidget ngListbox id="popover-listbox">
190+
<div ngOption value="A">A</div>
191+
<div ngOption value="B">B</div>
192+
</div>
193+
</ng-template>
194+
</ng-template>
195+
`,
196+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule],
197+
})
198+
class ComboboxNativePopoverTestApp {
199+
popupExpanded = signal(false);
200+
}

0 commit comments

Comments
 (0)