ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Dify E2E 测试实战:Cucumber + Playwright 的 Locator、断言、隔离与同步最佳实践

Dify E2E 测试实战:Cucumber + Playwright 的 Locator、断言、隔离与同步最佳实践 Dify E2E 测试实战Cucumber Playwright 的 Locator、断言、隔离与同步最佳实践【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify本文围绕 Dify 仓库内置的 E2E 技能参考文档 playwright-best-practices.md 展开。该文档规定了 Dify 的 Cucumber 场景e2e/features/与 Playwright 浏览器层e2e/features/step-definitions/协作时的五条核心准则场景隔离、按用户契约选择定位器、web-first 断言与超时归属、actionability 等待、以及与测试框架匹配的调试方式。读完本文你将掌握这套准则的原文要求并能在 Dify 仓库源码中逐条找到落地证据——从 DifyWorld 的场景级BrowserContext管理到 hooks.ts 中按场景捕获的诊断工件。一、文档定位谁在什么时候引用它该参考文档不是独立的 E2E 教程而是 e2e-cucumber-playwright 技能 的主题路由入口之一涉及locator、断言、隔离或等待决策时读references/playwright-best-practices.md即本文主体涉及场景措辞、step 粒度、World 状态或 tag 设计时读 cucumber-best-practices.md。技能文档明确划分了两个运行时的职责边界Cucumber 拥有场景与 hook 的执行、报告Playwright 提供浏览器自动化、context、page、locator、action、assertion、request 与 tracing API。一个关键约束是即使从playwright/test导入 APIPlaywright Test 运行器的配置trace: on-first-retry、projects、workers、fixtures、reporters、retries也不适用于这个 Cucumber 套件。超时域同样彼此独立。套件架构本身命令、tag、seed、清理契约由 e2e/AGENTS.md 负责本文的准则在该架构之上运行。二、准则一保持场景隔离Keep scenarios isolated文档要求把Playwright 围绕干净 browser context 构建作为隔离模型的基础并给出三条落地规则不要依赖另一个场景先运行过场景状态放在 runner 的场景自有 context 中而不是模块级全局变量特殊的认证或会话设置通过显式的 per-scenario fixture 建模而非共享可变状态。Dify 仓库的 world.ts 是这条准则的直接实现。DifyWorld在resetScenarioState()中把 console 错误、page 错误、createdAppIds、createdAgentIds、capturedDownloads等全部重置为空值保证每个 Cucumber scenario 拿到干净的 World 状态startSession()则按场景创建全新的BrowserContextasync startSession(browser: Browser, authenticated: boolean) { this.resetScenarioState() this.context await browser.newContext({ baseURL, locale: defaultLocale, ...(authenticated ? { storageState: authStatePath } : {}), }) this.context.setDefaultTimeout(30_000) // ... this.page await this.context.newPage() }值得注意的实现细节从源码结构看浏览器实例本身在 hooks.ts 中是模块级共享的let browser: Browser而隔离发生在context 粒度——这正是 Playwright context 模型的设计意图共享进程以节省开销隔离状态以避免串扰。同时浏览器身份BrowserContext与 API 身份scenario 或 process 拥有的 oRPCconsoleClient刻意保持分离使未登录/登出场景不会破坏 fixture 的所有权这一点在 e2e/AGENTS.md 的 Runtime Ownership 一节中有文字约定。认证状态也不是可变共享状态已认证的 context 通过静态的storageState: authStatePath注入未认证场景则由unauthenticatedtag 触发startUnauthenticatedSession()创建干净 context见 hooks.ts 的 Before hook。三、准则二按用户契约选择定位器Select locators by user contract文档把定位器选择定义为一种语义选择而非固定优先级排序核心原则是优先使用与目标元素如何暴露给用户相一致的内建定位器元素类型首选定位方式交互控件role 可访问名称表单控件关联的 label当 placeholder 才是相关稳定契约时尤其无 label 时用 placeholder非交互内容可见文本或相关文本替代text alternative没有有意义用户契约的元素有意的 test id三条附加约束不要为了给定位器喂数据而乱加 role 或可访问名称——如果产品元素本应有用户可见语义去修那个契约而不是迁就测试代码避免裸 CSS/XPath 选择器除非不存在稳定的用户契约且补充契约不切实际单元素 action 的 locator 是严格的strict应限定到稳定区域或用filter({ has, hasText })使目标唯一.first()/.last()/.nth()是评审信号——位置选择必须有意图且稳定不能只是为了压掉歧义告警。这套原则在 Dify 的步骤定义里是可见的。create-app.steps.ts 展示了三种契约形态的组合// 表单控件无 labelplaceholder 是稳定契约 await this.getPage().getByPlaceholder(Give your app a name).fill(appName) // 交互控件先限定到稳定区域dialog再用 role 名称 const createButton page.getByRole(dialog).getByRole(button, { name: /^Create(?:\s|$)/ }) // 交互控件直接 role 可访问名称 const toggle page.getByRole(button, { name: More basic app types }) await expect(toggle).toBeVisible() await toggle.click()getByRole(dialog).getByRole(button, ...)正是文档所说的限定到稳定区域再定位而命名采用^Create(?:\s|$)这类带意图的正则而不是.first()式的静默消歧符合位置/模糊选择必须是显式决策的要求。四、准则三web-first 断言与正确的超时归属Use web-first assertions with the right timeout owner文档要求优先使用自动等待与重试的 Playwright 断言替代手动状态检查推荐await expect(page).toHaveURL(...) await expect(locator).toBeVisible() await expect(locator).toBeHidden() await expect(locator).toBeEnabled() await expect(locator).toHaveText(...)禁止expect(await locator.isVisible()).toBe(true)这类一次性快照断言为 DOM 状态编写自定义轮询循环把waitForTimeout当同步手段。expect.poll的适用边界在文档中被明确划出非 DOM 的真值API 状态、后端最终一致性、生成的资源、捕获的浏览器事件才用expect.pollDOM 状态用 locator 断言以便 Playwright 应用 actionability 与 web-first 重试语义。Dify 的 export-app.steps.ts 是expect.poll的规范用例——下载完成属于捕获的浏览器事件而非 DOM 状态await expect.poll(() this.capturedDownloads.length, { timeout: 10_000 }).toBeGreaterThan(0)而 home.ts 的waitForConsoleHome则演示了 DOM 就绪等待的标准写法URL、aria-current属性、可见 heading 全部走 locator 断言并把可选的超时通过{ timeout }传给断言本身而不是外层包裹export const waitForConsoleHome async (page: Page, timeout?: number) { const options getExpectOptions(timeout) await expect(page).toHaveURL(/\/(?:\?.*)?$/, options) await expect(page.getByRole(link, { name: Home })).toHaveAttribute( aria-current, page, options, ) await expect(page.getByRole(heading, { name: Templates, exact: true })).toBeVisible(options) }三个相互独立的超时时钟文档特别强调Cucumber step/hook 超时、Playwright locator/action 超时、Playwright 断言超时是三个独立的预算browserContext.setDefaultTimeout()不会改变默认 5 秒的断言超时只在该条件的就绪责任人身上显式放更长的断言超时不要靠放大外层超时来掩盖内层失败。对照 Dify 源码三个时钟各有明确归属时钟预算位置Cucumber step 默认超时60 秒hooks.tssetDefaultTimeout(60_000)Cucumber 命名 hook 超时关闭会话 30s / 资源清理 120s / 诊断捕获 60shooks.ts 常量与After({ timeout })Playwright action/locator 超时30 秒world.tsthis.context.setDefaultTimeout(30_000)Playwright 断言超时默认 5 秒按断言显式覆盖home.ts 的getExpectOptions传参模式这正是显式超时属于真正慢的那个条件这一评审问题的工程化答案清理类 hook 最慢所以 120s断言默认保持短预算需要放长的地方如下载等待在断言调用点上写明{ timeout: 10_000 }。五、准则四让 action 等待 actionabilityLet actions wait for actionabilityPlaywright 的 locator action 已经内建对元素可操作的等待因此不要在每次 click/fill 前叠加额外计时逻辑。文档给出的好坏模式对比是好模式当可见状态本身是行为的一部分时先断言有意义的可见状态再经由 locator API 点击/填写/选择。坏模式在每个 action 前堆叠任意等待等待不稳定的实现细节而非用户关心的可见状态用force: true绕过真实的 hit-target、遮罩层或禁用态失败。Dify 的步骤定义普遍遵循先断言可见/可操作再动作的两步结构例如 create-app.steps.tsconst appTypeCard dialog.getByRole(button, { name: ... }) await expect(appTypeCard).toBeVisible() // 可见性断言行为契约的一部分 await appTypeCard.click() // action 自带 actionability 等待一次性事件先注册等待再触发动作对一次性 popup、下载、request 或 response文档要求在触发动作之前创建等待然后再 await 事件场景自有的监听器也可以捕获事件供后续断言使用。同时明确禁止把networkidle当作应用就绪断言——应等待用户可见状态或自有后端契约。create-app.steps.ts 的 confirm app creation 步骤 是该准则的完整示范先page.waitForResponse(...)注册响应等待再点击按钮最后 await 事件并用dify/contracts的 zod schema 解析响应体契约校验失败即契约失败不允许放宽或回退 schemaconst responsePromise page.waitForResponse( (response) response.request().method() POST new URL(response.url()).pathname.endsWith(/console/api/apps), ) await expect(createButton).toBeEnabled() await createButton.click() const response await responsePromise expect(response.ok()).toBe(true) const createdApp zPostAppsResponse.parse(await response.json())注意这里waitForResponse的注册严格早于click()——评审清单中事件等待是否注册在触发动作之前在 Dify 代码里是有据可查的。六、准则五让调试方式匹配当前运行框架Match debugging to the active harness由于套件由 Cucumber 驱动文档要求在 Cucumber hook 中配置工件捕获而不是给单个场景添加平行的诊断逻辑如果引入 tracing应使用browserContext.tracing而不是 Playwright Test 的trace选项。Dify 的诊断实现集中在 hooks.ts 的 Capture scenario diagnostics After hook仅在FAILED / AMBIGUOUS / PENDING / UNDEFINED / UNKNOWN状态diagnosticArtifactStatuses时触发成功场景零开销对 World 登记的所有诊断页面主页面、共享应用页、agent-v2 各引用页逐页截图page.screenshot({ fullPage: true })并抓取 HTML落盘到cucumber-report/artifacts/同时通过world.attach(...)挂到 Cucumber 报告上浏览器 console 错误与 uncaught page 错误来自DifyWorld.startSession()中挂在 context 上的console/weberror监听器统一在诊断 hook 中附加到报告。这与 e2e/AGENTS.md 的 Seeds, Cleanup, And Diagnostics 契约一致失败产物在cucumber-report/artifacts/HTML 与 Cucumber Messages 报告在cucumber-report/后端/前端启动日志在.logs/。清理cleanup与诊断diagnostics各自是命名 hook利用 Cucumber After hooks 的逆注册顺序执行先诊断、再清理、最后关闭会话hooks.ts 中的注释。七、评审清单把准则变成可执行的 Review 问题文档末尾给出的七条评审问题是这套准则的验收口径也是 Dify 审查 E2E 变更时的默认检查表这个 locator 能否扛住不改变用户可见行为的 DOM 重构位置定位positional locator表达的是产品排序还是在掩盖模糊匹配断言是否使用了 Playwright 的重试语义显式超时是否归属于真正慢的那个条件事件等待是否注册在触发动作之前代码是否保持了 per-scenario 隔离新抽象是否真的必要还是绕过了 runner 的场景自有 context 与生命周期对应到仓库前六条分别有 world.ts 的 resetScenarioState、home.ts 的断言超时传参、create-app.steps.ts 的响应等待顺序 作为可对照的正面样例。第 7 条与 e2e/AGENTS.md 的边界规则呼应浏览器动作属于被测行为本身API 只用于准备 fixture、轮询持久化与清理不能替代用户的When动作辅助代码只在拥有 fixture 构造、多操作编排、清理注册表或最终一致性轮询等明确职责时才存在。八、适用范围与前提上述准则适用于e2e/目录下的 Cucumber Playwright 套件不适用于 Vitest、React Testing Library 或后端测试技能文档已显式排除运行前提来自 e2e/AGENTS.md在仓库根目录执行pnpm install与pnpm -C e2e e2e:install安装依赖与浏览器随后用pnpm -C e2e e2e -- --tags smoke之类的窄化 tag 运行最小场景集合E2E_SLOW_MO500配合 headed 命令用于本地动作调试该参考文档同时声明以 Playwright 官方文档best-practices、locators、actionability、test-assertions、test-timeouts、browser-contexts、events、trace-viewer为外部权威来源本地文档负责把通用原则映射到 Cucumber harness 的具体约束上。小结这份参考文档的价值在于把 Playwright 的通用最佳实践收束到Cucumber 是执行者、Playwright 是浏览器层这一特定分工下——隔离靠 per-scenarioBrowserContext、断言靠 web-first 语义、超时各有归属、事件等待先于动作、诊断收敛在 hook 层。Dify 的e2e/源码world.ts、hooks.ts、home.ts、create-app.steps.ts、export-app.steps.ts为每一条准则提供了可直接引用的落地证据可作为自建 Cucumber Playwright E2E 体系时的评审基线。【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表