Skip to content

Commit

Permalink
fix(material-experimental/mdc-radio): de-duplicate test harness logic (
Browse files Browse the repository at this point in the history
…#21532)

Changes the MDC-based radio harnesses to extend the base ones directly since the logic
is identical, aside from some selectors.

(cherry picked from commit 9d92927)
  • Loading branch information
crisbeto authored and andrewseguin committed Jan 25, 2021
1 parent f8c3597 commit dfd566a
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 278 deletions.
5 changes: 5 additions & 0 deletions scripts/check-mdc-exports-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export const config = {
'_MatRadioGroupBase',
'_MatRadioButtonBase',
],
'mdc-radio/testing': [
// Private base classes that are only exported for MDC.
'_MatRadioGroupHarnessBase',
'_MatRadioButtonHarnessBase',
],
'mdc-select': [
// Private base class that is only exported for MDC.
'_MatSelectBase'
Expand Down
1 change: 0 additions & 1 deletion src/material-experimental/mdc-radio/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ ts_library(
),
module_name = "@angular/material-experimental/mdc-radio/testing",
deps = [
"//src/cdk/coercion",
"//src/cdk/testing",
"//src/material/radio/testing",
],
Expand Down
2 changes: 1 addition & 1 deletion src/material-experimental/mdc-radio/testing/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
*/

export * from './radio-harness';
export * from './radio-harness-filters';
export {RadioButtonHarnessFilters, RadioGroupHarnessFilters} from '@angular/material/radio/testing';

This file was deleted.

236 changes: 26 additions & 210 deletions src/material-experimental/mdc-radio/testing/radio-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,155 +6,39 @@
* found in the LICENSE file at https://angular.io/license
*/

import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
import {RadioButtonHarnessFilters, RadioGroupHarnessFilters} from './radio-harness-filters';
import {HarnessPredicate} from '@angular/cdk/testing';
import {
RadioButtonHarnessFilters,
RadioGroupHarnessFilters,
_MatRadioGroupHarnessBase,
_MatRadioButtonHarnessBase,
} from '@angular/material/radio/testing';

/** Harness for interacting with an MDC-based mat-radio-group in tests. */
export class MatRadioGroupHarness extends ComponentHarness {
export class MatRadioGroupHarness extends _MatRadioGroupHarnessBase<
typeof MatRadioButtonHarness,
MatRadioButtonHarness,
RadioButtonHarnessFilters
> {
/** The selector for the host element of a `MatRadioGroup` instance. */
static hostSelector = '.mat-mdc-radio-group';
protected _buttonClass = MatRadioButtonHarness;

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatRadioGroupHarness` that meets
* certain criteria.
* @param options Options for filtering which radio group instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: RadioGroupHarnessFilters = {}): HarnessPredicate<MatRadioGroupHarness> {
return new HarnessPredicate(MatRadioGroupHarness, options)
static with(options: RadioGroupHarnessFilters = {}):
HarnessPredicate<MatRadioGroupHarness> {
return new HarnessPredicate<MatRadioGroupHarness>(MatRadioGroupHarness, options)
.addOption('name', options.name, this._checkRadioGroupName);
}

/** Gets the name of the radio-group. */
async getName(): Promise<string|null> {
const hostName = await this._getGroupNameFromHost();
// It's not possible to always determine the "name" of a radio-group by reading
// the attribute. This is because the radio-group does not set the "name" as an
// element attribute if the "name" value is set through a binding.
if (hostName !== null) {
return hostName;
}
// In case we couldn't determine the "name" of a radio-group by reading the
// "name" attribute, we try to determine the "name" of the group by going
// through all radio buttons.
const radioNames = await this._getNamesFromRadioButtons();
if (!radioNames.length) {
return null;
}
if (!this._checkRadioNamesInGroupEqual(radioNames)) {
throw Error('Radio buttons in radio-group have mismatching names.');
}
return radioNames[0]!;
}

/** Gets the id of the radio-group. */
async getId(): Promise<string|null> {
return (await this.host()).getProperty('id');
}

/** Gets the checked radio-button in a radio-group. */
async getCheckedRadioButton(): Promise<MatRadioButtonHarness|null> {
for (let radioButton of await this.getRadioButtons()) {
if (await radioButton.isChecked()) {
return radioButton;
}
}
return null;
}

/** Gets the checked value of the radio-group. */
async getCheckedValue(): Promise<string|null> {
const checkedRadio = await this.getCheckedRadioButton();
if (!checkedRadio) {
return null;
}
return checkedRadio.getValue();
}

/**
* Gets a list of radio buttons which are part of the radio-group.
* @param filter Optionally filters which radio buttons are included.
*/
async getRadioButtons(filter: RadioButtonHarnessFilters = {}): Promise<MatRadioButtonHarness[]> {
return this.locatorForAll(MatRadioButtonHarness.with(filter))();
}

/**
* Checks a radio button in this group.
* @param filter An optional filter to apply to the child radio buttons. The first tab matching
* the filter will be selected.
*/
async checkRadioButton(filter: RadioButtonHarnessFilters = {}): Promise<void> {
const radioButtons = await this.getRadioButtons(filter);
if (!radioButtons.length) {
throw Error(`Could not find radio button matching ${JSON.stringify(filter)}`);
}
return radioButtons[0].check();
}

/** Gets the name attribute of the host element. */
private async _getGroupNameFromHost() {
return (await this.host()).getAttribute('name');
}

/** Gets a list of the name attributes of all child radio buttons. */
private async _getNamesFromRadioButtons(): Promise<string[]> {
const groupNames: string[] = [];
for (let radio of await this.getRadioButtons()) {
const radioName = await radio.getName();
if (radioName !== null) {
groupNames.push(radioName);
}
}
return groupNames;
}

/** Checks if the specified radio names are all equal. */
private _checkRadioNamesInGroupEqual(radioNames: string[]): boolean {
let groupName: string|null = null;
for (let radioName of radioNames) {
if (groupName === null) {
groupName = radioName;
} else if (groupName !== radioName) {
return false;
}
}
return true;
}

/**
* Checks if a radio-group harness has the given name. Throws if a radio-group with
* matching name could be found but has mismatching radio-button names.
*/
private static async _checkRadioGroupName(harness: MatRadioGroupHarness, name: string) {
// Check if there is a radio-group which has the "name" attribute set
// to the expected group name. It's not possible to always determine
// the "name" of a radio-group by reading the attribute. This is because
// the radio-group does not set the "name" as an element attribute if the
// "name" value is set through a binding.
if (await harness._getGroupNameFromHost() === name) {
return true;
}
// Check if there is a group with radio-buttons that all have the same
// expected name. This implies that the group has the given name. It's
// not possible to always determine the name of a radio-group through
// the attribute because there is
const radioNames = await harness._getNamesFromRadioButtons();
if (radioNames.indexOf(name) === -1) {
return false;
}
if (!harness._checkRadioNamesInGroupEqual(radioNames)) {
throw Error(
`The locator found a radio-group with name "${name}", but some ` +
`radio-button's within the group have mismatching names, which is invalid.`);
}
return true;
}
}

/** Harness for interacting with an MDC-based mat-radio-button in tests. */
export class MatRadioButtonHarness extends ComponentHarness {
export class MatRadioButtonHarness extends _MatRadioButtonHarnessBase {
/** The selector for the host element of a `MatRadioButton` instance. */
static hostSelector = '.mat-mdc-radio-button';

Expand All @@ -164,83 +48,15 @@ export class MatRadioButtonHarness extends ComponentHarness {
* @param options Options for filtering which radio button instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> {
return new HarnessPredicate(MatRadioButtonHarness, options)
.addOption(
'label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
.addOption(
'name', options.name, async (harness, name) => (await harness.getName()) === name);
static with(options: RadioButtonHarnessFilters = {}):
HarnessPredicate<MatRadioButtonHarness> {
return new HarnessPredicate<MatRadioButtonHarness>(MatRadioButtonHarness, options)
.addOption('label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
.addOption('name', options.name,
async (harness, name) => (await harness.getName()) === name);
}

private _label = this.locatorFor('label');
private _input = this.locatorFor('input');

/** Whether the radio-button is checked. */
async isChecked(): Promise<boolean> {
const checked = (await this._input()).getProperty('checked');
return coerceBooleanProperty(await checked);
}

/** Whether the radio-button is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this._input()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
}

/** Whether the radio-button is required. */
async isRequired(): Promise<boolean> {
const required = (await this._input()).getAttribute('required');
return coerceBooleanProperty(await required);
}

/** Gets the radio-button's name. */
async getName(): Promise<string|null> {
return (await this._input()).getAttribute('name');
}

/** Gets the radio-button's id. */
async getId(): Promise<string|null> {
return (await this.host()).getProperty('id');
}

/**
* Gets the value of the radio-button. The radio-button value will be converted to a string.
*
* Note: This means that for radio-button's with an object as a value `[object Object]` is
* intentionally returned.
*/
async getValue(): Promise<string|null> {
return (await this._input()).getProperty('value');
}

/** Gets the radio-button's label text. */
async getLabelText(): Promise<string> {
return (await this._label()).text();
}

/** Focuses the radio-button. */
async focus(): Promise<void> {
return (await this._input()).focus();
}

/** Blurs the radio-button. */
async blur(): Promise<void> {
return (await this._input()).blur();
}

/** Whether the radio-button is focused. */
async isFocused(): Promise<boolean> {
return (await this._input()).isFocused();
}

/**
* Puts the radio-button in a checked state by clicking it if it is currently unchecked,
* or doing nothing if it is already checked.
*/
async check(): Promise<void> {
if (!(await this.isChecked())) {
return (await this._label()).click();
}
}
protected _textLabel = this.locatorFor('label');
protected _clickLabel = this._textLabel;
}

0 comments on commit dfd566a

Please sign in to comment.