Skip to content

Commit 4d06a1f

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 4d06a1f

File tree

3 files changed

+293
-9
lines changed

3 files changed

+293
-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: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,133 @@
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, async (harness, name) => {
32+
// Check if there is a radio-group which has the "name" attribute set
33+
// to the expected group name. It's not possible to always determine
34+
// the "name" of a radio-group by reading the attribute. This is because
35+
// the radio-group does not set the "name" as an element attribute if the
36+
// "name" value is set through a binding.
37+
if (await harness._getGroupNameFromHost() === name) {
38+
return true;
39+
}
40+
// Check if there is a group with radio-buttons that all have the same
41+
// expected name. This implies that the group has the given name. It's
42+
// not possible to always determine the name of a radio-group through
43+
// the attribute because there is
44+
const radioNames = await harness._getNamesFromRadioButtons();
45+
if (radioNames.indexOf(name) === -1) {
46+
return false;
47+
}
48+
if (!harness._checkRadioNamesInGroupEqual(radioNames)) {
49+
throw Error(
50+
`The locator found a radio-group with name "${name}", but some ` +
51+
`radio-button's within the group have mismatching names, which is invalid.`);
52+
}
53+
return true;
54+
});
55+
}
56+
57+
private _radioButtons = this.locatorForAll(MatRadioButtonHarness);
58+
59+
/** Gets the name of the radio-group. */
60+
async getName(): Promise<string|null> {
61+
const hostName = await this._getGroupNameFromHost();
62+
// It's not possible to always determine the "name" of a radio-group by reading
63+
// the attribute. This is because the radio-group does not set the "name" as an
64+
// element attribute if the "name" value is set through a binding.
65+
if (hostName !== null) {
66+
return hostName;
67+
}
68+
// In case we couldn't determine the "name" of a radio-group by reading the
69+
// "name" attribute, we try to determine the "name" of the group by going
70+
// through all radio buttons.
71+
const radioNames = await this._getNamesFromRadioButtons();
72+
if (!radioNames.length) {
73+
return null;
74+
}
75+
if (!this._checkRadioNamesInGroupEqual(radioNames)) {
76+
throw Error('Radio buttons in radio-group have mismatching names.');
77+
}
78+
return radioNames[0]!;
79+
}
80+
81+
/** Gets the id of the radio-group. */
82+
async getId(): Promise<string|null> {
83+
return (await this.host()).getAttribute('id');
84+
}
85+
86+
/** Gets the selected radio-button in a radio-group. */
87+
async getSelectedRadioButton(): Promise<MatRadioButtonHarness|null> {
88+
for (let radioButton of await this.getRadioButtons()) {
89+
if (await radioButton.isChecked()) {
90+
return radioButton;
91+
}
92+
}
93+
return null;
94+
}
95+
96+
/** Gets the selected value of the radio-group. */
97+
async getSelectedValue(): Promise<string|null> {
98+
const selectedRadio = await this.getSelectedRadioButton();
99+
if (!selectedRadio) {
100+
return null;
101+
}
102+
return selectedRadio.getValue();
103+
}
104+
105+
/** Gets all radio buttons which are part of the radio-group. */
106+
async getRadioButtons(): Promise<MatRadioButtonHarness[]> {
107+
return (await this._radioButtons());
108+
}
109+
110+
private async _getGroupNameFromHost() {
111+
return (await this.host()).getAttribute('name');
112+
}
113+
114+
private async _getNamesFromRadioButtons(): Promise<string[]> {
115+
const groupNames: string[] = [];
116+
for (let radio of await this.getRadioButtons()) {
117+
const radioName = await radio.getName();
118+
if (radioName !== null) {
119+
groupNames.push(radioName);
120+
}
121+
}
122+
return groupNames;
123+
}
124+
125+
/** Checks if the specified radio names are all equal. */
126+
private _checkRadioNamesInGroupEqual(radioNames: string[]): boolean {
127+
let groupName: string|null = null;
128+
for (let radioName of radioNames) {
129+
if (groupName === null) {
130+
groupName = radioName;
131+
} else if (groupName !== radioName) {
132+
return false;
133+
}
134+
}
135+
return true;
136+
}
137+
}
12138

13139
/**
14140
* Harness for interacting with a standard mat-radio-button in tests.
@@ -23,6 +149,7 @@ export class MatRadioButtonHarness extends ComponentHarness {
23149
* @param options Options for narrowing the search:
24150
* - `label` finds a radio-button with specific label text.
25151
* - `name` finds a radio-button with specific name.
152+
* - `id` finds a radio-button with specific id.
26153
* @return a `HarnessPredicate` configured with the given options.
27154
*/
28155
static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> {
@@ -67,6 +194,17 @@ export class MatRadioButtonHarness extends ComponentHarness {
67194
return (await this.host()).getAttribute('id');
68195
}
69196

197+
/**
198+
* Gets the value of the radio-button. The radio-button value will be
199+
* converted to a string.
200+
*
201+
* Note that this means that radio-button's with objects as value will
202+
* intentionally have the `[object Object]` as return value.
203+
*/
204+
async getValue(): Promise<string|null> {
205+
return (await this._input()).getAttribute('value');
206+
}
207+
70208
/** Gets a promise for the radio-button's label text. */
71209
async getLabelText(): Promise<string> {
72210
return (await this._textLabel()).text();
@@ -99,3 +237,5 @@ export class MatRadioButtonHarness extends ComponentHarness {
99237
}
100238
}
101239
}
240+
241+
export function checkElementsInArrayEqual(array: string[]) {}

0 commit comments

Comments
 (0)