Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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
/src/material-experimental/mdc-menu/** @crisbeto
/src/material-experimental/mdc-progress-spinner/** @andrewseguin
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);
}
}