Skip to content

Commit 0c99cf2

Browse files
committed
feat(aria/accordion): introduce accordion harness
1 parent 046e1a2 commit 0c99cf2

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed

src/aria/accordion/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ng_project(
1313
"//src/aria/private",
1414
"//src/cdk/a11y",
1515
"//src/cdk/bidi",
16+
"//src/cdk/testing",
1617
],
1718
)
1819

@@ -21,12 +22,15 @@ ng_project(
2122
testonly = True,
2223
srcs = [
2324
"accordion.spec.ts",
25+
"testing/accordion-harness.spec.ts",
2426
],
2527
deps = [
2628
":accordion",
2729
"//:node_modules/@angular/core",
2830
"//:node_modules/@angular/platform-browser",
31+
"//src/cdk/testing",
2932
"//src/cdk/testing/private",
33+
"//src/cdk/testing/testbed",
3034
],
3135
)
3236

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 {
13+
AccordionTriggerHarness,
14+
AccordionPanelHarness,
15+
AccordionGroupHarness,
16+
} from './accordion-harness';
17+
import {AccordionGroup, AccordionPanel, AccordionTrigger} from '../index';
18+
19+
describe('Accordion Harnesses', () => {
20+
let fixture: any;
21+
let loader: any;
22+
23+
@Component({
24+
imports: [AccordionGroup, AccordionPanel, AccordionTrigger],
25+
template: `
26+
<div ngAccordionGroup>
27+
<div #panel1="ngAccordionPanel" ngAccordionPanel>Content 1</div>
28+
<button ngAccordionTrigger [panel]="panel1">Section 1</button>
29+
30+
<div #panel2="ngAccordionPanel" ngAccordionPanel>Content 2</div>
31+
<button ngAccordionTrigger [panel]="panel2" disabled>Section 2</button>
32+
</div>
33+
`,
34+
})
35+
class AccordionHarnessTestComponent {}
36+
37+
beforeEach(() => {
38+
TestBed.configureTestingModule({
39+
imports: [AccordionHarnessTestComponent],
40+
});
41+
fixture = TestBed.createComponent(AccordionHarnessTestComponent);
42+
fixture.detectChanges();
43+
loader = TestbedHarnessEnvironment.loader(fixture);
44+
});
45+
46+
it('should find all accordion triggers', async () => {
47+
const triggers = await loader.getAllHarnesses(AccordionTriggerHarness);
48+
expect(triggers.length).toBe(2);
49+
});
50+
51+
it('should support focusing and blurring accordion triggers', async () => {
52+
const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'}));
53+
await trigger.focus();
54+
expect(await trigger.isFocused()).toBeTrue();
55+
56+
await trigger.blur();
57+
expect(await trigger.isFocused()).toBeFalse();
58+
});
59+
60+
it('should correctly report the disabled state of a trigger', async () => {
61+
const activeTrigger = await loader.getHarness(
62+
AccordionTriggerHarness.with({text: 'Section 1'}),
63+
);
64+
const disabledTrigger = await loader.getHarness(
65+
AccordionTriggerHarness.with({text: 'Section 2'}),
66+
);
67+
68+
expect(await activeTrigger.isDisabled()).toBeFalse();
69+
expect(await disabledTrigger.isDisabled()).toBeTrue();
70+
});
71+
72+
it('should correctly report the expanded state of a trigger', async () => {
73+
const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'}));
74+
expect(await trigger.isExpanded()).toBeFalse();
75+
76+
await trigger.click();
77+
expect(await trigger.isExpanded()).toBeTrue();
78+
});
79+
80+
it('should filter triggers by disabled state', async () => {
81+
const disabledTriggers = await loader.getAllHarnesses(
82+
AccordionTriggerHarness.with({disabled: true}),
83+
);
84+
expect(disabledTriggers.length).toBe(1);
85+
expect(await disabledTriggers[0].getText()).toBe('Section 2');
86+
});
87+
88+
it('should filter triggers by expanded state', async () => {
89+
const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'}));
90+
await trigger.click();
91+
92+
const expandedTriggers = await loader.getAllHarnesses(
93+
AccordionTriggerHarness.with({expanded: true}),
94+
);
95+
expect(expandedTriggers.length).toBe(1);
96+
expect(await expandedTriggers[0].getText()).toBe('Section 1');
97+
});
98+
99+
it('should find the panel associated with a specific trigger', async () => {
100+
const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'}));
101+
const panel = await loader.getHarness(AccordionPanelHarness.with({trigger}));
102+
103+
expect(await panel.getText()).toBe('Content 1');
104+
});
105+
106+
it('should correctly report the expanded state of an accordion panel', async () => {
107+
const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'}));
108+
const panel = await loader.getHarness(AccordionPanelHarness.with({trigger}));
109+
110+
expect(await panel.isExpanded()).toBeFalse();
111+
112+
await trigger.click();
113+
expect(await panel.isExpanded()).toBeTrue();
114+
});
115+
116+
it('should find accordion group and list scoped triggers and panels', async () => {
117+
const group = await loader.getHarness(AccordionGroupHarness);
118+
const triggers = await group.getTriggers();
119+
const panels = await group.getPanels();
120+
121+
expect(triggers.length).toBe(2);
122+
expect(panels.length).toBe(2);
123+
});
124+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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, BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
/** Filters for locating an `AccordionTriggerHarness`. */
12+
export interface AccordionTriggerHarnessFilters extends BaseHarnessFilters {
13+
/** Only find instances whose text matches the given value. */
14+
text?: string | RegExp;
15+
/** Only find instances whose expanded state matches the given value. */
16+
expanded?: boolean;
17+
/** Only find instances whose disabled state matches the given value. */
18+
disabled?: boolean;
19+
}
20+
21+
/** Filters for locating an `AccordionPanelHarness`. */
22+
export interface AccordionPanelHarnessFilters extends BaseHarnessFilters {
23+
/** Find the panel associated with the given trigger harness. */
24+
trigger?: AccordionTriggerHarness;
25+
}
26+
27+
/** Filters for locating an `AccordionGroupHarness`. */
28+
export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {}
29+
30+
/** Harness for interacting with an `ngAccordionPanel` in tests. */
31+
export class AccordionPanelHarness extends ComponentHarness {
32+
/** The selector for the host element of an `ngAccordionPanel` instance. */
33+
static hostSelector = '[ngAccordionPanel]';
34+
35+
/**
36+
* Gets a `HarnessPredicate` that can be used to search for a panel with specific attributes.
37+
* @param options Options for narrowing the search.
38+
* @return a `HarnessPredicate` configured with the given options.
39+
*/
40+
static with(options: AccordionPanelHarnessFilters = {}): HarnessPredicate<AccordionPanelHarness> {
41+
return new HarnessPredicate(AccordionPanelHarness, options).addOption(
42+
'trigger',
43+
options.trigger,
44+
async (harness, trigger) => {
45+
const targetPanelId = await (await trigger.host()).getAttribute('aria-controls');
46+
const panelId = await (await harness.host()).getAttribute('id');
47+
return panelId === targetPanelId;
48+
},
49+
);
50+
}
51+
52+
/** Gets the text content of the accordion panel. */
53+
async getText(): Promise<string> {
54+
return (await this.host()).text();
55+
}
56+
57+
/** Whether the accordion panel is expanded (visible and not inert). */
58+
async isExpanded(): Promise<boolean> {
59+
return (await (await this.host()).getAttribute('inert')) === null;
60+
}
61+
}
62+
63+
/** Harness for interacting with an `ngAccordionTrigger` in tests. */
64+
export class AccordionTriggerHarness extends ComponentHarness {
65+
/** The selector for the host element of an `ngAccordionTrigger` instance. */
66+
static hostSelector = '[ngAccordionTrigger]';
67+
68+
/**
69+
* Gets a `HarnessPredicate` that can be used to search for a trigger with specific attributes.
70+
* @param options Options for narrowing the search.
71+
* @return a `HarnessPredicate` configured with the given options.
72+
*/
73+
static with(
74+
options: AccordionTriggerHarnessFilters = {},
75+
): HarnessPredicate<AccordionTriggerHarness> {
76+
return new HarnessPredicate(AccordionTriggerHarness, options)
77+
.addOption('text', options.text, (harness, text) =>
78+
HarnessPredicate.stringMatches(harness.getText(), text),
79+
)
80+
.addOption(
81+
'expanded',
82+
options.expanded,
83+
async (harness, expanded) => (await harness.isExpanded()) === expanded,
84+
)
85+
.addOption(
86+
'disabled',
87+
options.disabled,
88+
async (harness, disabled) => (await harness.isDisabled()) === disabled,
89+
);
90+
}
91+
92+
/** Gets the text content of the accordion trigger. */
93+
async getText(): Promise<string> {
94+
return (await this.host()).text();
95+
}
96+
97+
/** Clicks the accordion trigger. */
98+
async click(): Promise<void> {
99+
return (await this.host()).click();
100+
}
101+
102+
/** Focuses the accordion trigger. */
103+
async focus(): Promise<void> {
104+
return (await this.host()).focus();
105+
}
106+
107+
/** Blurs the accordion trigger. */
108+
async blur(): Promise<void> {
109+
return (await this.host()).blur();
110+
}
111+
112+
/** Whether the accordion trigger is focused. */
113+
async isFocused(): Promise<boolean> {
114+
return (await this.host()).isFocused();
115+
}
116+
117+
/** Whether the accordion panel associated with this trigger is expanded. */
118+
async isExpanded(): Promise<boolean> {
119+
const ariaExpanded = await (await this.host()).getAttribute('aria-expanded');
120+
return ariaExpanded === 'true';
121+
}
122+
123+
/** Whether the accordion trigger is disabled. */
124+
async isDisabled(): Promise<boolean> {
125+
const ariaDisabled = await (await this.host()).getAttribute('aria-disabled');
126+
return ariaDisabled === 'true';
127+
}
128+
}
129+
130+
/** Harness for interacting with an `ngAccordionGroup` in tests. */
131+
export class AccordionGroupHarness extends ComponentHarness {
132+
/** The selector for the host element of an `ngAccordionGroup` instance. */
133+
static hostSelector = '[ngAccordionGroup]';
134+
135+
/**
136+
* Gets a `HarnessPredicate` that can be used to search for an accordion group with specific attributes.
137+
* @param options Options for narrowing the search.
138+
* @return a `HarnessPredicate` configured with the given options.
139+
*/
140+
static with(options: AccordionGroupHarnessFilters = {}): HarnessPredicate<AccordionGroupHarness> {
141+
return new HarnessPredicate(AccordionGroupHarness, options);
142+
}
143+
144+
/** Gets all accordion triggers within this group. */
145+
async getTriggers(
146+
filters: AccordionTriggerHarnessFilters = {},
147+
): Promise<AccordionTriggerHarness[]> {
148+
return this.locatorForAll(AccordionTriggerHarness.with(filters))();
149+
}
150+
151+
/** Gets all accordion panels within this group. */
152+
async getPanels(filters: AccordionPanelHarnessFilters = {}): Promise<AccordionPanelHarness[]> {
153+
return this.locatorForAll(AccordionPanelHarness.with(filters))();
154+
}
155+
}
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 './accordion-harness';

0 commit comments

Comments
 (0)