-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(chips): add test harness #20028
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"], | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> { | ||
mmalerba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const inputEl = await this.host(); | ||
return inputEl.sendKeys(key); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}`}))(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mdc versions of chips has types of chips that don't have a
selected
state- this would affect the filter as well as the chip harness API. Would it make sense to author the harness for the existing chip-list against the concepts of the MDC-based chips so that we're going in the direction we want? (e.gMatChipListbox
,MatChipOption
, etc.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't those just always return
false
? I think that no matter what, we'll have some differences in the harnesses between the current ones and MDC, because the public API is different.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC the MDC-based
mat-chip-listbox
should be equivalent to the currentmat-chip-list
, so maybe the end result should look something likeMatChipHarness
- any kind of chip, no selection APIMatChipSetHarness
- any kind of chip container, no selection APIMatChipOptionHarness
- specifically a listbox (option) chip, extendsMatChipHarness
, selection APIMatChipListboxHarness
- specifically listbox chip container, extendsMatChipSetHarness
, selection APIMatChipListHarness
becomes an alias forMatChipListboxHarness
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO that makes sense for the MDC chips, but not for the existing ones. Somebody that hasn't used the MDC chips won't know what a "chip listbox" is referring to. I don't think it's worth it to avoid breaking changes in the harnesses, considering that the component's public API has breaking changes already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think since one of the main goals for the harnesses is to help with the MDC migration, we should align the API with the current version of chips for now. Once the migration is done we may want to change it to align better with the new API of the MDC version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this all work with the current version of chips though? Would there would be 2 different harnesses for
MatChip
(MatChipHarness
,MatChipOptionHarness
) and users just pick the ones that gives the functionality they want in their test?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth noting that
selectable
is an input so a chip could change type between change detection runs. That means that people might have to use two harnesses for the same element.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could just use
MatOptionHarness
exclusively and it would reportfalse
if it wasn't selectable, so I don't think you'd need to use both for a single element (unlessMatChipHarness
does something thatMatChipOptionHarness
doesn't)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the point I was making above about always using
MatChipOption
. Anyway, I don't think that we can get away with using the exact same set of harnesses for both MDC and the current version, because they're fundamentally different APIs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose I had just been imagining that we could just completely omit the selection APIs from
MatChipHarness
for now, and only add them to the mdc ones.I admit it's not ideal. Thinking about it more, I can live with going with this API for now and just committing to writing migration schematics for chips specifically because the APIs should be pretty easy to transform.