From 0b4da69e11e5a4b763f0eab18b8dfb78d54b87da Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 12 Sep 2024 13:49:15 +0200 Subject: [PATCH] fix: userEvent works consistently between providers (#6480) --- docs/api/expect.md | 6 +- packages/browser/src/client/tester/context.ts | 67 ++++++++++++------- .../src/client/tester/locators/preview.ts | 23 ++++++- .../browser/src/node/plugins/pluginContext.ts | 47 +------------ test/browser/fixtures/locators/blog.test.tsx | 4 +- 5 files changed, 71 insertions(+), 76 deletions(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index 2e41dd93ab37..5327906b7e7e 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -61,7 +61,11 @@ test('expect.soft test', () => { ## poll -- **Type:** `ExpectStatic & (actual: () => any, options: { interval, timeout, message }) => Assertions` +```ts +interface ExpectPoll extends ExpectStatic { + (actual: () => T, options: { interval; timeout; message }): Promise> +} +``` `expect.poll` reruns the _assertion_ until it is succeeded. You can configure how many times Vitest should rerun the `expect.poll` callback by setting `interval` and `timeout` options. diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index b4331e76f43e..f498e89128a2 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -1,5 +1,6 @@ import type { RunnerTask } from 'vitest' import type { BrowserRPC } from '@vitest/browser/client' +import type { UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event' import type { BrowserPage, Locator, @@ -28,14 +29,14 @@ function triggerCommand(command: string, ...args: any[]) { return rpc().triggerCommand(contextId, command, filepath(), args) } -function createUserEvent(): UserEvent { +export function createUserEvent(__tl_user_event__?: TestingLibraryUserEvent): UserEvent { const keyboard = { unreleased: [] as string[], } return { - setup() { - return createUserEvent() + setup(options?: any) { + return createUserEvent(__tl_user_event__?.setup(options)) }, click(element: Element | Locator, options: UserEventClickOptions = {}) { return convertToLocator(element).click(processClickOptions(options)) @@ -49,30 +50,9 @@ function createUserEvent(): UserEvent { selectOptions(element, value) { return convertToLocator(element).selectOptions(value) }, - async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { - const selector = convertToSelector(element) - const { unreleased } = await triggerCommand<{ unreleased: string[] }>( - '__vitest_type', - selector, - text, - { ...options, unreleased: keyboard.unreleased }, - ) - keyboard.unreleased = unreleased - }, clear(element: Element | Locator) { return convertToLocator(element).clear() }, - 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 | Locator, options: UserEventHoverOptions = {}) { return convertToLocator(element).hover(processHoverOptions(options)) }, @@ -92,11 +72,46 @@ function createUserEvent(): UserEvent { const targetLocator = convertToLocator(target) return sourceLocator.dropTo(targetLocator, processDragAndDropOptions(options)) }, + + // testing-library user-event + async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { + 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', + selector, + text, + { ...options, unreleased: keyboard.unreleased }, + ) + keyboard.unreleased = unreleased + }, + tab(options: UserEventTabOptions = {}) { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.tab(options) + } + return triggerCommand('__vitest_tab', options) + }, + async keyboard(text: string) { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.keyboard(text) + } + const { unreleased } = await triggerCommand<{ unreleased: string[] }>( + '__vitest_keyboard', + text, + keyboard, + ) + keyboard.unreleased = unreleased + }, } } -export const userEvent = createUserEvent() - 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 cd2e6fa8b220..faed774a3a09 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -77,13 +77,30 @@ class PreviewLocator extends Locator { return userEvent.unhover(this.element()) } - fill(text: string): Promise { + async fill(text: string): Promise { + await this.clear() return userEvent.type(this.element(), text) } async upload(file: string | string[] | File | File[]): Promise { - // we override userEvent.upload to support this in pluginContext.ts - return userEvent.upload(this.element() as HTMLElement, file as File[]) + 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(base64) + .then(r => r.blob()) + .then(blob => new File([blob], basename, { type: mime })) + return fileInstance + }) + const uploadFiles = await Promise.all(uploadPromise) + return userEvent.upload(this.element() as HTMLElement, uploadFiles) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index a22b75c261a3..f431ad399477 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -67,7 +67,7 @@ async function generateContextFile( const distContextPath = slash(`/@fs/${resolve(__dirname, 'context.js')}`) return ` -import { page, userEvent as __userEvent_CDP__, cdp } from '${distContextPath}' +import { page, createUserEvent, cdp } from '${distContextPath}' ${userEventNonProviderImport} const filepath = () => ${filepathCode} const rpc = () => __vitest_worker__.rpc @@ -84,55 +84,14 @@ export const server = { config: __vitest_browser_runner__.config, } export const commands = server.commands -export const userEvent = ${getUserEvent(provider)} +export const userEvent = createUserEvent(_userEventSetup) export { page, cdp } ` } -function getUserEvent(provider: BrowserProvider) { - if (provider.name !== 'preview') { - return '__userEvent_CDP__' - } - // TODO: have this in a separate file - return String.raw`{ - ..._userEventSetup, - setup() { - const userEvent = __vitest_user_event__.setup() - userEvent.setup = this.setup - userEvent.fill = this.fill.bind(userEvent) - userEvent._upload = userEvent.upload.bind(userEvent) - userEvent.upload = this.upload.bind(userEvent) - userEvent.dragAndDrop = this.dragAndDrop - return userEvent - }, - async upload(element, file) { - const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => { - if (typeof file !== 'string') { - return file - } - - const { content: base64, basename, mime } = await rpc().triggerCommand(contextId, "__vitest_fileInfo", filepath(), [file, 'base64']) - const fileInstance = fetch(base64) - .then(r => r.blob()) - .then(blob => new File([blob], basename, { type: mime })) - return fileInstance - }) - const uploadFiles = await Promise.all(uploadPromise) - return this._upload(element, uploadFiles) - }, - 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') - } -}` -} - async function getUserEventImport(provider: BrowserProvider, resolve: (id: string, importer: string) => Promise) { if (provider.name !== 'preview') { - return '' + return 'const _userEventSetup = undefined' } const resolved = await resolve('@testing-library/user-event', __dirname) if (!resolved) { diff --git a/test/browser/fixtures/locators/blog.test.tsx b/test/browser/fixtures/locators/blog.test.tsx index fc54b916e3c6..991c0eacbf0e 100644 --- a/test/browser/fixtures/locators/blog.test.tsx +++ b/test/browser/fixtures/locators/blog.test.tsx @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { page } from '@vitest/browser/context' +import { page, userEvent } from '@vitest/browser/context' import Blog from '../../src/blog-app/blog' test('renders blog posts', async () => { @@ -18,7 +18,7 @@ test('renders blog posts', async () => { await expect.element(secondPost.getByRole('heading')).toHaveTextContent('qui est esse') - await secondPost.getByRole('button', { name: 'Delete' }).click() + await userEvent.click(secondPost.getByRole('button', { name: 'Delete' })) expect(screen.getByRole('listitem').all()).toHaveLength(3)