From d22355b37741ecc4dec9dcef188b49570fb27f33 Mon Sep 17 00:00:00 2001 From: Cheng-Hsuan Tsai Date: Tue, 7 Apr 2026 23:23:47 +0000 Subject: [PATCH 1/4] feat(aria/accordion): introduce accordion harness --- goldens/aria/accordion/testing/index.api.md | 58 +++++++ src/aria/accordion/BUILD.bazel | 3 + src/aria/accordion/testing/BUILD.bazel | 42 +++++ .../testing/accordion-harness.spec.ts | 124 ++++++++++++++ .../accordion/testing/accordion-harness.ts | 155 ++++++++++++++++++ src/aria/accordion/testing/index.ts | 9 + src/aria/accordion/testing/public-api.ts | 9 + src/aria/config.bzl | 1 + 8 files changed, 401 insertions(+) create mode 100644 goldens/aria/accordion/testing/index.api.md create mode 100644 src/aria/accordion/testing/BUILD.bazel create mode 100644 src/aria/accordion/testing/accordion-harness.spec.ts create mode 100644 src/aria/accordion/testing/accordion-harness.ts create mode 100644 src/aria/accordion/testing/index.ts create mode 100644 src/aria/accordion/testing/public-api.ts diff --git a/goldens/aria/accordion/testing/index.api.md b/goldens/aria/accordion/testing/index.api.md new file mode 100644 index 000000000000..8e7cbe63088d --- /dev/null +++ b/goldens/aria/accordion/testing/index.api.md @@ -0,0 +1,58 @@ +## API Report File for "@angular/aria_accordion_testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseHarnessFilters } from '@angular/cdk/testing'; +import { ComponentHarness } from '@angular/cdk/testing'; +import { HarnessPredicate } from '@angular/cdk/testing'; + +// @public +export class AccordionGroupHarness extends ComponentHarness { + getPanels(filters?: AccordionPanelHarnessFilters): Promise; + getTriggers(filters?: AccordionTriggerHarnessFilters): Promise; + static hostSelector: string; + static with(options?: AccordionGroupHarnessFilters): HarnessPredicate; +} + +// @public +export interface AccordionGroupHarnessFilters extends BaseHarnessFilters { +} + +// @public +export class AccordionPanelHarness extends ComponentHarness { + getText(): Promise; + static hostSelector: string; + isExpanded(): Promise; + static with(options?: AccordionPanelHarnessFilters): HarnessPredicate; +} + +// @public +export interface AccordionPanelHarnessFilters extends BaseHarnessFilters { + trigger?: AccordionTriggerHarness; +} + +// @public +export class AccordionTriggerHarness extends ComponentHarness { + blur(): Promise; + click(): Promise; + focus(): Promise; + getText(): Promise; + static hostSelector: string; + isDisabled(): Promise; + isExpanded(): Promise; + isFocused(): Promise; + static with(options?: AccordionTriggerHarnessFilters): HarnessPredicate; +} + +// @public +export interface AccordionTriggerHarnessFilters extends BaseHarnessFilters { + disabled?: boolean; + expanded?: boolean; + text?: string | RegExp; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/src/aria/accordion/BUILD.bazel b/src/aria/accordion/BUILD.bazel index 51ffb8340196..59483aa59b55 100644 --- a/src/aria/accordion/BUILD.bazel +++ b/src/aria/accordion/BUILD.bazel @@ -13,6 +13,7 @@ ng_project( "//src/aria/private", "//src/cdk/a11y", "//src/cdk/bidi", + "//src/cdk/testing", ], ) @@ -26,7 +27,9 @@ ng_project( ":accordion", "//:node_modules/@angular/core", "//:node_modules/@angular/platform-browser", + "//src/cdk/testing", "//src/cdk/testing/private", + "//src/cdk/testing/testbed", ], ) diff --git a/src/aria/accordion/testing/BUILD.bazel b/src/aria/accordion/testing/BUILD.bazel new file mode 100644 index 000000000000..daefc5897c1a --- /dev/null +++ b/src/aria/accordion/testing/BUILD.bazel @@ -0,0 +1,42 @@ +load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//:node_modules/@angular/core", + "//src/cdk/testing", + ], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) + +ng_project( + name = "unit_tests_lib", + testonly = True, + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":testing", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//src/aria/accordion", + "//src/cdk/testing", + "//src/cdk/testing/private", + "//src/cdk/testing/testbed", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":unit_tests_lib", + ], +) diff --git a/src/aria/accordion/testing/accordion-harness.spec.ts b/src/aria/accordion/testing/accordion-harness.spec.ts new file mode 100644 index 000000000000..a55768d4d383 --- /dev/null +++ b/src/aria/accordion/testing/accordion-harness.spec.ts @@ -0,0 +1,124 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import { + AccordionTriggerHarness, + AccordionPanelHarness, + AccordionGroupHarness, +} from './accordion-harness'; +import {AccordionGroup, AccordionPanel, AccordionTrigger} from '../index'; + +describe('Accordion Harnesses', () => { + let fixture: any; + let loader: any; + + @Component({ + imports: [AccordionGroup, AccordionPanel, AccordionTrigger], + template: ` +
+
Content 1
+ + +
Content 2
+ +
+ `, + }) + class AccordionHarnessTestComponent {} + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AccordionHarnessTestComponent], + }); + fixture = TestBed.createComponent(AccordionHarnessTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should find all accordion triggers', async () => { + const triggers = await loader.getAllHarnesses(AccordionTriggerHarness); + expect(triggers.length).toBe(2); + }); + + it('should support focusing and blurring accordion triggers', async () => { + const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); + await trigger.focus(); + expect(await trigger.isFocused()).toBeTrue(); + + await trigger.blur(); + expect(await trigger.isFocused()).toBeFalse(); + }); + + it('should correctly report the disabled state of a trigger', async () => { + const activeTrigger = await loader.getHarness( + AccordionTriggerHarness.with({text: 'Section 1'}), + ); + const disabledTrigger = await loader.getHarness( + AccordionTriggerHarness.with({text: 'Section 2'}), + ); + + expect(await activeTrigger.isDisabled()).toBeFalse(); + expect(await disabledTrigger.isDisabled()).toBeTrue(); + }); + + it('should correctly report the expanded state of a trigger', async () => { + const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); + expect(await trigger.isExpanded()).toBeFalse(); + + await trigger.click(); + expect(await trigger.isExpanded()).toBeTrue(); + }); + + it('should filter triggers by disabled state', async () => { + const disabledTriggers = await loader.getAllHarnesses( + AccordionTriggerHarness.with({disabled: true}), + ); + expect(disabledTriggers.length).toBe(1); + expect(await disabledTriggers[0].getText()).toBe('Section 2'); + }); + + it('should filter triggers by expanded state', async () => { + const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); + await trigger.click(); + + const expandedTriggers = await loader.getAllHarnesses( + AccordionTriggerHarness.with({expanded: true}), + ); + expect(expandedTriggers.length).toBe(1); + expect(await expandedTriggers[0].getText()).toBe('Section 1'); + }); + + it('should find the panel associated with a specific trigger', async () => { + const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); + const panel = await loader.getHarness(AccordionPanelHarness.with({trigger})); + + expect(await panel.getText()).toBe('Content 1'); + }); + + it('should correctly report the expanded state of an accordion panel', async () => { + const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); + const panel = await loader.getHarness(AccordionPanelHarness.with({trigger})); + + expect(await panel.isExpanded()).toBeFalse(); + + await trigger.click(); + expect(await panel.isExpanded()).toBeTrue(); + }); + + it('should find accordion group and list scoped triggers and panels', async () => { + const group = await loader.getHarness(AccordionGroupHarness); + const triggers = await group.getTriggers(); + const panels = await group.getPanels(); + + expect(triggers.length).toBe(2); + expect(panels.length).toBe(2); + }); +}); diff --git a/src/aria/accordion/testing/accordion-harness.ts b/src/aria/accordion/testing/accordion-harness.ts new file mode 100644 index 000000000000..1b8fdfcdd0b9 --- /dev/null +++ b/src/aria/accordion/testing/accordion-harness.ts @@ -0,0 +1,155 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {ComponentHarness, HarnessPredicate, BaseHarnessFilters} from '@angular/cdk/testing'; + +/** Filters for locating an `AccordionTriggerHarness`. */ +export interface AccordionTriggerHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose text matches the given value. */ + text?: string | RegExp; + /** Only find instances whose expanded state matches the given value. */ + expanded?: boolean; + /** Only find instances whose disabled state matches the given value. */ + disabled?: boolean; +} + +/** Filters for locating an `AccordionPanelHarness`. */ +export interface AccordionPanelHarnessFilters extends BaseHarnessFilters { + /** Find the panel associated with the given trigger harness. */ + trigger?: AccordionTriggerHarness; +} + +/** Filters for locating an `AccordionGroupHarness`. */ +export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {} + +/** Harness for interacting with an `ngAccordionPanel` in tests. */ +export class AccordionPanelHarness extends ComponentHarness { + /** The selector for the host element of an `ngAccordionPanel` instance. */ + static hostSelector = '[ngAccordionPanel]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a panel with specific attributes. + * @param options Options for narrowing the search. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: AccordionPanelHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(AccordionPanelHarness, options).addOption( + 'trigger', + options.trigger, + async (harness, trigger) => { + const targetPanelId = await (await trigger.host()).getAttribute('aria-controls'); + const panelId = await (await harness.host()).getAttribute('id'); + return panelId === targetPanelId; + }, + ); + } + + /** Gets the text content of the accordion panel. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Whether the accordion panel is expanded (visible and not inert). */ + async isExpanded(): Promise { + return (await (await this.host()).getAttribute('inert')) === null; + } +} + +/** Harness for interacting with an `ngAccordionTrigger` in tests. */ +export class AccordionTriggerHarness extends ComponentHarness { + /** The selector for the host element of an `ngAccordionTrigger` instance. */ + static hostSelector = '[ngAccordionTrigger]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a trigger with specific attributes. + * @param options Options for narrowing the search. + * @return a `HarnessPredicate` configured with the given options. + */ + static with( + options: AccordionTriggerHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(AccordionTriggerHarness, options) + .addOption('text', options.text, (harness, text) => + HarnessPredicate.stringMatches(harness.getText(), text), + ) + .addOption( + 'expanded', + options.expanded, + async (harness, expanded) => (await harness.isExpanded()) === expanded, + ) + .addOption( + 'disabled', + options.disabled, + async (harness, disabled) => (await harness.isDisabled()) === disabled, + ); + } + + /** Gets the text content of the accordion trigger. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Clicks the accordion trigger. */ + async click(): Promise { + return (await this.host()).click(); + } + + /** Focuses the accordion trigger. */ + async focus(): Promise { + return (await this.host()).focus(); + } + + /** Blurs the accordion trigger. */ + async blur(): Promise { + return (await this.host()).blur(); + } + + /** Whether the accordion trigger is focused. */ + async isFocused(): Promise { + return (await this.host()).isFocused(); + } + + /** Whether the accordion panel associated with this trigger is expanded. */ + async isExpanded(): Promise { + const ariaExpanded = await (await this.host()).getAttribute('aria-expanded'); + return ariaExpanded === 'true'; + } + + /** Whether the accordion trigger is disabled. */ + async isDisabled(): Promise { + const ariaDisabled = await (await this.host()).getAttribute('aria-disabled'); + return ariaDisabled === 'true'; + } +} + +/** Harness for interacting with an `ngAccordionGroup` in tests. */ +export class AccordionGroupHarness extends ComponentHarness { + /** The selector for the host element of an `ngAccordionGroup` instance. */ + static hostSelector = '[ngAccordionGroup]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for an accordion group with specific attributes. + * @param options Options for narrowing the search. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: AccordionGroupHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(AccordionGroupHarness, options); + } + + /** Gets all accordion triggers within this group. */ + async getTriggers( + filters: AccordionTriggerHarnessFilters = {}, + ): Promise { + return this.locatorForAll(AccordionTriggerHarness.with(filters))(); + } + + /** Gets all accordion panels within this group. */ + async getPanels(filters: AccordionPanelHarnessFilters = {}): Promise { + return this.locatorForAll(AccordionPanelHarness.with(filters))(); + } +} diff --git a/src/aria/accordion/testing/index.ts b/src/aria/accordion/testing/index.ts new file mode 100644 index 000000000000..52b3c7a5156f --- /dev/null +++ b/src/aria/accordion/testing/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './public-api'; diff --git a/src/aria/accordion/testing/public-api.ts b/src/aria/accordion/testing/public-api.ts new file mode 100644 index 000000000000..94d969f785aa --- /dev/null +++ b/src/aria/accordion/testing/public-api.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './accordion-harness'; diff --git a/src/aria/config.bzl b/src/aria/config.bzl index 291412b5a3fb..de62934edcf6 100644 --- a/src/aria/config.bzl +++ b/src/aria/config.bzl @@ -1,6 +1,7 @@ # List of all entry-points of the Angular Aria package. ARIA_ENTRYPOINTS = [ "accordion", + "accordion/testing", "combobox", "grid", "listbox", From 4972394c2692e7e7ab5e500e9130d78052e9e060 Mon Sep 17 00:00:00 2001 From: Cheng-Hsuan Tsai Date: Thu, 9 Apr 2026 20:59:24 +0000 Subject: [PATCH 2/4] refactor(aria/accordion): consolidate accordion harness to match material expansion harness --- .../testing/accordion-harness.spec.ts | 142 +++++++++------- .../accordion/testing/accordion-harness.ts | 153 ++++++++---------- 2 files changed, 149 insertions(+), 146 deletions(-) diff --git a/src/aria/accordion/testing/accordion-harness.spec.ts b/src/aria/accordion/testing/accordion-harness.spec.ts index a55768d4d383..cd43d4096b21 100644 --- a/src/aria/accordion/testing/accordion-harness.spec.ts +++ b/src/aria/accordion/testing/accordion-harness.spec.ts @@ -9,13 +9,19 @@ import {Component} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import { - AccordionTriggerHarness, - AccordionPanelHarness, - AccordionGroupHarness, -} from './accordion-harness'; +import {ComponentHarness} from '@angular/cdk/testing'; +import {AccordionHarness, AccordionGroupHarness} from './accordion-harness'; import {AccordionGroup, AccordionPanel, AccordionTrigger} from '../index'; +/** Lightweight test harness to test querying inside the accordion body panel. */ +class TestButtonHarness extends ComponentHarness { + static hostSelector = 'button.test-button'; + + async getText(): Promise { + return (await this.host()).text(); + } +} + describe('Accordion Harnesses', () => { let fixture: any; let loader: any; @@ -24,7 +30,9 @@ describe('Accordion Harnesses', () => { imports: [AccordionGroup, AccordionPanel, AccordionTrigger], template: `
-
Content 1
+
+ +
Content 2
@@ -43,82 +51,100 @@ describe('Accordion Harnesses', () => { loader = TestbedHarnessEnvironment.loader(fixture); }); - it('should find all accordion triggers', async () => { - const triggers = await loader.getAllHarnesses(AccordionTriggerHarness); - expect(triggers.length).toBe(2); + it('should find accordion group and list all scoped accordions using getAccordions', async () => { + const group = await loader.getHarness(AccordionGroupHarness); + const accordions = await group.getAccordions(); + + expect(accordions.length).toBe(2); + expect(await accordions[0].getTitle()).toBe('Section 1'); + expect(await accordions[1].getTitle()).toBe('Section 2'); }); - it('should support focusing and blurring accordion triggers', async () => { - const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); - await trigger.focus(); - expect(await trigger.isFocused()).toBeTrue(); + it('should find all individual accordions via standard root loader', async () => { + const accordions = await loader.getAllHarnesses(AccordionHarness); + expect(accordions.length).toBe(2); + }); - await trigger.blur(); - expect(await trigger.isFocused()).toBeFalse(); + it('should filter accordions by title', async () => { + const accordions = await loader.getAllHarnesses(AccordionHarness.with({title: 'Section 1'})); + expect(accordions.length).toBe(1); + expect(await accordions[0].getTitle()).toBe('Section 1'); }); - it('should correctly report the disabled state of a trigger', async () => { - const activeTrigger = await loader.getHarness( - AccordionTriggerHarness.with({text: 'Section 1'}), - ); - const disabledTrigger = await loader.getHarness( - AccordionTriggerHarness.with({text: 'Section 2'}), + it('should filter accordions by expanded state', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + await accordion.expand(); + + const expandedAccordions = await loader.getAllHarnesses( + AccordionHarness.with({expanded: true}), ); + expect(expandedAccordions.length).toBe(1); + expect(await expandedAccordions[0].getTitle()).toBe('Section 1'); + }); - expect(await activeTrigger.isDisabled()).toBeFalse(); - expect(await disabledTrigger.isDisabled()).toBeTrue(); + it('should filter accordions by disabled state', async () => { + const disabledAccordions = await loader.getAllHarnesses( + AccordionHarness.with({disabled: true}), + ); + expect(disabledAccordions.length).toBe(1); + expect(await disabledAccordions[0].getTitle()).toBe('Section 2'); }); - it('should correctly report the expanded state of a trigger', async () => { - const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); - expect(await trigger.isExpanded()).toBeFalse(); + it('should get the title of the accordion', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + expect(await accordion.getTitle()).toBe('Section 1'); + }); - await trigger.click(); - expect(await trigger.isExpanded()).toBeTrue(); + it('should correctly report the expanded state of an accordion', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + expect(await accordion.isExpanded()).toBeFalse(); }); - it('should filter triggers by disabled state', async () => { - const disabledTriggers = await loader.getAllHarnesses( - AccordionTriggerHarness.with({disabled: true}), - ); - expect(disabledTriggers.length).toBe(1); - expect(await disabledTriggers[0].getText()).toBe('Section 2'); + it('should correctly report the disabled state of an accordion', async () => { + const activeAccordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + const disabledAccordion = await loader.getHarness(AccordionHarness.with({title: 'Section 2'})); + + expect(await activeAccordion.isDisabled()).toBeFalse(); + expect(await disabledAccordion.isDisabled()).toBeTrue(); }); - it('should filter triggers by expanded state', async () => { - const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); - await trigger.click(); + it('expands a collapsed accordion using the expand method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); - const expandedTriggers = await loader.getAllHarnesses( - AccordionTriggerHarness.with({expanded: true}), - ); - expect(expandedTriggers.length).toBe(1); - expect(await expandedTriggers[0].getText()).toBe('Section 1'); + await accordion.expand(); + + expect(await accordion.isExpanded()).toBeTrue(); }); - it('should find the panel associated with a specific trigger', async () => { - const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); - const panel = await loader.getHarness(AccordionPanelHarness.with({trigger})); + it('collapses an expanded accordion using the collapse method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + await accordion.expand(); - expect(await panel.getText()).toBe('Content 1'); + await accordion.collapse(); + + expect(await accordion.isExpanded()).toBeFalse(); }); - it('should correctly report the expanded state of an accordion panel', async () => { - const trigger = await loader.getHarness(AccordionTriggerHarness.with({text: 'Section 1'})); - const panel = await loader.getHarness(AccordionPanelHarness.with({trigger})); + it('toggles the expanded state of an accordion using the toggle method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); - expect(await panel.isExpanded()).toBeFalse(); + await accordion.toggle(); - await trigger.click(); - expect(await panel.isExpanded()).toBeTrue(); + expect(await accordion.isExpanded()).toBeTrue(); }); - it('should find accordion group and list scoped triggers and panels', async () => { - const group = await loader.getHarness(AccordionGroupHarness); - const triggers = await group.getTriggers(); - const panels = await group.getPanels(); + it('should support focusing and blurring accordion triggers', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + await accordion.focus(); + expect(await accordion.isFocused()).toBeTrue(); + + await accordion.blur(); + expect(await accordion.isFocused()).toBeFalse(); + }); - expect(triggers.length).toBe(2); - expect(panels.length).toBe(2); + it('should query components inside the accordion panel using ContentContainerComponentHarness', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + const button = await accordion.getHarness(TestButtonHarness); + expect(await button.getText()).toBe('Inside Content 1'); }); }); diff --git a/src/aria/accordion/testing/accordion-harness.ts b/src/aria/accordion/testing/accordion-harness.ts index 1b8fdfcdd0b9..0a401fa8f505 100644 --- a/src/aria/accordion/testing/accordion-harness.ts +++ b/src/aria/accordion/testing/accordion-harness.ts @@ -6,76 +6,44 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ComponentHarness, HarnessPredicate, BaseHarnessFilters} from '@angular/cdk/testing'; +import { + ComponentHarness, + ContentContainerComponentHarness, + HarnessPredicate, + BaseHarnessFilters, +} from '@angular/cdk/testing'; + +/** Selectors for the sections that may contain user content. */ +export enum AccordionSection { + TRIGGER = '[ngAccordionTrigger]', + PANEL = '[ngAccordionPanel]', +} -/** Filters for locating an `AccordionTriggerHarness`. */ -export interface AccordionTriggerHarnessFilters extends BaseHarnessFilters { - /** Only find instances whose text matches the given value. */ - text?: string | RegExp; +/** Filters for locating an `AccordionHarness`. */ +export interface AccordionHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose title text matches the given value. */ + title?: string | RegExp; /** Only find instances whose expanded state matches the given value. */ expanded?: boolean; /** Only find instances whose disabled state matches the given value. */ disabled?: boolean; } -/** Filters for locating an `AccordionPanelHarness`. */ -export interface AccordionPanelHarnessFilters extends BaseHarnessFilters { - /** Find the panel associated with the given trigger harness. */ - trigger?: AccordionTriggerHarness; -} - /** Filters for locating an `AccordionGroupHarness`. */ export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {} -/** Harness for interacting with an `ngAccordionPanel` in tests. */ -export class AccordionPanelHarness extends ComponentHarness { - /** The selector for the host element of an `ngAccordionPanel` instance. */ - static hostSelector = '[ngAccordionPanel]'; - - /** - * Gets a `HarnessPredicate` that can be used to search for a panel with specific attributes. - * @param options Options for narrowing the search. - * @return a `HarnessPredicate` configured with the given options. - */ - static with(options: AccordionPanelHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(AccordionPanelHarness, options).addOption( - 'trigger', - options.trigger, - async (harness, trigger) => { - const targetPanelId = await (await trigger.host()).getAttribute('aria-controls'); - const panelId = await (await harness.host()).getAttribute('id'); - return panelId === targetPanelId; - }, - ); - } - - /** Gets the text content of the accordion panel. */ - async getText(): Promise { - return (await this.host()).text(); - } - - /** Whether the accordion panel is expanded (visible and not inert). */ - async isExpanded(): Promise { - return (await (await this.host()).getAttribute('inert')) === null; - } -} - -/** Harness for interacting with an `ngAccordionTrigger` in tests. */ -export class AccordionTriggerHarness extends ComponentHarness { - /** The selector for the host element of an `ngAccordionTrigger` instance. */ +/** Harness for interacting with a standard ngAccordion item in tests. */ +export class AccordionHarness extends ContentContainerComponentHarness { static hostSelector = '[ngAccordionTrigger]'; /** - * Gets a `HarnessPredicate` that can be used to search for a trigger with specific attributes. - * @param options Options for narrowing the search. - * @return a `HarnessPredicate` configured with the given options. + * Gets a `HarnessPredicate` that can be used to search for an accordion + * with specific attributes. */ - static with( - options: AccordionTriggerHarnessFilters = {}, - ): HarnessPredicate { - return new HarnessPredicate(AccordionTriggerHarness, options) - .addOption('text', options.text, (harness, text) => - HarnessPredicate.stringMatches(harness.getText(), text), + static with(options: AccordionHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(AccordionHarness, options) + .addOption('title', options.title, (harness, title) => + HarnessPredicate.stringMatches(harness.getTitle(), title), ) .addOption( 'expanded', @@ -89,67 +57,76 @@ export class AccordionTriggerHarness extends ComponentHarness { ); } - /** Gets the text content of the accordion trigger. */ - async getText(): Promise { + /** Overrides the internal loader to automatically resolve queries inside the associated panel. */ + protected override async getRootHarnessLoader() { + const panelId = await (await this.host()).getAttribute('aria-controls'); + const documentRoot = await this.documentRootLocatorFactory().rootHarnessLoader(); + return documentRoot.getChildLoader(`[ngAccordionPanel][id="${panelId}"]`); + } + + /** Whether the accordion is expanded. */ + async isExpanded(): Promise { + return (await (await this.host()).getAttribute('aria-expanded')) === 'true'; + } + + /** Whether the accordion is disabled. */ + async isDisabled(): Promise { + return (await (await this.host()).getAttribute('aria-disabled')) === 'true'; + } + + /** Gets the title text of the accordion. */ + async getTitle(): Promise { return (await this.host()).text(); } - /** Clicks the accordion trigger. */ - async click(): Promise { - return (await this.host()).click(); + /** Toggles the expanded state of the accordion by clicking on the trigger. */ + async toggle(): Promise { + await (await this.host()).click(); + } + + /** Expands the accordion if collapsed. */ + async expand(): Promise { + if (!(await this.isExpanded())) { + await this.toggle(); + } + } + + /** Collapses the accordion if expanded. */ + async collapse(): Promise { + if (await this.isExpanded()) { + await this.toggle(); + } } /** Focuses the accordion trigger. */ async focus(): Promise { - return (await this.host()).focus(); + await (await this.host()).focus(); } /** Blurs the accordion trigger. */ async blur(): Promise { - return (await this.host()).blur(); + await (await this.host()).blur(); } /** Whether the accordion trigger is focused. */ async isFocused(): Promise { return (await this.host()).isFocused(); } - - /** Whether the accordion panel associated with this trigger is expanded. */ - async isExpanded(): Promise { - const ariaExpanded = await (await this.host()).getAttribute('aria-expanded'); - return ariaExpanded === 'true'; - } - - /** Whether the accordion trigger is disabled. */ - async isDisabled(): Promise { - const ariaDisabled = await (await this.host()).getAttribute('aria-disabled'); - return ariaDisabled === 'true'; - } } /** Harness for interacting with an `ngAccordionGroup` in tests. */ export class AccordionGroupHarness extends ComponentHarness { - /** The selector for the host element of an `ngAccordionGroup` instance. */ static hostSelector = '[ngAccordionGroup]'; /** * Gets a `HarnessPredicate` that can be used to search for an accordion group with specific attributes. - * @param options Options for narrowing the search. - * @return a `HarnessPredicate` configured with the given options. */ static with(options: AccordionGroupHarnessFilters = {}): HarnessPredicate { return new HarnessPredicate(AccordionGroupHarness, options); } - /** Gets all accordion triggers within this group. */ - async getTriggers( - filters: AccordionTriggerHarnessFilters = {}, - ): Promise { - return this.locatorForAll(AccordionTriggerHarness.with(filters))(); - } - - /** Gets all accordion panels within this group. */ - async getPanels(filters: AccordionPanelHarnessFilters = {}): Promise { - return this.locatorForAll(AccordionPanelHarness.with(filters))(); + /** Gets all accordions within this group. */ + async getAccordions(filters: AccordionHarnessFilters = {}): Promise { + return this.locatorForAll(AccordionHarness.with(filters))(); } } From 6a16ffabcaa1e77208a025680839f5cab880d9bb Mon Sep 17 00:00:00 2001 From: Cheng-Hsuan Tsai Date: Thu, 9 Apr 2026 21:12:50 +0000 Subject: [PATCH 3/4] fixup! refactor(aria/accordion): consolidate accordion harness to match material expansion harness --- goldens/aria/accordion/testing/index.api.md | 43 +++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/goldens/aria/accordion/testing/index.api.md b/goldens/aria/accordion/testing/index.api.md index 8e7cbe63088d..ef4d0432fa9a 100644 --- a/goldens/aria/accordion/testing/index.api.md +++ b/goldens/aria/accordion/testing/index.api.md @@ -4,14 +4,16 @@ ```ts +import * as _angular_cdk_testing from '@angular/cdk/testing'; import { BaseHarnessFilters } from '@angular/cdk/testing'; import { ComponentHarness } from '@angular/cdk/testing'; +import { ContentContainerComponentHarness } from '@angular/cdk/testing'; import { HarnessPredicate } from '@angular/cdk/testing'; // @public export class AccordionGroupHarness extends ComponentHarness { - getPanels(filters?: AccordionPanelHarnessFilters): Promise; - getTriggers(filters?: AccordionTriggerHarnessFilters): Promise; + getAccordions(filters?: AccordionHarnessFilters): Promise; + // (undocumented) static hostSelector: string; static with(options?: AccordionGroupHarnessFilters): HarnessPredicate; } @@ -21,36 +23,35 @@ export interface AccordionGroupHarnessFilters extends BaseHarnessFilters { } // @public -export class AccordionPanelHarness extends ComponentHarness { - getText(): Promise; - static hostSelector: string; - isExpanded(): Promise; - static with(options?: AccordionPanelHarnessFilters): HarnessPredicate; -} - -// @public -export interface AccordionPanelHarnessFilters extends BaseHarnessFilters { - trigger?: AccordionTriggerHarness; -} - -// @public -export class AccordionTriggerHarness extends ComponentHarness { +export class AccordionHarness extends ContentContainerComponentHarness { blur(): Promise; - click(): Promise; + collapse(): Promise; + expand(): Promise; focus(): Promise; - getText(): Promise; + protected getRootHarnessLoader(): Promise<_angular_cdk_testing.HarnessLoader>; + getTitle(): Promise; + // (undocumented) static hostSelector: string; isDisabled(): Promise; isExpanded(): Promise; isFocused(): Promise; - static with(options?: AccordionTriggerHarnessFilters): HarnessPredicate; + toggle(): Promise; + static with(options?: AccordionHarnessFilters): HarnessPredicate; } // @public -export interface AccordionTriggerHarnessFilters extends BaseHarnessFilters { +export interface AccordionHarnessFilters extends BaseHarnessFilters { disabled?: boolean; expanded?: boolean; - text?: string | RegExp; + title?: string | RegExp; +} + +// @public +export enum AccordionSection { + // (undocumented) + PANEL = "[ngAccordionPanel]", + // (undocumented) + TRIGGER = "[ngAccordionTrigger]" } // (No @packageDocumentation comment for this package) From 84ac04703e3cd970bee809a6871dd296272d1faa Mon Sep 17 00:00:00 2001 From: Cheng-Hsuan Tsai Date: Fri, 10 Apr 2026 06:03:22 +0000 Subject: [PATCH 4/4] fixup! refactor(aria/accordion): consolidate accordion harness to match material expansion harness --- .../testing/accordion-harness-filters.ts | 22 +++++++++++++++++++ .../accordion/testing/accordion-harness.ts | 15 +------------ src/aria/accordion/testing/public-api.ts | 1 + 3 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 src/aria/accordion/testing/accordion-harness-filters.ts diff --git a/src/aria/accordion/testing/accordion-harness-filters.ts b/src/aria/accordion/testing/accordion-harness-filters.ts new file mode 100644 index 000000000000..7c736ddb1c51 --- /dev/null +++ b/src/aria/accordion/testing/accordion-harness-filters.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {BaseHarnessFilters} from '@angular/cdk/testing'; + +/** Filters for locating an `AccordionHarness`. */ +export interface AccordionHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose title text matches the given value. */ + title?: string | RegExp; + /** Only find instances whose expanded state matches the given value. */ + expanded?: boolean; + /** Only find instances whose disabled state matches the given value. */ + disabled?: boolean; +} + +/** Filters for locating an `AccordionGroupHarness`. */ +export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {} diff --git a/src/aria/accordion/testing/accordion-harness.ts b/src/aria/accordion/testing/accordion-harness.ts index 0a401fa8f505..d37c40bdac01 100644 --- a/src/aria/accordion/testing/accordion-harness.ts +++ b/src/aria/accordion/testing/accordion-harness.ts @@ -10,8 +10,8 @@ import { ComponentHarness, ContentContainerComponentHarness, HarnessPredicate, - BaseHarnessFilters, } from '@angular/cdk/testing'; +import {AccordionHarnessFilters, AccordionGroupHarnessFilters} from './accordion-harness-filters'; /** Selectors for the sections that may contain user content. */ export enum AccordionSection { @@ -19,19 +19,6 @@ export enum AccordionSection { PANEL = '[ngAccordionPanel]', } -/** Filters for locating an `AccordionHarness`. */ -export interface AccordionHarnessFilters extends BaseHarnessFilters { - /** Only find instances whose title text matches the given value. */ - title?: string | RegExp; - /** Only find instances whose expanded state matches the given value. */ - expanded?: boolean; - /** Only find instances whose disabled state matches the given value. */ - disabled?: boolean; -} - -/** Filters for locating an `AccordionGroupHarness`. */ -export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {} - /** Harness for interacting with a standard ngAccordion item in tests. */ export class AccordionHarness extends ContentContainerComponentHarness { static hostSelector = '[ngAccordionTrigger]'; diff --git a/src/aria/accordion/testing/public-api.ts b/src/aria/accordion/testing/public-api.ts index 94d969f785aa..acb15bdb15bd 100644 --- a/src/aria/accordion/testing/public-api.ts +++ b/src/aria/accordion/testing/public-api.ts @@ -7,3 +7,4 @@ */ export * from './accordion-harness'; +export * from './accordion-harness-filters';