拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Astryx Probe Theme 深度解析:自动覆盖全部 Theming Target 的视觉门禁测试夹具

Astryx Probe Theme 深度解析自动覆盖全部 Theming Target 的视觉门禁测试夹具【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryxAstryx 的astryxdesign/theme-probe下文简称 probe theme是一个永远不会被任何人使用的主题——它是一份由组件文档自动生成的主题测试夹具专门用来回答一个其他工具都回答不了的问题每一个被文档声明的 theming target它的 override 是否真的到达了像素在 Astryx 的视觉门禁visual gate体系中probe theme 承担着覆盖率仪器的角色新增一个主题目标从文档落地的当天起就被自动覆盖不需要任何人记得去补用例。读完本文你将理解这套目标枚举 → 自动生成 → 像素可达性校验的完整闭环并能在自己的 Astryx 仓库中运行、校验和扩展它。为什么需要一个探测主题真实主题覆盖不到的盲区真实主题如neutral、butter、gothic等只会样式化其设计者关心的那一小部分表面。这带来一个结构性隐患绝大多数可主题化themeable的表面从来不会被任何真实主题实际使用一个新增的主题目标默认就生活在这个未经验证的集合里。没有任何东西告诉你它什么时候停止工作——因为从来没有任何东西在样式化它。这正是 packages/themes/probe/README.md 开头就强调的动机。在 .github/scripts/visual-gate/lib/probe-theme.mjs 的文档注释中给出了一个具体数字门禁曾在这个仓库里发现 49 个从未被任何真实主题触碰过的 override。这是截图基线根本无法察觉的视觉基线只能证明组件看起来和上周一样——哪怕它看起来一样的原因恰恰是主题 override 失效、且那张坏截图被当成了正确基线。像素 diff 永远无法区分这两种情况。probe theme 用构造的方式堵住这个盲区它从目标枚举生成因此覆盖每一个被声明的 target、variant 和 state。今天新增的目标明天文档一落地就自动被覆盖不需要任何人记住去写用例。设计核心一每个 selector 一个独特颜色让元素塌缩无处可藏probe theme 最直观的设计是它的配色方案每一个 selector 都获得一个从名字哈希派生的、刻意刺眼的独特颜色。它来自 probeColor生成器中的probeColor函数export function probeColor(seed, options {}) { let hash 0; for (let index 0; index seed.length; index 1) { hash (Math.imul(hash, 31) seed.charCodeAt(index)) | 0; } // Golden-angle hue stepping keeps adjacent selectors far apart in hue. const hue Math.abs(hash * 137.508) % 360; const saturation 70 (Math.abs(hash 8) % 25); const lightness options.lightness ?? 45 (Math.abs(hash 16) % 20); return hsl(${hue.toFixed(1)} ${saturation}% ${lightness}%); }算法要点确定性同一 seed 在任何一次运行中都产生同一颜色基线可跨运行比较黄金角步进137.508°让相邻 selector 在色相上尽可能远离便于人眼区分可控明度通过lightness选项固定明度保证文本对比度。为什么每个 selector 必须颜色不同README 给出了精确的理由两个本应是不同元素、实际上却解析到同一个元素的子目标会显示为同一个颜色——这是该子目标并没有真正独立这类 bug 的直接信号而一个统一粉红色hot-pink的主题会把它完美隐藏。在此基础上paint 把同一个 seed 派生为四组独立的颜色属性而不是一坨平色export function paint(seed) { return { backgroundColor: probeColor(seed), color: probeColor(${seed}/text, {lightness: 12}), borderColor: probeColor(${seed}/border, {lightness: 25}), outlineColor: probeColor(${seed}/outline, {lightness: 25}), }; }如果backgroundColor和color用同一种颜色文本会隐形——一个文字颜色回归会被正常的背景掩盖diff 报告对人类判读者也完全不可读。四色分离后即使元素无法显示背景内联图标、display: contents包装器仍可通过文本或边框色证明 override 到达。生成器还保留了一个非颜色探针PROPERTY_PROBES {popover: {borderRadius: 32px}}颜色只能证明目标可达这个圆角值能证明目标坐在负责绘制该文档化属性的元素上。设计核心二defineTheme六个轴全覆盖生成出的 packages/themes/probe/src/probeTheme.ts 头部注释明确声明defineTheme接收的六个输入它全部覆盖轴probe 的处理方式components277 个 target、898 个 selector从组件文档生成当前仓库实况tokens自定义属性从被主题化的元素上读回校验icons注册表里每一项都替换为带标记的字形indicatorscheck/radio/checkbox全替换——这是触及面最广的一次替换fonts一个只有主题才能产生的字体族名syntax每个代码 token 一种不容错认的颜色其中只有components是生成的其余固定值位于 packages/themes/probe/src/probeConfig.ts因为它们是要被断言校验的契约而非文档的投影。probeConfig.ts中可以看到三类探针值/** Tokens the probe overrides, observable on any themed subtree. */ export const PROBE_TOKENS: Recordstring, string { --color-accent: rgb(255, 0, 128), --color-background-body: rgb(0, 32, 16), --color-text-primary: rgb(240, 255, 0), --color-border: rgb(0, 224, 255), --radius-element: 13px, --spacing-4: 17px, --duration-fast: 11ms, --font-size-base: 15.5px, }; /** A font stack that can only have come from the theme. */ export const PROBE_FONT AstryxProbeFace, monospace;语法主题 PROBE_SYNTAX 则覆盖keyword、string、comment、number、function、type、variable、operator、constant、tag、attribute、property、punctuation、background共 14 个 token每个都以[light, dark]明暗成对给出。类型特意不注解为宽泛的Record——字面键和元组形状本身就是契约宽化类型反而会让缺失的 token 被接受任何东西的类型藏起来。图标与指示器用data-*标记做 DOM 级断言图标和指示器的注册表替换无法靠看像素来验证替换组件很容易画出与默认视觉上无法区分的东西。因此 packages/themes/probe/src/registries.tsx 让每个替换件用data-astryx-probe-swap标记自我宣告ProbeGlyph是一个带data-astryx-probe-swapicon的粉底青边 SVGaria-hidden因为承载它的组件已经拥有可访问名称probeIconRegistry通过Object.keys(getIconRegistry())反射现有注册表来构建而不是手写名单——手写名单会在有人新增图标那天悄悄失去覆盖这正是该夹具要消灭的漂移probeIndicatorRegistry把check/radio/checkbox替换为渲染状态首字母的ProbeIndicator并携带data-probe-indicator与data-probe-state——这样校验能区分换对了但传错了状态这种真实 bug。由于defineTheme({indicators})是触及面最远的替换替换check后所有渲染选中标记的组件都会跟着换probe 是仓库里第一个真正测试到它的地方。生成器与工作流文档是唯一输入probe theme 的整个生成入口是 .github/scripts/visual-gate/generate-probe-theme.mjs仓库根 package.json 中暴露了两个脚本pnpm visual:probe-theme # regenerate pnpm visual:probe-theme:check # CI guard — fails when a target is uncovered直接调用生成器等价于node .github/scripts/visual-gate/generate-probe-theme.mjs [--check]生成流程分四步loadThemingTargets调用 packages/cli/foundation/discovery/theming-targets.mjs 中 CLI 自己的collectThemingTargets扫描packages/core/src下所有*.doc.mjs读取每个组件的theming.targets声明loadProps读取组件文档的 props 类型用于把视觉 prop 展开成具体的值集合loadTypeAliases用正则从packages/core/src的.tsx源码提取字符串联合类型别名如AvatarSize并递归解析别名之间的组合深度上限 4 作为环保护。之所以用正则而非 TypeScript 编译器是因为--check在每次 PR 上运行完整类型检查的成本不划算——解析不了的联合会落入 skipped 列表被报告而不是静默丢弃renderProbeTheme用仓库自己的 prettier 配置格式化后写盘否则提交前钩子会重排格式--check将因空白差异永远失败。输出两个文件packages/themes/probe/src/probeTheme.ts与packages/themes/probe/src/probeConfig.ts。生成器会把覆盖摘要一并输出例如277 targets, 898 selectors若有无法枚举的视觉 prop非字符串联合类型会列出key.prop — reason明细。--check模式是 CI 保护的核心它把磁盘上的文件与重新生成的输出逐字比较不一致即以::error::退出并提示Run: pnpm visual:probe-theme。新增主题目标而不重新生成构建即失败——这正是全部意义所在一个没人记得覆盖的目标恰恰就是会悄悄停止工作的目标。为什么不要手编 probeTheme.tsREADME 有一句明确的告诫src/probeTheme.ts是生成文件不要编辑它要改生成逻辑改 .github/scripts/visual-gate/lib/probe-theme.mjs生成器把可复用的probeColor、unionValues、buildProbeComponents、paint、renderProbeTheme都拆在这里便于单独测试。生成文件头也带generated by ... — do not edit标记。同理probeConfig.ts由 probe-axes.mjs 的数据生成注释里写明它之所以生成而非手写是为了让主题设置的值与门禁断言的值是同一个字面量——同一份固定数据的两份拷贝会发生漂移而 check 会把漂移误报为主题损坏。Theming Target 枚举CLI 与门禁共享同一真相源probe theme 之所以能新目标文档落地即覆盖关键在于它复用了 CLI 自身的枚举。collectThemingTargets见 packages/cli/foundation/discovery/theming-targets.mjs扫描组件文档中的theming.targets声明这正是astryx theme targets打印、astryx theme build校验的同一份清单。正如 .github/scripts/visual-gate/lib/sources.mjs 注释所说这里没有第二份注册表——如果门禁和编译器对什么可主题化产生分歧那是同一个共享函数的 bug而不是两个列表之间的漂移。一个 target 声明的样子以 packages/core/src/Badge/Badge.doc.mjs 为例theming: { targets: [ {className: astryx-badge, visualProps: [variant]}, ], },packages/core/src/Switch/Switch.doc.mjs 则展示了多目标 状态轴的完整形态theming: { targets: [ { className: astryx-switch, visualProps: [size], states: [checked, disabled], }, { className: astryx-switch-thumb, visualProps: [size], states: [checked], }, { className: astryx-switch-field, visualProps: [labelPosition, labelSpacing], }, {className: astryx-switch-label}, ], },collectThemingTargets还会通过subComponentOf合并父/子重复声明子目标与父目标声明了同一个 class 时props 和 states 合并进父行、子行移除并按键排序保证确定性输出。unionValues如何把 prop 类型展开成值集合buildProbeComponents 对每个 target 生成一个baseselector再为每个视觉 prop 的每个文档化取值生成prop:value形式的 selector如variant:info为每个 state 生成独立 selector如checked。值集合由unionValues解析内联字符串联合sm | md | lg→[sm,md,lg]命名别名如size: AvatarSize→ 从源码解析出的值集——因为文档写着AvatarSize和内联写联合是同样真实的变体轴跳过它们会让三分之一表面未探测单一字面量 →[]那是常量不是变体轴number/boolean/ 对象 →[]没有可枚举值集可探测。解析不出值集的 prop 会进入coverage.skipped报告附原因类型不可枚举或并非所属组件的文档化 prop而不会静默丢弃——这正是 .github/scripts/visual-gate/lib/probe-theme.test.mjs 中单测所断言的行为。像素可达性校验reach check 如何工作生成主题只是前半段后半段由 .github/scripts/visual-gate/lib/probe-reach.mjs 完成。它的核心洞察是给每个 selector 唯一确定性的颜色后override 是否到达元素就从像素 diff 变成了等式判断——计算该 selector 应当产生的颜色读取元素的计算样式比较。没有基线、没有图片、没有 flake还能直接点名失败的目标而不是给出一块移动的像素。校验在浏览器内执行READ_TARGETS遍历[class*astryx-]元素读取其data-*反射属性name或name:value与getComputedStyle的backgroundColor、color、borderTopColor。expectedColors(key, data)计算该元素所有合法 selectorbase 每个反射 prop 每个反射 state经paint产生的全部颜色并转成getComputedStyle返回的rgb(...)字符串——因为variant:info覆盖base是级联在正常工作不是 miss所以元素要跟每一个合法作用于它的 selector 比对。fold累加器维护三个集合verified只要有一个元素证明 override 到达即达成同一目标的后续元素因合法原因显示别的值不得撤销它shadowed同一元素上另一个 target 获胜两个目标其实是同一个元素单独报告——这两个目标是同一个元素是有价值的事实且不同于这个 override 什么也没到达failures第一个失败的故事胜出保证报告指向稳定位置同一失败不会因遍历顺序在不同运行间读出不同故事。这些逻辑hslToRgb、expectedColors、READ_TARGETS、fold、emptyAccumulator都有对应的单元测试 .github/scripts/visual-gate/lib/probe-reach.test.mjs。门禁配置中的 probe 位置在 .github/scripts/visual-gate/visual-gate.config.json 中门禁把 probe 明确当作覆盖仪器而非设计对待probeTheme: probebaselineThemes: [neutral, probe]neutral 是默认产品参照probe 是生成的覆盖夹具门禁 tier 列表为[surface, theme-matrix, probe]。sources.mjs 的loadThemeOverrides在统计真实主题覆盖矩阵时刻意排除 probe——它按构造覆盖所有目标喂给主题矩阵会要求每个 (目标 × 渲染它的故事) 拍一张当前约 614 张而 probe tier 用 128 张集合覆盖set cover就回答了同一个问题。测试契约生成器自身的三重防线probe theme 的正确性由三层测试守护probe-theme.test.mjs生成器单元测试断言probeColor确定性、不同 selector 不同色、lightness生效断言buildProbeComponents对每个 target 生成 base、把变体 prop 展开为每个文档化值、覆盖每个声明状态、popover 带32px圆角探针、不可枚举 prop 进入 skipped、coverage 计数正确如 2 targets / 6 selectors、且确定性地可复现同文档同主题重新生成是无 diff 的空操作probeTheme.test.ts生成产物契约测试用generateThemeCSS(probeTheme)生成 CSS断言输出包含反射的[data-选择器、且绝不出现target 加裸值的选择器形式reach check见上在真实 Storybook 构建上逐元素比对计算样式。在仓库中如何运行与扩展当前仓库中运行 probe 相关流程# 在仓库根目录 pnpm install # 安装工作区依赖 pnpm visual:probe-theme # 从组件文档重新生成 probe theme pnpm visual:probe-theme:check # CI 守卫目标未覆盖/文件过期即失败构建该主题包本身packages/themes/probe目录内走的是标准 Astryx 主题构建管线见 packages/themes/probe/package.jsonpnpm -F astryxdesign/theme-probe build其 build 脚本等价于astryx theme build src/probeTheme.ts -o dist/theme.css --icons-specifier ./registries.mjs随后经tsup与tsc产出 ESM/CJS 双格式并由check-fully-specified.mjs校验。该包private: true永远不会发布——它是纯测试夹具。若要扩展 probe theme 的生成逻辑比如新增一个探针属性轴正确入口是修改 .github/scripts/visual-gate/lib/probe-theme.mjs以及需要时配套的 axes 数据然后运行pnpm visual:probe-theme重新生成手动编辑src/probeTheme.ts会在下一次--check中被判为过期。总结从有人记得到构造保证probe theme 的价值不在于它好看它刻意刺眼而在于它把每个文档化主题目标仍然生效从靠人记忆、靠运气的维护负担变成了由文档驱动、由构造保证的系统性质。它通过三个机制实现以 CLI 的 target 枚举为唯一真相源自动生成用确定性哈希颜色把是否到达像素变成等式判断用--checkCI 守卫让新增目标未覆盖直接失败。对任何维护大型设计系统主题契约的团队来说这套覆盖仪器 可达性断言的思路都值得直接借鉴——而 Astryx 仓库中的完整实现生成器、reach check、测试、配置就是可阅读、可运行、可复用的第一手参考。【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门