Skip to content

Commit 75fae52

Browse files
authored
feat(aria/menu): introduce menu harness (angular#33067)
* feat(aria/menu): introduce menu harness * fixup! feat(aria/menu): introduce menu harness * fixup! feat(aria/menu): introduce menu harness
1 parent 1885d35 commit 75fae52

File tree

8 files changed

+408
-0
lines changed

8 files changed

+408
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
import { TestElement } from '@angular/cdk/testing';
11+
12+
// @public
13+
export class MenuHarness extends ComponentHarness {
14+
close(): Promise<void>;
15+
getItems(filters?: MenuItemHarnessFilters): Promise<MenuItemHarness[]>;
16+
_getTrigger(): Promise<TestElement | null>;
17+
// (undocumented)
18+
static hostSelector: string;
19+
isOpen(): Promise<boolean>;
20+
open(): Promise<void>;
21+
// (undocumented)
22+
static with(options?: MenuHarnessFilters): HarnessPredicate<MenuHarness>;
23+
}
24+
25+
// @public
26+
export interface MenuHarnessFilters extends BaseHarnessFilters {
27+
triggerText?: string | RegExp;
28+
}
29+
30+
// @public
31+
export class MenuItemHarness extends ComponentHarness {
32+
click(): Promise<void>;
33+
getSubmenu(): Promise<MenuHarness | null>;
34+
getText(): Promise<string>;
35+
// (undocumented)
36+
static hostSelector: string;
37+
isDisabled(): Promise<boolean>;
38+
isExpanded(): Promise<boolean>;
39+
// (undocumented)
40+
static with(options?: MenuItemHarnessFilters): HarnessPredicate<MenuItemHarness>;
41+
}
42+
43+
// @public
44+
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
45+
disabled?: boolean;
46+
expanded?: boolean;
47+
text?: string | RegExp;
48+
}
49+
50+
// (No @packageDocumentation comment for this package)
51+
52+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ARIA_ENTRYPOINTS = [
77
"listbox",
88
"listbox/testing",
99
"menu",
10+
"menu/testing",
1011
"tabs",
1112
"toolbar",
1213
"toolbar/testing",

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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 `MenuHarness`. */
12+
export interface MenuHarnessFilters extends BaseHarnessFilters {
13+
/** Only find instances whose trigger text matches the given value. */
14+
triggerText?: string | RegExp;
15+
}
16+
17+
/** Filters for locating a `MenuItemHarness`. */
18+
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
19+
/** Only find instances whose text matches the given value. */
20+
text?: string | RegExp;
21+
/** Only find instances whose disabled state matches the given value. */
22+
disabled?: boolean;
23+
/** Only find instances whose expanded state matches the given value. */
24+
expanded?: boolean;
25+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 {ComponentFixture, TestBed} from '@angular/core/testing';
11+
import {HarnessLoader} from '@angular/cdk/testing';
12+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
13+
import {Menu} from '../menu';
14+
import {MenuContent} from '../menu-content';
15+
import {MenuItem} from '../menu-item';
16+
import {MenuTrigger} from '../menu-trigger';
17+
import {MenuBar} from '../menu-bar';
18+
import {MenuItemHarness, MenuHarness} from './menu-harness';
19+
20+
describe('Aria Menu Harness', () => {
21+
let fixture: ComponentFixture<MenuTestApp>;
22+
let loader: HarnessLoader;
23+
24+
beforeEach(() => {
25+
fixture = TestBed.createComponent(MenuTestApp);
26+
fixture.detectChanges();
27+
loader = TestbedHarnessEnvironment.loader(fixture);
28+
});
29+
30+
it('should locate the menu harness', async () => {
31+
await expectAsync(
32+
loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'})),
33+
).toBeResolved();
34+
});
35+
36+
it('should verify that the menu is initially closed', async () => {
37+
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
38+
expect(await menu.isOpen()).toBe(false);
39+
});
40+
41+
it('should open the menu', async () => {
42+
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
43+
await menu.open();
44+
fixture.detectChanges();
45+
46+
expect(await menu.isOpen()).toBe(true);
47+
});
48+
49+
it('should close the menu', async () => {
50+
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
51+
await menu.open();
52+
fixture.detectChanges();
53+
54+
await menu.close();
55+
fixture.detectChanges();
56+
expect(await menu.isOpen()).toBe(false);
57+
});
58+
59+
it('should get all items inside an open menu', async () => {
60+
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
61+
await menu.open();
62+
fixture.detectChanges();
63+
64+
const items = await menu.getItems();
65+
expect(items.length).toBe(3);
66+
expect(await items[0].getText()).toBe('Item 1');
67+
});
68+
69+
it('should filter menu items by their disabled state', async () => {
70+
const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
71+
await menu.open();
72+
fixture.detectChanges();
73+
74+
const disabledItems = await loader.getAllHarnesses(MenuItemHarness.with({disabled: true}));
75+
expect(disabledItems.length).toBe(1);
76+
expect(await disabledItems[0].getText()).toBe('Item 2');
77+
});
78+
79+
it('should locate and interact with nested submenus', async () => {
80+
const main = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
81+
await main.open();
82+
fixture.detectChanges();
83+
84+
const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'}));
85+
await subItem.click();
86+
fixture.detectChanges();
87+
88+
const submenu = await subItem.getSubmenu();
89+
expect(await submenu!.isOpen()).toBe(true);
90+
});
91+
92+
it('should read items within a nested submenu', async () => {
93+
const main = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
94+
await main.open();
95+
fixture.detectChanges();
96+
97+
const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'}));
98+
await subItem.click();
99+
fixture.detectChanges();
100+
101+
const submenu = await subItem.getSubmenu();
102+
const subItems = await submenu!.getItems();
103+
expect(subItems.length).toBe(1);
104+
expect(await subItems[0].getText()).toBe('Nested Item');
105+
});
106+
107+
it('should confirm persistent horizontal menu bars are always open', async () => {
108+
const menubar = await loader.getHarness(MenuHarness.with({selector: '[ngMenuBar]'}));
109+
expect(await menubar.isOpen()).toBe(true);
110+
});
111+
112+
it('should read items from a persistent horizontal menu bar', async () => {
113+
const menubar = await loader.getHarness(MenuHarness.with({selector: '[ngMenuBar]'}));
114+
const items = await menubar.getItems();
115+
116+
expect(items.length).toBe(2);
117+
expect(await items[0].getText()).toBe('File');
118+
expect(await items[1].getText()).toBe('Edit');
119+
});
120+
});
121+
122+
@Component({
123+
template: `
124+
<button ngMenuTrigger [menu]="testMenu">Open Menu</button>
125+
126+
<div ngMenu #testMenu="ngMenu">
127+
<ng-template ngMenuContent>
128+
<div ngMenuItem value="Item 1">Item 1</div>
129+
<div ngMenuItem value="Item 2" [disabled]="true">Item 2</div>
130+
<div ngMenuItem value="Submenu" [submenu]="nestedMenu">Submenu</div>
131+
</ng-template>
132+
</div>
133+
134+
<div ngMenu #nestedMenu="ngMenu">
135+
<ng-template ngMenuContent>
136+
<div ngMenuItem value="Nested Item">Nested Item</div>
137+
</ng-template>
138+
</div>
139+
140+
<div ngMenuBar>
141+
<div ngMenuItem value="File">File</div>
142+
<div ngMenuItem value="Edit">Edit</div>
143+
</div>
144+
`,
145+
imports: [Menu, MenuItem, MenuTrigger, MenuBar, MenuContent],
146+
})
147+
class MenuTestApp {}

0 commit comments

Comments
 (0)