-
Notifications
You must be signed in to change notification settings - Fork 466
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(waitFor): add complete and transparent support for fake timers
Closes #661
- Loading branch information
1 parent
260e1e8
commit 9aab321
Showing
10 changed files
with
167 additions
and
445 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
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,30 @@ | ||
import {waitForElement, waitForDomChange, wait} from '..' | ||
|
||
afterEach(() => { | ||
console.warn.mockClear() | ||
}) | ||
|
||
test('deprecation warnings only warn once', async () => { | ||
await wait(() => {}, {timeout: 1}) | ||
await waitForElement(() => {}, {timeout: 1}).catch(e => e) | ||
await waitForDomChange({timeout: 1}).catch(e => e) | ||
expect(console.warn.mock.calls).toMatchInlineSnapshot(` | ||
Array [ | ||
Array [ | ||
"\`wait\` has been deprecated and replaced by \`waitFor\` instead. In most cases you should be able to find/replace \`wait\` with \`waitFor\`. Learn more: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.", | ||
], | ||
Array [ | ||
"\`waitForElement\` has been deprecated. Use a \`find*\` query (preferred: https://testing-library.com/docs/dom-testing-library/api-queries#findby) or use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor", | ||
], | ||
Array [ | ||
"\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.", | ||
], | ||
] | ||
`) | ||
|
||
console.warn.mockClear() | ||
await wait(() => {}, {timeout: 1}) | ||
await waitForElement(() => {}, {timeout: 1}).catch(e => e) | ||
await waitForDomChange({timeout: 1}).catch(e => e) | ||
expect(console.warn).not.toHaveBeenCalled() | ||
}) |
This file was deleted.
Oops, something went wrong.
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,139 +1,66 @@ | ||
import {waitFor, waitForElementToBeRemoved} from '..' | ||
import {render} from './helpers/test-utils' | ||
|
||
// Because we're using fake timers here and I don't want these tests to run | ||
// for the actual length of the test (because it's waiting for a timeout error) | ||
// we'll mock the setTimeout, clearTimeout, and setImmediate to be the ones | ||
// that jest will mock for us. | ||
jest.mock('../helpers', () => { | ||
const actualHelpers = jest.requireActual('../helpers') | ||
return { | ||
...actualHelpers, | ||
setTimeout, | ||
clearTimeout, | ||
setImmediate, | ||
} | ||
beforeAll(() => { | ||
jest.useFakeTimers() | ||
}) | ||
|
||
jest.useFakeTimers() | ||
|
||
// Because of the way jest mocking works here's the order of things (and no, the order of the code above doesn't make a difference): | ||
// 1. Just mocks '../helpers' and setTimeout/clearTimeout/setImmediate are set to their "correct" values | ||
// 2. We tell Jest to use fake timers | ||
// 3. We reset the modules and we mock '../helpers' again so now setTimeout/clearTimeout/setImmediate are set to their mocked values | ||
// We're only doing this because want to mock those values so this test doesn't take 4501ms to run. | ||
jest.resetModules() | ||
|
||
const { | ||
wait, | ||
waitForElement, | ||
waitForDomChange, | ||
waitForElementToBeRemoved, | ||
} = require('../') | ||
|
||
test('waitForElementToBeRemoved: times out after 4500ms by default', () => { | ||
const {container} = render(`<div></div>`) | ||
// there's a bug with this rule here... | ||
// eslint-disable-next-line jest/valid-expect | ||
const promise = expect( | ||
waitForElementToBeRemoved(() => container), | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Timed out in waitForElementToBeRemoved."`, | ||
) | ||
jest.advanceTimersByTime(4501) | ||
return promise | ||
afterAll(() => { | ||
jest.useRealTimers() | ||
}) | ||
|
||
test('wait: can time out', async () => { | ||
const promise = wait(() => { | ||
// eslint-disable-next-line no-throw-literal | ||
throw undefined | ||
}) | ||
jest.advanceTimersByTime(4600) | ||
await expect(promise).rejects.toThrow(/timed out/i) | ||
}) | ||
|
||
test('waitForElement: can time out', async () => { | ||
const promise = waitForElement(() => {}) | ||
jest.advanceTimersByTime(4600) | ||
await expect(promise).rejects.toThrow(/timed out/i) | ||
}) | ||
async function runWaitFor() { | ||
const response = 'data' | ||
const doAsyncThing = () => | ||
new Promise(r => setTimeout(() => r(response), 300)) | ||
let result | ||
doAsyncThing().then(r => (result = r)) | ||
|
||
test('waitForElement: can specify our own timeout time', async () => { | ||
const promise = waitForElement(() => {}, {timeout: 4700}) | ||
const handler = jest.fn() | ||
promise.then(handler, handler) | ||
// advance beyond the default | ||
jest.advanceTimersByTime(4600) | ||
// promise was neither rejected nor resolved | ||
expect(handler).toHaveBeenCalledTimes(0) | ||
await waitFor(() => expect(result).toBe(response)) | ||
} | ||
|
||
// advance beyond our specified timeout | ||
jest.advanceTimersByTime(150) | ||
|
||
// timed out | ||
await expect(promise).rejects.toThrow(/timed out/i) | ||
test('real timers', async () => { | ||
// the only difference when not using fake timers is this test will | ||
// have to wait the full length of the timeout | ||
await runWaitFor() | ||
}) | ||
|
||
test('waitForDomChange: can time out', async () => { | ||
const promise = waitForDomChange() | ||
jest.advanceTimersByTime(4600) | ||
await expect(promise).rejects.toThrow(/timed out/i) | ||
test('legacy', async () => { | ||
jest.useFakeTimers('legacy') | ||
await runWaitFor() | ||
}) | ||
|
||
test('waitForDomChange: can specify our own timeout time', async () => { | ||
const promise = waitForDomChange({timeout: 4700}) | ||
const handler = jest.fn() | ||
promise.then(handler, handler) | ||
// advance beyond the default | ||
jest.advanceTimersByTime(4600) | ||
// promise was neither rejected nor resolved | ||
expect(handler).toHaveBeenCalledTimes(0) | ||
|
||
// advance beyond our specified timeout | ||
jest.advanceTimersByTime(150) | ||
|
||
// timed out | ||
await expect(promise).rejects.toThrow(/timed out/i) | ||
test('modern', async () => { | ||
jest.useFakeTimers() | ||
await runWaitFor() | ||
}) | ||
|
||
test('wait: ensures the interval is greater than 0', async () => { | ||
// Arrange | ||
const spy = jest.fn() | ||
spy.mockImplementationOnce(() => { | ||
throw new Error('first time does not work') | ||
}) | ||
const promise = wait(spy, {interval: 0}) | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
spy.mockClear() | ||
|
||
// Act | ||
// this line will throw an error if wait does not make the interval 1 instead of 0 | ||
// which is why it does that! | ||
jest.advanceTimersByTime(0) | ||
|
||
// Assert | ||
expect(spy).toHaveBeenCalledTimes(0) | ||
spy.mockImplementationOnce(() => 'second time does work') | ||
|
||
// Act | ||
jest.advanceTimersByTime(1) | ||
await promise | ||
|
||
// Assert | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
test('fake timer timeout', async () => { | ||
jest.useFakeTimers() | ||
await expect( | ||
waitFor( | ||
() => { | ||
throw new Error('always throws') | ||
}, | ||
{timeout: 10}, | ||
), | ||
).rejects.toMatchInlineSnapshot(`[Error: always throws]`) | ||
}) | ||
|
||
test('wait: times out if it runs out of attempts', () => { | ||
const spy = jest.fn(() => { | ||
throw new Error('example error') | ||
}) | ||
test('times out after 1000ms by default', async () => { | ||
const {container} = render(`<div></div>`) | ||
const start = performance.now() | ||
// there's a bug with this rule here... | ||
// eslint-disable-next-line jest/valid-expect | ||
const promise = expect( | ||
wait(spy, {interval: 1, timeout: 3}), | ||
).rejects.toThrowErrorMatchingInlineSnapshot(`"example error"`) | ||
jest.advanceTimersByTime(1) | ||
jest.advanceTimersByTime(1) | ||
jest.advanceTimersByTime(1) | ||
return promise | ||
await expect( | ||
waitForElementToBeRemoved(() => container), | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Timed out in waitForElementToBeRemoved."`, | ||
) | ||
// NOTE: this assertion ensures that even when we have fake timers, the | ||
// timeout still takes the full 1000ms | ||
// unfortunately, timeout clocks aren't super accurate, so we simply verify | ||
// that it's greater than or equal to 900ms. That's enough to be confident | ||
// that we're using real timers. | ||
expect(performance.now() - start).toBeGreaterThanOrEqual(900) | ||
}) |
Oops, something went wrong.