Skip to content

Commit

Permalink
Merge branch 'main' into selectOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche authored May 12, 2022
2 parents b057240 + 6f55fee commit b4872c8
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 24 deletions.
8 changes: 7 additions & 1 deletion src/setup/setup.ts
Original file line number Diff line number Diff line change
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
5 changes: 2 additions & 3 deletions src/utils/focus/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,13 @@ export function moveSelection(node: Element, direction: -1 | 1) {
} else {
const selection = node.ownerDocument.getSelection()

/* istanbul ignore if */
if (!selection) {
if (!selection?.focusNode) {
return
}

if (selection.isCollapsed) {
const nextPosition = getNextCursorPosition(
selection.focusNode as Node,
selection.focusNode,
selection.focusOffset,
direction,
)
Expand Down
4 changes: 1 addition & 3 deletions tests/keyboard/index.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions tests/utils/focus/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
setSelection,
setSelectionRange,
modifySelection,
moveSelection,
} from '#src/utils'
import {setup} from '#testHelpers'

Expand Down Expand Up @@ -183,3 +184,78 @@ describe('update selection when moving focus into element with own selection imp
expect(document.getSelection()).toHaveProperty('focusOffset', 0)
})
})

describe('move selection', () => {
test('do nothing without a selection range', () => {
const {element} = setup(`<div tabindex="0"></div>`)
document.getSelection()?.removeAllRanges()

moveSelection(element, 1)

expect(document.getSelection()).toHaveProperty('rangeCount', 0)
})

test('move to next cursor position', () => {
const {element} = setup(`<div tabindex="0">foo</div>`, {
selection: {focusNode: 'div/text()', focusOffset: 1},
})

moveSelection(element, 1)

expect(document.getSelection()).toHaveProperty(
'focusNode',
element.firstChild,
)
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
})

test('move to next cursor position', () => {
const {element} = setup(`<div tabindex="0">foo</div>`, {
selection: {focusNode: 'div/text()', focusOffset: 1},
})

moveSelection(element, 1)

expect(document.getSelection()).toHaveProperty(
'focusNode',
element.firstChild,
)
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
expect(document.getSelection()).toHaveProperty('isCollapsed', true)
})

test('collapse range', () => {
const {element} = setup(`<div tabindex="0">foo</div>`, {
selection: {focusNode: 'div/text()', anchorOffset: 1, focusOffset: 2},
})

moveSelection(element, 1)

expect(document.getSelection()).toHaveProperty(
'focusNode',
element.firstChild,
)
expect(document.getSelection()).toHaveProperty('focusOffset', 2)
expect(document.getSelection()).toHaveProperty('isCollapsed', true)
})

test('move cursor in input', () => {
const {element} = setup(`<input value="foo"/>`)

moveSelection(element, 1)

expect(element).toHaveProperty('selectionStart', 1)
expect(element).toHaveProperty('selectionEnd', 1)
})

test('collapse range in input', () => {
const {element} = setup(`<input value="foo"/>`, {
selection: {anchorOffset: 1, focusOffset: 2},
})

moveSelection(element, 1)

expect(element).toHaveProperty('selectionStart', 2)
expect(element).toHaveProperty('selectionEnd', 2)
})
})

0 comments on commit b4872c8

Please sign in to comment.