-
Notifications
You must be signed in to change notification settings - Fork 273
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
fix(breaking): use real timers internally to fix awaiting with fake timers #568
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3e9e373
capture global timers, use them in waitFor, tests+
mikeduminy b88aa2b
use real timers in flushMicroTasks
mikeduminy 08351f7
test to show changes with modern timer mocks
mikeduminy f45177e
use enum-like object for timer modes
mikeduminy c433020
fix tests
mikeduminy 1928ec5
remove default timer mode, add flow types
mikeduminy 90b6115
base waitFor on RTL implementation
mikeduminy ba2d439
[jest] Capture and restore global promise
mikeduminy 84280fb
[waitFor] Simulate intervals when using fake timers
mikeduminy 5086038
[tests] Fix waitForElementToBeRemoved
mikeduminy 1567942
cleanups; use more specific global
thymikee 8c88d6a
rename globals to match others
thymikee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const reactNativePreset = require('react-native/jest-preset'); | ||
|
||
module.exports = { | ||
...reactNativePreset, | ||
// this is needed to make modern fake timers work | ||
// because the react-native preset overrides global.Promise | ||
setupFiles: [require.resolve('./save-promise.js')] | ||
.concat(reactNativePreset.setupFiles) | ||
.concat([require.resolve('./restore-promise.js')]), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
global.Promise = global.RNTL_ORIGINAL_PROMISE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
global.RNTL_ORIGINAL_PROMISE = Promise; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// @flow | ||
|
||
import { setTimeout } from '../helpers/timers'; | ||
|
||
const TimerMode = { | ||
Legacy: 'legacy', | ||
Modern: 'modern', // broken for now | ||
}; | ||
|
||
async function sleep(ms: number): Promise<void> { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
export { TimerMode, sleep }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// @flow | ||
import waitFor from '../waitFor'; | ||
import { TimerMode } from './timerUtils'; | ||
|
||
describe.each([TimerMode.Legacy, TimerMode.Modern])( | ||
'%s fake timers tests', | ||
(fakeTimerType) => { | ||
beforeEach(() => { | ||
jest.useFakeTimers(fakeTimerType); | ||
}); | ||
|
||
test('it successfully runs tests', () => { | ||
expect(true).toBeTruthy(); | ||
}); | ||
|
||
test('it successfully uses waitFor', async () => { | ||
await waitFor(() => { | ||
expect(true).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
test('it successfully uses waitFor with real timers', async () => { | ||
jest.useRealTimers(); | ||
await waitFor(() => { | ||
expect(true).toBeTruthy(); | ||
}); | ||
}); | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
// @flow | ||
import * as React from 'react'; | ||
import { View, Text, TouchableOpacity } from 'react-native'; | ||
import { render, fireEvent, waitFor } from '..'; | ||
import { Text, TouchableOpacity, View } from 'react-native'; | ||
import { fireEvent, render, waitFor } from '..'; | ||
import { TimerMode } from './timerUtils'; | ||
|
||
class Banana extends React.Component<any> { | ||
changeFresh = () => { | ||
|
@@ -76,39 +77,63 @@ test('waits for element with custom interval', async () => { | |
// suppress | ||
} | ||
|
||
expect(mockFn).toHaveBeenCalledTimes(3); | ||
expect(mockFn).toHaveBeenCalledTimes(2); | ||
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 is a change in behavior |
||
}); | ||
|
||
test('works with legacy fake timers', async () => { | ||
jest.useFakeTimers('legacy'); | ||
test.each([TimerMode.Legacy, TimerMode.Modern])( | ||
'waits for element until it stops throwing using %s fake timers', | ||
async (fakeTimerType) => { | ||
jest.useFakeTimers(fakeTimerType); | ||
const { getByText, queryByText } = render(<BananaContainer />); | ||
|
||
const mockFn = jest.fn(() => { | ||
throw Error('test'); | ||
}); | ||
fireEvent.press(getByText('Change freshness!')); | ||
expect(queryByText('Fresh')).toBeNull(); | ||
|
||
try { | ||
waitFor(() => mockFn(), { timeout: 400, interval: 200 }); | ||
} catch (e) { | ||
// suppress | ||
jest.advanceTimersByTime(300); | ||
const freshBananaText = await waitFor(() => getByText('Fresh')); | ||
|
||
expect(freshBananaText.props.children).toBe('Fresh'); | ||
} | ||
jest.advanceTimersByTime(400); | ||
); | ||
|
||
expect(mockFn).toHaveBeenCalledTimes(3); | ||
}); | ||
test.each([TimerMode.Legacy, TimerMode.Modern])( | ||
'waits for assertion until timeout is met with %s fake timers', | ||
async (fakeTimerType) => { | ||
jest.useFakeTimers(fakeTimerType); | ||
|
||
test('works with fake timers', async () => { | ||
jest.useFakeTimers('modern'); | ||
const mockFn = jest.fn(() => { | ||
throw Error('test'); | ||
}); | ||
|
||
const mockFn = jest.fn(() => { | ||
throw Error('test'); | ||
}); | ||
try { | ||
await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); | ||
} catch (error) { | ||
// suppress | ||
} | ||
|
||
try { | ||
waitFor(() => mockFn(), { timeout: 400, interval: 200 }); | ||
} catch (e) { | ||
// suppress | ||
expect(mockFn).toHaveBeenCalledTimes(3); | ||
} | ||
jest.advanceTimersByTime(400); | ||
|
||
expect(mockFn).toHaveBeenCalledTimes(3); | ||
}); | ||
); | ||
|
||
test.each([TimerMode.Legacy, TimerMode.Legacy])( | ||
'awaiting something that succeeds before timeout works with %s fake timers', | ||
async (fakeTimerType) => { | ||
jest.useFakeTimers(fakeTimerType); | ||
|
||
let calls = 0; | ||
const mockFn = jest.fn(() => { | ||
calls += 1; | ||
if (calls < 3) { | ||
throw Error('test'); | ||
} | ||
}); | ||
|
||
try { | ||
await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); | ||
} catch (error) { | ||
// suppress | ||
} | ||
|
||
expect(mockFn).toHaveBeenCalledTimes(3); | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// Most content of this file sourced directly from https://github.com/testing-library/dom-testing-library/blob/master/src/helpers.js | ||
// @flow | ||
/* globals jest */ | ||
|
||
const globalObj = typeof window === 'undefined' ? global : window; | ||
|
||
// Currently this fn only supports jest timers, but it could support other test runners in the future. | ||
function runWithRealTimers<T>(callback: () => T): T { | ||
const fakeTimersType = getJestFakeTimersType(); | ||
if (fakeTimersType) { | ||
jest.useRealTimers(); | ||
} | ||
|
||
const callbackReturnValue = callback(); | ||
|
||
if (fakeTimersType) { | ||
jest.useFakeTimers(fakeTimersType); | ||
} | ||
|
||
return callbackReturnValue; | ||
} | ||
|
||
function getJestFakeTimersType() { | ||
// istanbul ignore if | ||
if ( | ||
typeof jest === 'undefined' || | ||
typeof globalObj.setTimeout === 'undefined' | ||
) { | ||
return null; | ||
} | ||
|
||
if ( | ||
typeof globalObj.setTimeout._isMockFunction !== 'undefined' && | ||
globalObj.setTimeout._isMockFunction | ||
) { | ||
return 'legacy'; | ||
} | ||
|
||
if ( | ||
typeof globalObj.setTimeout.clock !== 'undefined' && | ||
// $FlowIgnore[prop-missing] | ||
typeof jest.getRealSystemTime !== 'undefined' | ||
) { | ||
try { | ||
// jest.getRealSystemTime is only supported for Jest's `modern` fake timers and otherwise throws | ||
// $FlowExpectedError | ||
jest.getRealSystemTime(); | ||
return 'modern'; | ||
} catch { | ||
// not using Jest's modern fake timers | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
const jestFakeTimersAreEnabled = (): boolean => | ||
Boolean(getJestFakeTimersType()); | ||
|
||
// we only run our tests in node, and setImmediate is supported in node. | ||
function setImmediatePolyfill(fn) { | ||
return globalObj.setTimeout(fn, 0); | ||
} | ||
|
||
type BindTimeFunctions = { | ||
clearTimeoutFn: typeof clearTimeout, | ||
setImmediateFn: typeof setImmediate, | ||
setTimeoutFn: typeof setTimeout, | ||
}; | ||
|
||
function bindTimeFunctions(): BindTimeFunctions { | ||
return { | ||
clearTimeoutFn: globalObj.clearTimeout, | ||
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill, | ||
setTimeoutFn: globalObj.setTimeout, | ||
}; | ||
} | ||
|
||
const { clearTimeoutFn, setImmediateFn, setTimeoutFn } = (runWithRealTimers( | ||
bindTimeFunctions | ||
): BindTimeFunctions); | ||
|
||
export { | ||
runWithRealTimers, | ||
jestFakeTimersAreEnabled, | ||
clearTimeoutFn as clearTimeout, | ||
setImmediateFn as setImmediate, | ||
setTimeoutFn as setTimeout, | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Is this being exposed as a jest preset of
@testing-library/react-native
or is it just used locally for the tests in this codebase? If exposed, it should be documented somewhereThere 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.
Yup, will do in a followup