/** * Tests for SSR mode transformation. */ import { describe, expect, it } from "vitest"; import { generateSSRRender, generateStateObject, isSSRImport, SSR_IMPORTS, } from "./implementation"; import { transform } from "../transform/implementation"; // --------------------------------------------------------------------------- // Minimal ESTree node types used in tests // --------------------------------------------------------------------------- interface ESTNode { type: string; [key: string]: unknown; } interface CallExpression extends ESTNode { type: "CallExpression"; callee: ESTNode; arguments: ESTNode[]; } interface NewExpression extends ESTNode { type: "NewExpression"; callee: ESTNode; arguments: ESTNode[]; } interface ArrayExpression extends ESTNode { type: "ArrayExpression"; elements: (ESTNode | null)[]; } interface ObjectExpression extends ESTNode { type: "ObjectExpression"; properties: ESTNode[]; } interface Property extends ESTNode { type: "Property"; key: ESTNode; value: ESTNode; } interface Identifier extends ESTNode { type: "Identifier"; name: string; } interface Literal extends ESTNode { type: "Literal"; value: string | number | boolean | null; raw: string; } describe("SSR Mode Transformation", () => { it("emits a static string for pure static SSR html", () => { const code = `const element =
Hello
;`; const result = transform(code, { mode: "ssr" }); expect(result.code).toContain('"
Hello
"'); expect(result.code).not.toContain("renderToString"); }); it("generates different output for SSR vs CSR", () => { const code = `const element =
Hello
;`; const csrResult = transform(code, { mode: "csr" }); const ssrResult = transform(code, { mode: "ssr" }); // CSR should use fromTree expect(csrResult.code).toContain("fromTree"); // SSR should emit string output and avoid DOM helpers expect(ssrResult.code).not.toContain("fromTree"); expect(ssrResult.code).toContain("
Hello
"); }); it("handles dynamic text in SSR mode", () => { const code = ` const name = "World"; const element =
Hello {name}
; `; const result = transform(code, { mode: "ssr" }); expect(result.code).toContain("renderDynamicText"); expect(result.code).toContain(""); }); it("inserts SSR markers for dynamic text content", () => { const code = `const element =
{name}
;`; const result = transform(code, { mode: "ssr" }); expect(result.code).toContain(""); }); it("handles attributes in SSR mode", () => { const code = `const element =
Content
;`; const result = transform(code, { mode: "ssr" }); expect(result.code).not.toContain("fromTree"); expect(result.code).toContain("container"); }); it("handles nested elements in SSR mode", () => { const code = ` const element = (
Nested

Paragraph

); `; const result = transform(code, { mode: "ssr" }); expect(result.code).not.toContain("fromTree"); expect(result.code).toContain("div"); expect(result.code).toContain("span"); expect(result.code).toContain("p"); }); it("defaults to CSR mode when not specified", () => { const code = `const element =
Hello
;`; const result = transform(code, {}); expect(result.code).toContain("fromTree"); expect(result.code).not.toContain("renderToString"); }); it("handles Fragment in SSR mode", () => { const code = ` const element = ( <>
First
Second ); `; const result = transform(code, { mode: "ssr" }); expect(result.code).not.toContain("fromTree"); expect(result.code).toContain("div"); expect(result.code).toContain("span"); }); }); describe("isSSRImport", () => { it("returns true for all SSR-specific import names", () => { for (const name of SSR_IMPORTS) { expect(isSSRImport(name)).toBe(true); } }); it("returns true for renderToString", () => { expect(isSSRImport("renderToString")).toBe(true); }); it("returns true for renderTree", () => { expect(isSSRImport("renderTree")).toBe(true); }); it("returns true for serializeState", () => { expect(isSSRImport("serializeState")).toBe(true); }); it("returns true for createMarker", () => { expect(isSSRImport("createMarker")).toBe(true); }); it("returns true for MarkerType", () => { expect(isSSRImport("MarkerType")).toBe(true); }); it("returns false for non-SSR import names", () => { const nonSsrImports = [ "fromTree", "setText", "setAttr", "event", "insert", "templateEffect", ]; for (const name of nonSsrImports) { expect(isSSRImport(name)).toBe(false); } }); it("returns false for empty string", () => { expect(isSSRImport("")).toBe(false); }); }); describe("generateSSRRender", () => { it("generates renderToString call with empty Map when no dynamic values", () => { const tree: ESTNode = { type: "ArrayExpression", elements: [ { type: "Literal", value: "div", raw: '"div"' }, { type: "Literal", value: null, raw: "null" }, ], }; const result = generateSSRRender(tree, [], null); expect(result.type).toBe("CallExpression"); const call = result as CallExpression; expect(call.callee.type).toBe("Identifier"); expect((call.callee as Identifier).name).toBe("renderToString"); // Third argument should be new Map([]) expect(call.arguments[2]?.type).toBe("NewExpression"); }); it("generates renderToString call with indexed Map entries for dynamic values", () => { const tree: ESTNode = { type: "ArrayExpression", elements: [ { type: "Literal", value: "div", raw: '"div"' }, { type: "Literal", value: null, raw: "null" }, ], }; const val1: ESTNode = { type: "Literal", value: "hello", raw: '"hello"' }; const val2: ESTNode = { type: "Literal", value: 42, raw: "42" }; const result = generateSSRRender(tree, [val1, val2], null); expect(result.type).toBe("CallExpression"); const call = result as CallExpression; const mapExpr = call.arguments[2] as NewExpression; expect(mapExpr.type).toBe("NewExpression"); // Map constructor receives an ArrayExpression of entries const entries = (mapExpr.arguments[0] as ArrayExpression).elements; expect(entries).toHaveLength(2); // First entry: [1, val1] const first = entries[0] as ArrayExpression; expect((first.elements[0] as Literal).value).toBe(1); // Second entry: [2, val2] const second = entries[1] as ArrayExpression; expect((second.elements[0] as Literal).value).toBe(2); }); it("uses empty object as state when stateExpr is null", () => { const tree: ESTNode = { type: "ArrayExpression", elements: [] }; const result = generateSSRRender(tree, [], null); const call = result as CallExpression; // Second argument (state) should be an empty object expression expect(call.arguments[1]?.type).toBe("ObjectExpression"); const stateArg = call.arguments[1] as ObjectExpression; expect(stateArg.properties).toHaveLength(0); }); it("uses provided stateExpr as state argument", () => { const tree: ESTNode = { type: "ArrayExpression", elements: [] }; const stateExpr: ESTNode = { type: "ObjectExpression", properties: [ { type: "Property", key: { type: "Identifier", name: "count" }, value: { type: "Literal", value: 0, raw: "0" }, kind: "init", computed: false, method: false, shorthand: false, }, ], }; const result = generateSSRRender(tree, [], stateExpr); const call = result as CallExpression; // Second argument (state) should be the provided expression (same reference) expect(call.arguments[1]).toBe(stateExpr); }); it("returns a CallExpression with callee name renderToString", () => { const tree: ESTNode = { type: "ArrayExpression", elements: [] }; const result = generateSSRRender(tree, [], null); expect(result.type).toBe("CallExpression"); const call = result as CallExpression; expect(call.callee.type).toBe("Identifier"); expect((call.callee as Identifier).name).toBe("renderToString"); }); }); describe("generateStateObject", () => { it("returns an empty ObjectExpression when signals Map is empty", () => { const result = generateStateObject(new Map()); expect(result.type).toBe("ObjectExpression"); const obj = result as ObjectExpression; expect(obj.properties).toHaveLength(0); }); it("returns ObjectExpression with properties for each signal entry", () => { const val1: ESTNode = { type: "Literal", value: 0, raw: "0" }; const val2: ESTNode = { type: "Literal", value: "Alice", raw: '"Alice"' }; const signals = new Map([ ["count", val1], ["name", val2], ]); const result = generateStateObject(signals); expect(result.type).toBe("ObjectExpression"); const obj = result as ObjectExpression; expect(obj.properties).toHaveLength(2); // Verify property names const propNames = obj.properties.map( (p) => ((p as Property).key as Identifier).name, ); expect(propNames).toContain("count"); expect(propNames).toContain("name"); }); it("composes with generateSSRRender to produce state in renderToString call", () => { const signals = new Map([ ["count", { type: "Literal", value: 0, raw: "0" }], ]); const stateExpr = generateStateObject(signals); const tree: ESTNode = { type: "ArrayExpression", elements: [] }; const result = generateSSRRender(tree, [], stateExpr); const call = result as CallExpression; // Second argument (state) should be the ObjectExpression from generateStateObject expect(call.arguments[1]?.type).toBe("ObjectExpression"); const stateArg = call.arguments[1] as ObjectExpression; expect(stateArg.properties).toHaveLength(1); const prop = stateArg.properties[0] as Property; expect((prop.key as Identifier).name).toBe("count"); }); });