☰
Metabase 可视化回归测试实战:基于 Loki 与 Storybook 的 Visual Tests 完整指南
2026/10/10 20:32:01 网站建设 项目流程

Metabase 可视化回归测试实战:基于 Loki 与 Storybook 的 Visual Tests 完整指南

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

导读

Visual Tests(视觉回归测试)是 Metabase 前端质量保障体系中不可或缺的一环,用于捕捉组件和图表在代码改动后出现的“肉眼可见但单元测试无法发现”的样式回归。Metabase 选用 Loki 为骨架,结合仓库中的 loki.config.js、package.json、.storybook/main.ts、.storybook/preview.tsx 以及 GitHub Actions 工作流,系统讲解本地运行、CI 集成、新增测试与排障全流程,读完即可在 Metabase 仓库中独立开展视觉回归测试。

一、技术栈与运行原理

1.1 为什么是 Loki + Storybook

Metabase 的前端体量巨大(frontend/src/metabase下约 9800 个文件),图表可视化(frontend/src/metabase/visualizations)等模块极易在重构、主题定制或样式调整时产生细微渲染差异。Loki 的设计思路是:以 Storybook 为渲染容器,用真实浏览器引擎截取页面快照,再与历史参考图做像素级 diff。

其核心工作流为:

  1. Storybook 按 story 描述渲染组件(含 mock 数据与全局样式装饰器);
  2. Loki 通过 Chrome(Docker 容器内)逐故事截图,输出到.loki/current;
  3. 与.loki/reference中的 PNG 参考图比对,差异写入.loki/difference;
  4. 比对引擎与容差由 loki.config.js 控制。

1.2 三个关键目录

目录作用
.loki/reference已批准的参考截图(基线),由 CI 或人工确认后维护
.loki/current本次运行新截取的截图
.loki/difference与参考图不一致的差异图,用于人工复核

从 frontend/test/generate-loki-report-json.js 的源码可以看到,报告脚本正是分别读取reference、current、difference三个目录,将其中的差异项写入.loki/report.json,再交给reg-cli渲染成 HTML 报告。

二、本地运行 Visual Tests

2.1 前置条件

在本地运行前,需要同时满足:

  • Storybook 正在运行:Loki 默认向本地 Storybook 实例发起截图请求;
  • Docker 正在运行:Loki 使用chrome.docker作为截图目标,见 loki.config.js 中chrome.laptop配置对target: "chrome.docker"的引用。

2.2 常用命令

仓库在 package.json 中预置了整套 npm scripts,核心命令如下:

# 1. 本地运行视觉测试(开发环境 NODE_ENV=development) bun run test-visual:loki # 2. 以 CI 方式运行:先构建 Storybook 静态站,再对静态站截图 bun run test-visual:loki:ci # 3. 将失败的截图(.loki/difference)复制为新的参考图,更新基线 bun run test-visual:loki-approve-diff # 4. 仅生成 HTML 报告(不重新运行测试) bun run test-visual:loki-report # 5. 先运行测试,若失败则生成并自动打开 HTML 报告 bun run test-visual:loki-report-open

各命令底层实现(来自 package.json):

"test-visual:loki": "NODE_ENV=development loki test --chromeFlags='--headless --disable-gpu'", "test-visual:loki:ci": "bun run build-storybook && bun run test-visual:loki --reactUri file:./storybook-static --verboseRenderer", "test-visual:loki-approve-diff": "ls .loki/difference | xargs -I _ find .loki/current -name _ | xargs -I _ cp _ .loki/reference/", "test-visual:loki-prune": "ls .loki/reference | grep -v \"$(ls .loki/current)\" | xargs -I {} rm .loki/reference/{}", "test-visual:loki-report": "node frontend/test/generate-loki-report-json.js && reg-cli --from .loki/report.json --report .loki/report.html", "test-visual:loki-report-open": "bun run test-visual:loki || (echo 'Visual test failed, opening report...' && bun run test-visual:loki-report && open-cli .loki/report.html)"

要点说明:

  • test-visual:loki使用开发环境构建(NODE_ENV=development),并显式传入--headless --disable-gpu保证无头截图稳定;
  • test-visual:loki:ci是官方推荐的基线生成方式:原文档特别提示,本地运行得到的截图不要直接提交,应使用 CI 变体生成参考图,因为 CI 环境的字体、渲染栈更一致,参考图更稳定;
  • test-visual:loki-approve-diff将差异图对应位置的当前截图覆盖到reference目录,实现“一键批准”;
  • test-visual:loki-prune反向清理:删除参考图中已不存在的多余基线;
  • 报告最终落盘为.loki/report.html,可离线打开审查。

2.3 本地差异审查流程

bun run test-visual:loki-report-open

该命令先跑一轮测试:若全部通过则无事发生;若存在差异,自动生成报告并调用open-cli在浏览器打开.loki/report.html,即可在红绿对比中逐项确认是真实回归还是合理变更。

三、CI 集成:Pull Request 自动触发

3.1 工作流概览

视觉测试在 Pull Request 上自动触发,核心工作流为 .github/workflows/loki.yml:

  1. files-changed阶段:通过dorny/paths-filter依据 .github/file-paths.yaml 判断本次改动是否涉及前端源码、Loki 相关文件(.github/workflows/loki.yml、.loki/**)或前端 CI 基础设施;
  2. visual-test 阶段:仅在满足frontend_ci、frontend_sources或frontend_loki_ci任一条件时执行:
    • 启动 Docker 服务容器(docker:19.03.12,privileged 模式)供 Loki 的 Chrome 使用;
    • 依次准备前端/后端环境、编译 CLJS(NODE_ENV=development bun run build-pure:cljs);
    • 运行bun run test-visual:loki:ci;
    • 失败时生成视觉报告并上传loki-reportartifact(包含.loki/整个目录,含隐藏文件)。

3.2 失败时如何查看差异

当 PR 上出现 "Loki Visual Regression Testing" 检查失败时:

  1. 打开失败 Job 页面;
  2. 进入Summary区域;
  3. 下载loki-report构件;
  4. 解压后打开其中的report.html,逐条比对difference目录中的差异截图。

3.3 如何更新参考图(批准差异)

若差异是有意为之(如设计改版)或偶发不稳定(flake),无需手动下载构件,只需给 PR 打上loki-update标签,CI 便会以当前截图更新参考基线。这是团队推荐的“批量批准”方式,避免人工逐个复制差异图。

3.4 智能裁剪:受影响的 story 才会被跑

值得注意的细节:仓库通过 .github/scripts/create-test-plan.ts 和 .github/scripts/affected-tests.ts 构建“测试计划”。其中 Loki 相关的 story 列表来自frontend/**/*.stories.{js,jsx,ts,tsx}与enterprise/frontend/**/*.stories.{js,jsx,ts,tsx}(见 create-test-plan.ts),再结合依赖图(dependency-cruiser)与改动文件推断本次应执行的 Loki story 子集(loki_stories_to_run),并将结果输出供工作流消费。这意味着 CI 并非每次全量截图,而是基于改动影响面做智能裁剪,从而显著缩短反馈周期。

四、新增 Visual Test:写一个 story 即可

4.1 最小实践

新增视觉测试不需要额外测试代码,本质就是新增 Storybook story。原文档明确指出:当前视觉测试仅用于图表,但任何 story 都可纳入。唯一要求是确保loki.config.js中的storiesFilter覆盖到目标 story。

以仓库真实示例 BarChart.stories.tsx 为模板:

import type { StoryFn } from "@storybook/react"; import { VisualizationWrapper } from "__support__/storybook"; import { NumberColumn, StringColumn } from "__support__/visualizations"; import Visualization from "metabase/visualizations/components/Visualization"; import { registerVisualization } from "metabase/viz-core"; import type { Series } from "metabase-types/api"; import { createMockCard } from "metabase-types/api/mocks"; import { BarChart } from "./BarChart"; export default { title: "viz/BarChart", component: BarChart, }; registerVisualization(BarChart); const MOCK_SERIES = [ { card: createMockCard({ name: "Card", display: "bar" }), data: { cols: [StringColumn({ name: "Dimension" }), NumberColumn({ name: "Count" })], rows: [["foo", 1], ["bar", 2]], }, }, ] as Series; const DefaultTemplate: StoryFn = () => ( <VisualizationWrapper> <Box h={500}> <Visualization rawSeries={MOCK_SERIES} width={500} /> </Box> </VisualizationWrapper> ); export const Default = { render: DefaultTemplate, parameters: { loki: { skip: true }, // 需要纳入 Loki 时移除该参数 }, };

同时,在 loki.config.js 的storiesFilter中加入对应的 story 标题模式(例如"^viz/BarChart"),该字段是一个以|连接的正则表达式数组(代码中通过.join("|")合并),支持前缀锚定与精确匹配。

4.2 按需跳过:loki: { skip: true }

并非所有 story 都适合截图。仓库中BarChart的Default与Watermark两个 story 均设置了parameters.loki.skip = true(见 BarChart.stories.tsx),原因通常是:

  • 图表含动态动画/异步加载,截图不稳定;
  • 依赖用户交互态(hover、滚动);
  • 涉及外部字体、图片等非确定性渲染。

此类 story 通过 Storybook 的parameters.loki字段在渲染侧被 Loki 跳过,无需改动loki.config.js。

4.3 让截图确定性的工程细节

视觉测试最怕“时好时坏”。Metabase 在 Storybook 预览层做了大量确定性保障,见 .storybook/preview.tsx:

  • 去掉人为延迟:window.METABASE_REMOVE_DELAYS = true,跳过 story 中的可跳过 delay;
  • 同步加载 ECharts:注释明确指出 EChartsRenderer 在应用中按需分包加载,若不在 Storybook 中强制同步引入,快照会拍到“懒加载骨架屏闪烁”;
  • 字体预加载:在预览加载时同步注入@font-face,并通过fontsReadyloader 等待所有字体load()完成。注释解释:若不等待,表格列宽自动计算(依赖字体度量)会在不同机器上产生不同结果,导致截图不一致;
  • 同步加载全部可视化组件:通过loadVisualizationComponents()loader 确保图表组件注册完成,避免截图时组件仍处于 Suspense 骨架状态。

而 .storybook/main.ts 则支持环境变量STORYBOOK_STORIES_FILTER(逗号分隔的 story 文件路径),用于只构建指定的 story 文件——这是下方压力测试工作流的核心依赖。

4.4 渲染到图片的场景如何配合 Loki

部分 story 需要把图表导出为图片(如 PDF/PNG 导出场景),仓库提供了openImageBlobOnStorybook工具(frontend/src/metabase/utils/loki-utils.ts):它将导出的 blob 生成<img>挂到#storybook-root,并添加data-testid="image-downloaded"标记,直到图片完全加载后才触发就绪信号,从而保证 Loki 截图时画面上呈现的是完整导出的图片而非空白或半加载状态。该工具被 save-chart-image.ts 与 save-dashboard-pdf.ts 在 Storybook/Loki 环境下复用。

五、合并前必做:Loki Visual Stress Test

5.1 为什么需要压力测试

视觉测试天然受字体、GPU 渲染、时序影响,单次通过不代表稳定。原文档明确要求:合并 PR 前,运行 Loki Visual Stress Test 工作流验证新增测试不 flaky。

5.2 工作流用法

工作流为 .github/workflows/loki-stress-test-flake-fix.yml,支持两种触发方式:

  • PR 自动触发:当 PR 改动frontend/**/*.stories.tsx或enterprise/frontend/**/*.stories.tsx时自动运行(detect-changed-stories会调用 GitHub API 找出本次变更的 story 文件);
  • 手动触发(workflow_dispatch):填写两个输入项——
    • story_files:相对于仓库根目录、逗号分隔的 story 文件路径(必须匹配frontend/或enterprise/frontend/前缀且包含.stories.,否则会校验报错);
    • burn_in:重复运行次数,例如10,默认10。

原文档提到的“运行 50 次”即通过burn_in输入实现。

5.3 工作流内部逻辑

其核心stress-test-lokiJob 展示了官方判定 flake 的标准流程:

  1. 用STORYBOOK_STORIES_FILTER环境变量只构建变更的 story 文件(复用 .storybook/main.ts 的过滤逻辑);
  2. 循环seq 1 $BURN_IN,每轮执行bun run test-visual:loki --reactUri file:./storybook-static --verboseRenderer;
  3. 注意set -o pipefail防止管道吞掉 Loki 的退出码(注释明确说明:tee成功后管道会误报成功);
  4. 单轮失败时把.loki/difference拷入.loki/failures/run-$i留存现场;
  5. 任何一轮失败都会导致整个 Job 失败并输出X out of N runs failed错误;
  6. 特殊情况:若日志中出现No stories were found,说明改动文件里的 story 全部被 Loki 跳过,直接以成功退出。

若压力测试通过率不达标,说明 story 存在渲染不确定性,应回到 4.3 节的确定性保障手段排查(字体、异步、动画、懒加载等),而不是直接放宽容差。

六、配置详解:loki.config.js

.loki.config.js 是 Loki 行为的唯一事实来源,当前仓库配置如下:

module.exports = { diffingEngine: "looks-same", storiesFilter: [ "DataGrid", "static-viz", "viz", "Patterns/Upsells", "^visualizations/shared", "^app/embed", "^design system", "^Components/Overlays/Menu Hover state", "^Components/Overlays/Popover Opened", "^Components/Overlays/Modal Opened", "^Components/Overlays/HoverCard Opened", "^Components/Utils/Paper Shadow matrix", "^Components/Data display/Card Shadow matrix", "^Components/Inputs/Checkbox (Overview|Checkbox\\.Card)$", "^Components/Inputs/DatePicker Dates range", "^Components/Inputs/Radio (Overview|Radio\\.Card)$", "^Components/Inputs/Switch (Overview|Switch\\.Group)$", "^Components/Parameters/DatePicker", "^Components/Buttons/Button Compact size, custom color", "^Components/Overlays/Tooltip", "^Components/Documents", "^Components/Feedback/Alert", "^Components/Feedback/Loader Overview", "^Components/Ask Before Using/Chip Overview", "^Components/Data display/Badge Sizes and variants", "^Components/Navigation/NavLink Overview", "^Components/Data display/KeyboardShortcut Overview", "^Components/Table", "^App/Palette", "^viz/GridMapPdfExport", "ParameterValueWidget", "^Explorations/ExplorationGroupVisualization", ].join("|"), configurations: { "chrome.laptop": { target: "chrome.docker", width: 1366, height: 768, deviceScaleFactor: 1, mobile: false, }, }, "looks-same": { strict: false, antialiasingTolerance: 9, tolerance: 9, }, };

逐项解读:

  • diffingEngine: "looks-same":选用looks-same(Yandex 出品的像素比对库)作为 diff 引擎;
  • storiesFilter:正则数组,^表示以某前缀开头的 story 标题(如^viz/GridMapPdfExport),未加锚的条目(如DataGrid)则按子串/前缀语义匹配。新增测试时修改此数组是最常见的操作;同时注意 .storybook/preview.tsx 的注释:story 名称变更可能影响 Loki 测试,任何重命名都要同步更新storiesFilter;
  • configurations."chrome.laptop":定义截图视口为 1366×768(笔记本分辨率),deviceScaleFactor: 1保证 1:1 像素输出,target: "chrome.docker"说明实际渲染由 Docker 内的 Chrome 完成——这正是本地运行要求 Docker 的原因;
  • looks-same容差:strict: false关闭严格模式;antialiasingTolerance与tolerance均为 9,允许亚像素级的抗锯齿差异存在,从而容忍不同平台的字体渲染差异,避免高频误报。

容差参数的影响

  • tolerance是像素级颜色差异阈值,值越大越宽松。Metabase 设为 9 属于“相对严格但容忍抗锯齿”的折中;
  • antialiasingTolerance单独处理边缘像素的混色差异;
  • 若你的图表频繁出现“时有时无”的失败,优先排查渲染确定性,而不是盲目调大tolerance,否则会漏掉真实回归。

七、常见问题与排障路径

现象排查方向
本地报错要求 Docker检查 Docker daemon 是否运行,Loki 的chrome.dockertarget 依赖 Docker
Storybook 未启动导致截图失败先启动 Storybook dev server,或改用test-visual:loki:ci(自建静态站)
本地通过但 CI 失败本地截图与 CI 渲染环境不一致,按官方建议以 CI 生成的参考图为准,勿提交本地截图
新增 story 未被截图检查loki.config.js的storiesFilter是否覆盖该 story 标题
测试偶发失败(flake)运行压力测试工作流定位;检查字体预加载、动画/延迟、懒加载与异步图表渲染
参考图过期(story 已删)用bun run test-visual:loki-prune清理多余基线
有意改版需更新基线PR 打loki-update标签批量更新,或本地test-visual:loki-approve-diff
修改了 story 名称同步更新 loki.config.js 的storiesFilter,否则截图对不上基线

八、总结

Metabase 的 Visual Tests 体系由三层构成:

  1. 渲染层:Storybook 提供确定性的组件渲染环境(字体预加载、同步图表、去延迟),详见 .storybook/preview.tsx;
  2. 截图与比对层:Loki 借助 Docker 内 Chrome 在 1366×768 视口下截图,由 loki.config.js 的storiesFilter决定覆盖范围、looks-same容差决定敏感度;
  3. CI 与流程层:.github/workflows/loki.yml 在 PR 上自动执行并以loki-report构件交付差异报告,loki-stress-test-flake-fix.yml 通过多轮重复运行(如 50 次burn_in)把 flake 扼杀在合并之前,.github/scripts/create-test-plan.ts 则保证只运行受改动影响的 story,兼顾覆盖与效率。

对开发者而言,日常涉及的三条黄金规则是:本地只做快速验证、参考图一律以 CI 生成为准、合并前跑压力测试。遵循这套流程,即可在保证 Metabase 图表与组件视觉一致性的同时,把误报和 flake 控制在可接受范围内。

关联阅读:docs/developers-guide/visual-tests.md(本文依据)、frontend/test/generate-loki-report-json.js(报告生成实现)、.github/file-paths.yaml(Loki 相关文件变更判定)。

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询