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 自有契约归本包"这一边界来组织的。
文档给出的命令分为两层:
仓库根目录执行格式、lint 与 TypeScript 诊断:
vp check packages/dify-ui在
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(),实例为chromium,headless: 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-plugin的storybookTest({ 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-react的render与vite-plus/test/browser的userEvent; - 断言默认
type="button"、可覆盖为submit、nativeButton={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-a11y、addon-vitest、addon-docs、addon-themes等插件链——a11y与vitest两个 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包内三处相关配置分别对应文档中的三条规则:
unit 项目默认关闭动画:vitest.setup.ts 正是文档中代码片段的落地,此外它还引入
./vitest.css并将document.documentElement.dataset.theme固定为light,保证主题变量与故事一致;Storybook 项目保留真实动画生命周期:文档明确指出 Storybook 使用其 preview setup,"must retain real animation lifecycles",因此
vitest.setup.ts只对 unit 项目生效(见 vite.config.ts 中仅 unit 项目配置setupFiles);有意断言动画行为的单测可局部恢复为
false,但必须在 cleanup 中还原旧值。仓库中已有两处示范这一模式:- popover 测试(约 L41-L69):在测试前读取并保存
animationSettings.BASE_UI_ANIMATIONS_DISABLED,置为false,在 teardown 中还原为animationsDisabled; - toast 测试(L93-L162 附近):同样先保存
animationState,测试中关闭禁用标志,结束后还原。
这种"保存—改写—还原"的写法避免了测试间的全局状态污染,是遵循文档要求的具体实现范式。
- popover 测试(约 L41-L69):在测试前读取并保存
五、端到端自查流程
综合文档与仓库配置,修改packages/dify-ui中的一个组件后的完整验证流程为:
- 在
packages/dify-ui/下运行vp test --project unit,确认新增/修改的 wrapper 契约(class 变体、透传 props、data-attribute 钩子等)通过; - 在
packages/dify-ui/下运行vp test --project storybook --run,确认所有 story 的渲染契约与 a11y 检查通过(违规会因test = 'error'而失败); - 需要交互式核对组件行为时运行
vp run storybook打开 6006 端口的 Storybook(注意 Storybook 保留真实动画,验证 presence/transition 相关行为时不要依赖 unit 项目的动画禁用标志); - 回到仓库根目录运行
vp check packages/dify-ui,完成格式化、lint 与 TypeScript 诊断; - 若涉及动画相关的 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),仅供参考