Skip to content

Commit 88631b9

Browse files
authored
feat(material-experimental): add test harness for button (#16556)
1 parent 2a04ccf commit 88631b9

File tree

6 files changed

+346
-3
lines changed

6 files changed

+346
-3
lines changed

src/material-experimental/mdc-button/BUILD.bazel

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package(default_visibility = ["//visibility:public"])
22

33
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library")
4-
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module")
4+
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite", "ts_library")
55
load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
66

77
ng_module(
88
name = "mdc-button",
99
srcs = glob(
1010
["**/*.ts"],
11-
exclude = ["**/*.spec.ts"],
11+
exclude = [
12+
"**/*.spec.ts",
13+
"harness/**",
14+
],
1215
),
1316
assets = [
1417
":button_scss",
@@ -22,6 +25,18 @@ ng_module(
2225
],
2326
)
2427

28+
ts_library(
29+
name = "harness",
30+
srcs = glob(
31+
["harness/**/*.ts"],
32+
exclude = ["**/*.spec.ts"],
33+
),
34+
deps = [
35+
"//src/cdk-experimental/testing",
36+
"//src/cdk/coercion",
37+
],
38+
)
39+
2540
sass_library(
2641
name = "mdc_button_scss_lib",
2742
srcs = glob(["**/_*.scss"]),
@@ -71,6 +86,31 @@ sass_binary(
7186
],
7287
)
7388

89+
ng_test_library(
90+
name = "button_tests_lib",
91+
srcs = [
92+
"harness/button-harness.spec.ts",
93+
],
94+
deps = [
95+
":harness",
96+
":mdc-button",
97+
"//src/cdk-experimental/testing",
98+
"//src/cdk-experimental/testing/testbed",
99+
"//src/cdk/platform",
100+
"//src/cdk/testing",
101+
"//src/material/button",
102+
"@npm//@angular/platform-browser",
103+
],
104+
)
105+
106+
ng_web_test_suite(
107+
name = "unit_tests",
108+
deps = [
109+
":button_tests_lib",
110+
"//src/material-experimental:mdc_require_config.js",
111+
],
112+
)
113+
74114
ng_e2e_test_library(
75115
name = "e2e_test_sources",
76116
srcs = glob(["**/*.e2e.spec.ts"]),

src/material-experimental/mdc-button/button.spec.ts

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export type ButtonHarnessFilters = {
10+
text?: string | RegExp
11+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import {HarnessLoader} from '@angular/cdk-experimental/testing';
2+
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
3+
import {Platform, PlatformModule} from '@angular/cdk/platform';
4+
import {Component} from '@angular/core';
5+
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
6+
import {MatButtonModule} from '@angular/material/button';
7+
import {MatButtonModule as MatMdcButtonModule} from '../index';
8+
import {MatButtonHarness} from './button-harness';
9+
import {MatButtonHarness as MatMdcButtonHarness} from './mdc-button-harness';
10+
11+
let fixture: ComponentFixture<ButtonHarnessTest>;
12+
let loader: HarnessLoader;
13+
let buttonHarness: typeof MatButtonHarness;
14+
let platform: Platform;
15+
16+
describe('MatButtonHarness', () => {
17+
describe('non-MDC-based', () => {
18+
beforeEach(async () => {
19+
await TestBed.configureTestingModule({
20+
imports: [MatButtonModule, PlatformModule],
21+
declarations: [ButtonHarnessTest],
22+
}).compileComponents();
23+
24+
fixture = TestBed.createComponent(ButtonHarnessTest);
25+
fixture.detectChanges();
26+
loader = TestbedHarnessEnvironment.loader(fixture);
27+
buttonHarness = MatButtonHarness;
28+
});
29+
30+
runTests();
31+
});
32+
33+
describe('MDC-based', () => {
34+
beforeEach(async () => {
35+
await TestBed.configureTestingModule({
36+
imports: [MatMdcButtonModule],
37+
declarations: [ButtonHarnessTest],
38+
}).compileComponents();
39+
40+
fixture = TestBed.createComponent(ButtonHarnessTest);
41+
fixture.detectChanges();
42+
loader = TestbedHarnessEnvironment.loader(fixture);
43+
// Public APIs are the same as MatButtonHarness, but cast is necessary because of different
44+
// private fields.
45+
buttonHarness = MatMdcButtonHarness as any;
46+
});
47+
48+
runTests();
49+
});
50+
});
51+
52+
/** Shared tests to run on both the original and MDC-based buttons. */
53+
function runTests() {
54+
beforeEach(inject([Platform], (p: Platform) => {
55+
platform = p;
56+
}));
57+
58+
it('should load all button harnesses', async () => {
59+
const buttons = await loader.getAllHarnesses(buttonHarness);
60+
expect(buttons.length).toBe(14);
61+
});
62+
63+
it('should load button with exact text', async () => {
64+
const buttons = await loader.getAllHarnesses(buttonHarness.with({text: 'Basic button'}));
65+
expect(buttons.length).toBe(1);
66+
expect(await buttons[0].getText()).toBe('Basic button');
67+
});
68+
69+
it('should load button with regex label match', async () => {
70+
const buttons = await loader.getAllHarnesses(buttonHarness.with({text: /basic/i}));
71+
expect(buttons.length).toBe(2);
72+
expect(await buttons[0].getText()).toBe('Basic button');
73+
expect(await buttons[1].getText()).toBe('Basic anchor');
74+
});
75+
76+
it('should get disabled state', async () => {
77+
// Grab each combination of [enabled, disabled] ⨯ [button, anchor]
78+
const [disabledFlatButton, enabledFlatAnchor] =
79+
await loader.getAllHarnesses(buttonHarness.with({text: /flat/i}));
80+
const [enabledRaisedButton, disabledRaisedAnchor] =
81+
await loader.getAllHarnesses(buttonHarness.with({text: /raised/i}));
82+
83+
expect(await enabledFlatAnchor.isDisabled()).toBe(false);
84+
expect(await disabledFlatButton.isDisabled()).toBe(true);
85+
expect(await enabledRaisedButton.isDisabled()).toBe(false);
86+
expect(await disabledRaisedAnchor.isDisabled()).toBe(true);
87+
});
88+
89+
it('should get button text', async () => {
90+
const [firstButton, secondButton] = await loader.getAllHarnesses(buttonHarness);
91+
expect(await firstButton.getText()).toBe('Basic button');
92+
expect(await secondButton.getText()).toBe('Flat button');
93+
});
94+
95+
it('should focus and blur a button', async () => {
96+
const button = await loader.getHarness(buttonHarness.with({text: 'Basic button'}));
97+
expect(getActiveElementId()).not.toBe('basic');
98+
await button.foucs();
99+
expect(getActiveElementId()).toBe('basic');
100+
await button.blur();
101+
expect(getActiveElementId()).not.toBe('basic');
102+
});
103+
104+
it('should click a button', async () => {
105+
const button = await loader.getHarness(buttonHarness.with({text: 'Basic button'}));
106+
await button.click();
107+
108+
expect(fixture.componentInstance.clicked).toBe(true);
109+
});
110+
111+
it('should not click a disabled button', async () => {
112+
// Older versions of Edge have a bug where `disabled` buttons are still clickable if
113+
// they contain child elements. We skip this check on Edge.
114+
// See https://stackoverflow.com/questions/32377026/disabled-button-is-clickable-on-edge-browser
115+
if (platform.EDGE) {
116+
return;
117+
}
118+
119+
const button = await loader.getHarness(buttonHarness.with({text: 'Flat button'}));
120+
await button.click();
121+
122+
expect(fixture.componentInstance.clicked).toBe(false);
123+
});
124+
}
125+
126+
function getActiveElementId() {
127+
return document.activeElement ? document.activeElement.id : '';
128+
}
129+
130+
@Component({
131+
// Include one of each type of button selector to ensure that they're all captured by
132+
// the harness's selector.
133+
template: `
134+
<button id="basic" type="button" mat-button (click)="clicked = true">
135+
Basic button
136+
</button>
137+
<button id="flat" type="button" mat-flat-button disabled (click)="clicked = true">
138+
Flat button
139+
</button>
140+
<button id="raised" type="button" mat-raised-button>Raised button</button>
141+
<button id="stroked" type="button" mat-stroked-button>Stroked button</button>
142+
<button id="icon" type="button" mat-icon-button>Icon button</button>
143+
<button id="fab" type="button" mat-fab>Fab button</button>
144+
<button id="mini-fab" type="button" mat-mini-fab>Mini Fab button</button>
145+
146+
<a id="anchor-basic" mat-button>Basic anchor</a>
147+
<a id="anchor-flat" mat-flat-button>Flat anchor</a>
148+
<a id="anchor-raised" mat-raised-button disabled>Raised anchor</a>
149+
<a id="anchor-stroked" mat-stroked-button>Stroked anchor</a>
150+
<a id="anchor-icon" mat-icon-button>Icon anchor</a>
151+
<a id="anchor-fab" mat-fab>Fab anchor</a>
152+
<a id="anchor-mini-fab" mat-mini-fab>Mini Fab anchor</a>
153+
`
154+
})
155+
class ButtonHarnessTest {
156+
disabled = true;
157+
clicked = false;
158+
}
159+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {ButtonHarnessFilters} from './button-harness-filters';
12+
13+
14+
/**
15+
* Harness for interacting with a standard mat-button in tests.
16+
* @dynamic
17+
*/
18+
export class MatButtonHarness extends ComponentHarness {
19+
// TODO(jelbourn) use a single class, like `.mat-button-base`
20+
static hostSelector = [
21+
'[mat-button]',
22+
'[mat-raised-button]',
23+
'[mat-flat-button]',
24+
'[mat-icon-button]',
25+
'[mat-stroked-button]',
26+
'[mat-fab]',
27+
'[mat-mini-fab]',
28+
].join(',');
29+
30+
/**
31+
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
32+
* @param options Options for narrowing the search:
33+
* - `text` finds a button with specific text content.
34+
* @return a `HarnessPredicate` configured with the given options.
35+
*/
36+
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
37+
return new HarnessPredicate(MatButtonHarness)
38+
.addOption('text', options.text,
39+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
40+
}
41+
42+
/** Clicks the button. */
43+
async click(): Promise<void> {
44+
return (await this.host()).click();
45+
}
46+
47+
/** Gets a boolean promise indicating if the button is disabled. */
48+
async isDisabled(): Promise<boolean> {
49+
const disabled = (await this.host()).getAttribute('disabled');
50+
return coerceBooleanProperty(await disabled);
51+
}
52+
53+
/** Gets a promise for the button's label text. */
54+
async getText(): Promise<string> {
55+
return (await this.host()).text();
56+
}
57+
58+
/** Focuses the button and returns a void promise that indicates when the action is complete. */
59+
async foucs(): Promise<void> {
60+
return (await this.host()).focus();
61+
}
62+
63+
/** Blurs the button and returns a void promise that indicates when the action is complete. */
64+
async blur(): Promise<void> {
65+
return (await this.host()).blur();
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {ButtonHarnessFilters} from './button-harness-filters';
12+
13+
14+
/**
15+
* Harness for interacting with a MDC-based mat-button in tests.
16+
* @dynamic
17+
*/
18+
export class MatButtonHarness extends ComponentHarness {
19+
// TODO(jelbourn) use a single class, like `.mat-button-base`
20+
static hostSelector = [
21+
'[mat-button]',
22+
'[mat-raised-button]',
23+
'[mat-flat-button]',
24+
'[mat-icon-button]',
25+
'[mat-stroked-button]',
26+
'[mat-fab]',
27+
'[mat-mini-fab]',
28+
].join(',');
29+
30+
/**
31+
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
32+
* @param options Options for narrowing the search:
33+
* - `text` finds a button with specific text content.
34+
* @return a `HarnessPredicate` configured with the given options.
35+
*/
36+
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
37+
return new HarnessPredicate(MatButtonHarness)
38+
.addOption('text', options.text,
39+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
40+
}
41+
42+
/** Clicks the button. */
43+
async click(): Promise<void> {
44+
return (await this.host()).click();
45+
}
46+
47+
/** Gets a boolean promise indicating if the button is disabled. */
48+
async isDisabled(): Promise<boolean> {
49+
const disabled = (await this.host()).getAttribute('disabled');
50+
return coerceBooleanProperty(await disabled);
51+
}
52+
53+
/** Gets a promise for the button's label text. */
54+
async getText(): Promise<string> {
55+
return (await this.host()).text();
56+
}
57+
58+
/** Focuses the button and returns a void promise that indicates when the action is complete. */
59+
async foucs(): Promise<void> {
60+
return (await this.host()).focus();
61+
}
62+
63+
/** Blurs the button and returns a void promise that indicates when the action is complete. */
64+
async blur(): Promise<void> {
65+
return (await this.host()).blur();
66+
}
67+
}

0 commit comments

Comments
 (0)