Skip to content

Commit 4b269bc

Browse files
fix: infinite loop in waitFor when DOM is mutating (#231)
1 parent 7a64a6b commit 4b269bc

File tree

6 files changed

+44
-8
lines changed

6 files changed

+44
-8
lines changed

apps/example-app/src/app/examples/13-scrolling.component.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test('should scroll to load more items', async () => {
1212
expect(item0).toBeVisible();
1313

1414
screen.getByTestId('scroll-viewport').scrollTop = 500;
15-
await waitForElementToBeRemoved(() => screen.getByText(/Item #0/i));
15+
await waitForElementToBeRemoved(() => screen.queryByText(/Item #0/i));
1616

1717
const item12 = await screen.findByText(/Item #12/i);
1818
expect(item12).toBeVisible();

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,21 @@ function addAutoImports({ imports, routes }: Pick<RenderComponentOptions<any>, '
303303
}
304304

305305
/**
306-
* Wrap waitFor to poke the Angular change detection cycle before invoking the callback
306+
* Wrap waitFor to invoke the Angular change detection cycle before invoking the callback
307307
*/
308308
async function waitForWrapper<T>(
309309
detectChanges: () => void,
310310
callback: () => T extends Promise<any> ? never : T,
311311
options?: dtlWaitForOptions,
312312
): Promise<T> {
313+
detectChanges();
313314
return await dtlWaitFor(() => {
314-
detectChanges();
315-
return callback();
315+
try {
316+
return callback();
317+
} catch (error) {
318+
setImmediate(() => detectChanges());
319+
throw error;
320+
}
316321
}, options);
317322
}
318323

@@ -340,8 +345,10 @@ async function waitForElementToBeRemovedWrapper<T>(
340345
}
341346

342347
return await dtlWaitForElementToBeRemoved(() => {
348+
const result = cb();
343349
detectChanges();
344-
return cb();
350+
setImmediate(() => detectChanges());
351+
return result;
345352
}, options);
346353
}
347354

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Component } from '@angular/core';
2+
import { render, waitFor, screen } from '@testing-library/angular';
3+
4+
@Component({
5+
template: ` <button [ngClass]="classes">Load</button> `,
6+
})
7+
class LoopComponent {
8+
get classes() {
9+
return {
10+
someClass: true,
11+
};
12+
}
13+
}
14+
15+
test('wait does not end up in a loop', async () => {
16+
await render(LoopComponent);
17+
18+
await expect(
19+
waitFor(() => {
20+
expect(true).toEqual(false);
21+
}),
22+
).rejects.toThrow();
23+
});
24+
25+
test('find does not end up in a loop', async () => {
26+
await render(LoopComponent);
27+
28+
await expect(screen.findByText('foo')).rejects.toThrow();
29+
});

projects/testing-library/tests/wait-for.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component } from '@angular/core';
22
import { timer } from 'rxjs';
3-
import { render, screen, fireEvent, waitFor as waitForATL } from '../src/public_api';
3+
import { render, screen, fireEvent, waitFor } from '../src/public_api';
44

55
@Component({
66
selector: 'atl-fixture',
@@ -24,7 +24,7 @@ test('waits for assertion to become true', async () => {
2424

2525
fireEvent.click(screen.getByTestId('button'));
2626

27-
await waitForATL(() => screen.getByText('Success'));
27+
await waitFor(() => screen.getByText('Success'));
2828
screen.getByText('Success');
2929
});
3030

@@ -33,7 +33,7 @@ test('allows to override options', async () => {
3333

3434
fireEvent.click(screen.getByTestId('button'));
3535

36-
await expect(waitForATL(() => screen.getByText('Success'), { timeout: 200 })).rejects.toThrow(
36+
await expect(waitFor(() => screen.getByText('Success'), { timeout: 200 })).rejects.toThrow(
3737
/Unable to find an element with the text: Success/i,
3838
);
3939
});

0 commit comments

Comments
 (0)