Skip to content

Commit a3975cd

Browse files
feat: waitForDomChange, waitForElement, waitForElementToBeRemoved (#60)
1 parent 924382c commit a3975cd

File tree

6 files changed

+199
-10
lines changed

6 files changed

+199
-10
lines changed

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

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { Type, DebugElement } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
4-
import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom';
4+
import {
5+
BoundFunction,
6+
FireObject,
7+
Queries,
8+
queries,
9+
waitForElement,
10+
waitForElementToBeRemoved,
11+
waitForDomChange,
12+
} from '@testing-library/dom';
513
import { UserEvents } from './user-events';
614

715
export type RenderResultQueries<Q extends Queries = typeof queries> = { [P in keyof Q]: BoundFunction<Q[P]> };
@@ -34,9 +42,11 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType>
3442
detectChanges: () => void;
3543
/**
3644
* @description
37-
* Re-render the same component with different props.
45+
* The Angular `DebugElement` of the component.
46+
*
47+
* For more info see https://angular.io/api/core/DebugElement
3848
*/
39-
rerender: (componentProperties: Partial<ComponentType>) => void;
49+
debugElement: DebugElement;
4050
/**
4151
* @description
4252
* The Angular `ComponentFixture` of the component or the wrapper.
@@ -47,17 +57,36 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType>
4757
fixture: ComponentFixture<WrapperType>;
4858
/**
4959
* @description
50-
* The Angular `DebugElement` of the component.
60+
* Navigates to the href of the element or to the path.
5161
*
52-
* For more info see https://angular.io/api/core/DebugElement
5362
*/
54-
debugElement: DebugElement;
63+
navigate: (elementOrPath: Element | string, basePath?: string) => Promise<boolean>;
5564
/**
5665
* @description
57-
* Navigates to the href of the element or to the path.
66+
* Re-render the same component with different props.
67+
*/
68+
rerender: (componentProperties: Partial<ComponentType>) => void;
69+
/**
70+
* @description
71+
* Wait for the DOM to change.
5872
*
73+
* For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitfordomchange
5974
*/
60-
navigate: (elementOrPath: Element | string, basePath?: string) => Promise<boolean>;
75+
waitForDomChange: typeof waitForDomChange;
76+
/**
77+
* @description
78+
* Wait for DOM elements to appear, disappear, or change.
79+
*
80+
* For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitforelement
81+
*/
82+
waitForElement: typeof waitForElement;
83+
/**
84+
* @description
85+
* Wait for the removal of element(s) from the DOM.
86+
*
87+
* For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitforelementtoberemoved
88+
*/
89+
waitForElementToBeRemoved: typeof waitForElementToBeRemoved;
6190
}
6291

6392
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {

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

+52-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import { By } from '@angular/platform-browser';
44
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
55
import { Router } from '@angular/router';
66
import { RouterTestingModule } from '@angular/router/testing';
7-
import { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom';
7+
import {
8+
fireEvent,
9+
FireFunction,
10+
FireObject,
11+
getQueriesForElement,
12+
prettyDOM,
13+
waitForDomChange,
14+
waitForElement,
15+
waitForElementToBeRemoved,
16+
} from '@testing-library/dom';
817
import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models';
918
import { createSelectOptions, createType } from './user-events';
1019

@@ -111,6 +120,45 @@ export async function render<SutType, WrapperType = SutType>(
111120
return result;
112121
};
113122

123+
function componentWaitForDomChange<Result>(options?: {
124+
container?: HTMLElement;
125+
timeout?: number;
126+
mutationObserverOptions?: MutationObserverInit;
127+
}): Promise<Result> {
128+
const interval = setInterval(detectChanges, 10);
129+
return waitForDomChange<Result>({ container: fixture.nativeElement, ...options }).finally(() =>
130+
clearInterval(interval),
131+
);
132+
}
133+
134+
function componentWaitForElement<Result>(
135+
callback: () => Result,
136+
options?: {
137+
container?: HTMLElement;
138+
timeout?: number;
139+
mutationObserverOptions?: MutationObserverInit;
140+
},
141+
): Promise<Result> {
142+
const interval = setInterval(detectChanges, 10);
143+
return waitForElement(callback, { container: fixture.nativeElement, ...options }).finally(() =>
144+
clearInterval(interval),
145+
);
146+
}
147+
148+
function componentWaitForElementToBeRemoved<Result>(
149+
callback: () => Result,
150+
options?: {
151+
container?: HTMLElement;
152+
timeout?: number;
153+
mutationObserverOptions?: MutationObserverInit;
154+
},
155+
): Promise<Result> {
156+
const interval = setInterval(detectChanges, 10);
157+
return waitForElementToBeRemoved(callback, { container: fixture.nativeElement, ...options }).finally(() =>
158+
clearInterval(interval),
159+
);
160+
}
161+
114162
return {
115163
fixture,
116164
detectChanges,
@@ -121,6 +169,9 @@ export async function render<SutType, WrapperType = SutType>(
121169
debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)),
122170
type: createType(eventsWithDetectChanges),
123171
selectOptions: createSelectOptions(eventsWithDetectChanges),
172+
waitForDomChange: componentWaitForDomChange,
173+
waitForElement: componentWaitForElement,
174+
waitForElementToBeRemoved: componentWaitForElementToBeRemoved,
124175
...getQueriesForElement(fixture.nativeElement, queries),
125176
...eventsWithDetectChanges,
126177
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { render } from '../src/public_api';
3+
import { timer } from 'rxjs';
4+
5+
@Component({
6+
selector: 'fixture',
7+
template: `
8+
<div *ngIf="oneVisible" data-testid="block-one">One</div>
9+
<div *ngIf="twoVisible" data-testid="block-two">Two</div>
10+
`,
11+
})
12+
class FixtureComponent implements OnInit {
13+
oneVisible = false;
14+
twoVisible = false;
15+
16+
ngOnInit() {
17+
timer(200).subscribe(() => (this.oneVisible = true));
18+
timer(400).subscribe(() => (this.twoVisible = true));
19+
}
20+
}
21+
22+
test('waits for the DOM to change', async () => {
23+
const { queryByTestId, getByTestId, waitForDomChange } = await render(FixtureComponent);
24+
25+
await waitForDomChange();
26+
27+
getByTestId('block-one');
28+
expect(queryByTestId('block-two')).toBeNull();
29+
30+
await waitForDomChange();
31+
32+
getByTestId('block-one');
33+
getByTestId('block-two');
34+
});
35+
36+
test('allows to override options', async () => {
37+
const { waitForDomChange } = await render(FixtureComponent);
38+
39+
await expect(waitForDomChange({ timeout: 100 })).rejects.toThrow(/Timed out in waitForDomChange/i);
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { render } from '../src/public_api';
3+
import { timer } from 'rxjs';
4+
5+
@Component({
6+
selector: 'fixture',
7+
template: `
8+
<div *ngIf="visible" data-testid="im-here">👋</div>
9+
`,
10+
})
11+
class FixtureComponent implements OnInit {
12+
visible = true;
13+
ngOnInit() {
14+
timer(500).subscribe(() => (this.visible = false));
15+
}
16+
}
17+
18+
test('waits for element to be removed', async () => {
19+
const { queryByTestId, getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent);
20+
21+
await waitForElementToBeRemoved(() => getByTestId('im-here'));
22+
23+
expect(queryByTestId('im-here')).toBeNull();
24+
});
25+
26+
test('allows to override options', async () => {
27+
const { getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent);
28+
29+
await expect(waitForElementToBeRemoved(() => getByTestId('im-here'), { timeout: 200 })).rejects.toThrow(
30+
/Timed out in waitForElementToBeRemoved/i,
31+
);
32+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Component } from '@angular/core';
2+
import { render } from '../src/public_api';
3+
import { timer } from 'rxjs';
4+
5+
@Component({
6+
selector: 'fixture',
7+
template: `
8+
<button data-testid="button" (click)="load()">Load</button>
9+
<div>{{ result }}</div>
10+
`,
11+
})
12+
class FixtureComponent {
13+
result = '';
14+
15+
load() {
16+
timer(500).subscribe(() => (this.result = 'Success'));
17+
}
18+
}
19+
20+
test('waits for element to be visible', async () => {
21+
const { getByTestId, click, waitForElement, getByText } = await render(FixtureComponent);
22+
23+
click(getByTestId('button'));
24+
25+
await waitForElement(() => getByText('Success'));
26+
getByText('Success');
27+
});
28+
29+
test('allows to override options', async () => {
30+
const { getByTestId, click, waitForElement, getByText } = await render(FixtureComponent);
31+
32+
click(getByTestId('button'));
33+
34+
await expect(waitForElement(() => getByText('Success'), { timeout: 200 })).rejects.toThrow(
35+
/Unable to find an element with the text: Success/i,
36+
);
37+
});

projects/testing-library/tsconfig.lib.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"experimentalDecorators": true,
1313
"importHelpers": true,
1414
"types": [],
15-
"lib": ["dom", "es2015"]
15+
"lib": ["dom", "es2015", "es2018.promise"]
1616
},
1717
"angularCompilerOptions": {
1818
"annotateForClosureCompiler": true,

0 commit comments

Comments
 (0)