-
Notifications
You must be signed in to change notification settings - Fork 471
fix: Stop calling waitFor
callback after timeout
#1271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a0bb005
6b41483
55ff2f2
67793f8
86c5c08
ce11c0f
10a1ac7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -292,12 +292,11 @@ test('the real timers => fake timers error shows the original stack trace when c | |||||
|
||||||
test('does not work after it resolves', async () => { | ||||||
jest.useFakeTimers('modern') | ||||||
let context = 'initial' | ||||||
const contextStack = [] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or
Suggested change
|
||||||
configure({ | ||||||
// @testing-library/react usage to ensure `IS_REACT_ACT_ENVIRONMENT` is set when acting. | ||||||
unstable_advanceTimersWrapper: callback => { | ||||||
const originalContext = context | ||||||
context = 'act' | ||||||
contextStack.push('act:start') | ||||||
try { | ||||||
const result = callback() | ||||||
// eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive | ||||||
|
@@ -307,54 +306,144 @@ test('does not work after it resolves', async () => { | |||||
then: (resolve, reject) => { | ||||||
thenable.then( | ||||||
returnValue => { | ||||||
context = originalContext | ||||||
contextStack.push('act:end') | ||||||
resolve(returnValue) | ||||||
}, | ||||||
error => { | ||||||
context = originalContext | ||||||
contextStack.push('act:end') | ||||||
reject(error) | ||||||
}, | ||||||
) | ||||||
}, | ||||||
} | ||||||
} else { | ||||||
context = originalContext | ||||||
contextStack.push('act:end') | ||||||
return undefined | ||||||
} | ||||||
} catch { | ||||||
context = originalContext | ||||||
contextStack.push('act:end') | ||||||
return undefined | ||||||
} | ||||||
}, | ||||||
asyncWrapper: async callback => { | ||||||
const originalContext = context | ||||||
context = 'no-act' | ||||||
contextStack.push('no-act:start') | ||||||
try { | ||||||
await callback() | ||||||
} finally { | ||||||
context = originalContext | ||||||
contextStack.push('no-act:end') | ||||||
} | ||||||
}, | ||||||
}) | ||||||
|
||||||
let data = null | ||||||
let timeoutResolved = false | ||||||
setTimeout(() => { | ||||||
data = 'resolved' | ||||||
contextStack.push('timeout') | ||||||
timeoutResolved = true | ||||||
}, 100) | ||||||
|
||||||
contextStack.push('waitFor:start') | ||||||
await waitFor( | ||||||
() => { | ||||||
contextStack.push('callback') | ||||||
// eslint-disable-next-line jest/no-conditional-in-test -- false-positive | ||||||
if (data === null) { | ||||||
if (!timeoutResolved) { | ||||||
throw new Error('not found') | ||||||
} | ||||||
}, | ||||||
{interval: 50}, | ||||||
) | ||||||
|
||||||
expect(context).toEqual('initial') | ||||||
contextStack.push('waitFor:end') | ||||||
|
||||||
expect(contextStack).toMatchInlineSnapshot(` | ||||||
[ | ||||||
waitFor:start, | ||||||
no-act:start, | ||||||
callback, | ||||||
act:start, | ||||||
act:end, | ||||||
callback, | ||||||
act:start, | ||||||
timeout, | ||||||
act:end, | ||||||
callback, | ||||||
no-act:end, | ||||||
waitFor:end, | ||||||
] | ||||||
`) | ||||||
|
||||||
await Promise.resolve() | ||||||
|
||||||
expect(context).toEqual('initial') | ||||||
// The context call stack should not change | ||||||
expect(contextStack).toMatchInlineSnapshot(` | ||||||
[ | ||||||
waitFor:start, | ||||||
no-act:start, | ||||||
callback, | ||||||
act:start, | ||||||
act:end, | ||||||
callback, | ||||||
act:start, | ||||||
timeout, | ||||||
act:end, | ||||||
callback, | ||||||
no-act:end, | ||||||
waitFor:end, | ||||||
] | ||||||
`) | ||||||
}) | ||||||
|
||||||
test(`when fake timer is installed, on waitFor timeout, it doesn't call the callback afterward`, async () => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test ensures that the fix is working |
||||||
jest.useFakeTimers('modern') | ||||||
|
||||||
configure({ | ||||||
// @testing-library/react usage to ensure `IS_REACT_ACT_ENVIRONMENT` is set when acting. | ||||||
unstable_advanceTimersWrapper: callback => { | ||||||
try { | ||||||
const result = callback() | ||||||
// eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive | ||||||
if (typeof result?.then === 'function') { | ||||||
const thenable = result | ||||||
return { | ||||||
then: (resolve, reject) => { | ||||||
thenable.then( | ||||||
returnValue => { | ||||||
resolve(returnValue) | ||||||
}, | ||||||
error => { | ||||||
reject(error) | ||||||
}, | ||||||
) | ||||||
}, | ||||||
} | ||||||
} else { | ||||||
return undefined | ||||||
} | ||||||
} catch { | ||||||
return undefined | ||||||
} | ||||||
}, | ||||||
asyncWrapper: async callback => { | ||||||
try { | ||||||
await callback() | ||||||
} finally { | ||||||
/* eslint no-empty: "off" */ | ||||||
} | ||||||
}, | ||||||
}) | ||||||
|
||||||
const callback = jest.fn(() => { | ||||||
throw new Error('We want to timeout') | ||||||
}) | ||||||
const interval = 50 | ||||||
|
||||||
await expect(() => | ||||||
waitFor(callback, {interval, timeout: 100}), | ||||||
).rejects.toThrow() | ||||||
expect(callback).toHaveBeenCalledWith() | ||||||
|
||||||
callback.mockClear() | ||||||
|
||||||
await jest.advanceTimersByTimeAsync(interval) | ||||||
|
||||||
expect(callback).not.toHaveBeenCalledWith() | ||||||
}) |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -81,15 +81,15 @@ function waitFor( | |||
jest.advanceTimersByTime(interval) | ||||
}) | ||||
|
||||
// Could have timed-out | ||||
if (finished) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this factoring, I wonder if we could simplify further by moving At this point I would remove the "It's really important that checkCallback [...]" comment. We might have fixed the issue along the way. And since I'd rather ship this with the next major, we have a good opportunity to resurface the bug so that we can at least link a repro. It doesn't have to be a Jest test but somehow the reordering fixed "something". That "something" needs to be linked a t least. Doesn't need to be an automated test but it needs to be something. Otherwise we're chasing ghosts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we move Which will break this fix: https://github.com/testing-library/dom-testing-library/pull/1073/files There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
but not if we remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There will be a change of behavior in
timeout smaller than the interval , it will instead be rejected with [Error: Timed out in waitFor.]
|
||||
break | ||||
} | ||||
// It's really important that checkCallback is run *before* we flush | ||||
// in-flight promises. To be honest, I'm not sure why, and I can't quite | ||||
// think of a way to reproduce the problem in a test, but I spent | ||||
// an entire day banging my head against a wall on this. | ||||
checkCallback() | ||||
|
||||
if (finished) { | ||||
break | ||||
} | ||||
} | ||||
} else { | ||||
try { | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made some changes on this test to help understand the outcome: everything that starts needs to end