From 165622ee11163acf1a4ca60251039919abb78b01 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 28 Jul 2020 16:21:35 +0200 Subject: [PATCH] feat(chips): add test harness (#20028) Adds test harnesses for the Material chips and the related components. --- .github/CODEOWNERS | 1 + src/material/chips/chip-list.ts | 4 + src/material/chips/testing/BUILD.bazel | 52 +++ .../chips/testing/chip-harness-filters.ts | 30 ++ src/material/chips/testing/chip-harness.ts | 78 ++++ .../chips/testing/chip-input-harness.ts | 95 +++++ .../chips/testing/chip-list-harness.spec.ts | 11 + .../chips/testing/chip-list-harness.ts | 95 +++++ .../chips/testing/chip-remove-harness.ts | 30 ++ src/material/chips/testing/index.ts | 9 + src/material/chips/testing/public-api.ts | 13 + src/material/chips/testing/shared.spec.ts | 347 ++++++++++++++++++ src/material/config.bzl | 1 + .../material/chips/testing.d.ts | 61 +++ 14 files changed, 827 insertions(+) create mode 100644 src/material/chips/testing/BUILD.bazel create mode 100644 src/material/chips/testing/chip-harness-filters.ts create mode 100644 src/material/chips/testing/chip-harness.ts create mode 100644 src/material/chips/testing/chip-input-harness.ts create mode 100644 src/material/chips/testing/chip-list-harness.spec.ts create mode 100644 src/material/chips/testing/chip-list-harness.ts create mode 100644 src/material/chips/testing/chip-remove-harness.ts create mode 100644 src/material/chips/testing/index.ts create mode 100644 src/material/chips/testing/public-api.ts create mode 100644 src/material/chips/testing/shared.spec.ts create mode 100644 tools/public_api_guard/material/chips/testing.d.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b7715893236..b34c7e4543df 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/src/material/chips/chip-list.ts b/src/material/chips/chip-list.ts index 29107f44a2d8..ff4f47bc613b 100644 --- a/src/material/chips/chip-list.ts +++ b/src/material/chips/chip-list.ts @@ -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); } /** diff --git a/src/material/chips/testing/BUILD.bazel b/src/material/chips/testing/BUILD.bazel new file mode 100644 index 000000000000..ed632cbabb75 --- /dev/null +++ b/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"], +) diff --git a/src/material/chips/testing/chip-harness-filters.ts b/src/material/chips/testing/chip-harness-filters.ts new file mode 100644 index 000000000000..d1a53b55c8b3 --- /dev/null +++ b/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 {} diff --git a/src/material/chips/testing/chip-harness.ts b/src/material/chips/testing/chip-harness.ts new file mode 100644 index 000000000000..2ee85fe94e87 --- /dev/null +++ b/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 { + 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 { + return (await this.host()).text(); + } + + /** Whether the chip is selected. */ + async isSelected(): Promise { + return (await this.host()).hasClass('mat-chip-selected'); + } + + /** Whether the chip is disabled. */ + async isDisabled(): Promise { + return (await this.host()).hasClass('mat-chip-disabled'); + } + + /** Selects the given chip. Only applies if it's selectable. */ + async select(): Promise { + if (!(await this.isSelected())) { + await this.toggle(); + } + } + + /** Deselects the given chip. Only applies if it's selectable. */ + async deselect(): Promise { + if (await this.isSelected()) { + await this.toggle(); + } + } + + /** Toggles the selected state of the given chip. Only applies if it's selectable. */ + async toggle(): Promise { + return (await this.host()).sendKeys(' '); + } + + /** Removes the given chip. Only applies if it's removable. */ + async remove(): Promise { + 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 { + return this.locatorFor(MatChipRemoveHarness.with(filter))(); + } +} diff --git a/src/material/chips/testing/chip-input-harness.ts b/src/material/chips/testing/chip-input-harness.ts new file mode 100644 index 000000000000..f6f10404ae6a --- /dev/null +++ b/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 { + 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 { + return (await this.host()).getProperty('disabled')!; + } + + /** Whether the input is required. */ + async isRequired(): Promise { + return (await this.host()).getProperty('required')!; + } + + /** Gets the value of the input. */ + async getValue(): Promise { + // 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 { + return (await (await this.host()).getProperty('placeholder')); + } + + /** + * Focuses the input and returns a promise that indicates when the + * action is complete. + */ + async focus(): Promise { + return (await this.host()).focus(); + } + + /** + * Blurs the input and returns a promise that indicates when the + * action is complete. + */ + async blur(): Promise { + return (await this.host()).blur(); + } + + /** Whether the input is focused. */ + async isFocused(): Promise { + 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 { + 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 { + const inputEl = await this.host(); + return inputEl.sendKeys(key); + } +} diff --git a/src/material/chips/testing/chip-list-harness.spec.ts b/src/material/chips/testing/chip-list-harness.spec.ts new file mode 100644 index 000000000000..3a422b89e044 --- /dev/null +++ b/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); +}); diff --git a/src/material/chips/testing/chip-list-harness.ts b/src/material/chips/testing/chip-list-harness.ts new file mode 100644 index 000000000000..84d47e3b179d --- /dev/null +++ b/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 { + return new HarnessPredicate(MatChipListHarness, options); + } + + /** Gets whether the chip list is disabled. */ + async isDisabled(): Promise { + return await (await this.host()).getAttribute('aria-disabled') === 'true'; + } + + /** Gets whether the chip list is required. */ + async isRequired(): Promise { + return await (await this.host()).getAttribute('aria-required') === 'true'; + } + + /** Gets whether the chip list is invalid. */ + async isInvalid(): Promise { + return await (await this.host()).getAttribute('aria-invalid') === 'true'; + } + + /** Gets whether the chip list is in multi selection mode. */ + async isMultiple(): Promise { + 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 { + 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 { + 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 { + // 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}`}))(); + } +} diff --git a/src/material/chips/testing/chip-remove-harness.ts b/src/material/chips/testing/chip-remove-harness.ts new file mode 100644 index 000000000000..7a1ab89b71f5 --- /dev/null +++ b/src/material/chips/testing/chip-remove-harness.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 {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing'; +import {ChipRemoveHarnessFilters} from './chip-harness-filters'; + +/** Harness for interacting with a standard Material chip remove button in tests. */ +export class MatChipRemoveHarness extends ComponentHarness { + static hostSelector = '.mat-chip-remove'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatChipRemoveHarness` 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: ChipRemoveHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatChipRemoveHarness, options); + } + + /** Clicks the remove button. */ + async click(): Promise { + return (await this.host()).click(); + } +} diff --git a/src/material/chips/testing/index.ts b/src/material/chips/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material/chips/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/chips/testing/public-api.ts b/src/material/chips/testing/public-api.ts new file mode 100644 index 000000000000..c44839ab784f --- /dev/null +++ b/src/material/chips/testing/public-api.ts @@ -0,0 +1,13 @@ +/** + * @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 './chip-harness'; +export * from './chip-harness-filters'; +export * from './chip-list-harness'; +export * from './chip-input-harness'; +export * from './chip-remove-harness'; diff --git a/src/material/chips/testing/shared.spec.ts b/src/material/chips/testing/shared.spec.ts new file mode 100644 index 000000000000..22291729d397 --- /dev/null +++ b/src/material/chips/testing/shared.spec.ts @@ -0,0 +1,347 @@ +import {Component} from '@angular/core'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {HarnessLoader, TestKey} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatChipsModule} from '@angular/material/chips'; +import {MatChipListHarness} from './chip-list-harness'; +import {MatChipHarness} from './chip-harness'; +import {MatChipInputHarness} from './chip-input-harness'; +import {MatChipRemoveHarness} from './chip-remove-harness'; + +/** Shared tests to run on both the original and MDC-based chips. */ +export function runHarnessTests( + chipsModule: typeof MatChipsModule, + chipListHarness: typeof MatChipListHarness, + chipHarness: typeof MatChipHarness, + chipInputHarness: typeof MatChipInputHarness, + chipRemoveHarness: typeof MatChipRemoveHarness) { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [chipsModule, MatFormFieldModule, NoopAnimationsModule], + declarations: [ChipsHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(ChipsHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should load the harnesses for a chip list', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + expect(chipLists.length).toBe(3); + }); + + it('should get whether a chip list is disabled', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + + expect(await Promise.all(chipLists.map(list => list.isDisabled()))).toEqual([ + false, + false, + false + ]); + + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + + expect(await Promise.all(chipLists.map(list => list.isDisabled()))).toEqual([ + true, + false, + false + ]); + }); + + it('should get whether a chip list is required', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + + expect(await Promise.all(chipLists.map(list => list.isRequired()))).toEqual([ + false, + false, + true + ]); + + fixture.componentInstance.isRequired = true; + fixture.detectChanges(); + + expect(await Promise.all(chipLists.map(list => list.isRequired()))).toEqual([ + true, + false, + true + ]); + }); + + it('should get whether a chip list is in multi selection mode', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + + expect(await Promise.all(chipLists.map(list => list.isMultiple()))).toEqual([ + false, + false, + false + ]); + + fixture.componentInstance.isMultiple = true; + fixture.detectChanges(); + + expect(await Promise.all(chipLists.map(list => list.isMultiple()))).toEqual([ + true, + false, + false + ]); + }); + + it('should get the orientation of a chip list', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + + expect(await Promise.all(chipLists.map(list => list.getOrientation()))).toEqual([ + 'horizontal', + 'horizontal', + 'horizontal' + ]); + + fixture.componentInstance.orientation = 'vertical'; + fixture.detectChanges(); + + expect(await Promise.all(chipLists.map(list => list.getOrientation()))).toEqual([ + 'vertical', + 'horizontal', + 'horizontal' + ]); + }); + + it('should get the chips in a chip list', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + const chips = await Promise.all(chipLists.map(list => list.getChips())); + expect(chips.map(list => list.length)).toEqual([4, 0, 2]); + }); + + it('should be able to get the input associated with a chip list', async () => { + const chipLists = await loader.getAllHarnesses(chipListHarness); + expect((await chipLists[2].getInput())).toBeTruthy(); + }); + + it('should be able to get the selected chips in a list', async () => { + const chipList = await loader.getHarness(chipListHarness); + const chips = await chipList.getChips(); + + expect((await chipList.getChips({selected: true})).length).toBe(0); + await chips[1].select(); + + const selectedChips = await chipList.getChips({selected: true}); + expect(await Promise.all(selectedChips.map(chip => chip.getText()))).toEqual(['Chip 2']); + }); + + it('should be able to select chips based on a filter', async () => { + const chipList = await loader.getHarness(chipListHarness); + fixture.componentInstance.isMultiple = true; + + expect((await chipList.getChips({selected: true})).length).toBe(0); + await chipList.selectChips({text: /^Chip (2|4)$/}); + + const selectedChips = await chipList.getChips({selected: true}); + expect(await Promise.all(selectedChips.map(chip => chip.getText()))).toEqual([ + 'Chip 2', + 'Chip 4' + ]); + }); + + it('should the load the harnesses for chips', async () => { + const chips = await loader.getAllHarnesses(chipHarness); + expect(chips.length).toBe(6); + }); + + it('should get the text of a chip', async () => { + const chips = await loader.getAllHarnesses(chipHarness); + expect(await Promise.all(chips.map(chip => chip.getText()))).toEqual([ + 'Chip 1', + 'Chip 2', + 'Chip 3', + 'Chip 4', + 'Frodo', + 'Bilbo' + ]); + }); + + it('should be able to select a chip', async () => { + const chip = await loader.getHarness(chipHarness); + expect(await chip.isSelected()).toBe(false); + await chip.select(); + expect(await chip.isSelected()).toBe(true); + }); + + it('should be able to deselect a chip', async () => { + const chip = await loader.getHarness(chipHarness); + await chip.select(); + expect(await chip.isSelected()).toBe(true); + await chip.deselect(); + expect(await chip.isSelected()).toBe(false); + }); + + it('should be able to toggle the selected state of a chip', async () => { + const chip = await loader.getHarness(chipHarness); + expect(await chip.isSelected()).toBe(false); + await chip.toggle(); + expect(await chip.isSelected()).toBe(true); + await chip.toggle(); + expect(await chip.isSelected()).toBe(false); + }); + + it('should get the disabled text of a chip', async () => { + const chips = await loader.getAllHarnesses(chipHarness); + expect(await Promise.all(chips.map(chip => chip.isDisabled()))).toEqual([ + false, + false, + true, + false, + false, + false + ]); + }); + + it('should get the selected text of a chip', async () => { + const chips = await loader.getAllHarnesses(chipHarness); + expect(await Promise.all(chips.map(chip => chip.isSelected()))).toEqual([ + false, + false, + false, + false, + false, + false + ]); + + await chips[1].select(); + + expect(await Promise.all(chips.map(chip => chip.isSelected()))).toEqual([ + false, + true, + false, + false, + false, + false + ]); + }); + + it('should be able to trigger chip removal', async () => { + const chip = await loader.getHarness(chipHarness); + expect(fixture.componentInstance.remove).not.toHaveBeenCalled(); + await chip.remove(); + expect(fixture.componentInstance.remove).toHaveBeenCalled(); + }); + + it('should the load the harnesses for chip inputs', async () => { + const inputs = await loader.getAllHarnesses(chipInputHarness); + expect(inputs.length).toBe(1); + }); + + it('should get whether the input input is disabled', async () => { + const input = await loader.getHarness(chipInputHarness); + expect(await input.isDisabled()).toBe(false); + fixture.componentInstance.inputDisabled = true; + expect(await input.isDisabled()).toBe(true); + }); + + it('should get whether the input input is required', async () => { + const input = await loader.getHarness(chipInputHarness); + expect(await input.isRequired()).toBe(false); + fixture.componentInstance.inputRequired = true; + expect(await input.isRequired()).toBe(true); + }); + + it('should focus a chip input', async () => { + const input = await loader.getHarness(chipInputHarness); + expect(await input.isFocused()).toBe(false); + await input.focus(); + expect(await input.isFocused()).toBe(true); + }); + + it('should blur a chip input', async () => { + const input = await loader.getHarness(chipInputHarness); + await input.focus(); + expect(await input.isFocused()).toBe(true); + await input.blur(); + expect(await input.isFocused()).toBe(false); + }); + + it('should get the chip input placeholder', async () => { + const input = await loader.getHarness(chipInputHarness); + expect(await input.getPlaceholder()).toBe('Enter a hobbit'); + }); + + it('should be able to set the chip input value', async () => { + const input = await loader.getHarness(chipInputHarness); + expect(await input.getValue()).toBe(''); + await input.setValue('Hello'); + expect(await input.getValue()).toBe('Hello'); + }); + + it('should be able to dispatch a separator key from the input harness', async () => { + const input = await loader.getHarness(chipInputHarness); + await input.setValue('Hello'); + await input.sendSeparatorKey(TestKey.ENTER); + expect(fixture.componentInstance.add).toHaveBeenCalled(); + }); + + it('should get the chip remove button', async () => { + const formFieldChip = await loader.getHarness(chipHarness.with({ancestor: 'mat-form-field'})); + expect(await formFieldChip.getRemoveButton()).toBeTruthy(); + }); + + it('should be able to trigger removal through the remove button', async () => { + const removeButton = await loader.getHarness(chipRemoveHarness); + expect(fixture.componentInstance.remove).not.toHaveBeenCalled(); + await removeButton.click(); + expect(fixture.componentInstance.remove).toHaveBeenCalled(); + }); + +} + +@Component({ + template: ` + + Chip 1 + Chip 2 + Chip 3 + Chip 4 + + + + + + + + Frodo + + + + + Bilbo + + + + + + + ` +}) +class ChipsHarnessTest { + isDisabled = false; + isRequired = false; + isMultiple = false; + orientation = 'horizontal'; + inputDisabled = false; + inputRequired = false; + remove = jasmine.createSpy('remove spy'); + add = jasmine.createSpy('add spy'); +} diff --git a/src/material/config.bzl b/src/material/config.bzl index 053175d3e391..2d935e0a73a4 100644 --- a/src/material/config.bzl +++ b/src/material/config.bzl @@ -14,6 +14,7 @@ entryPoints = [ "checkbox", "checkbox/testing", "chips", + "chips/testing", "core", "core/testing", "datepicker", diff --git a/tools/public_api_guard/material/chips/testing.d.ts b/tools/public_api_guard/material/chips/testing.d.ts new file mode 100644 index 000000000000..a504508a8ed0 --- /dev/null +++ b/tools/public_api_guard/material/chips/testing.d.ts @@ -0,0 +1,61 @@ +export interface ChipHarnessFilters extends BaseHarnessFilters { + selected?: boolean; + text?: string | RegExp; +} + +export interface ChipInputHarnessFilters extends BaseHarnessFilters { + placeholder?: string | RegExp; + value?: string | RegExp; +} + +export interface ChipListHarnessFilters extends BaseHarnessFilters { +} + +export interface ChipRemoveHarnessFilters extends BaseHarnessFilters { +} + +export declare class MatChipHarness extends ComponentHarness { + deselect(): Promise; + getRemoveButton(filter?: ChipRemoveHarnessFilters): Promise; + getText(): Promise; + isDisabled(): Promise; + isSelected(): Promise; + remove(): Promise; + select(): Promise; + toggle(): Promise; + static hostSelector: string; + static with(options?: ChipHarnessFilters): HarnessPredicate; +} + +export declare class MatChipInputHarness extends ComponentHarness { + blur(): Promise; + focus(): Promise; + getPlaceholder(): Promise; + getValue(): Promise; + isDisabled(): Promise; + isFocused(): Promise; + isRequired(): Promise; + sendSeparatorKey(key: TestKey | string): Promise; + setValue(newValue: string): Promise; + static hostSelector: string; + static with(options?: ChipInputHarnessFilters): HarnessPredicate; +} + +export declare class MatChipListHarness extends ComponentHarness { + getChips(filter?: ChipHarnessFilters): Promise; + getInput(filter?: ChipInputHarnessFilters): Promise; + getOrientation(): Promise<'horizontal' | 'vertical'>; + isDisabled(): Promise; + isInvalid(): Promise; + isMultiple(): Promise; + isRequired(): Promise; + selectChips(filter?: ChipHarnessFilters): Promise; + static hostSelector: string; + static with(options?: ChipListHarnessFilters): HarnessPredicate; +} + +export declare class MatChipRemoveHarness extends ComponentHarness { + click(): Promise; + static hostSelector: string; + static with(options?: ChipRemoveHarnessFilters): HarnessPredicate; +}