Skip to content

Commit

Permalink
fix(material-experimental/mdc-snack-bar): add harness support for get…
Browse files Browse the repository at this point in the history
…ting label and actions (#23585)

* fix(material-experimental/mdc-snack-bar): add harness support for getting label and actions

of mdc-snack-bar with custom content

* fixup! fix(material-experimental/mdc-snack-bar): add harness support for getting label and actions
  • Loading branch information
mmalerba committed Sep 16, 2021
1 parent cfe79b8 commit 4e3b72c
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 46 deletions.
3 changes: 3 additions & 0 deletions src/material-experimental/mdc-snack-bar/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ ng_test_library(
srcs = glob(["**/*.spec.ts"]),
deps = [
":testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material-experimental/mdc-snack-bar",
"//src/material/snack-bar/testing:harness_tests_lib",
"@npm//@angular/platform-browser",
],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,88 @@
import {MatSnackBarModule, MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
import {
MatSnackBar,
MatSnackBarConfig,
MatSnackBarModule
} from '@angular/material-experimental/mdc-snack-bar';
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
import {MatSnackBarHarness} from './snack-bar-harness';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, TemplateRef, ViewChild} from '@angular/core';

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

describe('MDC-based MatSnackBarHarness (MDC only behavior)', () => {
let fixture: ComponentFixture<SnackbarHarnessTest>;
let loader: HarnessLoader;

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

fixture = TestBed.createComponent(SnackbarHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

it('should be able to get message of a snack-bar with custom content', async () => {
fixture.componentInstance.openCustom();
let snackBar = await loader.getHarness(MatSnackBarHarness);
expect(await snackBar.getMessage()).toBe('My custom snack-bar.');

fixture.componentInstance.openCustomWithAction();
snackBar = await loader.getHarness(MatSnackBarHarness);
expect(await snackBar.getMessage()).toBe('My custom snack-bar with action.');
});

it('should fail to get action description of a snack-bar with no action', async () => {
fixture.componentInstance.openCustom();
const snackBar = await loader.getHarness(MatSnackBarHarness);
await expectAsync(snackBar.getActionDescription()).toBeRejectedWithError(/without an action/);
});

it('should be able to get action description of a snack-bar with an action', async () => {
fixture.componentInstance.openCustomWithAction();
const snackBar = await loader.getHarness(MatSnackBarHarness);
expect(await snackBar.getActionDescription()).toBe('Ok');
});

it('should be able to check whether a snack-bar with custom content has an action', async () => {
fixture.componentInstance.openCustom();
let snackBar = await loader.getHarness(MatSnackBarHarness);
expect(await snackBar.hasAction()).toBe(false);

fixture.componentInstance.openCustomWithAction();
snackBar = await loader.getHarness(MatSnackBarHarness);
expect(await snackBar.hasAction()).toBe(true);
});
});

@Component({
template: `
<ng-template #custom>My custom snack-bar.</ng-template>
<ng-template #customWithAction>
<span matSnackBarLabel>My custom snack-bar with action.</span>
<div matSnackBarActions><button matSnackBarAction>Ok</button></div>
</ng-template>
`
})
class SnackbarHarnessTest {
@ViewChild('custom') customTmpl: TemplateRef<any>;
@ViewChild('customWithAction') customWithActionTmpl: TemplateRef<any>;

constructor(public snackBar: MatSnackBar) {}

openCustom(config?: MatSnackBarConfig) {
return this.snackBar.openFromTemplate(this.customTmpl, config);
}

openCustomWithAction(config?: MatSnackBarConfig) {
return this.snackBar.openFromTemplate(this.customWithActionTmpl, config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ export class MatSnackBarHarness extends BaseMatSnackBarHarness {
// notified when it's done.
/** The selector for the host element of a `MatSnackBar` instance. */
static override hostSelector = '.mat-mdc-snack-bar-container:not([mat-exit])';
protected override _messageSelector = '.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label';
protected override _simpleSnackBarSelector = '.mat-mdc-simple-snack-bar';
protected override _actionButtonSelector = '.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-action';
protected override _messageSelector = '.mdc-snackbar__label';
protected override _actionButtonSelector = '.mat-mdc-snack-bar-action';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSnackBarHarness` that meets
Expand All @@ -35,4 +34,6 @@ export class MatSnackBarHarness extends BaseMatSnackBarHarness {
options: SnackBarHarnessFilters = {}): HarnessPredicate<BaseMatSnackBarHarness> {
return new HarnessPredicate<BaseMatSnackBarHarness>(MatSnackBarHarness, options);
}

protected override async _assertContentAnnotated() {}
}
3 changes: 3 additions & 0 deletions src/material/snack-bar/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ ng_test_library(
deps = [
":harness_tests_lib",
":testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/snack-bar",
"@npm//@angular/platform-browser",
],
)

Expand Down
20 changes: 1 addition & 19 deletions src/material/snack-bar/testing/shared.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,12 @@ export function runHarnessTests(
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 expectAsync(snackBar.getMessage()).toBeRejectedWithError(/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 expectAsync(snackBar.getActionDescription()).toBeRejectedWithError(/custom content/);
});

it('should be able to check whether simple snack-bar has action', async () => {
Expand All @@ -119,12 +107,6 @@ export function runHarnessTests(
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 expectAsync(snackBar.hasAction()).toBeRejectedWithError(/custom content/);
});

it('should be able to dismiss simple snack-bar with action', async () => {
Expand All @@ -143,7 +125,7 @@ export function runHarnessTests(

fixture.componentInstance.openSimple('No action');
snackBar = await loader.getHarness(snackBarHarness);
await expectAsync(snackBar.dismissWithAction()).toBeRejectedWithError(/without action/);
await expectAsync(snackBar.dismissWithAction()).toBeRejectedWithError(/without an action/);
});

@Component({
Expand Down
54 changes: 53 additions & 1 deletion src/material/snack-bar/testing/snack-bar-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,59 @@
import {MatSnackBarModule, MatSnackBar} from '@angular/material/snack-bar';
import {MatSnackBar, MatSnackBarConfig, MatSnackBarModule} from '@angular/material/snack-bar';
import {runHarnessTests} from '@angular/material/snack-bar/testing/shared.spec';
import {MatSnackBarHarness} from './snack-bar-harness';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, TemplateRef, ViewChild} from '@angular/core';

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

describe('Non-MDC-based MatSnackBarHarness (non-MDC only behavior)', () => {
let fixture: ComponentFixture<SnackbarHarnessTest>;
let loader: HarnessLoader;

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

fixture = TestBed.createComponent(SnackbarHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

it('should fail to get message of a snack-bar with custom content', async () => {
fixture.componentInstance.openCustom();
const snackBar = await loader.getHarness(MatSnackBarHarness);
await expectAsync(snackBar.getMessage()).toBeRejectedWithError(/custom content/);
});

it('should fail to get action description of a snack-bar with custom content', async () => {
fixture.componentInstance.openCustom();
const snackBar = await loader.getHarness(MatSnackBarHarness);
await expectAsync(snackBar.getActionDescription()).toBeRejectedWithError(/custom content/);
});

it('should fail to check whether a snack-bar with custom content has an action', async () => {
fixture.componentInstance.openCustom();
const snackBar = await loader.getHarness(MatSnackBarHarness);
await expectAsync(snackBar.hasAction()).toBeRejectedWithError(/custom content/);
});
});

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

constructor(public snackBar: MatSnackBar) {}

openCustom(config?: MatSnackBarConfig) {
return this.snackBar.openFromTemplate(this.customTmpl, config);
}
}
39 changes: 19 additions & 20 deletions src/material/snack-bar/testing/snack-bar-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ export class MatSnackBarHarness extends ContentContainerComponentHarness<string>
/** The selector for the host element of a `MatSnackBar` instance. */
static hostSelector = '.mat-snack-bar-container';
protected _messageSelector = '.mat-simple-snackbar > span';
protected _simpleSnackBarSelector = '.mat-simple-snackbar';
protected _actionButtonSelector = '.mat-simple-snackbar-action > button';
private _simpleSnackBarLiveRegion = this.locatorFor('[aria-live]');
private _snackBarLiveRegion = this.locatorFor('[aria-live]');

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSnackBarHarness` that meets
Expand All @@ -46,25 +45,25 @@ export class MatSnackBarHarness extends ContentContainerComponentHarness<string>
* determined based on the ARIA politeness specified in the snack-bar config.
*/
async getAriaLive(): Promise<AriaLivePoliteness> {
return (await this._simpleSnackBarLiveRegion())
return (await this._snackBarLiveRegion())
.getAttribute('aria-live') as Promise<AriaLivePoliteness>;
}

/**
* 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._getSimpleSnackBarActionButton()) !== null;
await this._assertContentAnnotated();
return (await this._getActionButton()) !== 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._getSimpleSnackBarActionButton())!.text();
await this._assertHasAction();
return (await this._getActionButton())!.text();
}


Expand All @@ -73,15 +72,15 @@ export class MatSnackBarHarness extends ContentContainerComponentHarness<string>
* without action or with custom content.
*/
async dismissWithAction(): Promise<void> {
await this._assertSimpleSnackBarWithAction();
await (await this._getSimpleSnackBarActionButton())!.click();
await this._assertHasAction();
await (await this._getActionButton())!.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();
await this._assertContentAnnotated();
return (await this.locatorFor(this._messageSelector)()).text();
}

Expand All @@ -102,33 +101,33 @@ export class MatSnackBarHarness extends ContentContainerComponentHarness<string>
}

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

/** Whether the snack-bar is using the default content template. */
private async _isSimpleSnackBar(): Promise<boolean> {
return await this.locatorForOptional(this._simpleSnackBarSelector)() !== null;
return await this.locatorForOptional('.mat-simple-snackbar')() !== null;
}

/** Gets the simple snack bar action button. */
private async _getSimpleSnackBarActionButton() {
private async _getActionButton() {
return this.locatorForOptional(this._actionButtonSelector)();
}
}
4 changes: 2 additions & 2 deletions tools/public_api_guard/material/snack-bar-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { HarnessPredicate } from '@angular/cdk/testing';
export class MatSnackBarHarness extends ContentContainerComponentHarness<string> {
// (undocumented)
protected _actionButtonSelector: string;
protected _assertContentAnnotated(): Promise<void>;
protected _assertHasAction(): Promise<void>;
dismissWithAction(): Promise<void>;
getActionDescription(): Promise<string>;
getAriaLive(): Promise<AriaLivePoliteness>;
Expand All @@ -24,8 +26,6 @@ export class MatSnackBarHarness extends ContentContainerComponentHarness<string>
isDismissed(): Promise<boolean>;
// (undocumented)
protected _messageSelector: string;
// (undocumented)
protected _simpleSnackBarSelector: string;
static with(options?: SnackBarHarnessFilters): HarnessPredicate<MatSnackBarHarness>;
}

Expand Down

0 comments on commit 4e3b72c

Please sign in to comment.