Skip to content

Commit 041446b

Browse files
committed
test(aria/listbox): generate additional tests for Listbox directives, harness, and patterns
1 parent 3d599e7 commit 041446b

7 files changed

Lines changed: 76 additions & 8 deletions

File tree

goldens/aria/listbox/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Option_2<V> implements OnInit, OnDestroy {
4646
readonly active: _angular_core.Signal<boolean>;
4747
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
4848
readonly element: HTMLElement;
49+
readonly hardDisabled: _angular_core.Signal<boolean>;
4950
readonly id: _angular_core.InputSignal<string>;
5051
readonly label: _angular_core.InputSignal<string | undefined>;
5152
// (undocumented)

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: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,30 @@ 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('Inert attribute', () => {
458+
it('should set inert attribute on hard-disabled options', () => {
459+
setupListbox({disabledOptions: [0], softDisabled: false});
460+
expect(optionElements[0].hasAttribute('inert')).toBe(true);
452461
});
453462
});
454463

src/aria/listbox/option.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {LISTBOX} from './tokens';
5050
'[attr.tabindex]': '_pattern.tabIndex()',
5151
'[attr.aria-selected]': '_pattern.selected()',
5252
'[attr.aria-disabled]': '_pattern.disabled()',
53+
'[attr.inert]': 'hardDisabled() ? true : null',
5354
},
5455
})
5556
export class Option<V> implements OnInit, OnDestroy {
@@ -74,6 +75,9 @@ export class Option<V> implements OnInit, OnDestroy {
7475
/** Whether an item is disabled. */
7576
readonly disabled = input(false, {transform: booleanAttribute});
7677

78+
/** Whether the option is 'hard' disabled, which is different from `aria-disabled`. A hard disabled option cannot receive focus. */
79+
readonly hardDisabled = computed(() => this._pattern.disabled() && !this._listbox.softDisabled());
80+
7781
/** The text used by the typeahead search. */
7882
readonly label = input<string>();
7983

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)