Skip to content

Commit

Permalink
feat(material-experimental/mdc-snack-bar): add test harness (#20366)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Aug 20, 2020
1 parent f9c5ffe commit 4aa8718
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/material-experimental/config.bzl
Expand Up @@ -34,6 +34,7 @@ entryPoints = [
"mdc-slider",
"mdc-slider/testing",
"mdc-snack-bar",
"mdc-snack-bar/testing",
"mdc-table",
"mdc-table/testing",
"mdc-tabs",
Expand Down
41 changes: 41 additions & 0 deletions src/material-experimental/mdc-snack-bar/testing/BUILD.bazel
@@ -0,0 +1,41 @@
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/material-experimental/mdc-snack-bar/testing",
deps = [
"//src/cdk/testing",
],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(["**/*.spec.ts"]),
deps = [
":testing",
"//src/material-experimental/mdc-snack-bar",
"//src/material/snack-bar/testing:harness_tests_lib",
],
)

ng_web_test_suite(
name = "unit_tests",
static_files = [
"@npm//:node_modules/@material/snackbar/dist/mdc.snackbar.js",
],
deps = [
":unit_tests_lib",
"//src/material-experimental:mdc_require_config.js",
],
)
9 changes: 9 additions & 0 deletions src/material-experimental/mdc-snack-bar/testing/index.ts
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './public-api';
10 changes: 10 additions & 0 deletions src/material-experimental/mdc-snack-bar/testing/public-api.ts
@@ -0,0 +1,10 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './snack-bar-harness';
export * from './snack-bar-harness-filters';
@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {BaseHarnessFilters} from '@angular/cdk/testing';

/** A set of criteria that can be used to filter a list of `MatSnackBarHarness` instances. */
export interface SnackBarHarnessFilters extends BaseHarnessFilters {}
@@ -0,0 +1,7 @@
import {MatSnackBarModule, MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
import {MatSnackBarHarness} from './snack-bar-harness';

describe('MDC-based MatSnackBarHarness', () => {
runHarnessTests(MatSnackBarModule, MatSnackBar, MatSnackBarHarness as any);
});
122 changes: 122 additions & 0 deletions src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.ts
@@ -0,0 +1,122 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
import {SnackBarHarnessFilters} from './snack-bar-harness-filters';

/** Harness for interacting with an MDC-based mat-snack-bar in tests. */
export class MatSnackBarHarness extends ComponentHarness {
// Developers can provide a custom component or template for the
// snackbar. The canonical snack-bar parent is the "MatSnackBarContainer".
// We use `:not([mat-exit])` to exclude snack bars that are in the process of being dismissed,
// because the element only gets removed after the animation is finished and since it runs
// outside of Angular, we don't have a way of being notified when it's done.
/** The selector for the host element of a `MatSnackBar` instance. */
static hostSelector = '.mat-mdc-snack-bar-container:not([mat-exit])';

private _simpleSnackBar = this.locatorForOptional('.mat-mdc-simple-snack-bar');
private _simpleSnackBarMessage =
this.locatorFor('.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label');
private _simpleSnackBarActionButton =
this.locatorForOptional('.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-action');

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSnackBarHarness` that meets
* certain criteria.
* @param options Options for filtering which snack bar instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: SnackBarHarnessFilters = {}): HarnessPredicate<MatSnackBarHarness> {
return new HarnessPredicate(MatSnackBarHarness, options);
}

/**
* Gets the role of the snack-bar. The role of a snack-bar is determined based
* on the ARIA politeness specified in the snack-bar config.
*/
async getRole(): Promise<'alert'|'status'|null> {
return (await this.host()).getAttribute('role') as Promise<'alert'|'status'|null>;
}

/**
* Whether the snack-bar has an action. Method cannot be used for snack-bar's with custom content.
*/
async hasAction(): Promise<boolean> {
await this._assertSimpleSnackBar();
return (await this._simpleSnackBarActionButton()) !== null;
}

/**
* Gets the description of the snack-bar. Method cannot be used for snack-bar's without action or
* with custom content.
*/
async getActionDescription(): Promise<string> {
await this._assertSimpleSnackBarWithAction();
return (await this._simpleSnackBarActionButton())!.text();
}


/**
* Dismisses the snack-bar by clicking the action button. Method cannot be used for snack-bar's
* without action or with custom content.
*/
async dismissWithAction(): Promise<void> {
await this._assertSimpleSnackBarWithAction();
await (await this._simpleSnackBarActionButton())!.click();
}

/**
* Gets the message of the snack-bar. Method cannot be used for snack-bar's with custom content.
*/
async getMessage(): Promise<string> {
await this._assertSimpleSnackBar();
return (await this._simpleSnackBarMessage()).text();
}

/** Gets whether the snack-bar has been dismissed. */
async isDismissed(): Promise<boolean> {
// We consider the snackbar dismissed if it's not in the DOM. We can assert that the
// element isn't in the DOM by seeing that its width and height are zero.

const host = await this.host();
const [exit, dimensions] = await Promise.all([
// The snackbar container is marked with the "exit" attribute after it has been dismissed
// but before the animation has finished (after which it's removed from the DOM).
host.getAttribute('mat-exit'),
host.getDimensions(),
]);

return exit != null || (!!dimensions && dimensions.height === 0 && dimensions.width === 0);
}

/**
* Asserts that the current snack-bar does not use custom content. Promise rejects if
* custom content is used.
*/
private async _assertSimpleSnackBar(): Promise<void> {
if (!await this._isSimpleSnackBar()) {
throw Error('Method cannot be used for snack-bar with custom content.');
}
}

/**
* Asserts that the current snack-bar does not use custom content and has
* an action defined. Otherwise the promise will reject.
*/
private async _assertSimpleSnackBarWithAction(): Promise<void> {
await this._assertSimpleSnackBar();
if (!await this.hasAction()) {
throw Error('Method cannot be used for standard snack-bar without action.');
}
}

/** Whether the snack-bar is using the default content template. */
private async _isSimpleSnackBar(): Promise<boolean> {
return await this._simpleSnackBar() !== null;
}
}
34 changes: 17 additions & 17 deletions src/material/snack-bar/testing/shared.spec.ts
@@ -1,7 +1,7 @@
import {OverlayContainer} from '@angular/cdk/overlay';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, TemplateRef, ViewChild} from '@angular/core';
import {Component, TemplateRef, ViewChild, Injector} from '@angular/core';
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
Expand All @@ -13,6 +13,7 @@ import {MatSnackBarHarness} from './snack-bar-harness';
*/
export function runHarnessTests(
snackBarModule: typeof MatSnackBarModule,
snackBarToken: typeof MatSnackBar,
snackBarHarness: typeof MatSnackBarHarness) {
let fixture: ComponentFixture<SnackbarHarnessTest>;
let loader: HarnessLoader;
Expand Down Expand Up @@ -140,25 +141,24 @@ export function runHarnessTests(
snackBar = await loader.getHarness(snackBarHarness);
await expectAsync(snackBar.dismissWithAction()).toBeRejectedWithError(/without action/);
});
}

@Component({
template: `
<ng-template>
My custom snack-bar.
</ng-template>
`
})
class SnackbarHarnessTest {
@ViewChild(TemplateRef) customTmpl: TemplateRef<any>;
@Component({
template: `<ng-template>My custom snack-bar.</ng-template>`
})
class SnackbarHarnessTest {
@ViewChild(TemplateRef) customTmpl: TemplateRef<any>;
snackBar: MatSnackBar;

constructor(readonly snackBar: MatSnackBar) {}
constructor(injector: Injector) {
this.snackBar = injector.get(snackBarToken);
}

openSimple(message: string, action = '', config?: MatSnackBarConfig) {
return this.snackBar.open(message, action, config);
}
openSimple(message: string, action = '', config?: MatSnackBarConfig) {
return this.snackBar.open(message, action, config);
}

openCustom(config?: MatSnackBarConfig) {
return this.snackBar.openFromTemplate(this.customTmpl, config);
openCustom(config?: MatSnackBarConfig) {
return this.snackBar.openFromTemplate(this.customTmpl, config);
}
}
}
4 changes: 2 additions & 2 deletions src/material/snack-bar/testing/snack-bar-harness.spec.ts
@@ -1,7 +1,7 @@
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatSnackBarModule, MatSnackBar} from '@angular/material/snack-bar';
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
import {MatSnackBarHarness} from './snack-bar-harness';

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

0 comments on commit 4aa8718

Please sign in to comment.