macOS、Windows 和 Linux 上的桌面 UI 端到端测试¶
状态:等待负责人审阅的草案 · 2026-10-17 · 分支 test/electron-ui-e2e
为什么¶
CLAUDE.md 要求默认功能在 macOS、Windows 和 Linux 上表现一致,并将任何不一致视为 P0。目前没有任何检查在真实桌面窗口层面、针对拉取请求(PR)验证这一点:
| 层 | Runner | 数量 | 是否在 CI 中运行 |
|---|---|---|---|
渲染器冒烟测试:由 Vite 提供服务的 UI 运行在 Chromium 中,并使用模拟的 window.voicestudio 桥 |
Playwright chromium |
~70 | 4,仅 Linux(ci.yml 中的 “Electron renderer workflow smokes”) |
真实 Electron 应用,未打包(electron/out/main/index.js) |
Playwright _electron |
~11 个 native-*-smoke |
无 |
| 打包安装程序的启动 + 设置 | Playwright _electron(packaged-smoke.mjs) |
1 | macOS、Windows、Linux,但仅在手动预演、发布标签和 Microsoft Store 运行时 |
因此,macOS 或 Windows 上的 preload、IPC、主进程、user-data-path 和窗口回归问题要到发布时才会暴露,而且大约 66 个渲染器冒烟测试从未运行。渲染器冒烟测试还伪造了 platform: 'win32',因此它们永远看不到真实的操作系统。
目标¶
- 三个操作系统上每个 PR 的桌面门禁。 新增的
ci.yml任务在macos-15、windows-2022和ubuntu-24.04上使用 Playwright_electron启动真实(未打包的)Electron 应用,并运行一套共享的、与操作系统无关的测试套件。 - 运行所有可以离线运行的渲染器冒烟测试,在 Linux 上运行一次,通过规则而不是硬编码列表来选择,这样新增的冒烟测试不会在无人运行的情况下悄然遗漏。
- 可调试的失败:每个失败的操作系统都会上传 Playwright trace、截图和主进程日志。
非目标¶
- 打包安装程序测试。
packaged-smoke.mjs继续保留在预演/发布/商店流程中。 - 真实推理、Python 后端、模型下载、GPU。测试套件使用假后端。
- Rust 原生辅助程序(听写插入、采集、全局快捷键)。它只在打包时构建;需要它的测试继续保留在原来的位置。
- 真实操作系统对话框、麦克风权限提示、Gatekeeper/SmartScreen。对话框在主进程中被 stub;其余部分保持手动测试。
- 像素/截图对比。字体和 DPI 因操作系统而异;截图只是失败时的证据,而不是断言。
- Windows ARM64 和 Intel macOS 测试环境(成本;打包预演会覆盖它们)。
反目标(即使全绿也算失败)¶
- 不稳定的门禁。 一个偶尔失败的测试会让人养成重跑并忽略它的习惯。禁止固定睡眠;要等待条件满足。重试不是修复。
- 针对默认行为的每操作系统预期。 测试套件对三个操作系统都断言同一种行为。禁止使用
if (process.platform …)来改变用户可见的预期结果;它只能用来调整测试如何驱动操作系统(可执行文件路径、xvfb-run)。真正的行为差异属于产品缺陷。 - 为了可测试性而削弱应用。 禁止在应用代码中禁用 Chromium 沙箱、上下文隔离或 CSP。测试钩子通过主进程已经认可的 env vars(如下)或新增的、除非设置否则无效的 env vars 来传递。
- 网络泄漏。 测试套件不得访问互联网。
约束¶
- 复用已固定的内容:根目录
playwright(^1.63)、来自electron/node_modules的 Electron、node --test作为 runner。除非构建者证明该库无法完成某项必需操作,否则不要添加@playwright/test(一旦需要:重新生成bun.lock,并验证bun install --frozen-lockfile)。 - 每个远程操作都固定到提交 SHA(CONTRIBUTING → CI 供应链)。
- 启动模式已在
native-watch-ui-smoke.mjs中验证:electron.launch({ executablePath, args: [out/main/index.js, --user-data-dir=<tmp>], env: { ELECTRON_RENDERER_URL: '', OMNIVOICE_PORT: <fake>, VOICESTUDIO_SKIP_BACKEND: '1', VOICESTUDIO_ALLOW_MULTIPLE_INSTANCES: '1' } }),并使用来自fake-health-backend.mjs的startHealthBackend()。同时设置VOICESTUDIO_DISABLE_UPDATER=1。 - 每个测试使用全新的临时
--user-data-dir;在finally中删除。 - 文档同步:
.github/CONTRIBUTING.md(质量门禁 / 运行测试)在同一 PR 中更新。 - 保持主分支绿色:PR 必须通过完整的现有 CI 矩阵以及新任务。
设计¶
Part A:共享桌面测试套件(electron-ui 任务,3 个操作系统)¶
新文件 electron/tests/desktop-ui.test.mjs(node:test),外加一个小助手 electron/tests/desktop-harness.mjs,后者负责:按操作系统解析 Electron 可执行文件、启动假后端、使用临时 profile 启动、启动 context.tracing(截图 + 快照)、收集 pageerror、控制台错误和请求,并在失败时将 trace、截图和应用主日志保存到 electron/test-results/<os>/ 中。
测试用例所依赖的事实(已在代码中验证):
- 渲染器通过应用的自定义协议(
electron/src/main/protocol.ts)提供服务,并使用 TanStackcreateHashHistory(electron/src/renderer/src/router.tsx)进行路由,因此通过location.hash = '#' + path即可到达某个路由。 - 首次运行状态位于渲染器
localStorage:voicestudio.setup.complete.v1(lib/setup-progress.ts)和voicestudio.locale。除用例 2 外,其他测试都会像渲染器冒烟测试一样,用context.addInitScript预置这两项。 - 假后端用
prompted: true回答/api/settings/analytics,从而抑制同意提示;用例 2b 会覆盖这一行为。 - 保存对话框通过
window.voicestudio.files.saveData/saveAudio(ipc.ts中的filesSaveData/filesSaveAudio)触发。 - star 徽章请求由渲染器发起(
components/app-shell/github-star.tsx)。 - 主进程错误会记录到
<userData>/main-process-errors.json(index.ts);日志位于app.getPath('logs')。 test-results/已被 gitignore。
临时 profile 路径包含空格和非 ASCII 拉丁字符(例如 VoiceStudio Tëst <random>),这样 Windows 和 macOS 上 user-data 处理中的路径编码缺陷就会暴露出来。CJK 文本不进入测试(本地化规则)。
测试用例,在每个操作系统上预期完全一致:
- 启动与桥:第一个窗口打开;标题为 VoiceStudio;preload 桥
window.voicestudio暴露其命名空间;window.voicestudio.app.platform等于真实的process.platform(不能伪造)。 - 首次运行:使用全新 profile 且不预置任何内容时,会显示设置门禁。通过 UI 完成设置后会到达主页路由。
2b. 分析同意:将 fixture 覆盖为
prompted: false后,同意提示会显示两个按钮。拒绝后应用仍可使用,并且不会发送任何分析请求;用例 6 的守卫会强制验证第二部分。 - 每个路由都能渲染:将路由列表从
route-layout-smoke.mjs移到electron/tests/fixtures/routes.mjs,并从两处共同导入。对每个路由,设置 hash 并等待该路由的主区域。不能出现 error boundary,也不能出现pageerror。如果某个路由请求了假后端不存在的后端路径(404),则添加最小 fixture;绝不能跳过该路由。 - 设置能在重启后保留:通过 Settings 修改主题和语言,关闭应用,使用同一 profile 重新启动,断言两者都已持久化,并断言应用写入的每个文件都在给定的
--user-data-dir下(真实主目录 profile 中不能有任何文件)。在跳过后端时,没有任何主进程持有的设置可被触达(runtime-preferences.json只能在setup_required状态下写入),而packaged-smoke.mjs已经覆盖了该路径。 - 对话框 IPC 往返:像
native-media-picker-smoke.mjs那样,通过app.evaluatestubdialog.showOpenDialog/showSaveDialog。 - 像该 smoke 测试一样,通过 preload 桥驱动两者:用
files.authorizeModelsDirectory打开,用files.saveData保存。特定于操作系统的风险在于 IPC 和主进程。UI 控件已由用例 3 和渲染器冒烟测试覆盖。 - 断言 stub 路径能够往返、保存的文件确实存在于磁盘上,并且取消对话框时返回取消值。
- 本地优先守卫:在整个运行过程中,每个请求都必须指向
app:协议、file:、devtools:/data:/blob:,或任意端口上的回环主机(127.0.0.1、localhost、[::1])。这覆盖了假后端以及VOICESTUDIO_ELECTRON_PROXY_PORT上的应用后端代理。任何其他主机都导致测试失败。 - 在两个位置观察请求:渲染器请求使用
context.route('**'),在首次导航前安装;主进程请求通过app.evaluate使用session.defaultSession.webRequest。 - 用预设的计数 stub star 数量请求;
github.com永远不会被整体加入白名单。 - 构建者必须消除“请求在钩子安装之前就已发出”的竞态,例如在 reload 之后对完整请求列表进行断言。
- 无意外错误:零
pageerror、零控制台error条目(除非是 harness 中明确注释的允许列表),并且main-process-errors.json中没有新增条目。
伪后端:为 startHealthBackend() 提供一个可选的逐测试 fixture 覆盖,并且只在这些测试需要时添加 fixture。保持与现有原生 smoke 测试兼容。
第 B 部分:所有离线 renderer 冒烟测试(Linux,现有作业)¶
将“Electron renderer workflow smokes”步骤中四行硬编码的 node electron/tests/… 替换为 node electron/tests/run-renderer-smokes.mjs,该脚本:
- 发现
electron/tests/*smoke*文件,并按内容而非名称对每个文件分类,因为像convert-live-smoke这样的名称是经过 mock 且可运行的: - 导入 Playwright 的
chromium且不是*-real-smoke→ renderer(在此处运行;当前 42 个); *-real-smoke→ real-backend;- 导入
_electron→ electron(原生/已打包;Part A 或发布路径); - 其他任何文件都必须出现在 runner 中一个显式的、带有注释的列表里;
- 逐个运行 renderer smoke 测试,使用 CI 步骤已经启动的
smoke:server;打印通过/失败表格,并在任一失败时以非零状态退出。
node --test 检查(electron/tests/smoke-inventory.test.mjs)会在 electron/tests 中任何 *-smoke.* 文件未匹配任何类别时失败,因此新增的 smoke 要么会被 CI 运行,要么会被有意分类。
归类处理:当前失败的 smoke 会被修复。只有当某个 smoke 覆盖的功能已不存在时才会删除该 smoke,并且要在提交信息中说明原因。有些 smoke 可能需要超出 CI 步骤当前导出的四个环境变量(OMNIVOICE_PORT、VOICESTUDIO_UI_URL、PLAYWRIGHT_CHANNEL、HF_HUB_*)之外的其他变量。runner 会从 runner 内一个小的逐文件表中读取 smoke 所需的任何额外变量。需要真实后端的 smoke 会被重新分类,而不是用 hack 方式绕过。
交付:Part B 作为 Part A 之后的第二个 PR 推出。它采用相同的方案,基于 Part A 的分支再开分支。这样,大规模的分类处理就不会阻塞三操作系统门禁。
CI 作业形态¶
electron-ui:
name: Desktop UI (${{ matrix.label }})
strategy: { fail-fast: false, matrix: macos-15 / windows-2022 / ubuntu-24.04 }
timeout-minutes: 20
steps: checkout → setup-node 22 → setup-bun → bun cache →
bun install --frozen-lockfile → bun run --cwd electron build →
(Linux: xvfb-run -a) node --test electron/tests/desktop-ui.test.mjs →
on failure: upload electron/test-results/ (pinned upload-artifact)
不需要 Python,不需要 uv,也不需要原生辅助构建。它会在每个 PR 以及推送到 main 时运行(没有路径过滤,与 ci.yml 其余部分一致)。
Linux runner:Ubuntu 24.04 限制非特权用户命名空间,这可能会阻止 Electron 的沙箱启动。该作业在启动前会运行 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0:这很廉价、幂等,并能避免令人困惑的首次失败。应用代码中绝不要使用 --no-sandbox。xvfb-run 随 GitHub 的 Ubuntu 镜像提供;构建者会在首次运行时确认这一点,并且只在缺少 xvfb 时才安装它。macOS 和 Windows 从 node_modules 运行 Electron。该副本没有隔离属性,因此 Gatekeeper 和 SmartScreen 不适用。
失败产物:electron/test-results/<os>/ 保存 trace zip、最终截图、app.getPath('logs') 的副本以及 main-process-errors.json。该目录已被 gitignore。
已由所有者决定¶
- 在 macOS、Windows 和 Linux 上运行自动化 UI 测试(本请求要求的内容)。
- 计划和规格说明放在一个新分支上,与 Microsoft Store 的工作分开。
构建者自行决定的事项(已标记,所有者可以否决)¶
- 每个 PR 门禁使用未打包的应用;已打包的应用仍保留在发布路径。
- 使用
playwright库 +node --test,不引入新依赖。 - 没有路径过滤;该作业在每个 PR 上运行。
- 新作业在
main上连续两周保持绿色之前,不会加入分支保护的必要检查;之后由所有者添加。 - Renderer smoke 测试只在 Linux 上运行,因为它们测试的是基于 mock bridge 的 DOM 行为,而该行为不随 OS 变化;OS 相关行为由 Part A 负责。
- 案例 2b(拒绝分析功能同意)以及特殊字符 profile 路径是原方案之外的补充。两者成本都很低,并且都在守护 CLAUDE.md 中的硬性规则:本地优先同意和跨平台一致性。
- 案例 4 使用 renderer 持久化的设置,外加一项“不会在 userData 之外写入任何内容”的检查,因为没有后端时无法访问任何由 main 进程持有的设置。
- Part B 是一个独立的后续 PR。
- Linux 的
sysctl步骤会预先运行,而不仅仅是在失败后运行。
完成条件¶
electron-ui在 PR 上于三个 OS 上均为绿色,并且完整现有 CI 也为绿色。- 故意破坏 preload bridge、某个路由或设置持久化,会使该作业在每个 OS 上失败(在 Linux 上本地检查过一次,其余 OS 通过推理确认)。
- 每个离线 renderer smoke 测试都会在 CI 中运行,并且 inventory 测试为其提供保障。
- CONTRIBUTING 中记录如何在本地运行两套测试套件。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 om/VoiceStudio