Skip to content

Commit 41365e3

Browse files
feat: add type function (#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

+2-1
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

+2
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

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

+1
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';
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

+24
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

+12
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)