Round-3 fixes from gate validation + human visual review of fresh cropin.com and ooni.com clones: - app.ts: section data-var rename used an unanchored substring replace, so Tile2_data matched inside MediaTile2_data and emitted a corrupted Mediatile2Data usage (ReferenceError at prerender). Word-boundary regex; every map site now shares one derivation. - graft.ts: about:blank iframes were blanket-skipped, missing the Klaviyo newsletter form (mounted into a blank same-origin frame via JS) — the audit's #1 complaint. Blank frames now graft; the visibility gate still excludes 0-size tracking pixels. - walker.ts: isVisible() now rejects boxes wholly outside the viewport (off-left always; off-right only when the page isn't horizontally scrollable; fixed boxes fully above/below), so a closed slide-in drawer's contents no longer count as expected text (91.8% -> 93.4%). - css.ts: four sizing-regime corrections — pin captured px for circular shrink-0 slides (Splide slide chain collapsed 0x0); flex-basis:100% for full-width shrink-0 slides; keep fixed-px grid templates for scrolling track lists (repeat(N,1fr) squished a 50-track carousel); single full-bleed fixed track -> minmax(0,1fr); keep authored heights whose in-flow children are fill children (aspect-video heroes inflated 240 -> 720px); width:100% for inset-spanned aspect boxes. Gate scores: ooni home 88.7 -> 93.3 (responsive 32 fails -> pass), pizza-ovens 90.8 -> 97.6 (perceptual 46.7% -> 17.2%). 107/107 compiler tests, all workspaces green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
5.0 KiB
TypeScript
98 lines
5.0 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { sectionFiles, type ComponentRegistry, type SectionRegistry } from "../src/generate/app.js";
|
|
import type { SectionPlan } from "../src/generate/sectionSplit.js";
|
|
|
|
// Regression: the data-var → camelCase-prop rewrite in a section module must be
|
|
// word-boundary aware. A plain substring replace of `${v}.map(` also matched inside a
|
|
// LONGER var that shares a suffix — e.g. `Tile2_data` is a suffix of `MediaTile2_data`, so
|
|
// rewriting `Tile2_data.map(` corrupted `MediaTile2_data.map(` into `Mediatile2Data.map(`
|
|
// (lowercase 't'), which no longer matched the `MediaTile2_data` declaration/import → the
|
|
// generated app failed `next build` with `ReferenceError: Mediatile2Data is not defined`.
|
|
//
|
|
// The invariant these tests lock: every `X.map(` identifier a section emits has a matching
|
|
// declaration/import/param in the SAME module (single canonical derivation, all sites).
|
|
|
|
function emptyReg(): ComponentRegistry {
|
|
return {
|
|
plan: { clusters: [] } as unknown as ComponentRegistry["plan"],
|
|
nodeByCid: new Map(),
|
|
funcDefs: new Map(),
|
|
skeletonToName: new Map(),
|
|
nameCounts: new Map(),
|
|
dataDecls: [],
|
|
cidDecls: [],
|
|
styleDecls: [],
|
|
dataCounts: new Map(),
|
|
fieldTypes: new Map(),
|
|
styleFieldTypes: new Map(),
|
|
byName: new Map(),
|
|
failed: new Set(),
|
|
};
|
|
}
|
|
|
|
/** Register a shared-skeleton extracted component `name` with one data run, and return the
|
|
* `{Name_data.map(...)}` call the generator emits inline (mirrors registerComponent). */
|
|
function addComponent(reg: ComponentRegistry, name: string): string {
|
|
reg.funcDefs.set(name, `function ${name}({ d, cids, styles }: { d: ${name}Data; cids: string[]; styles: ${name}Styles }) {\n return (\n <div />\n );\n}`);
|
|
const dataVar = `${name}_data`;
|
|
const cidsVar = `${name}_cids`;
|
|
const stylesVar = `${name}_styles`;
|
|
reg.dataDecls.push({ varName: dataVar, compName: name, body: `[\n { alt: "x" }\n]` });
|
|
reg.cidDecls.push({ varName: cidsVar, body: `[\n ["a"]\n]` });
|
|
reg.styleDecls.push({ varName: stylesVar, compName: name, body: `[\n {}\n]` });
|
|
reg.fieldTypes.set(name, [{ name: "alt", type: "string", optional: false }]);
|
|
reg.byName.set(name, { runs: 1, instances: 1, cids: [`${name}-cid`] });
|
|
return `{${dataVar}.map((d, i) => <${name} key={i} d={d} cids={${cidsVar}[i]} styles={${stylesVar}[i]} />)}`;
|
|
}
|
|
|
|
/** Every `<ident>.map(` in a module must resolve to a binding declared in that module —
|
|
* a `const <ident>`, an `import { <ident> }`, a default import, or a function param. */
|
|
function assertMapIdentsResolved(module: string): void {
|
|
const mapIdents = [...module.matchAll(/([A-Za-z_$][\w$]*)\.map\(/g)].map((m) => m[1]!);
|
|
for (const id of mapIdents) {
|
|
const declared =
|
|
new RegExp(`\\bconst\\s+${id}\\b`).test(module) ||
|
|
new RegExp(`\\bimport\\b[^\\n]*\\b${id}\\b`).test(module) ||
|
|
// destructured function param default: `{ ... ${id} = ... }`
|
|
new RegExp(`[{,]\\s*${id}\\s*=`).test(module);
|
|
assert.ok(declared, `map identifier ${id} has no matching declaration/import/param in module:\n${module}`);
|
|
}
|
|
}
|
|
|
|
function buildHeroModule(componentNames: string[]): string {
|
|
const reg = emptyReg();
|
|
const calls = componentNames.map((n) => addComponent(reg, n));
|
|
// A section body that renders each component's `.map(` call, in order.
|
|
const jsx = ` <div>\n${calls.map((c) => ` ${c}`).join("\n")}\n </div>`;
|
|
const plan: SectionPlan = { roots: new Map([["hero-cid", "HeroSection"]]) };
|
|
const sreg: SectionRegistry = { plan, modules: new Map([["HeroSection", jsx]]), order: ["HeroSection"] };
|
|
const out = sectionFiles(sreg, reg);
|
|
return out.files.find((f) => f.name === "HeroSection")!.module;
|
|
}
|
|
|
|
describe("section data identifier derivation (digit-suffixed multi-word names)", () => {
|
|
it("does not corrupt MediaTile2 when a shorter Tile2 shares its suffix", () => {
|
|
// The exact bug shape: Tile2_data is a suffix of MediaTile2_data.
|
|
const module = buildHeroModule(["Tile", "Tile2", "MediaTile2"]);
|
|
// The map site and the param must agree on ONE identifier for MediaTile2's data.
|
|
assert.match(module, /mediaTile2Data\.map\(/);
|
|
assert.doesNotMatch(module, /Mediatile2Data/); // the corrupted (lowercase-t) form must never appear
|
|
assertMapIdentsResolved(module);
|
|
});
|
|
|
|
it("keeps a plain name and a Logo2-style digit name self-consistent", () => {
|
|
const module = buildHeroModule(["Logo", "Logo2"]);
|
|
// Logo2_data has Logo_data-ish neighbors; both map sites must stay resolvable.
|
|
assert.match(module, /logo2Data\.map\(/);
|
|
assert.doesNotMatch(module, /Logo2Data\.map\(/); // no PascalCase corruption of the usage
|
|
assertMapIdentsResolved(module);
|
|
});
|
|
|
|
it("every map identifier resolves for a suffix-overlapping cluster", () => {
|
|
const module = buildHeroModule(["Card", "MediaCard", "Card2", "MediaCard2"]);
|
|
assertMapIdentsResolved(module);
|
|
assert.doesNotMatch(module, /Mediacard/); // no lowercase-c corruption
|
|
});
|
|
});
|