-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(material-experimental): add test harness for input #16674
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
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/cdk/coercion", | ||
], | ||
) | ||
|
||
ng_test_library( | ||
name = "harness_tests", | ||
srcs = glob(["**/*.spec.ts"]), | ||
deps = [ | ||
":harness", | ||
"//src/cdk-experimental/testing", | ||
"//src/cdk-experimental/testing/testbed", | ||
"//src/material/input", | ||
"@npm//@angular/forms", | ||
"@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,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 type InputHarnessFilters = { | ||
id?: string; | ||
name?: string; | ||
value?: string; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import {HarnessLoader} from '@angular/cdk-experimental/testing'; | ||
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; | ||
import {Component} from '@angular/core'; | ||
import {ComponentFixture, TestBed} from '@angular/core/testing'; | ||
import {ReactiveFormsModule} from '@angular/forms'; | ||
import {MatInputModule} from '@angular/material/input'; | ||
import {NoopAnimationsModule} from '@angular/platform-browser/animations'; | ||
|
||
import {MatInputHarness} from './input-harness'; | ||
|
||
let fixture: ComponentFixture<InputHarnessTest>; | ||
let loader: HarnessLoader; | ||
let inputHarness: typeof MatInputHarness; | ||
|
||
describe('MatInputHarness', () => { | ||
describe('non-MDC-based', () => { | ||
beforeEach(async () => { | ||
await TestBed | ||
.configureTestingModule({ | ||
imports: [NoopAnimationsModule, MatInputModule, ReactiveFormsModule], | ||
declarations: [InputHarnessTest], | ||
}) | ||
.compileComponents(); | ||
|
||
fixture = TestBed.createComponent(InputHarnessTest); | ||
fixture.detectChanges(); | ||
loader = TestbedHarnessEnvironment.loader(fixture); | ||
inputHarness = MatInputHarness; | ||
}); | ||
|
||
runTests(); | ||
}); | ||
|
||
describe( | ||
'MDC-based', | ||
() => { | ||
// TODO: run tests for MDC based input once implemented. | ||
}); | ||
}); | ||
|
||
/** Shared tests to run on both the original and MDC-based input's. */ | ||
function runTests() { | ||
it('should load all input harnesses', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
}); | ||
|
||
it('should load input with specific id', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness.with({id: 'myTextarea'})); | ||
expect(inputs.length).toBe(1); | ||
}); | ||
|
||
it('should load input with specific name', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness.with({name: 'favorite-food'})); | ||
expect(inputs.length).toBe(1); | ||
}); | ||
|
||
it('should load input with specific value', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness.with({value: 'Sushi'})); | ||
expect(inputs.length).toBe(1); | ||
}); | ||
|
||
it('should be able to get id of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/); | ||
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/); | ||
expect(await inputs[2].getId()).toBe('myTextarea'); | ||
}); | ||
|
||
it('should be able to get name of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getName()).toBe('favorite-food'); | ||
expect(await inputs[1].getName()).toBe(''); | ||
expect(await inputs[2].getName()).toBe(''); | ||
}); | ||
|
||
it('should be able to get value of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getValue()).toBe('Sushi'); | ||
expect(await inputs[1].getValue()).toBe(''); | ||
expect(await inputs[2].getValue()).toBe(''); | ||
}); | ||
|
||
it('should be able to set value of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getValue()).toBe('Sushi'); | ||
expect(await inputs[1].getValue()).toBe(''); | ||
|
||
await inputs[0].setValue(''); | ||
await inputs[2].setValue('new-value'); | ||
|
||
expect(await inputs[0].getValue()).toBe(''); | ||
expect(await inputs[2].getValue()).toBe('new-value'); | ||
}); | ||
|
||
it('should be able to get disabled state', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
|
||
expect(await inputs[0].isDisabled()).toBe(false); | ||
expect(await inputs[1].isDisabled()).toBe(false); | ||
expect(await inputs[2].isDisabled()).toBe(false); | ||
|
||
fixture.componentInstance.disabled = true; | ||
|
||
expect(await inputs[1].isDisabled()).toBe(true); | ||
}); | ||
|
||
it('should be able to get readonly state', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
|
||
expect(await inputs[0].isReadonly()).toBe(false); | ||
expect(await inputs[1].isReadonly()).toBe(false); | ||
expect(await inputs[2].isReadonly()).toBe(false); | ||
|
||
fixture.componentInstance.readonly = true; | ||
|
||
expect(await inputs[1].isReadonly()).toBe(true); | ||
}); | ||
|
||
it('should be able to get required state', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
|
||
expect(await inputs[0].isRequired()).toBe(false); | ||
expect(await inputs[1].isRequired()).toBe(false); | ||
expect(await inputs[2].isRequired()).toBe(false); | ||
|
||
fixture.componentInstance.required = true; | ||
|
||
expect(await inputs[1].isRequired()).toBe(true); | ||
}); | ||
|
||
it('should be able to get placeholder of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getPlaceholder()).toBe('Favorite food'); | ||
expect(await inputs[1].getPlaceholder()).toBe(''); | ||
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment'); | ||
}); | ||
|
||
it('should be able to get type of input', async () => { | ||
const inputs = await loader.getAllHarnesses(inputHarness); | ||
expect(inputs.length).toBe(3); | ||
expect(await inputs[0].getType()).toBe('text'); | ||
expect(await inputs[1].getType()).toBe('number'); | ||
expect(await inputs[2].getType()).toBe('textarea'); | ||
|
||
fixture.componentInstance.inputType = 'text'; | ||
|
||
expect(await inputs[1].getType()).toBe('text'); | ||
}); | ||
|
||
it('should be able to focus input', async () => { | ||
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'})); | ||
expect(getActiveElementTagName()).not.toBe('input'); | ||
await input.focus(); | ||
expect(getActiveElementTagName()).toBe('input'); | ||
}); | ||
|
||
it('should be able to blur input', async () => { | ||
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'})); | ||
expect(getActiveElementTagName()).not.toBe('input'); | ||
await input.focus(); | ||
expect(getActiveElementTagName()).toBe('input'); | ||
await input.blur(); | ||
expect(getActiveElementTagName()).not.toBe('input'); | ||
}); | ||
} | ||
|
||
function getActiveElementTagName() { | ||
return document.activeElement ? document.activeElement.tagName.toLowerCase() : ''; | ||
} | ||
|
||
@Component({ | ||
template: ` | ||
<mat-form-field> | ||
<input matInput placeholder="Favorite food" value="Sushi" name="favorite-food"> | ||
</mat-form-field> | ||
|
||
<mat-form-field> | ||
<input matInput [type]="inputType" | ||
[readonly]="readonly" | ||
[disabled]="disabled" | ||
[required]="required"> | ||
</mat-form-field> | ||
|
||
<mat-form-field> | ||
<textarea id="myTextarea" matInput placeholder="Leave a comment"></textarea> | ||
</mat-form-field> | ||
` | ||
}) | ||
class InputHarnessTest { | ||
inputType = 'number'; | ||
readonly = false; | ||
disabled = false; | ||
required = false; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** | ||
* @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-experimental/testing'; | ||
import {InputHarnessFilters} from './input-harness-filters'; | ||
|
||
/** | ||
* Harness for interacting with a standard Material inputs in tests. | ||
* @dynamic | ||
*/ | ||
export class MatInputHarness extends ComponentHarness { | ||
static hostSelector = '[matInput]'; | ||
|
||
/** | ||
* Gets a `HarnessPredicate` that can be used to search for an input with | ||
* specific attributes. | ||
* @param options Options for narrowing the search: | ||
* - `name` finds an input with specific name. | ||
* - `id` finds an input with specific id. | ||
* - `value` finds an input with specific value. | ||
* @return a `HarnessPredicate` configured with the given options. | ||
*/ | ||
static with(options: InputHarnessFilters = {}): HarnessPredicate<MatInputHarness> { | ||
// TODO(devversion): "name" and "id" can be removed once components#16848 is merged. | ||
return new HarnessPredicate(MatInputHarness) | ||
.addOption( | ||
'name', options.name, async (harness, name) => (await harness.getName()) === name) | ||
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id) | ||
.addOption( | ||
'value', options.value, async (harness, value) => (await harness.getValue()) === value); | ||
} | ||
|
||
/** 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')!; | ||
} | ||
|
||
/** Whether the input is readonly. */ | ||
async isReadonly(): Promise<boolean> { | ||
return (await this.host()).getProperty('readOnly')!; | ||
} | ||
|
||
/** 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 name of the input. */ | ||
async getName(): Promise<string> { | ||
// The "name" property of the native input is never undefined. | ||
return (await (await this.host()).getProperty('name'))!; | ||
} | ||
|
||
/** | ||
* Gets the type of the input. Returns "textarea" if the input is | ||
* a textarea. | ||
*/ | ||
async getType(): Promise<string> { | ||
// The "type" property of the native input is never undefined. | ||
return (await (await this.host()).getProperty('type'))!; | ||
} | ||
|
||
/** Gets the placeholder of the input. / */ | ||
async getPlaceholder(): Promise<string> { | ||
// The "placeholder" property of the native input is never undefined. | ||
return (await (await this.host()).getProperty('placeholder'))!; | ||
} | ||
|
||
/** Gets the id of the input. */ | ||
async getId(): Promise<string> { | ||
// The input directive always assigns a unique id to the input in | ||
// case no id has been explicitly specified. | ||
return (await (await this.host()).getProperty('id'))!; | ||
} | ||
|
||
/** | ||
* 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(); | ||
} | ||
|
||
/** | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. I could see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, calling it with empty string is weird and people shouldn't do it, so I'm fine with changing it or just leaving it how it is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd vote for leaving it as is, but maybe throwing an error in |
||
// 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); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.