Skip to content

Commit

Permalink
feat(material): add test harness for snack-bar
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion committed Sep 18, 2019
1 parent 2001965 commit 3899191
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 0 deletions.
46 changes: 46 additions & 0 deletions src/material/snack-bar/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

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

ng_test_library(
name = "harness_tests_lib",
srcs = ["shared.spec.ts"],
deps = [
":testing",
"//src/cdk/overlay",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/snack-bar",
"@npm//@angular/platform-browser",
],
)

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

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
9 changes: 9 additions & 0 deletions src/material/snack-bar/testing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions src/material/snack-bar/testing/public-api.ts
Original file line number Diff line number Diff line change
@@ -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 './snack-bar-harness';
166 changes: 166 additions & 0 deletions src/material/snack-bar/testing/shared.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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 {ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
import {MatSnackBarHarness} from '@angular/material/snack-bar/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';

/**
* Function that can be used to run the shared snack-bar harness tests for either
* the non-MDC or MDC based snack-bar harness.
*/
export function runHarnessTests(
snackBarModule: typeof MatSnackBarModule,
snackBarHarness: typeof MatSnackBarHarness) {
let fixture: ComponentFixture<SnackbarHarnessTest>;
let loader: HarnessLoader;
let overlayContainer: OverlayContainer;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [snackBarModule, NoopAnimationsModule],
declarations: [SnackbarHarnessTest],
}).compileComponents();

fixture = TestBed.createComponent(SnackbarHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
inject([OverlayContainer], (oc: OverlayContainer) => {
overlayContainer = oc;
})();
});

afterEach(() => {
// Angular won't call this for us so we need to do it ourselves to avoid leaks.
overlayContainer.ngOnDestroy();
overlayContainer = null!;
});

it('should load harness for simple snack-bar', async () => {
const snackBarRef = fixture.componentInstance.openSimple('Hello!', '');
let snackBars = await loader.getAllHarnesses(snackBarHarness);

expect(snackBars.length).toBe(1);

snackBarRef.dismiss();
snackBars = await loader.getAllHarnesses(snackBarHarness);
expect(snackBars.length).toBe(0);
});

it('should load harness for custom snack-bar', async () => {
const snackBarRef = fixture.componentInstance.openCustom();
let snackBars = await loader.getAllHarnesses(snackBarHarness);

expect(snackBars.length).toBe(1);

snackBarRef.dismiss();
snackBars = await loader.getAllHarnesses(snackBarHarness);
expect(snackBars.length).toBe(0);
});

it('should be able to get role of snack-bar', async () => {
fixture.componentInstance.openCustom();
let snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.getRole()).toBe('alert');

fixture.componentInstance.openCustom({politeness: 'polite'});
snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.getRole()).toBe('status');

fixture.componentInstance.openCustom({politeness: 'off'});
snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.getRole()).toBe(null);
});

it('should be able to get message of simple snack-bar', async () => {
fixture.componentInstance.openSimple('Subscribed to newsletter.');
let snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.getMessage()).toBe('Subscribed to newsletter.');

// For snack-bar's with custom template, the message cannot be
// retrieved. We expect an error to be thrown.
fixture.componentInstance.openCustom();
snackBar = await loader.getHarness(snackBarHarness);
await expectAsyncError(() => snackBar.getMessage(), /custom content/);
});

it('should be able to get action description of simple snack-bar', async () => {
fixture.componentInstance.openSimple('Hello', 'Unsubscribe');
let snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.getActionDescription()).toBe('Unsubscribe');

// For snack-bar's with custom template, the action description
// cannot be retrieved. We expect an error to be thrown.
fixture.componentInstance.openCustom();
snackBar = await loader.getHarness(snackBarHarness);
await expectAsyncError(() => snackBar.getActionDescription(), /custom content/);
});

it('should be able to check whether simple snack-bar has action', async () => {
fixture.componentInstance.openSimple('With action', 'Unsubscribe');
let snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.hasAction()).toBe(true);

fixture.componentInstance.openSimple('No action');
snackBar = await loader.getHarness(snackBarHarness);
expect(await snackBar.hasAction()).toBe(false);

// For snack-bar's with custom template, the action cannot
// be found. We expect an error to be thrown.
fixture.componentInstance.openCustom();
snackBar = await loader.getHarness(snackBarHarness);
await expectAsyncError(() => snackBar.hasAction(), /custom content/);
});

it('should be able to dismiss simple snack-bar with action', async () => {
const snackBarRef = fixture.componentInstance.openSimple('With action', 'Unsubscribe');
let snackBar = await loader.getHarness(snackBarHarness);
let actionCount = 0;
snackBarRef.onAction().subscribe(() => actionCount++);

await snackBar.dismissWithAction();
expect(actionCount).toBe(1);

fixture.componentInstance.openSimple('No action');
snackBar = await loader.getHarness(snackBarHarness);
await expectAsyncError(() => snackBar.dismissWithAction(), /without action/);
});
}

/**
* Expects the asynchronous function to throw an error that matches
* the specified expectation.
*/
async function expectAsyncError(fn: () => Promise<any>, expectation: RegExp) {
let error: string|null = null;
try {
await fn();
} catch (e) {
error = e.toString();
}
expect(error).not.toBe(null);
expect(error!).toMatch(expectation);
}

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

constructor(readonly snackBar: MatSnackBar) {}

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

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

describe('Non-MDC-based MatSnackBarHarness', () => {
runHarnessTests(MatSnackBarModule, MatSnackBarHarness);
});
95 changes: 95 additions & 0 deletions src/material/snack-bar/testing/snack-bar-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @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} from '@angular/cdk/testing';

/**
* Harness for interacting with a standard mat-snack-bar in tests.
* @dynamic
*/
export class MatSnackBarHarness extends ComponentHarness {
// Developers can provide a custom component or template for the
// snackbar. The canonical snack-bar parent is the "MatSnackBarContainer".
static hostSelector = '.mat-snack-bar-container';

private _simpleSnackBar = this.locatorForOptional('.mat-simple-snackbar');
private _simpleSnackBarMessage = this.locatorFor('.mat-simple-snackbar > span');
private _simpleSnackBarActionButton =
this.locatorForOptional('.mat-simple-snackbar-action > button');

/**
* 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>;
}

/**
* Gets 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();
}

/**
* Asserts that the current snack-bar does not use custom content. Throws if
* custom content is used.
*/
private async _assertSimpleSnackBar(): Promise<void> {
if (!await this._isSimpleSnackBar()) {
throw new 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 an error will be thrown.
*/
private async _assertSimpleSnackBarWithAction(): Promise<void> {
await this._assertSimpleSnackBar();
if (!await this.hasAction()) {
throw new Error('Method cannot be used for standard snack-bar without action.');
}
}

/** Gets whether the snack-bar is using the default content template. */
private async _isSimpleSnackBar(): Promise<boolean> {
return await this._simpleSnackBar() !== null;
}
}
2 changes: 2 additions & 0 deletions test/karma-system-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ System.config({
'@angular/material/slide-toggle': 'dist/packages/material/slide-toggle/index.js',
'@angular/material/slider': 'dist/packages/material/slider/index.js',
'@angular/material/snack-bar': 'dist/packages/material/snack-bar/index.js',
'@angular/material/snack-bar/testing': 'dist/packages/material/snack-bar/testing/index.js',
'@angular/material/snack-bar/testing/shared.spec': 'dist/packages/material/snack-bar/testing/shared.spec.js',
'@angular/material/sort': 'dist/packages/material/sort/index.js',
'@angular/material/stepper': 'dist/packages/material/stepper/index.js',
'@angular/material/table': 'dist/packages/material/table/index.js',
Expand Down

0 comments on commit 3899191

Please sign in to comment.