Skip to content

Commit

Permalink
fix: wait after each method before leaving asyncWrapper (#952)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed May 12, 2022
1 parent ab78f3f commit 6f55fee
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 21 deletions.
8 changes: 7 additions & 1 deletion src/setup/setup.ts
Expand Up @@ -8,6 +8,7 @@ import {
attachClipboardStubToView,
getDocumentFromNode,
setLevelRef,
wait,
} from '../utils'
import type {Instance, UserEvent, UserEventApi} from './index'
import {Config} from './config'
Expand Down Expand Up @@ -80,7 +81,12 @@ function wrapAndBindImpl<
function method(...args: Args) {
setLevelRef(instance[Config], ApiLevel.Call)

return wrapAsync(() => impl.apply(instance, args))
return wrapAsync(() =>
impl.apply(instance, args).then(async ret => {
await wait(instance[Config])
return ret
}),
)
}
Object.defineProperty(method, 'name', {get: () => impl.name})

Expand Down
4 changes: 1 addition & 3 deletions tests/keyboard/index.ts
Expand Up @@ -123,9 +123,7 @@ describe('delay', () => {
const time0 = performance.now()
await user.keyboard('foo')

// we don't call delay after the last action
// TODO: Should we call it?
expect(spy).toBeCalledTimes(2)
expect(spy.mock.calls.length).toBeGreaterThanOrEqual(2)
expect(time0).toBeLessThan(performance.now() - 20)
})

Expand Down
6 changes: 4 additions & 2 deletions tests/keyboard/keyboardAction.ts
Expand Up @@ -184,7 +184,9 @@ test('do not call setTimeout with delay `null`', async () => {
const {user} = setup(`<div></div>`)
const spy = jest.spyOn(global, 'setTimeout')
await user.keyboard('ab')
expect(spy).toBeCalledTimes(1)
expect(spy.mock.calls.length).toBeGreaterThanOrEqual(1)

spy.mockClear()
await user.setup({delay: null}).keyboard('cd')
expect(spy).toBeCalledTimes(1)
expect(spy).not.toBeCalled()
})
4 changes: 1 addition & 3 deletions tests/pointer/index.ts
Expand Up @@ -73,9 +73,7 @@ describe('delay', () => {
'[/MouseLeft]',
])

// we don't call delay after the last action
// TODO: Should we call it?
expect(spy).toBeCalledTimes(2)
expect(spy.mock.calls.length).toBeGreaterThanOrEqual(2)
expect(time0).toBeLessThan(performance.now() - 20)
})

Expand Down
24 changes: 13 additions & 11 deletions tests/setup/_mockApis.ts
Expand Up @@ -6,13 +6,12 @@ import {Instance, UserEventApi} from '#src/setup'
// `const` are not initialized when mocking is executed, but `function` are when prefixed with `mock`
function mockApis() {}
// access the `function` as object
type mockApisRefHack = (() => void) &
{
[name in keyof UserEventApi]: {
mock: APIMock
real: UserEventApi[name]
}
type mockApisRefHack = (() => void) & {
[name in keyof UserEventApi]: {
mock: APIMock
real: UserEventApi[name]
}
}

// make the tests more readable by applying the typecast here
export function getSpy(k: keyof UserEventApi) {
Expand All @@ -34,6 +33,10 @@ interface APIMock
this: Instance,
...args: Parameters<UserEventApi[keyof UserEventApi]>
): ReturnType<UserEventApi[keyof UserEventApi]>
originalMockImplementation: (
this: Instance,
...args: Parameters<UserEventApi[keyof UserEventApi]>
) => ReturnType<UserEventApi[keyof UserEventApi]>
}

jest.mock('#src/setup/api', () => {
Expand All @@ -44,15 +47,14 @@ jest.mock('#src/setup/api', () => {
}

;(Object.keys(real) as Array<keyof UserEventApi>).forEach(key => {
const mock = jest.fn<unknown, unknown[]>(function mockImpl(
this: Instance,
...args: unknown[]
) {
const mock = jest.fn<unknown, unknown[]>(mockImpl) as unknown as APIMock
function mockImpl(this: Instance, ...args: unknown[]) {
Object.defineProperty(mock.mock.lastCall, 'this', {
get: () => this,
})
return (real[key] as Function).apply(this, args)
}) as unknown as APIMock
}
mock.originalMockImplementation = mockImpl

Object.defineProperty(mock, 'name', {
get: () => `mock-${key}`,
Expand Down
33 changes: 32 additions & 1 deletion tests/setup/index.ts
@@ -1,6 +1,7 @@
import {getConfig} from '@testing-library/dom'
import {getSpy} from './_mockApis'
import userEvent from '#src'
import {Config, UserEventApi} from '#src/setup'
import {Config, Instance, UserEventApi} from '#src/setup'
import {render} from '#testHelpers'

type ApiDeclarations = {
Expand Down Expand Up @@ -80,6 +81,13 @@ declare module '#src/options' {
}
}

// eslint-disable-next-line @typescript-eslint/unbound-method
const realAsyncWrapper = getConfig().asyncWrapper
afterEach(() => {
getConfig().asyncWrapper = realAsyncWrapper
jest.restoreAllMocks()
})

test.each(apiDeclarationsEntries)(
'call `%s` api on instance',
async (name, {args = [], elementArg, elementHtml = `<input/>`}) => {
Expand All @@ -95,11 +103,34 @@ test.each(apiDeclarationsEntries)(

expect(apis[name]).toHaveProperty('name', `mock-${name}`)

// Replace the asyncWrapper to make sure that a delayed state update happens inside of it
const stateUpdate = jest.fn()
spy.mockImplementation(async function impl(
this: Instance,
...a: Parameters<typeof spy>
) {
const ret = spy.originalMockImplementation.apply(this, a)
void ret.then(() => setTimeout(stateUpdate))
return ret
} as typeof spy['originalMockImplementation'])
const asyncWrapper = jest.fn(async (cb: () => Promise<unknown>) => {
stateUpdate.mockClear()
const ret = cb()
expect(stateUpdate).not.toBeCalled()
await ret
expect(stateUpdate).toBeCalled()
return ret
})
getConfig().asyncWrapper = asyncWrapper

await (apis[name] as Function)(...args)

expect(spy).toBeCalledTimes(1)
expect(spy.mock.lastCall?.this?.[Config][opt]).toBe(true)

// Make sure the asyncWrapper mock has been used in the API call
expect(asyncWrapper).toBeCalled()

const subApis = apis.setup({})

await (subApis[name] as Function)(...args)
Expand Down

0 comments on commit 6f55fee

Please sign in to comment.