首页
/ OpenScreen 测试体系实战:用 Vitest 双配置写单元测试与真实浏览器测试

OpenScreen 测试体系实战:用 Vitest 双配置写单元测试与真实浏览器测试

2026-09-05 11:06:25作者:昌雅子Ethen

OpenScreen 是一个基于 Electron + React 的开源录屏与视频编辑工具,其核心导出管线依赖 WebCodecs、MediaRecorder、OffscreenCanvas 等真实浏览器 API,无法在纯 Node 环境下验证。为此项目建立了一套"双配置"的 Vitest 测试体系:vitest.config.ts 面向 jsdom 环境的单元测试,vitest.browser.config.ts 面向 Playwright 驱动的无头 Chromium 浏览器测试。读完后,你将掌握如何根据代码是否依赖真实浏览器 API 来选择测试类型、如何按项目规范放置测试文件、如何加载测试固件(fixture)资源,以及如何在本地一键运行两套测试。

双配置体系总览

项目使用 Vitest 同时承担单元测试/集成测试与浏览器测试,两套配置各管各的文件,互不干扰:

单元测试 浏览器测试
配置文件 vitest.config.ts vitest.browser.config.ts
运行环境 jsdom(模拟 DOM,无真实浏览器) 真实 Chromium(Playwright 驱动,无头模式)
文件匹配 *.test.* / *.spec.*(排除 .browser.test.* src/**/*.browser.test.ts(x)
本地命令 npm run test(一次)、npm run test:watch(监听) npm run test:browser:install,再 npm run test:browser

两套配置在 package.json 中对应明确的脚本入口:

"test": "vitest --run",
"test:watch": "vitest",
"test:browser": "vitest --config vitest.browser.config.ts --run",
"test:browser:install": "playwright install --with-deps chromium-headless-shell"

其中 test:browser:install 是首次运行前的一次性步骤,用于下载 Chromium headless shell 及其系统依赖;日常运行只需 test:browser

单元测试:jsdom 环境与纯逻辑验证

配置与适用场景

单元测试运行在 jsdom 提供的模拟 DOM 中,适合纯函数、工具函数、数据转换,以及一切不依赖真实浏览器 API(Canvas、WebCodecs、MediaRecorder 等)的代码。vitest.config.ts 的关键配置如下:

import path from "node:path";
import { defineConfig } from "vitest/config";

export default defineConfig({
	test: {
		globals: true,
		environment: "jsdom",
		include: ["{src,electron}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
		exclude: ["src/**/*.browser.test.{ts,tsx}"],
	},
	resolve: {
		alias: {
			"@": path.resolve(__dirname, "src"),
		},
	},
});

几个值得注意的细节:

  • globals: true 开启全局 API,但项目中的测试文件仍然显式 import { describe, expect, it } from "vitest",两种写法并存;
  • 实际的匹配范围是 {src,electron} 两个目录下的 *.test.**.spec.* 文件——从源码看,主进程(Electron side)代码同样被纳入单元测试范围,例如 recordingStream.test.ts 就在 electron/ipc/ 下直接测试 RecordingStreamRegistry 的流式落盘逻辑;
  • exclude 显式排除 src/**/*.browser.test.ts(x),保证两套配置的文件集合互斥,不会重复执行;
  • @ 别名指向 src/,与浏览器测试配置保持一致。

文件放置约定

测试文件与源码文件同目录放置(co-locate),或放入同目录下的 __tests__/ 文件夹集中管理:

src/lib/compositeLayout.ts
src/lib/compositeLayout.test.ts        # co-located

src/i18n/__tests__/tutorialHelpTranslations.test.ts  # grouped

典型示例:布局计算的单元测试

compositeLayout.test.ts 测试的是导出画布中屏幕与摄像头窗口的布局计算函数 computeCompositeLayout——这是纯几何计算,天然适合单元测试。项目文档给出的最小示例:

import { describe, expect, it } from "vitest";
import { computeCompositeLayout } from "./compositeLayout";

describe("computeCompositeLayout", () => {
  it("anchors the overlay in the lower-right corner", () => {
    const layout = computeCompositeLayout({
      canvasSize: { width: 1920, height: 1080 },
      screenSize: { width: 1920, height: 1080 },
      webcamSize: { width: 1280, height: 720 },
    });

    expect(layout).not.toBeNull();
    expect(layout!.webcamRect!.x).toBeGreaterThan(1920 / 2);
    expect(layout!.webcamRect!.y).toBeGreaterThan(1080 / 2);
  });
});

实际仓库中的该文件覆盖了更多边界场景,可作为编写高质量单元测试的参考模板:

  • 越界约束:断言 webcamRect 的右下角不超过画布尺寸(x + width <= 1920y + height <= 1080);
  • 参数钳制:验证 webcamSizePreset 被钳制在 10–50 的有效范围内,传入 1 与传入 10 的结果一致,传入 100 与传入 50 的结果一致;
  • 横竖屏一致性:同像素总量下,1920×1080 横屏与 1080×1920 竖屏产生的摄像头窗口面积应完全相等;
  • 布局预设:分别验证 vertical-stack(上下堆叠)与 dual-frame(双框 2:1 分屏)两种布局下的具体坐标与比例;
  • 蒙版形状:圆形/方形蒙版强制宽高相等,rounded 蒙版的 borderRadius 大于 rectangle

i18n 键值覆盖率测试

文档将"i18n key coverage"列为单元测试场景,仓库中的对应实现是 tutorialHelpTranslations.test.ts。它的做法是:导入全部 13 个语言包(与 config.tsSUPPORTED_LOCALES 定义的 enaresfritja-JPko-KRrutrvipt-BRzh-CNzh-TW 一致),遍历一组固定的 tutorialHelpKeys 键列表,断言每个语言包的每个键都存在、是字符串且(除白名单内的可空键外)非空:

for (const locale of SUPPORTED_LOCALES) {
  const tutorial = dialogsByLocale[locale].tutorial;

  for (const key of tutorialHelpKeys) {
    const message = tutorial[key];
    const label = `${locale} dialogs.tutorial.${key}`;
    expect(message, label).toEqual(expect.any(String));
    if (!keysThatMayBeEmpty.has(key)) {
      expect((message as string).trim().length, label).toBeGreaterThan(0);
    }
  }
}

expect 的第二个参数 label 会在断言失败时打印出"哪个语言包的哪个键"出了问题,这是批量校验类测试的可读性技巧。

路径别名

@/ 别名解析到 src/,用于替代冗长的相对路径导入:

import { SUPPORTED_LOCALES } from "@/i18n/config";

本地运行

npm run test          # 运行一次
npm run test:watch    # watch 模式

浏览器测试:Playwright 无头 Chromium 与真实 Web API

配置细节:为什么要开 SwiftShader

当被测代码依赖 jsdom 没有实现的真浏览器 API——VideoDecoderVideoEncoderMediaRecorderOffscreenCanvas、WebGL 等——时,应写浏览器测试。vitest.browser.config.ts 的完整内容很短,但每个参数都有存在理由:

import path from "node:path";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";

export default defineConfig({
	test: {
		include: ["src/**/*.browser.test.{ts,tsx}"],
		browser: {
			enabled: true,
			provider: playwright({
				launch: {
					// Software WebGL so Pixi.js works in headless CI without a GPU.
					args: ["--enable-unsafe-swiftshader", "--use-gl=swiftshader"],
				},
			}),
			headless: true,
			instances: [{ browser: "chromium" }],
		},
		testTimeout: 120_000,
		hookTimeout: 30_000,
	},
	resolve: {
		alias: {
			"@": path.resolve(__dirname, "src"),
		},
	},
	assetsInclude: ["**/*.webm"],
});
  • 软件 WebGL--enable-unsafe-swiftshader--use-gl=swiftshader 两个启动参数让无头 CI 环境在没有 GPU 的情况下也能跑 Pixi.js 的 WebGL 渲染——这是该项目渲染引擎的硬性前提,本地有 GPU 的环境可忽略此问题,CI 上则必须;
  • 超时设置:每个测试 120 秒(testTimeout: 120_000)、每个 hook 30 秒(hookTimeout: 30_000)。文档提示:导出操作本身很慢,因此要优先使用小尺寸固件(320×180)和低码率来保持测试快速;
  • assetsInclude: ["**/*.webm"]:让 Vite 把 .webm 识别为可导入的静态资源,配合 ?url 导入语法工作(下文说明)。

文件放置约定

文件名固定为 <subject>.browser.test.ts,并与源码同目录:

src/lib/exporter/videoExporter.ts
src/lib/exporter/videoExporter.browser.test.ts

当前仓库中该规则下的文件包括 videoExporter.browser.test.tsgifExporter.browser.test.tsstreamingDecoder.test.ts 之外的 videoDecoder 相关用例,均以导出管线为测试对象。

加载固件资源(fixture)

视频、图片等静态资源统一放在 tests/fixtures/ 目录(当前包含 sample.webmsample-inflated-duration.webm),通过 Vite 的 ?url 后缀导入,由 dev server 在浏览器中提供:

import sampleVideoUrl from "../../../tests/fixtures/sample.webm?url";

?url 返回的是 dev server 上的资源 URL,浏览器测试里的真实 Chromium 可以直接用 <video>/fetch 拉取——这是 jsdom 环境做不到的关键一步。

示例:从真实视频导出 MP4 Blob

文档给出的浏览器测试示例,验证 VideoExporter 从真实视频导出一份合法 MP4:

import { describe, expect, it } from "vitest";
import sampleVideoUrl from "../../../tests/fixtures/sample.webm?url";
import { VideoExporter } from "./videoExporter";

describe("VideoExporter (real browser)", () => {
  it("exports a valid MP4 blob from a real video", async () => {
    const exporter = new VideoExporter({
      videoUrl: sampleVideoUrl,
      width: 320,
      height: 180,
      frameRate: 15,
      bitrate: 1_000_000,
      wallpaper: "#1a1a2e",
      zoomRegions: [],
      showShadow: false,
      shadowIntensity: 0,
      showBlur: false,
      cropRegion: { x: 0, y: 0, width: 1, height: 1 },
    });

    const result = await exporter.export();

    expect(result.success, result.error).toBe(true);
    expect(result.blob).toBeInstanceOf(Blob);
  });
});

注意参数取值对测试速度的影响:width: 320 / height: 180 的小画布、frameRate: 15 的低帧率、bitrate: 1_000_000(1 Mbps)的低码率,正是文档"用小尺寸 + 低码率保持测试快速"建议的落地。

实际仓库中的 videoExporter.browser.test.ts 在此基础上做了三处深化,值得借鉴:

  1. 魔数校验:不只看 Blob 存在,还解码字节流检查 MP4 的 ftyp box——bytes.slice(4, 8) 解码后应等于 "ftyp",确认产出物是真实可识别的 MP4 容器而非空壳;
  2. 进度事件断言:通过 onProgress 回调收集 ExportProgress 事件,断言 phase === "finalizing" 的事件存在且最后一个 percentage 为 100,把 UI 进度反馈也纳入测试;
  3. 负向路径:传入不存在的壁纸路径 /wallpapers/does-not-exist.jpg,断言导出以 BackgroundLoadError 拒绝(reject),且错误对象携带出错的 url——验证"加载失败时不静默回退为黑底"这一产品行为。gifExporter.browser.test.ts 采用同样的模式,另外用 /^GIF8[79]a/ 正则校验 GIF 文件头。

本地运行

首次运行先安装浏览器(一次性):

npm run test:browser:install

之后每次运行:

npm run test:browser

如何选择合适的测试类型

文档给出的决策表:

场景 使用
纯函数 / 数据转换 单元测试
i18n 键覆盖率 单元测试
React hook 逻辑(不依赖真实浏览器 API) 单元测试
VideoDecoder / VideoEncoder / MediaRecorder 浏览器测试
OffscreenCanvas / WebGL / Pixi.js 渲染 浏览器测试
产出真实 Blob 的文件导出 浏览器测试

判断的核心问题只有一个:被测代码是否触碰到 jsdom 无法实现的浏览器 API? 是则写 .browser.test.ts,否则写普通 .test.ts。两套配置的文件互斥保证每次 vitest 调用只跑其中一类,避免在无浏览器环境下误跑浏览器测试。

小结与延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384