Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(material/expansion): add test harness #17691

Merged
merged 2 commits into from
Dec 3, 2019
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
25 changes: 16 additions & 9 deletions src/cdk/testing/component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,16 +333,23 @@ export class HarnessPredicate<T extends ComponentHarness> {
}

/**
* Checks if a string matches the given pattern.
* @param s The string to check, or a Promise for the string to check.
* @param pattern The pattern the string is expected to match. If `pattern` is a string, `s` is
* expected to match exactly. If `pattern` is a regex, a partial match is allowed.
* @return A Promise that resolves to whether the string matches the pattern.
* Checks if the specified nullable string value matches the given pattern.
* @param value The nullable string value to check, or a Promise resolving to the
* nullable string value.
* @param pattern The pattern the value is expected to match. If `pattern` is a string,
* `value` is expected to match exactly. If `pattern` is a regex, a partial match is
* allowed. If `pattern` is `null`, the value is expected to be `null`.
* @return A Promise that resolves to whether the value matches the pattern.
*/
static async stringMatches(s: string | Promise<string>, pattern: string | RegExp):
Promise<boolean> {
s = await s;
return typeof pattern === 'string' ? s === pattern : pattern.test(s);
static async stringMatches(value: string | null | Promise<string | null>,
pattern: string | RegExp | null): Promise<boolean> {
value = await value;
if (pattern === null) {
return value === null;
} else if (value === null) {
return false;
}
return typeof pattern === 'string' ? value === pattern : pattern.test(value);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/material/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ entryPoints = [
"dialog/testing",
"divider",
"expansion",
"expansion/testing",
"form-field",
"grid-list",
"icon",
Expand Down
5 changes: 4 additions & 1 deletion src/material/expansion/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import {MatExpansionPanelHeader} from './expansion-panel-header';
useExisting: MatAccordion
}],
host: {
class: 'mat-accordion'
class: 'mat-accordion',
// Class binding which is only used by the test harness as there is no other
// way for the harness to detect if multiple panel support is enabled.
'[class.mat-accordion-multi]': 'this.multi',
}
})
export class MatAccordion extends CdkAccordion implements MatAccordionBase, AfterContentInit {
Expand Down
51 changes: 51 additions & 0 deletions src/material/expansion/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])

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

ts_library(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/material/expansion/testing",
deps = [
"//src/cdk/coercion",
"//src/cdk/testing",
],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)

ng_test_library(
name = "harness_tests_lib",
srcs = ["shared.spec.ts"],
deps = [
":testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/expansion",
"@npm//@angular/platform-browser",
],
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(
["**/*.spec.ts"],
exclude = ["shared.spec.ts"],
),
deps = [
":harness_tests_lib",
":testing",
"//src/material/expansion",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
37 changes: 37 additions & 0 deletions src/material/expansion/testing/accordion-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @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 {MatExpansionPanelHarness} from './expansion-harness';
import {AccordionHarnessFilters, ExpansionPanelHarnessFilters} from './expansion-harness-filters';

/** Harness for interacting with a standard mat-accordion in tests. */
export class MatAccordionHarness extends ComponentHarness {
static hostSelector = '.mat-accordion';

/**
* Gets a `HarnessPredicate` that can be used to search for an accordion
* with specific attributes.
* @param options Options for narrowing the search.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: AccordionHarnessFilters = {}): HarnessPredicate<MatAccordionHarness> {
return new HarnessPredicate(MatAccordionHarness, options);
}

/** Gets all expansion panels which are part of the accordion. */
async getExpansionPanels(filter: ExpansionPanelHarnessFilters = {}):
Promise<MatExpansionPanelHarness[]> {
return this.locatorForAll(MatExpansionPanelHarness.with(filter))();
}

/** Whether the accordion allows multiple expanded panels simultaneously. */
async isMulti(): Promise<boolean> {
return (await this.host()).hasClass('mat-accordion-multi');
}
}
19 changes: 19 additions & 0 deletions src/material/expansion/testing/expansion-harness-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @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';

export interface AccordionHarnessFilters extends BaseHarnessFilters {}

export interface ExpansionPanelHarnessFilters extends BaseHarnessFilters {
title?: string|RegExp|null;
description?: string|RegExp|null;
content?: string|RegExp;
expanded?: boolean;
disabled?: boolean;
}
9 changes: 9 additions & 0 deletions src/material/expansion/testing/expansion-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {MatExpansionModule} from '@angular/material/expansion';

import {MatAccordionHarness} from './accordion-harness';
import {MatExpansionPanelHarness} from './expansion-harness';
import {runHarnessTests} from './shared.spec';

describe('Non-MDC-based expansion harnesses', () => {
runHarnessTests(MatExpansionModule, MatAccordionHarness, MatExpansionPanelHarness);
});
141 changes: 141 additions & 0 deletions src/material/expansion/testing/expansion-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @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, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
import {ExpansionPanelHarnessFilters} from './expansion-harness-filters';

const EXPANSION_PANEL_CONTENT_SELECTOR = '.mat-expansion-panel-content';

/** Harness for interacting with a standard mat-expansion-panel in tests. */
export class MatExpansionPanelHarness extends ComponentHarness {
static hostSelector = '.mat-expansion-panel';

private _header = this.locatorFor('.mat-expansion-panel-header');
private _title = this.locatorForOptional('.mat-expansion-panel-header-title');
private _description = this.locatorForOptional('.mat-expansion-panel-header-description');
private _expansionIndicator = this.locatorForOptional('.mat-expansion-indicator');
private _content = this.locatorFor(EXPANSION_PANEL_CONTENT_SELECTOR);

/**
* Gets a `HarnessPredicate` that can be used to search for an expansion-panel
* with specific attributes.
* @param options Options for narrowing the search:
* - `title` finds an expansion-panel with a specific title text.
* - `description` finds an expansion-panel with a specific description text.
* - `expanded` finds an expansion-panel that is currently expanded.
* - `disabled` finds an expansion-panel that is disabled.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ExpansionPanelHarnessFilters = {}):
HarnessPredicate<MatExpansionPanelHarness> {
return new HarnessPredicate(MatExpansionPanelHarness, options)
.addOption(
'title', options.title,
(harness, title) => HarnessPredicate.stringMatches(harness.getTitle(), title))
.addOption(
'description', options.description,
(harness, description) =>
HarnessPredicate.stringMatches(harness.getDescription(), description))
.addOption(
'content', options.content,
(harness, content) => HarnessPredicate.stringMatches(harness.getTextContent(), content))
.addOption(
'expanded', options.expanded,
async (harness, expanded) => (await harness.isExpanded()) === expanded)
.addOption(
'disabled', options.disabled,
async (harness, disabled) => (await harness.isDisabled()) === disabled);
}

/** Whether the panel is expanded. */
async isExpanded(): Promise<boolean> {
return (await this.host()).hasClass('mat-expanded');
}

/**
* Gets the title text of the panel.
* @returns Title text or `null` if no title is set up.
*/
async getTitle(): Promise<string|null> {
const titleEl = await this._title();
return titleEl ? titleEl.text() : null;
mmalerba marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Gets the description text of the panel.
* @returns Description text or `null` if no description is set up.
*/
async getDescription(): Promise<string|null> {
const descriptionEl = await this._description();
return descriptionEl ? descriptionEl.text() : null;
}

/** Whether the panel is disabled. */
async isDisabled(): Promise<boolean> {
return await (await this._header()).getAttribute('aria-disabled') === 'true';
}

/**
* Toggles the expanded state of the panel by clicking on the panel
* header. This method will not work if the panel is disabled.
*/
async toggle(): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to toggle, I would add methods for expand and collapse that no-op if the panel is already expanded/collapsed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

await (await this._header()).click();
}

/** Expands the expansion panel if collapsed. */
async expand(): Promise<void> {
if (!await this.isExpanded()) {
await this.toggle();
}
}

/** Collapses the expansion panel if expanded. */
async collapse(): Promise<void> {
if (await this.isExpanded()) {
await this.toggle();
}
}

/** Gets the text content of the panel. */
async getTextContent(): Promise<string> {
return (await this._content()).text();
}

/**
* Gets a `HarnessLoader` that can be used to load harnesses for
* components within the panel's content area.
*/
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
return this.locatorFactory.harnessLoaderFor(EXPANSION_PANEL_CONTENT_SELECTOR);
}

/** Focuses the panel. */
async focus(): Promise<void> {
return (await this._header()).focus();
}

/** Blurs the panel. */
async blur(): Promise<void> {
return (await this._header()).blur();
}

/** Whether the panel has a toggle indicator displayed. */
async hasToggleIndicator(): Promise<boolean> {
return (await this._expansionIndicator()) !== null;
}

/** Gets the position of the toggle indicator. */
async getToggleIndicatorPosition(): Promise<'before'|'after'> {
// By default the expansion indicator will show "after" the panel header content.
if (await (await this._header()).hasClass('mat-expansion-toggle-indicator-before')) {
return 'before';
}
return 'after';
}
}
9 changes: 9 additions & 0 deletions src/material/expansion/testing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
11 changes: 11 additions & 0 deletions src/material/expansion/testing/public-api.ts
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 * from './accordion-harness';
export * from './expansion-harness';
export * from './expansion-harness-filters';
Loading