Files
ditto.site/compiler/test/irPrune.test.ts
T
Samraaj BathandClaude Fable 5 9aed2540aa Clone-fidelity fix waves 1-3 (+ wave 4 in flight): capture settling, media, iframes
Fixes from the cropin.com / ooni.com audit reviews:
- walker: preserve whitespace-only text in inline elements ("ofthe" bug);
  capture ::placeholder styles for inputs
- css: emit visibility (hidden-at-rest wordmark artifact); exempt list
  markers from inherited elision (lost bullets); ::placeholder rules
- seo: sanitize og:type to Next's enum (render crash -> dev badge leak);
  generated next.config disables devIndicators
- capture/stabilize: promote lazy-loader data attrs before snapshots
  (collapsed sections, viewport-inconsistent captures); settle autoplay
  carousels to home slide before every snapshot; force-reveal hidden
  videos for poster shots + log failures
- generate: ship downloaded video files as local <video src>; keep
  picture>source through IR prune with srcset rewrite (mobile-crop-on-
  desktop heroes); pin Tailwind named screens to computeBands boundaries
- capture/graft: capture cross-origin iframe subtrees (Klaviyo signup
  forms) with element-screenshot fallback; asset download retry +
  visual-assets-missing reporting

Checkpoint commit: wave 4 (reveal settling, 206 range-response fix,
hidden-geometry banding, bare-zero utility gating) is mid-implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:45:42 -07:00

101 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { RawNode, RawChild } from "../src/capture/walker.js";
import { buildIR, isTextChild, type IRNode } from "../src/normalize/ir.js";
const VPS = [375, 1280];
function raw(tag: string, attrs: Record<string, string> = {}, children: RawChild[] = [], visible = true): RawNode {
return {
tag, attrs,
computed: { display: visible ? "block" : "none", position: "static", visibility: "visible" },
bbox: { x: 0, y: 0, width: visible ? 640 : 0, height: visible ? 360 : 0 },
visible,
children,
};
}
/** Minimal PageSnapshot JSON around a body tree (same tree at every viewport). */
function snapshot(vp: number, root: RawNode): object {
return {
doc: {
url: "https://example.test/page", title: "Fixture",
head: { description: "", canonical: "", ogTitle: "", ogDescription: "", ogImage: "", ogType: "", ogSiteName: "", twitterCard: "", themeColor: "" },
lang: "en", charset: "UTF-8", viewportWidth: vp, viewportHeight: 800,
scrollWidth: vp, scrollHeight: 800, htmlBg: "rgb(255, 255, 255)", bodyBg: "rgb(255, 255, 255)",
bodyColor: "rgb(0, 0, 0)", bodyFont: "Arial", metaViewport: "width=device-width, initial-scale=1",
nodeCount: 10, truncated: false,
},
root, cssVars: {}, fontFaces: [], cssUrls: [], domAssets: [], keyframes: [],
};
}
function buildFixtureIR(root: RawNode): IRNode {
const sourceDir = mkdtempSync(join(tmpdir(), "ditto-ir-prune-"));
mkdirSync(join(sourceDir, "capture"), { recursive: true });
for (const vp of VPS) {
writeFileSync(join(sourceDir, "capture", `dom-${vp}.json`), JSON.stringify(snapshot(vp, structuredClone(root))));
}
return buildIR(sourceDir, VPS).root;
}
function findByTag(node: IRNode, tag: string): IRNode | null {
if (node.tag === tag) return node;
for (const c of node.children) {
if (isTextChild(c)) continue;
const hit = findByTag(c, tag);
if (hit) return hit;
}
return null;
}
describe("IR prune keeps <source> media candidates", () => {
it("keeps invisible source children of <picture> and <video>, still pruning other invisibles", () => {
// <source> is 0×0 at EVERY viewport (never painted), which the visibility prune
// reads as unobserved — real case (ooni.com): 47 <source> in dom-1280.json, 0 in
// ir.json, so the mobile img.src got baked into desktop layouts.
const picture = raw("picture", {}, [
raw("source", { srcset: "/img/hero-1280.jpg 1x", media: "(min-width: 768px)" }, [], false),
raw("img", { src: "/img/hero-375.jpg", alt: "hero" }),
]);
const video = raw("video", { autoplay: "" }, [
raw("source", { src: "/media/hero.mp4", type: "video/mp4" }, [], false),
]);
const noise = raw("div", {}, [raw("span", {}, [], false)], false); // invisible everywhere → pruned
const body = raw("body", {}, [picture, video, noise]);
const root = buildFixtureIR(body);
const pic = findByTag(root, "picture");
assert.ok(pic, "picture survives");
const picTags = pic!.children.filter((c) => !isTextChild(c)).map((c) => (c as IRNode).tag);
assert.deepEqual(picTags, ["source", "img"]);
const src = findByTag(pic!, "source")!;
assert.equal(src.attrs.srcset, "/img/hero-1280.jpg 1x");
assert.equal(src.attrs.media, "(min-width: 768px)");
const vid = findByTag(root, "video");
assert.ok(vid, "video survives");
const vidTags = vid!.children.filter((c) => !isTextChild(c)).map((c) => (c as IRNode).tag);
assert.deepEqual(vidTags, ["source"]);
// The carve-out is scoped: unrelated invisible-everywhere subtrees still prune.
assert.equal(findByTag(root, "span"), null);
});
it("does not keep a <source> outside picture/video, nor resurrect a pruned picture", () => {
const orphan = raw("div", {}, [raw("source", { srcset: "/x.jpg" }, [], false)]);
// A picture invisible everywhere with no visible descendants is still pruned whole.
const ghost = raw("picture", {}, [raw("source", { srcset: "/y.jpg" }, [], false)], false);
const body = raw("body", {}, [orphan, ghost]);
const root = buildFixtureIR(body);
assert.equal(findByTag(root, "source"), null);
assert.equal(findByTag(root, "picture"), null);
});
});