
Anki 端到端测试实战用 Playwright 驱动真实 Anki 实例做 UI 回归测试【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/ankiAnki 的端到端e2e测试体系通过 Playwright 启动一个真实的 headless Anki 实例并借助 Anki 内置的 mediasrv HTTP 服务驱动其 SvelteKit 前端页面完成交互断言。本文以仓库中的 docs/e2e-testing.md 为主线结合 playwright.config.ts、justfile、qt/tests/launch_anki_for_e2e.py 与 ts/tests/e2e/ 下的真实测试套件带你掌握 e2e 测试的三种运行模式、Playwright 配置细节、protobuf RPC 调用协议与 CI 集成方式读完即可在本地编写并运行属于你自己的 Anki UI 回归测试。测试架构Playwright × mediasrv × 真实 Anki与传统的Mock 前端路由 jsdom 模拟 DOM的单元测试不同Anki 的 e2e 测试运行在完整真实的 Anki 进程中Playwright 负责驱动 Chromium 浏览器模拟用户的点击、输入、粘贴等操作测试目标不是静态页面而是 Anki 通过mediasrv内嵌 HTTP 服务对外提供的真实 Web 界面统计图、牌组选项、编辑器、学习结束页等测试代码通过 mediasrv 暴露的/_anki/HTTP 端点直接调用 Anki 后端能力例如addNote、getNotetype、updateNotetype等 RPC。从代码组织上看e2e 测试全部位于 ts/tests/e2e/与基于 Vitest 的单元测试完全隔离。目录下的文件也印证了这套分工冒烟测试 sanity.test.ts验证 mediasrv 可达、SvelteKit 页面能加载功能回归测试.spec.ts后缀覆盖笔记新增往返note-add-roundtrip、媒体按钮media-buttons、粘贴过滤paste-filter、粘性字段sticky-field、上下文切换context-switching、图片遮挡保存竞态io-mask-save-race等真实场景。前置准备至少先构建一次 Anki运行 e2e 测试前需要先把 Anki 完整构建一次just buildjust build在 justfile 中定义为ninja pylib qt即构建 Python 库与 Qt 前端产物。e2e 测试依赖 TypeScript/Svelte 生成的代码与 Python 端 mediasrv因此构建产物是测试能否启动的前提。除此之外无需任何手动安装just test-e2e会在首次运行时自动把 Playwright 的 Chromium 浏览器安装到out/playwright-browsers/目录后续运行幂等跳过。这个行为在 justfile 与 justfile 中可以完整看到test-e2e ui: _install-playwright-browsers {{ ninja }} pyenv ts:generated pylib qt {{ playwright_env }} {{ yarn }} test:e2e {{ ui }}其中playwright_envjustfile显式设置PLAYWRIGHT_BROWSERS_PATHout/playwright-browsers把浏览器缓存锁定在仓库构建目录内避免污染用户主目录_install-playwright-browsers执行playwright install chromium完成浏览器安装。最终调用的是 package.json 中定义的playwright test对应playwright/test版本为^1.62.1。提示just test-e2e还会通过ninja pyenv ts:generated pylib qt增量重建相关产物确保ts/lib/generated/下的 protobuf 生成代码与 Python 端始终与当前源码一致。三种运行模式托管模式CI 风格just test-e2ePlaywright 通过webServer配置自动启动一个一次性的 Anki 实例测试结束后自动销毁just test-e2e首次运行会比较慢约 60 秒因为 Anki 必须完成完整的初始化流程Rust 后端、Python mediasrv、Qt 离屏渲染后测试才会真正开始。这一等待逻辑由 playwright.config.ts 的webServer.timeout: 60_000控制。复用服务器模式开发推荐ANKI_E2E_REUSE_SERVER1托管模式下每次运行都要冷启动 Anki迭代效率低。复用模式让你在一个终端里常驻 Anki另一个终端反复跑测试# 终端 1 —— 保持运行 ./run # 终端 2 —— 快速迭代 ANKI_E2E_REUSE_SERVER1 just test-e2e该模式对应的配置位于 playwright.config.tsreuseExistingServer: process.env.ANKI_E2E_REUSE_SERVER 1。注意 Anki 默认监听端口 40000可通过环境变量ANKI_API_PORT覆盖Playwright 与 Anki 两侧会读取同一个变量保证端口一致见 playwright.config.ts 与 qt/tests/launch_anki_for_e2e.py。交互式 UI 模式--ui在复用模式下追加--ui可打开 Playwright 的图形界面逐步查看每个测试步骤的页面快照、网络请求与控制台输出非常适合调试ANKI_E2E_REUSE_SERVER1 just test-e2e --uiPlaywright 配置深度解析playwright.config.ts 是整个 e2e 体系的关键值得逐项理解配置项值说明testDir./ts/tests/e2e测试文件目录outputDir./out/e2e-report/报告与失败产物输出目录fullyParallel/workersfalse/1强制串行执行。Anki 实例共享同一个集合collection并行会互相污染数据forbidOnly!!process.env.CICI 中禁止残留.only测试retries0不自动重试保证每次失败都可复现reporterCI 用githubhtml本地用list失败产物写入out/e2e-reportuse.baseURLhttp://127.0.0.1:${ANKI_API_PORT}默认端口 40000页面访问基于此地址use.traceretain-on-failure失败时保留完整 trace含每个步骤的 DOM 快照、网络、控制台use.screenshotonly-on-failure失败时自动截图webServer配置决定 Anki 实例如何被拉起playwright.config.tswebServer: { command: ${PYENV_PYTHON} qt/tests/launch_anki_for_e2e.py, url: http://127.0.0.1:${MEDIASRV_PORT}/_anki/readyz, timeout: 60_000, reuseExistingServer: process.env.ANKI_E2E_REUSE_SERVER 1, stdout: pipe, stderr: pipe, env: { ANKI_API_PORT: MEDIASRV_PORT }, },值得注意两点就绪探测用的是/_anki/readyz而不是/favicon.ico。配置注释明确说明原因/favicon.ico在 HTTP 服务线程一启动就会响应但此时 profile 的集合可能尚未加载完成/_anki/readyz只有在集合真正打开后才返回 200从而保证测试永远不会与异步 profile 加载竞争。启动命令指向 qt/tests/launch_anki_for_e2e.py该脚本承担了播种一次性环境的职责。e2e 启动器内部做了什么qt/tests/launch_anki_for_e2e.py 是 Playwright 与 Anki 之间的桥梁其核心逻辑包括创建一次性ANKI_BASE用tempfile.TemporaryDirectory生成隔离的数据目录测试完全不会触碰开发者的真实 profile播种预置项_seed_prefs函数直接向prefs21.db写入_global与test两个 profile跳过语言选择器和 profile 选择器并关闭自动更新检查。脚本注释特意说明该函数刻意复刻了 qt/tests/conftest.py 中的_seed_prefs使 pytest 与 TS 两套测试框架保持独立、互不依赖关键环境变量qt/tests/launch_anki_for_e2e.pyANKI_API_HOST0.0.0.0文档化的测试逃生门让_have_api_access()对全部/_anki/*请求放行外部 Chromium 无需注入 Authorization 头即可调用 API副作用是 mediasrv 会绑定所有网卡注释明确警告不要在共享环境开启QT_QPA_PLATFORMoffscreen使用 Qt 离屏渲染平台无显示器也能运行ANKIDEV1、RUST_BACKTRACE1开启开发模式与 Rust 回溯PYTHONUNBUFFERED1Python 输出即时刷新确保 Playwright 能捕获到日志最终通过tools/run.py -p test以testprofile 启动 Anki。编写测试从 fixtures 开始新增测试文件时放入 ts/tests/e2e/ 目录并使用.test.ts后缀CI 上.spec.ts与.test.ts都会被testDir收集并且必须从./fixtures导入而不是直接从playwright/test导入import { expect, test } from ./fixtures; test(my feature works, async ({ page }) { await page.goto(/some-anki-page); await expect(page.locator(#some-element)).toBeVisible(); });fixtures.ts预置好的三个编辑器上下文ts/tests/e2e/fixtures.ts 重新导出了expect与一个预配置的test对象并额外提供了三个与编辑器强相关的 fixture这是编辑类测试的基础设施fixture说明editorPage已导航到/editor/?modeadd且注入了bridgeCommand桩stub的页面。桩会拦截window.bridgeCommand调用并记录到window.__bridgeCalls避免无 Qt webChannel 时编辑器抛错同时供断言使用editoreditorPage之上调用loadNote({ initial: true })并等待首个字段容器出现——适合所有直接操作编辑器的交互测试legacyEditor在editorPage上再挂载一个第二NoteEditorlegacy 模式即 Qt 的 editor_legacy.py 加载方式并按 Python 驱动 webview 的顺序注入setFields、setIsImageOcclusion、setNotetypeMeta、setTags等一整套状态——用于验证旧编辑器路径的测试注入的 bridge 桩fixtures.ts通过page.addInitScript在任何页面脚本执行前生效await page.addInitScript(() { (window as any).__bridgeCalls []; (window as any).bridgeCommand ( cmd: string, _callback?: (value: unknown) void, ): void { (window as any).__bridgeCalls.push(cmd); }; });新增共享 fixture 时统一往 fixtures.ts 中添加即可。helpers.ts可复用的测试工具库ts/tests/e2e/helpers.ts 沉淀了 e2e 测试中最常用的操作RPC 匹配rpcUrl(method)生成/_anki/${method}路径isRpc(method)用endsWith做精确匹配避免addNote误匹配addNoteBulkisRpcResponse用于响应侧匹配字段定位fieldContainer(page, index)定位.field-container[data-indexN]editableField进一步穿透 shadow DOM 定位anki-editable[contenteditabletrue]Playwright 链式locator()自动穿透 open shadow root选择器操作chooserButton与openChooserAndSelect封装了打开笔记类型/牌组选择弹窗并选中某项的完整流程桥调用检查bridgeCalls(page)读取window.__bridgeCalls用于断言旧路径命令没有被执行protobuf 解码decodeRequestBody(request, messageType)从拦截到的请求中取出二进制 body 并调用生成的类型的fromBinary解码直接 RPC 调用callRpc(page, method, message, opChangesType)在浏览器上下文中用fetch向/_anki/${method}发起 POST可绕过 UI 直接驱动后端用于准备测试数据或校验后端行为粘贴模拟pasteData(locator, data)在浏览器上下文中构造真实ClipboardEvent——因为handlePaste读取event.clipboardData该数据只能存在于浏览器环境Node 侧构造的 DataTransfer 无法填充 clipboardData。调用 Anki 的 protobuf APIAnki 的/_anki/端点统一接收并返回protobuf 编码的二进制载荷Content-Type为application/binary。这与 ts/lib/generated/post.ts 中生产代码使用的协议完全一致export async function postProtoT( method: string, input: { toBinary(): Uint8Array; getType(): { typeName: string } }, outputType: { fromBinary(arr: Uint8Array): T }, options: PostProtoOptions {}, opChangesType 0, ): PromiseT底层通过fetch(/_anki/ method, { headers: { Content-Type: application/binary, Anki-Op-Changes: ... }, body })收发二进制post.ts。在 e2e 测试中发送请求使用page.request.post配合Buffer.from(protoMsg.toBinary())解码响应时使用 ts/lib/generated/ 下对应的生成类型。拦截并解码请求的完整范式取自 note-add-roundtrip.spec.tsimport { AddNoteRequest } from generated/anki/notes_pb; import { decodeRequestBody, isRpc } from ./helpers; // 先注册请求监听再触发 UI 操作避免竞争 const addNoteReqPromise page.waitForRequest(isRpc(addNote), { timeout: 10_000 }); await page.getByRole(button, { name: Add, exact: true }).click(); const addNoteReq await addNoteReqPromise; const decoded decodeRequestBody(addNoteReq, AddNoteRequest); expect(decoded.note?.fields[0]).toBe(Hello World); expect(decoded.deckId).not.toBe(0n);这里isRpc(addNote)用endsWith精确匹配端点decodeRequestBody拿到postDataBuffer后用生成的AddNoteRequest.fromBinary()解码从而可以在不 Mock 任何后端的情况下断言 UI 发出的请求内容——这是这套 e2e 体系最有价值的断言方式之一。通过 mediasrv 访问 Anki 页面mediasrv 通过 HTTP 提供以下页面非穷尽列表取自 docs/e2e-testing.mdURL 模式说明/graphs统计图SvelteKit/deck-options/[deckId]牌组选项SvelteKit/congrats学习结束页SvelteKit/card-info/[cardId]卡片信息SvelteKit/editor/?mode[mode]编辑器SvelteKit/favicon.icomediasrv 存活探测例如 sanity.test.ts 中的冒烟断言test(mediasrv is reachable, async ({ page }) { const response await page.goto(/favicon.ico); expect(response?.status()).toBe(200); }); test(congrats SvelteKit page loads, async ({ page }) { await page.goto(/congrats); await expect(page.locator(body)).toBeAttached(); });仓库内真实测试套件解读仓库 ts/tests/e2e/ 下已有 7 个测试文件是学习如何组织 e2e 测试的最佳范本。每个文件头部都有一段套件说明注释写明验证目标、断言清单与涉及的源码位置例如note-add-roundtrip.spec.ts验证新 TypeScript 编辑器点击 Add 时发出的addNoteRPC 载荷字段正确、deckId非零、updateNotes不得触发新增流程契约、Add 后newNote触发且首字段清空、bridgeCalls包含saved同时覆盖空首字段点击 Add 不得发出 addNote的校验对应旧版noteCanBeAdded()守卫。它还专门覆盖了焦点仍在第二字段时立即点击 Add的竞态场景media-buttons.spec.ts验证附加图片/录制音频按钮走openFilePicker/recordAudio/addMediaFromPath/playFile这套 RPC 链路而不是Qt bridge并分别验证新路径与 legacy 路径legacy 路径必须完全经由bridgeCommand不得发出任何 RPC且要在字段重新聚焦时才插入媒体sticky-field.spec.ts验证点击粘性徽章走getNotetypeupdateNotetype解码后断言fields[0].config.sticky翻转、徽章获得highlighted类、toggleStickybridge 命令未触发并验证粘性字段在 Add 后保留值、非粘性字段清空测试刻意开-关-开-关以便把 notetype 配置恢复原状paste-filter.spec.ts验证粘贴 HTML 时 TS 端html-filter真正运行p转div、script被剥除、on*事件属性被白名单过滤——即旧版 Python_pastePreFilter行为的端到端等价实现context-switching.spec.ts覆盖笔记类型/牌组选择器的会话行为、addNote载荷中的 ID 与选择器状态一致以及两种添加模式Mode A当前牌组固定Mode B笔记类型记住各自上次使用的牌组其底层逻辑对应 rslib/src/adding.rsio-mask-save-race.spec.ts回归验证 #4754——600ms 内切换字段导致图片遮挡Image Occlusion遮罩保存被取消的竞态缺陷测试会真实添加图片遮挡笔记类型并绘制遮罩。这些套件展示了几个共同的工程实践先注册监听再触发操作避免竞态、解码 protobuf 请求断言载荷、同时断言旧路径命令没有触发、测试后恢复被修改的配置sticky 开→关、notetype 复原等。CI 集成check-linux 工作流e2e 测试作为 .github/workflows/ci.yml 中check-linuxjobubuntu-24.04的一部分运行位置在常规构建与测试之后ci.yml- name: Build run: just build - name: Lint and test env: ONLINE_TESTS: 1 run: | just lint just test --coverage - name: Run e2e tests run: just test-e2e失败时Playwright 报告含 trace 与截图作为 artifact 上传并保留 7 天ci.yml- name: Upload Playwright report if: failure() uses: actions/upload-artifact... with: name: playwright-report path: out/e2e-report/ retention-days: 7本地调试时失败的 trace/screenshot 也落在 playwright.config.ts 配置的out/e2e-report/目录配合--ui与reuse-server模式即可快速定位问题。调试与常见问题排查首次运行超时约 60 秒webServer.timeout固定为 60 秒首次运行 Anki 初始化较慢属正常现象若反复超时先单独运行./run确认 Anki 能正常启动再用ANKI_E2E_REUSE_SERVER1模式复用实例端口冲突默认端口 40000 被占用时通过ANKI_API_PORT统一修改Playwright 与 Anki 都读取该变量测试间数据污染Playwright 强制workers: 1串行执行测试本身也应遵循恢复现场约定如 sticky-field 测试把 notetype 配置切回原状需要独立数据时可用callRpc直接驱动后端准备/清理断言不到 UI 效果优先检查bridgeCalls是否走了意外路径、请求是否真的发出用page.waitForRequest(isRpc(...))监听必要时用--ui模式逐步查看快照与网络面板共享环境警告启动器设置ANKI_API_HOST0.0.0.0会令 mediasrv 绑定所有网卡并放行全部 API 请求注释明确声明不要在共享环境启用。本文内容以 docs/e2e-testing.md 为骨架全部运行方式、配置与协议说明均可在上述仓库文件中逐一验证。掌握这套体系后你既能写出覆盖真实 Anki 行为的回归测试也能为新的 Web 界面功能快速补齐端到端覆盖。【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考