Skip to content

Commit

Permalink
feat(chips): add test harness (#20028)
Browse files Browse the repository at this point in the history
Adds test harnesses for the Material chips and the related components.
  • Loading branch information
crisbeto committed Jul 28, 2020
1 parent a6f1a33 commit 165622e
Show file tree
Hide file tree
Showing 14 changed files with 827 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -293,6 +293,7 @@
/tools/public_api_guard/material/card.d.ts @jelbourn
/tools/public_api_guard/material/checkbox.d.ts @jelbourn @devversion
/tools/public_api_guard/material/chips.d.ts @jelbourn
/tools/public_api_guard/material/chips/testing.d.ts @jelbourn
/tools/public_api_guard/material/core.d.ts @jelbourn
/tools/public_api_guard/material/datepicker.d.ts @mmalerba
/tools/public_api_guard/material/dialog.d.ts @jelbourn @crisbeto
Expand Down
4 changes: 4 additions & 0 deletions src/material/chips/chip-list.ts
Expand Up @@ -418,6 +418,10 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
/** Associates an HTML input element with this chip list. */
registerInput(inputElement: MatChipTextControl): void {
this._chipInput = inputElement;

// We use this attribute to match the chip list to its input in test harnesses.
// Set the attribute directly here to avoid "changed after checked" errors.
this._elementRef.nativeElement.setAttribute('data-mat-chip-input', inputElement.id);
}

/**
Expand Down
52 changes: 52 additions & 0 deletions src/material/chips/testing/BUILD.bazel
@@ -0,0 +1,52 @@
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/chips/testing",
deps = [
"//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/private",
"//src/cdk/testing/testbed",
"//src/material/chips",
"//src/material/form-field",
"@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/chips",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
30 changes: 30 additions & 0 deletions src/material/chips/testing/chip-harness-filters.ts
@@ -0,0 +1,30 @@
/**
* @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 `MatChipHarness` instances. */
export interface ChipHarnessFilters extends BaseHarnessFilters {
/** Only find instances whose text matches the given value. */
text?: string | RegExp;
/** Only find chip instances whose selected state matches the given value. */
selected?: boolean;
}

/** A set of criteria that can be used to filter a list of `MatChipListHarness` instances. */
export interface ChipListHarnessFilters extends BaseHarnessFilters {}

/** A set of criteria that can be used to filter a list of `MatChipListInputHarness` instances. */
export interface ChipInputHarnessFilters extends BaseHarnessFilters {
/** Filters based on the value of the input. */
value?: string | RegExp;
/** Filters based on the placeholder text of the input. */
placeholder?: string | RegExp;
}

/** A set of criteria that can be used to filter a list of `MatChipRemoveHarness` instances. */
export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {}
78 changes: 78 additions & 0 deletions src/material/chips/testing/chip-harness.ts
@@ -0,0 +1,78 @@
/**
* @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/testing';
import {ChipHarnessFilters, ChipRemoveHarnessFilters} from './chip-harness-filters';
import {MatChipRemoveHarness} from './chip-remove-harness';

/** Harness for interacting with a standard Angular Material chip in tests. */
export class MatChipHarness extends ComponentHarness {
/** The selector for the host element of a `MatChip` instance. */
static hostSelector = '.mat-chip';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatChipHarness` that meets
* certain criteria.
* @param options Options for filtering which chip instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ChipHarnessFilters = {}): HarnessPredicate<MatChipHarness> {
return new HarnessPredicate(MatChipHarness, options)
.addOption('text', options.text,
(harness, label) => HarnessPredicate.stringMatches(harness.getText(), label))
.addOption('selected', options.selected,
async (harness, selected) => (await harness.isSelected()) === selected);
}

/** Gets the text of the chip. */
async getText(): Promise<string> {
return (await this.host()).text();
}

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

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

/** Selects the given chip. Only applies if it's selectable. */
async select(): Promise<void> {
if (!(await this.isSelected())) {
await this.toggle();
}
}

/** Deselects the given chip. Only applies if it's selectable. */
async deselect(): Promise<void> {
if (await this.isSelected()) {
await this.toggle();
}
}

/** Toggles the selected state of the given chip. Only applies if it's selectable. */
async toggle(): Promise<void> {
return (await this.host()).sendKeys(' ');
}

/** Removes the given chip. Only applies if it's removable. */
async remove(): Promise<void> {
await (await this.host()).sendKeys(TestKey.DELETE);
}

/**
* Gets the remove button inside of a chip.
* @param filter Optionally filters which chips are included.
*/
async getRemoveButton(filter: ChipRemoveHarnessFilters = {}): Promise<MatChipRemoveHarness> {
return this.locatorFor(MatChipRemoveHarness.with(filter))();
}
}
95 changes: 95 additions & 0 deletions src/material/chips/testing/chip-input-harness.ts
@@ -0,0 +1,95 @@
/**
* @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 {HarnessPredicate, ComponentHarness, TestKey} from '@angular/cdk/testing';
import {ChipInputHarnessFilters} from './chip-harness-filters';

/** Harness for interacting with a standard Material chip inputs in tests. */
export class MatChipInputHarness extends ComponentHarness {
static hostSelector = '.mat-chip-input';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatChipInputHarness` that meets
* certain criteria.
* @param options Options for filtering which input instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ChipInputHarnessFilters = {}): HarnessPredicate<MatChipInputHarness> {
return new HarnessPredicate(MatChipInputHarness, options)
.addOption('value', options.value, async (harness, value) => {
return (await harness.getValue()) === value;
})
.addOption('placeholder', options.placeholder, async (harness, placeholder) => {
return (await harness.getPlaceholder()) === placeholder;
});
}

/** Whether the input is disabled. */
async isDisabled(): Promise<boolean> {
return (await this.host()).getProperty('disabled')!;
}

/** Whether the input is required. */
async isRequired(): Promise<boolean> {
return (await this.host()).getProperty('required')!;
}

/** Gets the value of the input. */
async getValue(): Promise<string> {
// The "value" property of the native input is never undefined.
return (await (await this.host()).getProperty('value'))!;
}

/** Gets the placeholder of the input. */
async getPlaceholder(): Promise<string> {
return (await (await this.host()).getProperty('placeholder'));
}

/**
* Focuses the input and returns a promise that indicates when the
* action is complete.
*/
async focus(): Promise<void> {
return (await this.host()).focus();
}

/**
* Blurs the input and returns a promise that indicates when the
* action is complete.
*/
async blur(): Promise<void> {
return (await this.host()).blur();
}

/** Whether the input is focused. */
async isFocused(): Promise<boolean> {
return (await this.host()).isFocused();
}

/**
* Sets the value of the input. The value will be set by simulating
* keypresses that correspond to the given value.
*/
async setValue(newValue: string): Promise<void> {
const inputEl = await this.host();
await inputEl.clear();

// We don't want to send keys for the value if the value is an empty
// string in order to clear the value. Sending keys with an empty string
// still results in unnecessary focus events.
if (newValue) {
await inputEl.sendKeys(newValue);
}
}

/** Sends a chip separator key to the input element. */
async sendSeparatorKey(key: TestKey | string): Promise<void> {
const inputEl = await this.host();
return inputEl.sendKeys(key);
}
}
11 changes: 11 additions & 0 deletions src/material/chips/testing/chip-list-harness.spec.ts
@@ -0,0 +1,11 @@
import {MatChipsModule} from '@angular/material/chips';
import {runHarnessTests} from '@angular/material/chips/testing/shared.spec';
import {MatChipListHarness} from './chip-list-harness';
import {MatChipHarness} from './chip-harness';
import {MatChipInputHarness} from './chip-input-harness';
import {MatChipRemoveHarness} from './chip-remove-harness';

describe('Non-MDC-based MatChipListHarness', () => {
runHarnessTests(MatChipsModule, MatChipListHarness, MatChipHarness, MatChipInputHarness,
MatChipRemoveHarness);
});
95 changes: 95 additions & 0 deletions src/material/chips/testing/chip-list-harness.ts
@@ -0,0 +1,95 @@
/**
* @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 {MatChipHarness} from './chip-harness';
import {MatChipInputHarness} from './chip-input-harness';
import {
ChipListHarnessFilters,
ChipHarnessFilters,
ChipInputHarnessFilters,
} from './chip-harness-filters';

/** Harness for interacting with a standard chip list in tests. */
export class MatChipListHarness extends ComponentHarness {
/** The selector for the host element of a `MatChipList` instance. */
static hostSelector = '.mat-chip-list';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatChipListHarness` that meets
* certain criteria.
* @param options Options for filtering which chip list instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ChipListHarnessFilters = {}): HarnessPredicate<MatChipListHarness> {
return new HarnessPredicate(MatChipListHarness, options);
}

/** Gets whether the chip list is disabled. */
async isDisabled(): Promise<boolean> {
return await (await this.host()).getAttribute('aria-disabled') === 'true';
}

/** Gets whether the chip list is required. */
async isRequired(): Promise<boolean> {
return await (await this.host()).getAttribute('aria-required') === 'true';
}

/** Gets whether the chip list is invalid. */
async isInvalid(): Promise<boolean> {
return await (await this.host()).getAttribute('aria-invalid') === 'true';
}

/** Gets whether the chip list is in multi selection mode. */
async isMultiple(): Promise<boolean> {
return await (await this.host()).getAttribute('aria-multiselectable') === 'true';
}

/** Gets whether the orientation of the chip list. */
async getOrientation(): Promise<'horizontal' | 'vertical'> {
const orientation = await (await this.host()).getAttribute('aria-orientation');
return orientation === 'vertical' ? 'vertical' : 'horizontal';
}

/**
* Gets the list of chips inside the chip list.
* @param filter Optionally filters which chips are included.
*/
async getChips(filter: ChipHarnessFilters = {}): Promise<MatChipHarness[]> {
return this.locatorForAll(MatChipHarness.with(filter))();
}

/**
* Selects a chip inside the chip list.
* @param filter An optional filter to apply to the child chips.
* All the chips matching the filter will be selected.
*/
async selectChips(filter: ChipHarnessFilters = {}): Promise<void> {
const chips = await this.getChips(filter);
if (!chips.length) {
throw Error(`Cannot find mat-chip matching filter ${JSON.stringify(filter)}`);
}
await Promise.all(chips.map(chip => chip.select()));
}

/**
* Gets the `MatChipInput` inside the chip list.
* @param filter Optionally filters which chip input is included.
*/
async getInput(filter: ChipInputHarnessFilters = {}): Promise<MatChipInputHarness> {
// The input isn't required to be a descendant of the chip list so we have to look it up by id.
const inputId = await (await this.host()).getAttribute('data-mat-chip-input');

if (!inputId) {
throw Error(`Chip list is not associated with an input`);
}

return this.documentRootLocatorFactory().locatorFor(
MatChipInputHarness.with({...filter, selector: `#${inputId}`}))();
}
}

0 comments on commit 165622e

Please sign in to comment.