Skip to content

Commit 0b4da69

Browse files
authored
fix: userEvent works consistently between providers (#6480)
1 parent ac698b1 commit 0b4da69

File tree

5 files changed

+71
-76
lines changed

5 files changed

+71
-76
lines changed

docs/api/expect.md

Lines changed: 5 additions & 1 deletion
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

Lines changed: 41 additions & 26 deletions
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

Lines changed: 20 additions & 3 deletions
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

Lines changed: 3 additions & 44 deletions
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

Lines changed: 2 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy