Skip to content

Commit d86732e

Browse files
eps1lonSebastian Silbermann
authored and
Sebastian Silbermann
committed
feat: Rough sketch for API (includes untested implementation)
1 parent 6f6f5a8 commit d86732e

10 files changed

+398
-44
lines changed

jest.config.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@ const {
77

88
module.exports = {
99
collectCoverageFrom,
10-
coveragePathIgnorePatterns: [
11-
...coveragePathIgnorePatterns,
12-
'/__tests__/',
13-
'/__node_tests__/',
14-
],
10+
coveragePathIgnorePatterns: [...coveragePathIgnorePatterns, '/__tests__/'],
1511
coverageThreshold,
1612
watchPlugins: [
1713
...watchPlugins,
1814
require.resolve('jest-watch-select-projects'),
1915
],
2016
projects: [
21-
require.resolve('./tests/jest.config.dom.js'),
22-
require.resolve('./tests/jest.config.node.js'),
17+
// No idea why I need to specify a project instead of having a single config
18+
require.resolve('./tests/jest.config.js'),
2319
],
2420
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"jest-watch-select-projects": "^2.0.0",
6969
"jsdom": "^16.4.0",
7070
"kcd-scripts": "^11.0.0",
71+
"pretty-format": "^29.3.1",
7172
"typescript": "^4.1.2"
7273
},
7374
"overrides": {

src/__tests__/waitFor.test.js

-5
This file was deleted.

src/__tests__/waitForDOM.test.js

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import * as prettyFormat from 'pretty-format'
6+
import {waitFor as waitForWeb} from '../'
7+
8+
function jestFakeTimersAreEnabled() {
9+
/* istanbul ignore else */
10+
// eslint-disable-next-line
11+
if (typeof jest !== 'undefined' && jest !== null) {
12+
return (
13+
// legacy timers
14+
setTimeout._isMockFunction === true ||
15+
// modern timers
16+
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
17+
)
18+
}
19+
// istanbul ignore next
20+
return false
21+
}
22+
23+
function getWindowFromNode(node) {
24+
if (node.defaultView) {
25+
// node is document
26+
return node.defaultView
27+
} else if (node.ownerDocument && node.ownerDocument.defaultView) {
28+
// node is a DOM node
29+
return node.ownerDocument.defaultView
30+
} else {
31+
// node is window
32+
return node.window
33+
}
34+
}
35+
36+
/**
37+
* Reference implementation of `waitFor` when a DOM is available.
38+
* Supports fake timers and configureable instrumentation.
39+
*/
40+
function waitFor(
41+
callback,
42+
{
43+
container = document,
44+
interval = 50,
45+
mutationObserverOptions = {
46+
subtree: true,
47+
childList: true,
48+
attributes: true,
49+
characterData: true,
50+
},
51+
timeout = 1000,
52+
} = {},
53+
) {
54+
function getElementError(message) {
55+
const prettifiedDOM = prettyFormat(container)
56+
const error = new Error(
57+
[message, prettifiedDOM].filter(Boolean).join('\n\n'),
58+
)
59+
error.name = 'TestingLibraryElementError'
60+
return error
61+
}
62+
63+
function handleTimeout(error) {
64+
error.message = getElementError(error.message).message
65+
return error
66+
}
67+
68+
function advanceTimersWrapper(cb) {
69+
// /dom config. /react uses act() here
70+
return cb()
71+
}
72+
73+
function runWithExpensiveErrorDiagnosticsDisabled() {
74+
// /dom would disable certain config options when running callback
75+
return callback()
76+
}
77+
78+
/** @type {import('../').FakeClock} */
79+
const jestFakeClock = {
80+
advanceTimersByTime: timeoutMS => {
81+
advanceTimersWrapper(() => {
82+
jest.advanceTimersByTime(timeoutMS)
83+
})
84+
},
85+
flushPromises: () => {
86+
return advanceTimersWrapper(async () => {
87+
await new Promise(r => {
88+
setTimeout(r, 0)
89+
jest.advanceTimersByTime(0)
90+
})
91+
})
92+
},
93+
}
94+
const clock = jestFakeTimersAreEnabled() ? jestFakeClock : undefined
95+
const controller = new AbortController()
96+
97+
return new Promise((resolve, reject) => {
98+
let promiseStatus = 'idle'
99+
100+
function onDone(error, result) {
101+
controller.abort()
102+
if (error === null) {
103+
resolve(result)
104+
} else {
105+
reject(error)
106+
}
107+
}
108+
109+
function checkCallbackWithExpensiveErrorDiagnosticsDisabled() {
110+
if (promiseStatus === 'pending') return undefined
111+
112+
const result = runWithExpensiveErrorDiagnosticsDisabled()
113+
if (typeof result?.then === 'function') {
114+
promiseStatus = 'pending'
115+
return result.then(
116+
resolvedValue => {
117+
promiseStatus = 'resolved'
118+
return resolvedValue
119+
},
120+
rejectedValue => {
121+
promiseStatus = 'rejected'
122+
throw rejectedValue
123+
},
124+
)
125+
}
126+
return result
127+
}
128+
129+
waitForWeb(checkCallbackWithExpensiveErrorDiagnosticsDisabled, {
130+
clock,
131+
interval,
132+
onTimeout: handleTimeout,
133+
signal: controller.signal,
134+
timeout,
135+
}).then(
136+
result => {
137+
onDone(null, result)
138+
},
139+
error => {
140+
// https://webidl.spec.whatwg.org/#idl-DOMException
141+
// https://dom.spec.whatwg.org/#ref-for-dom-abortcontroller-abortcontroller%E2%91%A0
142+
const isAbortError =
143+
error.name === 'AbortError' && error.code === DOMException.ABORT_ERR
144+
// Ignore abort errors
145+
if (!isAbortError) {
146+
onDone(error, null)
147+
}
148+
},
149+
)
150+
151+
const {MutationObserver} = getWindowFromNode(container)
152+
const observer = new MutationObserver(
153+
checkCallbackWithExpensiveErrorDiagnosticsDisabled,
154+
)
155+
observer.observe(container, mutationObserverOptions)
156+
controller.signal.addEventListener('abort', () => {
157+
observer.disconnect()
158+
})
159+
})
160+
}
161+
162+
test('runs', async () => {
163+
await expect(waitFor(() => {})).resolves.toBeUndefined()
164+
})

src/__tests__/waitForNode.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import {waitFor as waitForWeb} from '../'
6+
7+
function jestFakeTimersAreEnabled() {
8+
/* istanbul ignore else */
9+
// eslint-disable-next-line
10+
if (typeof jest !== 'undefined' && jest !== null) {
11+
return (
12+
// legacy timers
13+
setTimeout._isMockFunction === true ||
14+
// modern timers
15+
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
16+
)
17+
}
18+
// istanbul ignore next
19+
return false
20+
}
21+
22+
/**
23+
* Reference implementation of `waitFor` that supports Jest fake timers
24+
*/
25+
function waitFor(callback, {interval = 50, timeout = 1000} = {}) {
26+
/** @type {import('../').FakeClock} */
27+
const jestFakeClock = {
28+
advanceTimersByTime: timeoutMS => {
29+
jest.advanceTimersByTime(timeoutMS)
30+
},
31+
flushPromises: () => {
32+
return new Promise(r => {
33+
setTimeout(r, 0)
34+
jest.advanceTimersByTime(0)
35+
})
36+
},
37+
}
38+
const clock = jestFakeTimersAreEnabled() ? jestFakeClock : undefined
39+
40+
return waitForWeb(callback, {
41+
clock,
42+
interval,
43+
timeout,
44+
})
45+
}
46+
47+
test('runs', async () => {
48+
await expect(waitFor(() => {})).resolves.toBeUndefined()
49+
})

src/index.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO
2-
export function waitFor(fn: () => Promise<void>): Promise<void> {
3-
return Promise.resolve()
4-
}
1+
export {default as waitFor} from './waitFor'
2+
export type {FakeClock, WaitForOptions} from './waitFor'

0 commit comments

Comments
 (0)