Skip to content

Commit 3e9db90

Browse files
committed
feat(material-experimental): add test harness for snack-bar
1 parent 1a667df commit 3e9db90

File tree

5 files changed

+294
-1
lines changed

5 files changed

+294
-1
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
/src/material-experimental/mdc-radio/** @mmalerba
107107
/src/material-experimental/mdc-slide-toggle/** @crisbeto
108108
/src/material-experimental/mdc-slider/** @devversion
109+
/src/material-experimental/mdc-snack-bar/** @devversion
109110
/src/material-experimental/mdc-tabs/** @crisbeto
110111
/src/material-experimental/mdc-sidenav/** @crisbeto
111112
/src/material-experimental/mdc-theming/** @mmalerba

src/cdk-experimental/testing/testbed/testbed-harness-environment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {UnitTestElement} from './unit-test-element';
1414

1515
/** A `HarnessEnvironment` implementation for Angular's Testbed. */
1616
export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
17-
constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
17+
protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
1818
super(rawRootElement);
1919
}
2020

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
4+
5+
ts_library(
6+
name = "harness",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//src/cdk-experimental/testing",
13+
],
14+
)
15+
16+
ng_test_library(
17+
name = "harness_tests",
18+
srcs = glob(["**/*.spec.ts"]),
19+
deps = [
20+
":harness",
21+
"//src/cdk-experimental/testing",
22+
"//src/cdk-experimental/testing/testbed",
23+
"//src/material/snack-bar",
24+
"@npm//@angular/platform-browser",
25+
],
26+
)
27+
28+
ng_web_test_suite(
29+
name = "tests",
30+
deps = [":harness_tests"],
31+
)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {HarnessLoader} from '@angular/cdk-experimental/testing';
2+
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
3+
import {Component, TemplateRef, ViewChild} from '@angular/core';
4+
import {ComponentFixture, TestBed} from '@angular/core/testing';
5+
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
6+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
7+
import {MatSnackBarHarness} from './snack-bar-harness';
8+
9+
let fixture: ComponentFixture<SnackbarHarnessTest>;
10+
let loader: HarnessLoader;
11+
let snackBarHarness: typeof MatSnackBarHarness;
12+
13+
describe('MatSnackBarHarness', () => {
14+
describe('non-MDC-based', () => {
15+
beforeEach(async () => {
16+
await TestBed
17+
.configureTestingModule({
18+
imports: [MatSnackBarModule, NoopAnimationsModule],
19+
declarations: [SnackbarHarnessTest],
20+
})
21+
.compileComponents();
22+
23+
fixture = TestBed.createComponent(SnackbarHarnessTest);
24+
fixture.detectChanges();
25+
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
26+
snackBarHarness = MatSnackBarHarness;
27+
});
28+
29+
runTests();
30+
});
31+
32+
describe(
33+
'MDC-based',
34+
() => {
35+
// TODO: run tests for MDC based snack-bar once implemented.
36+
});
37+
});
38+
39+
/** Shared tests to run on both the original and MDC-based snack-bar's. */
40+
function runTests() {
41+
it('should load harness for simple snack-bar', async () => {
42+
const snackBarRef = fixture.componentInstance.openSimple('Hello!', '');
43+
let snackBars = await loader.getAllHarnesses(snackBarHarness);
44+
45+
expect(snackBars.length).toBe(1);
46+
47+
snackBarRef.dismiss();
48+
snackBars = await loader.getAllHarnesses(snackBarHarness);
49+
expect(snackBars.length).toBe(0);
50+
});
51+
52+
it('should load harness for custom snack-bar', async () => {
53+
const snackBarRef = fixture.componentInstance.openCustom();
54+
let snackBars = await loader.getAllHarnesses(snackBarHarness);
55+
56+
expect(snackBars.length).toBe(1);
57+
58+
snackBarRef.dismiss();
59+
snackBars = await loader.getAllHarnesses(snackBarHarness);
60+
expect(snackBars.length).toBe(0);
61+
});
62+
63+
it('should be able to get role of snack-bar', async () => {
64+
fixture.componentInstance.openCustom();
65+
let snackBar = await loader.getHarness(snackBarHarness);
66+
expect(await snackBar.getRole()).toBe('alert');
67+
68+
fixture.componentInstance.openCustom({politeness: 'polite'});
69+
snackBar = await loader.getHarness(snackBarHarness);
70+
expect(await snackBar.getRole()).toBe('status');
71+
72+
fixture.componentInstance.openCustom({politeness: 'off'});
73+
snackBar = await loader.getHarness(snackBarHarness);
74+
expect(await snackBar.getRole()).toBe(null);
75+
});
76+
77+
it('should be able to get message of simple snack-bar', async () => {
78+
fixture.componentInstance.openSimple('Subscribed to newsletter.');
79+
let snackBar = await loader.getHarness(snackBarHarness);
80+
expect(await snackBar.getMessage()).toBe('Subscribed to newsletter.');
81+
82+
// For snack-bar's with custom template, the message cannot be
83+
// retrieved. We expect an error to be thrown.
84+
fixture.componentInstance.openCustom();
85+
snackBar = await loader.getHarness(snackBarHarness);
86+
await expectAsyncError(() => snackBar.getMessage(), /custom content/);
87+
});
88+
89+
it('should be able to get action description of simple snack-bar', async () => {
90+
fixture.componentInstance.openSimple('Hello', 'Unsubscribe');
91+
let snackBar = await loader.getHarness(snackBarHarness);
92+
expect(await snackBar.getActionDescription()).toBe('Unsubscribe');
93+
94+
// For snack-bar's with custom template, the action description
95+
// cannot be retrieved. We expect an error to be thrown.
96+
fixture.componentInstance.openCustom();
97+
snackBar = await loader.getHarness(snackBarHarness);
98+
await expectAsyncError(() => snackBar.getActionDescription(), /custom content/);
99+
});
100+
101+
it('should be able to check whether simple snack-bar has action', async () => {
102+
fixture.componentInstance.openSimple('With action', 'Unsubscribe');
103+
let snackBar = await loader.getHarness(snackBarHarness);
104+
expect(await snackBar.hasAction()).toBe(true);
105+
106+
fixture.componentInstance.openSimple('No action');
107+
snackBar = await loader.getHarness(snackBarHarness);
108+
expect(await snackBar.hasAction()).toBe(false);
109+
110+
// For snack-bar's with custom template, the action cannot
111+
// be found. We expect an error to be thrown.
112+
fixture.componentInstance.openCustom();
113+
snackBar = await loader.getHarness(snackBarHarness);
114+
await expectAsyncError(() => snackBar.hasAction(), /custom content/);
115+
});
116+
117+
it('should be able to dismiss simple snack-bar with action', async () => {
118+
const snackBarRef = fixture.componentInstance.openSimple('With action', 'Unsubscribe');
119+
let snackBar = await loader.getHarness(snackBarHarness);
120+
let actionCount = 0;
121+
snackBarRef.onAction().subscribe(() => actionCount++);
122+
123+
await snackBar.dismissWithAction();
124+
expect(actionCount).toBe(1);
125+
126+
fixture.componentInstance.openSimple('No action');
127+
snackBar = await loader.getHarness(snackBarHarness);
128+
await expectAsyncError(() => snackBar.dismissWithAction(), /without action/);
129+
});
130+
}
131+
132+
/**
133+
* Expects the asynchronous function to throw an error that matches
134+
* the specified expectation.
135+
*/
136+
async function expectAsyncError(fn: () => Promise<any>, expectation: RegExp) {
137+
let error: string|null = null;
138+
try {
139+
await fn();
140+
} catch (e) {
141+
error = e.toString();
142+
}
143+
expect(error).not.toBe(null);
144+
expect(error!).toMatch(expectation);
145+
}
146+
147+
@Component({
148+
template: `
149+
<ng-template>
150+
My custom snack-bar.
151+
</ng-template>
152+
`
153+
})
154+
class SnackbarHarnessTest {
155+
@ViewChild(TemplateRef, {static: false}) customTmpl: TemplateRef<any>;
156+
157+
constructor(readonly snackBar: MatSnackBar) {}
158+
159+
openSimple(message: string, action = '', config?: MatSnackBarConfig) {
160+
return this.snackBar.open(message, action, config);
161+
}
162+
163+
openCustom(config?: MatSnackBarConfig) {
164+
return this.snackBar.openFromTemplate(this.customTmpl, config);
165+
}
166+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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} from '@angular/cdk-experimental/testing';
10+
11+
/**
12+
* Harness for interacting with a standard mat-snack-bar in tests.
13+
* @dynamic
14+
*/
15+
export class MatSnackBarHarness extends ComponentHarness {
16+
// Developers can provide a custom component or template for the
17+
// snackbar. The canonical snack-bar parent is the "MatSnackBarContainer".
18+
static hostSelector = '.mat-snack-bar-container';
19+
20+
private _simpleSnackBar = this.locatorForOptional('.mat-simple-snackbar');
21+
private _simpleSnackBarMessage = this.locatorFor('.mat-simple-snackbar > span');
22+
private _simpleSnackBarActionButton =
23+
this.locatorForOptional('.mat-simple-snackbar-action > button');
24+
25+
/**
26+
* Gets the role of the snack-bar. The role of a snack-bar is determined based
27+
* on the ARIA politeness specified in the snack-bar config.
28+
*/
29+
async getRole(): Promise<'alert'|'status'|null> {
30+
return (await this.host()).getAttribute('role') as Promise<'alert'|'status'|null>;
31+
}
32+
33+
/**
34+
* Gets whether the snack-bar has an action. Method cannot be
35+
* used for snack-bar's with custom content.
36+
*/
37+
async hasAction(): Promise<boolean> {
38+
await this._assertSimpleSnackBar();
39+
return (await this._simpleSnackBarActionButton()) !== null;
40+
}
41+
42+
/**
43+
* Gets the description of the snack-bar. Method cannot be
44+
* used for snack-bar's without action or with custom content.
45+
*/
46+
async getActionDescription(): Promise<string> {
47+
await this._assertSimpleSnackBarWithAction();
48+
return (await this._simpleSnackBarActionButton())!.text();
49+
}
50+
51+
52+
/**
53+
* Dismisses the snack-bar by clicking the action button. Method cannot
54+
* be used for snack-bar's without action or with custom content.
55+
*/
56+
async dismissWithAction(): Promise<void> {
57+
await this._assertSimpleSnackBarWithAction();
58+
await (await this._simpleSnackBarActionButton())!.click();
59+
}
60+
61+
/**
62+
* Gets the message of the snack-bar. Method cannot be used for
63+
* snack-bar's with custom content.
64+
*/
65+
async getMessage(): Promise<string> {
66+
await this._assertSimpleSnackBar();
67+
return (await this._simpleSnackBarMessage()).text();
68+
}
69+
70+
/**
71+
* Asserts that the current snack-bar does not use custom content. Throws if
72+
* custom content is used.
73+
*/
74+
private async _assertSimpleSnackBar(): Promise<void> {
75+
if (!await this._isSimpleSnackBar()) {
76+
throw new Error('Method cannot be used for snack-bar with custom content.');
77+
}
78+
}
79+
80+
/**
81+
* Asserts that the current snack-bar does not use custom content and has
82+
* an action defined. Otherwise an error will be thrown.
83+
*/
84+
private async _assertSimpleSnackBarWithAction(): Promise<void> {
85+
await this._assertSimpleSnackBar();
86+
if (!await this.hasAction()) {
87+
throw new Error('Method cannot be used for standard snack-bar without action.');
88+
}
89+
}
90+
91+
/** Gets whether the snack-bar is using the default content template. */
92+
private async _isSimpleSnackBar(): Promise<boolean> {
93+
return await this._simpleSnackBar() !== null;
94+
}
95+
}

0 commit comments

Comments
 (0)