Skip to content

Commit e337ce2

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

5 files changed

Lines changed: 115 additions & 8 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class ListboxHarness extends ComponentHarness {
1313
blur(): Promise<void>;
1414
// (undocumented)
1515
focus(): Promise<void>;
16+
getActiveDescendantId(): Promise<string | null>;
1617
// (undocumented)
1718
getOptions(filters?: ListboxOptionHarnessFilters): Promise<ListboxOptionHarness[]>;
1819
// (undocumented)

src/aria/listbox/listbox.spec.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,62 @@ 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');
454+
});
455+
});
456+
457+
describe('Custom IDs', () => {
458+
let customIdFixture: ComponentFixture<ListboxCustomIdExample>;
459+
460+
beforeEach(() => {
461+
TestBed.resetTestingModule();
462+
TestBed.configureTestingModule({
463+
imports: [ListboxCustomIdExample],
464+
});
465+
customIdFixture = TestBed.createComponent(ListboxCustomIdExample);
466+
document.body.appendChild(customIdFixture.nativeElement);
467+
fixture = customIdFixture as any;
468+
customIdFixture.detectChanges();
469+
});
470+
471+
afterEach(() => {
472+
customIdFixture.nativeElement.remove();
473+
});
474+
475+
it('should use custom ID for option when provided', async () => {
476+
const optionEl = customIdFixture.nativeElement.querySelector('#custom-option-id');
477+
expect(optionEl).toBeTruthy();
478+
});
479+
480+
it('should generate ID if not provided', async () => {
481+
const options = customIdFixture.debugElement
482+
.queryAll(By.directive(Option))
483+
.map((debugEl: DebugElement) => debugEl.nativeElement as HTMLElement);
484+
485+
expect(options[1].id).toContain('ng-option-');
486+
});
487+
});
488+
489+
describe('Inert attribute', () => {
490+
it('should set inert attribute on hard-disabled options', () => {
491+
setupListbox({disabledOptions: [0], softDisabled: false});
492+
expect(optionElements[0].hasAttribute('inert')).toBe(true);
452493
});
453494
});
454495

@@ -875,3 +916,15 @@ class ListboxExample {
875916
changeDetection: ChangeDetectionStrategy.Eager,
876917
})
877918
class DefaultListboxExample {}
919+
920+
@Component({
921+
template: `
922+
<ul ngListbox>
923+
<li ngOption value="1" id="custom-option-id">Item 1</li>
924+
<li ngOption value="2">Item 2</li>
925+
</ul>
926+
`,
927+
imports: [Listbox, Option],
928+
changeDetection: ChangeDetectionStrategy.Eager,
929+
})
930+
class ListboxCustomIdExample {}

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,10 @@ export class ListboxHarness extends ComponentHarness {
9292
async blur(): Promise<void> {
9393
await (await this.host()).blur();
9494
}
95+
96+
/** Gets the ID of the active option. */
97+
async getActiveDescendantId(): Promise<string | null> {
98+
const host = await this.host();
99+
return host.getAttribute('aria-activedescendant');
100+
}
95101
}

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)