news 2026/9/7 3:43:26

Dify 的 @langgenius/dify-ui 组件包测试体系:Vitest 双项目、Storybook 渲染契约与 a11y 门禁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dify 的 @langgenius/dify-ui 组件包测试体系:Vitest 双项目、Storybook 渲染契约与 a11y 门禁

Dify 的 @langgenius/dify-ui 组件包测试体系:Vitest 双项目、Storybook 渲染契约与 a11y 门禁

【免费下载链接】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 仓库中packages/dify-ui/docs/testing.md这份《Testing and Development》文档为核心,系统讲解@langgenius/dify-ui组件包的完整测试与开发工作流:如何运行格式化/lint/类型检查与双 Vitest 项目、"Storybook 故事即渲染契约"的测试边界设计、Storybook a11y 门禁的配置细节,以及 Base UI 动画在测试中的处理策略。读完本文,你将能够在该包中正确选择测试归属(unit 还是 storybook)、运行全部测试命令、并为组件编写符合包规范的单元测试与 Storybookplay测试。

一、包的定位与命令入口

packages/dify-ui是 Dify 工作区内的私有 UI 原语包(@langgenius/dify-ui),提供独立 UI 原语、设计 token、CSS 优先的 Tailwind 样式和cn()工具函数,大部分交互原语是对 Base UI headless 组件的"薄而有主见"的封装(见 README)。测试体系正是围绕"Base UI 上游行为归上游、Dify 自有契约归本包"这一边界来组织的。

文档给出的命令分为两层:

  1. 仓库根目录执行格式、lint 与 TypeScript 诊断

    vp check packages/dify-ui
  2. packages/dify-ui/目录下执行测试与 Storybook 命令

    命令作用
    vp test --project unit运行原语(primitive)单元测试
    vp run storybook启动 Storybook
    vp test --project storybook --run以浏览器模式运行 Storybook 组件测试
    vp test同时运行两个测试项目

这些命令与包内 package.json 中的scripts一一对应:"test": "vp test --project unit""test:storybook": "vp test --project storybook --run""test:watch": "vp test --project unit --watch""storybook": "storybook dev -p 6006""type-check": "tsc"。也就是说,vp test这类 vite-plus 命令既可以通过根目录直接执行,也可以落到 npm script 上日常调用,二者等价。

二、测试边界:两个 Vitest 项目,一个 Chromium 运行时

文档明确了两点架构性事实:

  • 包内配置了两个 Vitest 项目(Vitest projects),二者都运行在Playwright Chromium Browser Mode中——项目名标识的是"行为归属方",而不是不同的运行时。
  • Storybook 用于承载"有文档的组件示例":每一个 story 都是一份渲染契约(render contract),并通过 Storybook Vitest addon 运行配置的无障碍检查。当示例还承担"可见状态变化、用户交互、键盘路径、overlay 流程、表单行为、加载行为、受控状态协同"中的任一项时,应为其添加play测试。
  • 普通 Vitest 测试用于更底层的 wrapper 契约:如 class 变体(cva variants)、Base UI 透传 props、hidden-input 序列化、data-attribute 钩子、stores,以及不需要"有文档示例"覆盖的边界情况。

源码层面的项目配置印证

vite.config.ts 完整实现了这一边界:

  • 顶层test.browser开启enabled: true,provider 为playwright(),实例为chromiumheadless: true,失败时自动截图(screenshotFailures: true,截图落在./.vitest-browser/screenshots);
  • projects[0]命名unit:挂载 Tailwind 插件、开启globals、加载setupFiles: ['./vitest.setup.ts']include: ['src/**/__tests__/**/*.spec.{ts,tsx}'],并配置浏览器 trace 在失败时保留(trace.mode: 'retain-on-failure');
  • projects[1]命名storybook:通过@storybook/addon-vitest/vitest-pluginstorybookTest({ configDir })加载.storybook目录下的故事并生成测试;
  • 覆盖率由 v8 提供,include: ['src/**/*.{ts,tsx}'],排除 stories、__tests__、themes、styles;CI 环境(process.env.CI)输出json + json-summary,本地额外输出text报告。

这条路径解释了为什么"unit 项目也要跑在浏览器里":因为组件依赖真实 DOM、CSS 布局与 Base UI 的 presence 生命周期,浏览器模式才是与生产一致的验证环境。

unit 测试实例:Button 契约

以 Button 单元测试 为例,可以看到"wrapper 契约"测试的典型形态:

  • 使用vitest-browser-reactrendervite-plus/test/browseruserEvent
  • 断言默认type="button"、可覆盖为submitnativeButton={false}时经 render prop 渲染为非原生元素;
  • 断言disabled使用原生语义(toBeDisabled()aria-disabled),loading状态可通过focusableWhenDisabled={false}选择退出焦点;
  • 断言 loading 中的 submit 按钮不会隐式触发表单提交——这正是文档所说"不需要有文档示例的底层契约"的典型用例。

三、无障碍(a11y)门禁:test = 'error'与唯一的 color-contrast 例外

文档对 a11y 的约定非常严格:

  • Storybook 无障碍测试使用a11y.test = 'error'任何被启用的违例会直接让测试失败
  • 颜色对比度(color-contrast)是唯一被全局禁用的规则,原因是它是已知的设计 token 缺口(known design-token gap);
  • 明确规定:不要新增任何全局例外;临时例外必须局部化到受影响的 story;不要用play测试来替代一个无障碍修复

这一约定在 .storybook/preview.tsx 中逐行落实:

a11y: { test: 'error', config: { rules: [ { id: 'color-contrast', enabled: false, }, ], }, },

同时该 preview 文件通过withThemeByDataAttribute装饰器以data-theme属性切换light/dark主题(默认 light),并以tags: ['autodocs']为每个 story 自动生成文档页。而 .storybook/main.ts 声明了故事发现 glob../src/**/*.stories.@(js|jsx|mjs|ts|tsx)react-vite框架,以及addon-a11yaddon-vitestaddon-docsaddon-themes等插件链——a11yvitest两个 addon 正是"story 即契约 + 违规即失败"机制的执行者。

实践含义:如果你在某个 story 中确实需要临时豁免某条 a11y 规则,应把该豁免写在对应 story 的局部配置里,而不是回到 preview 里再加一条enabled: false

四、动画测试策略:BASE_UI_ANIMATIONS_DISABLED标志

Base UI 在卸载由 transition 驱动的原语前会等待element.getAnimations()。当测试断言的是最终 DOM 状态而非动画行为本身时,文档要求在 Vitest setup 文件中关闭动画:

;( globalThis as typeof globalThis & { BASE_UI_ANIMATIONS_DISABLED: boolean } ).BASE_UI_ANIMATIONS_DISABLED = true

包内三处相关配置分别对应文档中的三条规则:

  1. unit 项目默认关闭动画:vitest.setup.ts 正是文档中代码片段的落地,此外它还引入./vitest.css并将document.documentElement.dataset.theme固定为light,保证主题变量与故事一致;

  2. Storybook 项目保留真实动画生命周期:文档明确指出 Storybook 使用其 preview setup,"must retain real animation lifecycles",因此vitest.setup.ts只对 unit 项目生效(见 vite.config.ts 中仅 unit 项目配置setupFiles);

  3. 有意断言动画行为的单测可局部恢复为false,但必须在 cleanup 中还原旧值。仓库中已有两处示范这一模式:

    • popover 测试(约 L41-L69):在测试前读取并保存animationSettings.BASE_UI_ANIMATIONS_DISABLED,置为false,在 teardown 中还原为animationsDisabled
    • toast 测试(L93-L162 附近):同样先保存animationState,测试中关闭禁用标志,结束后还原。

    这种"保存—改写—还原"的写法避免了测试间的全局状态污染,是遵循文档要求的具体实现范式。

五、端到端自查流程

综合文档与仓库配置,修改packages/dify-ui中的一个组件后的完整验证流程为:

  1. packages/dify-ui/下运行vp test --project unit,确认新增/修改的 wrapper 契约(class 变体、透传 props、data-attribute 钩子等)通过;
  2. packages/dify-ui/下运行vp test --project storybook --run,确认所有 story 的渲染契约与 a11y 检查通过(违规会因test = 'error'而失败);
  3. 需要交互式核对组件行为时运行vp run storybook打开 6006 端口的 Storybook(注意 Storybook 保留真实动画,验证 presence/transition 相关行为时不要依赖 unit 项目的动画禁用标志);
  4. 回到仓库根目录运行vp check packages/dify-ui,完成格式化、lint 与 TypeScript 诊断;
  5. 若涉及动画相关的 DOM 状态断言,确认你的测试落在 unit 项目(setup 已自动关闭动画);若要断言动画本身,参考 popover/toast 的测试写法在局部临时恢复BASE_UI_ANIMATIONS_DISABLED = false并保证 cleanup 还原。

关键文件索引

关注点文件
本文核心文档testing.md
命令脚本package.json
双项目与浏览器模式配置vite.config.ts
unit 项目 setup 与动画禁用vitest.setup.ts
a11y 门禁与主题装饰器preview.tsx
Story 发现与 addon 链main.ts
unit 测试示例button/index.spec.tsx
动画标志的局部恢复范式popover/index.spec.tsx、toast/index.spec.tsx

这套体系的核心思想可以概括为:用浏览器模式统一运行时,用"项目名"划分行为归属,用 Storybook 把文档示例升级为可执行的渲染契约,用error级别的 a11y 检查把可访问性变成 CI 硬门禁,并用一个全局动画标志在"断言状态"与"断言动画"两类测试之间做出清晰取舍。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 3:43:15

微机原理与接口技术核心总结:从8086寻址到中断与接口芯片

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 3:42:00

Codex桌面版打不开?Windows环境手动排查修复指南

如果你的 Codex 桌面版双击之后完全没反应,或者好不容易弹出个窗口又秒退,这篇文章应该是你目前最需要的东西。我前前后后在 Windows 上修过很多次 Codex 桌面版,网上各种“一键修复脚本”也试过不少,最后发现一个扎心的事实&…

作者头像 李华
网站建设 2026/9/7 3:41:21

WebGPU + MobileNet:浏览器端实现以图搜图特征提取

我最早接触到“以图搜图”这个需求,是帮一个摄影社区做图库管理。当时第一反应是上服务端跑特征提取,模型用 MobileNet,最后一层截掉,拿 1024 维向量做余弦相似度。方案本身不复杂,真正让我头疼的是服务端的资源成本、…

作者头像 李华
网站建设 2026/9/7 3:40:35

整活短视频批量处理流水线:抽帧、OCR、TTS与FFmpeg合成实战

这次我们来看一个很容易被当成“纯梗标题”的需求:鲨鱼大招炸空气之后破防误吃麦的章鱼老头。先别纠结标题里的“星导晶”,那更像一个自用标签;真正有价值的是这串文字背后的处理需求。如果把这句话交给内容处理工具,它其实是一条…

作者头像 李华
网站建设 2026/9/7 3:40:20

触摸屏报警急停开关异常?从急停回路原理到排查流程全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华