visual-regression-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Visual Regression Testing

视觉回归测试

Two render paths must produce identical pixels. The native canvas path (
renderToImageNative
via
drawElementImage
) renders the live DOM directly through Blink and is the ground truth. The foreignObject path (
captureTimelineToDataUri
) serializes the DOM to XHTML inside an SVG
<foreignObject>
. Any difference between them is a serializer bug.
两条渲染路径必须生成完全相同的像素。原生canvas路径(通过
drawElementImage
实现的
renderToImageNative
)直接通过Blink渲染实时DOM,是基准参照。foreignObject路径
captureTimelineToDataUri
)将DOM序列化为SVG
<foreignObject>
内的XHTML。两者之间的任何差异都是序列化器的bug。

Utility

工具类

elements/packages/elements/test/visualRegressionUtils.ts
provides the testing API. Key functions:
typescript
// Compare two canvases directly — returns diffPercentage, no baseline file needed
compareTwoCanvases(canvas1, canvas2, testName, comparisonName, options)
expectCanvasesToMatch(canvas1, canvas2, testName, comparisonName, options)

// Compare a canvas against a stored baseline PNG
assertCanvasSnapshot(canvas, testName, snapshotName, options)
expectCanvasToMatchSnapshot(source, testName, snapshotName, options)
options.acceptableDiffPercentage
(default
1.0
) controls pass/fail. Use
0
to
0.5
for tight parity checks.
elements/packages/elements/test/visualRegressionUtils.ts
提供了测试API。核心函数:
typescript
// 直接对比两个canvas —— 返回差异百分比,无需基准文件
compareTwoCanvases(canvas1, canvas2, testName, comparisonName, options)
expectCanvasesToMatch(canvas1, canvas2, testName, comparisonName, options)

// 将canvas与存储的基准PNG对比
assertCanvasSnapshot(canvas, testName, snapshotName, options)
expectCanvasToMatchSnapshot(source, testName, snapshotName, options)
options.acceptableDiffPercentage
(默认值
1.0
)控制测试通过/失败的阈值。严格的一致性检查可使用
0
0.5
之间的值。

Standard Comparison Pattern

标准对比模式

typescript
import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
import { loadImageFromDataUri } from "./rendering/loadImage.js";
import { renderToImageNative } from "./rendering/renderToImageNative.js";
import { isNativeCanvasApiAvailable } from "./previewSettings.js";
import { expectCanvasesToMatch } from "../../test/visualRegressionUtils.js";

async function captureForComparison(tg: EFTimegroup, W: number, H: number) {
  // foreignObject path
  const dataUri = await captureTimelineToDataUri(tg, W, H, { canvasScale: 1, timeMs: 0 });
  const img = await loadImageFromDataUri(dataUri);
  const fc = document.createElement("canvas");
  fc.width = W; fc.height = H;
  fc.getContext("2d")!.drawImage(img, 0, 0);

  // native path (ground truth — Blink renders the live DOM)
  const nc = await renderToImageNative(tg, W, H, { skipDprScaling: true });

  return { foreignCanvas: fc, nativeCanvas: nc };
}
Always guard native-path tests with
isNativeCanvasApiAvailable()
— the WICG
drawElementImage
API is Chromium-only.
typescript
import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
import { loadImageFromDataUri } from "./rendering/loadImage.js";
import { renderToImageNative } from "./rendering/renderToImageNative.js";
import { isNativeCanvasApiAvailable } from "./previewSettings.js";
import { expectCanvasesToMatch } from "../../test/visualRegressionUtils.js";

async function captureForComparison(tg: EFTimegroup, W: number, H: number) {
  // foreignObject路径
  const dataUri = await captureTimelineToDataUri(tg, W, H, { canvasScale: 1, timeMs: 0 });
  const img = await loadImageFromDataUri(dataUri);
  const fc = document.createElement("canvas");
  fc.width = W; fc.height = H;
  fc.getContext("2d")!.drawImage(img, 0, 0);

  // 原生路径(基准参照 —— Blink渲染实时DOM)
  const nc = await renderToImageNative(tg, W, H, { skipDprScaling: true });

  return { foreignCanvas: fc, nativeCanvas: nc };
}
始终使用
isNativeCanvasApiAvailable()
来保护原生路径测试——WICG的
drawElementImage
API仅支持Chromium浏览器。

Writing a Minimal Reproduction Test

编写最小复现测试

A minimal reproduction isolates a single CSS property. The test should fail before the fix and pass after.
typescript
it("text-shadow from inline style", async () => {
  if (!isNativeCanvasApiAvailable()) return;
  const tg = document.createElement("ef-timegroup") as EFTimegroup;
  tg.style.cssText = "width:400px;height:200px;background:#000;position:relative;";
  const el = document.createElement("div");
  el.style.cssText = "position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);"
    + "font-size:60px;font-weight:900;color:white;text-shadow:0 0 40px red;";
  el.textContent = "GLOW";
  tg.appendChild(el);
  document.body.appendChild(tg);
  await tg.updateComplete;

  const { foreignCanvas, nativeCanvas } = await captureForComparison(tg, 400, 200);
  await expectCanvasesToMatch(foreignCanvas, nativeCanvas, "canvasDomParity", "text-shadow-inline", {
    acceptableDiffPercentage: 0.5,
  });
  tg.remove();
});
最小复现测试需隔离单个CSS属性。测试应在修复前失败,修复后通过
typescript
it("text-shadow from inline style", async () => {
  if (!isNativeCanvasApiAvailable()) return;
  const tg = document.createElement("ef-timegroup") as EFTimegroup;
  tg.style.cssText = "width:400px;height:200px;background:#000;position:relative;";
  const el = document.createElement("div");
  el.style.cssText = "position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);"
    + "font-size:60px;font-weight:900;color:white;text-shadow:0 0 40px red;";
  el.textContent = "GLOW";
  tg.appendChild(el);
  document.body.appendChild(tg);
  await tg.updateComplete;

  const { foreignCanvas, nativeCanvas } = await captureForComparison(tg, 400, 200);
  await expectCanvasesToMatch(foreignCanvas, nativeCanvas, "canvasDomParity", "text-shadow-inline", {
    acceptableDiffPercentage: 0.5,
  });
  tg.remove();
});

Snapshot Files

快照文件

Diff PNGs are written to
elements/test-assets/test/__snapshots__/<testName>/
. Open them to visually inspect which pixels differ.
差异PNG会被写入
elements/test-assets/test/__snapshots__/<testName>/
目录。打开这些文件可直观查看哪些像素存在差异。

Serializer Properties

序列化器属性

The foreignObject serializer captures computed styles from
SERIALIZED_STYLE_PROPERTIES
in
elements/packages/elements/src/preview/rendering/serializeTimelineDirect.ts
. Missing a property there means it is lost when animations are frozen with
animation:none
. Check this list first when a CSS feature produces a parity gap.
foreignObject序列化器从
elements/packages/elements/src/preview/rendering/serializeTimelineDirect.ts
中的
SERIALIZED_STYLE_PROPERTIES
获取计算样式。如果某个属性未在该列表中,那么当动画被
animation:none
冻结时,该属性会丢失。当CSS特性出现一致性差异时,首先检查这个列表。

When to Use This Skill

何时使用该技能

  • Writing new parity tests for a CSS feature gap
  • Investigating a report that the preview thumbnail differs from what the DOM shows
  • After any change to
    serializeTimelineDirect.ts
    to verify no new regression
  • 为CSS特性差异编写新的一致性测试
  • 排查预览缩略图与DOM显示内容不一致的问题
  • 修改
    serializeTimelineDirect.ts
    后,验证是否引入新的回归问题