Skip to content

Commit b4872c8

Browse files
authored
Merge branch 'main' into selectOptions
2 parents b057240 + 6f55fee commit b4872c8

8 files changed

Lines changed: 136 additions & 24 deletions

File tree

src/setup/setup.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
attachClipboardStubToView,
99
getDocumentFromNode,
1010
setLevelRef,
11+
wait,
1112
} from '../utils'
1213
import type {Instance, UserEvent, UserEventApi} from './index'
1314
import {Config} from './config'
@@ -80,7 +81,12 @@ function wrapAndBindImpl<
8081
function method(...args: Args) {
8182
setLevelRef(instance[Config], ApiLevel.Call)
8283

83-
return wrapAsync(() => impl.apply(instance, args))
84+
return wrapAsync(() =>
85+
impl.apply(instance, args).then(async ret => {
86+
await wait(instance[Config])
87+
return ret
88+
}),
89+
)
8490
}
8591
Object.defineProperty(method, 'name', {get: () => impl.name})
8692

src/utils/focus/selection.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,13 @@ export function moveSelection(node: Element, direction: -1 | 1) {
217217
} else {
218218
const selection = node.ownerDocument.getSelection()
219219

220-
/* istanbul ignore if */
221-
if (!selection) {
220+
if (!selection?.focusNode) {
222221
return
223222
}
224223

225224
if (selection.isCollapsed) {
226225
const nextPosition = getNextCursorPosition(
227-
selection.focusNode as Node,
226+
selection.focusNode,
228227
selection.focusOffset,
229228
direction,
230229
)

tests/keyboard/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,7 @@ describe('delay', () => {
123123
const time0 = performance.now()
124124
await user.keyboard('foo')
125125

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

tests/keyboard/keyboardAction.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ test('do not call setTimeout with delay `null`', async () => {
184184
const {user} = setup(`<div></div>`)
185185
const spy = jest.spyOn(global, 'setTimeout')
186186
await user.keyboard('ab')
187-
expect(spy).toBeCalledTimes(1)
187+
expect(spy.mock.calls.length).toBeGreaterThanOrEqual(1)
188+
189+
spy.mockClear()
188190
await user.setup({delay: null}).keyboard('cd')
189-
expect(spy).toBeCalledTimes(1)
191+
expect(spy).not.toBeCalled()
190192
})

tests/pointer/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ describe('delay', () => {
7373
'[/MouseLeft]',
7474
])
7575

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

tests/setup/_mockApis.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {Instance, UserEventApi} from '#src/setup'
66
// `const` are not initialized when mocking is executed, but `function` are when prefixed with `mock`
77
function mockApis() {}
88
// access the `function` as object
9-
type mockApisRefHack = (() => void) &
10-
{
11-
[name in keyof UserEventApi]: {
12-
mock: APIMock
13-
real: UserEventApi[name]
14-
}
9+
type mockApisRefHack = (() => void) & {
10+
[name in keyof UserEventApi]: {
11+
mock: APIMock
12+
real: UserEventApi[name]
1513
}
14+
}
1615

1716
// make the tests more readable by applying the typecast here
1817
export function getSpy(k: keyof UserEventApi) {
@@ -34,6 +33,10 @@ interface APIMock
3433
this: Instance,
3534
...args: Parameters<UserEventApi[keyof UserEventApi]>
3635
): ReturnType<UserEventApi[keyof UserEventApi]>
36+
originalMockImplementation: (
37+
this: Instance,
38+
...args: Parameters<UserEventApi[keyof UserEventApi]>
39+
) => ReturnType<UserEventApi[keyof UserEventApi]>
3740
}
3841

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

4649
;(Object.keys(real) as Array<keyof UserEventApi>).forEach(key => {
47-
const mock = jest.fn<unknown, unknown[]>(function mockImpl(
48-
this: Instance,
49-
...args: unknown[]
50-
) {
50+
const mock = jest.fn<unknown, unknown[]>(mockImpl) as unknown as APIMock
51+
function mockImpl(this: Instance, ...args: unknown[]) {
5152
Object.defineProperty(mock.mock.lastCall, 'this', {
5253
get: () => this,
5354
})
5455
return (real[key] as Function).apply(this, args)
55-
}) as unknown as APIMock
56+
}
57+
mock.originalMockImplementation = mockImpl
5658

5759
Object.defineProperty(mock, 'name', {
5860
get: () => `mock-${key}`,

tests/setup/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import {getConfig} from '@testing-library/dom'
12
import {getSpy} from './_mockApis'
23
import userEvent from '#src'
3-
import {Config, UserEventApi} from '#src/setup'
4+
import {Config, Instance, UserEventApi} from '#src/setup'
45
import {render} from '#testHelpers'
56

67
type ApiDeclarations = {
@@ -80,6 +81,13 @@ declare module '#src/options' {
8081
}
8182
}
8283

84+
// eslint-disable-next-line @typescript-eslint/unbound-method
85+
const realAsyncWrapper = getConfig().asyncWrapper
86+
afterEach(() => {
87+
getConfig().asyncWrapper = realAsyncWrapper
88+
jest.restoreAllMocks()
89+
})
90+
8391
test.each(apiDeclarationsEntries)(
8492
'call `%s` api on instance',
8593
async (name, {args = [], elementArg, elementHtml = `<input/>`}) => {
@@ -95,11 +103,34 @@ test.each(apiDeclarationsEntries)(
95103

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

106+
// Replace the asyncWrapper to make sure that a delayed state update happens inside of it
107+
const stateUpdate = jest.fn()
108+
spy.mockImplementation(async function impl(
109+
this: Instance,
110+
...a: Parameters<typeof spy>
111+
) {
112+
const ret = spy.originalMockImplementation.apply(this, a)
113+
void ret.then(() => setTimeout(stateUpdate))
114+
return ret
115+
} as typeof spy['originalMockImplementation'])
116+
const asyncWrapper = jest.fn(async (cb: () => Promise<unknown>) => {
117+
stateUpdate.mockClear()
118+
const ret = cb()
119+
expect(stateUpdate).not.toBeCalled()
120+
await ret
121+
expect(stateUpdate).toBeCalled()
122+
return ret
123+
})
124+
getConfig().asyncWrapper = asyncWrapper
125+
98126
await (apis[name] as Function)(...args)
99127

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

131+
// Make sure the asyncWrapper mock has been used in the API call
132+
expect(asyncWrapper).toBeCalled()
133+
103134
const subApis = apis.setup({})
104135

105136
await (subApis[name] as Function)(...args)

tests/utils/focus/selection.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setSelection,
55
setSelectionRange,
66
modifySelection,
7+
moveSelection,
78
} from '#src/utils'
89
import {setup} from '#testHelpers'
910

@@ -183,3 +184,78 @@ describe('update selection when moving focus into element with own selection imp
183184
expect(document.getSelection()).toHaveProperty('focusOffset', 0)
184185
})
185186
})
187+
188+
describe('move selection', () => {
189+
test('do nothing without a selection range', () => {
190+
const {element} = setup(`<div tabindex="0"></div>`)
191+
document.getSelection()?.removeAllRanges()
192+
193+
moveSelection(element, 1)
194+
195+
expect(document.getSelection()).toHaveProperty('rangeCount', 0)
196+
})
197+
198+
test('move to next cursor position', () => {
199+
const {element} = setup(`<div tabindex="0">foo</div>`, {
200+
selection: {focusNode: 'div/text()', focusOffset: 1},
201+
})
202+
203+
moveSelection(element, 1)
204+
205+
expect(document.getSelection()).toHaveProperty(
206+
'focusNode',
207+
element.firstChild,
208+
)
209+
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
210+
})
211+
212+
test('move to next cursor position', () => {
213+
const {element} = setup(`<div tabindex="0">foo</div>`, {
214+
selection: {focusNode: 'div/text()', focusOffset: 1},
215+
})
216+
217+
moveSelection(element, 1)
218+
219+
expect(document.getSelection()).toHaveProperty(
220+
'focusNode',
221+
element.firstChild,
222+
)
223+
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
224+
expect(document.getSelection()).toHaveProperty('isCollapsed', true)
225+
})
226+
227+
test('collapse range', () => {
228+
const {element} = setup(`<div tabindex="0">foo</div>`, {
229+
selection: {focusNode: 'div/text()', anchorOffset: 1, focusOffset: 2},
230+
})
231+
232+
moveSelection(element, 1)
233+
234+
expect(document.getSelection()).toHaveProperty(
235+
'focusNode',
236+
element.firstChild,
237+
)
238+
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
239+
expect(document.getSelection()).toHaveProperty('isCollapsed', true)
240+
})
241+
242+
test('move cursor in input', () => {
243+
const {element} = setup(`<input value="foo"/>`)
244+
245+
moveSelection(element, 1)
246+
247+
expect(element).toHaveProperty('selectionStart', 1)
248+
expect(element).toHaveProperty('selectionEnd', 1)
249+
})
250+
251+
test('collapse range in input', () => {
252+
const {element} = setup(`<input value="foo"/>`, {
253+
selection: {anchorOffset: 1, focusOffset: 2},
254+
})
255+
256+
moveSelection(element, 1)
257+
258+
expect(element).toHaveProperty('selectionStart', 2)
259+
expect(element).toHaveProperty('selectionEnd', 2)
260+
})
261+
})

0 commit comments

Comments
 (0)