Skip to content

Commit 583af19

Browse files
devversionjelbourn
authored andcommitted
feat(material-experimental): add test harness for radio-button (#16593)
Adds a test harness for the `MatRadioButton` implementation. Even though there is no implementation of radio w/ MDC, the standard harness and tests are helpful and reduce work that is needed to create a MDC prototype of radio.
1 parent b5b9ff7 commit 583af19

File tree

6 files changed

+319
-2
lines changed

6 files changed

+319
-2
lines changed

src/material-experimental/mdc-radio/BUILD.bazel

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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")
4+
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_web_test_suite")
55
load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
66

77
ng_module(
@@ -31,6 +31,15 @@ sass_binary(
3131
src = "radio.scss",
3232
)
3333

34+
ng_web_test_suite(
35+
name = "unit_tests",
36+
static_files = ["@npm//:node_modules/@material/radio/dist/mdc.radio.js"],
37+
deps = [
38+
"//src/material-experimental:mdc_require_config.js",
39+
"//src/material-experimental/mdc-radio/harness:harness_tests",
40+
],
41+
)
42+
3443
ng_e2e_test_library(
3544
name = "e2e_test_sources",
3645
srcs = glob(["**/*.e2e.spec.ts"]),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_test_library", "ts_library")
4+
5+
ts_library(
6+
name = "harness",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//src/cdk-experimental/testing",
13+
"//src/cdk/coercion",
14+
],
15+
)
16+
17+
ng_test_library(
18+
name = "harness_tests",
19+
srcs = glob(["**/*.spec.ts"]),
20+
deps = [
21+
":harness",
22+
"//src/cdk-experimental/testing",
23+
"//src/cdk-experimental/testing/testbed",
24+
"//src/material/radio",
25+
"@npm//@angular/forms",
26+
],
27+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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 RadioButtonHarnessFilters = {
10+
label?: string|RegExp,
11+
id?: string;
12+
name?: string,
13+
};
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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 {ReactiveFormsModule} from '@angular/forms';
6+
import {MatRadioModule} from '@angular/material/radio';
7+
import {MatRadioButtonHarness} from './radio-harness';
8+
9+
let fixture: ComponentFixture<MultipleRadioButtonsHarnessTest>;
10+
let loader: HarnessLoader;
11+
let radioButtonHarness: typeof MatRadioButtonHarness;
12+
13+
describe('MatRadioButtonHarness', () => {
14+
describe('non-MDC-based', () => {
15+
beforeEach(async () => {
16+
await TestBed
17+
.configureTestingModule({
18+
imports: [MatRadioModule, ReactiveFormsModule],
19+
declarations: [MultipleRadioButtonsHarnessTest],
20+
})
21+
.compileComponents();
22+
23+
fixture = TestBed.createComponent(MultipleRadioButtonsHarnessTest);
24+
fixture.detectChanges();
25+
loader = TestbedHarnessEnvironment.loader(fixture);
26+
radioButtonHarness = MatRadioButtonHarness;
27+
});
28+
29+
runTests();
30+
});
31+
32+
describe(
33+
'MDC-based',
34+
() => {
35+
// TODO: run tests for MDC based radio-button once implemented.
36+
});
37+
});
38+
39+
/** Shared tests to run on both the original and MDC-based radio-button's. */
40+
function runTests() {
41+
it('should load all radio-button harnesses', async () => {
42+
const radios = await loader.getAllHarnesses(radioButtonHarness);
43+
expect(radios.length).toBe(4);
44+
});
45+
46+
it('should load radio-button with exact label', async () => {
47+
const radios = await loader.getAllHarnesses(radioButtonHarness.with({label: 'Option #2'}));
48+
expect(radios.length).toBe(1);
49+
expect(await radios[0].getId()).toBe('opt2');
50+
expect(await radios[0].getLabelText()).toBe('Option #2');
51+
});
52+
53+
it('should load radio-button with regex label match', async () => {
54+
const radios = await loader.getAllHarnesses(radioButtonHarness.with({label: /#3$/i}));
55+
expect(radios.length).toBe(1);
56+
expect(await radios[0].getId()).toBe('opt3');
57+
expect(await radios[0].getLabelText()).toBe('Option #3');
58+
});
59+
60+
it('should load radio-button with id', async () => {
61+
const radios = await loader.getAllHarnesses(radioButtonHarness.with({id: 'opt3'}));
62+
expect(radios.length).toBe(1);
63+
expect(await radios[0].getId()).toBe('opt3');
64+
expect(await radios[0].getLabelText()).toBe('Option #3');
65+
});
66+
67+
it('should load radio-buttons with same name', async () => {
68+
const radios = await loader.getAllHarnesses(radioButtonHarness.with({name: 'group1'}));
69+
expect(radios.length).toBe(2);
70+
71+
expect(await radios[0].getId()).toBe('opt1');
72+
expect(await radios[1].getId()).toBe('opt2');
73+
});
74+
75+
it('should get checked state', async () => {
76+
const [uncheckedRadio, checkedRadio] = await loader.getAllHarnesses(radioButtonHarness);
77+
expect(await uncheckedRadio.isChecked()).toBe(false);
78+
expect(await checkedRadio.isChecked()).toBe(true);
79+
});
80+
81+
it('should get label text', async () => {
82+
const [firstRadio, secondRadio, thirdRadio] = await loader.getAllHarnesses(radioButtonHarness);
83+
expect(await firstRadio.getLabelText()).toBe('Option #1');
84+
expect(await secondRadio.getLabelText()).toBe('Option #2');
85+
expect(await thirdRadio.getLabelText()).toBe('Option #3');
86+
});
87+
88+
it('should get disabled state', async () => {
89+
const [firstRadio] = await loader.getAllHarnesses(radioButtonHarness);
90+
expect(await firstRadio.isDisabled()).toBe(false);
91+
92+
fixture.componentInstance.disableAll = true;
93+
fixture.detectChanges();
94+
95+
expect(await firstRadio.isDisabled()).toBe(true);
96+
});
97+
98+
it('should focus radio-button', async () => {
99+
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt2'}));
100+
expect(getActiveElementTagName()).not.toBe('input');
101+
await radioButton.focus();
102+
expect(getActiveElementTagName()).toBe('input');
103+
});
104+
105+
it('should blur radio-button', async () => {
106+
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt2'}));
107+
await radioButton.focus();
108+
expect(getActiveElementTagName()).toBe('input');
109+
await radioButton.blur();
110+
expect(getActiveElementTagName()).not.toBe('input');
111+
});
112+
113+
it('should check radio-button', async () => {
114+
const [uncheckedRadio, checkedRadio] = await loader.getAllHarnesses(radioButtonHarness);
115+
await uncheckedRadio.check();
116+
expect(await uncheckedRadio.isChecked()).toBe(true);
117+
// Checked radio state should change since the two radio's
118+
// have the same name and only one can be selected.
119+
expect(await checkedRadio.isChecked()).toBe(false);
120+
});
121+
122+
it('should not be able to check disabled radio-button', async () => {
123+
fixture.componentInstance.disableAll = true;
124+
fixture.detectChanges();
125+
126+
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt3'}));
127+
expect(await radioButton.isChecked()).toBe(false);
128+
await radioButton.check();
129+
expect(await radioButton.isChecked()).toBe(false);
130+
131+
fixture.componentInstance.disableAll = false;
132+
fixture.detectChanges();
133+
134+
expect(await radioButton.isChecked()).toBe(false);
135+
await radioButton.check();
136+
expect(await radioButton.isChecked()).toBe(true);
137+
});
138+
139+
it('should get required state', async () => {
140+
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'required-radio'}));
141+
expect(await radioButton.isRequired()).toBe(true);
142+
});
143+
}
144+
function getActiveElementTagName() {
145+
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
146+
}
147+
148+
@Component({
149+
template: `
150+
<mat-radio-button *ngFor="let value of values, let i = index"
151+
[name]="value === 'opt3' ? 'group2' : 'group1'"
152+
[disabled]="disableAll"
153+
[checked]="value === 'opt2'"
154+
[id]="value"
155+
[required]="value === 'opt2'"
156+
[value]="value">
157+
Option #{{i + 1}}
158+
</mat-radio-button>
159+
160+
<mat-radio-button id="required-radio" required name="acceptsTerms">
161+
Accept terms of conditions
162+
</mat-radio-button>
163+
`
164+
})
165+
class MultipleRadioButtonsHarnessTest {
166+
values = ['opt1', 'opt2', 'opt3'];
167+
disableAll = false;
168+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 {RadioButtonHarnessFilters} from './radio-harness-filters';
12+
13+
/**
14+
* Harness for interacting with a standard mat-radio-button in tests.
15+
* @dynamic
16+
*/
17+
export class MatRadioButtonHarness extends ComponentHarness {
18+
static hostSelector = 'mat-radio-button';
19+
20+
/**
21+
* Gets a `HarnessPredicate` that can be used to search for a radio-button with
22+
* specific attributes.
23+
* @param options Options for narrowing the search:
24+
* - `label` finds a radio-button with specific label text.
25+
* - `name` finds a radio-button with specific name.
26+
* @return a `HarnessPredicate` configured with the given options.
27+
*/
28+
static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> {
29+
return new HarnessPredicate(MatRadioButtonHarness)
30+
.addOption(
31+
'label', options.label,
32+
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
33+
.addOption(
34+
'name', options.name, async (harness, name) => (await harness.getName()) === name)
35+
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id);
36+
}
37+
38+
private _textLabel = this.locatorFor('.mat-radio-label-content');
39+
private _clickLabel = this.locatorFor('.mat-radio-label');
40+
private _input = this.locatorFor('input');
41+
42+
/** Whether the radio-button is checked. */
43+
async isChecked(): Promise<boolean> {
44+
const checked = (await this._input()).getAttribute('checked');
45+
return coerceBooleanProperty(await checked);
46+
}
47+
48+
/** Whether the radio-button is disabled. */
49+
async isDisabled(): Promise<boolean> {
50+
const disabled = (await this._input()).getAttribute('disabled');
51+
return coerceBooleanProperty(await disabled);
52+
}
53+
54+
/** Whether the radio-button is required. */
55+
async isRequired(): Promise<boolean> {
56+
const required = (await this._input()).getAttribute('required');
57+
return coerceBooleanProperty(await required);
58+
}
59+
60+
/** Gets a promise for the radio-button's name. */
61+
async getName(): Promise<string|null> {
62+
return (await this._input()).getAttribute('name');
63+
}
64+
65+
/** Gets a promise for the radio-button's id. */
66+
async getId(): Promise<string|null> {
67+
return (await this.host()).getAttribute('id');
68+
}
69+
70+
/** Gets a promise for the radio-button's label text. */
71+
async getLabelText(): Promise<string> {
72+
return (await this._textLabel()).text();
73+
}
74+
75+
/**
76+
* Focuses the radio-button and returns a void promise that indicates when the
77+
* action is complete.
78+
*/
79+
async focus(): Promise<void> {
80+
return (await this._input()).focus();
81+
}
82+
83+
/**
84+
* Blurs the radio-button and returns a void promise that indicates when the
85+
* action is complete.
86+
*/
87+
async blur(): Promise<void> {
88+
return (await this._input()).blur();
89+
}
90+
91+
/**
92+
* Puts the radio-button in a checked state by clicking it if it is currently unchecked,
93+
* or doing nothing if it is already checked. Returns a void promise that indicates when
94+
* the action is complete.
95+
*/
96+
async check(): Promise<void> {
97+
if (!(await this.isChecked())) {
98+
return (await this._clickLabel()).click();
99+
}
100+
}
101+
}

src/material-experimental/mdc-radio/radio.spec.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)