visual-regression-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVisual Regression Testing
视觉回归测试
Two render paths must produce identical pixels. The native canvas path ( via ) renders the live DOM directly through Blink and is the ground truth. The foreignObject path () serializes the DOM to XHTML inside an SVG . Any difference between them is a serializer bug.
renderToImageNativedrawElementImagecaptureTimelineToDataUri<foreignObject>两条渲染路径必须生成完全相同的像素。原生canvas路径(通过实现的)直接通过Blink渲染实时DOM,是基准参照。foreignObject路径()将DOM序列化为SVG 内的XHTML。两者之间的任何差异都是序列化器的bug。
drawElementImagerenderToImageNativecaptureTimelineToDataUri<foreignObject>Utility
工具类
elements/packages/elements/test/visualRegressionUtils.tstypescript
// 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.acceptableDiffPercentage1.000.5elements/packages/elements/test/visualRegressionUtils.tstypescript
// 直接对比两个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.acceptableDiffPercentage1.000.5Standard 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 — the WICG API is Chromium-only.
isNativeCanvasApiAvailable()drawElementImagetypescript
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 };
}始终使用来保护原生路径测试——WICG的API仅支持Chromium浏览器。
isNativeCanvasApiAvailable()drawElementImageWriting 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 . Open them to visually inspect which pixels differ.
elements/test-assets/test/__snapshots__/<testName>/差异PNG会被写入目录。打开这些文件可直观查看哪些像素存在差异。
elements/test-assets/test/__snapshots__/<testName>/Serializer Properties
序列化器属性
The foreignObject serializer captures computed styles from in . Missing a property there means it is lost when animations are frozen with . Check this list first when a CSS feature produces a parity gap.
SERIALIZED_STYLE_PROPERTIESelements/packages/elements/src/preview/rendering/serializeTimelineDirect.tsanimation:noneforeignObject序列化器从中的获取计算样式。如果某个属性未在该列表中,那么当动画被冻结时,该属性会丢失。当CSS特性出现一致性差异时,首先检查这个列表。
elements/packages/elements/src/preview/rendering/serializeTimelineDirect.tsSERIALIZED_STYLE_PROPERTIESanimation:noneWhen 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 to verify no new regression
serializeTimelineDirect.ts
- 为CSS特性差异编写新的一致性测试
- 排查预览缩略图与DOM显示内容不一致的问题
- 修改后,验证是否引入新的回归问题
serializeTimelineDirect.ts