Skip to content

Commit 9b1725e

Browse files
Kent C. Doddskentcdodds
Kent C. Dodds
authored andcommitted
feat(wait): wait will now also run your callback on DOM changes (#415)
Closes #376 Closes #416 BREAKING CHANGE: `waitForElement` is deprecated in favor of `find*` queries or `wait`. BREAKING CHANGE: `waitForDomChange` is deprecated in favor of `wait` BREAKING CHANGE: default timeout for async utilities is now 1000ms rather than 4500ms. This can be configured: https://testing-library.com/docs/dom-testing-library/api-configuration
1 parent 8bcffe0 commit 9b1725e

17 files changed

+248
-161
lines changed

Diff for: package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@
4545
"@types/testing-library__dom": "^6.12.1",
4646
"aria-query": "^4.0.2",
4747
"dom-accessibility-api": "^0.3.0",
48-
"pretty-format": "^25.1.0",
49-
"wait-for-expect": "^3.0.2"
48+
"pretty-format": "^25.1.0"
5049
},
5150
"devDependencies": {
5251
"@testing-library/jest-dom": "^5.1.1",
@@ -61,7 +60,8 @@
6160
"rules": {
6261
"import/prefer-default-export": "off",
6362
"import/no-unassigned-import": "off",
64-
"import/no-useless-path-segments": "off"
63+
"import/no-useless-path-segments": "off",
64+
"no-console": "off"
6565
}
6666
},
6767
"eslintIgnore": [

Diff for: src/__tests__/fake-timers.js

+52
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jest.useFakeTimers()
2424
jest.resetModules()
2525

2626
const {
27+
wait,
2728
waitForElement,
2829
waitForDomChange,
2930
waitForElementToBeRemoved,
@@ -42,6 +43,15 @@ test('waitForElementToBeRemoved: times out after 4500ms by default', () => {
4243
return promise
4344
})
4445

46+
test('wait: can time out', async () => {
47+
const promise = wait(() => {
48+
// eslint-disable-next-line no-throw-literal
49+
throw undefined
50+
})
51+
jest.advanceTimersByTime(4600)
52+
await expect(promise).rejects.toThrow(/timed out/i)
53+
})
54+
4555
test('waitForElement: can time out', async () => {
4656
const promise = waitForElement(() => {})
4757
jest.advanceTimersByTime(4600)
@@ -85,3 +95,45 @@ test('waitForDomChange: can specify our own timeout time', async () => {
8595
// timed out
8696
await expect(promise).rejects.toThrow(/timed out/i)
8797
})
98+
99+
test('wait: ensures the interval is greater than 0', async () => {
100+
// Arrange
101+
const spy = jest.fn()
102+
spy.mockImplementationOnce(() => {
103+
throw new Error('first time does not work')
104+
})
105+
const promise = wait(spy, {interval: 0})
106+
expect(spy).toHaveBeenCalledTimes(1)
107+
spy.mockClear()
108+
109+
// Act
110+
// this line will throw an error if wait does not make the interval 1 instead of 0
111+
// which is why it does that!
112+
jest.advanceTimersByTime(0)
113+
114+
// Assert
115+
expect(spy).toHaveBeenCalledTimes(0)
116+
spy.mockImplementationOnce(() => 'second time does work')
117+
118+
// Act
119+
jest.advanceTimersByTime(1)
120+
await promise
121+
122+
// Assert
123+
expect(spy).toHaveBeenCalledTimes(1)
124+
})
125+
126+
test('wait: times out if it runs out of attempts', () => {
127+
const spy = jest.fn(() => {
128+
throw new Error('example error')
129+
})
130+
// there's a bug with this rule here...
131+
// eslint-disable-next-line jest/valid-expect
132+
const promise = expect(
133+
wait(spy, {interval: 1, timeout: 3}),
134+
).rejects.toThrowErrorMatchingInlineSnapshot(`"example error"`)
135+
jest.advanceTimersByTime(1)
136+
jest.advanceTimersByTime(1)
137+
jest.advanceTimersByTime(1)
138+
return promise
139+
})

Diff for: src/__tests__/pretty-dom.js

-2
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,3 @@ describe('prettyDOM fails with first parameter without outerHTML field', () => {
7777
)
7878
})
7979
})
80-
81-
/* eslint no-console:0 */

Diff for: src/__tests__/role-helpers.js

-2
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,3 @@ test.each([
184184

185185
expect(isInaccessible(container.querySelector('button'))).toBe(expected)
186186
})
187-
188-
/* eslint no-console:0 */

Diff for: src/__tests__/screen.js

-2
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,3 @@ test('exposes debug method', () => {
6161
`)
6262
console.log.mockClear()
6363
})
64-
65-
/* eslint no-console:0 */

Diff for: src/__tests__/wait-for-dom-change.js

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ beforeEach(() => {
1010
jest.useRealTimers()
1111
jest.resetModules()
1212
waitForDomChange = importModule()
13+
console.warn.mockClear()
1314
})
1415

1516
test('waits for the dom to change in the document', async () => {
@@ -34,6 +35,13 @@ test('waits for the dom to change in the document', async () => {
3435
},
3536
]
3637
`)
38+
expect(console.warn.mock.calls).toMatchInlineSnapshot(`
39+
Array [
40+
Array [
41+
"\`waitForDomChange\` has been deprecated. Use \`wait\` instead: https://testing-library.com/docs/dom-testing-library/api-async#wait.",
42+
],
43+
]
44+
`)
3745
})
3846

3947
test('waits for the dom to change in a specified container', async () => {

Diff for: src/__tests__/wait-for-element-to-be-removed.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,23 @@ test('requires a function as the first parameter', () => {
4949
return expect(
5050
waitForElementToBeRemoved(),
5151
).rejects.toThrowErrorMatchingInlineSnapshot(
52-
`"waitForElementToBeRemoved requires a function as the first parameter"`,
52+
`"waitForElementToBeRemoved requires a callback as the first parameter"`,
5353
)
5454
})
5555

5656
test('requires an element to exist first', () => {
5757
return expect(
5858
waitForElementToBeRemoved(() => null),
5959
).rejects.toThrowErrorMatchingInlineSnapshot(
60-
`"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal."`,
60+
`"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal."`,
6161
)
6262
})
6363

6464
test('requires an unempty array of elements to exist first', () => {
6565
return expect(
6666
waitForElementToBeRemoved(() => []),
6767
).rejects.toThrowErrorMatchingInlineSnapshot(
68-
`"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal."`,
68+
`"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal."`,
6969
)
7070
})
7171

@@ -117,3 +117,18 @@ test("doesn't change jest's timers value when importing the module", () => {
117117

118118
expect(window.setTimeout._isMockFunction).toEqual(true)
119119
})
120+
121+
test('rethrows non-testing-lib errors', () => {
122+
let throwIt = false
123+
const div = document.createElement('div')
124+
const error = new Error('my own error')
125+
return expect(
126+
waitForElementToBeRemoved(() => {
127+
if (throwIt) {
128+
throw error
129+
}
130+
throwIt = true
131+
return div
132+
}),
133+
).rejects.toBe(error)
134+
})

Diff for: src/__tests__/wait-for-element.js

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ beforeEach(() => {
1010
jest.useRealTimers()
1111
jest.resetModules()
1212
waitForElement = importModule()
13+
console.warn.mockClear()
1314
})
1415

1516
test('waits for element to appear in the document', async () => {
@@ -18,6 +19,13 @@ test('waits for element to appear in the document', async () => {
1819
setTimeout(() => rerender('<div data-testid="div" />'))
1920
const element = await promise
2021
expect(element).toBeInTheDocument()
22+
expect(console.warn.mock.calls).toMatchInlineSnapshot(`
23+
Array [
24+
Array [
25+
"\`waitForElement\` has been deprecated. Use a \`find*\` query (preferred: https://testing-library.com/docs/dom-testing-library/api-queries#findby) or use \`wait\` instead (it's the same API, so you can find/replace): https://testing-library.com/docs/dom-testing-library/api-async#wait",
26+
],
27+
]
28+
`)
2129
})
2230

2331
test('waits for element to appear in a specified container', async () => {

Diff for: src/__tests__/wait.js

+11
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,14 @@ test('wait defaults to a noop callback', async () => {
1717
await wait()
1818
expect(handler).toHaveBeenCalledTimes(1)
1919
})
20+
21+
test('can timeout after the given timeout time', async () => {
22+
const error = new Error('throws every time')
23+
const result = await wait(
24+
() => {
25+
throw error
26+
},
27+
{timeout: 8, interval: 5},
28+
).catch(e => e)
29+
expect(result).toBe(error)
30+
})

Diff for: src/config.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {prettyDOM} from './pretty-dom'
55
// './queries' are query functions.
66
let config = {
77
testIdAttribute: 'data-testid',
8-
asyncUtilTimeout: 4500,
8+
asyncUtilTimeout: 1000,
99
// this is to support React's async `act` function.
1010
// forcing react-testing-library to wrap all async functions would've been
1111
// a total nightmare (consider wrapping every findBy* query and then also
@@ -19,9 +19,11 @@ let config = {
1919

2020
// called when getBy* queries fail. (message, container) => Error
2121
getElementError(message, container) {
22-
return new Error(
22+
const error = new Error(
2323
[message, prettyDOM(container)].filter(Boolean).join('\n\n'),
2424
)
25+
error.name = 'TestingLibraryElementError'
26+
return error
2527
},
2628
}
2729

Diff for: src/pretty-dom.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const inNode = () =>
1818
const getMaxLength = dom =>
1919
inCypress(dom)
2020
? 0
21-
: typeof process !== 'undefined' && process.env.DEBUG_PRINT_LIMIT || 7000
21+
: (typeof process !== 'undefined' && process.env.DEBUG_PRINT_LIMIT) || 7000
2222

2323
const {DOMElement, DOMCollection} = prettyFormat.plugins
2424

@@ -64,5 +64,3 @@ function prettyDOM(dom, maxLength, options) {
6464
const logDOM = (...args) => console.log(prettyDOM(...args))
6565

6666
export {prettyDOM, logDOM}
67-
68-
/* eslint no-console:0 */

Diff for: src/role-helpers.js

-2
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,3 @@ export {
176176
prettyRoles,
177177
isInaccessible,
178178
}
179-
180-
/* eslint no-console:0 */

Diff for: src/wait-for-dom-change.js

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
} from './helpers'
99
import {getConfig} from './config'
1010

11+
let hasWarned = false
12+
13+
// deprecated... TODO: remove this method. People should use wait instead
14+
// the reasoning is that waiting for just any DOM change is an implementation
15+
// detail. People should be waiting for a specific thing to change.
1116
function waitForDomChange({
1217
container = getDocument(),
1318
timeout = getConfig().asyncUtilTimeout,
@@ -18,6 +23,12 @@ function waitForDomChange({
1823
characterData: true,
1924
},
2025
} = {}) {
26+
if (!hasWarned) {
27+
hasWarned = true
28+
console.warn(
29+
`\`waitForDomChange\` has been deprecated. Use \`wait\` instead: https://testing-library.com/docs/dom-testing-library/api-async#wait.`,
30+
)
31+
}
2132
return new Promise((resolve, reject) => {
2233
const timer = setTimeout(onTimeout, timeout)
2334
const observer = newMutationObserver(onMutation)

0 commit comments

Comments
 (0)