尧图精选

Sanity Studio 仓库实战:Playwright 测试注解与组织(skip / fixme / fail / slow / step / 自定义注解)完整指南

🕒 发布时间:2026/9/17 19:33:49 📁 来源:尧图网络
Sanity Studio 仓库实战Playwright 测试注解与组织skip / fixme / fail / slow / step / 自定义注解完整指南【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity本文基于.agents/skills/playwright-best-practices技能库中的 annotations.md 核心文档讲解 Playwright 测试注解体系的六大主题Skip、Fixme/Fail、Slow、Test Steps、自定义注解与条件注解。作为佐证文中会大量对照 Sanity Studio 仓库e2e/目录中真实运行的端到端测试与配置例如 e2e/tests/inputs/reference.spec.ts、e2e/playwright.config.ts 与 e2e/studio-test.ts。读完本文你将掌握如何用注解精确控制哪些测试跑、哪些测试跳过、哪些测试允许失败如何用test.step()组织可读的测试报告以及如何通过testInfo与自定义 fixture 构建团队级注解体系并直接迁移到 Sanity Studio 这类大型前端仓库的 E2E 工程中。目录Skip 注解精确控制哪些测试不执行Fixme 与 Fail 注解管理已知问题与预期失败Slow 测试与自定义超时Test Steps让测试报告可读、可定位自定义注解把业务元数据挂到测试上条件注解按环境、浏览器与设备动态决策必须避开的反模式相关参考Skip 注解精确控制哪些测试不执行test.skip()是 Playwright 中最常用的注解用于声明这条测试在当前条件下不需要运行。它的核心价值在于测试仍然被收集、仍然出现在报告中标记为 skipped但不会真正执行从而把为什么没跑的意图固化在代码里。基础 Skip无条件跳过无条件跳过适用于功能尚未实现、或者当前环境根本不支持该场景的情况// Skip unconditionally test.skip(feature not implemented, async ({page}) { // This test wont run }) // Skip with reason test(payment flow, async ({page}) { test.skip(true, Payment gateway in maintenance) // Test body wont execute })注意两种写法的差异第一种直接在test()前加.skip修饰符整个测试体都不会执行第二种在测试体内调用test.skip(condition, reason)调用点之前的代码仍会执行调用点之后的代码会提前返回。因此带条件的跳转应尽量放在测试体最顶部避免无谓的准备工作。条件 Skip按浏览器或环境变量决策条件 Skip 是 Sanity Studio 这类跨浏览器测试套件里最常见的形态。仓库中 e2e/tests/inputs/reference.spec.ts 就有一个非常典型的真实案例test(value can be changed after the document has been published, async ({ page, createDraftDocument, browserName, }) { // Skip Firefox due to flaky publish operation timing test.skip(browserName firefox) test.slow() // ... 测试体 })这段代码说明注解可以依赖 Playwright 自动注入的 fixture如browserName在 Firefox 上因发布操作时序不稳定而跳过该测试。同理也可以根据环境变量决策test(webkit-specific feature, async ({page, browserName}) { test.skip(browserName ! webkit, This feature only works in WebKit) await page.goto(/webkit-feature) }) test(production only, async ({page}) { test.skip(process.env.ENV ! production, Only runs against production) await page.goto(/prod-feature) })Skip by Platform按操作系统或 CI 环境跳过Playwright 测试运行在 Node.js 进程中因此可以直接读取 Node 的运行时信息test(windows-specific, async ({page}) { test.skip(process.platform ! win32, Windows only) }) test(not on CI, async ({page}) { test.skip(!!process.env.CI, Skipped in CI environment) })这正对应 Sanity Studio 仓库 e2e/playwright.config.ts 中的做法——配置层根据os.platform()决定是否加入 WebKit 项目os.platform() darwin ? [{name: webkit, use: {...devices[Desktop Safari]}}] : []而测试层则用注解做更细粒度的兜底。Skip Describe Block整组跳过当某个test.describe块内的所有用例都因同一原因不可用时可以在 describe 级别声明 skip子测试会统一继承test.describe(Admin features, () { test.skip(({browserName}) browserName firefox, Firefox admin bug) test(admin dashboard, async ({page}) { // Skipped in Firefox }) test(admin settings, async ({page}) { // Skipped in Firefox }) })这里的test.skip()接收一个接收 fixture 的回调函数Playwright 会在运行每个子测试前求值从而支持按浏览器/设备动态判断。这比在每个用例里重复写test.skip(...)要 DRY 得多。Fixme 与 Fail 注解管理已知问题与预期失败Fixme已知问题先跳过但保持追踪test.fixme()与 skip 的执行效果相同测试不运行、记为 skipped但语义不同它表达的是这里有一个已知 bug 或未完成的重构需要后续修复是技术债的显式记录// Mark test as needing fix (skips the test) test.fixme(broken after refactor, async ({page}) { // Test wont run but is tracked }) // Conditional fixme test(flaky on CI, async ({page}) { test.fixme(!!process.env.CI, Investigate CI flakiness - ticket #123) await page.goto(/flaky-feature) })Fail预期失败照常运行但期待断言不通过test.fail()与 skip/fixme 完全不同测试会真实运行但 Playwright 期待它失败。如果它居然通过了测试反而判为失败——这正是bug 已被修复的信号提醒你及时移除 fail 注解并恢复正常的断言// Test is expected to fail (runs but expects failure) test(known bug, async ({page}) { test.fail() await page.goto(/buggy-page) // If this passes, the test fails (bug was fixed!) await expect(page.getByText(Working)).toBeVisible() }) // Conditional fail test(fails on webkit, async ({page, browserName}) { test.fail(browserName webkit, WebKit rendering bug #456) await page.goto(/render-test) await expect(page.getByTestId(element)).toHaveCSS(width, 100px) })Skip、Fixme、Fail 三者对比AnnotationRuns?Use Casetest.skip()NoFeature not applicabletest.fixme()NoKnown bug, needs investigationtest.fail()YesExpected to fail, tracking a bug选择建议功能在当前环境不适用 →skip有已知缺陷、暂时无法通过 →fixme保存意图缺陷被追踪但希望持续验证其存在、并在修复瞬间得到通知 →fail。Sanity Studio 的失败处理还更进一步其自定义 fixture e2e/studio-test.ts 中通过testInfo.status ! testInfo.expectedStatus判断是否为预期外的失败并自动附加诊断报告见下文自定义注解章节。Slow 测试与自定义超时标记慢测试test.slow()会把该测试的默认超时放大三倍适用于确实耗时较长的用例大数据导入、视频处理、文件上传等避免简单粗暴地全局调大超时导致整体回归时间失控// Triple the default timeout test(large data import, async ({page}) { test.slow() await page.goto(/import) await page.setInputFiles(#file, large-file.csv) await page.getByRole(button, {name: Import}).click() await expect(page.getByText(Import complete)).toBeVisible() }) // Conditional slow test(video processing, async ({page, browserName}) { test.slow(browserName webkit, WebKit video processing is slow) await page.goto(/video-editor) })Sanity Studio 的 E2E 套件大量使用这一模式。仓库全局默认超时为 60 秒e2e/playwright.config.ts 的timeout: 60_000而 e2e/tests/inputs/reference.spec.ts 中对涉及草稿文档引用关系的测试同时使用了test.skip(browserName firefox || browserName chromium)与test.slow()把超时放宽到 180 秒以容纳搜索索引最终一致性的重试等待代码中多处使用{timeout: 60_000}的显式等待。自定义超时当三倍默认超时仍不够、或某个用例需要更精细的超时控制时用test.setTimeout()显式指定毫秒值describe 块则可用test.describe.configure()统一设置组内超时test(very long operation, async ({page}) { // Set specific timeout (in milliseconds) test.setTimeout(120000) // 2 minutes await page.goto(/long-operation) }) // Timeout for describe block test.describe(Integration tests, () { test.describe.configure({timeout: 60000}) test(test 1, async ({page}) { // Has 60 second timeout }) })仓库中 e2e/helpers/failureDiagnostics.ts 还展示了一个进阶用法在测试失败后收集诊断信息时用testInfo.setTimeout(testInfo.timeout CAPTURE_TIMEOUT_EXTENSION_MS)临时延长超时为诊断数据收集争取时间——这证明testInfo.setTimeout()可以在运行期动态调整。另外test.slow()的三倍放大同样作用于expect断言的默认超时吗答案是否定的断言超时由expect.timeout单独控制。Sanity Studio 配置里将其设为 30 秒e2e/playwright.config.ts远高于 Playwright 默认的 5 秒这本身就体现了Studio 加载大量代码分割资源与异步配置场景下的现实需求。Test Steps让测试报告可读、可定位test.step()将一段测试体包装为有名字的步骤。它不影响测试逻辑但对报告可读性与失败定位有决定性影响当断言失败时Playwright 报告会精确指出失败发生在哪个步骤内配合 trace 回放可以快速定位到具体操作。基础步骤test(checkout flow, async ({page}) { await test.step(Add item to cart, async () { await page.goto(/products) await page.getByRole(button, {name: Add to Cart}).click() }) await test.step(Go to checkout, async () { await page.getByRole(link, {name: Cart}).click() await page.getByRole(button, {name: Checkout}).click() }) await test.step(Fill shipping info, async () { await page.getByLabel(Address).fill(123 Test St) await page.getByLabel(City).fill(Test City) }) await test.step(Complete payment, async () { await page.getByLabel(Card).fill(4242424242424242) await page.getByRole(button, {name: Pay}).click() }) await expect(page.getByText(Order confirmed)).toBeVisible() })注意最后一句断言放在所有步骤之外它代表整个流程的结果验证如果失败报告会显示为未归属到任何步骤的顶层失败语义上更清晰。嵌套步骤步骤可以任意嵌套适合表单分组填写这类层次化流程test(user registration, async ({page}) { await test.step(Fill registration form, async () { await page.goto(/register) await test.step(Personal info, async () { await page.getByLabel(Name).fill(John Doe) await page.getByLabel(Email).fill(johnexample.com) }) await test.step(Security, async () { await page.getByLabel(Password).fill(SecurePass123) await page.getByLabel(Confirm Password).fill(SecurePass123) }) }) await test.step(Submit and verify, async () { await page.getByRole(button, {name: Register}).click() await expect(page.getByText(Welcome)).toBeVisible() }) })步骤返回值test.step()的回调可以返回任意值供后续步骤使用——例如在创建订单后把orderId传递给验证步骤test(verify order, async ({page}) { const orderId await test.step(Create order, async () { await page.goto(/checkout) await page.getByRole(button, {name: Place Order}).click() // Return value from step return await page.getByTestId(order-id).textContent() }) await test.step(Verify order details, async () { await page.goto(/orders/${orderId}) await expect(page.getByText(Order #${orderId})).toBeVisible() }) })在 Page Object 中使用步骤步骤同样适用于 Page Object 的方法内部让 POM 方法在报告中呈现为语义化的操作名// pages/checkout.page.ts export class CheckoutPage { async fillShippingInfo(address: string, city: string) { await test.step(Fill shipping information, async () { await this.page.getByLabel(Address).fill(address) await this.page.getByLabel(City).fill(city) }) } async completePayment(cardNumber: string) { await test.step(Complete payment, async () { await this.page.getByLabel(Card).fill(cardNumber) await this.page.getByRole(button, {name: Pay}).click() }) } }自定义注解把业务元数据挂到测试上通过 testInfo 添加注解每个测试在运行期都能拿到testInfo对象其中testInfo.annotations是自由数组可以向其中 push 任意{type, description}对。这些注解会出现在 HTML 报告与 JSON 报告中是把测试与工单、优先级、负责人等信息关联起来的官方途径test(important feature, async ({page}, testInfo) { // Add custom annotation testInfo.annotations.push({ type: priority, description: high, }) testInfo.annotations.push({ type: ticket, description: JIRA-123, }) await page.goto(/feature) })type是注解的键如ticket、priority、ownerdescription是值。Sanity Studio 的 e2e/helpers/failureDiagnostics.ts 正是这种模式的实战变体它读取testInfo.status、testInfo.expectedStatus、testInfo.timeout等运行时状态并在失败时用testInfo.attach()把studio-diagnostics.json附加到测试报告里——attach与annotations一样都是TestInfo暴露给测试与报告系统的标准能力。注解 Fixture把注解封装成团队 API逐个push略显繁琐更优雅的方式是用 Playwright 的fixture 扩展机制把注解封装成一个小型 API。这也是 Sanity Studio 的做法——e2e/studio-test.ts 通过test baseTest.extendSanityFixtures({...})导出自定义test测试文件统一从该模块导入而非直接 import Playwright 的原始test// fixtures/annotations.fixture.ts import {test as base, TestInfo} from playwright/test type AnnotationFixtures { annotate: { ticket: (id: string) void priority: (level: low | medium | high) void owner: (name: string) void } } export const test base.extendAnnotationFixtures({ annotate: async ({}, use, testInfo) { await use({ ticket: (id) { testInfo.annotations.push({type: ticket, description: id}) }, priority: (level) { testInfo.annotations.push({type: priority, description: level}) }, owner: (name) { testInfo.annotations.push({type: owner, description: name}) }, }) }, }) // Usage test(critical feature, async ({page, annotate}) { annotate.ticket(JIRA-456) annotate.priority(high) annotate.owner(Alice) await page.goto(/critical) })借助 fixture 作用域团队可以约定统一的注解类型与取值枚举如优先级只能取low | medium | high从机制上杜绝拼写随意性。Sanity Studio 的 fixture 体系还展示了注解之外的扩展能力其createDraftDocumentfixturee2e/studio-test.ts自动创建草稿文档并等待表单可编辑用expect.poll连续三次读到可编辑状态sanityClientfixture 则注入一个指向https://api.sanity.work的 API 客户端——测试体只需要声明依赖设置与清理逻辑都被 fixture 接管。在 Reporter 中读取注解注解的终极用途是驱动自定义 reporter 或 CI 后处理。实现一个 reporter在onTestEnd中读取test.annotations并执行策略如高优先级用例失败时告警// reporters/annotation-reporter.ts import {Reporter, TestCase, TestResult} from playwright/test/reporter class AnnotationReporter implements Reporter { onTestEnd(test: TestCase, result: TestResult) { const ticket test.annotations.find((a) a.type ticket) const priority test.annotations.find((a) a.type priority) if (ticket) { console.log(Test linked to: ${ticket.description}) } if (priority?.description high result.status failed) { console.log(HIGH PRIORITY FAILURE: ${test.title}) } } } export default AnnotationReporter注意此处读取的是test.annotations用例声明期而测试体内 push 的是testInfo.annotations运行期两者在测试执行后是打通的reporter 侧统一通过test.annotations读取。条件注解按环境、浏览器与设备动态决策注解 Helper复用条件判断把常见的条件跳转抽成 helper 函数可以消除测试文件之间的重复代码// helpers/test-annotations.ts import {test} from playwright/test export function skipInCI(reason Skipped in CI) { test.skip(!!process.env.CI, reason) } export function skipInBrowser(browser: string, reason: string) { test.beforeEach(({browserName}) { test.skip(browserName browser, reason) }) } export function onlyInEnv(env: string) { test.skip(process.env.ENV ! env, Only runs in ${env}) }// tests/feature.spec.ts import {skipInCI, onlyInEnv} from ../helpers/test-annotations test(local only feature, async ({page}) { skipInCI(Uses local resources) await page.goto(/local-feature) }) test(production check, async ({page}) { onlyInEnv(production) await page.goto(/prod-only) })skipInBrowser使用test.beforeEach实现意味着它对该文件内所有后续用例生效——这是文件级条件跳过的实用技巧。Describe 级条件按设备类型分组移动端与桌面端测试的典型组织方式是在 describe 层用beforeEach按isMobile分流test.describe(Mobile features, () { test.beforeEach(({isMobile}) { test.skip(!isMobile, Mobile only tests) }) test(touch gestures, async ({page}) { // Only runs on mobile }) }) test.describe(Desktop features, () { test.beforeEach(({isMobile}) { test.skip(isMobile, Desktop only tests) }) test(hover interactions, async ({page}) { // Only runs on desktop }) })这里的isMobile与browserName一样来自 Playwright 自动注入的 fixture由 e2e/playwright.config.ts 这类项目配置中devices[Desktop Chrome]/devices[Desktop Firefox]等 device 描述符决定。Sanity Studio 的视觉回归辅助 e2e/studio-visual-test.ts 也遵循同一思路在takeChromaticSnapshot里用testInfo.project.name ! chromium直接返回确保快照只针对 Chromium 项目拍摄——用代码显式表达此能力仅适用于某项目。与配置层 grep 的配合条件注解解决的是测试代码内部的动态决策如果要在不修改代码的情况下筛选测试则应配合 Playwright 的--grep/--grep-invert或配置中的grep/grepInvert详见 test-tags.md。例如 Sanity 这类仓库可按需只跑某浏览器项目npx playwright test --projectchromium而注解则负责在代码层兜底确保即使误跑了不支持的浏览器组合也会被优雅跳过而不是产生误报。必须避开的反模式Anti-PatternProblemSolutionSkipping without reasonHard to track whyAlways provide descriptionToo many skipped testsTest debt accumulatesReview and clean up regularlyUsing skip instead of fixmeLoses intentUse fixme for bugs, skip for N/ANot using stepsHard to debug failuresGroup logical actions in steps实践建议每个skip/fixme/fail都带原因描述——Sanity Studio 的参考测试就注释了Skip Firefox due to flaky publish operation timing这种描述让 CI 上的跳过项无需考古即可理解。另外注解属于会过期的技术债fail注解在用例意外通过时应当触发一次清理定期 review 被跳过用例区分永久不适用删除或归档与临时待修fixme 工单号避免测试债务无限累积。相关参考Test Tags见 test-tags.md掌握用--grep打标签与过滤测试与注解配合实现代码层决策 命令行筛选的双层控制Test Organization见 test-suite-structure.md了解大型套件的结构组织Debugging见 debugging.md处理疑难失败与 flaky 排查仓库实战参考e2e/tests/inputs/reference.spec.ts —test.skip(browserName firefox)与test.slow()的浏览器条件组合e2e/playwright.config.ts — 超时、重试、浏览器项目与 webServer 的全局配置e2e/studio-test.ts — 自定义 fixture 体系sanityClient、createDraftDocument、失败诊断附加e2e/helpers/failureDiagnostics.ts — 基于testInfo的运行时状态读取、attach与setTimeout动态调整e2e/studio-visual-test.ts — 按testInfo.project.name条件执行的可视化能力开关【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →