From cdc984e2253c2546cfcde75d3e511d14617c82e1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 10 Apr 2026 12:22:59 +0200 Subject: [PATCH] feat(aria/toolbar): add test harnesses Sets up test harnesses for the Aria toolbar. --- goldens/aria/toolbar/testing/index.api.md | 58 +++++++++ src/aria/config.bzl | 1 + src/aria/toolbar/testing/BUILD.bazel | 39 ++++++ src/aria/toolbar/testing/index.ts | 9 ++ src/aria/toolbar/testing/public-api.ts | 12 ++ .../testing/toolbar-harness-filters.ts | 24 ++++ .../toolbar/testing/toolbar-harness.spec.ts | 123 ++++++++++++++++++ src/aria/toolbar/testing/toolbar-harness.ts | 55 ++++++++ .../testing/toolbar-widget-group-harness.ts | 36 +++++ .../toolbar/testing/toolbar-widget-harness.ts | 55 ++++++++ src/aria/toolbar/toolbar-widget.ts | 2 + 11 files changed, 414 insertions(+) create mode 100644 goldens/aria/toolbar/testing/index.api.md create mode 100644 src/aria/toolbar/testing/BUILD.bazel create mode 100644 src/aria/toolbar/testing/index.ts create mode 100644 src/aria/toolbar/testing/public-api.ts create mode 100644 src/aria/toolbar/testing/toolbar-harness-filters.ts create mode 100644 src/aria/toolbar/testing/toolbar-harness.spec.ts create mode 100644 src/aria/toolbar/testing/toolbar-harness.ts create mode 100644 src/aria/toolbar/testing/toolbar-widget-group-harness.ts create mode 100644 src/aria/toolbar/testing/toolbar-widget-harness.ts diff --git a/goldens/aria/toolbar/testing/index.api.md b/goldens/aria/toolbar/testing/index.api.md new file mode 100644 index 000000000000..89f1e49fa675 --- /dev/null +++ b/goldens/aria/toolbar/testing/index.api.md @@ -0,0 +1,58 @@ +## API Report File for "@angular/aria_toolbar_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 { ContentContainerComponentHarness } from '@angular/cdk/testing'; +import { HarnessPredicate } from '@angular/cdk/testing'; + +// @public +export class ToolbarHarness extends ComponentHarness { + getOrientation(): Promise<'vertical' | 'horizontal'>; + getWidgetGroups(filters?: ToolbarWidgetGroupHarnessFilters): Promise; + getWidgets(filters?: ToolbarWidgetHarnessFilters): Promise; + // (undocumented) + static hostSelector: string; + isDisabled(): Promise; + static with(options?: ToolbarHarnessFilters): HarnessPredicate; +} + +// @public +export interface ToolbarHarnessFilters extends BaseHarnessFilters { +} + +// @public +export class ToolbarWidgetGroupHarness extends ComponentHarness { + getWidgets(filters?: ToolbarWidgetHarnessFilters): Promise; + // (undocumented) + static hostSelector: string; + static with(options?: ToolbarWidgetGroupHarnessFilters): HarnessPredicate; +} + +// @public +export interface ToolbarWidgetGroupHarnessFilters extends BaseHarnessFilters { +} + +// @public +export class ToolbarWidgetHarness extends ContentContainerComponentHarness { + click(): Promise; + getText(): Promise; + // (undocumented) + static hostSelector: string; + isActive(): Promise; + isDisabled(): Promise; + static with(options?: ToolbarWidgetHarnessFilters): HarnessPredicate; +} + +// @public +export interface ToolbarWidgetHarnessFilters extends BaseHarnessFilters { + active?: boolean; + text?: string | RegExp; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/src/aria/config.bzl b/src/aria/config.bzl index 291412b5a3fb..533b83f17bba 100644 --- a/src/aria/config.bzl +++ b/src/aria/config.bzl @@ -7,6 +7,7 @@ ARIA_ENTRYPOINTS = [ "menu", "tabs", "toolbar", + "toolbar/testing", "tree", "private", ] diff --git a/src/aria/toolbar/testing/BUILD.bazel b/src/aria/toolbar/testing/BUILD.bazel new file mode 100644 index 000000000000..9aa18a3ffd5a --- /dev/null +++ b/src/aria/toolbar/testing/BUILD.bazel @@ -0,0 +1,39 @@ +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 = [ + "//src/aria/toolbar", + "//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", + "//src/aria/toolbar", + "//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/toolbar/testing/index.ts b/src/aria/toolbar/testing/index.ts new file mode 100644 index 000000000000..52b3c7a5156f --- /dev/null +++ b/src/aria/toolbar/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/toolbar/testing/public-api.ts b/src/aria/toolbar/testing/public-api.ts new file mode 100644 index 000000000000..b9722d2d434c --- /dev/null +++ b/src/aria/toolbar/testing/public-api.ts @@ -0,0 +1,12 @@ +/** + * @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 './toolbar-widget-harness'; +export * from './toolbar-widget-group-harness'; +export * from './toolbar-harness'; +export * from './toolbar-harness-filters'; diff --git a/src/aria/toolbar/testing/toolbar-harness-filters.ts b/src/aria/toolbar/testing/toolbar-harness-filters.ts new file mode 100644 index 000000000000..e71fee33a16a --- /dev/null +++ b/src/aria/toolbar/testing/toolbar-harness-filters.ts @@ -0,0 +1,24 @@ +/** + * @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'; + +/** A set of criteria that can be used to filter a list of Aria toolbar instances. */ +export interface ToolbarHarnessFilters extends BaseHarnessFilters {} + +/** A set of criteria that can be used to filter a list of Aria toolbar widget group instances. */ +export interface ToolbarWidgetGroupHarnessFilters extends BaseHarnessFilters {} + +/** A set of criteria that can be used to filter a list of Aria toolbar widgets. */ +export interface ToolbarWidgetHarnessFilters extends BaseHarnessFilters { + /** Text that the widget should match. */ + text?: string | RegExp; + + /** Active state that the widget should match. */ + active?: boolean; +} diff --git a/src/aria/toolbar/testing/toolbar-harness.spec.ts b/src/aria/toolbar/testing/toolbar-harness.spec.ts new file mode 100644 index 000000000000..3f1885dcb937 --- /dev/null +++ b/src/aria/toolbar/testing/toolbar-harness.spec.ts @@ -0,0 +1,123 @@ +import {Component, signal} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader, parallel} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {Toolbar} from '../toolbar'; +import {ToolbarWidget} from '../toolbar-widget'; +import {ToolbarWidgetGroup} from '../toolbar-widget-group'; +import {ToolbarHarness} from './toolbar-harness'; +import {ToolbarWidgetGroupHarness} from './toolbar-widget-group-harness'; +import {ToolbarWidgetHarness} from './toolbar-widget-harness'; + +describe('ToolbarHarness', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(() => { + fixture = TestBed.createComponent(ToolbarHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + }); + + it('should be able to load toolbar harnesses', async () => { + const harnesses = await loader.getAllHarnesses(ToolbarHarness); + expect(harnesses.length).toBe(1); + }); + + it('should be able to load toolbar widget group harnesses', async () => { + const harnesses = await loader.getAllHarnesses(ToolbarWidgetGroupHarness); + expect(harnesses.length).toBe(2); + }); + + it('should be able to load toolbar widget harnesses', async () => { + const harnesses = await loader.getAllHarnesses(ToolbarWidgetHarness); + expect(harnesses.length).toBe(8); + }); + + it('should be able to get the widgets in a toolbar', async () => { + const toolbar = await loader.getHarness(ToolbarHarness); + const widgets = await toolbar.getWidgets(); + expect(widgets.length).toBe(8); + }); + + it('should be able to get the widget groups in a toolbar', async () => { + const toolbar = await loader.getHarness(ToolbarHarness); + const groups = await toolbar.getWidgetGroups(); + expect(groups.length).toBe(2); + }); + + it('should be able to get the toolbar orientation', async () => { + const toolbar = await loader.getHarness(ToolbarHarness); + expect(await toolbar.getOrientation()).toBe('horizontal'); + + fixture.componentInstance.orientation.set('vertical'); + expect(await toolbar.getOrientation()).toBe('vertical'); + }); + + it('should be able to get whether the toolbar is disabled', async () => { + const toolbar = await loader.getHarness(ToolbarHarness); + expect(await toolbar.isDisabled()).toBe(false); + + fixture.componentInstance.toolbarDisabled.set(true); + expect(await toolbar.isDisabled()).toBe(true); + }); + + it('should be able to get the widgets in a widget group', async () => { + const group = await loader.getHarness(ToolbarWidgetGroupHarness); + const widgets = await group.getWidgets(); + expect(widgets.length).toBe(3); + }); + + it('should be able to load a widget harness by text', async () => { + const harnesses = await loader.getAllHarnesses(ToolbarWidgetHarness.with({text: /^Align/})); + expect(harnesses.length).toBe(3); + expect(await parallel(() => harnesses.map(harness => harness.getText()))).toEqual([ + 'Align left', + 'Align center', + 'Align right', + ]); + }); + + it('should be able to toggle the active state of a widget', async () => { + const widget = await loader.getHarness(ToolbarWidgetHarness.with({text: 'Align left'})); + expect(await widget.isActive()).toBe(false); + + await widget.click(); + expect(await widget.isActive()).toBe(true); + }); + + it('should be able to get whether a widget is disabled', async () => { + const widget = await loader.getHarness(ToolbarWidgetHarness.with({text: 'Undo'})); + expect(await widget.isDisabled()).toBe(false); + + fixture.componentInstance.undoDisabled.set(true); + expect(await widget.isDisabled()).toBe(true); + }); +}); + +@Component({ + template: ` +
+ + + +
+ + + +
+ +
+ + + +
+
+ `, + imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup], +}) +class ToolbarHarnessTest { + orientation = signal<'vertical' | 'horizontal'>('horizontal'); + toolbarDisabled = signal(false); + undoDisabled = signal(false); +} diff --git a/src/aria/toolbar/testing/toolbar-harness.ts b/src/aria/toolbar/testing/toolbar-harness.ts new file mode 100644 index 000000000000..35655384e4f5 --- /dev/null +++ b/src/aria/toolbar/testing/toolbar-harness.ts @@ -0,0 +1,55 @@ +/** + * @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} from '@angular/cdk/testing'; +import { + ToolbarHarnessFilters, + ToolbarWidgetHarnessFilters, + ToolbarWidgetGroupHarnessFilters, +} from './toolbar-harness-filters'; +import {ToolbarWidgetHarness} from './toolbar-widget-harness'; +import {ToolbarWidgetGroupHarness} from './toolbar-widget-group-harness'; + +/** Harness for interacting with an Aria toolbar in tests. */ +export class ToolbarHarness extends ComponentHarness { + static hostSelector = '[ngToolbar]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `ToolbarHarness` + * that meets certain criteria. + * @param options Options for filtering which dialog instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: ToolbarHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(ToolbarHarness, options); + } + + /** Gets all widgets in the toolbar. */ + async getWidgets(filters: ToolbarWidgetHarnessFilters = {}): Promise { + return await this.locatorForAll(ToolbarWidgetHarness.with(filters))(); + } + + /** Gets all widget groups in the toolbar. */ + async getWidgetGroups( + filters: ToolbarWidgetGroupHarnessFilters = {}, + ): Promise { + return await this.locatorForAll(ToolbarWidgetGroupHarness.with(filters))(); + } + + /** Gets whether the toolbar is disabled. */ + async isDisabled(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-disabled')) === 'true'; + } + + /** Gets the orientation of the toolbar. */ + async getOrientation(): Promise<'vertical' | 'horizontal'> { + const host = await this.host(); + return (await host.getAttribute('aria-orientation')) as 'vertical' | 'horizontal'; + } +} diff --git a/src/aria/toolbar/testing/toolbar-widget-group-harness.ts b/src/aria/toolbar/testing/toolbar-widget-group-harness.ts new file mode 100644 index 000000000000..f1eaa7ee8986 --- /dev/null +++ b/src/aria/toolbar/testing/toolbar-widget-group-harness.ts @@ -0,0 +1,36 @@ +/** + * @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} from '@angular/cdk/testing'; +import { + ToolbarWidgetHarnessFilters, + ToolbarWidgetGroupHarnessFilters, +} from './toolbar-harness-filters'; +import {ToolbarWidgetHarness} from './toolbar-widget-harness'; + +/** Harness for interacting with an Aria toolbar widget group in tests. */ +export class ToolbarWidgetGroupHarness extends ComponentHarness { + static hostSelector = '[ngToolbarWidgetGroup]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `ToolbarWidgetGroupHarness` + * that meets certain criteria. + * @param options Options for filtering which dialog instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with( + options: ToolbarWidgetGroupHarnessFilters = {}, + ): HarnessPredicate { + return new HarnessPredicate(ToolbarWidgetGroupHarness, options); + } + + /** Gets all widgets in the group. */ + async getWidgets(filters: ToolbarWidgetHarnessFilters = {}): Promise { + return await this.locatorForAll(ToolbarWidgetHarness.with(filters))(); + } +} diff --git a/src/aria/toolbar/testing/toolbar-widget-harness.ts b/src/aria/toolbar/testing/toolbar-widget-harness.ts new file mode 100644 index 000000000000..6825aad100bf --- /dev/null +++ b/src/aria/toolbar/testing/toolbar-widget-harness.ts @@ -0,0 +1,55 @@ +/** + * @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 {ContentContainerComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {ToolbarWidgetHarnessFilters} from './toolbar-harness-filters'; + +/** Harness for interacting with an Aria toolbar widget in tests. */ +export class ToolbarWidgetHarness extends ContentContainerComponentHarness { + static hostSelector = '[ngToolbarWidget]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `ToolbarWidgetHarness` + * that meets certain criteria. + * @param options Options for filtering which dialog instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: ToolbarWidgetHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(ToolbarWidgetHarness, options) + .addOption('text', options.text, (harness, pattern) => + HarnessPredicate.stringMatches(harness.getText(), pattern), + ) + .addOption( + 'active', + options.active, + async (harness, active) => (await harness.isActive()) === active, + ); + } + + /** Gets the widget's text. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Clicks the widget. */ + async click(): Promise { + return (await this.host()).click(); + } + + /** Gets whether the widget is active. */ + async isActive(): Promise { + const host = await this.host(); + return (await host.getAttribute('data-active')) === 'true'; + } + + /** Gets whether the widget is disabled. */ + async isDisabled(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-disabled')) === 'true'; + } +} diff --git a/src/aria/toolbar/toolbar-widget.ts b/src/aria/toolbar/toolbar-widget.ts index 76d632952607..5b4fe4fdae1a 100644 --- a/src/aria/toolbar/toolbar-widget.ts +++ b/src/aria/toolbar/toolbar-widget.ts @@ -48,6 +48,8 @@ import type {ToolbarWidgetGroup} from './toolbar-widget-group'; selector: '[ngToolbarWidget]', exportAs: 'ngToolbarWidget', host: { + // Ensure the attribute is applied even when used as a host directive. + 'ngToolbarWidget': '', '[attr.data-active]': 'active()', '[attr.tabindex]': '_pattern.tabIndex()', '[attr.inert]': 'hardDisabled() ? true : null',