Skip to content

Commit 4abf85f

Browse files
committed
Inline waitFor from dtl
1 parent 3e65b4e commit 4abf85f

File tree

3 files changed

+290
-11
lines changed

3 files changed

+290
-11
lines changed

src/dtlHelpers.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// @source @testing-library/dom/src/helpers.js
2+
3+
// Constant node.nodeType for text nodes, see:
4+
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
5+
const TEXT_NODE = 3
6+
7+
function jestFakeTimersAreEnabled() {
8+
/* istanbul ignore else */
9+
if (typeof jest !== 'undefined' && jest !== null) {
10+
return (
11+
// legacy timers
12+
setTimeout._isMockFunction === true ||
13+
// modern timers
14+
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
15+
)
16+
}
17+
// istanbul ignore next
18+
return false
19+
}
20+
21+
function getDocument() {
22+
/* istanbul ignore if */
23+
if (typeof window === 'undefined') {
24+
throw new Error('Could not find default container')
25+
}
26+
return window.document
27+
}
28+
function getWindowFromNode(node) {
29+
if (node.defaultView) {
30+
// node is document
31+
return node.defaultView
32+
} else if (node.ownerDocument && node.ownerDocument.defaultView) {
33+
// node is a DOM node
34+
return node.ownerDocument.defaultView
35+
} else if (node.window) {
36+
// node is window
37+
return node.window
38+
} else if (node.then instanceof Function) {
39+
throw new Error(
40+
`It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?`,
41+
)
42+
} else if (Array.isArray(node)) {
43+
throw new Error(
44+
`It looks like you passed an Array instead of a DOM node. Did you do something like \`fireEvent.click(screen.getAllBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`?`,
45+
)
46+
} else if (
47+
typeof node.debug === 'function' &&
48+
typeof node.logTestingPlaygroundURL === 'function'
49+
) {
50+
throw new Error(
51+
`It looks like you passed a \`screen\` object. Did you do something like \`fireEvent.click(screen, ...\` when you meant to use a query, e.g. \`fireEvent.click(screen.getBy..., \`?`,
52+
)
53+
} else {
54+
// The user passed something unusual to a calling function
55+
throw new Error(
56+
`Unable to find the "window" object for the given node. Please file an issue with the code that's causing you to see this error: https://github.com/testing-library/dom-testing-library/issues/new`,
57+
)
58+
}
59+
}
60+
61+
function checkContainerType(container) {
62+
if (
63+
!container ||
64+
!(typeof container.querySelector === 'function') ||
65+
!(typeof container.querySelectorAll === 'function')
66+
) {
67+
throw new TypeError(
68+
`Expected container to be an Element, a Document or a DocumentFragment but got ${getTypeName(
69+
container,
70+
)}.`,
71+
)
72+
}
73+
74+
function getTypeName(object) {
75+
if (typeof object === 'object') {
76+
return object === null ? 'null' : object.constructor.name
77+
}
78+
return typeof object
79+
}
80+
}
81+
82+
export {
83+
getWindowFromNode,
84+
getDocument,
85+
checkContainerType,
86+
jestFakeTimersAreEnabled,
87+
TEXT_NODE,
88+
}

src/pure.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import {
44
getQueriesForElement,
55
prettyDOM,
66
configure as configureDTL,
7-
waitFor as waitForDTL,
87
waitForElementToBeRemoved as waitForElementToBeRemovedDTL,
98
} from '@testing-library/dom'
109
import act from './act-compat'
1110
import {fireEvent} from './fire-event'
11+
import {waitFor} from './wait-for'
1212

1313
configureDTL({
1414
eventWrapper: cb => {
@@ -192,16 +192,6 @@ function cleanup() {
192192
mountedContainers.clear()
193193
}
194194

195-
function waitFor(callback, options) {
196-
return waitForDTL(() => {
197-
let result
198-
act(() => {
199-
result = callback()
200-
})
201-
return result
202-
}, options)
203-
}
204-
205195
function waitForElementToBeRemoved(callback, options) {
206196
return waitForElementToBeRemovedDTL(() => {
207197
let result

src/wait-for.js

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// @source @testing-library/dom/src/wait-for.js
2+
import {getConfig} from '@testing-library/dom'
3+
import {
4+
getWindowFromNode,
5+
getDocument,
6+
jestFakeTimersAreEnabled,
7+
// We import these from the helpers rather than using the global version
8+
// because these will be *real* timers, regardless of whether we're in
9+
// an environment that's faked the timers out.
10+
checkContainerType,
11+
} from './dtlHelpers'
12+
13+
// Not supported for external libraries. Only supported internally in @testing-library/dom
14+
function runWithExpensiveErrorDiagnosticsDisabled(callback) {
15+
return callback()
16+
}
17+
18+
// This is so the stack trace the developer sees is one that's
19+
// closer to their code (because async stack traces are hard to follow).
20+
function copyStackTrace(target, source) {
21+
target.stack = source.stack.replace(source.message, target.message)
22+
}
23+
24+
function waitFor(
25+
callback,
26+
{
27+
container = getDocument(),
28+
timeout = getConfig().asyncUtilTimeout,
29+
showOriginalStackTrace = getConfig().showOriginalStackTrace,
30+
stackTraceError,
31+
interval = 50,
32+
onTimeout = error => {
33+
error.message = getConfig().getElementError(
34+
error.message,
35+
container,
36+
).message
37+
return error
38+
},
39+
mutationObserverOptions = {
40+
subtree: true,
41+
childList: true,
42+
attributes: true,
43+
characterData: true,
44+
},
45+
},
46+
) {
47+
if (typeof callback !== 'function') {
48+
throw new TypeError('Received `callback` arg must be a function')
49+
}
50+
51+
return new Promise(async (resolve, reject) => {
52+
let lastError, intervalId, observer
53+
let finished = false
54+
let promiseStatus = 'idle'
55+
56+
const overallTimeoutTimer = setTimeout(handleTimeout, timeout)
57+
58+
const usingJestFakeTimers = jestFakeTimersAreEnabled()
59+
if (usingJestFakeTimers) {
60+
checkCallback()
61+
// this is a dangerous rule to disable because it could lead to an
62+
// infinite loop. However, eslint isn't smart enough to know that we're
63+
// setting finished inside `onDone` which will be called when we're done
64+
// waiting or when we've timed out.
65+
// eslint-disable-next-line no-unmodified-loop-condition
66+
while (!finished) {
67+
if (!jestFakeTimersAreEnabled()) {
68+
const error = new Error(
69+
`Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`,
70+
)
71+
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
72+
reject(error)
73+
return
74+
}
75+
// we *could* (maybe should?) use `advanceTimersToNextTimer` but it's
76+
// possible that could make this loop go on forever if someone is using
77+
// third party code that's setting up recursive timers so rapidly that
78+
// the user's timer's don't get a chance to resolve. So we'll advance
79+
// by an interval instead. (We have a test for this case).
80+
jest.advanceTimersByTime(interval)
81+
82+
// It's really important that checkCallback is run *before* we flush
83+
// in-flight promises. To be honest, I'm not sure why, and I can't quite
84+
// think of a way to reproduce the problem in a test, but I spent
85+
// an entire day banging my head against a wall on this.
86+
checkCallback()
87+
88+
// In this rare case, we *need* to wait for in-flight promises
89+
// to resolve before continuing. We don't need to take advantage
90+
// of parallelization so we're fine.
91+
// https://stackoverflow.com/a/59243586/971592
92+
// eslint-disable-next-line no-await-in-loop
93+
await new Promise(r => {
94+
setTimeout(r, 0)
95+
jest.advanceTimersByTime(0)
96+
})
97+
}
98+
} else {
99+
try {
100+
checkContainerType(container)
101+
} catch (e) {
102+
reject(e)
103+
return
104+
}
105+
intervalId = setInterval(checkRealTimersCallback, interval)
106+
const {MutationObserver} = getWindowFromNode(container)
107+
observer = new MutationObserver(checkRealTimersCallback)
108+
observer.observe(container, mutationObserverOptions)
109+
checkCallback()
110+
}
111+
112+
function onDone(error, result) {
113+
finished = true
114+
clearTimeout(overallTimeoutTimer)
115+
116+
if (!usingJestFakeTimers) {
117+
clearInterval(intervalId)
118+
observer.disconnect()
119+
}
120+
121+
if (error) {
122+
reject(error)
123+
} else {
124+
resolve(result)
125+
}
126+
}
127+
128+
function checkRealTimersCallback() {
129+
if (jestFakeTimersAreEnabled()) {
130+
const error = new Error(
131+
`Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`,
132+
)
133+
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
134+
return reject(error)
135+
} else {
136+
return checkCallback()
137+
}
138+
}
139+
140+
function checkCallback() {
141+
if (promiseStatus === 'pending') return
142+
try {
143+
const result = runWithExpensiveErrorDiagnosticsDisabled(callback)
144+
if (typeof result?.then === 'function') {
145+
promiseStatus = 'pending'
146+
result.then(
147+
resolvedValue => {
148+
promiseStatus = 'resolved'
149+
onDone(null, resolvedValue)
150+
},
151+
rejectedValue => {
152+
promiseStatus = 'rejected'
153+
lastError = rejectedValue
154+
},
155+
)
156+
} else {
157+
onDone(null, result)
158+
}
159+
// If `callback` throws, wait for the next mutation, interval, or timeout.
160+
} catch (error) {
161+
// Save the most recent callback error to reject the promise with it in the event of a timeout
162+
lastError = error
163+
}
164+
}
165+
166+
function handleTimeout() {
167+
let error
168+
if (lastError) {
169+
error = lastError
170+
if (
171+
!showOriginalStackTrace &&
172+
error.name === 'TestingLibraryElementError'
173+
) {
174+
copyStackTrace(error, stackTraceError)
175+
}
176+
} else {
177+
error = new Error('Timed out in waitFor.')
178+
if (!showOriginalStackTrace) {
179+
copyStackTrace(error, stackTraceError)
180+
}
181+
}
182+
onDone(onTimeout(error), null)
183+
}
184+
})
185+
}
186+
187+
function waitForWrapper(callback, options) {
188+
// create the error here so its stack trace is as close to the
189+
// calling code as possible
190+
const stackTraceError = new Error('STACK_TRACE_MESSAGE')
191+
return getConfig().asyncWrapper(() =>
192+
waitFor(callback, {stackTraceError, ...options}),
193+
)
194+
}
195+
196+
export {waitForWrapper as waitFor}
197+
198+
/*
199+
eslint
200+
max-lines-per-function: ["error", {"max": 200}],
201+
*/

0 commit comments

Comments
 (0)