Skip to content

Commit 3899191

Browse files
committed
feat(material): add test harness for snack-bar
1 parent 2001965 commit 3899191

File tree

7 files changed

+334
-0
lines changed

7 files changed

+334
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
4+
5+
ng_module(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material/snack-bar/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
],
15+
)
16+
17+
ng_test_library(
18+
name = "harness_tests_lib",
19+
srcs = ["shared.spec.ts"],
20+
deps = [
21+
":testing",
22+
"//src/cdk/overlay",
23+
"//src/cdk/testing",
24+
"//src/cdk/testing/testbed",
25+
"//src/material/snack-bar",
26+
"@npm//@angular/platform-browser",
27+
],
28+
)
29+
30+
ng_test_library(
31+
name = "unit_tests_lib",
32+
srcs = glob(
33+
["**/*.spec.ts"],
34+
exclude = ["shared.spec.ts"],
35+
),
36+
deps = [
37+
":harness_tests_lib",
38+
":testing",
39+
"//src/material/snack-bar",
40+
],
41+
)
42+
43+
ng_web_test_suite(
44+
name = "unit_tests",
45+
deps = [":unit_tests_lib"],
46+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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 * from './public-api';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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 * from './snack-bar-harness';
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {OverlayContainer} from '@angular/cdk/overlay';
2+
import {HarnessLoader} from '@angular/cdk/testing';
3+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
4+
import {Component, TemplateRef, ViewChild} from '@angular/core';
5+
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
6+
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
7+
import {MatSnackBarHarness} from '@angular/material/snack-bar/testing';
8+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
9+
10+
/**
11+
* Function that can be used to run the shared snack-bar harness tests for either
12+
* the non-MDC or MDC based snack-bar harness.
13+
*/
14+
export function runHarnessTests(
15+
snackBarModule: typeof MatSnackBarModule,
16+
snackBarHarness: typeof MatSnackBarHarness) {
17+
let fixture: ComponentFixture<SnackbarHarnessTest>;
18+
let loader: HarnessLoader;
19+
let overlayContainer: OverlayContainer;
20+
21+
beforeEach(async () => {
22+
await TestBed.configureTestingModule({
23+
imports: [snackBarModule, NoopAnimationsModule],
24+
declarations: [SnackbarHarnessTest],
25+
}).compileComponents();
26+
27+
fixture = TestBed.createComponent(SnackbarHarnessTest);
28+
fixture.detectChanges();
29+
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
30+
inject([OverlayContainer], (oc: OverlayContainer) => {
31+
overlayContainer = oc;
32+
})();
33+
});
34+
35+
afterEach(() => {
36+
// Angular won't call this for us so we need to do it ourselves to avoid leaks.
37+
overlayContainer.ngOnDestroy();
38+
overlayContainer = null!;
39+
});
40+
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {MatSnackBarModule} from '@angular/material/snack-bar';
2+
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
3+
import {MatSnackBarHarness} from '@angular/material/snack-bar/testing/snack-bar-harness';
4+
5+
describe('Non-MDC-based MatSnackBarHarness', () => {
6+
runHarnessTests(MatSnackBarModule, MatSnackBarHarness);
7+
});
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/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+
}

test/karma-system-config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ System.config({
139139
'@angular/material/slide-toggle': 'dist/packages/material/slide-toggle/index.js',
140140
'@angular/material/slider': 'dist/packages/material/slider/index.js',
141141
'@angular/material/snack-bar': 'dist/packages/material/snack-bar/index.js',
142+
'@angular/material/snack-bar/testing': 'dist/packages/material/snack-bar/testing/index.js',
143+
'@angular/material/snack-bar/testing/shared.spec': 'dist/packages/material/snack-bar/testing/shared.spec.js',
142144
'@angular/material/sort': 'dist/packages/material/sort/index.js',
143145
'@angular/material/stepper': 'dist/packages/material/stepper/index.js',
144146
'@angular/material/table': 'dist/packages/material/table/index.js',

0 commit comments

Comments
 (0)