Skip to content

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

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 1 commit into from
Jul 26, 2019
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
44 changes: 42 additions & 2 deletions src/material-experimental/mdc-button/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"]),
Expand Down Expand Up @@ -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"]),
Expand Down
1 change: 0 additions & 1 deletion src/material-experimental/mdc-button/button.spec.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
};
159 changes: 159 additions & 0 deletions src/material-experimental/mdc-button/harness/button-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ButtonHarnessTest>;
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: `
<button id="basic" type="button" mat-button (click)="clicked = true">
Basic button
</button>
<button id="flat" type="button" mat-flat-button disabled (click)="clicked = true">
Flat button
</button>
<button id="raised" type="button" mat-raised-button>Raised button</button>
<button id="stroked" type="button" mat-stroked-button>Stroked button</button>
<button id="icon" type="button" mat-icon-button>Icon button</button>
<button id="fab" type="button" mat-fab>Fab button</button>
<button id="mini-fab" type="button" mat-mini-fab>Mini Fab button</button>

<a id="anchor-basic" mat-button>Basic anchor</a>
<a id="anchor-flat" mat-flat-button>Flat anchor</a>
<a id="anchor-raised" mat-raised-button disabled>Raised anchor</a>
<a id="anchor-stroked" mat-stroked-button>Stroked anchor</a>
<a id="anchor-icon" mat-icon-button>Icon anchor</a>
<a id="anchor-fab" mat-fab>Fab anchor</a>
<a id="anchor-mini-fab" mat-mini-fab>Mini Fab anchor</a>
`
})
class ButtonHarnessTest {
disabled = true;
clicked = false;
}

67 changes: 67 additions & 0 deletions src/material-experimental/mdc-button/harness/button-harness.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an array of these defined as BUTTON_HOST_ATTRIBUTES. Could we export and re-use it here? Otherwise we'll have to remember to update this array if we add more button types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really just want to change this to use e.g. .mat-button-base, which was in another PR when I wrote this. Added a TODO for now

'[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<MatButtonHarness> {
return new HarnessPredicate(MatButtonHarness)
.addOption('text', options.text,
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
}

/** Clicks the button. */
async click(): Promise<void> {
return (await this.host()).click();
}

/** Gets a boolean promise indicating if the button is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this.host()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
}

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

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

/** Blurs the button and returns a void promise that indicates when the action is complete. */
async blur(): Promise<void> {
return (await this.host()).blur();
}
}
67 changes: 67 additions & 0 deletions src/material-experimental/mdc-button/harness/mdc-button-harness.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment above importing these

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TODO

'[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<MatButtonHarness> {
return new HarnessPredicate(MatButtonHarness)
.addOption('text', options.text,
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
}

/** Clicks the button. */
async click(): Promise<void> {
return (await this.host()).click();
}

/** Gets a boolean promise indicating if the button is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this.host()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
}

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

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

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