diff --git a/goldens/aria/accordion/testing/index.api.md b/goldens/aria/accordion/testing/index.api.md new file mode 100644 index 000000000000..ef4d0432fa9a --- /dev/null +++ b/goldens/aria/accordion/testing/index.api.md @@ -0,0 +1,59 @@ +## 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 * 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 { + getAccordions(filters?: AccordionHarnessFilters): Promise; + // (undocumented) + static hostSelector: string; + static with(options?: AccordionGroupHarnessFilters): HarnessPredicate; +} + +// @public +export interface AccordionGroupHarnessFilters extends BaseHarnessFilters { +} + +// @public +export class AccordionHarness extends ContentContainerComponentHarness { + blur(): Promise; + collapse(): Promise; + expand(): Promise; + focus(): Promise; + protected getRootHarnessLoader(): Promise<_angular_cdk_testing.HarnessLoader>; + getTitle(): Promise; + // (undocumented) + static hostSelector: string; + isDisabled(): Promise; + isExpanded(): Promise; + isFocused(): Promise; + toggle(): Promise; + static with(options?: AccordionHarnessFilters): HarnessPredicate; +} + +// @public +export interface AccordionHarnessFilters extends BaseHarnessFilters { + disabled?: boolean; + expanded?: boolean; + title?: string | RegExp; +} + +// @public +export enum AccordionSection { + // (undocumented) + PANEL = "[ngAccordionPanel]", + // (undocumented) + TRIGGER = "[ngAccordionTrigger]" +} + +// (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-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.spec.ts b/src/aria/accordion/testing/accordion-harness.spec.ts new file mode 100644 index 000000000000..cd43d4096b21 --- /dev/null +++ b/src/aria/accordion/testing/accordion-harness.spec.ts @@ -0,0 +1,150 @@ +/** + * @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 {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; + + @Component({ + imports: [AccordionGroup, AccordionPanel, AccordionTrigger], + template: ` +
+
+ +
+ + +
Content 2
+ +
+ `, + }) + class AccordionHarnessTestComponent {} + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AccordionHarnessTestComponent], + }); + fixture = TestBed.createComponent(AccordionHarnessTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + 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 find all individual accordions via standard root loader', async () => { + const accordions = await loader.getAllHarnesses(AccordionHarness); + expect(accordions.length).toBe(2); + }); + + 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 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'); + }); + + 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 get the title of the accordion', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + expect(await accordion.getTitle()).toBe('Section 1'); + }); + + 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 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('expands a collapsed accordion using the expand method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + + await accordion.expand(); + + expect(await accordion.isExpanded()).toBeTrue(); + }); + + it('collapses an expanded accordion using the collapse method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + await accordion.expand(); + + await accordion.collapse(); + + expect(await accordion.isExpanded()).toBeFalse(); + }); + + it('toggles the expanded state of an accordion using the toggle method', async () => { + const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); + + await accordion.toggle(); + + expect(await accordion.isExpanded()).toBeTrue(); + }); + + 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(); + }); + + 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 new file mode 100644 index 000000000000..d37c40bdac01 --- /dev/null +++ b/src/aria/accordion/testing/accordion-harness.ts @@ -0,0 +1,119 @@ +/** + * @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, + ContentContainerComponentHarness, + HarnessPredicate, +} from '@angular/cdk/testing'; +import {AccordionHarnessFilters, AccordionGroupHarnessFilters} from './accordion-harness-filters'; + +/** Selectors for the sections that may contain user content. */ +export enum AccordionSection { + TRIGGER = '[ngAccordionTrigger]', + PANEL = '[ngAccordionPanel]', +} + +/** 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 an accordion + * with specific attributes. + */ + static with(options: AccordionHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(AccordionHarness, options) + .addOption('title', options.title, (harness, title) => + HarnessPredicate.stringMatches(harness.getTitle(), title), + ) + .addOption( + 'expanded', + options.expanded, + async (harness, expanded) => (await harness.isExpanded()) === expanded, + ) + .addOption( + 'disabled', + options.disabled, + async (harness, disabled) => (await harness.isDisabled()) === disabled, + ); + } + + /** 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(); + } + + /** 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 { + await (await this.host()).focus(); + } + + /** Blurs the accordion trigger. */ + async blur(): Promise { + await (await this.host()).blur(); + } + + /** Whether the accordion trigger is focused. */ + async isFocused(): Promise { + return (await this.host()).isFocused(); + } +} + +/** Harness for interacting with an `ngAccordionGroup` in tests. */ +export class AccordionGroupHarness extends ComponentHarness { + static hostSelector = '[ngAccordionGroup]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for an accordion group with specific attributes. + */ + static with(options: AccordionGroupHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(AccordionGroupHarness, options); + } + + /** Gets all accordions within this group. */ + async getAccordions(filters: AccordionHarnessFilters = {}): Promise { + return this.locatorForAll(AccordionHarness.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..acb15bdb15bd --- /dev/null +++ b/src/aria/accordion/testing/public-api.ts @@ -0,0 +1,10 @@ +/** + * @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'; +export * from './accordion-harness-filters'; 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",