Skip to content

Commit

Permalink
fix(browser): userEvent.setup initiates a separate state for userEven…
Browse files Browse the repository at this point in the history
…t instance
  • Loading branch information
sheremet-va committed Jul 11, 2024
1 parent 99a12ae commit be15d61
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 71 deletions.
130 changes: 74 additions & 56 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,65 +84,83 @@ function getParent(el: Element) {
return parent
}

export const userEvent: UserEvent = {
// TODO: actually setup userEvent with config options
setup() {
return userEvent
},
click(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_click', css, options)
},
dblClick(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_dblClick', css, options)
},
tripleClick(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_tripleClick', css, options)
},
selectOptions(element, value) {
const values = provider === 'webdriverio'
? getWebdriverioSelectOptions(element, value)
: getSimpleSelectOptions(element, value)
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_selectOptions', css, values)
},
type(element: Element, text: string, options: UserEventTypeOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_type', css, text, options)
},
clear(element: Element) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_clear', css)
},
tab(options: UserEventTabOptions = {}) {
return triggerCommand('__vitest_tab', options)
},
keyboard(text: string) {
return triggerCommand('__vitest_keyboard', text)
},
hover(element: Element) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_hover', css)
},
unhover(element: Element) {
const css = convertElementToCssSelector(element.ownerDocument.body)
return triggerCommand('__vitest_hover', css)
},
function createUserEvent(): UserEvent {
const keyboard = {
unreleased: [] as string[],
}

// non userEvent events, but still useful
fill(element: Element, text: string, options) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_fill', css, text, options)
},
dragAndDrop(source: Element, target: Element, options = {}) {
const sourceCss = convertElementToCssSelector(source)
const targetCss = convertElementToCssSelector(target)
return triggerCommand('__vitest_dragAndDrop', sourceCss, targetCss, options)
},
return {
setup() {
return createUserEvent()
},
click(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_click', css, options)
},
dblClick(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_dblClick', css, options)
},
tripleClick(element: Element, options: UserEventClickOptions = {}) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_tripleClick', css, options)
},
selectOptions(element, value) {
const values = provider === 'webdriverio'
? getWebdriverioSelectOptions(element, value)
: getSimpleSelectOptions(element, value)
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_selectOptions', css, values)
},
async type(element: Element, text: string, options: UserEventTypeOptions = {}) {
const css = convertElementToCssSelector(element)
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
'__vitest_type',
css,
text,
{ ...options, unreleased: keyboard.unreleased },
)
keyboard.unreleased = unreleased
},
clear(element: Element) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_clear', css)
},
tab(options: UserEventTabOptions = {}) {
return triggerCommand('__vitest_tab', options)
},
async keyboard(text: string) {
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
'__vitest_keyboard',
text,
keyboard,
)
keyboard.unreleased = unreleased
},
hover(element: Element) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_hover', css)
},
unhover(element: Element) {
const css = convertElementToCssSelector(element.ownerDocument.body)
return triggerCommand('__vitest_hover', css)
},

// non userEvent events, but still useful
fill(element: Element, text: string, options) {
const css = convertElementToCssSelector(element)
return triggerCommand('__vitest_fill', css, text, options)
},
dragAndDrop(source: Element, target: Element, options = {}) {
const sourceCss = convertElementToCssSelector(source)
const targetCss = convertElementToCssSelector(target)
return triggerCommand('__vitest_dragAndDrop', sourceCss, targetCss, options)
},
}
}

export const userEvent: UserEvent = createUserEvent()

function getWebdriverioSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) {
const options = [...element.querySelectorAll('option')] as HTMLOptionElement[]

Expand Down
18 changes: 14 additions & 4 deletions packages/browser/src/node/commands/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/key
import type { BrowserProvider } from 'vitest/node'
import { PlaywrightBrowserProvider } from '../providers/playwright'
import { WebdriverBrowserProvider } from '../providers/webdriver'
import type { UserEvent } from '../../../context'
import type { UserEventCommand } from './utils'

export const keyboard: UserEventCommand<UserEvent['keyboard']> = async (
export interface KeyboardState {
unreleased: string[]
}

export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async (
context,
text,
state,
) => {
function focusIframe() {
if (
Expand All @@ -28,7 +32,10 @@ export const keyboard: UserEventCommand<UserEvent['keyboard']> = async (
await context.browser.execute(focusIframe)
}

const pressed = new Set<string>(state.unreleased)

await keyboardImplementation(
pressed,
context.provider,
context.contextId,
text,
Expand All @@ -52,17 +59,20 @@ export const keyboard: UserEventCommand<UserEvent['keyboard']> = async (
},
true,
)

return {
unreleased: Array.from(pressed),
}
}

export async function keyboardImplementation(
pressed: Set<string>,
provider: BrowserProvider,
contextId: string,
text: string,
selectAll: () => Promise<void>,
skipRelease: boolean,
) {
const pressed = new Set<string>()

if (provider instanceof PlaywrightBrowserProvider) {
const page = provider.getPage(contextId)
const actions = parseKeyDef(defaultKeyMap, text)
Expand Down
7 changes: 7 additions & 0 deletions packages/browser/src/node/commands/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const type: UserEventCommand<UserEvent['type']> = async (
options = {},
) => {
const { skipClick = false, skipAutoClose = false } = options
const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])

if (context.provider instanceof PlaywrightBrowserProvider) {
const { iframe } = context
Expand All @@ -21,6 +22,7 @@ export const type: UserEventCommand<UserEvent['type']> = async (
}

await keyboardImplementation(
unreleased,
context.provider,
context.contextId,
text,
Expand All @@ -37,6 +39,7 @@ export const type: UserEventCommand<UserEvent['type']> = async (
}

await keyboardImplementation(
unreleased,
context.provider,
context.contextId,
text,
Expand All @@ -52,4 +55,8 @@ export const type: UserEventCommand<UserEvent['type']> = async (
else {
throw new TypeError(`Provider "${context.provider.name}" does not support typing`)
}

return {
unreleased: Array.from(unreleased),
}
}
17 changes: 12 additions & 5 deletions packages/browser/src/node/plugins/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,17 @@ function getUserEvent(provider: BrowserProvider) {
}
// TODO: have this in a separate file
return `{
...__vitest_user_event__,
fill: async (element, text) => {
await __vitest_user_event__.clear(element)
await __vitest_user_event__.type(element, text)
..._userEventSetup,
setup() {
const userEvent = __vitest_user_event__.setup()
userEvent.setup = this.setup
userEvent.fill = this.fill.bind(userEvent)
userEvent.dragAndDrop = this.dragAndDrop
return userEvent
},
async fill(element, text) {
await this.clear(element)
await this.type(element, text)
},
dragAndDrop: async () => {
throw new Error('Provider "preview" does not support dragging elements')
Expand All @@ -115,5 +122,5 @@ async function getUserEventImport(provider: BrowserProvider, resolve: (id: strin
}
return `import { userEvent as __vitest_user_event__ } from '${slash(
`/@fs/${resolved.id}`,
)}'`
)}'\nconst _userEventSetup = __vitest_user_event__.setup()\n`
}
16 changes: 10 additions & 6 deletions test/browser/test/userEvent.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { server, userEvent } from '@vitest/browser/context'
import { userEvent as _uE, server } from '@vitest/browser/context'
import '../src/button.css'

beforeEach(() => {
// clear body
document.body.replaceChildren()
})

const userEvent = _uE.setup()

describe('userEvent.click', () => {
test('correctly clicks a button', async () => {
const button = document.createElement('button')
Expand Down Expand Up @@ -347,11 +349,11 @@ describe.each(inputLike)('userEvent.type', (getElement) => {
test('repeating without manual up works correctly', async () => {
const { input, keydown, keyup, value } = createTextInput()

await userEvent.type(input, '{a>3}4')
expect(value()).toBe('aaa4')
const userEvent = _uE.setup()
await userEvent.type(input, '{a>2}4')
expect(value()).toBe('aa4')

expect(keydown).toEqual([
'a',
'a',
'a',
'4',
Expand All @@ -366,6 +368,7 @@ describe.each(inputLike)('userEvent.type', (getElement) => {
test('repeating with manual up works correctly', async () => {
const { input, keydown, keyup, value } = createTextInput()

const userEvent = _uE.setup()
await userEvent.type(input, '{a>3/}4')
expect(value()).toBe('aaa4')

Expand All @@ -385,6 +388,7 @@ describe.each(inputLike)('userEvent.type', (getElement) => {
test('repeating with disabled up works correctly', async () => {
const { input, keydown, keyup, value } = createTextInput()

const userEvent = _uE.setup()
await userEvent.type(input, '{a>3}4', {
skipAutoClose: true,
})
Expand All @@ -406,6 +410,7 @@ describe.each(inputLike)('userEvent.type', (getElement) => {
const shadowRoot = createShadowRoot()
const { input, keydown, value } = createTextInput(shadowRoot)

const userEvent = _uE.setup()
await userEvent.type(input, 'Hello')
expect(value()).toBe('Hello')
expect(keydown).toEqual([
Expand Down Expand Up @@ -569,8 +574,7 @@ describe('userEvent.keyboard', async () => {
expect(spyKeydown).toHaveBeenCalledOnce()
expect(spyKeyup).not.toHaveBeenCalled()
await userEvent.keyboard('{/Enter}')
// userEvent doesn't fire any event here, but should we?
expect(spyKeyup).not.toHaveBeenCalled()
expect(spyKeyup).toHaveBeenCalled()
})

test('standalone keyboard works correctly with active input', async () => {
Expand Down

0 comments on commit be15d61

Please sign in to comment.