Skip to content
2 changes: 2 additions & 0 deletions goldens/aria/grid/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export class GridCellHarness extends ContentContainerComponentHarness {
getText(): Promise<string>;
// (undocumented)
static hostSelector: string;
isActive(): Promise<boolean>;
isDisabled(): Promise<boolean>;
isFocused(): Promise<boolean>;
isSelected(): Promise<boolean>;
static with(options?: GridCellHarnessFilters): HarnessPredicate<GridCellHarness>;
}
Expand Down
16 changes: 3 additions & 13 deletions goldens/aria/listbox/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,17 @@ import { BaseHarnessFilters } from '@angular/cdk/testing';
import { ComponentHarness } from '@angular/cdk/testing';
import { HarnessPredicate } from '@angular/cdk/testing';

// @public (undocumented)
// @public
export class ListboxHarness extends ComponentHarness {
blur(): Promise<void>;
// (undocumented)
focus(): Promise<void>;
// (undocumented)
getActiveDescendantId(): Promise<string | null>;
getOptions(filters?: ListboxOptionHarnessFilters): Promise<ListboxOptionHarness[]>;
// (undocumented)
getOrientation(): Promise<'vertical' | 'horizontal'>;
// (undocumented)
static hostSelector: string;
// (undocumented)
isDisabled(): Promise<boolean>;
// (undocumented)
isMulti(): Promise<boolean>;
// (undocumented)
static with(options?: ListboxHarnessFilters): HarnessPredicate<ListboxHarness>;
}

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

// @public (undocumented)
// @public
export class ListboxOptionHarness extends ComponentHarness {
// (undocumented)
click(): Promise<void>;
// (undocumented)
getText(): Promise<string>;
// (undocumented)
static hostSelector: string;
// (undocumented)
isDisabled(): Promise<boolean>;
// (undocumented)
isSelected(): Promise<boolean>;
// (undocumented)
static with(options?: ListboxOptionHarnessFilters): HarnessPredicate<ListboxOptionHarness>;
}

Expand Down
3 changes: 3 additions & 0 deletions goldens/aria/menu/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class MenuHarness extends ComponentHarness {
_getTrigger(): Promise<TestElement | null>;
// (undocumented)
static hostSelector: string;
isMenuBar(): Promise<boolean>;
isOpen(): Promise<boolean>;
open(): Promise<void>;
// (undocumented)
Expand All @@ -32,10 +33,12 @@ export class MenuItemHarness extends ComponentHarness {
click(): Promise<void>;
getSubmenu(): Promise<MenuHarness | null>;
getText(): Promise<string>;
hasSubmenu(): Promise<boolean>;
// (undocumented)
static hostSelector: string;
isDisabled(): Promise<boolean>;
isExpanded(): Promise<boolean>;
isFocused(): Promise<boolean>;
// (undocumented)
static with(options?: MenuItemHarnessFilters): HarnessPredicate<MenuItemHarness>;
}
Expand Down
2 changes: 2 additions & 0 deletions goldens/aria/toolbar/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ export class ToolbarWidgetHarness extends ContentContainerComponentHarness<strin
static hostSelector: string;
isActive(): Promise<boolean>;
isDisabled(): Promise<boolean>;
isSelected(): Promise<boolean>;
static with(options?: ToolbarWidgetHarnessFilters): HarnessPredicate<ToolbarWidgetHarness>;
}

// @public
export interface ToolbarWidgetHarnessFilters extends BaseHarnessFilters {
active?: boolean;
selected?: boolean;
text?: string | RegExp;
}

Expand Down
22 changes: 11 additions & 11 deletions src/aria/accordion/accordion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,23 +293,23 @@ describe('AccordionGroup', () => {
});

it('should update collection order when items are shuffled', async () => {
const groupDebugElement = fixture.debugElement.query(By.directive(AccordionGroup));
const groupDirective = groupDebugElement.injector.get(AccordionGroup);

let orderedItems = groupDirective._collection.orderedItems();
expect(orderedItems.length).toBe(3);
expect(orderedItems[0].element.textContent?.trim()).toBe('Item 1 Header');
expect(orderedItems[2].element.textContent?.trim()).toBe('Item 3 Header');
// Verify initial DOM order
expect(triggerElements.length).toBe(3);
expect(triggerElements[0].textContent?.trim()).toBe('Item 1 Header');
expect(triggerElements[2].textContent?.trim()).toBe('Item 3 Header');

// Shuffle (reverse) data
const items = testComponent.items().reverse();
testComponent.items.set([...items]);
fixture.detectChanges();
await waitForMicrotasks();

orderedItems = groupDirective._collection.orderedItems();
expect(orderedItems.length).toBe(3);
expect(orderedItems[0].element.textContent?.trim()).toBe('Item 3 Header');
expect(orderedItems[2].element.textContent?.trim()).toBe('Item 1 Header');
// Re-query elements to check new DOM order
setupTriggerAndPanels();

expect(triggerElements.length).toBe(3);
expect(triggerElements[0].textContent?.trim()).toBe('Item 3 Header');
expect(triggerElements[2].textContent?.trim()).toBe('Item 1 Header');
});

describe('wrap behavior', () => {
Expand Down
13 changes: 13 additions & 0 deletions src/aria/grid/testing/grid-harness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ describe('Grid Harness', () => {
const rows = await loader.getAllHarnesses(GridRowHarness);
expect(await rows[0].getCellTextByIndex()).toEqual(['Cell 1.1', 'Cell 1.2']);
});

it('reports the active state of a cell', async () => {
const cell = await loader.getHarness(GridCellHarness.with({text: 'Cell 1.1'}));
expect(await cell.isActive()).toBeTrue();
});

it('reports the focused state of a cell', async () => {
const cell = await loader.getHarness(GridCellHarness.with({text: 'Cell 1.1'}));
expect(await cell.isFocused()).toBeFalse();

await cell.focus();
expect(await cell.isFocused()).toBeTrue();
});
});

@Component({
Expand Down
12 changes: 12 additions & 0 deletions src/aria/grid/testing/grid-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ export class GridCellHarness extends ContentContainerComponentHarness {
const host = await this.host();
return host.blur();
}

/** Whether the cell is active. */
async isActive(): Promise<boolean> {
const host = await this.host();
return (await host.getAttribute('data-active')) === 'true';
}

/** Whether the cell is focused. */
async isFocused(): Promise<boolean> {
const host = await this.host();
return host.isFocused();
}
}

/** Harness for interacting with a standard ngGridRow in tests. */
Expand Down
18 changes: 10 additions & 8 deletions src/aria/listbox/listbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,21 +434,23 @@ describe('Listbox', () => {
],
});

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

const testComponent = fixture.componentInstance as ListboxExample;
const items = testComponent.options().reverse();
testComponent.options.set([...items]);
fixture.detectChanges();
await waitForMicrotasks();

orderedItems = listboxInstance._collection.orderedItems();
expect(orderedItems.length).toBe(3);
expect(orderedItems[0].element.textContent?.trim()).toBe('Item 3');
expect(orderedItems[2].element.textContent?.trim()).toBe('Item 1');
// Re-query elements to check new DOM order
defineTestVariables(fixture);

expect(optionElements.length).toBe(3);
expect(optionElements[0].textContent?.trim()).toBe('Item 3');
expect(optionElements[2].textContent?.trim()).toBe('Item 1');
});
});

Expand Down
27 changes: 27 additions & 0 deletions src/aria/listbox/testing/listbox-harness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ describe('Listbox Harness', () => {
expect(orientation).toBe('horizontal');
});

it('gets the active descendant ID', async () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [ListboxActiveDescendantTestComponent],
});
const customFixture = TestBed.createComponent(ListboxActiveDescendantTestComponent);
customFixture.detectChanges();
const customLoader = TestbedHarnessEnvironment.loader(customFixture);

const listbox = await customLoader.getHarness(ListboxHarness);
const options = await listbox.getOptions();

await options[0].click();
expect(await listbox.getActiveDescendantId()).toBe('apple-id');
});

it('clicks an option inside the listbox', async () => {
const option = await loader.getHarness(ListboxOptionHarness.with({text: 'Apple'}));

Expand All @@ -91,3 +107,14 @@ describe('Listbox Harness', () => {
expect(await option.isSelected()).toBeTrue();
});
});

@Component({
imports: [Listbox, Option],
template: `
<ul ngListbox focusMode="activedescendant">
<li ngOption [value]="1" id="apple-id">Apple</li>
<li ngOption [value]="2" id="banana-id">Banana</li>
</ul>
`,
})
class ListboxActiveDescendantTestComponent {}
29 changes: 29 additions & 0 deletions src/aria/listbox/testing/listbox-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
import {ListboxHarnessFilters, ListboxOptionHarnessFilters} from './listbox-harness-filters';

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

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

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

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

/** Gets the option's text. */
async getText(): Promise<string> {
const host = await this.host();
return host.text();
}

/** Clicks the option to toggle its selected state. */
async click(): Promise<void> {
const host = await this.host();
return host.click();
}
}

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

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

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

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

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

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

/** Focuses the listbox container. */
async focus(): Promise<void> {
await (await this.host()).focus();
}
Expand All @@ -92,4 +115,10 @@ export class ListboxHarness extends ComponentHarness {
async blur(): Promise<void> {
await (await this.host()).blur();
}

/** Gets the ID of the active option. */
async getActiveDescendantId(): Promise<string | null> {
const host = await this.host();
return host.getAttribute('aria-activedescendant');
}
}
30 changes: 30 additions & 0 deletions src/aria/menu/testing/menu-harness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,36 @@ describe('Aria Menu Harness', () => {
expect(await items[0].getText()).toBe('File');
expect(await items[1].getText()).toBe('Edit');
});

it('should be able to get whether a menu item is focused', async () => {
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
await menu.open();
const items = await menu.getItems();

const itemHost = await items[0].host();
await itemHost.focus();
fixture.detectChanges();

expect(await items[0].isFocused()).toBe(true);
expect(await items[1].isFocused()).toBe(false);
});

it('should be able to get whether a menu item has a submenu', async () => {
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
await menu.open();
const items = await menu.getItems();

expect(await items[0].hasSubmenu()).toBe(false);
expect(await items[2].hasSubmenu()).toBe(true);
});

it('should be able to get whether a menu is a menu bar', async () => {
const menubar = await loader.getHarness(MenuHarness.with({selector: '[ngMenuBar]'}));
expect(await menubar.isMenuBar()).toBe(true);

const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
expect(await menu.isMenuBar()).toBe(false);
});
});

@Component({
Expand Down
Loading
Loading