From be15d617fdda51271d4545df4cd539b1c12f7a8f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 11 Jul 2024 10:07:58 +0200 Subject: [PATCH] fix(browser): userEvent.setup initiates a separate state for userEvent instance --- packages/browser/src/client/tester/context.ts | 130 ++++++++++-------- .../browser/src/node/commands/keyboard.ts | 18 ++- packages/browser/src/node/commands/type.ts | 7 + .../browser/src/node/plugins/pluginContext.ts | 17 ++- test/browser/test/userEvent.test.ts | 16 ++- 5 files changed, 117 insertions(+), 71 deletions(-) diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index b23d36df426e..881dd808b9e8 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -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[] diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index 08fd74292e28..d05c1b5d4d6c 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -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 = async ( +export interface KeyboardState { + unreleased: string[] +} + +export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async ( context, text, + state, ) => { function focusIframe() { if ( @@ -28,7 +32,10 @@ export const keyboard: UserEventCommand = async ( await context.browser.execute(focusIframe) } + const pressed = new Set(state.unreleased) + await keyboardImplementation( + pressed, context.provider, context.contextId, text, @@ -52,17 +59,20 @@ export const keyboard: UserEventCommand = async ( }, true, ) + + return { + unreleased: Array.from(pressed), + } } export async function keyboardImplementation( + pressed: Set, provider: BrowserProvider, contextId: string, text: string, selectAll: () => Promise, skipRelease: boolean, ) { - const pressed = new Set() - if (provider instanceof PlaywrightBrowserProvider) { const page = provider.getPage(contextId) const actions = parseKeyDef(defaultKeyMap, text) diff --git a/packages/browser/src/node/commands/type.ts b/packages/browser/src/node/commands/type.ts index ff79fb7e9c34..c211d1e6a697 100644 --- a/packages/browser/src/node/commands/type.ts +++ b/packages/browser/src/node/commands/type.ts @@ -11,6 +11,7 @@ export const type: UserEventCommand = 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 @@ -21,6 +22,7 @@ export const type: UserEventCommand = async ( } await keyboardImplementation( + unreleased, context.provider, context.contextId, text, @@ -37,6 +39,7 @@ export const type: UserEventCommand = async ( } await keyboardImplementation( + unreleased, context.provider, context.contextId, text, @@ -52,4 +55,8 @@ export const type: UserEventCommand = async ( else { throw new TypeError(`Provider "${context.provider.name}" does not support typing`) } + + return { + unreleased: Array.from(unreleased), + } } diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 6802ca12e874..6546d5b48a03 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -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') @@ -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` } diff --git a/test/browser/test/userEvent.test.ts b/test/browser/test/userEvent.test.ts index 77363addd2e3..4508cd5fe78d 100644 --- a/test/browser/test/userEvent.test.ts +++ b/test/browser/test/userEvent.test.ts @@ -1,5 +1,5 @@ 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(() => { @@ -7,6 +7,8 @@ beforeEach(() => { document.body.replaceChildren() }) +const userEvent = _uE.setup() + describe('userEvent.click', () => { test('correctly clicks a button', async () => { const button = document.createElement('button') @@ -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', @@ -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') @@ -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, }) @@ -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([ @@ -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 () => {