diff --git a/src/material-experimental/mdc-button/BUILD.bazel b/src/material-experimental/mdc-button/BUILD.bazel index 19e90e299e1a..9cdc67e24ca5 100644 --- a/src/material-experimental/mdc-button/BUILD.bazel +++ b/src/material-experimental/mdc-button/BUILD.bazel @@ -1,14 +1,17 @@ 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_test_library", "ng_web_test_suite", "ts_library") load("//src/e2e-app:test_suite.bzl", "e2e_test_suite") ng_module( name = "mdc-button", srcs = glob( ["**/*.ts"], - exclude = ["**/*.spec.ts"], + exclude = [ + "**/*.spec.ts", + "harness/**", + ], ), assets = [ ":button_scss", @@ -22,6 +25,18 @@ ng_module( ], ) +ts_library( + name = "harness", + srcs = glob( + ["harness/**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src/cdk-experimental/testing", + "//src/cdk/coercion", + ], +) + sass_library( name = "mdc_button_scss_lib", srcs = glob(["**/_*.scss"]), @@ -71,6 +86,31 @@ sass_binary( ], ) +ng_test_library( + name = "button_tests_lib", + srcs = [ + "harness/button-harness.spec.ts", + ], + deps = [ + ":harness", + ":mdc-button", + "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/testbed", + "//src/cdk/platform", + "//src/cdk/testing", + "//src/material/button", + "@npm//@angular/platform-browser", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":button_tests_lib", + "//src/material-experimental:mdc_require_config.js", + ], +) + ng_e2e_test_library( name = "e2e_test_sources", srcs = glob(["**/*.e2e.spec.ts"]), diff --git a/src/material-experimental/mdc-button/button.spec.ts b/src/material-experimental/mdc-button/button.spec.ts deleted file mode 100644 index a48744d50226..000000000000 --- a/src/material-experimental/mdc-button/button.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: copy tests from existing mat-button, update as necessary to fix. diff --git a/src/material-experimental/mdc-button/harness/button-harness-filters.ts b/src/material-experimental/mdc-button/harness/button-harness-filters.ts new file mode 100644 index 000000000000..43cf80aec02a --- /dev/null +++ b/src/material-experimental/mdc-button/harness/button-harness-filters.ts @@ -0,0 +1,11 @@ +/** + * @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 ButtonHarnessFilters = { + text?: string | RegExp +}; diff --git a/src/material-experimental/mdc-button/harness/button-harness.spec.ts b/src/material-experimental/mdc-button/harness/button-harness.spec.ts new file mode 100644 index 000000000000..94b708c10b6c --- /dev/null +++ b/src/material-experimental/mdc-button/harness/button-harness.spec.ts @@ -0,0 +1,159 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; +import {Platform, PlatformModule} from '@angular/cdk/platform'; +import {Component} from '@angular/core'; +import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {MatButtonModule} from '@angular/material/button'; +import {MatButtonModule as MatMdcButtonModule} from '../index'; +import {MatButtonHarness} from './button-harness'; +import {MatButtonHarness as MatMdcButtonHarness} from './mdc-button-harness'; + +let fixture: ComponentFixture; +let loader: HarnessLoader; +let buttonHarness: typeof MatButtonHarness; +let platform: Platform; + +describe('MatButtonHarness', () => { + describe('non-MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatButtonModule, PlatformModule], + declarations: [ButtonHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(ButtonHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + buttonHarness = MatButtonHarness; + }); + + runTests(); + }); + + describe('MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatMdcButtonModule], + declarations: [ButtonHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(ButtonHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + // Public APIs are the same as MatButtonHarness, but cast is necessary because of different + // private fields. + buttonHarness = MatMdcButtonHarness as any; + }); + + runTests(); + }); +}); + +/** Shared tests to run on both the original and MDC-based buttons. */ +function runTests() { + beforeEach(inject([Platform], (p: Platform) => { + platform = p; + })); + + it('should load all button harnesses', async () => { + const buttons = await loader.getAllHarnesses(buttonHarness); + expect(buttons.length).toBe(14); + }); + + it('should load button with exact text', async () => { + const buttons = await loader.getAllHarnesses(buttonHarness.with({text: 'Basic button'})); + expect(buttons.length).toBe(1); + expect(await buttons[0].getText()).toBe('Basic button'); + }); + + it('should load button with regex label match', async () => { + const buttons = await loader.getAllHarnesses(buttonHarness.with({text: /basic/i})); + expect(buttons.length).toBe(2); + expect(await buttons[0].getText()).toBe('Basic button'); + expect(await buttons[1].getText()).toBe('Basic anchor'); + }); + + it('should get disabled state', async () => { + // Grab each combination of [enabled, disabled] тип [button, anchor] + const [disabledFlatButton, enabledFlatAnchor] = + await loader.getAllHarnesses(buttonHarness.with({text: /flat/i})); + const [enabledRaisedButton, disabledRaisedAnchor] = + await loader.getAllHarnesses(buttonHarness.with({text: /raised/i})); + + expect(await enabledFlatAnchor.isDisabled()).toBe(false); + expect(await disabledFlatButton.isDisabled()).toBe(true); + expect(await enabledRaisedButton.isDisabled()).toBe(false); + expect(await disabledRaisedAnchor.isDisabled()).toBe(true); + }); + + it('should get button text', async () => { + const [firstButton, secondButton] = await loader.getAllHarnesses(buttonHarness); + expect(await firstButton.getText()).toBe('Basic button'); + expect(await secondButton.getText()).toBe('Flat button'); + }); + + it('should focus and blur a button', async () => { + const button = await loader.getHarness(buttonHarness.with({text: 'Basic button'})); + expect(getActiveElementId()).not.toBe('basic'); + await button.foucs(); + expect(getActiveElementId()).toBe('basic'); + await button.blur(); + expect(getActiveElementId()).not.toBe('basic'); + }); + + it('should click a button', async () => { + const button = await loader.getHarness(buttonHarness.with({text: 'Basic button'})); + await button.click(); + + expect(fixture.componentInstance.clicked).toBe(true); + }); + + it('should not click a disabled button', async () => { + // Older versions of Edge have a bug where `disabled` buttons are still clickable if + // they contain child elements. We skip this check on Edge. + // See https://stackoverflow.com/questions/32377026/disabled-button-is-clickable-on-edge-browser + if (platform.EDGE) { + return; + } + + const button = await loader.getHarness(buttonHarness.with({text: 'Flat button'})); + await button.click(); + + expect(fixture.componentInstance.clicked).toBe(false); + }); +} + +function getActiveElementId() { + return document.activeElement ? document.activeElement.id : ''; +} + +@Component({ + // Include one of each type of button selector to ensure that they're all captured by + // the harness's selector. + template: ` + + + + + + + + + Basic anchor + Flat anchor + Raised anchor + Stroked anchor + Icon anchor + Fab anchor + Mini Fab anchor + ` +}) +class ButtonHarnessTest { + disabled = true; + clicked = false; +} + diff --git a/src/material-experimental/mdc-button/harness/button-harness.ts b/src/material-experimental/mdc-button/harness/button-harness.ts new file mode 100644 index 000000000000..967b70410376 --- /dev/null +++ b/src/material-experimental/mdc-button/harness/button-harness.ts @@ -0,0 +1,67 @@ +/** + * @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 {ButtonHarnessFilters} from './button-harness-filters'; + + +/** + * Harness for interacting with a standard mat-button in tests. + * @dynamic + */ +export class MatButtonHarness extends ComponentHarness { + // TODO(jelbourn) use a single class, like `.mat-button-base` + static hostSelector = [ + '[mat-button]', + '[mat-raised-button]', + '[mat-flat-button]', + '[mat-icon-button]', + '[mat-stroked-button]', + '[mat-fab]', + '[mat-mini-fab]', + ].join(','); + + /** + * Gets a `HarnessPredicate` that can be used to search for a button with specific attributes. + * @param options Options for narrowing the search: + * - `text` finds a button with specific text content. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: ButtonHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatButtonHarness) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } + + /** Clicks the button. */ + async click(): Promise { + return (await this.host()).click(); + } + + /** Gets a boolean promise indicating if the button is disabled. */ + async isDisabled(): Promise { + const disabled = (await this.host()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets a promise for the button's label text. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Focuses the button and returns a void promise that indicates when the action is complete. */ + async foucs(): Promise { + return (await this.host()).focus(); + } + + /** Blurs the button and returns a void promise that indicates when the action is complete. */ + async blur(): Promise { + return (await this.host()).blur(); + } +} diff --git a/src/material-experimental/mdc-button/harness/mdc-button-harness.ts b/src/material-experimental/mdc-button/harness/mdc-button-harness.ts new file mode 100644 index 000000000000..bad557612e80 --- /dev/null +++ b/src/material-experimental/mdc-button/harness/mdc-button-harness.ts @@ -0,0 +1,67 @@ +/** + * @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 {ButtonHarnessFilters} from './button-harness-filters'; + + +/** + * Harness for interacting with a MDC-based mat-button in tests. + * @dynamic + */ +export class MatButtonHarness extends ComponentHarness { + // TODO(jelbourn) use a single class, like `.mat-button-base` + static hostSelector = [ + '[mat-button]', + '[mat-raised-button]', + '[mat-flat-button]', + '[mat-icon-button]', + '[mat-stroked-button]', + '[mat-fab]', + '[mat-mini-fab]', + ].join(','); + + /** + * Gets a `HarnessPredicate` that can be used to search for a button with specific attributes. + * @param options Options for narrowing the search: + * - `text` finds a button with specific text content. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: ButtonHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatButtonHarness) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } + + /** Clicks the button. */ + async click(): Promise { + return (await this.host()).click(); + } + + /** Gets a boolean promise indicating if the button is disabled. */ + async isDisabled(): Promise { + const disabled = (await this.host()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets a promise for the button's label text. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Focuses the button and returns a void promise that indicates when the action is complete. */ + async foucs(): Promise { + return (await this.host()).focus(); + } + + /** Blurs the button and returns a void promise that indicates when the action is complete. */ + async blur(): Promise { + return (await this.host()).blur(); + } +}