Skip to content

命令 ​

命令是一个函数,它调用服务器上的另一个函数并将结果传递回浏览器。Vitest 公开了几个可以在浏览器测试中使用的内置命令。

内置命令 ​

文件处理 ​

在浏览器测试中,可借助 readFile、writeFile 与 removeFile 三个 API 完成文件操作。自 Vitest 3.2 起,所有路径均以 project 根目录为基准解析(根目录默认为 process.cwd(),可手动重写);旧版本则以当前测试文件所在目录为基准。

默认情况下,Vitest 使用 utf-8 编码,但你可以使用选项覆盖它。

提示

出于安全原因,内置的文件命令遵循 Vite 的 server.fs 限制。

writeFile 和 removeFile 还需要通过 api.allowWrite 获得写入权限。

ts
import { server } from 'vitest/browser'

const { readFile, writeFile, removeFile } = server.commands

it('handles files', async () => {
  const file = './test.txt'

  await writeFile(file, 'hello world')
  const content = await readFile(file)

  expect(content).toBe('hello world')

  await removeFile(file)
})

CDP Session ​

Vitest 通过 vitest/browser 中导出的 cdp 方法访问原始 Chrome DevTools 协议。它主要用于库作者在其基础上构建工具。

ts
import { cdp } from 'vitest/browser'

const input = document.createElement('input')
document.body.appendChild(input)
input.focus()

await cdp().send('Input.dispatchKeyEvent', {
  type: 'keyDown',
  text: 'a',
})

expect(input).toHaveValue('a')

注意

CDP session 仅适用于 playwright provider,并且仅在使用 chromium 浏览器时有效。有关详细信息,请参阅 playwright 的 CDPSession 文档。

CDP 是一种特权调试 API。仅当通过 api.allowWrite, and api.allowExec 启用浏览器 API 的写入及执行操作时,才可使用 CDP。

自定义命令 ​

我们也可以通过 browser.commands 配置选项添加自己的命令。如果我们正在开发一个库,可以通过插件内的 config 钩子来提供它们:

ts
import type { Plugin } from 'vitest/config'
import type { BrowserCommand } from 'vitest/node'

const myCustomCommand: BrowserCommand<[arg1: string, arg2: string]> = ({
  testPath,
  provider
}, arg1, arg2) => {
  if (provider.name === 'playwright') {
    console.log(testPath, arg1, arg2)
    return { someValue: true }
  }

  throw new Error(`provider ${provider.name} is not supported`)
}

export default function BrowserCommands(): Plugin {
  return {
    name: 'vitest:custom-commands',
    config() {
      return {
        test: {
          browser: {
            commands: {
              myCustomCommand,
            }
          }
        }
      }
    }
  }
}

然后,你可以通过从 vitest/browser 导入它,在测试中调用它:

ts
import { commands } from 'vitest/browser'
import { expect, test } from 'vitest'

test('custom command works correctly', async () => {
  const result = await commands.myCustomCommand('test1', 'test2')
  expect(result).toEqual({ someValue: true })
})

// 如果你正在使用 TypeScript,你可以扩展类型声明:

declare module 'vitest/browser' {
  interface BrowserCommands {
    myCustomCommand: (arg1: string, arg2: string) => Promise<{
      someValue: true
    }>
  }
}

注意

如果自定义命令具有相同的名称,则它们将覆盖内置命令。

安全

自定义命令在 Vitest 的 Node 进程中运行,浏览器测试代码可以通过 Vitest 的浏览器 RPC 连接调用这些命令。它们可以访问本地文件、环境变量、网络服务、数据库、shell 命令以及其他 Node API。

Vitest 的内置文件命令会根据 Vite 的 server.fs 限制校验路径,并单独检查是否允许写入。自定义命令不会自动继承这些保护措施。如果自定义命令接收浏览器提供的输入,并用它来读取、写入、删除、执行或暴露本地资源,请在使用前校验输入。

读取文件或加载 fixture 时,请使用 vitest/node 中的 isFileLoadingAllowed,或显式指定白名单。写入和删除操作还须有明确的修改策略,例如 api.allowWrite 为命令指定允许操作的目录。如果命令会执行代码、shell 命令或项目脚本,还须检查 api.allowExec。

例如,如果你自行创建文件写入命令,而不是使用 Vitest 内置的 writeFile,请执行相同的检查:

ts
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { normalizePath } from 'vite'
import { isFileLoadingAllowed } from 'vitest/node'
import type { BrowserCommand } from 'vitest/node'

function assertFileAccess(path: string, project: any) {
  if (
    !isFileLoadingAllowed(project.vite.config, path)
    && !isFileLoadingAllowed(project.vitest.vite.config, path)
  ) {
    throw new Error(`Access denied to "${path}".`)
  }
}

function assertWrite(project: any) {
  if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
    throw new Error('Writing files is disabled.')
  }
}

export const myWriteFileCommand: BrowserCommand<[path: string, content: string]> = async (
  { project },
  path,
  content,
) => {
  assertWrite(project)

  const file = resolve(project.config.root, path)
  assertFileAccess(normalizePath(file), project)

  await mkdir(dirname(file), { recursive: true })
  await writeFile(file, content)
}

记录追踪标记 ​

自定义命令可以通过 context.mark 为调用它的测试记录 追踪标记。它的作用与 page.mark 相同,但在服务端使用,用于在 追踪视图 中标注命令内部执行的自定义操作。

ts
import type { BrowserCommand } from 'vitest/node'

export const uploadFixture: BrowserCommand<[name: string]> = async (
  context,
  name,
) => {
  await context.mark(`upload start: ${name}`, { kind: 'action' })
  // 执行服务端操作...
  await context.mark(`upload done: ${name}`, { kind: 'action' })
}

如果未启用浏览器追踪,或当前会话中没有正在运行的测试,context.mark 不会执行任何操作。与 page.mark 不同,它不支持传入回调函数。

自定义 playwright 命令 ​

Vitest 在命令上下文中公开了几个playwright特定属性。

  • page引用包含测试 iframe 的完整页面。这是协调器 HTML,为避免出现问题,最好不要碰它。
  • frame 是一个异步方法,用于解析测试器 Frame。它的 API 与 page 类似,但不支持某些方法。如果你需要查询元素,应优先使用 context.iframe 代替,因为它更稳定、更快速。
  • iframe 是一个 FrameLocator,用于查询页面上的其他元素。
  • context 是指唯一的BrowserContext。
ts
import { BrowserCommand } from 'vitest/node'

export const myCommand: BrowserCommand<[string, number]> = async (
  ctx,
  arg1: string,
  arg2: number
) => {
  if (ctx.provider.name === 'playwright') {
    const element = await ctx.iframe.findByRole('alert')
    const screenshot = await element.screenshot()
    // 对截图进行一些操作。
    return difference
  }
}

自定义 webdriverio 命令 ​

Vitest 在上下文对象上公开了一些 webdriverio 特有属性。

  • browser 是 WebdriverIO.Browser API.

Vitest 会在每条命令执行前自动调用 browser.switchFrame,将 webdriver 上下文切换至测试 iframe ,因此 $ 与 $$ 获取的是 iframe 内的元素,而非 orchestrator 中的元素;非 webdriver API 则仍作用于父级 frame。