Skip to content

Commit

Permalink
feat(material-experimental): test harness for dialog (#16709)
Browse files Browse the repository at this point in the history
Resolves COMP-180.
  • Loading branch information
devversion authored and jelbourn committed Aug 30, 2019
1 parent dd548be commit f5e087b
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
/src/material-experimental/mdc-card/** @mmalerba
/src/material-experimental/mdc-checkbox/** @mmalerba
/src/material-experimental/mdc-chips/** @mmalerba
/src/material-experimental/mdc-dialog/** @devversion
/src/material-experimental/mdc-helpers/** @mmalerba
# Note to implementer: please repossess
/src/material-experimental/mdc-input/** @devversion
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
32 changes: 32 additions & 0 deletions src/material-experimental/mdc-dialog/harness/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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",
"//src/material/dialog",
],
)

ng_test_library(
name = "harness_tests",
srcs = glob(["**/*.spec.ts"]),
deps = [
":harness",
"//src/cdk-experimental/testing",
"//src/cdk-experimental/testing/testbed",
"//src/material/dialog",
"@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,11 @@
/**
* @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 type DialogHarnessFilters = {
id?: string;
};
132 changes: 132 additions & 0 deletions src/material-experimental/mdc-dialog/harness/dialog-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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 {MatDialog, MatDialogConfig, MatDialogModule} from '@angular/material/dialog';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatDialogHarness} from './dialog-harness';

let fixture: ComponentFixture<DialogHarnessTest>;
let loader: HarnessLoader;
let dialogHarness: typeof MatDialogHarness;

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

fixture = TestBed.createComponent(DialogHarnessTest);
fixture.detectChanges();
loader = new TestbedHarnessEnvironment(document.body, fixture);
dialogHarness = MatDialogHarness;
});

runTests();
});

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

/** Shared tests to run on both the original and MDC-based radio-button's. */
function runTests() {
it('should load harness for dialog', async () => {
fixture.componentInstance.open();
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(dialogs.length).toBe(1);
});

it('should load harness for dialog with specific id', async () => {
fixture.componentInstance.open({id: 'my-dialog'});
fixture.componentInstance.open({id: 'other'});
let dialogs = await loader.getAllHarnesses(dialogHarness);
expect(dialogs.length).toBe(2);

dialogs = await loader.getAllHarnesses(dialogHarness.with({id: 'my-dialog'}));
expect(dialogs.length).toBe(1);
});

it('should be able to get id of dialog', async () => {
fixture.componentInstance.open({id: 'my-dialog'});
fixture.componentInstance.open({id: 'other'});
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(await dialogs[0].getId()).toBe('my-dialog');
expect(await dialogs[1].getId()).toBe('other');
});

it('should be able to get role of dialog', async () => {
fixture.componentInstance.open({role: 'alertdialog'});
fixture.componentInstance.open({role: 'dialog'});
fixture.componentInstance.open({role: undefined});
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(await dialogs[0].getRole()).toBe('alertdialog');
expect(await dialogs[1].getRole()).toBe('dialog');
expect(await dialogs[2].getRole()).toBe(null);
});

it('should be able to get aria-label of dialog', async () => {
fixture.componentInstance.open();
fixture.componentInstance.open({ariaLabel: 'Confirm purchase.'});
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(await dialogs[0].getAriaLabel()).toBe(null);
expect(await dialogs[1].getAriaLabel()).toBe('Confirm purchase.');
});

it('should be able to get aria-labelledby of dialog', async () => {
fixture.componentInstance.open();
fixture.componentInstance.open({ariaLabelledBy: 'dialog-label'});
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(await dialogs[0].getAriaLabelledby()).toBe(null);
expect(await dialogs[1].getAriaLabelledby()).toBe('dialog-label');
});

it('should be able to get aria-describedby of dialog', async () => {
fixture.componentInstance.open();
fixture.componentInstance.open({ariaDescribedBy: 'dialog-description'});
const dialogs = await loader.getAllHarnesses(dialogHarness);
expect(await dialogs[0].getAriaDescribedby()).toBe(null);
expect(await dialogs[1].getAriaDescribedby()).toBe('dialog-description');
});

it('should be able to close dialog', async () => {
fixture.componentInstance.open({disableClose: true});
fixture.componentInstance.open();
let dialogs = await loader.getAllHarnesses(dialogHarness);

expect(dialogs.length).toBe(2);
await dialogs[0].close();

dialogs = await loader.getAllHarnesses(dialogHarness);
expect(dialogs.length).toBe(1);

// should be a noop since "disableClose" is set to "true".
await dialogs[0].close();
dialogs = await loader.getAllHarnesses(dialogHarness);
expect(dialogs.length).toBe(1);
});
}

@Component({
template: `
<ng-template>
Hello from the dialog!
</ng-template>
`
})
class DialogHarnessTest {
@ViewChild(TemplateRef, {static: false}) dialogTmpl: TemplateRef<any>;

constructor(readonly dialog: MatDialog) {}

open(config?: MatDialogConfig) {
return this.dialog.open(this.dialogTmpl, config);
}
}
69 changes: 69 additions & 0 deletions src/material-experimental/mdc-dialog/harness/dialog-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @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, TestKey} from '@angular/cdk-experimental/testing';
import {DialogRole} from '@angular/material/dialog';
import {DialogHarnessFilters} from './dialog-harness-filters';

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

/**
* Gets a `HarnessPredicate` that can be used to search for a dialog with
* specific attributes.
* @param options Options for narrowing the search:
* - `id` finds a dialog with specific id.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: DialogHarnessFilters = {}): HarnessPredicate<MatDialogHarness> {
return new HarnessPredicate(MatDialogHarness)
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id);
}

/** Gets the id of the dialog. */
async getId(): Promise<string|null> {
const id = await (await this.host()).getAttribute('id');
// In case no id has been specified, the "id" property always returns
// an empty string. To make this method more explicit, we return null.
return id !== '' ? id : null;
}

/** Gets the role of the dialog. */
async getRole(): Promise<DialogRole|null> {
return (await this.host()).getAttribute('role') as Promise<DialogRole|null>;
}

/** Gets the value of the dialog's "aria-label" attribute. */
async getAriaLabel(): Promise<string|null> {
return (await this.host()).getAttribute('aria-label');
}

/** Gets the value of the dialog's "aria-labelledby" attribute. */
async getAriaLabelledby(): Promise<string|null> {
return (await this.host()).getAttribute('aria-labelledby');
}

/** Gets the value of the dialog's "aria-describedby" attribute. */
async getAriaDescribedby(): Promise<string|null> {
return (await this.host()).getAttribute('aria-describedby');
}

/**
* Closes the dialog by pressing escape. Note that this method cannot
* be used if "disableClose" has been set to true for the dialog.
*/
async close(): Promise<void> {
await (await this.host()).sendKeys(TestKey.ESCAPE);
}
}

0 comments on commit f5e087b

Please sign in to comment.