Skip to content

Commit d3287a1

Browse files
feat(waitFor): improve error stack traces for async errors (#542)
1 parent 7afa997 commit d3287a1

File tree

3 files changed

+87
-7
lines changed

3 files changed

+87
-7
lines changed

src/__tests__/wait-for.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ test('can timeout after the given timeout time', async () => {
2323
expect(result).toBe(error)
2424
})
2525

26-
test('uses generic error if there was no last error', async () => {
26+
test('if no error is thrown then throws a timeout error', async () => {
2727
const result = await waitFor(
2828
() => {
2929
// eslint-disable-next-line no-throw-literal
@@ -34,6 +34,58 @@ test('uses generic error if there was no last error', async () => {
3434
expect(result).toMatchInlineSnapshot(`[Error: Timed out in waitFor.]`)
3535
})
3636

37+
test('if showOriginalStackTrace on a timeout error then the stack trace does not include this file', async () => {
38+
const result = await waitFor(
39+
() => {
40+
// eslint-disable-next-line no-throw-literal
41+
throw undefined
42+
},
43+
{timeout: 8, interval: 5, showOriginalStackTrace: true},
44+
).catch(e => e)
45+
expect(result.stack).not.toMatch(__dirname)
46+
})
47+
48+
test('uses full stack error trace when showOriginalStackTrace present', async () => {
49+
const error = new Error('Throws the full stack trace')
50+
// even if the error is a TestingLibraryElementError
51+
error.name = 'TestingLibraryElementError'
52+
const originalStackTrace = error.stack
53+
const result = await waitFor(
54+
() => {
55+
throw error
56+
},
57+
{timeout: 8, interval: 5, showOriginalStackTrace: true},
58+
).catch(e => e)
59+
expect(result.stack).toBe(originalStackTrace)
60+
})
61+
62+
test('does not change the stack trace if the thrown error is not a TestingLibraryElementError', async () => {
63+
const error = new Error('Throws the full stack trace')
64+
const originalStackTrace = error.stack
65+
const result = await waitFor(
66+
() => {
67+
throw error
68+
},
69+
{timeout: 8, interval: 5},
70+
).catch(e => e)
71+
expect(result.stack).toBe(originalStackTrace)
72+
})
73+
74+
test('provides an improved stack trace if the thrown error is a TestingLibraryElementError', async () => {
75+
const error = new Error('Throws the full stack trace')
76+
error.name = 'TestingLibraryElementError'
77+
const originalStackTrace = error.stack
78+
const result = await waitFor(
79+
() => {
80+
throw error
81+
},
82+
{timeout: 8, interval: 5},
83+
).catch(e => e)
84+
// too hard to test that the stack trace is what we want it to be
85+
// so we'll just make sure that it's not the same as the origianl
86+
expect(result.stack).not.toBe(originalStackTrace)
87+
})
88+
3789
test('throws nice error if provided callback is not a function', () => {
3890
const {queryByTestId} = renderIntoDocument(`
3991
<div data-testid="div"></div>

src/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ let config = {
1616
asyncWrapper: cb => cb(),
1717
// default value for the `hidden` option in `ByRole` queries
1818
defaultHidden: false,
19+
//showOriginalStackTrace flag to show the full error stack traces for async errors
20+
showOriginalStackTrace: false,
1921

2022
// called when getBy* queries fail. (message, container) => Error
2123
getElementError(message, container) {

src/wait-for.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,32 @@ import {
88
} from './helpers'
99
import {getConfig} from './config'
1010

11+
// This is so the stack trace the developer sees is one that's
12+
// closer to their code (because async stack traces are hard to follow).
13+
function copyStackTrace(target, source) {
14+
target.stack = source.stack.replace(source.message, target.message)
15+
}
16+
1117
function waitFor(
1218
callback,
1319
{
1420
container = getDocument(),
1521
timeout = getConfig().asyncUtilTimeout,
22+
showOriginalStackTrace = getConfig().showOriginalStackTrace,
23+
stackTraceError,
1624
interval = 50,
1725
mutationObserverOptions = {
1826
subtree: true,
1927
childList: true,
2028
attributes: true,
2129
characterData: true,
2230
},
23-
} = {},
31+
},
2432
) {
2533
if (typeof callback !== 'function') {
2634
throw new TypeError('Received `callback` arg must be a function')
2735
}
2836

29-
// created here so we get a nice stacktrace
30-
const timedOutError = new Error('Timed out in waitFor.')
3137
if (interval < 1) interval = 1
3238
return new Promise((resolve, reject) => {
3339
let lastError
@@ -63,13 +69,33 @@ function waitFor(
6369
}
6470

6571
function onTimeout() {
66-
onDone(lastError || timedOutError, null)
72+
let error
73+
if (lastError) {
74+
error = lastError
75+
if (
76+
!showOriginalStackTrace &&
77+
error.name === 'TestingLibraryElementError'
78+
) {
79+
copyStackTrace(error, stackTraceError)
80+
}
81+
} else {
82+
error = new Error('Timed out in waitFor.')
83+
if (!showOriginalStackTrace) {
84+
copyStackTrace(error, stackTraceError)
85+
}
86+
}
87+
onDone(error, null)
6788
}
6889
})
6990
}
7091

71-
function waitForWrapper(...args) {
72-
return getConfig().asyncWrapper(() => waitFor(...args))
92+
function waitForWrapper(callback, options) {
93+
// create the error here so its stack trace is as close to the
94+
// calling code as possible
95+
const stackTraceError = new Error('STACK_TRACE_MESSAGE')
96+
return getConfig().asyncWrapper(() =>
97+
waitFor(callback, {stackTraceError, ...options}),
98+
)
7399
}
74100

75101
let hasWarned = false

0 commit comments

Comments
 (0)