Skip to content

Draft: added typescript types to waitfor functions #982

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 42 additions & 17 deletions src/helpers.js → src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
import {Screen} from '../types'

const globalObj = typeof window === 'undefined' ? global : window
// Constant node.nodeType for text nodes, see:
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
const TEXT_NODE = 3

// Currently this fn only supports jest timers, but it could support other test runners in the future.
function runWithRealTimers(callback) {
function runWithRealTimers<T>(callback: () => T): T {
return hasJestTimers()
? runWithJestRealTimers(callback).callbackReturnValue
: // istanbul ignore next
callback()
}

function hasJestTimers() {
function hasJestTimers(): boolean {
return (
typeof jest !== 'undefined' &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
jest !== null &&
typeof jest.useRealTimers === 'function'
)
}

function runWithJestRealTimers(callback) {
const timerAPI = {
export interface TimerApi {
clearInterval: typeof clearInterval
clearTimeout: typeof clearTimeout
setInterval: typeof setInterval
setTimeout: typeof setTimeout & {clock?: unknown}
setImmediate?: typeof setImmediate
clearImmediate?: typeof clearImmediate
}

function runWithJestRealTimers<T>(callback: () => T) {
const timerAPI: TimerApi = {
clearInterval,
clearTimeout,
setInterval,
Expand All @@ -41,10 +53,11 @@ function runWithJestRealTimers(callback) {
const callbackReturnValue = callback()

const usedFakeTimers = Object.entries(timerAPI).some(
([name, func]) => func !== globalObj[name],
([name, func]) => func !== globalObj[name as keyof TimerApi],
)

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

Expand All @@ -54,7 +67,7 @@ function runWithJestRealTimers(callback) {
}
}

function jestFakeTimersAreEnabled() {
function jestFakeTimersAreEnabled(): boolean {
return hasJestTimers()
? runWithJestRealTimers(() => {}).usedFakeTimers
: // istanbul ignore next
Expand All @@ -63,14 +76,15 @@ function jestFakeTimersAreEnabled() {

// we only run our tests in node, and setImmediate is supported in node.
// istanbul ignore next
function setImmediatePolyfill(fn) {
function setImmediatePolyfill(fn: TimerHandler): number {
return globalObj.setTimeout(fn, 0)
}

function getTimeFunctions() {
// istanbul ignore next
return {
clearTimeoutFn: globalObj.clearTimeout,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
setTimeoutFn: globalObj.setTimeout,
}
Expand All @@ -80,24 +94,30 @@ const {clearTimeoutFn, setImmediateFn, setTimeoutFn} = runWithRealTimers(
getTimeFunctions,
)

function getDocument() {
function getDocument(): Document {
/* istanbul ignore if */
if (typeof window === 'undefined') {
throw new Error('Could not find default container')
}
return window.document
}
function getWindowFromNode(node) {
function getWindowFromNode(
node: EventTarget & {
defaultView?: (Window & typeof globalThis) | null
ownerDocument?: Document | null
window?: Window & typeof globalThis
},
): Window & typeof globalThis {
if (node.defaultView) {
// node is document
return node.defaultView
} else if (node.ownerDocument && node.ownerDocument.defaultView) {
} else if (node.ownerDocument?.defaultView) {
// node is a DOM node
return node.ownerDocument.defaultView
} else if (node.window) {
// node is window
return node.window
} else if (node.then instanceof Function) {
} else if (((node as unknown) as Promise<unknown>).then instanceof Function) {
throw new Error(
`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...\`?`,
)
Expand All @@ -106,8 +126,8 @@ function getWindowFromNode(node) {
`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...\`?`,
)
} else if (
typeof node.debug === 'function' &&
typeof node.logTestingPlaygroundURL === 'function'
typeof ((node as unknown) as Screen).debug === 'function' &&
typeof ((node as unknown) as Screen).logTestingPlaygroundURL === 'function'
) {
throw new Error(
`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..., \`?`,
Expand All @@ -120,11 +140,16 @@ function getWindowFromNode(node) {
}
}

function checkContainerType(container) {
export type Container = Document | DocumentFragment | Element

function checkContainerType(
container: Node | null,
): asserts container is Container {
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
!container ||
!(typeof container.querySelector === 'function') ||
!(typeof container.querySelectorAll === 'function')
!(typeof (container as Container).querySelector === 'function') ||
!(typeof (container as Container).querySelectorAll === 'function')
) {
throw new TypeError(
`Expected container to be an Element, a Document or a DocumentFragment but got ${getTypeName(
Expand All @@ -133,7 +158,7 @@ function checkContainerType(container) {
)
}

function getTypeName(object) {
function getTypeName(object: unknown) {
if (typeof object === 'object') {
return object === null ? 'null' : object.constructor.name
}
Expand Down
17 changes: 10 additions & 7 deletions src/wait-for-dom-change.js → src/wait-for-dom-change.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {waitForOptions} from '../types'
import {
getWindowFromNode,
getDocument,
Expand All @@ -22,7 +23,7 @@ function waitForDomChange({
attributes: true,
characterData: true,
},
} = {}) {
}: waitForOptions = {}) {
if (!hasWarned) {
hasWarned = true
console.warn(
Expand All @@ -37,17 +38,19 @@ function waitForDomChange({
observer.observe(container, mutationObserverOptions),
)

function onDone(error, result) {
clearTimeout(timer)
function onDone(error: Error | null, result: MutationRecord[] | null) {
;(clearTimeout as (t: NodeJS.Timeout | number) => void)(timer)
setImmediate(() => observer.disconnect())
if (error) {
reject(error)
} else {
resolve(result)
// either error or result is null, so if error is null then result is not
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
resolve(result!)
}
}

function onMutation(mutationsList) {
function onMutation(mutationsList: MutationRecord[]) {
onDone(null, mutationsList)
}

Expand All @@ -57,8 +60,8 @@ function waitForDomChange({
})
}

function waitForDomChangeWrapper(...args) {
return getConfig().asyncWrapper(() => waitForDomChange(...args))
function waitForDomChangeWrapper(options?: waitForOptions) {
return getConfig().asyncWrapper(() => waitForDomChange(options))
}

export {waitForDomChangeWrapper as waitForDomChange}
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
import {waitForOptions} from '../types'
import {waitFor} from './wait-for'

const isRemoved = result => !result || (Array.isArray(result) && !result.length)
type Result = (Node | null)[] | Node | null

const isRemoved = (result: Result): boolean =>
!result || (Array.isArray(result) && !result.length)

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

async function waitForElementToBeRemoved(callback, options) {
async function waitForElementToBeRemoved(
arg: Result | (() => Result),
options?: waitForOptions,
): Promise<void> {
// created here so we get a nice stacktrace
const timeoutError = new Error('Timed out in waitForElementToBeRemoved.')
if (typeof callback !== 'function') {
initialCheck(callback)
const elements = Array.isArray(callback) ? callback : [callback]

function handleArg(argument: Result): () => Result {
initialCheck(argument)
const elements = Array.isArray(argument) ? argument : [argument]
const getRemainingElements = elements.map(element => {
let parent = element.parentElement
if (parent === null) return () => null
let parent = element?.parentElement
if (!parent) return () => null
while (parent.parentElement) parent = parent.parentElement
return () => (parent.contains(element) ? element : null)
return () => (parent?.contains(element) ? element : null)
})
callback = () => getRemainingElements.map(c => c()).filter(Boolean)
return () => getRemainingElements.map(c => c()).filter(Boolean)
}
const callback = typeof arg === 'function' ? arg : handleArg(arg)

initialCheck(callback())

return waitFor(() => {
let result
try {
result = callback()
} catch (error) {
if (error.name === 'TestingLibraryElementError') {
} catch (error: unknown) {
if ((error as Error).name === 'TestingLibraryElementError') {
return undefined
}
throw error
Expand Down
7 changes: 6 additions & 1 deletion src/wait-for-element.js → src/wait-for-element.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {waitForOptions} from '../types'
import {waitFor} from './wait-for'

let hasWarned = false

// deprecated... TODO: remove this method. People should use a find* query or
// wait instead the reasoning is that this doesn't really do anything useful
// that you can't get from using find* or wait.
async function waitForElement(callback, options) {
async function waitForElement<T>(
callback: () => T,
options?: waitForOptions,
): Promise<T> {
if (!hasWarned) {
hasWarned = true
console.warn(
`\`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`,
)
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!callback) {
throw new Error('waitForElement requires a callback as the first parameter')
}
Expand Down
Loading