Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 41365e3

Browse files
feat: add type function (testing-library#37)
1 parent b65505e commit 41365e3

File tree

11 files changed

+396
-3
lines changed

11 files changed

+396
-3
lines changed

‎projects/testing-library/src/lib/models.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Type } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
33
import { FireObject, Queries, queries, BoundFunction } from '@testing-library/dom';
4+
import { UserEvents } from './user-events';
45

56
export type RenderResultQueries<Q extends Queries = typeof queries> = { [P in keyof Q]: BoundFunction<Q[P]> };
67

7-
export interface RenderResult extends RenderResultQueries, FireObject {
8+
export interface RenderResult extends RenderResultQueries, FireObject,UserEvents {
89
container: HTMLElement;
910
debug: (element?: HTMLElement) => void;
1011
fixture: ComponentFixture<any>;

‎projects/testing-library/src/lib/testing-library.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { By } from '@angular/platform-browser';
33
import { TestBed, ComponentFixture } from '@angular/core/testing';
44
import { getQueriesForElement, prettyDOM, fireEvent, FireObject, FireFunction } from '@testing-library/dom';
55
import { RenderResult, RenderOptions } from './models';
6+
import { createType } from './user-events';
67

78
@Component({ selector: 'wrapper-component', template: '' })
89
class WrapperComponent implements OnInit {
@@ -84,6 +85,7 @@ export async function render<T>(
8485
debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)),
8586
...getQueriesForElement(fixture.nativeElement, queries),
8687
...eventsWithDetectChanges,
88+
type: createType(eventsWithDetectChanges),
8789
} as any;
8890
}
8991

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { fireEvent } from '@testing-library/dom';
2+
import { createType } from './type';
3+
4+
export interface UserEvents {
5+
type: ReturnType<typeof createType>;
6+
}
7+
8+
const type = createType(fireEvent);
9+
10+
export { createType, type };
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { FireFunction, FireObject } from '@testing-library/dom';
2+
3+
function wait(time) {
4+
return new Promise(function(resolve) {
5+
setTimeout(() => resolve(), time);
6+
});
7+
}
8+
9+
// implementation from https://github.com/testing-library/user-event
10+
export function createType(fireEvent: FireFunction & FireObject) {
11+
function createFireChangeEvent(value: string) {
12+
return function fireChangeEvent(event) {
13+
if (value !== event.target.value) {
14+
fireEvent.change(event.target);
15+
}
16+
event.target.removeEventListener('blur', fireChangeEvent);
17+
};
18+
}
19+
20+
return async function type(element: HTMLElement, value: string, { allAtOnce = false, delay = 0 } = {}) {
21+
const initialValue = (element as HTMLInputElement).value;
22+
23+
if (allAtOnce) {
24+
fireEvent.input(element, { target: { value } });
25+
element.addEventListener('blur', createFireChangeEvent(initialValue));
26+
return;
27+
}
28+
29+
let actuallyTyped = '';
30+
for (let index = 0; index < value.length; index++) {
31+
const char = value[index];
32+
const key = char;
33+
const keyCode = char.charCodeAt(0);
34+
35+
if (delay > 0) {
36+
await wait(delay);
37+
}
38+
39+
const downEvent = fireEvent.keyDown(element, {
40+
key: key,
41+
keyCode: keyCode,
42+
which: keyCode,
43+
});
44+
45+
if (downEvent) {
46+
const pressEvent = fireEvent.keyPress(element, {
47+
key: key,
48+
keyCode,
49+
charCode: keyCode,
50+
});
51+
52+
if (pressEvent) {
53+
actuallyTyped += key;
54+
fireEvent.input(element, {
55+
target: {
56+
value: actuallyTyped,
57+
},
58+
bubbles: true,
59+
cancelable: true,
60+
});
61+
}
62+
}
63+
64+
fireEvent.keyUp(element, {
65+
key: key,
66+
keyCode: keyCode,
67+
which: keyCode,
68+
});
69+
}
70+
71+
element.addEventListener('blur', createFireChangeEvent(initialValue));
72+
};
73+
}

‎projects/testing-library/src/public_api.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
export * from './lib/models';
66
export * from './lib/testing-library';
7+
export * from './lib/user-events';
78
export * from '@testing-library/dom';
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
2+
import { render, RenderResult } from '../../src/public_api';
3+
import { Component, ViewChild, Input } from '@angular/core';
4+
import { fakeAsync, tick } from '@angular/core/testing';
5+
6+
describe('updates the value', () => {
7+
test('with a template-driven form', async () => {
8+
@Component({
9+
selector: 'fixture',
10+
template: `
11+
<input type="text" [(ngModel)]="value" data-testid="input" />
12+
<p data-testid="text">{{ value }}</p>
13+
`,
14+
})
15+
class FixtureComponent {
16+
value: string;
17+
}
18+
19+
const component = await render(FixtureComponent, {
20+
imports: [FormsModule],
21+
});
22+
23+
assertType(component, () => component.fixture.componentInstance.value);
24+
});
25+
26+
test('with a reactive form', async () => {
27+
@Component({
28+
selector: 'fixture',
29+
template: `
30+
<input type="text" [formControl]="value" data-testid="input" />
31+
<p data-testid="text">{{ value.value }}</p>
32+
`,
33+
})
34+
class FixtureComponent {
35+
value = new FormControl('');
36+
}
37+
38+
const component = await render(FixtureComponent, {
39+
imports: [ReactiveFormsModule],
40+
});
41+
42+
assertType(component, () => component.fixture.componentInstance.value.value);
43+
});
44+
45+
test('with events', async () => {
46+
@Component({
47+
selector: 'fixture',
48+
template: `
49+
<input type="text" (input)="onInput($event)" data-testid="input" />
50+
<p data-testid="text">{{ value }}</p>
51+
`,
52+
})
53+
class FixtureComponent {
54+
value = '';
55+
56+
onInput(event: KeyboardEvent) {
57+
this.value = (<HTMLInputElement>event.target).value;
58+
}
59+
}
60+
61+
const component = await render(FixtureComponent);
62+
63+
assertType(component, () => component.fixture.componentInstance.value);
64+
});
65+
66+
test('by reference', async () => {
67+
@Component({
68+
selector: 'fixture',
69+
template: `
70+
<input type="text" data-testid="input" #input />
71+
<p data-testid="text">{{ input.value }}</p>
72+
`,
73+
})
74+
class FixtureComponent {
75+
@ViewChild('input', { static: false }) value;
76+
}
77+
78+
const component = await render(FixtureComponent);
79+
80+
assertType(component, () => component.fixture.componentInstance.value.nativeElement.value);
81+
});
82+
83+
function assertType(component: RenderResult, value: () => string) {
84+
const input = '@testing-library/angular';
85+
const inputControl = component.getByTestId('input') as HTMLInputElement;
86+
component.type(inputControl, input);
87+
88+
expect(value()).toBe(input);
89+
expect(component.getByTestId('text').textContent).toBe(input);
90+
expect(inputControl.value).toBe(input);
91+
expect(inputControl).toHaveProperty('value', input);
92+
}
93+
});
94+
95+
describe('options', () => {
96+
@Component({
97+
selector: 'fixture',
98+
template: `
99+
<input
100+
type="text"
101+
data-testid="input"
102+
(input)="onInput($event)"
103+
(change)="onChange($event)"
104+
(keydown)="onKeyDown($event)"
105+
(keypress)="onKeyPress($event)"
106+
(keyup)="onKeyUp($event)"
107+
/>
108+
`,
109+
})
110+
class FixtureComponent {
111+
onInput($event) {}
112+
onChange($event) {}
113+
onKeyDown($event) {}
114+
onKeyPress($event) {}
115+
onKeyUp($event) {}
116+
}
117+
118+
async function setup() {
119+
const componentProperties = {
120+
onInput: jest.fn(),
121+
onChange: jest.fn(),
122+
onKeyDown: jest.fn(),
123+
onKeyPress: jest.fn(),
124+
onKeyUp: jest.fn(),
125+
};
126+
const component = await render(FixtureComponent, { componentProperties });
127+
128+
return { component, ...componentProperties };
129+
}
130+
131+
describe('allAtOnce', () => {
132+
test('false: updates the value one char at a time', async () => {
133+
const { component, onInput, onChange, onKeyDown, onKeyPress, onKeyUp } = await setup();
134+
135+
const inputControl = component.getByTestId('input') as HTMLInputElement;
136+
const inputValue = 'foobar';
137+
component.type(inputControl, inputValue);
138+
139+
expect(onInput).toBeCalledTimes(inputValue.length);
140+
expect(onKeyDown).toBeCalledTimes(inputValue.length);
141+
expect(onKeyPress).toBeCalledTimes(inputValue.length);
142+
expect(onKeyUp).toBeCalledTimes(inputValue.length);
143+
144+
component.blur(inputControl);
145+
expect(onChange).toBeCalledTimes(1);
146+
});
147+
148+
test('true: updates the value in one time and does not trigger other events', async () => {
149+
const { component, onInput, onChange, onKeyDown, onKeyPress, onKeyUp } = await setup();
150+
151+
const inputControl = component.getByTestId('input') as HTMLInputElement;
152+
const inputValue = 'foobar';
153+
component.type(inputControl, inputValue, { allAtOnce: true });
154+
155+
expect(onInput).toBeCalledTimes(1);
156+
expect(onKeyDown).toBeCalledTimes(0);
157+
expect(onKeyPress).toBeCalledTimes(0);
158+
expect(onKeyUp).toBeCalledTimes(0);
159+
160+
component.blur(inputControl);
161+
expect(onChange).toBeCalledTimes(1);
162+
});
163+
});
164+
165+
describe('delay', () => {
166+
test('delays the input', fakeAsync(async () => {
167+
const { component } = await setup();
168+
169+
const inputControl = component.getByTestId('input') as HTMLInputElement;
170+
const inputValue = 'foobar';
171+
component.type(inputControl, inputValue, { delay: 25 });
172+
173+
[...inputValue].forEach((_, i) => {
174+
expect(inputControl.value).toBe(inputValue.substr(0, i));
175+
tick(25);
176+
});
177+
}));
178+
});
179+
});
180+
181+
test('should not type when event.preventDefault() is called', async () => {
182+
@Component({
183+
selector: 'fixture',
184+
template: `
185+
<input
186+
type="text"
187+
data-testid="input"
188+
(input)="onInput($event)"
189+
(change)="onChange($event)"
190+
(keydown)="onKeyDown($event)"
191+
(keypress)="onKeyPress($event)"
192+
(keyup)="onKeyUp($event)"
193+
/>
194+
`,
195+
})
196+
class FixtureComponent {
197+
onInput($event) {}
198+
onChange($event) {}
199+
onKeyDown($event) {}
200+
onKeyPress($event) {}
201+
onKeyUp($event) {}
202+
}
203+
204+
const componentProperties = {
205+
onChange: jest.fn(),
206+
onKeyDown: jest.fn().mockImplementation(event => event.preventDefault()),
207+
};
208+
209+
const component = await render(FixtureComponent, { componentProperties });
210+
211+
const inputControl = component.getByTestId('input') as HTMLInputElement;
212+
const inputValue = 'foobar';
213+
component.type(inputControl, inputValue);
214+
215+
expect(componentProperties.onKeyDown).toHaveBeenCalledTimes(inputValue.length);
216+
217+
component.blur(inputControl);
218+
expect(componentProperties.onChange).toBeCalledTimes(0);
219+
220+
expect(inputControl.value).toBe('');
221+
});

‎src/app/__snapshots__/app.component.spec.ts.snap‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@ exports[`matches snapshot 1`] = `
5858
<button>
5959
Greet
6060
</button>
61+
<form
62+
class="ng-untouched ng-pristine ng-invalid"
63+
ng-reflect-form="[object Object]"
64+
novalidate=""
65+
>
66+
<label>
67+
Name:
68+
<input
69+
class="ng-untouched ng-pristine ng-invalid"
70+
formcontrolname="name"
71+
ng-reflect-name="name"
72+
type="text"
73+
/>
74+
</label>
75+
<label>
76+
Age:
77+
<input
78+
class="ng-untouched ng-pristine ng-valid"
79+
formcontrolname="age"
80+
ng-reflect-name="age"
81+
type="number"
82+
/>
83+
</label>
84+
</form>
6185
</app-root>
6286
</div>
6387
`;

‎src/app/app.component.html‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular bl
2121
</ul>
2222

2323
<button (click)="greet()">Greet</button>
24+
25+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
26+
<label>
27+
Name:
28+
<input type="text" formControlName="name" />
29+
</label>
30+
31+
<label>
32+
Age:
33+
<input type="number" formControlName="age" />
34+
</label>
35+
</form>

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /