From 8e94427e9be5f8cd5e023979ddae60a770d69fb8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 9 Dec 2024 18:23:18 +0900 Subject: [PATCH] fix(browser): fix user event state on preview provider (#7041) --- packages/browser/src/client/tester/context.ts | 122 +++++++++++++++--- .../src/client/tester/locators/preview.ts | 55 ++------ .../fixtures/user-event/keyboard.test.ts | 19 ++- 3 files changed, 130 insertions(+), 66 deletions(-) diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index d648f50aa7ea..433b5b1220c1 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -30,21 +30,20 @@ function triggerCommand(command: string, ...args: any[]) { } export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent, options?: TestingLibraryOptions): UserEvent { - let __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) + if (__tl_user_event_base__) { + return createPreviewUserEvent(__tl_user_event_base__, options ?? {}) + } + const keyboard = { unreleased: [] as string[], } return { - setup(options?: any) { - return createUserEvent(__tl_user_event_base__, options) + setup() { + return createUserEvent() }, async cleanup() { return ensureAwaited(async () => { - if (typeof __tl_user_event_base__ !== 'undefined') { - __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) - return - } await triggerCommand('__vitest_cleanup', keyboard) keyboard.unreleased = [] }) @@ -87,14 +86,6 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent // testing-library user-event async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { return ensureAwaited(async () => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.type( - element instanceof Element ? element : element.element(), - text, - options, - ) - } - const selector = convertToSelector(element) const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_type', @@ -107,17 +98,11 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent }, tab(options: UserEventTabOptions = {}) { return ensureAwaited(() => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.tab(options) - } return triggerCommand('__vitest_tab', options) }) }, async keyboard(text: string) { return ensureAwaited(async () => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.keyboard(text) - } const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_keyboard', text, @@ -129,6 +114,101 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent } } +function createPreviewUserEvent(userEventBase: TestingLibraryUserEvent, options: TestingLibraryOptions): UserEvent { + let userEvent = userEventBase.setup(options) + + function toElement(element: Element | Locator) { + return element instanceof Element ? element : element.element() + } + + const vitestUserEvent: UserEvent = { + setup(options?: any) { + return createPreviewUserEvent(userEventBase, options) + }, + async cleanup() { + userEvent = userEventBase.setup(options ?? {}) + }, + async click(element) { + await userEvent.click(toElement(element)) + }, + async dblClick(element) { + await userEvent.dblClick(toElement(element)) + }, + async tripleClick(element) { + await userEvent.tripleClick(toElement(element)) + }, + async selectOptions(element, value) { + const options = (Array.isArray(value) ? value : [value]).map((option) => { + if (typeof option !== 'string') { + return toElement(option) + } + return option + }) + await userEvent.selectOptions( + element, + options as string[] | HTMLElement[], + ) + }, + async clear(element) { + await userEvent.clear(toElement(element)) + }, + async hover(element: Element | Locator) { + await userEvent.hover(toElement(element)) + }, + async unhover(element: Element | Locator) { + await userEvent.unhover(toElement(element)) + }, + async upload(element: Element | Locator, files: string | string[] | File | File[]) { + const uploadPromise = (Array.isArray(files) ? files : [files]).map(async (file) => { + if (typeof file !== 'string') { + return file + } + + const { content: base64, basename, mime } = await triggerCommand<{ + content: string + basename: string + mime: string + }>('__vitest_fileInfo', file, 'base64') + + const fileInstance = fetch(`data:${mime};base64,${base64}`) + .then(r => r.blob()) + .then(blob => new File([blob], basename, { type: mime })) + return fileInstance + }) + const uploadFiles = await Promise.all(uploadPromise) + return userEvent.upload(toElement(element) as HTMLElement, uploadFiles) + }, + + async fill(element: Element | Locator, text: string) { + await userEvent.clear(toElement(element)) + return userEvent.type(toElement(element), text) + }, + async dragAndDrop() { + throw new Error(`The "preview" provider doesn't support 'userEvent.dragAndDrop'`) + }, + + async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { + await userEvent.type(toElement(element), text, options) + }, + async tab(options: UserEventTabOptions = {}) { + await userEvent.tab(options) + }, + async keyboard(text: string) { + await userEvent.keyboard(text) + }, + } + + for (const [name, fn] of Object.entries(vitestUserEvent)) { + if (name !== 'setup') { + (vitestUserEvent as any)[name] = function (this: any, ...args: any[]) { + return ensureAwaited(() => fn.apply(this, args)) + } + } + } + + return vitestUserEvent +} + export function cdp() { return getBrowserState().cdp! } diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index 0e966a8557c7..5881100fd315 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -1,5 +1,4 @@ -import { userEvent } from '@testing-library/user-event' -import { page, server } from '@vitest/browser/context' +import { page, server, userEvent } from '@vitest/browser/context' import { getByAltTextSelector, getByLabelSelector, @@ -9,7 +8,7 @@ import { getByTextSelector, getByTitleSelector, } from 'ivya' -import { convertElementToCssSelector, ensureAwaited } from '../../utils' +import { convertElementToCssSelector } from '../../utils' import { getElementError } from '../public-utils' import { Locator, selectorEngine } from './index' @@ -58,71 +57,39 @@ class PreviewLocator extends Locator { } click(): Promise { - return ensureAwaited(() => userEvent.click(this.element())) + return userEvent.click(this.element()) } dblClick(): Promise { - return ensureAwaited(() => userEvent.dblClick(this.element())) + return userEvent.dblClick(this.element()) } tripleClick(): Promise { - return ensureAwaited(() => userEvent.tripleClick(this.element())) + return userEvent.tripleClick(this.element()) } hover(): Promise { - return ensureAwaited(() => userEvent.hover(this.element())) + return userEvent.hover(this.element()) } unhover(): Promise { - return ensureAwaited(() => userEvent.unhover(this.element())) + return userEvent.unhover(this.element()) } async fill(text: string): Promise { - await this.clear() - return ensureAwaited(() => userEvent.type(this.element(), text)) + return userEvent.fill(this.element(), text) } async upload(file: string | string[] | File | File[]): Promise { - const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => { - if (typeof file !== 'string') { - return file - } - - const { content: base64, basename, mime } = await this.triggerCommand<{ - content: string - basename: string - mime: string - }>('__vitest_fileInfo', file, 'base64') - - const fileInstance = fetch(`data:${mime};base64,${base64}`) - .then(r => r.blob()) - .then(blob => new File([blob], basename, { type: mime })) - return fileInstance - }) - const uploadFiles = await Promise.all(uploadPromise) - return ensureAwaited(() => userEvent.upload(this.element() as HTMLElement, uploadFiles)) + return userEvent.upload(this.element(), file) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { - const options = (Array.isArray(options_) ? options_ : [options_]).map((option) => { - if (typeof option !== 'string' && 'element' in option) { - return option.element() as HTMLElement - } - return option - }) - return ensureAwaited(() => userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) - } - - async dropTo(): Promise { - throw new Error('The "preview" provider doesn\'t support `dropTo` method.') + return userEvent.selectOptions(this.element(), options_) } clear(): Promise { - return ensureAwaited(() => userEvent.clear(this.element())) - } - - async screenshot(): Promise { - throw new Error('The "preview" provider doesn\'t support `screenshot` method.') + return userEvent.clear(this.element()) } protected locator(selector: string) { diff --git a/test/browser/fixtures/user-event/keyboard.test.ts b/test/browser/fixtures/user-event/keyboard.test.ts index 372eecd62a0a..90541b433e67 100644 --- a/test/browser/fixtures/user-event/keyboard.test.ts +++ b/test/browser/fixtures/user-event/keyboard.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { userEvent, page, server } from '@vitest/browser/context' +import { userEvent, page } from '@vitest/browser/context' test('non US keys', async () => { document.body.innerHTML = ` @@ -34,3 +34,20 @@ test('non US keys', async () => { console.error(e) } }) + +test('click with modifier', async () => { + document.body.innerHTML = ` +
test shift and click
+ ` + const el = document.getElementById("test") + el.addEventListener("pointerup", (e) => { + if (e.shiftKey && e.type === 'pointerup') { + el.textContent += " [ok]" + } + }); + + await userEvent.keyboard('{Shift>}') + await userEvent.click(el) + await userEvent.keyboard('{/Shift}') + await expect.poll(() => el.textContent).toContain("[ok]") +})