Skip to content

Commit 8f6c6bd

Browse files
committed
feat(aria/menu): introduce menu harness
1 parent 30f2239 commit 8f6c6bd

File tree

8 files changed

+364
-0
lines changed

8 files changed

+364
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
## API Report File for "@angular/aria_menu_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 { HarnessPredicate } from '@angular/cdk/testing';
10+
11+
// @public
12+
export class MenuHarness extends ComponentHarness {
13+
// (undocumented)
14+
getItems(filters?: MenuItemHarnessFilters): Promise<MenuItemHarness[]>;
15+
// (undocumented)
16+
static hostSelector: string;
17+
// (undocumented)
18+
static with(options?: MenuHarnessFilters): HarnessPredicate<MenuHarness>;
19+
}
20+
21+
// @public
22+
export interface MenuHarnessFilters extends BaseHarnessFilters {
23+
}
24+
25+
// @public
26+
export class MenuItemHarness extends ComponentHarness {
27+
// (undocumented)
28+
click(): Promise<void>;
29+
// (undocumented)
30+
getSubmenu(): Promise<MenuHarness | null>;
31+
// (undocumented)
32+
getText(): Promise<string>;
33+
// (undocumented)
34+
hasSubmenu(): Promise<boolean>;
35+
// (undocumented)
36+
static hostSelector: string;
37+
// (undocumented)
38+
isDisabled(): Promise<boolean>;
39+
// (undocumented)
40+
isExpanded(): Promise<boolean>;
41+
// (undocumented)
42+
static with(options?: MenuItemHarnessFilters): HarnessPredicate<MenuItemHarness>;
43+
}
44+
45+
// @public
46+
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
47+
disabled?: boolean;
48+
expanded?: boolean;
49+
text?: string | RegExp;
50+
}
51+
52+
// @public
53+
export class MenuTriggerHarness extends ComponentHarness {
54+
// (undocumented)
55+
click(): Promise<void>;
56+
// (undocumented)
57+
getMenu(filters?: MenuHarnessFilters): Promise<MenuHarness>;
58+
// (undocumented)
59+
getText(): Promise<string>;
60+
// (undocumented)
61+
static hostSelector: string;
62+
// (undocumented)
63+
static with(options?: MenuTriggerHarnessFilters): HarnessPredicate<MenuTriggerHarness>;
64+
}
65+
66+
// @public
67+
export interface MenuTriggerHarnessFilters extends BaseHarnessFilters {
68+
text?: string | RegExp;
69+
}
70+
71+
// (No @packageDocumentation comment for this package)
72+
73+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ARIA_ENTRYPOINTS = [
55
"grid",
66
"listbox",
77
"menu",
8+
"menu/testing",
89
"tabs",
910
"toolbar",
1011
"tree",

src/aria/menu/testing/BUILD.bazel

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
"//src/cdk/testing",
13+
],
14+
)
15+
16+
filegroup(
17+
name = "source-files",
18+
srcs = glob(["**/*.ts"]),
19+
)
20+
21+
ng_project(
22+
name = "unit_tests_lib",
23+
testonly = True,
24+
srcs = glob(["**/*.spec.ts"]),
25+
deps = [
26+
":testing",
27+
"//:node_modules/@angular/core",
28+
"//:node_modules/@angular/platform-browser",
29+
"//src/aria/menu",
30+
"//src/cdk/testing",
31+
"//src/cdk/testing/testbed",
32+
],
33+
)
34+
35+
ng_web_test_suite(
36+
name = "unit_tests",
37+
deps = [
38+
":unit_tests_lib",
39+
],
40+
)

src/aria/menu/testing/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
export * from './public-api';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/** Filters for locating a `MenuTriggerHarness`. */
12+
export interface MenuTriggerHarnessFilters extends BaseHarnessFilters {
13+
/** Only find instances whose text matches the given value. */
14+
text?: string | RegExp;
15+
}
16+
17+
/** Filters for locating a `MenuHarness`. */
18+
export interface MenuHarnessFilters extends BaseHarnessFilters {}
19+
20+
/** Filters for locating a `MenuItemHarness`. */
21+
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
22+
/** Only find instances whose text matches the given value. */
23+
text?: string | RegExp;
24+
/** Only find instances whose disabled state matches the given value. */
25+
disabled?: boolean;
26+
/** Only find instances whose expanded state matches the given value. */
27+
expanded?: boolean;
28+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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} from '@angular/core';
10+
import {TestBed} from '@angular/core/testing';
11+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
12+
import {Menu} from '../menu';
13+
import {MenuItem} from '../menu-item';
14+
import {MenuTrigger} from '../menu-trigger';
15+
import {MenuItemHarness, MenuTriggerHarness} from './menu-harness';
16+
17+
describe('Aria Menu Harness', () => {
18+
let fixture: any;
19+
let loader: any;
20+
21+
beforeEach(() => {
22+
TestBed.configureTestingModule({
23+
imports: [Menu, MenuItem, MenuTrigger, MenuTestApp],
24+
});
25+
26+
fixture = TestBed.createComponent(MenuTestApp);
27+
fixture.detectChanges();
28+
loader = TestbedHarnessEnvironment.loader(fixture);
29+
});
30+
31+
it('should locate the menu trigger harness', async () => {
32+
const trigger = await loader.getHarness(MenuTriggerHarness.with({text: 'Open Menu'}));
33+
expect(trigger).toBeTruthy();
34+
expect(await trigger.getText()).toBe('Open Menu');
35+
});
36+
37+
it('should open the menu and locate items', async () => {
38+
const trigger = await loader.getHarness(MenuTriggerHarness);
39+
await trigger.click();
40+
fixture.detectChanges();
41+
42+
const menu = await trigger.getMenu();
43+
expect(menu).toBeTruthy();
44+
45+
const items = await menu.getItems();
46+
expect(items.length).toBe(4);
47+
expect(await items[0].getText()).toBe('Item 1');
48+
expect(await items[1].getText()).toBe('Item 2');
49+
expect(await items[2].getText()).toBe('Submenu');
50+
expect(await items[3].getText()).toBe('Nested Item');
51+
});
52+
53+
it('should filter menu items by state', async () => {
54+
const trigger = await loader.getHarness(MenuTriggerHarness);
55+
await trigger.click();
56+
fixture.detectChanges();
57+
58+
const disabledItems = await loader.getAllHarnesses(MenuItemHarness.with({disabled: true}));
59+
expect(disabledItems.length).toBe(1);
60+
expect(await disabledItems[0].getText()).toBe('Item 2');
61+
});
62+
63+
it('should locate and open a nested submenu', async () => {
64+
const mainTrigger = await loader.getHarness(MenuTriggerHarness.with({text: 'Open Menu'}));
65+
await mainTrigger.click();
66+
fixture.detectChanges();
67+
68+
const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'}));
69+
expect(await subItem.hasSubmenu()).toBe(true);
70+
await subItem.click();
71+
fixture.detectChanges();
72+
73+
const submenu = await subItem.getSubmenu();
74+
expect(submenu).toBeTruthy();
75+
const subItems = await submenu!.getItems();
76+
expect(subItems.length).toBe(1);
77+
expect(await subItems[0].getText()).toBe('Nested Item');
78+
});
79+
});
80+
81+
@Component({
82+
template: `
83+
<button ngMenuTrigger [menu]="testMenu">Open Menu</button>
84+
85+
<div ngMenu #testMenu="ngMenu">
86+
<div ngMenuItem value="Item 1">Item 1</div>
87+
<div ngMenuItem value="Item 2" [disabled]="true">Item 2</div>
88+
<div ngMenuItem value="Submenu" [submenu]="nestedMenu">Submenu</div>
89+
90+
<div ngMenu #nestedMenu="ngMenu">
91+
<div ngMenuItem value="Nested Item">Nested Item</div>
92+
</div>
93+
</div>
94+
`,
95+
imports: [Menu, MenuItem, MenuTrigger],
96+
})
97+
class MenuTestApp {}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {
11+
MenuHarnessFilters,
12+
MenuItemHarnessFilters,
13+
MenuTriggerHarnessFilters,
14+
} from './menu-harness-filters';
15+
16+
/** Harness for interacting with a standard ngMenuItem in tests. */
17+
export class MenuItemHarness extends ComponentHarness {
18+
static hostSelector = '[ngMenuItem]';
19+
20+
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MenuItemHarness> {
21+
return new HarnessPredicate(MenuItemHarness, options)
22+
.addOption('text', options.text, (harness, text) =>
23+
HarnessPredicate.stringMatches(harness.getText(), text),
24+
)
25+
.addOption(
26+
'disabled',
27+
options.disabled,
28+
async (harness, disabled) => (await harness.isDisabled()) === disabled,
29+
)
30+
.addOption(
31+
'expanded',
32+
options.expanded,
33+
async (harness, expanded) => (await harness.isExpanded()) === expanded,
34+
);
35+
}
36+
37+
async getText(): Promise<string> {
38+
return (await this.host()).text();
39+
}
40+
41+
async isDisabled(): Promise<boolean> {
42+
const host = await this.host();
43+
return (await host.getAttribute('aria-disabled')) === 'true';
44+
}
45+
46+
async isExpanded(): Promise<boolean> {
47+
const host = await this.host();
48+
return (await host.getAttribute('aria-expanded')) === 'true';
49+
}
50+
51+
async click(): Promise<void> {
52+
return (await this.host()).click();
53+
}
54+
55+
async hasSubmenu(): Promise<boolean> {
56+
return (await (await this.host()).getAttribute('aria-haspopup')) !== null;
57+
}
58+
59+
async getSubmenu(): Promise<MenuHarness | null> {
60+
if (await this.hasSubmenu()) {
61+
const controlsId = await (await this.host()).getAttribute('aria-controls');
62+
return this.documentRootLocatorFactory().locatorFor(
63+
MenuHarness.with({selector: controlsId ? `#${controlsId}` : undefined}),
64+
)();
65+
}
66+
return null;
67+
}
68+
}
69+
70+
/** Harness for interacting with a standard ngMenu in tests. */
71+
export class MenuHarness extends ComponentHarness {
72+
static hostSelector = '[ngMenu]';
73+
74+
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MenuHarness> {
75+
return new HarnessPredicate(MenuHarness, options);
76+
}
77+
78+
async getItems(filters: MenuItemHarnessFilters = {}): Promise<MenuItemHarness[]> {
79+
return this.locatorForAll(MenuItemHarness.with(filters))();
80+
}
81+
}
82+
83+
/** Harness for interacting with a standard ngMenuTrigger in tests. */
84+
export class MenuTriggerHarness extends ComponentHarness {
85+
static hostSelector = '[ngMenuTrigger]';
86+
87+
static with(options: MenuTriggerHarnessFilters = {}): HarnessPredicate<MenuTriggerHarness> {
88+
return new HarnessPredicate(MenuTriggerHarness, options).addOption(
89+
'text',
90+
options.text,
91+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text),
92+
);
93+
}
94+
95+
async getText(): Promise<string> {
96+
return (await this.host()).text();
97+
}
98+
99+
async click(): Promise<void> {
100+
return (await this.host()).click();
101+
}
102+
103+
async getMenu(filters: MenuHarnessFilters = {}): Promise<MenuHarness> {
104+
return this.documentRootLocatorFactory().locatorFor(MenuHarness.with(filters))();
105+
}
106+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
export * from './menu-harness';
10+
export * from './menu-harness-filters';

0 commit comments

Comments
 (0)