资讯动态

Vitest browser.commands 配置详解:在浏览器测试中安全扩展服务端命令

发布时间:2026/9/14 11:41:00 来源:尧图企业网站定制
Vitest browser.commands 配置详解在浏览器测试中安全扩展服务端命令【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest本文围绕 Vitest 浏览器模式的核心配置项browser.commands展开它是如何在 Vitest Node 进程中执行、如何从vitest/browser在测试代码里调用、内置文件命令readFile/writeFile/removeFile背后的路径校验与写权限机制以及自定义命令的完整编写方式与安全边界。读完本文你既能把自定义命令接入自己的浏览器测试项目也能从源码层面理解命令的分发链路RPC → 上下文构建 → 命令注册表与安全防护点。配置项总览browser.commands的官方定义为类型Recordstring, BrowserCommand默认值{ readFile, writeFile, ... }即一组内置文件处理命令它的作用是注册一批可以在浏览器测试中从vitest/browser导入并调用的自定义命令。每个命令本身是一个运行在 Vitest Node 进程中的函数浏览器侧发起调用后结果会经由 RPC 通道回传到浏览器。在类型定义中BrowserCommand是一个泛型函数签名参数元组Payload描述命令接受的参数列表ReturnValue描述返回值// packages/vitest/src/node/types/browser.ts#L468-L470 export interface BrowserCommandPayload extends unknown[] [], ReturnValue any { (context: BrowserCommandContext, ...payload: Payload): AwaitableReturnValue }命令的第一个参数是BrowserCommandContext上下文其余参数来自浏览器侧的调用实参。上下文包含以下关键成员见 browser.ts成员说明testPath触发该命令的测试文件路径provider当前浏览器 provider如playwright、webdriverioproject所属TestProject可读取project.config.root、project.vitest.config等sessionId当前浏览器会话 IDmark为触发命令的测试记录 trace 标记服务端等价于page.marktriggerCommand在命令内部再触发另一个命令实现命令间编排__ensureCDPHandler获取当前 tester 连接上缓存的 CDP handler内部 API此外provider 还可以往上下文上挂载自己的专属字段playwright 提供page、frame、iframe、contextwebdriverio 提供browser调用前 Vitest 会自动switchFrame到测试 iframe。内置命令文件处理三件套内置文件命令实现在 fs.ts包含readFile、writeFile、removeFile和一个内部的_fileInfo。以readFile为例// packages/browser/src/node/commands/fs.ts#L8-L18 export const readFile: BrowserCommand ParametersBrowserCommands[readFile] async ({ project }, path, options {}) { const filepath resolve(project.config.root, path) assertBrowserFileAccess(project, filepath) // never return a Buffer if (typeof options object !options.encoding) { options.encoding utf-8 } return fsp.readFile(filepath, options) }三个关键实现细节值得注意路径解析基准resolve(project.config.root, path)—— 自 Vitest 3.2 起所有路径都相对 project root即process.cwd()除非手动覆盖解析更早版本是相对测试文件。默认编码默认使用utf-8可通过 options 覆盖但永远不会向浏览器返回Buffer。权限校验writeFile与removeFile在解析路径前会先执行assertBrowserApiWrite读取类命令则执行assertBrowserFileAccess。这两个断言函数的源码位于 utils.tsexport function assertBrowserFileAccess(project: TestProject, path: string): void { const normalized slash(path) if ( !isFileLoadingAllowed(project.vite.config, normalized) !isFileLoadingAllowed(project.vitest.vite.config, normalized) ) { throw new Error( Access denied to ${path}. See Vite config documentation for server.fs: https://vitejs.dev/config/server-options.html#server-fs-strict., ) } } export function assertBrowserApiWrite(project: TestProject, path: string): void { if (!project.config.api.allowWrite || !project.vitest.config.api.allowWrite) { throw new Error( Cannot modify file ${path}. File writing is disabled because the server is exposed to the internet, see https://vitest.dev/config/browser/api., ) } }也就是说内置文件命令遵循 Vite 的server.fs目录边界同时检查项目自身与父级两个 Vite 配置而writeFile/removeFile还额外要求项目级与全局的api.allowWrite都为真writeFile若目标目录不存在还会自动mkdir -p创建。在浏览器测试中的典型用法继承自官方 Commands API 文档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) })自定义命令定义、注册与调用自定义命令通过browser.commands配置项注册。官方文档给出的最小形态是一个带参的BrowserCommand根据provider.name区分支持情况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导入commands调用并可用声明合并补全类型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 }) }) // if you are using TypeScript, you can augment the module declare module vitest/browser { interface BrowserCommands { myCustomCommand: (arg1: string, arg2: string) Promise{ someValue: true } } }注册链路自定义命令如何覆盖内置命令从源码结构看命令注册发生在浏览器父项目的初始化阶段projectParent.ts// 先注册内置命令且只在名字未被占用时写入 for (const [name, command] of Object.entries(builtinCommands)) { this.commands[name] ?? command } // 校验命令名后无条件覆盖注册用户命令 for (const command in this.config.browser.commands) { if (!/^[a-z_$][\w$]*$/i.test(command)) { throw new Error( Invalid command name ${command}. Only alphanumeric characters, $ and _ are allowed., ) } this.commands[command] this.config.browser.commands[command] }由此可以确认两个事实同名覆盖自定义命令会与内置命令同名时直接覆盖它内置命令用??让位。这也与 API 文档中的警告一致——“Custom functions will override built-in ones if they have the same name”。命令名即标识符命令名必须匹配/^[a-z_$][\w$]*$/i因为浏览器侧以commands.xxx属性访问的方式调用它含-、.等字符的名字会在启动时报错。provider 侧注册命令时ProjectBrowser.registerCommand也执行同样的正则校验。分发链路浏览器调用如何到达 Node 命令浏览器侧的调用最终到达 WebSocket RPC 通道。在 rpc.ts 中triggerCommand(sessionId, command, testPath, payload)处理器做了三件事取当前项目的 provider组装命令上下文testPath、project、provider、sessionId、mark转发到 tester 的pageMark用于在 trace 中标注命令动作、triggerCommand供命令间互相调用、__ensureCDPHandler并合并provider.getCommandsContext(sessionId)注入的 provider 专属字段playwright 的page/iframe等通过project.browser!.triggerCommand在命令注册表中查找并执行命令将返回值经flatted序列化后回传浏览器。查找逻辑在 ProjectBrowser.triggerCommand先查项目自身注册表provider 注册的命令如 playwright 的page.*系列再回落到父项目注册表内置命令 browser.commands配置都找不到则抛出Provider ${name} does not support command ${name}。配置本身在 resolveConfig.ts 中被初始化为空对象resolved.browser.commands ?? {}因此不写该配置项时仅存在内置命令。provider 专属命令上下文字段playwrightVitest 在命令上下文上暴露几个专属属性见 commands API 文档page包含测试 iframe 的完整页面即 orchestrator HTML一般不应直接操作以免破坏结构frame异步方法解析出 tester 的FrameAPI 与page类似但支持的方法更少iframeFrameLocator查询页面元素时应优先使用它更稳定且更快context对应的BrowserContext。示例——在服务端截取页面中某个元素的截图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() // do something with the screenshot } }webdriverio上下文上暴露browserWebdriverIO.BrowserAPI。Vitest 在调用命令前会自动browser.switchFrame切换到测试 iframe因此$/$$选择器作用于 iframe 内的元素而非 orchestrator但非 webdriver API 仍指向父 frame 上下文。在命令中记录 trace 标记自定义命令可以通过context.mark为触发它的测试记录 trace 标记这是page.mark的服务端等价物用于在 trace view 中标注命令内部执行的自定义动作import type { BrowserCommand } from vitest/node export const uploadFixture: BrowserCommand[name: string] async ( context, name, ) { await context.mark(upload start: ${name}, { kind: action }) // ... do server-side work await context.mark(upload done: ${name}, { kind: action }) }context.mark在未启用浏览器 tracing 或当前会话没有测试运行时是 no-op与page.mark不同它不接受回调形式。安全边界自定义命令必须自行防护这是browser.commands文档中最重要的一段警告值得完整展开。Commands run in the Vitest Node process.If a command exposes filesystem, process, network, database, or shell access based on browser-provided input, validate and restrict that input inside the command. Built-in file commands apply Viteserver.fschecks and write-access checks, but custom commands are responsible for their own protections.翻译成工程约束执行位置命令函数运行在 Vitest 的 Node 进程中可以访问本地文件、环境变量、网络、数据库、shell 等全部 Node API浏览器测试代码通过 RPC 通道以受控方式调用它们。内置防护不会自动继承内置文件命令的路径校验Viteserver.fs边界 api.allowWrite不会传递给自定义命令。如果你的自定义命令接受浏览器传入的路径并据此读写/删除/执行必须在命令内部自行校验。推荐的防护组合来自 Custom Commands security notes文件读取/fixture 加载使用isFileLoadingAllowed来自vitest/node或显式允许列表写入/删除额外要求api.allowWrite且限定命令级别的允许目录执行代码、shell 命令或项目脚本的命令额外检查api.allowExec。官方的自定义writeFile命令安全写法示例完整继承自 API 文档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) }这套写法与内置命令 fs.ts 的实现模式一一对应先做写权限断言再resolve到项目root下最后通过isFileLoadingAllowed把路径约束在server.fs边界内。适用前提与限制小结browser.commands只在浏览器模式下生效命令经由浏览器 RPC 连接调用非浏览器测试无法使用。命令名必须满足标识符规则字母/数字/$/_字母或$_开头非法名字在启动阶段即报错。内置文件命令受 Viteserver.fs边界与api.allowWrite约束自定义命令不继承这些约束防护责任在命令实现方。CDP 会话cdp()是另一个相邻能力仅在playwrightprovider chromium下可用且同时要求api.allowWrite与api.allowExec开启校验逻辑见 rpc.ts 中的assertCdpAllowed。本文结论以当前仓库源码为准关键参考文件browser.commands 配置文档、Commands API 文档、browser 类型定义、内置文件命令实现、命令注册与分发、RPC 触发器、playwright provider 命令注册。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价