Skip to content

Commit 54e554b

Browse files
committed
feat(material-experimental): add test harness for snack-bar
1 parent 06542d1 commit 54e554b

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
# Note to implementer: please repossess
9898
/src/material-experimental/mdc-radio/** @mmalerba
9999
/src/material-experimental/mdc-slide-toggle/** @crisbeto
100+
/src/material-experimental/mdc-snack-bar/** @devversion
100101
/src/material-experimental/mdc-tabs/** @crisbeto
101102
/src/material-experimental/mdc-theming/** @mmalerba
102103
/src/material-experimental/mdc-typography/** @mmalerba
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.loader(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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 _documentRootLocator = this.documentRootLocatorFactory();
21+
private _simpleSnackBar = this._documentRootLocator.locatorForOptional('.mat-simple-snackbar');
22+
private _simpleSnackBarMessage =
23+
this._documentRootLocator.locatorFor('.mat-simple-snackbar > span');
24+
private _simpleSnackBarActionButton =
25+
this._documentRootLocator.locatorForOptional('.mat-simple-snackbar-action > button');
26+
27+
/**
28+
* Gets the role of the snack-bar. The role of a snack-bar is determined based
29+
* on the ARIA politeness specified in the snack-bar config.
30+
*/
31+
async getRole(): Promise<'alert'|'status'|null> {
32+
return (await this.host()).getAttribute('role') as Promise<'alert'|'status'|null>;
33+
}
34+
35+
/**
36+
* Gets whether the snack-bar has an action. Method cannot be
37+
* used for snack-bar's with custom content.
38+
*/
39+
async hasAction(): Promise<boolean> {
40+
await this._assertSimpleSnackBar();
41+
return (await this._simpleSnackBarActionButton()) !== null;
42+
}
43+
44+
/**
45+
* Gets the description of the snack-bar. Method cannot be
46+
* used for snack-bar's without action or with custom content.
47+
*/
48+
async getActionDescription(): Promise<string> {
49+
await this._assertSimpleSnackBarWithAction();
50+
return (await this._simpleSnackBarActionButton())!.text();
51+
}
52+
53+
54+
/**
55+
* Dismisses the snack-bar by clicking the action button. Method cannot
56+
* be used for snack-bar's without action or with custom content.
57+
*/
58+
async dismissWithAction(): Promise<void> {
59+
await this._assertSimpleSnackBarWithAction();
60+
await (await this._simpleSnackBarActionButton())!.click();
61+
}
62+
63+
/**
64+
* Gets the message of the snack-bar. Method cannot be used for
65+
* snack-bar's with custom content.
66+
*/
67+
async getMessage(): Promise<string> {
68+
await this._assertSimpleSnackBar();
69+
return (await this._simpleSnackBarMessage()).text();
70+
}
71+
72+
/**
73+
* Asserts that the current snack-bar does not use custom content. Throws if
74+
* custom content is used.
75+
*/
76+
private async _assertSimpleSnackBar(): Promise<void> {
77+
if (!await this._isSimpleSnackBar()) {
78+
throw new Error('Method cannot be used for snack-bar with custom content.');
79+
}
80+
}
81+
82+
/**
83+
* Asserts that the current snack-bar does not use custom content and has
84+
* an action defined. Otherwise an error will be thrown.
85+
*/
86+
private async _assertSimpleSnackBarWithAction(): Promise<void> {
87+
await this._assertSimpleSnackBar();
88+
if (!await this.hasAction()) {
89+
throw new Error('Method cannot be used for standard snack-bar without action.');
90+
}
91+
}
92+
93+
/** Gets whether the snack-bar is using the default content template. */
94+
private async _isSimpleSnackBar(): Promise<boolean> {
95+
return await this._simpleSnackBar() !== null;
96+
}
97+
}

0 commit comments

Comments
 (0)