Skip to content

Commit 4aa8718

Browse files
authored
feat(material-experimental/mdc-snack-bar): add test harness (#20366)
Adds a test harness for the MDC-based snack bar. One gotcha here compared to the standard snack bar is that the harness excludes (via a CSS selector) instances that are in the process of being closed. We have to take this approach, because MDC's animations are run outside of Angular and we don't have a way of waiting for them to finish.
1 parent f9c5ffe commit 4aa8718

File tree

9 files changed

+221
-19
lines changed

9 files changed

+221
-19
lines changed

src/material-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ entryPoints = [
3434
"mdc-slider",
3535
"mdc-slider/testing",
3636
"mdc-snack-bar",
37+
"mdc-snack-bar/testing",
3738
"mdc-table",
3839
"mdc-table/testing",
3940
"mdc-tabs",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material-experimental/mdc-snack-bar/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
],
15+
)
16+
17+
filegroup(
18+
name = "source-files",
19+
srcs = glob(["**/*.ts"]),
20+
)
21+
22+
ng_test_library(
23+
name = "unit_tests_lib",
24+
srcs = glob(["**/*.spec.ts"]),
25+
deps = [
26+
":testing",
27+
"//src/material-experimental/mdc-snack-bar",
28+
"//src/material/snack-bar/testing:harness_tests_lib",
29+
],
30+
)
31+
32+
ng_web_test_suite(
33+
name = "unit_tests",
34+
static_files = [
35+
"@npm//:node_modules/@material/snackbar/dist/mdc.snackbar.js",
36+
],
37+
deps = [
38+
":unit_tests_lib",
39+
"//src/material-experimental:mdc_require_config.js",
40+
],
41+
)
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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';
10+
export * from './snack-bar-harness-filters';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
/** A set of criteria that can be used to filter a list of `MatSnackBarHarness` instances. */
12+
export interface SnackBarHarnessFilters extends BaseHarnessFilters {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {MatSnackBarModule, MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
2+
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
3+
import {MatSnackBarHarness} from './snack-bar-harness';
4+
5+
describe('MDC-based MatSnackBarHarness', () => {
6+
runHarnessTests(MatSnackBarModule, MatSnackBar, MatSnackBarHarness as any);
7+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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/testing';
10+
import {SnackBarHarnessFilters} from './snack-bar-harness-filters';
11+
12+
/** Harness for interacting with an MDC-based mat-snack-bar in tests. */
13+
export class MatSnackBarHarness extends ComponentHarness {
14+
// Developers can provide a custom component or template for the
15+
// snackbar. The canonical snack-bar parent is the "MatSnackBarContainer".
16+
// We use `:not([mat-exit])` to exclude snack bars that are in the process of being dismissed,
17+
// because the element only gets removed after the animation is finished and since it runs
18+
// outside of Angular, we don't have a way of being notified when it's done.
19+
/** The selector for the host element of a `MatSnackBar` instance. */
20+
static hostSelector = '.mat-mdc-snack-bar-container:not([mat-exit])';
21+
22+
private _simpleSnackBar = this.locatorForOptional('.mat-mdc-simple-snack-bar');
23+
private _simpleSnackBarMessage =
24+
this.locatorFor('.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label');
25+
private _simpleSnackBarActionButton =
26+
this.locatorForOptional('.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-action');
27+
28+
/**
29+
* Gets a `HarnessPredicate` that can be used to search for a `MatSnackBarHarness` that meets
30+
* certain criteria.
31+
* @param options Options for filtering which snack bar instances are considered a match.
32+
* @return a `HarnessPredicate` configured with the given options.
33+
*/
34+
static with(options: SnackBarHarnessFilters = {}): HarnessPredicate<MatSnackBarHarness> {
35+
return new HarnessPredicate(MatSnackBarHarness, options);
36+
}
37+
38+
/**
39+
* Gets the role of the snack-bar. The role of a snack-bar is determined based
40+
* on the ARIA politeness specified in the snack-bar config.
41+
*/
42+
async getRole(): Promise<'alert'|'status'|null> {
43+
return (await this.host()).getAttribute('role') as Promise<'alert'|'status'|null>;
44+
}
45+
46+
/**
47+
* Whether the snack-bar has an action. Method cannot be used for snack-bar's with custom content.
48+
*/
49+
async hasAction(): Promise<boolean> {
50+
await this._assertSimpleSnackBar();
51+
return (await this._simpleSnackBarActionButton()) !== null;
52+
}
53+
54+
/**
55+
* Gets the description of the snack-bar. Method cannot be used for snack-bar's without action or
56+
* with custom content.
57+
*/
58+
async getActionDescription(): Promise<string> {
59+
await this._assertSimpleSnackBarWithAction();
60+
return (await this._simpleSnackBarActionButton())!.text();
61+
}
62+
63+
64+
/**
65+
* Dismisses the snack-bar by clicking the action button. Method cannot be used for snack-bar's
66+
* without action or with custom content.
67+
*/
68+
async dismissWithAction(): Promise<void> {
69+
await this._assertSimpleSnackBarWithAction();
70+
await (await this._simpleSnackBarActionButton())!.click();
71+
}
72+
73+
/**
74+
* Gets the message of the snack-bar. Method cannot be used for snack-bar's with custom content.
75+
*/
76+
async getMessage(): Promise<string> {
77+
await this._assertSimpleSnackBar();
78+
return (await this._simpleSnackBarMessage()).text();
79+
}
80+
81+
/** Gets whether the snack-bar has been dismissed. */
82+
async isDismissed(): Promise<boolean> {
83+
// We consider the snackbar dismissed if it's not in the DOM. We can assert that the
84+
// element isn't in the DOM by seeing that its width and height are zero.
85+
86+
const host = await this.host();
87+
const [exit, dimensions] = await Promise.all([
88+
// The snackbar container is marked with the "exit" attribute after it has been dismissed
89+
// but before the animation has finished (after which it's removed from the DOM).
90+
host.getAttribute('mat-exit'),
91+
host.getDimensions(),
92+
]);
93+
94+
return exit != null || (!!dimensions && dimensions.height === 0 && dimensions.width === 0);
95+
}
96+
97+
/**
98+
* Asserts that the current snack-bar does not use custom content. Promise rejects if
99+
* custom content is used.
100+
*/
101+
private async _assertSimpleSnackBar(): Promise<void> {
102+
if (!await this._isSimpleSnackBar()) {
103+
throw Error('Method cannot be used for snack-bar with custom content.');
104+
}
105+
}
106+
107+
/**
108+
* Asserts that the current snack-bar does not use custom content and has
109+
* an action defined. Otherwise the promise will reject.
110+
*/
111+
private async _assertSimpleSnackBarWithAction(): Promise<void> {
112+
await this._assertSimpleSnackBar();
113+
if (!await this.hasAction()) {
114+
throw Error('Method cannot be used for standard snack-bar without action.');
115+
}
116+
}
117+
118+
/** Whether the snack-bar is using the default content template. */
119+
private async _isSimpleSnackBar(): Promise<boolean> {
120+
return await this._simpleSnackBar() !== null;
121+
}
122+
}

src/material/snack-bar/testing/shared.spec.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {OverlayContainer} from '@angular/cdk/overlay';
22
import {HarnessLoader} from '@angular/cdk/testing';
33
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
4-
import {Component, TemplateRef, ViewChild} from '@angular/core';
4+
import {Component, TemplateRef, ViewChild, Injector} from '@angular/core';
55
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
66
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
77
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -13,6 +13,7 @@ import {MatSnackBarHarness} from './snack-bar-harness';
1313
*/
1414
export function runHarnessTests(
1515
snackBarModule: typeof MatSnackBarModule,
16+
snackBarToken: typeof MatSnackBar,
1617
snackBarHarness: typeof MatSnackBarHarness) {
1718
let fixture: ComponentFixture<SnackbarHarnessTest>;
1819
let loader: HarnessLoader;
@@ -140,25 +141,24 @@ export function runHarnessTests(
140141
snackBar = await loader.getHarness(snackBarHarness);
141142
await expectAsync(snackBar.dismissWithAction()).toBeRejectedWithError(/without action/);
142143
});
143-
}
144144

145-
@Component({
146-
template: `
147-
<ng-template>
148-
My custom snack-bar.
149-
</ng-template>
150-
`
151-
})
152-
class SnackbarHarnessTest {
153-
@ViewChild(TemplateRef) customTmpl: TemplateRef<any>;
145+
@Component({
146+
template: `<ng-template>My custom snack-bar.</ng-template>`
147+
})
148+
class SnackbarHarnessTest {
149+
@ViewChild(TemplateRef) customTmpl: TemplateRef<any>;
150+
snackBar: MatSnackBar;
154151

155-
constructor(readonly snackBar: MatSnackBar) {}
152+
constructor(injector: Injector) {
153+
this.snackBar = injector.get(snackBarToken);
154+
}
156155

157-
openSimple(message: string, action = '', config?: MatSnackBarConfig) {
158-
return this.snackBar.open(message, action, config);
159-
}
156+
openSimple(message: string, action = '', config?: MatSnackBarConfig) {
157+
return this.snackBar.open(message, action, config);
158+
}
160159

161-
openCustom(config?: MatSnackBarConfig) {
162-
return this.snackBar.openFromTemplate(this.customTmpl, config);
160+
openCustom(config?: MatSnackBarConfig) {
161+
return this.snackBar.openFromTemplate(this.customTmpl, config);
162+
}
163163
}
164164
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {MatSnackBarModule} from '@angular/material/snack-bar';
1+
import {MatSnackBarModule, MatSnackBar} from '@angular/material/snack-bar';
22
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
33
import {MatSnackBarHarness} from './snack-bar-harness';
44

55
describe('Non-MDC-based MatSnackBarHarness', () => {
6-
runHarnessTests(MatSnackBarModule, MatSnackBarHarness);
6+
runHarnessTests(MatSnackBarModule, MatSnackBar, MatSnackBarHarness);
77
});

0 commit comments

Comments
 (0)