diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl index fc5da4f7c854..bd597da02e56 100644 --- a/src/material-experimental/config.bzl +++ b/src/material-experimental/config.bzl @@ -34,6 +34,7 @@ entryPoints = [ "mdc-slider", "mdc-slider/testing", "mdc-snack-bar", + "mdc-snack-bar/testing", "mdc-table", "mdc-table/testing", "mdc-tabs", diff --git a/src/material-experimental/mdc-snack-bar/testing/BUILD.bazel b/src/material-experimental/mdc-snack-bar/testing/BUILD.bazel new file mode 100644 index 000000000000..1b1520d343ee --- /dev/null +++ b/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", + ], +) diff --git a/src/material-experimental/mdc-snack-bar/testing/index.ts b/src/material-experimental/mdc-snack-bar/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/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'; diff --git a/src/material-experimental/mdc-snack-bar/testing/public-api.ts b/src/material-experimental/mdc-snack-bar/testing/public-api.ts new file mode 100644 index 000000000000..0c6bbfb1e726 --- /dev/null +++ b/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'; diff --git a/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness-filters.ts b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness-filters.ts new file mode 100644 index 000000000000..f8c6ed681d8a --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness-filters.ts @@ -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 {} diff --git a/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts new file mode 100644 index 000000000000..b9235851c9d5 --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts @@ -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); +}); diff --git a/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.ts b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.ts new file mode 100644 index 000000000000..334a1d3385a8 --- /dev/null +++ b/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 { + 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 { + 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 { + 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 { + 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 { + await this._assertSimpleSnackBar(); + return (await this._simpleSnackBarMessage()).text(); + } + + /** Gets whether the snack-bar has been dismissed. */ + async isDismissed(): Promise { + // 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 { + 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 { + 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 { + return await this._simpleSnackBar() !== null; + } +} diff --git a/src/material/snack-bar/testing/shared.spec.ts b/src/material/snack-bar/testing/shared.spec.ts index 7dc828e78d73..bd63cba3c059 100644 --- a/src/material/snack-bar/testing/shared.spec.ts +++ b/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'; @@ -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; let loader: HarnessLoader; @@ -140,25 +141,24 @@ export function runHarnessTests( snackBar = await loader.getHarness(snackBarHarness); await expectAsync(snackBar.dismissWithAction()).toBeRejectedWithError(/without action/); }); -} -@Component({ - template: ` - - My custom snack-bar. - - ` -}) -class SnackbarHarnessTest { - @ViewChild(TemplateRef) customTmpl: TemplateRef; + @Component({ + template: `My custom snack-bar.` + }) + class SnackbarHarnessTest { + @ViewChild(TemplateRef) customTmpl: TemplateRef; + 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); + } } } diff --git a/src/material/snack-bar/testing/snack-bar-harness.spec.ts b/src/material/snack-bar/testing/snack-bar-harness.spec.ts index 393419f37f2d..9eb1bf771d90 100644 --- a/src/material/snack-bar/testing/snack-bar-harness.spec.ts +++ b/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); });