Skip to content

Commit 5cb2692

Browse files
committed
test(aria/listbox): generate additional tests for Listbox directives, harness, and patterns
1 parent c4039ae commit 5cb2692

5 files changed

Lines changed: 89 additions & 21 deletions

File tree

goldens/aria/listbox/testing/index.api.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,17 @@ import { BaseHarnessFilters } from '@angular/cdk/testing';
88
import { ComponentHarness } from '@angular/cdk/testing';
99
import { HarnessPredicate } from '@angular/cdk/testing';
1010

11-
// @public (undocumented)
11+
// @public
1212
export class ListboxHarness extends ComponentHarness {
1313
blur(): Promise<void>;
14-
// (undocumented)
1514
focus(): Promise<void>;
16-
// (undocumented)
15+
getActiveDescendantId(): Promise<string | null>;
1716
getOptions(filters?: ListboxOptionHarnessFilters): Promise<ListboxOptionHarness[]>;
18-
// (undocumented)
1917
getOrientation(): Promise<'vertical' | 'horizontal'>;
2018
// (undocumented)
2119
static hostSelector: string;
22-
// (undocumented)
2320
isDisabled(): Promise<boolean>;
24-
// (undocumented)
2521
isMulti(): Promise<boolean>;
26-
// (undocumented)
2722
static with(options?: ListboxHarnessFilters): HarnessPredicate<ListboxHarness>;
2823
}
2924

@@ -32,19 +27,14 @@ export interface ListboxHarnessFilters extends BaseHarnessFilters {
3227
disabled?: boolean;
3328
}
3429

35-
// @public (undocumented)
30+
// @public
3631
export class ListboxOptionHarness extends ComponentHarness {
37-
// (undocumented)
3832
click(): Promise<void>;
39-
// (undocumented)
4033
getText(): Promise<string>;
4134
// (undocumented)
4235
static hostSelector: string;
43-
// (undocumented)
4436
isDisabled(): Promise<boolean>;
45-
// (undocumented)
4637
isSelected(): Promise<boolean>;
47-
// (undocumented)
4838
static with(options?: ListboxOptionHarnessFilters): HarnessPredicate<ListboxOptionHarness>;
4939
}
5040

src/aria/listbox/listbox.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,23 @@ describe('Listbox', () => {
434434
],
435435
});
436436

437-
let orderedItems = listboxInstance._collection.orderedItems();
438-
expect(orderedItems.length).toBe(3);
439-
expect(orderedItems[0].element.textContent?.trim()).toBe('Item 1');
440-
expect(orderedItems[2].element.textContent?.trim()).toBe('Item 3');
437+
// Verify initial DOM order
438+
expect(optionElements.length).toBe(3);
439+
expect(optionElements[0].textContent?.trim()).toBe('Item 1');
440+
expect(optionElements[2].textContent?.trim()).toBe('Item 3');
441441

442442
const testComponent = fixture.componentInstance as ListboxExample;
443443
const items = testComponent.options().reverse();
444444
testComponent.options.set([...items]);
445445
fixture.detectChanges();
446446
await waitForMicrotasks();
447447

448-
orderedItems = listboxInstance._collection.orderedItems();
449-
expect(orderedItems.length).toBe(3);
450-
expect(orderedItems[0].element.textContent?.trim()).toBe('Item 3');
451-
expect(orderedItems[2].element.textContent?.trim()).toBe('Item 1');
448+
// Re-query elements to check new DOM order
449+
defineTestVariables(fixture);
450+
451+
expect(optionElements.length).toBe(3);
452+
expect(optionElements[0].textContent?.trim()).toBe('Item 3');
453+
expect(optionElements[2].textContent?.trim()).toBe('Item 1');
452454
});
453455
});
454456

src/aria/listbox/testing/listbox-harness.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ describe('Listbox Harness', () => {
8383
expect(orientation).toBe('horizontal');
8484
});
8585

86+
it('gets the active descendant ID', async () => {
87+
TestBed.resetTestingModule();
88+
TestBed.configureTestingModule({
89+
imports: [ListboxActiveDescendantTestComponent],
90+
});
91+
const customFixture = TestBed.createComponent(ListboxActiveDescendantTestComponent);
92+
customFixture.detectChanges();
93+
const customLoader = TestbedHarnessEnvironment.loader(customFixture);
94+
95+
const listbox = await customLoader.getHarness(ListboxHarness);
96+
const options = await listbox.getOptions();
97+
98+
await options[0].click();
99+
expect(await listbox.getActiveDescendantId()).toBe('apple-id');
100+
});
101+
86102
it('clicks an option inside the listbox', async () => {
87103
const option = await loader.getHarness(ListboxOptionHarness.with({text: 'Apple'}));
88104

@@ -91,3 +107,14 @@ describe('Listbox Harness', () => {
91107
expect(await option.isSelected()).toBeTrue();
92108
});
93109
});
110+
111+
@Component({
112+
imports: [Listbox, Option],
113+
template: `
114+
<ul ngListbox focusMode="activedescendant">
115+
<li ngOption [value]="1" id="apple-id">Apple</li>
116+
<li ngOption [value]="2" id="banana-id">Banana</li>
117+
</ul>
118+
`,
119+
})
120+
class ListboxActiveDescendantTestComponent {}

src/aria/listbox/testing/listbox-harness.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@
99
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
1010
import {ListboxHarnessFilters, ListboxOptionHarnessFilters} from './listbox-harness-filters';
1111

12+
/** Harness for interacting with a standard ngOption in tests. */
1213
export class ListboxOptionHarness extends ComponentHarness {
1314
static hostSelector = '[ngOption]';
1415

16+
/**
17+
* Gets a `HarnessPredicate` that can be used to search for an option
18+
* with specific attributes.
19+
* @param options Options for filtering which option instances are considered a match.
20+
* @return a `HarnessPredicate` configured with the given options.
21+
*/
1522
static with(options: ListboxOptionHarnessFilters = {}): HarnessPredicate<ListboxOptionHarness> {
1623
return new HarnessPredicate(ListboxOptionHarness, options)
1724
.addOption('text', options.text, (harness, text) =>
@@ -29,11 +36,13 @@ export class ListboxOptionHarness extends ComponentHarness {
2936
);
3037
}
3138

39+
/** Whether the option is selected. */
3240
async isSelected(): Promise<boolean> {
3341
const host = await this.host();
3442
return (await host.getAttribute('aria-selected')) === 'true';
3543
}
3644

45+
/** Whether the option is disabled. */
3746
async isDisabled(): Promise<boolean> {
3847
const host = await this.host();
3948
return (
@@ -42,20 +51,29 @@ export class ListboxOptionHarness extends ComponentHarness {
4251
);
4352
}
4453

54+
/** Gets the option's text. */
4555
async getText(): Promise<string> {
4656
const host = await this.host();
4757
return host.text();
4858
}
4959

60+
/** Clicks the option to toggle its selected state. */
5061
async click(): Promise<void> {
5162
const host = await this.host();
5263
return host.click();
5364
}
5465
}
5566

67+
/** Harness for interacting with a standard ngListbox in tests. */
5668
export class ListboxHarness extends ComponentHarness {
5769
static hostSelector = '[ngListbox]';
5870

71+
/**
72+
* Gets a `HarnessPredicate` that can be used to search for a listbox
73+
* with specific attributes.
74+
* @param options Options for filtering which listbox instances are considered a match.
75+
* @return a `HarnessPredicate` configured with the given options.
76+
*/
5977
static with(options: ListboxHarnessFilters = {}): HarnessPredicate<ListboxHarness> {
6078
return new HarnessPredicate(ListboxHarness, options).addOption(
6179
'disabled',
@@ -64,26 +82,31 @@ export class ListboxHarness extends ComponentHarness {
6482
);
6583
}
6684

85+
/** Gets the orientation of the listbox. */
6786
async getOrientation(): Promise<'vertical' | 'horizontal'> {
6887
const host = await this.host();
6988
const orientation = await host.getAttribute('aria-orientation');
7089
return orientation === 'horizontal' ? 'horizontal' : 'vertical';
7190
}
7291

92+
/** Whether the listbox is multiselectable. */
7393
async isMulti(): Promise<boolean> {
7494
const host = await this.host();
7595
return (await host.getAttribute('aria-multiselectable')) === 'true';
7696
}
7797

98+
/** Whether the listbox is disabled. */
7899
async isDisabled(): Promise<boolean> {
79100
const host = await this.host();
80101
return (await host.getAttribute('aria-disabled')) === 'true';
81102
}
82103

104+
/** Gets the options inside the listbox. */
83105
async getOptions(filters: ListboxOptionHarnessFilters = {}): Promise<ListboxOptionHarness[]> {
84106
return this.locatorForAll(ListboxOptionHarness.with(filters))();
85107
}
86108

109+
/** Focuses the listbox container. */
87110
async focus(): Promise<void> {
88111
await (await this.host()).focus();
89112
}
@@ -92,4 +115,10 @@ export class ListboxHarness extends ComponentHarness {
92115
async blur(): Promise<void> {
93116
await (await this.host()).blur();
94117
}
118+
119+
/** Gets the ID of the active option. */
120+
async getActiveDescendantId(): Promise<string | null> {
121+
const host = await this.host();
122+
return host.getAttribute('aria-activedescendant');
123+
}
95124
}

src/aria/private/listbox/listbox.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ describe('Listbox Pattern', () => {
9090
);
9191
}
9292

93+
describe('Tabindex', () => {
94+
it('should expose tabIndex signals on listbox and options', () => {
95+
const {listbox, options} = getDefaultPatterns();
96+
97+
expect(listbox.tabIndex()).toBe(-1);
98+
expect(options[0].tabIndex()).toBe(0);
99+
expect(options[1].tabIndex()).toBe(-1);
100+
101+
listbox.onKeydown(down());
102+
103+
expect(options[0].tabIndex()).toBe(-1);
104+
expect(options[1].tabIndex()).toBe(0);
105+
});
106+
107+
it('should set tabindex 0 for listbox in activedescendant mode', () => {
108+
const {listbox} = getDefaultPatterns({focusMode: signal('activedescendant')});
109+
expect(listbox.tabIndex()).toBe(0);
110+
});
111+
});
112+
93113
describe('Keyboard Navigation', () => {
94114
it('should navigate next on ArrowDown', () => {
95115
const {listbox} = getDefaultPatterns();

0 commit comments

Comments
 (0)