Polished和Chromatic经常被放在一起比较,但严格来说它们不是同一类工具。Polished是一个样式工具函数库,提供类似Sass的lighten、darken、mix等混色函数,帮助你在JavaScript中计算样式值;而Chromatic是一个云端视觉测试平台,它依赖Storybook对组件进行渲染截图,再通过像素级比对发现界面变化。如果你此前的测试策略停留在断言样式计算结果是否正确,那么界面真实渲染出来是否偏离设计稿,这块基本是空白。把测试重心从样式函数迁移到视觉回归,正是这次迁移要解决的核心问题。

先想清楚:Polished和Chromatic到底在测什么
使用Polished做测试时,通常的做法是对混色函数的返回值做单元断言,比如验证lighten(0.2, '#000')是否等于#333333。这类测试有价值,但它验证的是纯函数的输入输出,组件最终在浏览器里长什么样,它完全管不到。一个很典型的场景是:主题色计算逻辑全部正确,但某处CSS书写顺序导致覆盖失效,按钮背景色就是错的,纯函数测试依然全绿。
Chromatic的思路完全不同。它不关心样式是怎么算出来的,只关心渲染结果。每个Story会被上传到云端,在真实浏览器(包括不同视口尺寸)中截图,然后与基线快照做像素级比对。任何一处UI变化——哪怕是一个像素的边框偏移——都会被标记出来供人工审查。这补上了从「样式计算正确」到「界面呈现正确」之间的断层。
迁移前建议做一次盘点:项目中哪些测试是验证Polished函数输出、哪些是验证组件行为的,前者可以保留(成本极低),后者中涉及视觉呈现的部分逐步交给Chromatic接管。完全删掉Polished测试没有必要,两者是互补关系而非替代关系。
接入Storybook并编写可测试的Story
Chromatic强依赖Storybook,如果你的项目还没有接入,第一步就是安装并组织好组件文档。假设项目使用的是Vite构建:
npm install storybook @storybook/react-vite --save-dev npx storybook init
每个需要视觉回归覆盖的组件至少编写一个Story,关键原则是让Story的渲染结果完全确定。以下是一个按钮组件的Story示例:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: '确认提交',
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
children: '确认提交',
disabled: true,
},
};
注意这里为每个状态(正常、禁用、加载中等)单独定义Story,而不是用一个Story配合装饰器切换状态。独立的Story在Chromatic中会各自生成快照,视觉差异的定位会更精准——一旦某次提交只影响了禁用态按钮,你只会收到一个变更通知,而不是整个组件组全部标红。
配置Chromatic并处理动态内容导致的误报
接入Chromatic本身很简单,在官网创建项目拿到token后执行:
npm install chromatic --save-dev npx chromatic --project-token=你的token
首次运行会把所有Story截图并保存为基线,之后的每次运行都会与基线比对。真正需要花时间的是消除误报。组件中如果有当前时间、随机数、异步加载的数据、动画帧,每次截图都会不同,Chromatic会把它们识别成视觉变更。处理手段主要有三类:禁用动画、固定时间、对不稳定区域做遮罩。
export const WithTimer: Story = {
parameters: {
// 禁用CSS动画与过渡,避免截图落在动画中间帧
pseudo: { hover: false },
},
decorators: [
(Story) => (
<div>
{/* 时间戳区域用遮罩忽略像素差异 */}
<div data-chromatic="ignore">
<Clock />
</div>
<Story />
</div>
),
],
};
上面代码中的data-chromatic="ignore"是Chromatic提供的官方遮罩方案,被包裹的区域不参与像素比对,适合时间、头像图片这类天然不稳定的内容。对于动画,可以在全局配置中关闭:chromatic --no-snapshot-dependencies之外,更常用的做法是在Storybook的preview配置里统一禁用CSS transition,保证每个组件在截图瞬间处于静止的最终状态。
异步数据也是常见误报源。建议在Story中使用mock数据配合msw或Storybook自带的loader机制,保证每次渲染的数据集固定。这不仅是Chromatic的要求,对其他类型的组件测试同样受益。
集成CI流水线与基线审批流程
本地手动跑Chromatic只适合调试,真正发挥作用要接入CI。以GitHub Actions为例,在Pull Request触发时运行构建与截图任务:
name: Visual Test
on:
pull_request:
branches: [main]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} --exit-zero-on-changes
这里有两个细节值得注意。第一是fetch-depth: 0,Chromatic需要完整的提交历史来做基线追溯;第二是--exit-zero-on-changes,它让CI在有视觉变更时也不直接失败,而是等待人工在Chasonic平台(也就是Chromatic的Web界面)上审查确认。审查时你可以逐个查看差异区域的放大对比,确认是有意的改版就点击接受,新基线随之生成;发现是意外破坏就打回,附上评论定位到具体提交。
这个审批机制让视觉回归从机器判断变成了人机协作:机器负责精确到像素的检测,人负责判断变更是期望还是缺陷。对于多人协作的组件库团队,建议约定「任何涉及样式的PR必须附上Chasonic审查链接」,把视觉审查固化为合并前的检查项。
迁移后的验证策略与常见坑
迁移完成后,团队整体的测试分层大致是:纯函数逻辑(包括Polished混色计算)用单元测试覆盖,组件交互行为用Testing Library覆盖,视觉呈现用Chromatic覆盖。三层各司其职,不要让视觉测试承担逻辑验证的职责——给表单填错数据再截图,这种测试既慢又脆弱。
几个实际踩过的坑供参考。一是视口数量不要贪多,Chromatic按快照数量计费,移动端和桌面端各留一个典型尺寸通常够用;二是第三方字体加载时机不一致会造成文字区域闪烁性差异,配置--delay让截图等待字体就绪;三是使用了CSS-in-JS方案时注意运行时样式注入顺序,在Storybook中最好与生产构建保持一致的SSR或提取配置,否则测试环境和线上环境可能出现微妙差异。
最后,基线管理要有纪律。大版本改版时接受全部变更前,先在Chromatic中创建新的分支基线,避免主分支的历史快照被污染。基线一旦混乱,视觉回归检测的价值就会大打折扣,恢复成本远高于日常维护的投入。
React样式测试Chromatic视觉回归Polished修改时间:2026-09-10 16:18:47