diff --git a/src/material-experimental/mdc-radio/BUILD.bazel b/src/material-experimental/mdc-radio/BUILD.bazel index 548d9a72510a..eb74204552e2 100644 --- a/src/material-experimental/mdc-radio/BUILD.bazel +++ b/src/material-experimental/mdc-radio/BUILD.bazel @@ -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( @@ -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"]), diff --git a/src/material-experimental/mdc-radio/harness/BUILD.bazel b/src/material-experimental/mdc-radio/harness/BUILD.bazel new file mode 100644 index 000000000000..28a1dd42d816 --- /dev/null +++ b/src/material-experimental/mdc-radio/harness/BUILD.bazel @@ -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", + ], +) diff --git a/src/material-experimental/mdc-radio/harness/radio-harness-filters.ts b/src/material-experimental/mdc-radio/harness/radio-harness-filters.ts new file mode 100644 index 000000000000..e24a23e5ed4f --- /dev/null +++ b/src/material-experimental/mdc-radio/harness/radio-harness-filters.ts @@ -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, +}; diff --git a/src/material-experimental/mdc-radio/harness/radio-harness.spec.ts b/src/material-experimental/mdc-radio/harness/radio-harness.spec.ts new file mode 100644 index 000000000000..0c911dad6acd --- /dev/null +++ b/src/material-experimental/mdc-radio/harness/radio-harness.spec.ts @@ -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; +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: ` + + Option #{{i + 1}} + + + + Accept terms of conditions + + ` +}) +class MultipleRadioButtonsHarnessTest { + values = ['opt1', 'opt2', 'opt3']; + disableAll = false; +} diff --git a/src/material-experimental/mdc-radio/harness/radio-harness.ts b/src/material-experimental/mdc-radio/harness/radio-harness.ts new file mode 100644 index 000000000000..6d2fd82c117d --- /dev/null +++ b/src/material-experimental/mdc-radio/harness/radio-harness.ts @@ -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 { + 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 { + const checked = (await this._input()).getAttribute('checked'); + return coerceBooleanProperty(await checked); + } + + /** Whether the radio-button is disabled. */ + async isDisabled(): Promise { + const disabled = (await this._input()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Whether the radio-button is required. */ + async isRequired(): Promise { + const required = (await this._input()).getAttribute('required'); + return coerceBooleanProperty(await required); + } + + /** Gets a promise for the radio-button's name. */ + async getName(): Promise { + return (await this._input()).getAttribute('name'); + } + + /** Gets a promise for the radio-button's id. */ + async getId(): Promise { + return (await this.host()).getAttribute('id'); + } + + /** Gets a promise for the radio-button's label text. */ + async getLabelText(): Promise { + return (await this._textLabel()).text(); + } + + /** + * Focuses the radio-button and returns a void promise that indicates when the + * action is complete. + */ + async focus(): Promise { + return (await this._input()).focus(); + } + + /** + * Blurs the radio-button and returns a void promise that indicates when the + * action is complete. + */ + async blur(): Promise { + 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 { + if (!(await this.isChecked())) { + return (await this._clickLabel()).click(); + } + } +} diff --git a/src/material-experimental/mdc-radio/radio.spec.ts b/src/material-experimental/mdc-radio/radio.spec.ts deleted file mode 100644 index 4882d270483e..000000000000 --- a/src/material-experimental/mdc-radio/radio.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: copy tests from existing mat-radio, update as necessary to fix.