Skip to content

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

Merged
merged 1 commit into from
Aug 23, 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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
/src/material-experimental/mdc-checkbox/** @mmalerba
/src/material-experimental/mdc-chips/** @mmalerba
/src/material-experimental/mdc-helpers/** @mmalerba
# Note to implementer: please repossess
/src/material-experimental/mdc-input/** @devversion
/src/material-experimental/mdc-menu/** @crisbeto
/src/material-experimental/mdc-select/** @crisbeto
/src/material-experimental/mdc-progress-spinner/** @andrewseguin
Expand Down
33 changes: 33 additions & 0 deletions src/material-experimental/mdc-input/harness/BUILD.bazel
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;
};
203 changes: 203 additions & 0 deletions src/material-experimental/mdc-input/harness/input-harness.spec.ts
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;
}
117 changes: 117 additions & 0 deletions src/material-experimental/mdc-input/harness/input-harness.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

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

should we update sendKeys to not do that?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure. I could see sendKeys being a noop in that case. Though it also feels like an anti-pattern in general to call sendKeys with an empty value.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 sendKeys instead? (follow-up)?

// 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);
}
}
}