交互性 API
Vitest 使用 Chrome DevTools Protocol 或 webdriver 实现了 @testing-library/user-event 库的子集 API,而不是伪造事件,这使得浏览器行为更加可靠和一致,符合用户与页面交互的方式。
import { userEvent } from 'vitest/browser'
await userEvent.click(document.querySelector('.button'))几乎所有的 userEvent 方法都继承了其提供者的选项。
userEvent.setup
function setup(): UserEvent创建一个新的用户事件实例。如果需要保持键盘状态,以便正确按下和释放按钮,这将非常有用。
注意
与 @testing-library/user-event 不同,来自 vitest/browser 的默认 userEvent 实例只创建一次,而不是每次调用其方法时都创建一次!你可以从本代码段中看到其工作方式的不同之处:
import { userEvent as vitestUserEvent } from 'vitest/browser'
import { userEvent as originalUserEvent } from '@testing-library/user-event'
import { userEvent as vitestUserEvent } from '@vitest/browser/context'
await vitestUserEvent.keyboard('{Shift}') // 按住 shift 键不放
await vitestUserEvent.keyboard('{/Shift}') // 放开 shift 键不放
await originalUserEvent.keyboard('{Shift}') // 按住 shift 键不放
await originalUserEvent.keyboard('{/Shift}') // 没有放开 shift 键,因为状态不同这种行为更有用,因为我们并没有模拟键盘,而是实际按下了 Shift 键,所以保留原来的行为会在字段中键入时造成意想不到的问题。
注意
使用 playwright 和 webdriverio provider 时,交互由底层浏览器 provider 执行。这意味着同一文件中的测试之间可能会保留某些交互状态,例如按下的按键、指针位置以及由此产生的悬停状态。
Vitest 会在每个测试用例开始前自动重置尚未释放的键盘状态,但不会自动重置指针位置及由此产生的悬停状态,这是因为重置指针位置的开销可能很大。
这一点既适用于 userEvent.* 调用,也适用于 locator.click() 或 locator.hover() 等定位器方法,因为它们使用相同的底层交互状态。
如果测试依赖于无元素被悬停的状态,请显式重置该状态,例如在 beforeEach 中进行重置:
import { beforeEach } from 'vitest'
import { userEvent } from 'vitest/browser'
beforeEach(async () => {
await userEvent.unhover(document.body)
})userEvent.click
function click(
element: Element | Locator,
options?: UserEventClickOptions,
): Promise<void>点击元素。继承 provider 的选项。有关此方法如何工作的详细说明,请参阅 provider 的文档。
import { page, userEvent } from 'vitest/browser'
test('clicks on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
await userEvent.click(logo)
// 或者你可以直接从定位器上访问
await logo.click()
// 在 WebdriverIO 中,该方法根据参数情况使用 ElementClick(无参数时)或行为链(有参数时)
// 传入空对象可以强制使用行为链
await logo.click({})
})使用修饰键点击
使用 WebdriverIO 或 Playwright:
await userEvent.keyboard('{Shift>}')
// 通过传入空对象作为选项参数,该方法强制会选择使用行为链
// 而非 webdriver 的原生 ElementClick
// 由于 Firefox 存在一个 bug ,所以必须使用这种方式
// 关注 https://bugzilla.mozilla.org/show_bug.cgi?id=1456642 以获取该问题的修复进展
await userEvent.click(element, {})
await userEvent.keyboard('{/Shift}')使用 Playwright:
await userEvent.click(element, { modifiers: ['Shift'] })相关链接:
userEvent.dblClick
function dblClick(
element: Element | Locator,
options?: UserEventDoubleClickOptions,
): Promise<void>触发元素的双击事件
请参阅你的 provider 的文档以获取有关此方法如何工作的详细说明。
import { page, userEvent } from 'vitest/browser'
test('triggers a double click on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
await userEvent.dblClick(logo)
// 或者你可以直接从定位器上访问
await logo.dblClick()
})相关链接:
userEvent.tripleClick
function tripleClick(
element: Element | Locator,
options?: UserEventTripleClickOptions,
): Promise<void>在元素上触发三连击事件。由于浏览器 API 中没有 tripleclick,此方法会连续触发三次点击事件,因此你必须检查 点击事件的 detail 属性 来过滤事件:evt.detail === 3。
请参阅你的提供商文档以获取有关此方法工作原理的详细说明。
import { page, userEvent } from 'vitest/browser'
test('triggers a triple click on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
let tripleClickFired = false
logo.addEventListener('click', (evt) => {
if (evt.detail === 3) {
tripleClickFired = true
}
})
await userEvent.tripleClick(logo)
// 或者你可以直接从定位器上访问
await logo.tripleClick()
expect(tripleClickFired).toBe(true)
})相关链接:
- Playwright
locator.clickAPI:通过click方法并设置clickCount: 3来实现。 - WebdriverIO
browser.actionAPI:通过动作 API 实现,包含move操作加上连续的三个down + up + pause事件。 - testing-library
tripleClickAPI:通过tripleClick方法实现。
userEvent.wheel 4.1.0+
function wheel(
element: Element | Locator,
options: UserEventWheelOptions,
): Promise<void>在元素上触发 wheel 事件。
你可以使用 delta 指定滚动量,以像素为单位进行精确控制;也可以使用 direction 进行更简单的定向滚动(up、down、left、right)。需要触发多次滚轮事件时,请使用 times 选项获得更好的性能,而不是多次调用此方法。
import { page, userEvent } from 'vitest/browser'
test('scroll using delta values', async () => {
const tablist = page.getByRole('tablist')
// 向右滚动 100 像素
await userEvent.wheel(tablist, { delta: { x: 100 } })
// 向下滚动 50 像素
await userEvent.wheel(tablist, { delta: { y: 50 } })
// 沿对角线滚动 2 次
await userEvent.wheel(tablist, { delta: { x: 50, y: 100 }, times: 2 })
})
test('scroll using direction', async () => {
const tablist = page.getByRole('tablist')
// 向右滚动 5 次
await userEvent.wheel(tablist, { direction: 'right', times: 5 })
// 向左滚动 1 次
await userEvent.wheel(tablist, { direction: 'left' })
})也可以直接通过 定位器 触发滚轮事件:
import { page } from 'vitest/browser'
await page.getByRole('tablist').wheel({ direction: 'right' })注意
此方法用于测试显式监听 wheel 事件的 UI(例如自定义缩放控制、水平滚动标签页或 canvas 交互)。如果需要滚动页面以使元素进入视口,请改用其他 userEvent 方法或 定位器操作 提供的内置自动滚动功能。
userEvent.fill
function fill(
element: Element | Locator,
text: string,
): Promise<void>为 input、textarea 或 contenteditable 元素设置新的内容,并且在赋值前会先清空其中已有的文本。
import { page, userEvent } from 'vitest/browser'
test('update input', async () => {
const input = page.getByRole('input')
await userEvent.fill(input, 'foo') // input.value == foo
await userEvent.fill(input, '{{a[[') // input.value == {{a[[
await userEvent.fill(input, '{Shift}') // input.value == {Shift}
// 或者你可以直接从定位器上访问
await input.fill('foo') // input.value == foo
})该方法聚焦元素、填充元素并在填充后触发一个 input 事件。你可以使用空字符串来清除字段。
提示
该 API 比使用 userEvent.type 或 userEvent.keyboard 更快,但 不支持 user-event keyboard syntax(例如,{Shift}{selectall})。
在不需要输入特殊字符或对按键事件进行细粒度控制的情况下,我们建议使用此 API 而不是 userEvent.type。
相关链接:
userEvent.keyboard
function keyboard(text: string): Promise<void>通过 userEvent.keyboard 可以触发键盘输入。如果任何输入有焦点,它就会在该输入中键入字符。否则,它将触发当前焦点元素(如果没有焦点元素,则为 document.body)上的键盘事件。
此 API 支持 user-event keyboard 语法。可以在大括号内引用的常见特殊键包括:
- 修饰符:
{Shift},{Control},{Alt},{Meta} - 导航键:
{ArrowUp},{ArrowDown},{ArrowLeft},{ArrowRight},{Home},{End},{PageUp},{PageDown} - 编辑键:
{Backspace},{Delete},{Insert},{Tab},{Enter},{Escape} - 功能键:
{F1}through{F12}
注意:具体支持的按键集合可能因底层浏览器 provider(Playwright 与 WebdriverIO)而异。如果某个按键未触发预期行为,请查阅对应 provider 的文档或提交 issue。
import { userEvent } from 'vitest/browser'
test('trigger keystrokes', async () => {
await userEvent.keyboard('foo') // 转化成:f, o, o
await userEvent.keyboard('{{a[[') // 转化成:{, a, [
await userEvent.keyboard('{Shift}{f}{o}{o}') // 转化成:Shift, f, o, o
await userEvent.keyboard('{a>5}') // 按 a 不松开,触发 5 次按键
await userEvent.keyboard('{a>5/}') // 按 a 键 5 次,然后松开
})相关链接:
userEvent.tab
function tab(options?: UserEventTabOptions): Promise<void>发送一个 Tab 键事件。这是 userEvent.keyboard('{tab}') 的简写。
import { page, userEvent } from 'vitest/browser'
test('tab works', async () => {
const [input1, input2] = page.getByRole('input').elements()
expect(input1).toHaveFocus()
await userEvent.tab()
expect(input2).toHaveFocus()
await userEvent.tab({ shift: true })
expect(input1).toHaveFocus()
})相关链接:
userEvent.type
function type(
element: Element | Locator,
text: string,
options?: UserEventTypeOptions,
): Promise<void>注意
如果不依赖 special characters(例如,{shift} 或 {selectall}),建议使用 userEvent.fill。
type 方法在 keyboard API 的基础上实现了 @testing-library/user-event 的 type 工具。
你可以使用此函数向 input 、textarea 或 contenteditable 元素中模拟键盘输入。它兼容 user-event 提供的 keyboard 语法。
如果只需按下字符而无需输入,请使用 userEvent.keyboard API。
import { page, userEvent } from 'vitest/browser'
test('update input', async () => {
const input = page.getByRole('input')
await userEvent.type(input, 'foo') // input.value == foo
await userEvent.type(input, '{{a[[') // input.value == foo{a[
await userEvent.type(input, '{Shift}') // input.value == foo{a[
})说明
Vitest 没有像 input.type 那样在定位器上公开 .type 方法,因为它的存在只是为了与 userEvent 库兼容。请考虑使用 .fill,因为它更快。
相关链接:
userEvent.clear
function clear(element: Element | Locator, options?: UserEventClearOptions): Promise<void>此方法会清除输入元素的内容。
import { page, userEvent } from 'vitest/browser'
test('clears input', async () => {
const input = page.getByRole('input')
await userEvent.fill(input, 'foo')
expect(input).toHaveValue('foo')
await userEvent.clear(input)
// 或者你可以直接从定位器上访问
await input.clear()
expect(input).toHaveValue('')
})相关链接:
userEvent.selectOptions
function selectOptions(
element: Element | Locator,
values:
| HTMLElement
| HTMLElement[]
| Locator
| Locator[]
| string
| string[],
options?: UserEventSelectOptions,
): Promise<void>The userEvent.selectOptions allows selecting a value in a <select> element.
注意
如果 select 元素没有 multiple 属性,Vitest 将只选择数组中的第一个元素。
与 @testing-library 不同,Vitest 目前不支持 listbox,但我们计划在将来添加对它的支持。
import { page, userEvent } from 'vitest/browser'
test('clears input', async () => {
const select = page.getByRole('select')
await userEvent.selectOptions(select, 'Option 1')
// 或者你可以直接从定位器上访问
await select.selectOptions('Option 1')
expect(select).toHaveValue('option-1')
await userEvent.selectOptions(select, 'option-1')
expect(select).toHaveValue('option-1')
await userEvent.selectOptions(select, [
page.getByRole('option', { name: 'Option 1' }),
page.getByRole('option', { name: 'Option 2' }),
])
expect(select).toHaveValue(['option-1', 'option-2'])
})注意
webdriverio provider 不支持选择多个元素,因为它不提供选择多个元素的 API。
相关链接:
- Playwright
locator.selectOptionAPI - WebdriverIO
element.selectByIndexAPI - testing-library
selectOptionsAPI
userEvent.hover
function hover(
element: Element | Locator,
options?: UserEventHoverOptions,
): Promise<void>该方法将光标位置移动到所选元素上。有关此方法如何工作的详细说明,请参阅 provider 的文档。
注意
如果使用的是 webdriverio provider,光标默认会移动到元素的中心。
如果使用的是 playwright provider,光标会移动到元素的某个可见点。
import { page, userEvent } from 'vitest/browser'
test('hovers logo element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
await userEvent.hover(logo)
// 或者你可以直接从定位器上访问
await logo.hover()
})相关链接:
userEvent.unhover
function unhover(
element: Element | Locator,
options?: UserEventHoverOptions,
): Promise<void>其作用与 userEvent.hover 相同,但会将光标移至 document.body 元素。
注意
默认情况下,光标位置位于 body 元素的 "某个" 可见位置(在 playwright provider 中)或中心位置(在 webdriverio provider 中),因此如果当前悬停的元素已经位于相同位置,本方法将不起作用。
import { page, userEvent } from 'vitest/browser'
test('unhover logo element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
await userEvent.unhover(logo)
// 或者你可以直接从定位器上访问
await logo.unhover()
})相关链接:
userEvent.upload
function upload(
element: Element | Locator,
files: string[] | string | File[] | File,
options?: UserEventUploadOptions,
): Promise<void>更改文件输入元素,使其包含指定文件。
import { page, userEvent } from 'vitest/browser'
test('can upload a file', async () => {
const input = page.getByRole('button', { name: /Upload files/ })
const file = new File(['file'], 'file.png', { type: 'image/png' })
await userEvent.upload(input, file)
// 或者你可以直接从定位器上访问
await input.upload(file)
// 也可以使用相对于项目根目录的文件路径
await userEvent.upload(input, './fixtures/file.png')
})注意
webdriverio provider 仅在 chrome 和 edge 浏览器中支持该命令。目前也只支持字符串类型。
相关链接:
userEvent.dragAndDrop
function dragAndDrop(
source: Element | Locator,
target: Element | Locator,
options?: UserEventDragAndDropOptions,
): Promise<void>将源元素拖到目标元素的顶部。不要忘记,源元素的 draggable 属性必须设置为 true。
import { page, userEvent } from 'vitest/browser'
test('drag and drop works', async () => {
const source = page.getByRole('img', { name: /logo/ })
const target = page.getByTestId('logo-target')
await userEvent.dragAndDrop(source, target)
// 或者你可以直接从定位器上访问
await source.dropTo(target)
await expect.element(target).toHaveTextContent('Logo is processed')
})注意
preview provider 不支持此 API。
相关链接:
userEvent.copy
function copy(): Promise<void>将选中的文本复制到剪贴板。
import { page, userEvent } from 'vitest/browser'
test('copy and paste', async () => {
// 写入 'source'
await userEvent.click(page.getByPlaceholder('source'))
await userEvent.keyboard('hello')
// 选择并复制 'source'
await userEvent.dblClick(page.getByPlaceholder('source'))
await userEvent.copy()
// 粘贴到 'target'
await userEvent.click(page.getByPlaceholder('target'))
await userEvent.paste()
await expect.element(page.getByPlaceholder('source')).toHaveTextContent('hello')
await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello')
})相关链接:
userEvent.cut
function cut(): Promise<void>将选中的文本剪切到剪贴板。
import { page, userEvent } from 'vitest/browser'
test('copy and paste', async () => {
// 写入 'source'
await userEvent.click(page.getByPlaceholder('source'))
await userEvent.keyboard('hello')
// 选择并剪切 'source'
await userEvent.dblClick(page.getByPlaceholder('source'))
await userEvent.cut()
// 粘贴到 'target'
await userEvent.click(page.getByPlaceholder('target'))
await userEvent.paste()
await expect.element(page.getByPlaceholder('source')).toHaveTextContent('')
await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello')
})相关链接:
userEvent.paste
function paste(): Promise<void>将文本从剪贴板粘贴。请参阅 userEvent.copy 和 userEvent.cut 以获取使用示例。
相关链接: