Skip to content

Commit 2e2f9d6

Browse files
committed
added typescript types to waitfor functions
1 parent 56a4c75 commit 2e2f9d6

8 files changed

+176
-119
lines changed

src/helpers.js renamed to src/helpers.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
1+
import {Screen} from '../types'
2+
13
const globalObj = typeof window === 'undefined' ? global : window
24
// Constant node.nodeType for text nodes, see:
35
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
46
const TEXT_NODE = 3
57

68
// Currently this fn only supports jest timers, but it could support other test runners in the future.
7-
function runWithRealTimers(callback) {
9+
function runWithRealTimers<T>(callback: () => T): T {
810
return hasJestTimers()
911
? runWithJestRealTimers(callback).callbackReturnValue
1012
: // istanbul ignore next
1113
callback()
1214
}
1315

14-
function hasJestTimers() {
16+
function hasJestTimers(): boolean {
1517
return (
1618
typeof jest !== 'undefined' &&
19+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1720
jest !== null &&
1821
typeof jest.useRealTimers === 'function'
1922
)
2023
}
2124

22-
function runWithJestRealTimers(callback) {
23-
const timerAPI = {
25+
export interface TimerApi {
26+
clearInterval: typeof clearInterval
27+
clearTimeout: typeof clearTimeout
28+
setInterval: typeof setInterval
29+
setTimeout: typeof setTimeout & {clock?: unknown}
30+
setImmediate?: typeof setImmediate
31+
clearImmediate?: typeof clearImmediate
32+
}
33+
34+
function runWithJestRealTimers<T>(callback: () => T) {
35+
const timerAPI: TimerApi = {
2436
clearInterval,
2537
clearTimeout,
2638
setInterval,
@@ -41,10 +53,11 @@ function runWithJestRealTimers(callback) {
4153
const callbackReturnValue = callback()
4254

4355
const usedFakeTimers = Object.entries(timerAPI).some(
44-
([name, func]) => func !== globalObj[name],
56+
([name, func]) => func !== globalObj[name as keyof TimerApi],
4557
)
4658

4759
if (usedFakeTimers) {
60+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
4861
jest.useFakeTimers(timerAPI.setTimeout?.clock ? 'modern' : 'legacy')
4962
}
5063

@@ -54,7 +67,7 @@ function runWithJestRealTimers(callback) {
5467
}
5568
}
5669

57-
function jestFakeTimersAreEnabled() {
70+
function jestFakeTimersAreEnabled(): boolean {
5871
return hasJestTimers()
5972
? runWithJestRealTimers(() => {}).usedFakeTimers
6073
: // istanbul ignore next
@@ -63,14 +76,15 @@ function jestFakeTimersAreEnabled() {
6376

6477
// we only run our tests in node, and setImmediate is supported in node.
6578
// istanbul ignore next
66-
function setImmediatePolyfill(fn) {
79+
function setImmediatePolyfill(fn: TimerHandler): number {
6780
return globalObj.setTimeout(fn, 0)
6881
}
6982

7083
function getTimeFunctions() {
7184
// istanbul ignore next
7285
return {
7386
clearTimeoutFn: globalObj.clearTimeout,
87+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
7488
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
7589
setTimeoutFn: globalObj.setTimeout,
7690
}
@@ -80,24 +94,30 @@ const {clearTimeoutFn, setImmediateFn, setTimeoutFn} = runWithRealTimers(
8094
getTimeFunctions,
8195
)
8296

83-
function getDocument() {
97+
function getDocument(): Document {
8498
/* istanbul ignore if */
8599
if (typeof window === 'undefined') {
86100
throw new Error('Could not find default container')
87101
}
88102
return window.document
89103
}
90-
function getWindowFromNode(node) {
104+
function getWindowFromNode(
105+
node: EventTarget & {
106+
defaultView?: (Window & typeof globalThis) | null
107+
ownerDocument?: Document | null
108+
window?: Window & typeof globalThis
109+
},
110+
): Window & typeof globalThis {
91111
if (node.defaultView) {
92112
// node is document
93113
return node.defaultView
94-
} else if (node.ownerDocument && node.ownerDocument.defaultView) {
114+
} else if (node.ownerDocument?.defaultView) {
95115
// node is a DOM node
96116
return node.ownerDocument.defaultView
97117
} else if (node.window) {
98118
// node is window
99119
return node.window
100-
} else if (node.then instanceof Function) {
120+
} else if (((node as unknown) as Promise<unknown>).then instanceof Function) {
101121
throw new Error(
102122
`It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?`,
103123
)
@@ -106,8 +126,8 @@ function getWindowFromNode(node) {
106126
`It looks like you passed an Array instead of a DOM node. Did you do something like \`fireEvent.click(screen.getAllBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`?`,
107127
)
108128
} else if (
109-
typeof node.debug === 'function' &&
110-
typeof node.logTestingPlaygroundURL === 'function'
129+
typeof ((node as unknown) as Screen).debug === 'function' &&
130+
typeof ((node as unknown) as Screen).logTestingPlaygroundURL === 'function'
111131
) {
112132
throw new Error(
113133
`It looks like you passed a \`screen\` object. Did you do something like \`fireEvent.click(screen, ...\` when you meant to use a query, e.g. \`fireEvent.click(screen.getBy..., \`?`,
@@ -120,11 +140,16 @@ function getWindowFromNode(node) {
120140
}
121141
}
122142

123-
function checkContainerType(container) {
143+
export type Container = Document | DocumentFragment | Element
144+
145+
function checkContainerType(
146+
container: Node | null,
147+
): asserts container is Container {
124148
if (
149+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
125150
!container ||
126-
!(typeof container.querySelector === 'function') ||
127-
!(typeof container.querySelectorAll === 'function')
151+
!(typeof (container as Container).querySelector === 'function') ||
152+
!(typeof (container as Container).querySelectorAll === 'function')
128153
) {
129154
throw new TypeError(
130155
`Expected container to be an Element, a Document or a DocumentFragment but got ${getTypeName(
@@ -133,7 +158,7 @@ function checkContainerType(container) {
133158
)
134159
}
135160

136-
function getTypeName(object) {
161+
function getTypeName(object: unknown) {
137162
if (typeof object === 'object') {
138163
return object === null ? 'null' : object.constructor.name
139164
}

src/wait-for-dom-change.js renamed to src/wait-for-dom-change.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {waitForOptions} from '../types'
12
import {
23
getWindowFromNode,
34
getDocument,
@@ -22,7 +23,7 @@ function waitForDomChange({
2223
attributes: true,
2324
characterData: true,
2425
},
25-
} = {}) {
26+
}: waitForOptions = {}) {
2627
if (!hasWarned) {
2728
hasWarned = true
2829
console.warn(
@@ -37,17 +38,19 @@ function waitForDomChange({
3738
observer.observe(container, mutationObserverOptions),
3839
)
3940

40-
function onDone(error, result) {
41-
clearTimeout(timer)
41+
function onDone(error: Error | null, result: MutationRecord[] | null) {
42+
;(clearTimeout as (t: NodeJS.Timeout | number) => void)(timer)
4243
setImmediate(() => observer.disconnect())
4344
if (error) {
4445
reject(error)
4546
} else {
46-
resolve(result)
47+
// either error or result is null, so if error is null then result is not
48+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49+
resolve(result!)
4750
}
4851
}
4952

50-
function onMutation(mutationsList) {
53+
function onMutation(mutationsList: MutationRecord[]) {
5154
onDone(null, mutationsList)
5255
}
5356

@@ -57,8 +60,8 @@ function waitForDomChange({
5760
})
5861
}
5962

60-
function waitForDomChangeWrapper(...args) {
61-
return getConfig().asyncWrapper(() => waitForDomChange(...args))
63+
function waitForDomChangeWrapper(options?: waitForOptions) {
64+
return getConfig().asyncWrapper(() => waitForDomChange(options))
6265
}
6366

6467
export {waitForDomChangeWrapper as waitForDomChange}

src/wait-for-element-to-be-removed.js renamed to src/wait-for-element-to-be-removed.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
1+
import {waitForOptions} from '../types'
12
import {waitFor} from './wait-for'
23

3-
const isRemoved = result => !result || (Array.isArray(result) && !result.length)
4+
type Result = (Node | null)[] | Node | null
5+
6+
const isRemoved = (result: Result): boolean =>
7+
!result || (Array.isArray(result) && !result.length)
48

59
// Check if the element is not present.
610
// As the name implies, waitForElementToBeRemoved should check `present` --> `removed`
7-
function initialCheck(elements) {
11+
function initialCheck(elements: Result): void {
812
if (isRemoved(elements)) {
913
throw new Error(
1014
'The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.',
1115
)
1216
}
1317
}
1418

15-
async function waitForElementToBeRemoved(callback, options) {
19+
async function waitForElementToBeRemoved(
20+
arg: Result | (() => Result),
21+
options?: waitForOptions,
22+
): Promise<void> {
1623
// created here so we get a nice stacktrace
1724
const timeoutError = new Error('Timed out in waitForElementToBeRemoved.')
18-
if (typeof callback !== 'function') {
19-
initialCheck(callback)
20-
const elements = Array.isArray(callback) ? callback : [callback]
25+
26+
function handleArg(argument: Result): () => Result {
27+
initialCheck(argument)
28+
const elements = Array.isArray(argument) ? argument : [argument]
2129
const getRemainingElements = elements.map(element => {
22-
let parent = element.parentElement
23-
if (parent === null) return () => null
30+
let parent = element?.parentElement
31+
if (!parent) return () => null
2432
while (parent.parentElement) parent = parent.parentElement
25-
return () => (parent.contains(element) ? element : null)
33+
return () => (parent?.contains(element) ? element : null)
2634
})
27-
callback = () => getRemainingElements.map(c => c()).filter(Boolean)
35+
return () => getRemainingElements.map(c => c()).filter(Boolean)
2836
}
37+
const callback = typeof arg === 'function' ? arg : handleArg(arg)
2938

3039
initialCheck(callback())
3140

3241
return waitFor(() => {
3342
let result
3443
try {
3544
result = callback()
36-
} catch (error) {
37-
if (error.name === 'TestingLibraryElementError') {
45+
} catch (error: unknown) {
46+
if ((error as Error).name === 'TestingLibraryElementError') {
3847
return undefined
3948
}
4049
throw error

src/wait-for-element.js renamed to src/wait-for-element.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import {waitForOptions} from '../types'
12
import {waitFor} from './wait-for'
23

34
let hasWarned = false
45

56
// deprecated... TODO: remove this method. People should use a find* query or
67
// wait instead the reasoning is that this doesn't really do anything useful
78
// that you can't get from using find* or wait.
8-
async function waitForElement(callback, options) {
9+
async function waitForElement<T>(
10+
callback: () => T,
11+
options?: waitForOptions,
12+
): Promise<T> {
913
if (!hasWarned) {
1014
hasWarned = true
1115
console.warn(
1216
`\`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`,
1317
)
1418
}
19+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1520
if (!callback) {
1621
throw new Error('waitForElement requires a callback as the first parameter')
1722
}

0 commit comments

Comments
 (0)