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>
103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
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 { generateCss } from "../src/generate/css.js";
|
|
|
|
const VPS = [375, 1280];
|
|
const CANONICAL = 1280;
|
|
|
|
/** Computed style with the minimum the emitter reads, per viewport. */
|
|
function computed(over: StyleMap = {}): StyleMap {
|
|
return { display: "block", position: "static", visibility: "visible", listStyleType: "disc", listStylePosition: "outside", ...over };
|
|
}
|
|
|
|
function node(id: string, tag: string, cs: StyleMap, children: IRChild[] = [], visible = true): IRNode {
|
|
const computedByVp: Record<number, StyleMap> = {};
|
|
const bboxByVp: Record<number, BBox> = {};
|
|
const visibleByVp: Record<number, boolean> = {};
|
|
for (const vp of VPS) {
|
|
computedByVp[vp] = { ...cs };
|
|
bboxByVp[vp] = { x: 0, y: 0, width: vp, height: 100 };
|
|
visibleByVp[vp] = visible;
|
|
}
|
|
return { id, tag, attrs: {}, visibleByVp, bboxByVp, computedByVp, children };
|
|
}
|
|
|
|
function irWith(root: IRNode): IR {
|
|
return {
|
|
doc: {
|
|
sourceUrl: "https://example.test/css",
|
|
title: "CSS Fixture",
|
|
lang: "en",
|
|
charset: "UTF-8",
|
|
metaViewport: "width=device-width, initial-scale=1",
|
|
viewports: VPS,
|
|
sampleViewports: VPS,
|
|
canonicalViewport: CANONICAL,
|
|
perViewport: Object.fromEntries(VPS.map((vp) => [vp, { scrollHeight: 800, scrollWidth: vp, htmlBg: "rgb(255, 255, 255)", bodyBg: "rgb(255, 255, 255)", bodyColor: "rgb(0, 0, 0)", bodyFont: "Arial" }])),
|
|
nodeCount: 4,
|
|
keyframes: [],
|
|
},
|
|
root,
|
|
};
|
|
}
|
|
|
|
/** The base-rule body for a selector (first non-banded `.c<id>{…}` block). */
|
|
function baseRule(css: string, id: string): string {
|
|
const m = css.match(new RegExp(`\\.c${id}\\{([^}]*)\\}`));
|
|
return m?.[1] ?? "";
|
|
}
|
|
|
|
describe("generateCss list markers", () => {
|
|
it("re-establishes list-style on a <ul> whose disc equals the parent's initial value", () => {
|
|
// list-style-type's initial value is `disc` on EVERY element, so a real <ul disc>
|
|
// equals its parent <div>'s computed value — but the reset (`ul, ol, menu
|
|
// { list-style: none; }`) breaks the inheritance chain, so it must still emit.
|
|
const li = node("n2", "li", computed({ display: "list-item" }));
|
|
const ul = node("n1", "ul", computed(), [li]);
|
|
const root = node("n0", "body", computed(), [ul]);
|
|
const css = generateCss(irWith(root), new Map());
|
|
assert.ok(baseRule(css, "n1").includes("list-style-type:disc"));
|
|
// The <li> inherits from the ul (not reset), so parent-equality elision still applies.
|
|
assert.ok(!baseRule(css, "n2").includes("list-style-type"));
|
|
});
|
|
|
|
it("does not emit list-style-type on non-list tags at the initial disc", () => {
|
|
const div = node("n1", "div", computed());
|
|
const root = node("n0", "body", computed(), [div]);
|
|
const css = generateCss(irWith(root), new Map());
|
|
assert.ok(!baseRule(css, "n1").includes("list-style-type"));
|
|
});
|
|
});
|
|
|
|
describe("generateCss visibility", () => {
|
|
it("emits visibility:hidden for a node hidden at the canonical viewport", () => {
|
|
const hidden = node("n1", "div", computed({ visibility: "hidden" }), [], false);
|
|
const shown = node("n2", "div", computed());
|
|
const root = node("n0", "body", computed(), [hidden, shown]);
|
|
const css = generateCss(irWith(root), new Map());
|
|
assert.ok(baseRule(css, "n1").includes("visibility:hidden"));
|
|
assert.ok(!baseRule(css, "n2").includes("visibility"));
|
|
});
|
|
|
|
it("restores inherited visibility at a band where the node is shown", () => {
|
|
const n = node("n1", "div", computed({ visibility: "hidden" }), [], false);
|
|
n.computedByVp[375]!.visibility = "visible";
|
|
n.visibleByVp[375] = true;
|
|
const root = node("n0", "body", computed(), [n]);
|
|
const css = generateCss(irWith(root), new Map());
|
|
assert.ok(baseRule(css, "n1").includes("visibility:hidden"));
|
|
const band = css.match(/@media \(max-width: \d+px\) \{\n([\s\S]*?)\n\}/);
|
|
assert.ok(band?.[1]?.includes("visibility:inherit"));
|
|
});
|
|
|
|
it("stays silent on the descendants of a hidden subtree (inheritance covers them)", () => {
|
|
const child = node("n2", "div", computed({ visibility: "hidden" }), [], false);
|
|
const parent = node("n1", "div", computed({ visibility: "hidden" }), [child], false);
|
|
const root = node("n0", "body", computed(), [parent]);
|
|
const css = generateCss(irWith(root), new Map());
|
|
assert.ok(baseRule(css, "n1").includes("visibility:hidden"));
|
|
assert.ok(!baseRule(css, "n2").includes("visibility"));
|
|
});
|
|
});
|