ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OHIF Viewer Playwright 端到端测试实战:从运行、编写到视觉回归校验

2026/9/18 12:22:52 拓冰建站 浏览量
OHIF Viewer Playwright 端到端测试实战:从运行、编写到视觉回归校验 OHIF Viewer Playwright 端到端测试实战从运行、编写到视觉回归校验【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers本文是一份面向 OHIF Viewer 开源项目的端到端E2E测试实战指南围绕仓库中的 Playwright 测试体系展开如何准备环境并运行测试套件、如何用visitStudy加载指定检查与模式、如何通过checkForScreenshot做截图基线校验、如何模拟鼠标拖拽、以及如何在测试中直接访问services、commandsManager和 cornerstone3D 底层能力。读完本文你将能够独立编写、运行、调试并提交一个符合 OHIF 测试规范的 Playwright 规格测试spec。一、测试套件总体结构OHIF 的 E2E 测试不是零散堆积的脚本而是一套有明确分层约定的工程体系。测试配置集中在仓库根目录的 playwright.config.ts测试代码统一位于tests/目录整体结构如下playwright.config.ts → Chromium-only端口 3335data-cy 作为测试 id 属性 tests/ ├── *.spec.ts → 每个 spec 对应一个功能/行为 ├── pages/ → 页面对象ViewportPageObject、MainToolbarPageObject 等 ├── utils/ → 共享工具visitStudy、checkForViewportScreenshot 等 │ └── fixture.ts → 扩展 Playwright 测试运行器并注入页面对象 └── screenshots/ → 视觉回归基线1.1 配置要点从 playwright.config.ts 可以看到几个关键决策仅启用 Chromium 项目firefox与webkit项目在配置中处于注释状态Firefox 测试待修复WebKit 则受限于 SharedArrayBuffer 问题因此当前流水线只产出chromium下的截图基线。测试 id 使用data-cyuse.testIdAttribute被设置为data-cy所有getByTestId(...)都会解析到组件上的data-cy属性而不是 Playwright 默认的data-testid。新增测试控件的data-cy时需要遵循现有命名模式并检查拼写。基线与报告输出snapshotPathTemplate将截图基线输出到./tests/screenshots{/projectName}/{testFilePath}/{arg}{ext}即形如tests/screenshots/chromium/spec/name.png测试产物输出到tests/test-resultsHTML 报告输出到tests/playwright-report。webServer 自动托管应用测试启动时由 Playwright 自动拉起应用服务器命令为cross-env APP_CONFIGconfig/e2e.js COVERAGEtrue OHIF_PORT3335 OHIF_OPENfalse nyc pnpm --filter ohif/app exec rspack serve ...监听http://localhost:3335本地非 CI模式下reuseExistingServer为 true若 3335 端口已有服务则直接复用。1.2 一次测试的完整生命周期除了 Playwright 常规流程OHIF 还通过 tests/globalSetup.ts 挂载了全局预热步骤正式用例开始前先用一个已知 StudyInstanceUID 打开一次 viewer触发 Rspack 的首次懒编译、预热浏览器编解码器缓存并拉取检查元数据避免第一个用例因冷编译超时。二、环境准备与运行测试首次运行前需要完成一次性初始化对应 tests/CONTRIBUTING.md 的说明# 一次性环境准备 pnpm install pnpm run test:data # 拉取 DICOM 测试数据子模块 pnpm exec playwright install chromium # 安装浏览器若尚未安装 Playwright 浏览器可能还需要先执行pnpm exec playwright install。2.1 常用运行命令运行整套套件、进入 UI 模式或 headed 模式均可使用仓库根 package.json 中预置的脚本test:e2e:ci与test:e2e:ui内部都是cross-env TEST_ENVtrue pnpm exec playwright test ...# 运行整套测试自动在 3335 端口启动 viewer pnpm run test:e2e:ci # Playwright UI 模式可视化查看用例执行、逐步调试 pnpm run test:e2e:ui # 有头浏览器模式观察真实浏览器窗口 pnpm run test:e2e:headed # 仅运行单个 spec TEST_ENVtrue pnpm exec playwright test tests/YourNew.spec.ts原文档给出的bun test:e2e:ui与yarn test:e2e:ci属于历史写法当前仓库是 pnpm monorepopackageManager: pnpm11.5.2实际可执行脚本以 package.json 中定义的test:e2e:*为准。2.2 查看报告与更新基线# 生成并查看 HTML 报告 pnpm exec playwright show-report tests/playwright-report # 有选择地更新截图基线 pnpm run test:e2e:update需要注意的是当通过pnpm run test:e2e -- flags向 Playwright 传递--update-snapshots、--reporter、-g等参数时pnpm 会插入--分隔符可能导致 Playwright 解析失败正确做法是像上面那样直接调用pnpm exec playwright test ...。另外仓库还提供了pnpm run review:screenshots基于 scripts/screenshot-reviewer.mjs用于人工复核截图基线。三、使用指定的 Study 与 Mode每个用例通常都在test.beforeEach中通过visitStudy加载一个真实存在的检查。文档给出的基础示例为import { test } from playwright/test; import { visitStudy, checkForScreenshot, screenShotPaths } from ./utils/index.js; test.beforeEach(async ({ page }) { const studyInstanceUID 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2; const mode Basic Viewer; await visitStudy(page, studyInstanceUID, mode); }); test.describe(Some Test, async () { test(should do something., async ({ page }) { // Your test code here... }); });3.1visitStudy的实现细节从源码 tests/utils/visitStudy.ts 可以看到visitStudy的完整签名是visitStudy(page, studyInstanceUID, mode viewer, delay 0, datasources ohif)它最终拼出这样的 URL 并等待domcontentloaded与networkidle/{mode}/{datasources}?StudyInstanceUIDs{studyInstanceUID}例如默认参数下即为/viewer/ohif?StudyInstanceUIDsuid。studyInstanceUID本身可以附带额外的查询参数如uidhangingprotocolidmpr源码特意用字符串拼接而非URLSearchParams以避免/被百分号编码后破坏参数。更灵活的新式入口是visitStudyOptions支持{ mode, delay, datasources, customization }对象参数其中customization会被追加为customizationencodeURIComponent(...)。3.2 常用检查清单根据 tests/CONTRIBUTING.md测试数据服务器上常用且真实存在的检查UID 不可凭空编造StudyInstanceUIDMode用途1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5viewer测量、标注、右键菜单1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785viewer3D、MPR、Crosshairs1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458viewer/segmentationLabelmap SEG1.2.840.113619.2.290.3.3767434740.226.1600859119.501viewer/segmentation/tmtvRTSTRUCT/contour、TMTVtmtv 模式建议 delay 100001.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543viewerSR 水合hydration约定俗成的做法是默认在beforeEach中带上约 2000ms 的 settle 延迟如await visitStudy(page, studyInstanceUID, mode, 2000)同时在“被测功能所属的 mode”下运行——例如分割工具在viewer模式下并不存在需要切到segmentation模式。四、截图验证视觉回归的两种方式截图是验证 WebGL 视口渲染结果最直接的手段。文档明确给出了“先规划、再断言”的流程。4.1 规划截图路径截图基线需要预先在 tests/utils/screenShotPaths.ts 中登记命名而不是在用例里手写字符串路径。例如const screenShotPaths { your_test_name: { measurementAdded: measurementAdded.png, measurementRemoved: measurementRemoved.png, }, };基线文件会落在tests/screenshots/chromium/spec/name.png下当前仅 chromium 项目处于启用状态因此不再产出文档所述的firefox/、webkit/目录。4.2 使用checkForScreenshot断言在用例中通过 tests/utils/checkForScreenshot.ts 提供的checkForScreenshot拍摄并比对截图import { test } from playwright/test; import { visitStudy, checkForScreenshot, screenshotPath, } from ./utils/index.js; test.beforeEach(async ({ page }) { const studyInstanceUID 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2; const mode Basic Viewer; await visitStudy(page, studyInstanceUID, mode); }); test.describe(Some test, async () { test(should do something, async ({ page }) { // 你的测试代码添加测量 await checkForScreenshot( page, page, screenshotPath.your_test_name.measurementAdded ); }); });首次运行失败是正常现象基线文件不存在时Playwright 会先自动生成截图此时用例失败再次运行时才会拿新截图与基线做像素级比对。因此提交前务必人工打开生成的 PNG确认基线内容正确。4.3checkForScreenshot的可调参数从源码 tests/utils/checkForScreenshot.ts 可以看到它支持两种调用形态位置参数形式(page, locator, screenshotPath, attempts?, delay?)与对象形式。对象形式的完整参数及默认值如下参数默认值说明attempts10比对失败后的重试次数配合 delay 相当于最大等待时间delay1250每次重试之间的等待毫秒数maxDiffPixelRatio0.02允许的最大差异像素比例threshold0.05像素颜色差异阈值normalizedClip无相对 locator 包围盒的归一化裁剪区域{x,y,width,height}0~1fullPagefalse是否整页截图beforeAttempt无每次尝试含首次截图前执行的回调实现上checkForScreenshot在每次尝试前会先waitForLoadState(networkidle)失败后清理-actual.png/-diff.png/-expected.png中间产物并等待delay再重试直至成功或耗尽尝试次数后抛出原始错误。除非确有需求不要调大maxDiffPixelRatio或threshold来“凑过”测试——基线不匹配时应修复抖动或使用--update-snapshots重新生成基线并人工核对。4.4 视口专属截图checkForViewportScreenshot对 WebGL 视口OHIF 提供了更专用的checkForViewportScreenshot({ page, viewport, screenshotPath })见 tests/utils/index.ts 的导出它会在拍摄前通过页面对象隐藏视口上的叠层文本、注释文本与方向标记文本避免日期、序列描述、窗宽窗位数值等易漂移的文本污染基线。真实用例可参考 tests/Length.spec.tsawait checkForViewportScreenshot({ page, viewport: activeViewport, screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly, });原则是永远不要对整页应用截图尽量通过 locator 把范围限定到视口面板能通过 DOM/SVG 断言的结果优先用断言只有结果只存在于 WebGL 画布像素时才动用截图。五、模拟鼠标交互点击与拖拽视口是一个 WebGL 画布无法用 CSS 选择器选中渲染出的像素因此 OHIF 提供了一组归一化坐标交互工具全部从 tests/utils/index.ts 导出。5.1 文档中的拖拽示例文档推荐在cornerstone-canvas元素上模拟拖拽import { visitStudy, checkForScreenshot, screenShotPaths, simulateDrag, } from ./utils/index.js; test.beforeEach(async ({ page }) { const studyInstanceUID 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2; const mode Basic Viewer; await visitStudy(page, studyInstanceUID, mode); }); test.describe(Some Test, async () { test(should do something.., async ({ page }) { const locator page.locator(.cornerstone-canvas); await simulateDrag(page, locator); }); });该工具会自动计算元素的包围盒bounding box确保拖拽始终落在元素边界内相比手写 x/y 坐标更健壮、更易维护也适用于绝大多数绘图工具。5.2 当前仓库的归一化交互工具在当前仓库中拖拽能力由 tests/utils/simulateDragOnElement.ts 提供核心是两个工具simulateNormalizedDragOnElement({ locator, start, end, button?, delay?, steps?, mouseUp? })两点直线拖拽。start/end为归一化坐标0~1相对于元素包围盒原点在左上角delay默认 50ms、steps默认 10。simulateNormalizedPathDragOnElement({ locator, path, ... })多点路径拖拽。mousedown发生在path[0]鼠标沿所有中间点平滑移动mouseup在path[last]除非mouseUp: false。要求路径至少两个点。此外还有点击类工具simulateNormalizedClicksOnElement、simulateNormalizedDoubleClickOnElement、simulateClicksOnElement、simulateDoubleClickOnElement等。这些能力都被封装进了页面对象 tests/pages/ViewportPageObject.ts用例中应优先使用页面对象而非裸调用const activeViewport await viewportPageObject.active; await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }]); await activeViewport.normalizedDragAt({ start: { x: 0.3, y: 0.3 }, end: { x: 0.7, y: 0.7 } });归一化坐标让测试与视口尺寸解耦仅当目标确实与像素锚定如拖拽某个像素级手柄时才使用绝对像素坐标并需在注释中说明理由。六、等待渲染完成而不是盲目 Sleep睡眠等待page.waitForTimeout是 E2E 测试最常见的抖动来源太短用例失败太长套件变慢。OHIF 的做法是等待视口自身的渲染信号。相关工具定义在 tests/utils/waitForViewportsRendered.tswaitForViewportRenderCycle(page)等待一次完整渲染周期——先有任意视口进入needsRender默认 5s 超时再等所有视口进入rendered默认 15s 超时可选等待关联 volume 加载完成最后做一次“画面稳定”两个动画帧加短 idle 窗口。waitForViewportsRendered(page)渲染已在途时布局切换、序列加载直接等待所有视口渲染完成可传{ waitVolumeLoad, settle }开关。waitForAnyViewportNeedsRender(page)等待至少一个视口请求渲染。正确用法是在触发渲染的动作之前先捕获 Promise避免错过渲染周期// ❌ 不推荐sleep-and-pray await segmentRow.delete(); await page.waitForTimeout(5000); // ✅ 推荐等待真实渲染信号 const renderCycle waitForViewportRenderCycle(page); await segmentRow.delete(); await renderCycle;对于面板行、对话框等 DOM 侧状态直接依赖 Playwright 的自动重试断言即可它们本身就会等到状态稳定。每个残留在 spec 中的waitForTimeout、hover或非常规交互都必须有理由最好带注释评审者会逐一追问“为什么需要它”。七、在测试中访问 services、commandsManager 与 cornerstone有时需要绕过 UI 直接驱动应用内部能力比如弹出一条 UI 通知。文档给出了利用page.evaluate访问window上暴露对象的方法await page.evaluate(({ services }: AppTypes.Test) { const { uiNotificationService } services; uiNotificationService.show({ title: Test, message: This is a test, type: info, }); }, await page.evaluateHandle(window));AppTypes.Test是应用侧暴露给测试的类型命名空间window.services下挂着各 UI 服务如uiNotificationServicewindow.commandsManager则可运行命令。这一模式的典型用例出现在 tests/pages/ViewportPageObject.ts 中切层跳转通过commandsManager.runCommand(jumpToImage, ...)完成滚动通过services.cornerstoneViewportService.getCornerstoneViewport(viewportId).scroll(delta)完成。注意从 tests/CONTRIBUTING.md 的约定看window.services状态读取不能替代渲染断言——它即使在渲染损坏时也可能通过。DOM/SVG 断言优先服务访问只在确实需要驱动内部逻辑时使用。八、测试编写约定fixture、页面对象与断言文档指向的 tests/CONTRIBUTING.md 是 E2E 测试的编写规范核心规则可概括为十条test、expect及工具一律从./utils导入不要从playwright/test导入否则页面对象 fixture 不会被注入页面对象从测试参数中解构fixture 注入绝不new所有应用控件都通过页面对象操作spec 中不出现裸page.getByTestId(...)视口点击/拖拽优先用归一化坐标用真实 StudyInstanceUID 配合正确的 mode 加载检查并处理水合、跟踪确认等弹窗使用 web-first 自动重试断言断言精确期望值禁止“取值后再断言”视口重渲染后等待waitForViewportRenderCycle而非 sleep断言真实效果图像缩放了、分割段消失了而不是data-active这类代理属性截图仅用于画布独占输出通过screenShotPaths命名每个 locator 动作与断言都必须await。8.1 fixture 注入的页面对象tests/utils/fixture.ts 扩展了测试运行器底层使用playwright-test-coverage自动注入 6 个页面对象DOMOverlayPageObject、mainToolbarPageObject、leftPanelPageObject、rightPanelPageObject、viewportPageObject、notFoundStudyPageObject并带一个 auto fixture_applyGlobalE2EOHIFBaseline在每条用例前应用全局 E2E 配置addOHIFConfiguration。// ✅ 正确从 ./utils 导入fixture 才会被注入 import { test, expect, visitStudy, checkForViewportScreenshot, screenShotPaths } from ./utils; // ❌ 错误能编译但页面对象 fixture 永远不会注入 import { test, expect } from playwright/test; test(renames a segment, async ({ viewportPageObject, mainToolbarPageObject, leftPanelPageObject, rightPanelPageObject, DOMOverlayPageObject, }) { // ... });各页面对象的职责划分需要的控件归属工具栏按钮或工具MainToolbarPageObject菜单、弹窗或小型对话框DOMOverlayPageObject带字段的大型对话框独立页面对象经DOMOverlayPageObject触达如DicomTagBrowserPageObject侧面板控件LeftPanelPageObject/RightPanelPageObject视口内的一切ViewportPageObject8.2 断言策略文档与规范都强调“断言结果而非代理”// ❌ 取值后断言无重试易抖动 const count await panel.rows.count(); expect(count).toBe(3); // ✅ Web-first 断言自动重试直到 UI 稳定 await expect(panel.rows).toHaveCount(3); await expect(segmentRow.title).toHaveText(Segment 5);具体建议还包括优先toHaveText、精确toHaveCount、toHaveAttribute避免toBeTruthy、toContainText与式宽松计数expect(locator).not.toBeNull()永远是空操作locator 永不为 null应改用toBeVisible()仅当值只能通过方法获得时才用await expect.poll(...)绘制后的标注不仅要存在还要toBeVisible()。九、手动启动 Viewer 加速开发默认情况下运行测试套件时 Playwright 会自动启动应用服务器。本地开发时可手动启动应用、让 Playwright 复用已有服务以跳过启动步骤当前配置中reuseExistingServer: !process.env.CI且端口为 3335。按文档描述手动服务就绪后测试会在已有服务器上直接执行从而显著加速迭代。也可以自行执行与 webServer 相同的命令来精确复现测试环境cross-env APP_CONFIGconfig/e2e.js COVERAGEtrue OHIF_PORT3335 OHIF_OPENfalse pnpm --filter ohif/app exec rspack serve --config .webpack/webpack.pwa.js十、使用 Playwright VS Code 扩展录制测试文档末尾的示例视频展示了如何借助 Playwright 的 VS Code 扩展为 OHIF 添加新测试在扩展中打开录制面板勾选需要的测试文件然后在真实浏览器里操作应用选择工具、在视口上绘制、点击面板等扩展会把操作自动转写为 Playwright 代码。录制得到的是“裸” Playwright 代码提交前需要按上文约定改造成规范形式从./utils导入工具、把选择器抽象进页面对象、用visitStudy替换直接导航、用screenShotPaths命名截图基线。十一、命名、提交与 Agent 开发协作Spec 命名要无歧义如ContourSegRename到底是重命名“分割段”还是“分割体”必须说清。测试标题要精确不重复方法与参数名自解释。PR 遵循 Conventional Commits标题形如test(contour): add segment rename interactions仓库的发布工具链会解析该格式推导语义化版本。保持 PR 范围小一个 PR 对应一个功能或行为小且解耦的 PR 评审更快。仓库内 Agent skilltests/CONTRIBUTING.md 的 “Agentic development” 一节提到仓库内含一套镜像上述约定的 Agent skill含按功能分类的种子 spec 索引与失败排查指南对人工阅读同样有参考价值多数测试失败源于时序或水合问题而非真实回归先做失败分类再深入调试。十二、小结OHIF Viewer 的 Playwright 测试体系是一个“配置—工具—规范”三位一体的工程playwright.config.ts定义了 Chromium-only、data-cy定位、3335 端口与截图基线路径tests/utils提供visitStudy、checkForScreenshot、归一化交互与渲染等待工具tests/pages与fixture.ts保证 spec 只表达意图而不堆积选择器tests/CONTRIBUTING.md则以十条规则约束稳定性和可评审性。按“先复制现有 spec、再按规范改造、最后人工核对基线”的路径任何人都能快速为 OHIF 贡献高质量、低抖动的端到端测试。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考