Skip to content

Commit 29b97d5

Browse files
committed
fix: userEvent works consistently between providers (#6480)
1 parent 0221212 commit 29b97d5

File tree

5 files changed

+71
-76
lines changed

5 files changed

+71
-76
lines changed

docs/api/expect.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ test('expect.soft test', () => {
6161

6262
## poll
6363

64-
- **Type:** `ExpectStatic & (actual: () => any, options: { interval, timeout, message }) => Assertions`
64+
```ts
65+
interface ExpectPoll extends ExpectStatic {
66+
(actual: () => T, options: { interval; timeout; message }): Promise<Assertions<T>>
67+
}
68+
```
6569

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

packages/browser/src/client/tester/context.ts

+41-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RunnerTask } from 'vitest'
22
import type { BrowserRPC } from '@vitest/browser/client'
3+
import type { UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event'
34
import type {
45
BrowserPage,
56
Locator,
@@ -28,14 +29,14 @@ function triggerCommand<T>(command: string, ...args: any[]) {
2829
return rpc().triggerCommand<T>(contextId, command, filepath(), args)
2930
}
3031

31-
function createUserEvent(): UserEvent {
32+
export function createUserEvent(__tl_user_event__?: TestingLibraryUserEvent): UserEvent {
3233
const keyboard = {
3334
unreleased: [] as string[],
3435
}
3536

3637
return {
37-
setup() {
38-
return createUserEvent()
38+
setup(options?: any) {
39+
return createUserEvent(__tl_user_event__?.setup(options))
3940
},
4041
click(element: Element | Locator, options: UserEventClickOptions = {}) {
4142
return convertToLocator(element).click(processClickOptions(options))
@@ -49,30 +50,9 @@ function createUserEvent(): UserEvent {
4950
selectOptions(element, value) {
5051
return convertToLocator(element).selectOptions(value)
5152
},
52-
async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) {
53-
const selector = convertToSelector(element)
54-
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
55-
'__vitest_type',
56-
selector,
57-
text,
58-
{ ...options, unreleased: keyboard.unreleased },
59-
)
60-
keyboard.unreleased = unreleased
61-
},
6253
clear(element: Element | Locator) {
6354
return convertToLocator(element).clear()
6455
},
65-
tab(options: UserEventTabOptions = {}) {
66-
return triggerCommand('__vitest_tab', options)
67-
},
68-
async keyboard(text: string) {
69-
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
70-
'__vitest_keyboard',
71-
text,
72-
keyboard,
73-
)
74-
keyboard.unreleased = unreleased
75-
},
7656
hover(element: Element | Locator, options: UserEventHoverOptions = {}) {
7757
return convertToLocator(element).hover(processHoverOptions(options))
7858
},
@@ -92,11 +72,46 @@ function createUserEvent(): UserEvent {
9272
const targetLocator = convertToLocator(target)
9373
return sourceLocator.dropTo(targetLocator, processDragAndDropOptions(options))
9474
},
75+
76+
// testing-library user-event
77+
async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) {
78+
if (typeof __tl_user_event__ !== 'undefined') {
79+
return __tl_user_event__.type(
80+
element instanceof Element ? element : element.element(),
81+
text,
82+
options,
83+
)
84+
}
85+
86+
const selector = convertToSelector(element)
87+
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
88+
'__vitest_type',
89+
selector,
90+
text,
91+
{ ...options, unreleased: keyboard.unreleased },
92+
)
93+
keyboard.unreleased = unreleased
94+
},
95+
tab(options: UserEventTabOptions = {}) {
96+
if (typeof __tl_user_event__ !== 'undefined') {
97+
return __tl_user_event__.tab(options)
98+
}
99+
return triggerCommand('__vitest_tab', options)
100+
},
101+
async keyboard(text: string) {
102+
if (typeof __tl_user_event__ !== 'undefined') {
103+
return __tl_user_event__.keyboard(text)
104+
}
105+
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
106+
'__vitest_keyboard',
107+
text,
108+
keyboard,
109+
)
110+
keyboard.unreleased = unreleased
111+
},
95112
}
96113
}
97114

98-
export const userEvent = createUserEvent()
99-
100115
export function cdp() {
101116
return getBrowserState().cdp!
102117
}

packages/browser/src/client/tester/locators/preview.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,30 @@ class PreviewLocator extends Locator {
7777
return userEvent.unhover(this.element())
7878
}
7979

80-
fill(text: string): Promise<void> {
80+
async fill(text: string): Promise<void> {
81+
await this.clear()
8182
return userEvent.type(this.element(), text)
8283
}
8384

8485
async upload(file: string | string[] | File | File[]): Promise<void> {
85-
// we override userEvent.upload to support this in pluginContext.ts
86-
return userEvent.upload(this.element() as HTMLElement, file as File[])
86+
const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => {
87+
if (typeof file !== 'string') {
88+
return file
89+
}
90+
91+
const { content: base64, basename, mime } = await this.triggerCommand<{
92+
content: string
93+
basename: string
94+
mime: string
95+
}>('__vitest_fileInfo', file, 'base64')
96+
97+
const fileInstance = fetch(base64)
98+
.then(r => r.blob())
99+
.then(blob => new File([blob], basename, { type: mime }))
100+
return fileInstance
101+
})
102+
const uploadFiles = await Promise.all(uploadPromise)
103+
return userEvent.upload(this.element() as HTMLElement, uploadFiles)
87104
}
88105

89106
selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise<void> {

packages/browser/src/node/plugins/pluginContext.ts

+3-44
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async function generateContextFile(
6767
const distContextPath = slash(`/@fs/${resolve(__dirname, 'context.js')}`)
6868

6969
return `
70-
import { page, userEvent as __userEvent_CDP__, cdp } from '${distContextPath}'
70+
import { page, createUserEvent, cdp } from '${distContextPath}'
7171
${userEventNonProviderImport}
7272
const filepath = () => ${filepathCode}
7373
const rpc = () => __vitest_worker__.rpc
@@ -84,55 +84,14 @@ export const server = {
8484
config: __vitest_browser_runner__.config,
8585
}
8686
export const commands = server.commands
87-
export const userEvent = ${getUserEvent(provider)}
87+
export const userEvent = createUserEvent(_userEventSetup)
8888
export { page, cdp }
8989
`
9090
}
9191

92-
function getUserEvent(provider: BrowserProvider) {
93-
if (provider.name !== 'preview') {
94-
return '__userEvent_CDP__'
95-
}
96-
// TODO: have this in a separate file
97-
return String.raw`{
98-
..._userEventSetup,
99-
setup() {
100-
const userEvent = __vitest_user_event__.setup()
101-
userEvent.setup = this.setup
102-
userEvent.fill = this.fill.bind(userEvent)
103-
userEvent._upload = userEvent.upload.bind(userEvent)
104-
userEvent.upload = this.upload.bind(userEvent)
105-
userEvent.dragAndDrop = this.dragAndDrop
106-
return userEvent
107-
},
108-
async upload(element, file) {
109-
const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => {
110-
if (typeof file !== 'string') {
111-
return file
112-
}
113-
114-
const { content: base64, basename, mime } = await rpc().triggerCommand(contextId, "__vitest_fileInfo", filepath(), [file, 'base64'])
115-
const fileInstance = fetch(base64)
116-
.then(r => r.blob())
117-
.then(blob => new File([blob], basename, { type: mime }))
118-
return fileInstance
119-
})
120-
const uploadFiles = await Promise.all(uploadPromise)
121-
return this._upload(element, uploadFiles)
122-
},
123-
async fill(element, text) {
124-
await this.clear(element)
125-
await this.type(element, text)
126-
},
127-
dragAndDrop: async () => {
128-
throw new Error('Provider "preview" does not support dragging elements')
129-
}
130-
}`
131-
}
132-
13392
async function getUserEventImport(provider: BrowserProvider, resolve: (id: string, importer: string) => Promise<null | { id: string }>) {
13493
if (provider.name !== 'preview') {
135-
return ''
94+
return 'const _userEventSetup = undefined'
13695
}
13796
const resolved = await resolve('@testing-library/user-event', __dirname)
13897
if (!resolved) {

test/browser/fixtures/locators/blog.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from 'vitest'
2-
import { page } from '@vitest/browser/context'
2+
import { page, userEvent } from '@vitest/browser/context'
33
import Blog from '../../src/blog-app/blog'
44

55
test('renders blog posts', async () => {
@@ -18,7 +18,7 @@ test('renders blog posts', async () => {
1818

1919
await expect.element(secondPost.getByRole('heading')).toHaveTextContent('qui est esse')
2020

21-
await secondPost.getByRole('button', { name: 'Delete' }).click()
21+
await userEvent.click(secondPost.getByRole('button', { name: 'Delete' }))
2222

2323
expect(screen.getByRole('listitem').all()).toHaveLength(3)
2424

0 commit comments

Comments
 (0)