Skip to content

Commit

Permalink
fix: userEvent works consistently between providers (#6480)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Sep 12, 2024
1 parent ac698b1 commit 0b4da69
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 76 deletions.
6 changes: 5 additions & 1 deletion docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Assertions<T>>
}
```

`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.

Expand Down
67 changes: 41 additions & 26 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -28,14 +29,14 @@ function triggerCommand<T>(command: string, ...args: any[]) {
return rpc().triggerCommand<T>(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))
Expand All @@ -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))
},
Expand All @@ -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!
}
Expand Down
23 changes: 20 additions & 3 deletions packages/browser/src/client/tester/locators/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,30 @@ class PreviewLocator extends Locator {
return userEvent.unhover(this.element())
}

fill(text: string): Promise<void> {
async fill(text: string): Promise<void> {
await this.clear()
return userEvent.type(this.element(), text)
}

async upload(file: string | string[] | File | File[]): Promise<void> {
// 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<void> {
Expand Down
47 changes: 3 additions & 44 deletions packages/browser/src/node/plugins/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<null | { id: string }>) {
if (provider.name !== 'preview') {
return ''
return 'const _userEventSetup = undefined'
}
const resolved = await resolve('@testing-library/user-event', __dirname)
if (!resolved) {
Expand Down
4 changes: 2 additions & 2 deletions test/browser/fixtures/locators/blog.test.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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)

Expand Down

0 comments on commit 0b4da69

Please sign in to comment.