Skip to content

Commit 2a04ccf

Browse files
authored
feat(material-experimental): slide-toggle test harnesses (#16545)
It slides, it toggles, it makes julienne fries.
1 parent 615070a commit 2a04ccf

File tree

5 files changed

+478
-2
lines changed

5 files changed

+478
-2
lines changed

src/material-experimental/mdc-slide-toggle/BUILD.bazel

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package(default_visibility = ["//visibility:public"])
22

33
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library")
4-
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite")
4+
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite", "ts_library")
55
load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
66

77
ng_module(
88
name = "mdc-slide-toggle",
99
srcs = glob(
1010
["**/*.ts"],
11-
exclude = ["**/*.spec.ts"],
11+
exclude = [
12+
"**/*.spec.ts",
13+
"harness/**",
14+
],
1215
),
1316
assets = [":slide_toggle_scss"] + glob(["**/*.html"]),
1417
module_name = "@angular/material-experimental/mdc-slide-toggle",
@@ -24,6 +27,18 @@ ng_module(
2427
],
2528
)
2629

30+
ts_library(
31+
name = "harness",
32+
srcs = glob(
33+
["harness/**/*.ts"],
34+
exclude = ["**/*.spec.ts"],
35+
),
36+
deps = [
37+
"//src/cdk-experimental/testing",
38+
"//src/cdk/coercion",
39+
],
40+
)
41+
2742
sass_library(
2843
name = "mdc_slide_toggle_scss_lib",
2944
srcs = glob(["**/_*.scss"]),
@@ -53,9 +68,13 @@ ng_test_library(
5368
exclude = ["**/*.e2e.spec.ts"],
5469
),
5570
deps = [
71+
":harness",
5672
":mdc-slide-toggle",
73+
"//src/cdk-experimental/testing",
74+
"//src/cdk-experimental/testing/testbed",
5775
"//src/cdk/bidi",
5876
"//src/cdk/testing",
77+
"//src/material/slide-toggle",
5978
"@npm//@angular/forms",
6079
"@npm//@angular/platform-browser",
6180
],
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.io/license
7+
*/
8+
9+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {SlideToggleHarnessFilters} from './slide-toggle-harness-filters';
12+
13+
14+
/**
15+
* Harness for interacting with a MDC-based mat-slide-toggle in tests.
16+
* @dynamic
17+
*/
18+
export class MatSlideToggleHarness extends ComponentHarness {
19+
static hostSelector = 'mat-slide-toggle';
20+
21+
/**
22+
* Gets a `HarnessPredicate` that can be used to search for a slide-toggle w/ specific attributes.
23+
* @param options Options for narrowing the search:
24+
* - `label` finds a slide-toggle with specific label text.
25+
* @return a `HarnessPredicate` configured with the given options.
26+
*/
27+
static with(options: SlideToggleHarnessFilters = {}): HarnessPredicate<MatSlideToggleHarness> {
28+
return new HarnessPredicate(MatSlideToggleHarness)
29+
.addOption('label', options.label,
30+
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label));
31+
}
32+
33+
private _label = this.locatorFor('label');
34+
private _input = this.locatorFor('input');
35+
private _inputContainer = this.locatorFor('.mdc-switch');
36+
37+
/** Gets a boolean promise indicating if the slide-toggle is checked. */
38+
async isChecked(): Promise<boolean> {
39+
const checked = (await this._input()).getAttribute('checked');
40+
return coerceBooleanProperty(await checked);
41+
}
42+
43+
/** Gets a boolean promise indicating if the slide-toggle is disabled. */
44+
async isDisabled(): Promise<boolean> {
45+
const disabled = (await this._input()).getAttribute('disabled');
46+
return coerceBooleanProperty(await disabled);
47+
}
48+
49+
/** Gets a boolean promise indicating if the slide-toggle is required. */
50+
async isRequired(): Promise<boolean> {
51+
const required = (await this._input()).getAttribute('required');
52+
return coerceBooleanProperty(await required);
53+
}
54+
55+
/** Gets a boolean promise indicating if the slide-toggle is valid. */
56+
async isValid(): Promise<boolean> {
57+
const invalid = (await this.host()).hasClass('ng-invalid');
58+
return !(await invalid);
59+
}
60+
61+
/** Gets a promise for the slide-toggle's name. */
62+
async getName(): Promise<string | null> {
63+
return (await this._input()).getAttribute('name');
64+
}
65+
66+
/** Gets a promise for the slide-toggle's aria-label. */
67+
async getAriaLabel(): Promise<string | null> {
68+
return (await this._input()).getAttribute('aria-label');
69+
}
70+
71+
/** Gets a promise for the slide-toggle's aria-labelledby. */
72+
async getAriaLabelledby(): Promise<string | null> {
73+
return (await this._input()).getAttribute('aria-labelledby');
74+
}
75+
76+
/** Gets a promise for the slide-toggle's label text. */
77+
async getLabelText(): Promise<string> {
78+
return (await this._label()).text();
79+
}
80+
81+
/** Focuses the slide-toggle and returns a void promise that indicates action completion. */
82+
async foucs(): Promise<void> {
83+
return (await this._input()).focus();
84+
}
85+
86+
/** Blurs the slide-toggle and returns a void promise that indicates action completion. */
87+
async blur(): Promise<void> {
88+
return (await this._input()).blur();
89+
}
90+
91+
/**
92+
* Toggle the checked state of the slide-toggle and returns a void promise that indicates when the
93+
* action is complete.
94+
*
95+
* Note: This attempts to toggle the slide-toggle as a user would, by clicking it.
96+
*/
97+
async toggle(): Promise<void> {
98+
const elToClick = await this.isDisabled() ? this._inputContainer() : this._input();
99+
return (await elToClick).click();
100+
}
101+
102+
/**
103+
* Puts the slide-toggle in a checked state by toggling it if it is currently unchecked, or doing
104+
* nothing if it is already checked. Returns a void promise that indicates when the action is
105+
* complete.
106+
*
107+
* Note: This attempts to check the slide-toggle as a user would, by clicking it.
108+
*/
109+
async check(): Promise<void> {
110+
if (!(await this.isChecked())) {
111+
await this.toggle();
112+
}
113+
}
114+
115+
/**
116+
* Puts the slide-toggle in an unchecked state by toggling it if it is currently checked, or doing
117+
* nothing if it is already unchecked. Returns a void promise that indicates when the action is
118+
* complete.
119+
*
120+
* Note: This attempts to uncheck the slide-toggle as a user would, by clicking it.
121+
*/
122+
async uncheck(): Promise<void> {
123+
if (await this.isChecked()) {
124+
await this.toggle();
125+
}
126+
}
127+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.io/license
7+
*/
8+
9+
export type SlideToggleHarnessFilters = {
10+
label?: string | RegExp
11+
};
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {HarnessLoader} from '@angular/cdk-experimental/testing';
2+
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
3+
import {Component} from '@angular/core';
4+
import {ComponentFixture, TestBed} from '@angular/core/testing';
5+
import {FormControl, ReactiveFormsModule} from '@angular/forms';
6+
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
7+
import {MatSlideToggleModule as MatMdcSlideToggleModule} from '../index';
8+
import {MatSlideToggleHarness} from './slide-toggle-harness';
9+
import {MatSlideToggleHarness as MatMdcSlideToggleHarness} from './mdc-slide-toggle-harness';
10+
11+
12+
let fixture: ComponentFixture<SlideToggleHarnessTest>;
13+
let loader: HarnessLoader;
14+
let slideToggleHarness: typeof MatSlideToggleHarness;
15+
16+
describe('MatSlideToggleHarness', () => {
17+
describe('non-MDC-based', () => {
18+
beforeEach(async () => {
19+
await TestBed.configureTestingModule({
20+
imports: [MatSlideToggleModule, ReactiveFormsModule],
21+
declarations: [SlideToggleHarnessTest],
22+
}).compileComponents();
23+
24+
fixture = TestBed.createComponent(SlideToggleHarnessTest);
25+
fixture.detectChanges();
26+
loader = TestbedHarnessEnvironment.loader(fixture);
27+
slideToggleHarness = MatSlideToggleHarness;
28+
});
29+
30+
runTests();
31+
});
32+
33+
describe('MDC-based', () => {
34+
beforeEach(async () => {
35+
await TestBed.configureTestingModule({
36+
imports: [MatMdcSlideToggleModule, ReactiveFormsModule],
37+
declarations: [SlideToggleHarnessTest],
38+
}).compileComponents();
39+
40+
fixture = TestBed.createComponent(SlideToggleHarnessTest);
41+
fixture.detectChanges();
42+
loader = TestbedHarnessEnvironment.loader(fixture);
43+
// Public APIs are the same as MatSlideToggleHarness, but cast is necessary because of
44+
// different private fields.
45+
slideToggleHarness = MatMdcSlideToggleHarness as any;
46+
});
47+
48+
runTests();
49+
});
50+
});
51+
52+
/** Shared tests to run on both the original and MDC-based slide-toggles. */
53+
function runTests() {
54+
it('should load all slide-toggle harnesses', async () => {
55+
const slideToggles = await loader.getAllHarnesses(slideToggleHarness);
56+
expect(slideToggles.length).toBe(2);
57+
});
58+
59+
it('should load slide-toggle with exact label', async () => {
60+
const slideToggles = await loader.getAllHarnesses(slideToggleHarness.with({label: 'First'}));
61+
expect(slideToggles.length).toBe(1);
62+
expect(await slideToggles[0].getLabelText()).toBe('First');
63+
});
64+
65+
it('should load slide-toggle with regex label match', async () => {
66+
const slideToggles = await loader.getAllHarnesses(slideToggleHarness.with({label: /^s/i}));
67+
expect(slideToggles.length).toBe(1);
68+
expect(await slideToggles[0].getLabelText()).toBe('Second');
69+
});
70+
71+
it('should get checked state', async () => {
72+
const [checkedToggle, uncheckedToggle] = await loader.getAllHarnesses(slideToggleHarness);
73+
expect(await checkedToggle.isChecked()).toBe(true);
74+
expect(await uncheckedToggle.isChecked()).toBe(false);
75+
});
76+
77+
it('should get disabled state', async () => {
78+
const [enabledToggle, disabledToggle] = await loader.getAllHarnesses(slideToggleHarness);
79+
expect(await enabledToggle.isDisabled()).toBe(false);
80+
expect(await disabledToggle.isDisabled()).toBe(true);
81+
});
82+
83+
it('should get required state', async () => {
84+
const [requiredToggle, optionalToggle] = await loader.getAllHarnesses(slideToggleHarness);
85+
expect(await requiredToggle.isRequired()).toBe(true);
86+
expect(await optionalToggle.isRequired()).toBe(false);
87+
});
88+
89+
it('should get valid state', async () => {
90+
const [requiredToggle, optionalToggle] = await loader.getAllHarnesses(slideToggleHarness);
91+
expect(await optionalToggle.isValid()).toBe(true, 'Expected optional toggle to be valid');
92+
expect(await requiredToggle.isValid())
93+
.toBe(true, 'Expected required checked toggle to be valid');
94+
await requiredToggle.uncheck();
95+
expect(await requiredToggle.isValid())
96+
.toBe(false, 'Expected required unchecked toggle to be invalid');
97+
});
98+
99+
it('should get name', async () => {
100+
const slideToggle = await loader.getHarness(slideToggleHarness.with({label: 'First'}));
101+
expect(await slideToggle.getName()).toBe('first-name');
102+
});
103+
104+
it('should get aria-label', async () => {
105+
const slideToggle = await loader.getHarness(slideToggleHarness.with({label: 'First'}));
106+
expect(await slideToggle.getAriaLabel()).toBe('First slide-toggle');
107+
});
108+
109+
it('should get aria-labelledby', async () => {
110+
const slideToggle = await loader.getHarness(slideToggleHarness.with({label: 'Second'}));
111+
expect(await slideToggle.getAriaLabelledby()).toBe('second-label');
112+
});
113+
114+
it('should get label text', async () => {
115+
const [firstToggle, secondToggle] = await loader.getAllHarnesses(slideToggleHarness);
116+
expect(await firstToggle.getLabelText()).toBe('First');
117+
expect(await secondToggle.getLabelText()).toBe('Second');
118+
});
119+
120+
it('should focus slide-toggle', async () => {
121+
const slideToggle = await loader.getHarness(slideToggleHarness.with({label: 'First'}));
122+
expect(getActiveElementTagName()).not.toBe('input');
123+
await slideToggle.foucs();
124+
expect(getActiveElementTagName()).toBe('input');
125+
});
126+
127+
it('should blur slide-toggle', async () => {
128+
const slideToggle = await loader.getHarness(slideToggleHarness.with({label: 'First'}));
129+
await slideToggle.foucs();
130+
expect(getActiveElementTagName()).toBe('input');
131+
await slideToggle.blur();
132+
expect(getActiveElementTagName()).not.toBe('input');
133+
});
134+
135+
it('should toggle slide-toggle', async () => {
136+
fixture.componentInstance.disabled = false;
137+
const [checkedToggle, uncheckedToggle] = await loader.getAllHarnesses(slideToggleHarness);
138+
await checkedToggle.toggle();
139+
await uncheckedToggle.toggle();
140+
expect(await checkedToggle.isChecked()).toBe(false);
141+
expect(await uncheckedToggle.isChecked()).toBe(true);
142+
});
143+
144+
it('should check slide-toggle', async () => {
145+
fixture.componentInstance.disabled = false;
146+
const [checkedToggle, uncheckedToggle] = await loader.getAllHarnesses(slideToggleHarness);
147+
await checkedToggle.check();
148+
await uncheckedToggle.check();
149+
expect(await checkedToggle.isChecked()).toBe(true);
150+
expect(await uncheckedToggle.isChecked()).toBe(true);
151+
});
152+
153+
it('should uncheck slide-toggle', async () => {
154+
fixture.componentInstance.disabled = false;
155+
const [checkedToggle, uncheckedToggle] = await loader.getAllHarnesses(slideToggleHarness);
156+
await checkedToggle.uncheck();
157+
await uncheckedToggle.uncheck();
158+
expect(await checkedToggle.isChecked()).toBe(false);
159+
expect(await uncheckedToggle.isChecked()).toBe(false);
160+
});
161+
162+
it('should not toggle disabled slide-toggle', async () => {
163+
const disabledToggle = await loader.getHarness(slideToggleHarness.with({label: 'Second'}));
164+
expect(await disabledToggle.isChecked()).toBe(false);
165+
await disabledToggle.toggle();
166+
expect(await disabledToggle.isChecked()).toBe(false);
167+
});
168+
}
169+
170+
function getActiveElementTagName() {
171+
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
172+
}
173+
174+
@Component({
175+
template: `
176+
<mat-slide-toggle
177+
[formControl]="ctrl"
178+
required
179+
name="first-name"
180+
aria-label="First slide-toggle">
181+
First
182+
</mat-slide-toggle>
183+
<mat-slide-toggle [disabled]="disabled" aria-labelledby="second-label">
184+
Second
185+
</mat-slide-toggle>
186+
<span id="second-label">Second slide-toggle</span>
187+
`
188+
})
189+
class SlideToggleHarnessTest {
190+
ctrl = new FormControl(true);
191+
disabled = true;
192+
}
193+

0 commit comments

Comments
 (0)