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
\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 `.map(` in a module must resolve to a binding declared in that module — * a `const `, an `import { }`, 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 = `
\n${calls.map((c) => ` ${c}`).join("\n")}\n
`; 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 }); });