Skip to content

feat(material-experimental): add test harness for radio-button #16593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/material-experimental/mdc-radio/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package(default_visibility = ["//visibility:public"])

load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library")
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module")
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_web_test_suite")
load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")

ng_module(
Expand Down Expand Up @@ -31,6 +31,15 @@ sass_binary(
src = "radio.scss",
)

ng_web_test_suite(
name = "unit_tests",
static_files = ["@npm//:node_modules/@material/radio/dist/mdc.radio.js"],
deps = [
"//src/material-experimental:mdc_require_config.js",
"//src/material-experimental/mdc-radio/harness:harness_tests",
],
)

ng_e2e_test_library(
name = "e2e_test_sources",
srcs = glob(["**/*.e2e.spec.ts"]),
Expand Down
27 changes: 27 additions & 0 deletions src/material-experimental/mdc-radio/harness/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ng_test_library", "ts_library")

ts_library(
name = "harness",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
deps = [
"//src/cdk-experimental/testing",
"//src/cdk/coercion",
],
)

ng_test_library(
name = "harness_tests",
srcs = glob(["**/*.spec.ts"]),
deps = [
":harness",
"//src/cdk-experimental/testing",
"//src/cdk-experimental/testing/testbed",
"//src/material/radio",
"@npm//@angular/forms",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export type RadioButtonHarnessFilters = {
label?: string|RegExp,
id?: string;
name?: string,
};
168 changes: 168 additions & 0 deletions src/material-experimental/mdc-radio/harness/radio-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {HarnessLoader} from '@angular/cdk-experimental/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ReactiveFormsModule} from '@angular/forms';
import {MatRadioModule} from '@angular/material/radio';
import {MatRadioButtonHarness} from './radio-harness';

let fixture: ComponentFixture<MultipleRadioButtonsHarnessTest>;
let loader: HarnessLoader;
let radioButtonHarness: typeof MatRadioButtonHarness;

describe('MatRadioButtonHarness', () => {
describe('non-MDC-based', () => {
beforeEach(async () => {
await TestBed
.configureTestingModule({
imports: [MatRadioModule, ReactiveFormsModule],
declarations: [MultipleRadioButtonsHarnessTest],
})
.compileComponents();

fixture = TestBed.createComponent(MultipleRadioButtonsHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
radioButtonHarness = MatRadioButtonHarness;
});

runTests();
});

describe(
'MDC-based',
() => {
// TODO: run tests for MDC based radio-button once implemented.
});
});

/** Shared tests to run on both the original and MDC-based radio-button's. */
function runTests() {
it('should load all radio-button harnesses', async () => {
const radios = await loader.getAllHarnesses(radioButtonHarness);
expect(radios.length).toBe(4);
});

it('should load radio-button with exact label', async () => {
const radios = await loader.getAllHarnesses(radioButtonHarness.with({label: 'Option #2'}));
expect(radios.length).toBe(1);
expect(await radios[0].getId()).toBe('opt2');
expect(await radios[0].getLabelText()).toBe('Option #2');
});

it('should load radio-button with regex label match', async () => {
const radios = await loader.getAllHarnesses(radioButtonHarness.with({label: /#3$/i}));
expect(radios.length).toBe(1);
expect(await radios[0].getId()).toBe('opt3');
expect(await radios[0].getLabelText()).toBe('Option #3');
});

it('should load radio-button with id', async () => {
const radios = await loader.getAllHarnesses(radioButtonHarness.with({id: 'opt3'}));
expect(radios.length).toBe(1);
expect(await radios[0].getId()).toBe('opt3');
expect(await radios[0].getLabelText()).toBe('Option #3');
});

it('should load radio-buttons with same name', async () => {
const radios = await loader.getAllHarnesses(radioButtonHarness.with({name: 'group1'}));
expect(radios.length).toBe(2);

expect(await radios[0].getId()).toBe('opt1');
expect(await radios[1].getId()).toBe('opt2');
});

it('should get checked state', async () => {
const [uncheckedRadio, checkedRadio] = await loader.getAllHarnesses(radioButtonHarness);
expect(await uncheckedRadio.isChecked()).toBe(false);
expect(await checkedRadio.isChecked()).toBe(true);
});

it('should get label text', async () => {
const [firstRadio, secondRadio, thirdRadio] = await loader.getAllHarnesses(radioButtonHarness);
expect(await firstRadio.getLabelText()).toBe('Option #1');
expect(await secondRadio.getLabelText()).toBe('Option #2');
expect(await thirdRadio.getLabelText()).toBe('Option #3');
});

it('should get disabled state', async () => {
const [firstRadio] = await loader.getAllHarnesses(radioButtonHarness);
expect(await firstRadio.isDisabled()).toBe(false);

fixture.componentInstance.disableAll = true;
fixture.detectChanges();

expect(await firstRadio.isDisabled()).toBe(true);
});

it('should focus radio-button', async () => {
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt2'}));
expect(getActiveElementTagName()).not.toBe('input');
await radioButton.focus();
expect(getActiveElementTagName()).toBe('input');
});

it('should blur radio-button', async () => {
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt2'}));
await radioButton.focus();
expect(getActiveElementTagName()).toBe('input');
await radioButton.blur();
expect(getActiveElementTagName()).not.toBe('input');
});

it('should check radio-button', async () => {
const [uncheckedRadio, checkedRadio] = await loader.getAllHarnesses(radioButtonHarness);
await uncheckedRadio.check();
expect(await uncheckedRadio.isChecked()).toBe(true);
// Checked radio state should change since the two radio's
// have the same name and only one can be selected.
expect(await checkedRadio.isChecked()).toBe(false);
});

it('should not be able to check disabled radio-button', async () => {
fixture.componentInstance.disableAll = true;
fixture.detectChanges();

const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'opt3'}));
expect(await radioButton.isChecked()).toBe(false);
await radioButton.check();
expect(await radioButton.isChecked()).toBe(false);

fixture.componentInstance.disableAll = false;
fixture.detectChanges();

expect(await radioButton.isChecked()).toBe(false);
await radioButton.check();
expect(await radioButton.isChecked()).toBe(true);
});

it('should get required state', async () => {
const radioButton = await loader.getHarness(radioButtonHarness.with({id: 'required-radio'}));
expect(await radioButton.isRequired()).toBe(true);
});
}
function getActiveElementTagName() {
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
}

@Component({
template: `
<mat-radio-button *ngFor="let value of values, let i = index"
[name]="value === 'opt3' ? 'group2' : 'group1'"
[disabled]="disableAll"
[checked]="value === 'opt2'"
[id]="value"
[required]="value === 'opt2'"
[value]="value">
Option #{{i + 1}}
</mat-radio-button>

<mat-radio-button id="required-radio" required name="acceptsTerms">
Accept terms of conditions
</mat-radio-button>
`
})
class MultipleRadioButtonsHarnessTest {
values = ['opt1', 'opt2', 'opt3'];
disableAll = false;
}
101 changes: 101 additions & 0 deletions src/material-experimental/mdc-radio/harness/radio-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

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

/**
* Harness for interacting with a standard mat-radio-button in tests.
* @dynamic
*/
export class MatRadioButtonHarness extends ComponentHarness {
static hostSelector = 'mat-radio-button';

/**
* Gets a `HarnessPredicate` that can be used to search for a radio-button with
* specific attributes.
* @param options Options for narrowing the search:
* - `label` finds a radio-button with specific label text.
* - `name` finds a radio-button with specific name.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> {
return new HarnessPredicate(MatRadioButtonHarness)
.addOption(
'label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
.addOption(
'name', options.name, async (harness, name) => (await harness.getName()) === name)
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id);
}

private _textLabel = this.locatorFor('.mat-radio-label-content');
private _clickLabel = this.locatorFor('.mat-radio-label');
private _input = this.locatorFor('input');

/** Whether the radio-button is checked. */
async isChecked(): Promise<boolean> {
const checked = (await this._input()).getAttribute('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 a promise for the radio-button's name. */
async getName(): Promise<string|null> {
return (await this._input()).getAttribute('name');
}

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

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

/**
* Focuses the radio-button and returns a void promise that indicates when the
* action is complete.
*/
async focus(): Promise<void> {
return (await this._input()).focus();
}

/**
* Blurs the radio-button and returns a void promise that indicates when the
* action is complete.
*/
async blur(): Promise<void> {
return (await this._input()).blur();
}

/**
* Puts the radio-button in a checked state by clicking it if it is currently unchecked,
* or doing nothing if it is already checked. Returns a void promise that indicates when
* the action is complete.
*/
async check(): Promise<void> {
if (!(await this.isChecked())) {
return (await this._clickLabel()).click();
}
}
}
1 change: 0 additions & 1 deletion src/material-experimental/mdc-radio/radio.spec.ts

This file was deleted.