visual-regression-testing
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.
Utility
elements/packages/elements/test/visualRegressionUtils.ts provides the testing API. Key functions:
// 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.
Standard Comparison Pattern
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.
Writing a Minimal Reproduction Test
A minimal reproduction isolates a single CSS property. The test should fail before the fix and pass after.
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.
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.
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.tsto verify no new regression
More from editframe/skills
video-analysis
Analyze video files using ffprobe, mp4dump, and jq. Use when investigating video samples, keyframes, MP4 box structure, codec info, packet timing, or debugging video playback issues.
74visual-thinking
Create visual analogies by mapping relational structure from familiar domains onto unfamiliar concepts using spatial relationships to make abstract patterns concrete. Covers static diagrams AND animated video storytelling (camera choreography, race comparisons, pacing). Use when explaining complex concepts, creating analogies, designing diagrams, creating explainer animations, or revealing system structure.
70css-animations
CSS animation fill-mode requirements for Editframe timeline system. Use when creating CSS animations, debugging flashing/flickering issues, or when user mentions animation problems, fade effects, slide effects, or sequential animations.
68threejs-compositions
Integrate Three.js 3D scenes into Editframe compositions via addFrameTask. Scenes are pure functions of time, fully scrubable, and renderable to MP4. Use when creating 3D animations, WebGL content in compositions, or integrating Three.js with Editframe's timeline system.
65editor-gui
Build video editing interfaces using Editframe's GUI web components. Assemble timeline, scrubber, filmstrip, preview, and playback controls like lego bricks. Use when creating video editors, editing tools, or when user mentions timeline, scrubber, preview, playback controls, trim handles, or wants to build editing UIs.
63elements-new-package
Create a new @editframe/* workspace package in the elements monorepo and publish it to npm.
63