Canvas raster fallback, CDP rest-state screenshots, lottie overlay containment

- Visible canvases (>=48px) are captured as PNG stills at capture time
  (toDataURL, element-screenshot fallback for tainted/webgl) and emitted as
  cid-carrying <img> elements filling the captured box, so canvas-driven
  bands no longer render blank
- Source AND clone full-page screenshots now capture via CDP
  Page.captureScreenshot with captureBeyondViewport: no scroll-stitching,
  no scroll events, so scroll-linked animations can't smear mid-scrub
  states into perceptual evidence (pixel-identical to the old path on
  static pages; Playwright fallback retained)
- DittoLottie's runtime overlay stays position:absolute/inset:0 for its
  whole life and the host carries data-ditto-lottie with a scoped
  svg/canvas fit rule, so the runtime animation always fills the pinned
  per-viewport box instead of escaping to aspect height

259 tests pass (12 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-04 00:36:08 -07:00
co-authored by Claude Fable 5
parent e33354cfb5
commit 15d1e7a9e3
9 changed files with 426 additions and 8 deletions
+27 -1
View File
@@ -2,7 +2,7 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import type { IR, IRNode, IRChild, StyleMap, BBox } from "../src/normalize/ir.js";
import type { RawSizing } from "../src/capture/walker.js";
import { generateCss, collectNodeRules } from "../src/generate/css.js";
import { generateCss, collectNodeRules, RESET_CSS } from "../src/generate/css.js";
const VPS = [375, 1280];
const CANONICAL = 1280;
@@ -618,6 +618,32 @@ describe("collectNodeRules lottie mount height pin", () => {
});
});
// The reset ships a rule that forces the lottie runtime svg/canvas to fit its overlay box. The
// player re-mounts its media into an absolute overlay that fills the host's pinned (per-viewport)
// height; without this rule an aspect-mismatched viewBox (a portrait animation in a shorter,
// letterboxed source box) inflates past that height. Scoped to the runtime-marked host only, so it
// is inert when a page has no lottie.
describe("RESET_CSS lottie runtime-fit rule", () => {
it("constrains the runtime svg/canvas to the overlay box, scoped to the marked host", () => {
assert.match(RESET_CSS, /\[data-ditto-lottie\]\s*>\s*div\s*>\s*svg/);
assert.match(RESET_CSS, /\[data-ditto-lottie\]\s*>\s*div\s*>\s*canvas/);
// the rule must pin both dimensions so height:100% resolves against the definite overlay
const m = RESET_CSS.match(/\[data-ditto-lottie\][^{]*\{([^}]*)\}/);
assert.ok(m, "the data-ditto-lottie rule must be present");
assert.match(m![1], /width:\s*100%/);
assert.match(m![1], /height:\s*100%/);
});
it("does not affect the pre-swap placeholder (a direct-child svg, not nested under a div)", () => {
// The selector targets `> div > svg` (the runtime overlay), never a `> svg` placeholder, so the
// captured placeholder keeps its own pinned classes and pre/post-swap geometry stays identical.
assert.ok(
!/\[data-ditto-lottie\]\s*>\s*svg\b/.test(RESET_CSS),
"the rule must not target a direct-child placeholder svg",
);
});
});
// Defect E — a content-sized flex ROW's items are dropped to `width:auto` only when NO shrink fired
// (positive slack ⇒ items at content size). But a WRAPPING text leaf paints at its balanced-wrap
// width, which is BELOW its max-content: `width:auto` on the block child fills the line to