Skip to content

Commit 4520fb8

Browse files
feat: version 9.1
2 parents f7f5971 + 13dd9ff commit 4520fb8

15 files changed

+319
-85
lines changed

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"build": "npm run build:library && npm run build:library:jest-utils && npm run build:migrations && npm run build:readme",
99
"build:library": "ng build --prod testing-library",
1010
"build:library:jest-utils": "ng build --prod jest-utils",
11-
"build:migrations": "tsc -p ./projects/testing-library/migrations/tsconfig.migrations.json && cp ./projects/testing-library/migrations/migration.json ./dist/@testing-library/angular/migrations",
12-
"build:readme": "cp ./README.md ./dist/@testing-library/angular",
11+
"build:migrations": "tsc -p ./projects/testing-library/migrations/tsconfig.migrations.json && cpy ./projects/testing-library/migrations/migration.json ./dist/@testing-library/angular/migrations",
12+
"build:readme": "cpy ./README.md ./dist/@testing-library/angular",
1313
"test": "jest --config ./projects/jest.lib.config.js",
1414
"test:app": "jest --config ./src/jest.app.config.js",
1515
"precommit": "lint-staged",
@@ -39,7 +39,7 @@
3939
"@testing-library/user-event": "^8.1.0",
4040
"core-js": "^3.1.3",
4141
"rxjs": "^6.5.4",
42-
"tslib": "^1.10.0",
42+
"tslib": "^1.11.1",
4343
"tslint": "^5.16.0",
4444
"zone.js": "~0.10.2"
4545
},
@@ -53,6 +53,7 @@
5353
"@types/jest": "~24.0.11",
5454
"@types/node": "^13.7.6",
5555
"codelyzer": "^5.1.2",
56+
"cpy-cli": "^3.1.0",
5657
"husky": "^2.3.0",
5758
"jest": "^24.1.0",
5859
"jest-preset-angular": "^7.1.1",

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ComponentFixture } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
44
import { BoundFunction, FireObject, Queries, queries, waitFor, waitForElementToBeRemoved } from '@testing-library/dom';
55
import { UserEvents } from './user-events';
6+
import { OptionsReceived } from 'pretty-format';
67

78
export type RenderResultQueries<Q extends Queries = typeof queries> = { [P in keyof Q]: BoundFunction<Q[P]> };
8-
99
export interface RenderResult<ComponentType, WrapperType = ComponentType>
1010
extends RenderResultQueries,
1111
FireObject,
@@ -24,7 +24,11 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType>
2424
* @param
2525
* element: The to be printed HTML element, if not provided it will log the whole component's DOM
2626
*/
27-
debug: (element?: HTMLElement) => void;
27+
debug: (
28+
element?: Element | HTMLDocument | (Element | HTMLDocument)[],
29+
maxLength?: number,
30+
options?: OptionsReceived,
31+
) => void;
2832
/**
2933
* @description
3034
* Trigger a change detection cycle for the component.

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

+74-18
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform
55
import { Router } from '@angular/router';
66
import { RouterTestingModule } from '@angular/router/testing';
77
import {
8-
fireEvent,
98
FireFunction,
109
FireObject,
1110
getQueriesForElement,
1211
prettyDOM,
1312
waitFor,
1413
waitForElementToBeRemoved,
14+
fireEvent as dtlFireEvent,
15+
screen as dtlScreen,
16+
queries as dtlQueries,
1517
} from '@testing-library/dom';
1618
import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models';
1719
import { createSelectOptions, createType, tab } from './user-events';
1820

1921
@Component({ selector: 'wrapper-component', template: '' })
2022
class WrapperComponent {}
2123

22-
const mountedContainers = new Set();
24+
const mountedFixtures = new Set<ComponentFixture<any>>();
2325

2426
export async function render<ComponentType>(
2527
component: Type<ComponentType>,
@@ -75,9 +77,10 @@ export async function render<SutType, WrapperType = SutType>(
7577
if (idAttribute && idAttribute.startsWith('root')) {
7678
fixture.nativeElement.removeAttribute('id');
7779
}
78-
mountedContainers.add(fixture.nativeElement);
7980
}
8081

82+
mountedFixtures.add(fixture);
83+
8184
await TestBed.compileComponents();
8285

8386
let isAlive = true;
@@ -93,10 +96,10 @@ export async function render<SutType, WrapperType = SutType>(
9396
detectChanges();
9497
}
9598

96-
const eventsWithDetectChanges = Object.keys(fireEvent).reduce(
99+
const eventsWithDetectChanges = Object.keys(dtlFireEvent).reduce(
97100
(events, key) => {
98101
events[key] = (element: HTMLElement, options?: {}) => {
99-
const result = fireEvent[key](element, options);
102+
const result = dtlFireEvent[key](element, options);
100103
detectChanges();
101104
return result;
102105
};
@@ -137,10 +140,12 @@ export async function render<SutType, WrapperType = SutType>(
137140
attributes: boolean;
138141
characterData: boolean;
139142
};
140-
} = { container: fixture.nativeElement, interval: 50 },
143+
} = { container: fixture.nativeElement },
141144
): Promise<T> {
142-
const interval = setInterval(detectChanges, options.interval);
143-
return waitFor<T>(callback, options).finally(() => clearInterval(interval));
145+
return waitFor<T>(() => {
146+
detectChanges();
147+
return callback();
148+
}, options);
144149
}
145150

146151
function componentWaitForElementToBeRemoved<T>(
@@ -155,10 +160,12 @@ export async function render<SutType, WrapperType = SutType>(
155160
attributes: boolean;
156161
characterData: boolean;
157162
};
158-
} = { container: fixture.nativeElement, interval: 50 },
163+
} = { container: fixture.nativeElement },
159164
): Promise<T> {
160-
const interval = setInterval(detectChanges, options.interval);
161-
return waitForElementToBeRemoved<T>(callback, options).finally(() => clearInterval(interval));
165+
return waitForElementToBeRemoved<T>(() => {
166+
detectChanges();
167+
return callback();
168+
}, options);
162169
}
163170

164171
return {
@@ -168,13 +175,16 @@ export async function render<SutType, WrapperType = SutType>(
168175
rerender,
169176
debugElement: fixture.debugElement.query(By.directive(sut)),
170177
container: fixture.nativeElement,
171-
debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)),
178+
debug: (element = fixture.nativeElement, maxLength, options) =>
179+
Array.isArray(element)
180+
? element.forEach(e => console.log(prettyDOM(e, maxLength, options)))
181+
: console.log(prettyDOM(element, maxLength, options)),
172182
type: createType(eventsWithDetectChanges),
173183
selectOptions: createSelectOptions(eventsWithDetectChanges),
174184
tab,
175185
waitFor: componentWaitFor,
176186
waitForElementToBeRemoved: componentWaitForElementToBeRemoved,
177-
...getQueriesForElement(fixture.nativeElement, queries),
187+
...replaceFindWithFindAndDetectChanges(fixture.nativeElement, getQueriesForElement(fixture.nativeElement, queries)),
178188
...eventsWithDetectChanges,
179189
};
180190
}
@@ -237,19 +247,65 @@ function addAutoImports({ imports, routes }: Pick<RenderComponentOptions<any>, '
237247
return [...imports, ...animations(), ...routing()];
238248
}
239249

250+
// for the findBy queries we first want to run a change detection cycle
251+
function replaceFindWithFindAndDetectChanges<T>(container: HTMLElement, originalQueriesForContainer: T): T {
252+
return Object.keys(originalQueriesForContainer).reduce(
253+
(newQueries, key) => {
254+
if (key.startsWith('find')) {
255+
const getByQuery = dtlQueries[key.replace('find', 'get')];
256+
newQueries[key] = async (text, options, waitForOptions) => {
257+
// original implementation at https://github.com/testing-library/dom-testing-library/blob/master/src/query-helpers.js
258+
const result = await waitFor(() => {
259+
detectChangesForMountedFixtures();
260+
return getByQuery(container, text, options);
261+
}, waitForOptions);
262+
return result;
263+
};
264+
} else {
265+
newQueries[key] = originalQueriesForContainer[key];
266+
}
267+
268+
return newQueries;
269+
},
270+
{} as T,
271+
);
272+
}
273+
240274
function cleanup() {
241-
mountedContainers.forEach(cleanupAtContainer);
275+
mountedFixtures.forEach(cleanupAtFixture);
242276
}
243277

244-
function cleanupAtContainer(container) {
245-
if (container.parentNode === document.body) {
246-
document.body.removeChild(container);
278+
function cleanupAtFixture(fixture) {
279+
if (!fixture.nativeElement.getAttribute('ng-version') && fixture.nativeElement.parentNode === document.body) {
280+
document.body.removeChild(fixture.nativeElement);
247281
}
248-
mountedContainers.delete(container);
282+
mountedFixtures.delete(fixture);
249283
}
250284

251285
if (typeof afterEach === 'function' && !process.env.ATL_SKIP_AUTO_CLEANUP) {
252286
afterEach(async () => {
253287
cleanup();
254288
});
255289
}
290+
291+
function detectChangesForMountedFixtures() {
292+
mountedFixtures.forEach(fixture => fixture.detectChanges());
293+
}
294+
295+
export * from '@testing-library/dom';
296+
297+
const fireEvent = Object.keys(dtlFireEvent).reduce(
298+
(events, key) => {
299+
events[key] = (element: HTMLElement, options?: {}) => {
300+
const result = dtlFireEvent[key](element, options);
301+
detectChangesForMountedFixtures();
302+
return result;
303+
};
304+
return events;
305+
},
306+
{} as typeof dtlFireEvent,
307+
);
308+
309+
const screen = replaceFindWithFindAndDetectChanges(document.body, dtlScreen);
310+
311+
export { fireEvent, screen };

projects/testing-library/src/public_api.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55
export * from './lib/models';
66
export * from './lib/testing-library';
77
export * from './lib/user-events';
8-
export * from '@testing-library/dom';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component } from '@angular/core';
2+
import { timer } from 'rxjs';
3+
import { render, screen } from '../src/public_api';
4+
import { mapTo, timeout } from 'rxjs/operators';
5+
6+
@Component({
7+
selector: 'fixture',
8+
template: `
9+
<div>{{ result | async }}</div>
10+
`,
11+
})
12+
class FixtureComponent {
13+
result = timer(30).pipe(mapTo('I am visible'));
14+
}
15+
16+
describe('screen', () => {
17+
test('waits for element to be added to the DOM', async () => {
18+
await render(FixtureComponent);
19+
await expect(screen.findByText('I am visible')).resolves.toBeTruthy();
20+
});
21+
22+
test('rejects when something cannot be found', async () => {
23+
await render(FixtureComponent);
24+
await expect(screen.findByText('I am invisible', {}, { timeout: 40 })).rejects.toThrow('x');
25+
});
26+
});
27+
28+
describe('rendered component', () => {
29+
test('waits for element to be added to the DOM', async () => {
30+
const { findByText } = await render(FixtureComponent);
31+
await expect(findByText('I am visible')).resolves.toBeTruthy();
32+
});
33+
34+
test('rejects when something cannot be found', async () => {
35+
const { findByText } = await render(FixtureComponent);
36+
await expect(findByText('I am invisible', {}, { timeout: 40 })).rejects.toThrow('x');
37+
});
38+
});

projects/testing-library/tsconfig.lib.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
"compilerOptions": {
44
"outDir": "../../out-tsc/lib",
55
"target": "es2015",
6-
"module": "es2015",
6+
"module": "CommonJS",
77
"moduleResolution": "node",
88
"declaration": true,
99
"sourceMap": true,
1010
"inlineSources": true,
1111
"emitDecoratorMetadata": true,
1212
"experimentalDecorators": true,
13-
"importHelpers": true,
13+
"importHelpers": false,
1414
"types": ["@types/jest", "@types/node"],
1515
"lib": ["dom", "es2015", "es2018.promise"]
1616
},
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { render, screen } from '@testing-library/angular';
1+
import { render, screen, fireEvent } from '@testing-library/angular';
22

33
import { SingleComponent } from './00-single-component';
44

55
test('renders the current value and can increment and decrement', async () => {
6-
const { click } = await render(SingleComponent);
6+
await render(SingleComponent);
77

88
const incrementControl = screen.getByText('Increment');
99
const decrementControl = screen.getByText('Decrement');
1010
const valueControl = screen.getByTestId('value');
1111

1212
expect(valueControl.textContent).toBe('0');
1313

14-
click(incrementControl);
15-
click(incrementControl);
14+
fireEvent.click(incrementControl);
15+
fireEvent.click(incrementControl);
1616
expect(valueControl.textContent).toBe('2');
1717

18-
click(decrementControl);
18+
fireEvent.click(decrementControl);
1919
expect(valueControl.textContent).toBe('1');
2020
});
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { render, screen } from '@testing-library/angular';
1+
import { render, screen, fireEvent } from '@testing-library/angular';
22

33
import { NestedButtonComponent, NestedValueComponent, NestedContainerComponent } from './01-nested-component';
44

55
test('renders the current value and can increment and decrement', async () => {
6-
const { click } = await render(NestedContainerComponent, {
6+
await render(NestedContainerComponent, {
77
declarations: [NestedButtonComponent, NestedValueComponent],
88
});
99

@@ -13,10 +13,10 @@ test('renders the current value and can increment and decrement', async () => {
1313

1414
expect(valueControl.textContent).toBe('0');
1515

16-
click(incrementControl);
17-
click(incrementControl);
16+
fireEvent.click(incrementControl);
17+
fireEvent.click(incrementControl);
1818
expect(valueControl.textContent).toBe('2');
1919

20-
click(decrementControl);
20+
fireEvent.click(decrementControl);
2121
expect(valueControl.textContent).toBe('1');
2222
});

src/app/examples/02-input-output.spec.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { render, screen } from '@testing-library/angular';
1+
import { render, screen, fireEvent } from '@testing-library/angular';
22

33
import { InputOutputComponent } from './02-input-output';
44

55
test('is possible to set input and listen for output', async () => {
66
const sendValue = jest.fn();
77

8-
const { click } = await render(InputOutputComponent, {
8+
await render(InputOutputComponent, {
99
componentProperties: {
1010
value: 47,
1111
sendValue: {
@@ -20,12 +20,12 @@ test('is possible to set input and listen for output', async () => {
2020

2121
expect(valueControl.textContent).toBe('47');
2222

23-
click(incrementControl);
24-
click(incrementControl);
25-
click(incrementControl);
23+
fireEvent.click(incrementControl);
24+
fireEvent.click(incrementControl);
25+
fireEvent.click(incrementControl);
2626
expect(valueControl.textContent).toBe('50');
2727

28-
click(sendControl);
28+
fireEvent.click(sendControl);
2929
expect(sendValue).toHaveBeenCalledTimes(1);
3030
expect(sendValue).toHaveBeenCalledWith(50);
3131
});

src/app/examples/03-forms.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ReactiveFormsModule } from '@angular/forms';
2-
import { render, screen } from '@testing-library/angular';
2+
import { render, screen, fireEvent } from '@testing-library/angular';
33

44
import { FormsComponent } from './03-forms';
55

66
test('is possible to fill in a form and verify error messages (with the help of jest-dom https://testing-library.com/docs/ecosystem-jest-dom)', async () => {
7-
const { type, blur, selectOptions } = await render(FormsComponent, {
7+
const { type, selectOptions } = await render(FormsComponent, {
88
imports: [ReactiveFormsModule],
99
});
1010

@@ -20,7 +20,7 @@ test('is possible to fill in a form and verify error messages (with the help of
2020
expect(nameControl).toBeInvalid();
2121
type(nameControl, 'Tim');
2222
type(scoreControl, '12');
23-
blur(scoreControl);
23+
fireEvent.blur(scoreControl);
2424
selectOptions(colorControl, 'Green');
2525

2626
expect(screen.queryByText('name is required')).not.toBeInTheDocument();
@@ -29,7 +29,7 @@ test('is possible to fill in a form and verify error messages (with the help of
2929

3030
expect(scoreControl).toBeInvalid();
3131
type(scoreControl, 7);
32-
blur(scoreControl);
32+
fireEvent.blur(scoreControl);
3333
expect(scoreControl).toBeValid();
3434

3535
expect(errors).not.toBeInTheDocument();

0 commit comments

Comments
 (0)