
Apache Maka 共享 UI Storybook 编写规范分层测试、Fidelity 约定与浏览器级验证实践【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka导读本文基于 Apache Maka 仓库中packages/ui/stories/AGENTS.md及其上游约定apps/desktop/stories/FIDELITY.md、apps/desktop/e2e/AGENTS.md展开。共享 UI 包packages/ui的 19 个 Storybook 故事文件运行在桌面端 Storybook host 中如何让每一个故事都成为「产品真实渲染状态的像素级证据」而非自说自话的脚手架是本篇要解决的核心问题。读完本文你将掌握Maka 的三层测试边界划分单元/组件测试 → 浏览器 Storybook → Electron、Fidelity 约定的六大铁律、Real path注释的写作方法以及从 typecheck 到 smoke 的完整验证链路。背景为什么共享 UI 需要一个「约定」而不是一堆故事Maka 的 UI 代码分为两层packages/ui共享组件库被 CLI、桌面端等多处复用与apps/desktopElectron 桌面应用。UI 组件的 Storybook 故事运行在Desktop Storybook host中——apps/desktop/stories/AGENTS.md明确说明「The Desktop Storybook host also discoverspackages/ui/stories」即不存在独立的 UI Storybook runner。这意味着packages/ui/stories里的每一个故事实际上都是桌面端产品表面的投影。packages/ui/stories/AGENTS.md共享 UI Storybook 约定只有四条规则却精确地划出了边界写或改故事之前先读 FIDELITY.md——桌面端与 UI 共享的 fidelity 约定不要在这里维护第二套规则纯状态与组件行为优先进packages/ui/src/__tests__不要仅仅因为某个测试碰巧渲染了 React 就建一个浏览器故事真实 Chromium 的布局、滚动、选择、焦点、动画检查放在浏览器层fake DOM 的几何不是布局证据仅凭 viewport、主题或滚轮需求不足以升级到 Electron升级前必须遵循 Electron 准入通过生产 props 与行为驱动共享组件桌面端的 shell 接线属于 Desktop stories不要复制 shell 进 UI fixture同时保留真实的 owning frame。这四条规则背后的工程哲学是每一个断言都应当落在能暴露缺陷的最低层级上。下面逐层展开。三层测试边界的精确划分apps/desktop/e2e/AGENTS.mdElectron 测试准入把这一原则形式化为三级阶梯且要求对每条行为断言单独应用而不是对整个文件层级适用断言依据与示例单元/组件/集成测试状态、事件顺序、请求路由、存储与恢复桌面端渲染测试运行于apps/desktop/src/main/__tests__经node --testReact 测试用既有 fake DOM共享 UI 测试位于packages/ui/src/__tests__Storybook / 聚焦浏览器测试需要 Chromium 的行为布局、滚动、选择、焦点、动画、viewport、主题复用apps/desktop/stories、packages/ui/stories与 FIDELITY.mdElectron更低层级会漏掉的具体 Electron 边界原生窗口/输入、preload/main 集成、跨进程持久化或生命周期故障例如会话持久化必须由真实应用承载三个层级之间的「准入证据」要求同样严格每个测试旁须说明它验证的是哪个Electron 持有的机制以及更低层级测试会漏掉什么缺陷。仅用于准备场景的调用不算该机制——「创建一个 Host Session 不把一个焦点测试变成 IPC 测试读取一个 Host 快照本身不证明需要 Electron」。真实的 wheel 输入、CDP、几何、localStorage、页面重载、preload 测试锁存latch单独存在都不构成 Electron 需求必须追踪被断言行为的真正属主。不能以「同文件里的另一个测试」「窗口已经打开」「测试数量没变」来为纯渲染器断言辩护独立的渲染器契约应当下移。迁移时先搜索既有低层级覆盖扩展真实的组件/控制器/服务 seam不要复制产品逻辑到第二个 shell也不要引入全局渲染完成协议global render-completion protocol来迁就测试。Fidelity 约定故事必须是产品真实渲染的状态apps/desktop/stories/FIDELITY.md是 Desktop 与 UI 两层 story 共用的总约定适用于每一个Product/*故事Primitives/*与Design System/*豁免因为它们展示组件状态而非产品表面。其开篇就点明了动机Storybook 是像素工作的场所故事被当作产品外观的 ground truth——这只有在每个故事都是应用真实渲染的状态时才成立。铁律一每个 Product 故事都对应一个真实用户可达的状态文档以 #1433 的两个发现为例一个 135px 的 hero 偏移和一个坏掉的垂直居中都是在「组合了应用从不渲染的状态」的故事上量出来的构建后的应用里两者都不复现。故事对组件没错但对产品错了所有基于它的测量全部作废。因此每个故事上方都要有一条// Real path:注释写明用户如何到达该状态// Real path: sidebar → 扩展 → 技能, with skills installed. export const Populated: Story { … }注释故意采用散文形式它的价值在于「有人真的沿路径走了一遍并写了下来」机械的在场检查presence check同样容易被一个看起来合理的谎言满足——只有顺着调用链审读的 reviewer 才能判断它是否为真。文档记录了首批注释中有两条是错的且都是靠阅读而不是靠运行抓出来的一条指向了一个无法产生该状态的 builderCommandPaletteDisabledCommand另一条给一个 frame 写了两个 host实际只有一个。所以注释必须写得足够窄、足够可证伪——host、builder、gate 都要具体因为「永远为真的句子」买不到任何东西。铁律二一个状态一个故事而不是一个变体一个故事故事靠「渲染了别的故事没有渲染的像素」来获得存在价值。页面的第二层级——替换列表的路由层级、与背后屏幕无共享内容的表单——是独立状态即使点击可达也要单独成故事已在屏幕上的状态的更窄版本则不是。有两个事实决定取舍且两者都曾被猜错过故事渲染在哪里。实际 viewport 与主题覆盖由 smoke runner 拥有。查看 scripts/storybook-visual-smoke.mjs 而不是假设 Storybook 工具栏参数会生成 CI 任务runner 目前为 story ID 含narrow的故事选择窄 viewport720×900并为选定的 sentinel 增加额外的主题/palette/forced-color 运行。响应式或主题契约需要为必要条件写显式浏览器场景不要因为默认 smoke 没覆盖就搬进 Electron 窗口。play是否到达该状态。play驱动故事进入 reviewer 需要看到的状态而 CI 会运行它——所以它停靠的状态就是 smoke 读到的状态。一个仅靠play步骤区分的故事是第二个状态不是变体。多余的故事仍要付成本reviewer 扫 sidebar 时无法判断哪条是页面本体重复故事每次运行都重渲相同像素却声称带来了不存在的覆盖。若某状态重要但不渲染新像素优先用既有单元/组件测试浏览器或 Electron 测试需要低层级无法验证的边界见 Electron 准入。铁律三frame 重要不只是组件在错误的 wrapper 里挂载正确的组件故事依然不可达。如果应用用一个拥有高度、padding、对齐的类包裹某个表面故事必须同样使用该 wrapper——否则每次对故事的几何比较量的都是故事自己的脚手架。导入 wrapper而不是重打它的 class。手抄的 class 链和手抄的约定块一样会漂移而且是不可见地漂移onboarding.stories.tsx曾被重写为「与应用的 class 链逐 class 相同」结果重写把两层嵌套颠倒、丢了一个 32px 的 header。只写出真正无法导入的部分并在注释里说明是哪部分。当一个组件有两个 host 时一个 frame 不能同时代表两者。capability-audit-strip.stories.tsx曾把「技能」和「定时任务」两条路径指向同一个构建在技能 frame 里的故事但定时任务页面是在 1024px clamp、没有.maka-module-main祖先的容器里挂载同一组件的——于是那条页面根本拿不到的:has( …)网格规则进入了每一次测量。要么构建第二个 frame要么在注释中说明本故事是哪个 host、另一个 host 改变了什么。点明分歧是廉价的一个默默平均两个 frame 的故事比没有故事更糟。在packages/ui/stories/session-list-panel.stories.tsx中frame 的宽度同样被反复校准故事 frame 曾默认 240px把 rail 的默认宽度 260px 裁掉 20px恰好落在尾槽上导致故事无法展示时间戳是否放得下现在 frame 默认 260px窄 rail 故事则把 180pxminWidth同时传给 frame 与组件。铁律四派生 fixture而不是断言它如果 runtime 计算某个字段就去问 runtime 要。硬编码「分类器本应返回什么」的故事是在断言一个事实而非展示事实——分类器移动时没有任何东西会失败。capability-audit-strip.stories.tsx的report()工厂正是如此它接受 summary 的 Partial 输入其余字段全部由运行时默认值补齐故事只注入与风险状态相关的计数。铁律五play在 CI 中运行它的断言是真实覆盖渲染 smoke 会先等 Storybook 的storyFinished事件再读 accessibility tree因此每个play函数都会执行其中任何失败的断言都会让该 lane 失败。FIDELITY 特意纠正了旧文档的错误说法曾写「smoke 以禁用 autoplay 的方式挂载」——实际没有任何东西传embed#4766 落地了 18 个「断言即覆盖」的故事。这让play成为浏览器行为契约的正确归宿活 Selection、文本节点间的 caret、undo 事务、跨重渲染的 portal 身份。这些在packages/ui的 DOM shim 里都不存在也都不需要 Electron。断言要写到只能因为它点名的理由而通过挂载表面后再观察的故事看不到挂载期间发生的任何事所以必须在挂载前安装的探针构造计数、首帧前事件应放进拥有该全局的测试里。session-list-panel.stories.tsx的SessionSwitchRenderBudget是绝佳范例它用MutationObserver在play内统计 style 写入数、行重挂载数与选中行序列断言会话切换只改写 ≤10 次 style、不重挂载任何行、状态节点零变化——「控制器单元测试钉住稳定命令身份这个真实布局故事钉住产生的 DOM 预算于是整轨重写的另一个来源照样失败却不必付 Electron 启动成本」。铁律六渲染 null 的故事不是故事app 与故事冲突时二选一并排是脚手架按异常报告report by exception的组件在健康状态下返回null。capability-audit-strip曾有四个故事、其中三个渲染空白面板还带着自信满满的Real path:注释——它们描述的行为在组件停止汇总健康状态时已经丢失。「这个元素不在页面上」不需要故事删除它并在保留的故事里说明元素何时出现。如今该文件只剩一个WithRisks故事注释明确写着四项计数全为零时组件返回null、页面不承载 strip那个状态没有像素所以没有故事。当 app 与故事不一致时其中必有一个是错的修故事或删故事绝不同时保留「app 版」与「story 版」两个表面——第二个会无声腐烂并带走 reviewer。并排side-by-side故事是脚手架刻意把多个状态并排展示时要在注释里说清楚这个排列是评审辅助每个面板才是可达状态整行不是任何人会看到的屏幕。实操链路typecheck → build → smoke由于没有独立的 UI Storybook runnerpackages/ui/stories的验证复用桌面端指令见 Desktop 验证说明从仓库根目录依次执行# 1. 类型检查Storybook 专属 tsconfig npm --workspace maka/desktop run typecheck:stories # 2. 构建 Storybook 目录产物输出到 apps/desktop/storybook-static npm --workspace maka/desktop run build-storybook # 3. 渲染 smoke不重建 catalog必须先 build npm --workspace maka/desktop run smoke:storybook三点关键约束smoke:storybook对应脚本 scripts/storybook-visual-smoke.mjs它不重建 catalog直接读取storybook-static/index.json——所以改故事后必须先 build 再 smoke聚焦调试时要验证相关play函数真的跑完仅凭最终画面截图不能证明play内的断言通过了smoke 是「目录渲染 无障碍树健康检查」而非像素对比默认 viewport 1280×900story ID 含narrow时用 720×900product-workhub--colored-work-history用 1600×900product-workhub--progress-model-picker用 360×900每个故事至少以 light 渲染DARK_THEME_SENTINEL_STORY_IDS中的 8 个 sentinel 额外跑 darkFORCED_COLORS_STORY_IDS中的general-forced-colors-focus-ring跑 forced-colors。runner 还会等storyFinished事件、收集console.error与 unhandled rejection、检查焦点是否落入aria-hidden/inert表面、检查可见模态对话框是否持有焦点、拒绝「多个可见模态对话框同时打开」、并用Accessibility.getFullAXTree做无障碍审计auditAxTree。另外有REQUIRED_COMPUTER_USE_STORY_IDS清单兜底缺失即失败。迁移回归的纪律先证伪再删除当把旧 E2E 覆盖迁移到浏览器故事或把浏览器断言下移到单元测试时两条规则贯穿始终先证明替代断言能检测出原始缺陷——最好通过回退相关行为或定向 mutation 来验证要验证行为性失败而不是 import/类型错误或「没有新 helper」。在同一 PR 中删除被替换的 E2E 用例、重复断言与未引用的 fixture 钩子但保留独立的原生/跨进程保护。同时存在明确的「不许」清单不得以提高超时、加重试或削弱断言来让迁移通过迁移失败要先诊断是 harness 问题还是真实回归。涉及 E2E 预算时在 apps/desktop/e2e-budget.json 更新计数与具体的边界理由然后从仓库根运行npm run check:e2e-budget校验清单一致性——文档特意提醒绿灯只代表清单一致不代表层级选择正确。编译测试或 Storybook smoke 前必须先 buildElectron 测试要从apps/desktop目录运行因为 fixtures 依赖该工作目录。写在最后这套约定想让什么不发生纵观packages/ui/stories的 19 个文件可以看到约定落地的完整样貌toast.stories.tsx在play中断言 confirm 队列的串行化、初始焦点落在「取消」、error toast 的诊断动作自身失败时 provider 会再抛一条失败 toastsandbox-boundary-prompt.stories.tsx用SandboxBoundaryRequestEvent生产类型构造两个产品级路径文件系统网络 / 仅网络session-list-panel.stories.tsx用 32 行渲染预算会话与 MutationObserver 验证 DOM 预算。它们共同回答一个问题当一个 reviewer 把故事当作产品外观的证据时证据必须是真实渲染的证据。Real path注释是手段三条层级边界是护栏smoke runner 是最终执行者——它运行每一个play、检查每一个焦点与对话框、审计每一棵无障碍树。若你想进一步深入推荐按此顺序阅读FIDELITY.md约定本体、Electron 准入层级判据、storybook-visual-smoke.mjs执行细节再对照 capability-audit-strip.stories.tsx 与 session-list-panel.stories.tsx 两个正反案例即可完整掌握 Maka 的浏览器层测试方法论。【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考