Skip to content

Commit 9e64b40

Browse files
committed
feat(material-experimental): add test harness for radio-group
Follow-up for 583af19. This commit introduces a new test harness for the `mat-radio-group` implementation. Note that we can't provide harness methods for getting the selected value because the selected value or the value of a radio-button are part of the component-internal state (no way to determine from DOM)
1 parent 67532db commit 9e64b40

File tree

3 files changed

+297
-9
lines changed

3 files changed

+297
-9
lines changed

src/material-experimental/mdc-radio/harness/radio-harness-filters.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
export type RadioGroupHarnessFilters = {
10+
id?: string;
11+
name?: string;
12+
};
13+
914
export type RadioButtonHarnessFilters = {
1015
label?: string|RegExp,
1116
id?: string;

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

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import {Component} from '@angular/core';
44
import {ComponentFixture, TestBed} from '@angular/core/testing';
55
import {ReactiveFormsModule} from '@angular/forms';
66
import {MatRadioModule} from '@angular/material/radio';
7-
import {MatRadioButtonHarness} from './radio-harness';
7+
import {MatRadioButtonHarness, MatRadioGroupHarness} from './radio-harness';
88

99
let fixture: ComponentFixture<MultipleRadioButtonsHarnessTest>;
1010
let loader: HarnessLoader;
1111
let radioButtonHarness: typeof MatRadioButtonHarness;
12+
let radioGroupHarness: typeof MatRadioGroupHarness;
1213

13-
describe('MatRadioButtonHarness', () => {
14+
describe('standard radio harnesses', () => {
1415
describe('non-MDC-based', () => {
1516
beforeEach(async () => {
1617
await TestBed
@@ -24,9 +25,11 @@ describe('MatRadioButtonHarness', () => {
2425
fixture.detectChanges();
2526
loader = TestbedHarnessEnvironment.loader(fixture);
2627
radioButtonHarness = MatRadioButtonHarness;
28+
radioGroupHarness = MatRadioGroupHarness;
2729
});
2830

29-
runTests();
31+
describe('MatRadioButtonHarness', () => runRadioButtonTests());
32+
describe('MatRadioGroupHarness', () => runRadioGroupTests());
3033
});
3134

3235
describe(
@@ -36,11 +39,119 @@ describe('MatRadioButtonHarness', () => {
3639
});
3740
});
3841

42+
/** Shared tests to run on both the original and MDC-based radio-group's. */
43+
function runRadioGroupTests() {
44+
it('should load all radio-group harnesses', async () => {
45+
const groups = await loader.getAllHarnesses(radioGroupHarness);
46+
expect(groups.length).toBe(3);
47+
});
48+
49+
it('should load radio-group with exact id', async () => {
50+
const groups = await loader.getAllHarnesses(radioGroupHarness.with({id: 'my-group-2'}));
51+
expect(groups.length).toBe(1);
52+
});
53+
54+
it('should load radio-group by name', async () => {
55+
let groups = await loader.getAllHarnesses(radioGroupHarness.with({name: 'my-group-2-name'}));
56+
expect(groups.length).toBe(1);
57+
expect(await groups[0].getId()).toBe('my-group-2');
58+
59+
groups = await loader.getAllHarnesses(radioGroupHarness.with({name: 'my-group-1-name'}));
60+
expect(groups.length).toBe(1);
61+
expect(await groups[0].getId()).toBe('my-group-1');
62+
});
63+
64+
it('should throw when finding radio-group with specific name that has mismatched ' +
65+
'radio-button names',
66+
async () => {
67+
fixture.componentInstance.thirdGroupButtonName = 'other-name';
68+
fixture.detectChanges();
69+
70+
let errorMessage: string|null = null;
71+
try {
72+
await loader.getAllHarnesses(radioGroupHarness.with({name: 'third-group-name'}));
73+
} catch (e) {
74+
errorMessage = e.toString();
75+
}
76+
77+
expect(errorMessage)
78+
.toMatch(
79+
/locator found a radio-group with name "third-group-name".*have mismatching names/);
80+
});
81+
82+
it('should get name of radio-group', async () => {
83+
const groups = await loader.getAllHarnesses(radioGroupHarness);
84+
expect(groups.length).toBe(3);
85+
expect(await groups[0].getName()).toBe('my-group-1-name');
86+
expect(await groups[1].getName()).toBe('my-group-2-name');
87+
expect(await groups[2].getName()).toBe('third-group-name');
88+
89+
fixture.componentInstance.secondGroupId = 'new-group';
90+
fixture.detectChanges();
91+
92+
expect(await groups[1].getName()).toBe('new-group-name');
93+
94+
fixture.componentInstance.thirdGroupButtonName = 'other-button-name';
95+
fixture.detectChanges();
96+
97+
let errorMessage: string|null = null;
98+
try {
99+
await groups[2].getName();
100+
} catch (e) {
101+
errorMessage = e.toString();
102+
}
103+
104+
expect(errorMessage).toMatch(/Radio buttons in radio-group have mismatching names./);
105+
});
106+
107+
it('should get id of radio-group', async () => {
108+
const groups = await loader.getAllHarnesses(radioGroupHarness);
109+
expect(groups.length).toBe(3);
110+
expect(await groups[0].getId()).toBe('my-group-1');
111+
expect(await groups[1].getId()).toBe('my-group-2');
112+
expect(await groups[2].getId()).toBe('');
113+
114+
fixture.componentInstance.secondGroupId = 'new-group-name';
115+
fixture.detectChanges();
116+
117+
expect(await groups[1].getId()).toBe('new-group-name');
118+
});
119+
120+
it('should get selected value of radio-group', async () => {
121+
const [firstGroup, secondGroup] = await loader.getAllHarnesses(radioGroupHarness);
122+
expect(await firstGroup.getSelectedValue()).toBe('opt2');
123+
expect(await secondGroup.getSelectedValue()).toBe(null);
124+
});
125+
126+
it('should get radio-button harnesses of radio-group', async () => {
127+
const groups = await loader.getAllHarnesses(radioGroupHarness);
128+
expect(groups.length).toBe(3);
129+
130+
expect((await groups[0].getRadioButtons()).length).toBe(3);
131+
expect((await groups[1].getRadioButtons()).length).toBe(1);
132+
expect((await groups[2].getRadioButtons()).length).toBe(2);
133+
});
134+
135+
it('should get selected radio-button harnesses of radio-group', async () => {
136+
const groups = await loader.getAllHarnesses(radioGroupHarness);
137+
expect(groups.length).toBe(3);
138+
139+
const groupOneSelected = await groups[0].getSelectedRadioButton();
140+
const groupTwoSelected = await groups[1].getSelectedRadioButton();
141+
const groupThreeSelected = await groups[2].getSelectedRadioButton();
142+
143+
expect(groupOneSelected).not.toBeNull();
144+
expect(groupTwoSelected).toBeNull();
145+
expect(groupThreeSelected).toBeNull();
146+
expect(await groupOneSelected!.getId()).toBe('opt2-group-one');
147+
});
148+
}
149+
39150
/** Shared tests to run on both the original and MDC-based radio-button's. */
40-
function runTests() {
151+
function runRadioButtonTests() {
41152
it('should load all radio-button harnesses', async () => {
42153
const radios = await loader.getAllHarnesses(radioButtonHarness);
43-
expect(radios.length).toBe(4);
154+
expect(radios.length).toBe(9);
44155
});
45156

46157
it('should load radio-button with exact label', async () => {
@@ -85,6 +196,13 @@ function runTests() {
85196
expect(await thirdRadio.getLabelText()).toBe('Option #3');
86197
});
87198

199+
it('should get value', async () => {
200+
const [firstRadio, secondRadio, thirdRadio] = await loader.getAllHarnesses(radioButtonHarness);
201+
expect(await firstRadio.getValue()).toBe('opt1');
202+
expect(await secondRadio.getValue()).toBe('opt2');
203+
expect(await thirdRadio.getValue()).toBe('opt3');
204+
});
205+
88206
it('should get disabled state', async () => {
89207
const [firstRadio] = await loader.getAllHarnesses(radioButtonHarness);
90208
expect(await firstRadio.isDisabled()).toBe(false);
@@ -141,6 +259,7 @@ function runTests() {
141259
expect(await radioButton.isRequired()).toBe(true);
142260
});
143261
}
262+
144263
function getActiveElementTagName() {
145264
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
146265
}
@@ -157,12 +276,32 @@ function getActiveElementTagName() {
157276
Option #{{i + 1}}
158277
</mat-radio-button>
159278
160-
<mat-radio-button id="required-radio" required name="acceptsTerms">
161-
Accept terms of conditions
162-
</mat-radio-button>
279+
<mat-radio-group id="my-group-1" name="my-group-1-name">
280+
<mat-radio-button *ngFor="let value of values"
281+
[checked]="value === 'opt2'"
282+
[value]="value"
283+
[id]="value + '-group-one'">
284+
{{value}}
285+
</mat-radio-button>
286+
</mat-radio-group>
287+
288+
289+
<mat-radio-group [id]="secondGroupId" [name]="secondGroupId + '-name'">
290+
<mat-radio-button id="required-radio" required [value]="true">
291+
Accept terms of conditions
292+
</mat-radio-button>
293+
</mat-radio-group>
294+
295+
<mat-radio-group [name]="thirdGroupName">
296+
<mat-radio-button [value]="true">First</mat-radio-button>
297+
<mat-radio-button [value]="false" [name]="thirdGroupButtonName"></mat-radio-button>
298+
</mat-radio-group>
163299
`
164300
})
165301
class MultipleRadioButtonsHarnessTest {
166302
values = ['opt1', 'opt2', 'opt3'];
167303
disableAll = false;
304+
secondGroupId = 'my-group-2';
305+
thirdGroupName: string = 'third-group-name';
306+
thirdGroupButtonName: string|undefined = undefined;
168307
}

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

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,139 @@
88

99
import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11-
import {RadioButtonHarnessFilters} from './radio-harness-filters';
11+
import {RadioButtonHarnessFilters, RadioGroupHarnessFilters} from './radio-harness-filters';
12+
13+
/**
14+
* Harness for interacting with a standard mat-radio-group in tests.
15+
* @dynamic
16+
*/
17+
export class MatRadioGroupHarness extends ComponentHarness {
18+
static hostSelector = 'mat-radio-group';
19+
20+
/**
21+
* Gets a `HarnessPredicate` that can be used to search for a radio-group with
22+
* specific attributes.
23+
* @param options Options for narrowing the search:
24+
* - `id` finds a radio-group with specific id.
25+
* - `name` finds a radio-group with specific name.
26+
* @return a `HarnessPredicate` configured with the given options.
27+
*/
28+
static with(options: RadioGroupHarnessFilters = {}): HarnessPredicate<MatRadioGroupHarness> {
29+
return new HarnessPredicate(MatRadioGroupHarness)
30+
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id)
31+
.addOption('name', options.name, this._checkRadioGroupName);
32+
}
33+
34+
private _radioButtons = this.locatorForAll(MatRadioButtonHarness);
35+
36+
/** Gets the name of the radio-group. */
37+
async getName(): Promise<string|null> {
38+
const hostName = await this._getGroupNameFromHost();
39+
// It's not possible to always determine the "name" of a radio-group by reading
40+
// the attribute. This is because the radio-group does not set the "name" as an
41+
// element attribute if the "name" value is set through a binding.
42+
if (hostName !== null) {
43+
return hostName;
44+
}
45+
// In case we couldn't determine the "name" of a radio-group by reading the
46+
// "name" attribute, we try to determine the "name" of the group by going
47+
// through all radio buttons.
48+
const radioNames = await this._getNamesFromRadioButtons();
49+
if (!radioNames.length) {
50+
return null;
51+
}
52+
if (!this._checkRadioNamesInGroupEqual(radioNames)) {
53+
throw Error('Radio buttons in radio-group have mismatching names.');
54+
}
55+
return radioNames[0]!;
56+
}
57+
58+
/** Gets the id of the radio-group. */
59+
async getId(): Promise<string|null> {
60+
return (await this.host()).getAttribute('id');
61+
}
62+
63+
/** Gets the selected radio-button in a radio-group. */
64+
async getSelectedRadioButton(): Promise<MatRadioButtonHarness|null> {
65+
for (let radioButton of await this.getRadioButtons()) {
66+
if (await radioButton.isChecked()) {
67+
return radioButton;
68+
}
69+
}
70+
return null;
71+
}
72+
73+
/** Gets the selected value of the radio-group. */
74+
async getSelectedValue(): Promise<string|null> {
75+
const selectedRadio = await this.getSelectedRadioButton();
76+
if (!selectedRadio) {
77+
return null;
78+
}
79+
return selectedRadio.getValue();
80+
}
81+
82+
/** Gets all radio buttons which are part of the radio-group. */
83+
async getRadioButtons(): Promise<MatRadioButtonHarness[]> {
84+
return (await this._radioButtons());
85+
}
86+
87+
private async _getGroupNameFromHost() {
88+
return (await this.host()).getAttribute('name');
89+
}
90+
91+
private async _getNamesFromRadioButtons(): Promise<string[]> {
92+
const groupNames: string[] = [];
93+
for (let radio of await this.getRadioButtons()) {
94+
const radioName = await radio.getName();
95+
if (radioName !== null) {
96+
groupNames.push(radioName);
97+
}
98+
}
99+
return groupNames;
100+
}
101+
102+
/** Checks if the specified radio names are all equal. */
103+
private _checkRadioNamesInGroupEqual(radioNames: string[]): boolean {
104+
let groupName: string|null = null;
105+
for (let radioName of radioNames) {
106+
if (groupName === null) {
107+
groupName = radioName;
108+
} else if (groupName !== radioName) {
109+
return false;
110+
}
111+
}
112+
return true;
113+
}
114+
115+
/**
116+
* Checks if a radio-group harness has the given name. Throws if a radio-group with
117+
* matching name could be found but has mismatching radio-button names.
118+
*/
119+
private static async _checkRadioGroupName(harness: MatRadioGroupHarness, name: string) {
120+
// Check if there is a radio-group which has the "name" attribute set
121+
// to the expected group name. It's not possible to always determine
122+
// the "name" of a radio-group by reading the attribute. This is because
123+
// the radio-group does not set the "name" as an element attribute if the
124+
// "name" value is set through a binding.
125+
if (await harness._getGroupNameFromHost() === name) {
126+
return true;
127+
}
128+
// Check if there is a group with radio-buttons that all have the same
129+
// expected name. This implies that the group has the given name. It's
130+
// not possible to always determine the name of a radio-group through
131+
// the attribute because there is
132+
const radioNames = await harness._getNamesFromRadioButtons();
133+
if (radioNames.indexOf(name) === -1) {
134+
return false;
135+
}
136+
if (!harness._checkRadioNamesInGroupEqual(radioNames)) {
137+
throw Error(
138+
`The locator found a radio-group with name "${name}", but some ` +
139+
`radio-button's within the group have mismatching names, which is invalid.`);
140+
}
141+
return true;
142+
}
143+
}
12144

13145
/**
14146
* Harness for interacting with a standard mat-radio-button in tests.
@@ -23,6 +155,7 @@ export class MatRadioButtonHarness extends ComponentHarness {
23155
* @param options Options for narrowing the search:
24156
* - `label` finds a radio-button with specific label text.
25157
* - `name` finds a radio-button with specific name.
158+
* - `id` finds a radio-button with specific id.
26159
* @return a `HarnessPredicate` configured with the given options.
27160
*/
28161
static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> {
@@ -67,6 +200,17 @@ export class MatRadioButtonHarness extends ComponentHarness {
67200
return (await this.host()).getAttribute('id');
68201
}
69202

203+
/**
204+
* Gets the value of the radio-button. The radio-button value will be
205+
* converted to a string.
206+
*
207+
* Note that this means that radio-button's with objects as value will
208+
* intentionally have the `[object Object]` as return value.
209+
*/
210+
async getValue(): Promise<string|null> {
211+
return (await this._input()).getAttribute('value');
212+
}
213+
70214
/** Gets a promise for the radio-button's label text. */
71215
async getLabelText(): Promise<string> {
72216
return (await this._textLabel()).text();

0 commit comments

Comments
 (0)