Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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