Skip to content

Commit 0c25d10

Browse files
authored
Improve forgot-await detection by tracking async calls (#216)
1 parent b1d8eed commit 0c25d10

7 files changed

Lines changed: 487 additions & 443 deletions

File tree

.changeset/chilled-mails-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'pleasantest': minor
3+
---
4+
5+
Improve forgot-await detection

src/async-hooks.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Manages asynchronous calls within withBrowser.
3+
* If any async calls are "left over" when withBrowser finishes executing,
4+
* then that means the user forgot to await the async calls,
5+
* so we should throw an error that indicates that.
6+
*/
7+
8+
import { removeFuncFromStackTrace } from './utils';
9+
10+
/**
11+
* Set of all active async hook trackers
12+
* We need to store this module-level so that jest-dom matchers can know which withBrowser they "belong" to
13+
* If there are multiple active at a time, the jest-dom matchers won't include the forgot-await behavior.
14+
*/
15+
export const activeAsyncHookTrackers = new Set<AsyncHookTracker>();
16+
17+
export interface AsyncHookTracker {
18+
addHook<T extends unknown>(
19+
func: () => Promise<T>,
20+
captureFunction: (...args: any[]) => any,
21+
): Promise<T | undefined>;
22+
close(): Error | undefined;
23+
}
24+
25+
export const createAsyncHookTracker = (): AsyncHookTracker => {
26+
const hooks = new Set<Error>();
27+
let isClosed = false;
28+
29+
const addHook: AsyncHookTracker['addHook'] = async (
30+
func,
31+
captureFunction,
32+
) => {
33+
const forgotAwaitError = new Error(
34+
'Cannot interact with browser after test finishes. Did you forget to await?',
35+
);
36+
removeFuncFromStackTrace(forgotAwaitError, captureFunction);
37+
hooks.add(forgotAwaitError);
38+
try {
39+
return await func();
40+
} catch (error) {
41+
if (!isClosed) throw error;
42+
} finally {
43+
if (!isClosed) hooks.delete(forgotAwaitError);
44+
}
45+
};
46+
47+
const close = () => {
48+
if (isClosed) return;
49+
isClosed = true;
50+
activeAsyncHookTrackers.delete(asyncHookTracker);
51+
if (hooks.size > 0) return hooks[Symbol.iterator]().next().value as Error;
52+
};
53+
54+
const asyncHookTracker: AsyncHookTracker = { addHook, close };
55+
activeAsyncHookTrackers.add(asyncHookTracker);
56+
return asyncHookTracker;
57+
};

0 commit comments

Comments
 (0)