Skip to content

Commit

Permalink
feat(material-experimental): add test harness for snack-bar
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion committed Aug 28, 2019
1 parent 77b0e0e commit 44d4f44
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
/src/material-experimental/mdc-slide-toggle/** @crisbeto
# Note to implementer: please repossess
/src/material-experimental/mdc-slider/** @devversion
/src/material-experimental/mdc-snack-bar/** @devversion
/src/material-experimental/mdc-tabs/** @crisbeto
/src/material-experimental/mdc-sidenav/** @crisbeto
/src/material-experimental/mdc-theming/** @mmalerba
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {UnitTestElement} from './unit-test-element';

/** A `HarnessEnvironment` implementation for Angular's Testbed. */
export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
super(rawRootElement);
}

Expand Down
31 changes: 31 additions & 0 deletions src/material-experimental/mdc-snack-bar/harness/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package(default_visibility = ["//visibility:public"])

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

ts_library(
name = "harness",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
deps = [
"//src/cdk-experimental/testing",
],
)

ng_test_library(
name = "harness_tests",
srcs = glob(["**/*.spec.ts"]),
deps = [
":harness",
"//src/cdk-experimental/testing",
"//src/cdk-experimental/testing/testbed",
"//src/material/snack-bar",
"@npm//@angular/platform-browser",
],
)

ng_web_test_suite(
name = "tests",
deps = [":harness_tests"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {HarnessLoader} from '@angular/cdk-experimental/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
import {Component, TemplateRef, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatSnackBarHarness} from './snack-bar-harness';

let fixture: ComponentFixture<SnackbarHarnessTest>;
let loader: HarnessLoader;
let snackBarHarness: typeof MatSnackBarHarness;

describe('MatSnackBarHarness', () => {
describe('non-MDC-based', () => {
beforeEach(async () => {
await TestBed
.configureTestingModule({
imports: [MatSnackBarModule, NoopAnimationsModule],
declarations: [SnackbarHarnessTest],
})
.compileComponents();

fixture = TestBed.createComponent(SnackbarHarnessTest);
fixture.detectChanges();
// Note: we need to specify "document.body" as root element because
// the snack-bar's will be added as immediate children of the body.
loader = new TestbedHarnessEnvironment(document.body, fixture);
snackBarHarness = MatSnackBarHarness;
});

runTests();
});

describe(
'MDC-based',
() => {
// TODO: run tests for MDC based snack-bar once implemented.
});
});

/** Shared tests to run on both the original and MDC-based snack-bar's. */
function runTests() {
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);
}
}
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-experimental/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;
}
}

0 comments on commit 44d4f44

Please sign in to comment.