Skip to content

Commit 9546fe7

Browse files
committed
feat(material/timepicker): add test harnesses
Adds test harnesses for `MatTimepickerInput`, `MatTimepicker` and `MatTimepickerToggle`.
1 parent 2646e08 commit 9546fe7

10 files changed

+635
-17
lines changed

src/material/timepicker/testing/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ ts_library(
99
exclude = ["**/*.spec.ts"],
1010
),
1111
deps = [
12+
"//src/cdk/coercion",
1213
"//src/cdk/testing",
14+
"//src/material/core/testing",
1315
"//src/material/timepicker",
1416
],
1517
)
@@ -27,6 +29,7 @@ ng_test_library(
2729
"//src/cdk/testing",
2830
"//src/cdk/testing/private",
2931
"//src/cdk/testing/testbed",
32+
"//src/material/core",
3033
"//src/material/timepicker",
3134
"@npm//@angular/platform-browser",
3235
],

src/material/timepicker/testing/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88

99
export * from './timepicker-harness';
1010
export * from './timepicker-harness-filters';
11+
export * from './timepicker-input-harness';
12+
export * from './timepicker-toggle-harness';

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,14 @@ import {BaseHarnessFilters} from '@angular/cdk/testing';
1010

1111
/** A set of criteria that can be used to filter a list of `MatTimepickerHarness` instances. */
1212
export interface TimepickerHarnessFilters extends BaseHarnessFilters {}
13+
14+
/** A set of criteria that can be used to filter a list of timepicker input instances. */
15+
export interface TimepickerInputHarnessFilters extends BaseHarnessFilters {
16+
/** Filters based on the value of the input. */
17+
value?: string | RegExp;
18+
/** Filters based on the placeholder text of the input. */
19+
placeholder?: string | RegExp;
20+
}
21+
22+
/** A set of criteria that can be used to filter a list of timepicker toggle instances. */
23+
export interface TimepickerToggleHarnessFilters extends BaseHarnessFilters {}
Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
import {Component} from '@angular/core';
1+
import {Component, signal} from '@angular/core';
22
import {ComponentFixture, TestBed} from '@angular/core/testing';
3-
import {HarnessLoader} from '@angular/cdk/testing';
3+
import {HarnessLoader, parallel} from '@angular/cdk/testing';
4+
import {DateAdapter, provideNativeDateAdapter} from '@angular/material/core';
45
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
56
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
6-
import {MatTimepicker} from '@angular/material/timepicker';
7+
import {MatTimepicker, MatTimepickerInput} from '@angular/material/timepicker';
78
import {MatTimepickerHarness} from './timepicker-harness';
9+
import {MatTimepickerInputHarness} from './timepicker-input-harness';
810

9-
describe('MatTimepicker', () => {
11+
describe('MatTimepickerHarness', () => {
1012
let fixture: ComponentFixture<TimepickerHarnessTest>;
1113
let loader: HarnessLoader;
1214

1315
beforeEach(() => {
1416
TestBed.configureTestingModule({
17+
providers: [provideNativeDateAdapter()],
1518
imports: [NoopAnimationsModule, TimepickerHarnessTest],
1619
});
1720

21+
const adapter = TestBed.inject(DateAdapter);
22+
adapter.setLocale('en-US');
1823
fixture = TestBed.createComponent(TimepickerHarnessTest);
1924
fixture.detectChanges();
2025
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
@@ -24,14 +29,54 @@ describe('MatTimepicker', () => {
2429
const harnesses = await loader.getAllHarnesses(MatTimepickerHarness);
2530
expect(harnesses.length).toBe(2);
2631
});
32+
33+
it('should get the open state of a timepicker', async () => {
34+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#one'}));
35+
const timepicker = await input.getTimepicker();
36+
expect(await timepicker.isOpen()).toBe(false);
37+
38+
await input.openTimepicker();
39+
expect(await timepicker.isOpen()).toBe(true);
40+
});
41+
42+
it('should throw when trying to get the options while closed', async () => {
43+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#one'}));
44+
const timepicker = await input.getTimepicker();
45+
46+
await expectAsync(timepicker.getOptions()).toBeRejectedWithError(
47+
/Unable to retrieve options for timepicker\. Timepicker panel is closed\./,
48+
);
49+
});
50+
51+
it('should get the options in a timepicker', async () => {
52+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#one'}));
53+
const timepicker = await input.openTimepicker();
54+
const options = await timepicker.getOptions();
55+
const labels = await parallel(() => options.map(o => o.getText()));
56+
expect(labels).toEqual(['12:00 AM', '4:00 AM', '8:00 AM', '12:00 PM', '4:00 PM', '8:00 PM']);
57+
});
58+
59+
it('should be able to select an option', async () => {
60+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#one'}));
61+
const timepicker = await input.openTimepicker();
62+
expect(await input.getValue()).toBe('');
63+
64+
await timepicker.selectOption({text: '4:00 PM'});
65+
expect(await input.getValue()).toBe('4:00 PM');
66+
expect(await timepicker.isOpen()).toBe(false);
67+
});
2768
});
2869

2970
@Component({
3071
template: `
31-
<mat-timepicker/>
32-
<mat-timepicker/>
72+
<input id="one" [matTimepicker]="onePicker">
73+
<mat-timepicker #onePicker [interval]="interval()"/>
74+
<input id="two" [matTimepicker]="twoPicker">
75+
<mat-timepicker #twoPicker [interval]="interval()"/>
3376
`,
3477
standalone: true,
35-
imports: [MatTimepicker],
78+
imports: [MatTimepickerInput, MatTimepicker],
3679
})
37-
class TimepickerHarnessTest {}
80+
class TimepickerHarnessTest {
81+
interval = signal('4h');
82+
}

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

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,63 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
9+
import {
10+
ComponentHarness,
11+
ComponentHarnessConstructor,
12+
HarnessPredicate,
13+
} from '@angular/cdk/testing';
14+
import {MatOptionHarness, OptionHarnessFilters} from '@angular/material/core/testing';
1015
import {TimepickerHarnessFilters} from './timepicker-harness-filters';
1116

12-
/** Harness for interacting with a standard `MatTimepicker` in tests. */
1317
export class MatTimepickerHarness extends ComponentHarness {
14-
/** The selector for the host element of a `MatTimepicker` instance. */
15-
static hostSelector = '.mat-timepicker';
18+
private _documentRootLocator = this.documentRootLocatorFactory();
19+
static hostSelector = 'mat-timepicker';
1620

1721
/**
18-
* Gets a `HarnessPredicate` that can be used to search for a `MatTimepicker`
19-
* that meets certain criteria.
20-
* @param options Options for filtering which dialog instances are considered a match.
22+
* Gets a `HarnessPredicate` that can be used to search for a timepicker with specific
23+
* attributes.
24+
* @param options Options for filtering which timepicker instances are considered a match.
2125
* @return a `HarnessPredicate` configured with the given options.
2226
*/
23-
static with(options: TimepickerHarnessFilters = {}): HarnessPredicate<MatTimepickerHarness> {
24-
return new HarnessPredicate(MatTimepickerHarness, options);
27+
static with<T extends MatTimepickerHarness>(
28+
this: ComponentHarnessConstructor<T>,
29+
options: TimepickerHarnessFilters = {},
30+
): HarnessPredicate<T> {
31+
return new HarnessPredicate(this, options);
32+
}
33+
34+
/** Whether the timepicker is open. */
35+
async isOpen(): Promise<boolean> {
36+
const selector = await this._getPanelSelector();
37+
const panel = await this._documentRootLocator.locatorForOptional(selector)();
38+
return panel !== null;
39+
}
40+
41+
/** Gets the options inside the timepicker panel. */
42+
async getOptions(filters?: Omit<OptionHarnessFilters, 'ancestor'>): Promise<MatOptionHarness[]> {
43+
if (!(await this.isOpen())) {
44+
throw new Error('Unable to retrieve options for timepicker. Timepicker panel is closed.');
45+
}
46+
47+
return this._documentRootLocator.locatorForAll(
48+
MatOptionHarness.with({
49+
...(filters || {}),
50+
ancestor: await this._getPanelSelector(),
51+
} as OptionHarnessFilters),
52+
)();
53+
}
54+
55+
/** Selects the first option matching the given filters. */
56+
async selectOption(filters: OptionHarnessFilters): Promise<void> {
57+
const options = await this.getOptions(filters);
58+
if (!options.length) {
59+
throw Error(`Could not find a mat-option matching ${JSON.stringify(filters)}`);
60+
}
61+
await options[0].click();
62+
}
63+
64+
/** Gets the selector that can be used to find the timepicker's panel. */
65+
protected async _getPanelSelector(): Promise<string> {
66+
return `#${await (await this.host()).getAttribute('mat-timepicker-panel-id')}`;
2567
}
2668
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {HarnessLoader, parallel} from '@angular/cdk/testing';
2+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
3+
import {Component, signal} from '@angular/core';
4+
import {ComponentFixture, TestBed} from '@angular/core/testing';
5+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
6+
import {DateAdapter, provideNativeDateAdapter} from '@angular/material/core';
7+
import {MatTimepicker, MatTimepickerInput} from '@angular/material/timepicker';
8+
import {MatTimepickerHarness} from './timepicker-harness';
9+
import {MatTimepickerInputHarness} from './timepicker-input-harness';
10+
11+
describe('MatTimepickerInputHarness', () => {
12+
let fixture: ComponentFixture<TimepickerInputHarnessTest>;
13+
let loader: HarnessLoader;
14+
let adapter: DateAdapter<Date>;
15+
16+
beforeEach(() => {
17+
TestBed.configureTestingModule({
18+
providers: [provideNativeDateAdapter()],
19+
imports: [NoopAnimationsModule, TimepickerInputHarnessTest],
20+
});
21+
22+
adapter = TestBed.inject(DateAdapter);
23+
adapter.setLocale('en-US');
24+
fixture = TestBed.createComponent(TimepickerInputHarnessTest);
25+
fixture.detectChanges();
26+
loader = TestbedHarnessEnvironment.loader(fixture);
27+
});
28+
29+
it('should load all timepicker input harnesses', async () => {
30+
const inputs = await loader.getAllHarnesses(MatTimepickerInputHarness);
31+
expect(inputs.length).toBe(2);
32+
});
33+
34+
it('should filter inputs based on their value', async () => {
35+
fixture.componentInstance.value.set(createTime(15, 10));
36+
fixture.changeDetectorRef.markForCheck();
37+
const inputs = await loader.getAllHarnesses(MatTimepickerInputHarness.with({value: /3:10/}));
38+
expect(inputs.length).toBe(1);
39+
});
40+
41+
it('should filter inputs based on their placeholder', async () => {
42+
const inputs = await loader.getAllHarnesses(
43+
MatTimepickerInputHarness.with({
44+
placeholder: /^Pick/,
45+
}),
46+
);
47+
48+
expect(inputs.length).toBe(1);
49+
});
50+
51+
it('should get whether the input is disabled', async () => {
52+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
53+
expect(await input.isDisabled()).toBe(false);
54+
55+
fixture.componentInstance.disabled.set(true);
56+
expect(await input.isDisabled()).toBe(true);
57+
});
58+
59+
it('should get whether the input is required', async () => {
60+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
61+
expect(await input.isRequired()).toBe(false);
62+
63+
fixture.componentInstance.required.set(true);
64+
expect(await input.isRequired()).toBe(true);
65+
});
66+
67+
it('should get the input value', async () => {
68+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
69+
fixture.componentInstance.value.set(createTime(15, 10));
70+
fixture.changeDetectorRef.markForCheck();
71+
72+
expect(await input.getValue()).toBe('3:10 PM');
73+
});
74+
75+
it('should set the input value', async () => {
76+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
77+
expect(await input.getValue()).toBeFalsy();
78+
79+
await input.setValue('3:10 PM');
80+
expect(await input.getValue()).toBe('3:10 PM');
81+
});
82+
83+
it('should set the input value based on date adapter validation and formatting', async () => {
84+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
85+
const validValues: any[] = [createTime(15, 10), '', 0, false];
86+
const invalidValues: any[] = [null, undefined];
87+
spyOn(adapter, 'format').and.returnValue('FORMATTED_VALUE');
88+
spyOn(adapter, 'isValid').and.callFake(value => validValues.includes(value));
89+
spyOn(adapter, 'deserialize').and.callFake(value =>
90+
validValues.includes(value) ? value : null,
91+
);
92+
spyOn(adapter, 'getValidDateOrNull').and.callFake((value: Date) =>
93+
adapter.isValid(value) ? value : null,
94+
);
95+
96+
for (let value of validValues) {
97+
fixture.componentInstance.value.set(value);
98+
fixture.changeDetectorRef.markForCheck();
99+
expect(await input.getValue()).toBe('FORMATTED_VALUE');
100+
}
101+
102+
for (let value of invalidValues) {
103+
fixture.componentInstance.value.set(value);
104+
fixture.changeDetectorRef.markForCheck();
105+
expect(await input.getValue()).toBe('');
106+
}
107+
});
108+
109+
it('should get the input placeholder', async () => {
110+
const inputs = await loader.getAllHarnesses(MatTimepickerInputHarness);
111+
expect(await parallel(() => inputs.map(input => input.getPlaceholder()))).toEqual([
112+
'Pick a time',
113+
'Select a time',
114+
]);
115+
});
116+
117+
it('should be able to change the input focused state', async () => {
118+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
119+
expect(await input.isFocused()).toBe(false);
120+
121+
await input.focus();
122+
expect(await input.isFocused()).toBe(true);
123+
124+
await input.blur();
125+
expect(await input.isFocused()).toBe(false);
126+
});
127+
128+
it('should be able to open and close a timepicker', async () => {
129+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
130+
expect(await input.isTimepickerOpen()).toBe(false);
131+
132+
await input.openTimepicker();
133+
expect(await input.isTimepickerOpen()).toBe(true);
134+
135+
await input.closeTimepicker();
136+
expect(await input.isTimepickerOpen()).toBe(false);
137+
});
138+
139+
it('should be able to get the harness for the associated timepicker', async () => {
140+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
141+
await input.openTimepicker();
142+
expect(await input.getTimepicker()).toBeInstanceOf(MatTimepickerHarness);
143+
});
144+
145+
it('should emit the `valueChange` event when the value is changed', async () => {
146+
const input = await loader.getHarness(MatTimepickerInputHarness.with({selector: '#bound'}));
147+
expect(fixture.componentInstance.changeCount).toBe(0);
148+
149+
await input.setValue('3:15 PM');
150+
expect(fixture.componentInstance.changeCount).toBeGreaterThan(0);
151+
});
152+
153+
function createTime(hours: number, minutes: number): Date {
154+
return adapter.setTime(adapter.today(), hours, minutes, 0);
155+
}
156+
});
157+
158+
@Component({
159+
template: `
160+
<input
161+
[matTimepicker]="boundPicker"
162+
[value]="value()"
163+
[disabled]="disabled()"
164+
[required]="required()"
165+
(valueChange)="changeCount = changeCount + 1"
166+
placeholder="Pick a time"
167+
id="bound">
168+
<mat-timepicker #boundPicker/>
169+
170+
<input [matTimepicker]="basicPicker" id="basic" placeholder="Select a time">
171+
<mat-timepicker #basicPicker/>
172+
`,
173+
standalone: true,
174+
imports: [MatTimepickerInput, MatTimepicker],
175+
})
176+
class TimepickerInputHarnessTest {
177+
readonly value = signal<Date | null>(null);
178+
readonly disabled = signal(false);
179+
readonly required = signal(false);
180+
changeCount = 0;
181+
}

0 commit comments

Comments
 (0)