diff --git a/src/cdk/testing/component-harness.ts b/src/cdk/testing/component-harness.ts index 4fe49ab6e220..646ac6ef1edc 100644 --- a/src/cdk/testing/component-harness.ts +++ b/src/cdk/testing/component-harness.ts @@ -119,6 +119,17 @@ export interface HarnessLoader { */ getHarnessOrNull<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T | null>; + /** + * Searches for an instance of the component corresponding to the given harness type under the + * `HarnessLoader`'s root element, and returns a `ComponentHarness` for the instance on the page + * at the given index. If no matching component exists at that index, an error is thrown. + * @param query A query for a harness to create + * @param index The zero-indexed offset of the matching component instance to return + * @return An instance of the given harness type. + * @throws If a matching component instance can't be found at the given index. + */ + getHarnessAtIndex<T extends ComponentHarness>(query: HarnessQuery<T>, index: number): Promise<T>; + /** * Searches for all instances of the component corresponding to the given harness type under the * `HarnessLoader`'s root element, and returns a list `ComponentHarness` for each instance. @@ -127,6 +138,14 @@ export interface HarnessLoader { */ getAllHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T[]>; + /** + * Searches for all instances of the component corresponding to the given harness type under the + * `HarnessLoader`'s root element, and returns the total count of all matching components. + * @param query A query for a harness to create + * @return An integer indicating the number of instances that were found. + */ + countHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<number>; + /** * Searches for an instance of the component corresponding to the given harness type under the * `HarnessLoader`'s root element, and returns a boolean indicating if any were found. @@ -500,6 +519,20 @@ export abstract class ContentContainerComponentHarness<S extends string = string return (await this.getRootHarnessLoader()).getHarnessOrNull(query); } + /** + * Gets a matching harness for the given query and index within the current harness's content. + * @param query The harness query to search for. + * @param index The zero-indexed offset of the component to find. + * @returns The first harness matching the given query. + * @throws If no matching harness is found. + */ + async getHarnessAtIndex<T extends ComponentHarness>( + query: HarnessQuery<T>, + index: number, + ): Promise<T> { + return (await this.getRootHarnessLoader()).getHarnessAtIndex(query, index); + } + /** * Gets all matching harnesses for the given query within the current harness's content. * @param query The harness query to search for. @@ -509,12 +542,23 @@ export abstract class ContentContainerComponentHarness<S extends string = string return (await this.getRootHarnessLoader()).getAllHarnesses(query); } + /** + * Returns the number of matching harnesses for the given query within the current harness's + * content. + * + * @param query The harness query to search for. + * @returns The number of matching harnesses for the given query. + */ + async countHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<number> { + return (await this.getRootHarnessLoader()).countHarnesses(query); + } + /** * Checks whether there is a matching harnesses for the given query within the current harness's * content. * * @param query The harness query to search for. - * @returns Whetehr there is matching harnesses for the given query. + * @returns Whether there is matching harnesses for the given query. */ async hasHarness<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<boolean> { return (await this.getRootHarnessLoader()).hasHarness(query); diff --git a/src/cdk/testing/harness-environment.ts b/src/cdk/testing/harness-environment.ts index e03f479e17d0..a2fdb2228c40 100644 --- a/src/cdk/testing/harness-environment.ts +++ b/src/cdk/testing/harness-environment.ts @@ -248,6 +248,30 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac return this.locatorForOptional(query)(); } + /** + * Searches for an instance of the component corresponding to the given harness type and index + * under the `HarnessEnvironment`'s root element, and returns a `ComponentHarness` for that + * instance. The index specifies the offset of the component to find. If no matching + * component is found at that index, an error is thrown. + * @param query A query for a harness to create + * @param index The zero-indexed offset of the component to find + * @return An instance of the given harness type + * @throws If a matching component instance can't be found. + */ + async getHarnessAtIndex<T extends ComponentHarness>( + query: HarnessQuery<T>, + offset: number, + ): Promise<T> { + if (offset < 0) { + throw Error('Index must not be negative'); + } + const harnesses = await this.locatorForAll(query)(); + if (offset >= harnesses.length) { + throw Error(`No harness was located at index ${offset}`); + } + return harnesses[offset]; + } + /** * Searches for all instances of the component corresponding to the given harness type under the * `HarnessEnvironment`'s root element, and returns a list `ComponentHarness` for each instance. @@ -258,6 +282,16 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac return this.locatorForAll(query)(); } + /** + * Searches for all instance of the component corresponding to the given harness type under the + * `HarnessEnvironment`'s root element, and returns the number that were found. + * @param query A query for a harness to create + * @return The number of instances that were found. + */ + async countHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<number> { + return (await this.locatorForAll(query)()).length; + } + /** * Searches for an instance of the component corresponding to the given harness type under the * `HarnessEnvironment`'s root element, and returns a boolean indicating if any were found. diff --git a/src/cdk/testing/test-harnesses.md b/src/cdk/testing/test-harnesses.md index 9025d5b0ea8a..c2710b1eff6e 100644 --- a/src/cdk/testing/test-harnesses.md +++ b/src/cdk/testing/test-harnesses.md @@ -134,9 +134,12 @@ are used to create `ComponentHarness` instances for elements under this root ele | `getChildLoader(selector: string): Promise<HarnessLoader>` | Searches for an element matching the given selector below the root element of this `HarnessLoader`, and returns a new `HarnessLoader` rooted at the first matching element | | `getAllChildLoaders(selector: string): Promise<HarnessLoader[]>` | Acts like `getChildLoader`, but returns an array of `HarnessLoader` instances, one for each matching element, rather than just the first matching element | | `getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<T>` | Searches for an instance of the given `ComponentHarness` class or `HarnessPredicate` below the root element of this `HarnessLoader` and returns an instance of the harness corresponding to the first matching element | +| `getHarnessAtIndex<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>, index: number): Promise<T>` | Acts like `getHarness`, but returns an instance of the harness corresponding to the matching element with the given index (zero-indexed) | | `getAllHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<T[]>` | Acts like `getHarness`, but returns an array of harness instances, one for each matching element, rather than just the first matching element | +| `countHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<number>` | Counts the number of instances of the given `ComponentHarness` class or `HarnessPredicate` below the root element of this `HarnessLoader`, and returns the result. | +| `hasHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<boolean>` | Returns true if an instance of the given `ComponentHarness` class or `HarnessPredicate` exists below the root element of this `HarnessLoader` | -Calls to `getHarness` and `getAllHarnesses` can either take `ComponentHarness` subclass or a +Calls to the harness functions can either take `ComponentHarness` subclass or a `HarnessPredicate`. `HarnessPredicate` applies additional restrictions to the search (e.g. searching for a button that has some particular text, etc). The [details of `HarnessPredicate`](#filtering-harness-instances-with-harnesspredicate) are discussed in diff --git a/src/material/datepicker/testing/date-range-input-harness.spec.ts b/src/material/datepicker/testing/date-range-input-harness.spec.ts index 5396975fe711..cb1a9b5356d9 100644 --- a/src/material/datepicker/testing/date-range-input-harness.spec.ts +++ b/src/material/datepicker/testing/date-range-input-harness.spec.ts @@ -11,6 +11,7 @@ import { MatEndDate, MatStartDate, } from '../../datepicker'; +import {MatFormFieldModule} from '../../form-field'; import {MatCalendarHarness} from './calendar-harness'; import { MatDateRangeInputHarness, @@ -34,7 +35,14 @@ describe('matDateRangeInputHarness', () => { it('should load all date range input harnesses', async () => { const inputs = await loader.getAllHarnesses(MatDateRangeInputHarness); - expect(inputs.length).toBe(2); + expect(inputs.length).toBe(3); + }); + + it('should load date range input with a specific label', async () => { + const inputs = await loader.getAllHarnesses( + MatDateRangeInputHarness.with({label: 'Date range'}), + ); + expect(inputs.length).toBe(1); }); it('should get whether the input is disabled', async () => { @@ -261,6 +269,14 @@ describe('matDateRangeInputHarness', () => { <input matStartDate> <input matEndDate> </mat-date-range-input> + + <mat-form-field> + <mat-label>Date range</mat-label> + <mat-date-range-input basic> + <input matStartDate> + <input matEndDate> + </mat-date-range-input> + </mat-form-field> `, imports: [ MatNativeDateModule, @@ -268,6 +284,7 @@ describe('matDateRangeInputHarness', () => { MatStartDate, MatEndDate, MatDateRangePicker, + MatFormFieldModule, FormsModule, ], }) diff --git a/src/material/datepicker/testing/date-range-input-harness.ts b/src/material/datepicker/testing/date-range-input-harness.ts index c2a12d8dc040..037f6f9a5987 100644 --- a/src/material/datepicker/testing/date-range-input-harness.ts +++ b/src/material/datepicker/testing/date-range-input-harness.ts @@ -48,6 +48,8 @@ export class MatEndDateHarness extends MatDatepickerInputHarnessBase { export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase { static hostSelector = '.mat-date-range-input'; + private readonly floatingLabelSelector = '.mdc-floating-label'; + /** * Gets a `HarnessPredicate` that can be used to search for a `MatDateRangeInputHarness` * that meets certain criteria. @@ -57,11 +59,13 @@ export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase { static with( options: DateRangeInputHarnessFilters = {}, ): HarnessPredicate<MatDateRangeInputHarness> { - return new HarnessPredicate(MatDateRangeInputHarness, options).addOption( - 'value', - options.value, - (harness, value) => HarnessPredicate.stringMatches(harness.getValue(), value), - ); + return new HarnessPredicate(MatDateRangeInputHarness, options) + .addOption('value', options.value, (harness, value) => + HarnessPredicate.stringMatches(harness.getValue(), value), + ) + .addOption('label', options.label, (harness, label) => { + return HarnessPredicate.stringMatches(harness.getLabel(), label); + }); } /** Gets the combined value of the start and end inputs, including the separator. */ @@ -87,6 +91,31 @@ export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase { return this.locatorFor(MatEndDateHarness)(); } + /** Gets the floating label text for the range input, if it exists. */ + async getLabel(): Promise<string | null> { + // Copied from MatFormFieldControlHarnessBase since this class cannot extend two classes + const documentRootLocator = await this.documentRootLocatorFactory(); + const labelId = await (await this.host()).getAttribute('aria-labelledby'); + const hostId = await (await this.host()).getAttribute('id'); + + if (labelId) { + // First option, try to fetch the label using the `aria-labelledby` + // attribute. + const labelEl = await await documentRootLocator.locatorForOptional( + `${this.floatingLabelSelector}[id="${labelId}"]`, + )(); + return labelEl ? labelEl.text() : null; + } else if (hostId) { + // Fallback option, try to match the id of the input with the `for` + // attribute of the label. + const labelEl = await await documentRootLocator.locatorForOptional( + `${this.floatingLabelSelector}[for="${hostId}"]`, + )(); + return labelEl ? labelEl.text() : null; + } + return null; + } + /** Gets the separator text between the values of the two inputs. */ async getSeparator(): Promise<string> { return (await this.locatorFor('.mat-date-range-input-separator')()).text(); diff --git a/src/material/datepicker/testing/datepicker-harness-filters.ts b/src/material/datepicker/testing/datepicker-harness-filters.ts index 4245ad9625ef..90e750fd7ad2 100644 --- a/src/material/datepicker/testing/datepicker-harness-filters.ts +++ b/src/material/datepicker/testing/datepicker-harness-filters.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ +import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control'; import {BaseHarnessFilters} from '@angular/cdk/testing'; /** A set of criteria that can be used to filter a list of datepicker input instances. */ -export interface DatepickerInputHarnessFilters extends BaseHarnessFilters { +export interface DatepickerInputHarnessFilters extends MatFormFieldControlHarnessFilters { /** Filters based on the value of the input. */ value?: string | RegExp; /** Filters based on the placeholder text of the input. */ @@ -43,7 +44,7 @@ export interface CalendarCellHarnessFilters extends BaseHarnessFilters { } /** A set of criteria that can be used to filter a list of date range input instances. */ -export interface DateRangeInputHarnessFilters extends BaseHarnessFilters { +export interface DateRangeInputHarnessFilters extends MatFormFieldControlHarnessFilters { /** Filters based on the value of the input. */ value?: string | RegExp; } diff --git a/src/material/datepicker/testing/datepicker-input-harness-base.ts b/src/material/datepicker/testing/datepicker-input-harness-base.ts index 6e736928836f..e2a41fa70e76 100644 --- a/src/material/datepicker/testing/datepicker-input-harness-base.ts +++ b/src/material/datepicker/testing/datepicker-input-harness-base.ts @@ -7,7 +7,7 @@ */ import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing'; -import {MatFormFieldControlHarness} from '../../form-field/testing/control'; +import {MatFormFieldControlHarnessBase} from '@angular/material/form-field/testing/control'; import {DatepickerInputHarnessFilters} from './datepicker-harness-filters'; /** Sets up the filter predicates for a datepicker input harness. */ @@ -21,11 +21,14 @@ export function getInputPredicate<T extends MatDatepickerInputHarnessBase>( }) .addOption('placeholder', options.placeholder, (harness, placeholder) => { return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder); + }) + .addOption('label', options.label, (harness, label) => { + return HarnessPredicate.stringMatches(harness.getLabel(), label); }); } /** Base class for datepicker input harnesses. */ -export abstract class MatDatepickerInputHarnessBase extends MatFormFieldControlHarness { +export abstract class MatDatepickerInputHarnessBase extends MatFormFieldControlHarnessBase { /** Whether the input is disabled. */ async isDisabled(): Promise<boolean> { return (await this.host()).getProperty<boolean>('disabled'); diff --git a/src/material/datepicker/testing/datepicker-input-harness.spec.ts b/src/material/datepicker/testing/datepicker-input-harness.spec.ts index 8cf8739c62a8..72aa47c88353 100644 --- a/src/material/datepicker/testing/datepicker-input-harness.spec.ts +++ b/src/material/datepicker/testing/datepicker-input-harness.spec.ts @@ -5,6 +5,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; import {DateAdapter, MATERIAL_ANIMATIONS, MatNativeDateModule} from '../../core'; import {MatDatepickerModule} from '../../datepicker'; +import {MatFormFieldModule} from '../../form-field'; +import {MatInputModule} from '../../input'; import {MatCalendarHarness} from './calendar-harness'; import {MatDatepickerInputHarness} from './datepicker-input-harness'; @@ -27,6 +29,13 @@ describe('MatDatepickerInputHarness', () => { expect(inputs.length).toBe(2); }); + it('should load datepicker input with a specific label', async () => { + const selects = await loader.getAllHarnesses( + MatDatepickerInputHarness.with({label: 'Pick a date'}), + ); + expect(selects.length).toBe(1); + }); + it('should filter inputs based on their value', async () => { fixture.componentInstance.date = new Date(2020, 0, 1, 12, 0, 0); fixture.changeDetectorRef.markForCheck(); @@ -187,21 +196,31 @@ describe('MatDatepickerInputHarness', () => { @Component({ template: ` - <input - id="basic" - matInput - [matDatepicker]="picker" - (dateChange)="dateChangeCount = dateChangeCount + 1" - [(ngModel)]="date" - [min]="minDate" - [max]="maxDate" - [disabled]="disabled" - [required]="required" - placeholder="Type a date"> - <mat-datepicker #picker [touchUi]="touchUi"></mat-datepicker> + <mat-form-field> + <mat-label>Pick a date</mat-label> + <input + id="basic" + matInput + [matDatepicker]="picker" + (dateChange)="dateChangeCount = dateChangeCount + 1" + [(ngModel)]="date" + [min]="minDate" + [max]="maxDate" + [disabled]="disabled" + [required]="required" + placeholder="Type a date"> + <mat-datepicker #picker [touchUi]="touchUi"></mat-datepicker> + </mat-form-field> + <input id="no-datepicker" matDatepicker> `, - imports: [MatNativeDateModule, MatDatepickerModule, FormsModule], + imports: [ + MatNativeDateModule, + MatDatepickerModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + ], }) class DatepickerInputHarnessTest { date: Date | null = null; diff --git a/src/material/form-field/testing/control/form-field-control-harness-filters.ts b/src/material/form-field/testing/control/form-field-control-harness-filters.ts new file mode 100644 index 000000000000..2959983406ab --- /dev/null +++ b/src/material/form-field/testing/control/form-field-control-harness-filters.ts @@ -0,0 +1,18 @@ +/** + * @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 shared by any class derived from `MatFormFieldControlHarness`, that can be + * used to filter a list of those components. + */ +export interface MatFormFieldControlHarnessFilters extends BaseHarnessFilters { + /** Filters based on the text of the form field's floating label. */ + label?: string | RegExp; +} diff --git a/src/material/form-field/testing/control/form-field-control-harness.ts b/src/material/form-field/testing/control/form-field-control-harness.ts index 518f893a29dd..05edc1b89fe8 100644 --- a/src/material/form-field/testing/control/form-field-control-harness.ts +++ b/src/material/form-field/testing/control/form-field-control-harness.ts @@ -13,3 +13,34 @@ import {ComponentHarness} from '@angular/cdk/testing'; * custom controls with form-fields need to implement this interface. */ export abstract class MatFormFieldControlHarness extends ComponentHarness {} + +/** + * Shared behavior for `MatFormFieldControlHarness` implementations + */ +export abstract class MatFormFieldControlHarnessBase extends MatFormFieldControlHarness { + private readonly floatingLabelSelector = '.mdc-floating-label'; + + /** Gets the text content of the floating label, if it exists. */ + public async getLabel(): Promise<string | null> { + const documentRootLocator = await this.documentRootLocatorFactory(); + const labelId = await (await this.host()).getAttribute('aria-labelledby'); + const hostId = await (await this.host()).getAttribute('id'); + + if (labelId) { + // First option, try to fetch the label using the `aria-labelledby` + // attribute. + const labelEl = await await documentRootLocator.locatorForOptional( + `${this.floatingLabelSelector}[id="${labelId}"]`, + )(); + return labelEl ? labelEl.text() : null; + } else if (hostId) { + // Fallback option, try to match the id of the input with the `for` + // attribute of the label. + const labelEl = await await documentRootLocator.locatorForOptional( + `${this.floatingLabelSelector}[for="${hostId}"]`, + )(); + return labelEl ? labelEl.text() : null; + } + return null; + } +} diff --git a/src/material/form-field/testing/control/index.ts b/src/material/form-field/testing/control/index.ts index e289ebb6c6f2..4454049bc88a 100644 --- a/src/material/form-field/testing/control/index.ts +++ b/src/material/form-field/testing/control/index.ts @@ -7,3 +7,4 @@ */ export * from './form-field-control-harness'; +export * from './form-field-control-harness-filters'; diff --git a/src/material/input/testing/input-harness-filters.ts b/src/material/input/testing/input-harness-filters.ts index c68b0d626708..1635877e6ae9 100644 --- a/src/material/input/testing/input-harness-filters.ts +++ b/src/material/input/testing/input-harness-filters.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import {BaseHarnessFilters} from '@angular/cdk/testing'; +import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control'; /** A set of criteria that can be used to filter a list of `MatInputHarness` instances. */ -export interface InputHarnessFilters extends BaseHarnessFilters { +export interface InputHarnessFilters extends MatFormFieldControlHarnessFilters { /** Filters based on the value of the input. */ value?: string | RegExp; /** Filters based on the placeholder text of the input. */ diff --git a/src/material/input/testing/input-harness.spec.ts b/src/material/input/testing/input-harness.spec.ts index e4d95ee6f4de..3d93514de9fa 100644 --- a/src/material/input/testing/input-harness.spec.ts +++ b/src/material/input/testing/input-harness.spec.ts @@ -18,8 +18,7 @@ describe('MatInputHarness', () => { }); it('should load all input harnesses', async () => { - const inputs = await loader.getAllHarnesses(MatInputHarness); - expect(inputs.length).toBe(7); + expect(await loader.countHarnesses(MatInputHarness)).toBe(7); }); it('should load input with specific id', async () => { @@ -39,6 +38,11 @@ describe('MatInputHarness', () => { expect(inputs.length).toBe(1); }); + it('should load input with a specific label', async () => { + const inputs = await loader.getAllHarnesses(MatInputHarness.with({label: 'Favorite food'})); + expect(inputs.length).toBe(1); + }); + it('should load input with a value that matches a regex', async () => { const inputs = await loader.getAllHarnesses(MatInputHarness.with({value: /shi$/})); expect(inputs.length).toBe(1); @@ -231,6 +235,7 @@ describe('MatInputHarness', () => { @Component({ template: ` <mat-form-field> + <mat-label>Favorite food</mat-label> <input matInput placeholder="Favorite food" value="Sushi" name="favorite-food"> </mat-form-field> diff --git a/src/material/input/testing/input-harness.ts b/src/material/input/testing/input-harness.ts index 61e087f01cbb..c88101a32ad9 100644 --- a/src/material/input/testing/input-harness.ts +++ b/src/material/input/testing/input-harness.ts @@ -7,12 +7,12 @@ */ import {HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {MatFormFieldControlHarness} from '../../form-field/testing/control'; +import {MatFormFieldControlHarnessBase} from '@angular/material/form-field/testing/control'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {InputHarnessFilters} from './input-harness-filters'; /** Harness for interacting with a standard Material inputs in tests. */ -export class MatInputHarness extends MatFormFieldControlHarness { +export class MatInputHarness extends MatFormFieldControlHarnessBase { // TODO: We do not want to handle `select` elements with `matNativeControl` because // not all methods of this harness work reasonably for native select elements. // For more details. See: https://github.com/angular/components/pull/18221. @@ -31,6 +31,9 @@ export class MatInputHarness extends MatFormFieldControlHarness { }) .addOption('placeholder', options.placeholder, (harness, placeholder) => { return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder); + }) + .addOption('label', options.label, (harness, label) => { + return HarnessPredicate.stringMatches(harness.getLabel(), label); }); } diff --git a/src/material/input/testing/native-select-harness-filters.ts b/src/material/input/testing/native-select-harness-filters.ts index 438549ff0cb4..e44566192d32 100644 --- a/src/material/input/testing/native-select-harness-filters.ts +++ b/src/material/input/testing/native-select-harness-filters.ts @@ -7,9 +7,10 @@ */ import {BaseHarnessFilters} from '@angular/cdk/testing'; +import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control'; /** A set of criteria that can be used to filter a list of `MatNativeSelectHarness` instances. */ -export interface NativeSelectHarnessFilters extends BaseHarnessFilters {} +export interface NativeSelectHarnessFilters extends MatFormFieldControlHarnessFilters {} /** A set of criteria that can be used to filter a list of `MatNativeOptionHarness` instances. */ export interface NativeOptionHarnessFilters extends BaseHarnessFilters { diff --git a/src/material/input/testing/native-select-harness.spec.ts b/src/material/input/testing/native-select-harness.spec.ts index 856b3c2739ef..a3492f0e2460 100644 --- a/src/material/input/testing/native-select-harness.spec.ts +++ b/src/material/input/testing/native-select-harness.spec.ts @@ -21,6 +21,13 @@ describe('MatNativeSelectHarness', () => { expect(selects.length).toBe(2); }); + it('should load select with a specific label', async () => { + const inputs = await loader.getAllHarnesses( + MatNativeSelectHarness.with({label: 'Favorite food'}), + ); + expect(inputs.length).toBe(1); + }); + it('should get the id of a select', async () => { const selects = await loader.getAllHarnesses(MatNativeSelectHarness); expect(await parallel(() => selects.map(select => select.getId()))).toEqual(['food', 'drink']); @@ -187,6 +194,7 @@ describe('MatNativeSelectHarness', () => { @Component({ template: ` <mat-form-field> + <mat-label>Favorite food</mat-label> <select id="food" matNativeControl diff --git a/src/material/input/testing/native-select-harness.ts b/src/material/input/testing/native-select-harness.ts index 3641d08b3438..7b0b17b175b1 100644 --- a/src/material/input/testing/native-select-harness.ts +++ b/src/material/input/testing/native-select-harness.ts @@ -7,7 +7,7 @@ */ import {HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {MatFormFieldControlHarness} from '../../form-field/testing/control'; +import {MatFormFieldControlHarnessBase} from '../../form-field/testing/control'; import {MatNativeOptionHarness} from './native-option-harness'; import { NativeOptionHarnessFilters, @@ -15,7 +15,7 @@ import { } from './native-select-harness-filters'; /** Harness for interacting with a native `select` in tests. */ -export class MatNativeSelectHarness extends MatFormFieldControlHarness { +export class MatNativeSelectHarness extends MatFormFieldControlHarnessBase { static hostSelector = 'select[matNativeControl]'; /** @@ -25,7 +25,13 @@ export class MatNativeSelectHarness extends MatFormFieldControlHarness { * @return a `HarnessPredicate` configured with the given options. */ static with(options: NativeSelectHarnessFilters = {}): HarnessPredicate<MatNativeSelectHarness> { - return new HarnessPredicate(MatNativeSelectHarness, options); + return new HarnessPredicate(MatNativeSelectHarness, options).addOption( + 'label', + options.label, + (harness, label) => { + return HarnessPredicate.stringMatches(harness.getLabel(), label); + }, + ); } /** Gets a boolean promise indicating if the select is disabled. */ diff --git a/src/material/select/testing/select-harness-filters.ts b/src/material/select/testing/select-harness-filters.ts index db83564fc513..34e219a456d0 100644 --- a/src/material/select/testing/select-harness-filters.ts +++ b/src/material/select/testing/select-harness-filters.ts @@ -7,9 +7,10 @@ */ import {BaseHarnessFilters} from '@angular/cdk/testing'; +import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control'; /** A set of criteria that can be used to filter a list of `MatSelectHarness` instances. */ -export interface SelectHarnessFilters extends BaseHarnessFilters { +export interface SelectHarnessFilters extends MatFormFieldControlHarnessFilters { /** Only find instances which match the given disabled state. */ disabled?: boolean; } diff --git a/src/material/select/testing/select-harness.spec.ts b/src/material/select/testing/select-harness.spec.ts index df198b2eade4..ff4fc2165a3f 100644 --- a/src/material/select/testing/select-harness.spec.ts +++ b/src/material/select/testing/select-harness.spec.ts @@ -52,6 +52,11 @@ describe('MatSelectHarness', () => { expect(disabledSelects.length).toBe(1); }); + it('should load select with a specific label', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness.with({label: 'US States'})); + expect(selects.length).toBe(1); + }); + it('should be able to check whether a select is in multi-selection mode', async () => { const singleSelection = await loader.getHarness( MatSelectHarness.with({ @@ -265,6 +270,7 @@ describe('MatSelectHarness', () => { @Component({ template: ` <mat-form-field> + <mat-label>US States</mat-label> <mat-select [disabled]="isDisabled()" [required]="isRequired()" id="single-selection"> @for (state of states; track state) { <mat-option [value]="state.code">{{ state.name }}</mat-option> diff --git a/src/material/select/testing/select-harness.ts b/src/material/select/testing/select-harness.ts index 8c577d63cc99..36d10d4ed5b0 100644 --- a/src/material/select/testing/select-harness.ts +++ b/src/material/select/testing/select-harness.ts @@ -12,12 +12,12 @@ import { MatOptgroupHarness, OptionHarnessFilters, OptgroupHarnessFilters, -} from '../../core/testing'; -import {MatFormFieldControlHarness} from '../../form-field/testing/control'; +} from '@angular/material/core/testing'; +import {MatFormFieldControlHarnessBase} from '@angular/material/form-field/testing/control'; import {SelectHarnessFilters} from './select-harness-filters'; /** Harness for interacting with a mat-select in tests. */ -export class MatSelectHarness extends MatFormFieldControlHarness { +export class MatSelectHarness extends MatFormFieldControlHarnessBase { static hostSelector = '.mat-mdc-select'; private _prefix = 'mat-mdc'; private _optionClass = MatOptionHarness; @@ -34,13 +34,13 @@ export class MatSelectHarness extends MatFormFieldControlHarness { this: ComponentHarnessConstructor<T>, options: SelectHarnessFilters = {}, ): HarnessPredicate<T> { - return new HarnessPredicate(this, options).addOption( - 'disabled', - options.disabled, - async (harness, disabled) => { + return new HarnessPredicate(this, options) + .addOption('disabled', options.disabled, async (harness, disabled) => { return (await harness.isDisabled()) === disabled; - }, - ); + }) + .addOption('label', options.label, (harness, label) => { + return HarnessPredicate.stringMatches(harness.getLabel(), label); + }); } /** Gets a boolean promise indicating if the select is disabled. */