Dify Web 前端测试体系详解:从 web/docs/test.md 看双测试项目、Browser Mode 准入与测试边界设计

发布时间:2026/9/7 15:39:52
Dify Web 前端测试体系详解:从 web/docs/test.md 看双测试项目、Browser Mode 准入与测试边界设计 Dify Web 前端测试体系详解从 web/docs/test.md 看双测试项目、Browser Mode 准入与测试边界设计【免费下载链接】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 仓库web/目录的前端测试指南为蓝本完整解读其测试心智、unit/browser 双 Vitest 项目划分、Browser Mode 准入标准、查询与 Mock 规范并结合 web/vite.config.ts、web/vitest.setup.ts 等真实配置与源码说明这套体系如何在大型 React Next.js 应用中保护产品契约、让重构更安全。读完后你能掌握 Dify 前端测试的边界选择方法、命令用法与评审标准并可将其迁移到自己的前端项目中。一、定位与测试心智保护契约而非凑覆盖率Frontend Testing Guide 开宗明义web/下的自动化测试应当保护产品行为、让重构更安全而不是逐文件凑完成度的练习。Dify UI 组件库则在其包内单独拥有 Dify UI testing contract 定义的测试边界。什么时候该写测试文档给出的应写/应更新测试清单全部围绕稳定、可观察的契约用户交互及由此产生的 UI 状态导航、URL 状态、持久化、权限或数据流用户真实可达的 loading、成功、错误与空状态可访问性语义、键盘行为、焦点管理、禁用态具有明确输入/输出行为的业务逻辑或可复用工具函数可以通过公开边界复现回归的 bug 修复。同时文档明确列出了不应仅因以下理由新增测试的情形组件、hook、prop、分支或文件存在组件能渲染不报错实现里用了useState、useEffect、useMemo、useCallback覆盖率报告出现未覆盖行TypeScript 已经让某种输入在类型上不可能改动仅调整类名、间距、颜色或响应式布局而未改变行为。对于纯视觉改动文档建议在代表性宽度与状态下真实验证 UI浏览器、截图、Storybook 或 E2E风险足够时才自动化。覆盖率是诊断信号不是质量目标原文明确该指南不定义任何必须达到的覆盖率百分比评审者不应单纯为提高覆盖率而要求补测试。正确用法是把覆盖率报告当作发现可疑缺口的工具再逐个判断缺口是否代表值得保护的产品风险。这与 web/vite.config.ts 中 coverage 配置相印证coverage 使用v8providerCI 下 reporter 为[json, json-summary]本地为[text, json, json-summary]且排除**/__mocks__/**——配置上只产出诊断数据不存在达标线机制。二、选择合适的测试边界找到行为的所有者文档的核心方法论是使用能包含行为所有者并证明产品契约的最小边界且不让测试耦合到实现细节纯转换与业务规则 → 单元测试hook 只有在其本身暴露可复用公开契约时才直接测否则通过它所属的组件或 feature 间接驱动组件与 feature 中通过 DOM 或外部副作用可见的行为 → React Testing Library跨越有意义模块边界的行为 → 集成测试只有当happy-dom无法忠实表达浏览器行为时才使用browser项目Dify UI 原语的 Storybook / Vitest 边界遵循 Dify UI testing contract。两条重要推论测行为所有者。Barrel 导出、透传 wrapper、纯展示性子组件在所属 feature 已证明契约时不需要单独测试不重复造轮子。Base UI、React Aria 或浏览器本身已拥有的通用行为不必再测只测 Dify 的集成、覆写与已知回归。从源码结构看这一边界划分在仓库中有明确落点web/下的 spec 分为组件旁路的__tests__/目录如web/app/__tests__/、跨 feature 集成测试 web/tests/含billing/、workflow/等跨模块用例以及 4 个*.browser.spec.tsx如 home-banner.browser.spec.tsx、reflow.browser.spec.tsx——browser 模式确实只被用在极少量、有明确浏览器行为诉求的用例上。Browser Mode 准入标准这是文档中实操价值最高的一节值得完整掌握。默认选择是happy-domunit项目负责纯逻辑、hook、不依赖浏览器行为的 DOM 可观察组件/feature 行为。文档要求先按行为所有者选测试范围再按证明所断言契约所需的环境选项目——而不是按focuskeyboardpointer这类交互标签机械归类。例如testing-library/user-event的user.tab()足以保护由简单语义化标记编码的焦点序列断言由 DOM 顺序、disabled 状态、tabindex决定但它不验证浏览器原生的顺序焦点导航。只有当你能明确命名一个happy-dom会漏掉的浏览器专属失败时才使用browser项目例如CSS 布局或渲染可见性改变了几何、命中测试、响应式行为或指针目标浏览器计算的可聚焦性、inert、Shadow DOM 遍历或浏览器默认导致的原生焦点行为/焦点事件顺序变化选区、滚动、真实键盘/指针输入、浏览器 API、observer、动画生命周期等原生实现会改变结果的情形。文档还给出了严格的反例清单存在 portal、focus trap、shadow root、observer、焦点断言或键盘/指针交互本身不构成使用 Browser Mode 的理由——说不出它能改变的浏览器专属结果就不该用。渲染 UI、减少 mock、增加信心、提高覆盖率都不是理由。其他硬性约束每个web/app/下的*.browser.spec.{ts,tsx}必须通过语义化定位器驱动最小所有者并用其额外运行时成本去匹配所证明的浏览器契约禁止强制交互、固定 sleep、私有 DOM/CSS 断言、真实网络请求Browser Mode 仍是聚焦的组件/feature 测试且当前只证明 Chromium正在运行的应用、鉴权、真实路由、后端 API、持久化或完整用户旅程应交给 E2E 套件。三、断言行为而非实现文档给出八条断言纪律通过 props、用户交互、URL 变化或公开 API 驱动状态迁移断言渲染结果、ARIA 状态、导航、持久化、网络边界调用等可观察结果针对隐藏表面如 portal 弹窗上的重置/持久化回归要走公开迁移打开 → 修改 → 关闭并等待表面消失 → 再打开然后断言而不是耦合 hook 位置、组件名、key 或私有挂载结构不检查 React 内部 state、ref、hook 调用顺序、effect 依赖或私有 DOM 结构仅在引用相等性本身就是公开契约时才测它一个测试描述一个行为允许多条断言共同证明该行为只测类型与产品契约支持的输入状态不要凭空制造null、undefined或极端值除非序列化输出或类名契约被有意公开且稳定否则避免快照和 CSS 类断言。四、查询、交互与可访问性选择器优先级按此顺序优先带可访问名称的getByRole带标签表单控件的getByLabelText合适的用户可见查询getByText、getByPlaceholderText等仅当边界没有可用的 DOM 语义时canvas 输出、编辑器 shim、被 mock 的非视觉集成才用getByTestId。重复内容造成歧义时先收窄到语义化容器再用 RTL 的within或 Browser Mode 的 locator 链在其中查询。如果某个交互控件无法被语义化找到先检查生产代码是否缺一个真正的 button、link、label、landmark 或可访问名称——这是文档对生产代码质量的反向约束。交互与断言的分工RTL 测试中使用userEvent.setup()实例只有当低层事件本身就是契约时才用fireEventBrowser Mode 中通过带等待的 locator 交互仅当 locator 未暴露所需 DOM API 时才用.element()键盘与焦点属于交互契约时要测传达产品状态的 ARIA 属性要断言但语义化查询和自动化检查不等于完整的可访问性合规精确文案断言在文案或翻译 key 本身就是契约时有效否则优先语义化查询同步缺席用queryBy*异步出现用findBy*异步消失用waitForElementToBeRemoved或waitForBrowser Mode 中用expect.element做最终断言。五、在真实边界处 Mock原则让拥有/转换所断言行为的生产代码保持真实只 mock 目标契约之外的依赖且 mock 必须保留测试所需的公开契约。允许 mock 的边界服务与网络边界测试环境未提供的 Next.js 导航或浏览器 API外部 SDK 与昂贵 provider独立测过、不拥有也不转换所断言行为的子边界否则其搭建成本会淹没所有者测试。特别强调不要 mock 交互型 Dify UI 原语或 feature 对它们的封装——保持其语义角色、状态属性、portal、焦点行为和render(props, state)契约真实只 mock 到达场景所需的服务/外部数据边界。硬性规则绝不发起真实网络请求每个会改动的共享 mock 状态在测试前重置测 query 行为时创建全新的 TanStack Query client复杂数据优先用带合法默认值的 typed builder只为场景相关字段加覆写本地 mock 保持本地只有多个测试套件真正共享时才把 helper 移入web/__mocks__/——仓库中该目录确实只放了 provider-context.ts 与 zustand.ts 这类跨套件共享项。六、异步、时间与隔离等待用户交互、promise、findBy*与waitFor等待可观察状态变化不用固定 sleep 或宽泛 retry 掩盖时序错误异步出现用findBy*最终为真的外部断言用waitFor仅当定时器行为本身是契约时才用 fake timers且测后恢复真实计时器控制时间、随机性、网络响应与共享 store保证确定性web/vitest.setup.ts 已统一执行 Testing Library cleanup 并在每个测试后重置 Zustand store依赖 mock 调用历史的套件应在beforeEach里vi.clearAllMocks()不要用afterEach为下一个测试做准备。源码层面 web/vitest.setup.ts 值得逐行读它把文档的纪律变成了工程事实BASE_UI_ANIMATIONS_DISABLED true关闭 Base UI 动画配合补全Element.prototype.getAnimations与 Dify UI testing contract 中Animation setup一节的做法一致Storybook 侧则保留真实动画生命周期全局 fetch 守卫globalThis.fetch被替换为记录器任何测试若意外发起 fetchafterEach会直接throw new Error(Unexpected fetch request(s): ...)——绝不真实网络请求由此从文字变成机制vi.mock(zustand)自动在每测后重置所有 storereact-i18next被全局 mock见下文 i18n-mockmonaco-editor/react被 mock 成轻量textarea替身避免测试环境加载重型编辑器beforeEach中清理 localStorage、重置 fetch 记录。i18n 与 nuqs 的测试基建文档要求 i18n 使用全局react-i18nextmock需要自定义翻译时才用 createReactI18nextMock。阅读该实现可以看到它并非简单打桩支持字符串 key 与 selector 函数两种i18nKeyselector 通过 Proxy 捕获访问路径还原为ns.key形式的翻译 keyt()的解析顺序为translations[key]→translations[ns.key]→ 回退返回ns.key参数以:JSON后缀序列化进返回值便于断言插值t函数按 namespace 缓存保证同引用跨渲染稳定避免把t放进依赖数组时引发无限重渲染Transmock 渲染span>projects: [ { extends: true, test: { name: unit, pool: threads, environment: happy-dom, globals: true, setupFiles: [./vitest.setup.ts], exclude: [...configDefaults.exclude, browserTestPattern], }, }, { extends: true, // ...tailwindcss 插件、browser 优化 test: { name: browser, globals: true, setupFiles: [./vitest.browser.setup.ts], include: [app/**/*.browser.spec.{ts,tsx}], browser: { enabled: true, provider: playwright(), instances: [{ browser: chromium }], headless: true, screenshotDirectory: ./.vitest-browser/screenshots, screenshotFailures: true, trace: { mode: retain-on-failure, tracesDir: ./.vitest-browser/traces }, }, }, }, ]要点对照文档unithappy-dom环境 加载 web/vitest.setup.ts并排除app/**/*.browser.spec.{ts,tsx}browser仅包含上述 browser spec 模式Playwright Chromium headless 运行加载 web/vitest.browser.setup.ts——该文件导入globals.css、固定data-themelight、关闭 Base UI 动画并同样全局 mockreact-i18next保证浏览器环境与单元测试的翻译契约一致失败产物截图与 trace 保留在web/.vitest-browser/screenshotFailures: true、retain-on-failure与文档CI 仅在存在失败产物时上传该目录Browser Mode 不负责 coverage 与报告合并一一对应。命令从web/目录运行web/package.json 中test: vp test --project unit也印证标准命令必须显式选项目# happy-dom省略路径即跑完整 unit 项目 vp test run --project unit path/to/spec-or-directory # Browser Mode省略路径即跑完整 browser 项目 vp test run --project browser path/to/spec.browser.spec.tsx # Watch 模式Browser Mode 换 --project browser vp test watch --project unit path/to/spec # unit 项目的诊断性覆盖率报告不是验收目标 vp test run --project unit --coverage path/to/spec-or-directory文档再次强调总是显式传--project unit或--project browser。裸vp test会运行两个已注册项目不是标准 Web 测试命令。测试文件组织与共享约定新组件/feature spec 通常放在同级__tests__/目录既有共存的工具与 hook spec 跟随所属模块约定跨 feature 集成 spec 放 web/tests/react-i18next共享 mock 全局加载自定义翻译才用web/test/i18n-mock的createReactI18nextMocknuqs行为用web/test/nuqs-testing.tsxhelper 并断言 URL 更新仅在 URL 同步明确不在契约内时 mock未经项目级论证不得再引入新的测试 runner、DOM 环境或网络拦截库——这是对测试栈漂移的显式治理。八、测试工作流与评审清单七步工作流先读行为所有者、其公开依赖与邻近测试在决定加测试之前先陈述契约与回归风险选择能证明契约的最小边界行为变更或 bug 修复时尽量先建立失败用例实现一个连贯场景只跑聚焦 spec修复后再扩大范围运行受影响套件与相关仓库检查删除冗余断言、不必要的 mock 与只镜像实现的测试。跨多文件时按依赖排序验证每个连贯切片后再继续默认不要为每个源文件建一个测试文件。评审清单可直接作为 PR 检查项每个测试是否保护一个可达的产品契约或有意义的回归行为是否通过公开边界被驱动该用语义化查询与可访问性契约的地方是否用了mock 是否落在有意选择的边界上且对该边界保真套件是否确定、聚焦且维护成本低于它所防的回归一次保留行为的 refactor 后测试是否依然存活评审者能否说出一个现实回归以及哪条断言会失败Browser Mode浏览器专属契约是否明确、在happy-dom中无法忠实证明、且值回额外运行时九、小结这套体系的可迁移设计web/docs/test.md 表面上是一份前端测试规范实质上是 Dify Web 应用的可维护性设计文档以契约为纲测试价值由可达的产品契约与回归风险定义覆盖率、行数、文件数都不是目标双项目显式分环境unithappy-dom快而默认与browserPlaywright Chromium只证明浏览器专属契约在 web/vite.config.ts 中用 include/exclude 模式硬隔离命令层强制--project避免随手切模式纪律机制化fetch 守卫、zustand 自动重置、i18n 稳定引用缓存、nuqs URL spy 等让规范变成 setup 文件里的可执行约束web/vitest.setup.ts、web/test/nuqs-testing.tsx测试栈冻结禁止未经论证引入新 runner/环境/拦截库保证数千个 spec 的技术债不膨胀。如果你是 Dify 前端贡献者建议按读行为所有者 → 陈述契约 → 选最小边界 → 先写失败用例的顺序工作并用上文的评审清单自查如果你在维护一个类似规模Next.js React Testing Library 组件库的前端项目这份指南中Browser Mode 准入标准与mock 真实边界两节是最值得直接借用的部分。【免费下载链接】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),仅供参考