Skip to content

Commit 9aa24d7

Browse files
committed
feat(material/testing): Add 'label' filter to form field control
harnesses This facilitates testing by making it possible to fetch certain harnesses using their floating label text (mat-label). Previously, the user would have to locate the harness using an id or class, or by calling MatFormFieldHarness.getControl(). This affects the following harnesses: - MatInputHarness - MatSelectHarness - MatNativeSelectHarness - MatDatepickerInputHarness - MatDateRangeInputHarness Tests via unit tests
1 parent 0e39170 commit 9aa24d7

17 files changed

+204
-37
lines changed

src/material/datepicker/testing/date-range-input-harness.spec.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MatEndDate,
1212
MatStartDate,
1313
} from '../../datepicker';
14+
import {MatFormFieldModule} from '../../form-field';
1415
import {MatCalendarHarness} from './calendar-harness';
1516
import {
1617
MatDateRangeInputHarness,
@@ -34,7 +35,14 @@ describe('matDateRangeInputHarness', () => {
3435

3536
it('should load all date range input harnesses', async () => {
3637
const inputs = await loader.getAllHarnesses(MatDateRangeInputHarness);
37-
expect(inputs.length).toBe(2);
38+
expect(inputs.length).toBe(3);
39+
});
40+
41+
it('should load date range input with a specific label', async () => {
42+
const inputs = await loader.getAllHarnesses(
43+
MatDateRangeInputHarness.with({label: 'Date range'}),
44+
);
45+
expect(inputs.length).toBe(1);
3846
});
3947

4048
it('should get whether the input is disabled', async () => {
@@ -261,13 +269,22 @@ describe('matDateRangeInputHarness', () => {
261269
<input matStartDate>
262270
<input matEndDate>
263271
</mat-date-range-input>
272+
273+
<mat-form-field>
274+
<mat-label>Date range</mat-label>
275+
<mat-date-range-input basic>
276+
<input matStartDate>
277+
<input matEndDate>
278+
</mat-date-range-input>
279+
</mat-form-field>
264280
`,
265281
imports: [
266282
MatNativeDateModule,
267283
MatDateRangeInput,
268284
MatStartDate,
269285
MatEndDate,
270286
MatDateRangePicker,
287+
MatFormFieldModule,
271288
FormsModule,
272289
],
273290
})

src/material/datepicker/testing/date-range-input-harness.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export class MatEndDateHarness extends MatDatepickerInputHarnessBase {
4848
export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase {
4949
static hostSelector = '.mat-date-range-input';
5050

51+
private readonly floatingLabelSelector = '.mdc-floating-label';
52+
5153
/**
5254
* Gets a `HarnessPredicate` that can be used to search for a `MatDateRangeInputHarness`
5355
* that meets certain criteria.
@@ -57,11 +59,13 @@ export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase {
5759
static with(
5860
options: DateRangeInputHarnessFilters = {},
5961
): HarnessPredicate<MatDateRangeInputHarness> {
60-
return new HarnessPredicate(MatDateRangeInputHarness, options).addOption(
61-
'value',
62-
options.value,
63-
(harness, value) => HarnessPredicate.stringMatches(harness.getValue(), value),
64-
);
62+
return new HarnessPredicate(MatDateRangeInputHarness, options)
63+
.addOption('value', options.value, (harness, value) =>
64+
HarnessPredicate.stringMatches(harness.getValue(), value),
65+
)
66+
.addOption('label', options.label, (harness, label) => {
67+
return HarnessPredicate.stringMatches(harness.getLabel(), label);
68+
});
6569
}
6670

6771
/** Gets the combined value of the start and end inputs, including the separator. */
@@ -87,6 +91,31 @@ export class MatDateRangeInputHarness extends DatepickerTriggerHarnessBase {
8791
return this.locatorFor(MatEndDateHarness)();
8892
}
8993

94+
/** Gets the floating label text for the range input, if it exists. */
95+
async getLabel(): Promise<string | null> {
96+
// Copied from MatFormFieldControlHarness since this class cannot extend two classes
97+
const documentRootLocator = await this.documentRootLocatorFactory();
98+
const labelId = await (await this.host()).getAttribute('aria-labelledby');
99+
const hostId = await (await this.host()).getAttribute('id');
100+
101+
if (labelId) {
102+
// First option, try to fetch the label using the `aria-labelledby`
103+
// attribute.
104+
const labelEl = await await documentRootLocator.locatorForOptional(
105+
`${this.floatingLabelSelector}[id="${labelId}"]`,
106+
)();
107+
return labelEl ? labelEl.text() : null;
108+
} else if (hostId) {
109+
// Fallback option, try to match the id of the input with the `for`
110+
// attribute of the label.
111+
const labelEl = await await documentRootLocator.locatorForOptional(
112+
`${this.floatingLabelSelector}[for="${hostId}"]`,
113+
)();
114+
return labelEl ? labelEl.text() : null;
115+
}
116+
return null;
117+
}
118+
90119
/** Gets the separator text between the values of the two inputs. */
91120
async getSeparator(): Promise<string> {
92121
return (await this.locatorFor('.mat-date-range-input-separator')()).text();

src/material/datepicker/testing/datepicker-harness-filters.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control';
910
import {BaseHarnessFilters} from '@angular/cdk/testing';
1011

1112
/** A set of criteria that can be used to filter a list of datepicker input instances. */
12-
export interface DatepickerInputHarnessFilters extends BaseHarnessFilters {
13+
export interface DatepickerInputHarnessFilters extends MatFormFieldControlHarnessFilters {
1314
/** Filters based on the value of the input. */
1415
value?: string | RegExp;
1516
/** Filters based on the placeholder text of the input. */
@@ -43,7 +44,7 @@ export interface CalendarCellHarnessFilters extends BaseHarnessFilters {
4344
}
4445

4546
/** A set of criteria that can be used to filter a list of date range input instances. */
46-
export interface DateRangeInputHarnessFilters extends BaseHarnessFilters {
47+
export interface DateRangeInputHarnessFilters extends MatFormFieldControlHarnessFilters {
4748
/** Filters based on the value of the input. */
4849
value?: string | RegExp;
4950
}

src/material/datepicker/testing/datepicker-input-harness-base.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';
10-
import {MatFormFieldControlHarness} from '../../form-field/testing/control';
10+
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
1111
import {DatepickerInputHarnessFilters} from './datepicker-harness-filters';
1212

1313
/** Sets up the filter predicates for a datepicker input harness. */
@@ -21,6 +21,9 @@ export function getInputPredicate<T extends MatDatepickerInputHarnessBase>(
2121
})
2222
.addOption('placeholder', options.placeholder, (harness, placeholder) => {
2323
return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder);
24+
})
25+
.addOption('label', options.label, (harness, label) => {
26+
return HarnessPredicate.stringMatches(harness.getLabel(), label);
2427
});
2528
}
2629

@@ -36,6 +39,11 @@ export abstract class MatDatepickerInputHarnessBase extends MatFormFieldControlH
3639
return (await this.host()).getProperty<boolean>('required');
3740
}
3841

42+
/** Gets the floating label text for the input, if it exists. */
43+
async getLabel(): Promise<string | null> {
44+
return await this._getFloatingLabelText();
45+
}
46+
3947
/** Gets the value of the input. */
4048
async getValue(): Promise<string> {
4149
// The "value" property of the native input is always defined.

src/material/datepicker/testing/datepicker-input-harness.spec.ts

+32-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
55
import {FormsModule} from '@angular/forms';
66
import {DateAdapter, MATERIAL_ANIMATIONS, MatNativeDateModule} from '../../core';
77
import {MatDatepickerModule} from '../../datepicker';
8+
import {MatFormFieldModule} from '../../form-field';
9+
import {MatInputModule} from '../../input';
810
import {MatCalendarHarness} from './calendar-harness';
911
import {MatDatepickerInputHarness} from './datepicker-input-harness';
1012

@@ -27,6 +29,13 @@ describe('MatDatepickerInputHarness', () => {
2729
expect(inputs.length).toBe(2);
2830
});
2931

32+
it('should load datepicker input with a specific label', async () => {
33+
const selects = await loader.getAllHarnesses(
34+
MatDatepickerInputHarness.with({label: 'Pick a date'}),
35+
);
36+
expect(selects.length).toBe(1);
37+
});
38+
3039
it('should filter inputs based on their value', async () => {
3140
fixture.componentInstance.date = new Date(2020, 0, 1, 12, 0, 0);
3241
fixture.changeDetectorRef.markForCheck();
@@ -187,21 +196,31 @@ describe('MatDatepickerInputHarness', () => {
187196

188197
@Component({
189198
template: `
190-
<input
191-
id="basic"
192-
matInput
193-
[matDatepicker]="picker"
194-
(dateChange)="dateChangeCount = dateChangeCount + 1"
195-
[(ngModel)]="date"
196-
[min]="minDate"
197-
[max]="maxDate"
198-
[disabled]="disabled"
199-
[required]="required"
200-
placeholder="Type a date">
201-
<mat-datepicker #picker [touchUi]="touchUi"></mat-datepicker>
199+
<mat-form-field>
200+
<mat-label>Pick a date</mat-label>
201+
<input
202+
id="basic"
203+
matInput
204+
[matDatepicker]="picker"
205+
(dateChange)="dateChangeCount = dateChangeCount + 1"
206+
[(ngModel)]="date"
207+
[min]="minDate"
208+
[max]="maxDate"
209+
[disabled]="disabled"
210+
[required]="required"
211+
placeholder="Type a date">
212+
<mat-datepicker #picker [touchUi]="touchUi"></mat-datepicker>
213+
</mat-form-field>
214+
202215
<input id="no-datepicker" matDatepicker>
203216
`,
204-
imports: [MatNativeDateModule, MatDatepickerModule, FormsModule],
217+
imports: [
218+
MatNativeDateModule,
219+
MatDatepickerModule,
220+
MatFormFieldModule,
221+
MatInputModule,
222+
FormsModule,
223+
],
205224
})
206225
class DatepickerInputHarnessTest {
207226
date: Date | null = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
/**
12+
* A set of criteria shared by any class derived from `MatFormFieldControlHarness`, that can be
13+
* used to filter a list of those components.
14+
*/
15+
export interface MatFormFieldControlHarnessFilters extends BaseHarnessFilters {
16+
/** Filters based on the text of the form field's floating label. */
17+
label?: string | RegExp;
18+
}

src/material/form-field/testing/control/form-field-control-harness.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,30 @@ import {ComponentHarness} from '@angular/cdk/testing';
1212
* Base class for custom form-field control harnesses. Harnesses for
1313
* custom controls with form-fields need to implement this interface.
1414
*/
15-
export abstract class MatFormFieldControlHarness extends ComponentHarness {}
15+
export abstract class MatFormFieldControlHarness extends ComponentHarness {
16+
private readonly floatingLabelSelector = '.mdc-floating-label';
17+
18+
/** Gets the text content of the floating label, if it exists. */
19+
protected async _getFloatingLabelText(): Promise<string | null> {
20+
const documentRootLocator = await this.documentRootLocatorFactory();
21+
const labelId = await (await this.host()).getAttribute('aria-labelledby');
22+
const hostId = await (await this.host()).getAttribute('id');
23+
24+
if (labelId) {
25+
// First option, try to fetch the label using the `aria-labelledby`
26+
// attribute.
27+
const labelEl = await await documentRootLocator.locatorForOptional(
28+
`${this.floatingLabelSelector}[id="${labelId}"]`,
29+
)();
30+
return labelEl ? labelEl.text() : null;
31+
} else if (hostId) {
32+
// Fallback option, try to match the id of the input with the `for`
33+
// attribute of the label.
34+
const labelEl = await await documentRootLocator.locatorForOptional(
35+
`${this.floatingLabelSelector}[for="${hostId}"]`,
36+
)();
37+
return labelEl ? labelEl.text() : null;
38+
}
39+
return null;
40+
}
41+
}

src/material/form-field/testing/control/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
*/
88

99
export * from './form-field-control-harness';
10+
export * from './form-field-control-harness-filters';

src/material/input/testing/input-harness-filters.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {BaseHarnessFilters} from '@angular/cdk/testing';
9+
import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control';
1010

1111
/** A set of criteria that can be used to filter a list of `MatInputHarness` instances. */
12-
export interface InputHarnessFilters extends BaseHarnessFilters {
12+
export interface InputHarnessFilters extends MatFormFieldControlHarnessFilters {
1313
/** Filters based on the value of the input. */
1414
value?: string | RegExp;
1515
/** Filters based on the placeholder text of the input. */

src/material/input/testing/input-harness.spec.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ describe('MatInputHarness', () => {
3939
expect(inputs.length).toBe(1);
4040
});
4141

42+
it('should load input with a specific label', async () => {
43+
const inputs = await loader.getAllHarnesses(MatInputHarness.with({label: 'Favorite food'}));
44+
expect(inputs.length).toBe(1);
45+
});
46+
4247
it('should load input with a value that matches a regex', async () => {
4348
const inputs = await loader.getAllHarnesses(MatInputHarness.with({value: /shi$/}));
4449
expect(inputs.length).toBe(1);
@@ -231,6 +236,7 @@ describe('MatInputHarness', () => {
231236
@Component({
232237
template: `
233238
<mat-form-field>
239+
<mat-label>Favorite food</mat-label>
234240
<input matInput placeholder="Favorite food" value="Sushi" name="favorite-food">
235241
</mat-form-field>
236242

src/material/input/testing/input-harness.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
*/
88

99
import {HarnessPredicate, parallel} from '@angular/cdk/testing';
10-
import {MatFormFieldControlHarness} from '../../form-field/testing/control';
10+
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
1111
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1212
import {InputHarnessFilters} from './input-harness-filters';
1313

1414
/** Harness for interacting with a standard Material inputs in tests. */
1515
export class MatInputHarness extends MatFormFieldControlHarness {
16+
private readonly _documentRootLocator = this.documentRootLocatorFactory();
17+
1618
// TODO: We do not want to handle `select` elements with `matNativeControl` because
1719
// not all methods of this harness work reasonably for native select elements.
1820
// For more details. See: https://github.com/angular/components/pull/18221.
@@ -31,6 +33,9 @@ export class MatInputHarness extends MatFormFieldControlHarness {
3133
})
3234
.addOption('placeholder', options.placeholder, (harness, placeholder) => {
3335
return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder);
36+
})
37+
.addOption('label', options.label, (harness, label) => {
38+
return HarnessPredicate.stringMatches(harness.getLabel(), label);
3439
});
3540
}
3641

@@ -94,6 +99,11 @@ export class MatInputHarness extends MatFormFieldControlHarness {
9499
return await (await this.host()).getProperty<string>('id');
95100
}
96101

102+
/** Gets the floating label text for the input, if it exists. */
103+
async getLabel(): Promise<string | null> {
104+
return await this._getFloatingLabelText();
105+
}
106+
97107
/**
98108
* Focuses the input and returns a promise that indicates when the
99109
* action is complete.

src/material/input/testing/native-select-harness-filters.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
*/
88

99
import {BaseHarnessFilters} from '@angular/cdk/testing';
10+
import {MatFormFieldControlHarnessFilters} from '@angular/material/form-field/testing/control';
1011

1112
/** A set of criteria that can be used to filter a list of `MatNativeSelectHarness` instances. */
12-
export interface NativeSelectHarnessFilters extends BaseHarnessFilters {}
13+
export interface NativeSelectHarnessFilters extends MatFormFieldControlHarnessFilters {}
1314

1415
/** A set of criteria that can be used to filter a list of `MatNativeOptionHarness` instances. */
1516
export interface NativeOptionHarnessFilters extends BaseHarnessFilters {

src/material/input/testing/native-select-harness.spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ describe('MatNativeSelectHarness', () => {
2121
expect(selects.length).toBe(2);
2222
});
2323

24+
it('should load select with a specific label', async () => {
25+
const inputs = await loader.getAllHarnesses(
26+
MatNativeSelectHarness.with({label: 'Favorite food'}),
27+
);
28+
expect(inputs.length).toBe(1);
29+
});
30+
2431
it('should get the id of a select', async () => {
2532
const selects = await loader.getAllHarnesses(MatNativeSelectHarness);
2633
expect(await parallel(() => selects.map(select => select.getId()))).toEqual(['food', 'drink']);
@@ -187,6 +194,7 @@ describe('MatNativeSelectHarness', () => {
187194
@Component({
188195
template: `
189196
<mat-form-field>
197+
<mat-label>Favorite food</mat-label>
190198
<select
191199
id="food"
192200
matNativeControl

0 commit comments

Comments
 (0)