190 lines
7.8 KiB
TypeScript
190 lines
7.8 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { basename, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { buildRouter } from "@wrnexus/router";
|
|
import { createHandlers } from "../src/runtime.ts";
|
|
import { encodeControlTemplate } from "@wrnexus/core";
|
|
|
|
test("nested encoded conditional and empty-list templates prepare shared controls", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "wrn-nested-controls-"));
|
|
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64");
|
|
try {
|
|
const app = join(root, "app");
|
|
mkdirSync(join(app, "pages"), { recursive: true });
|
|
mkdirSync(join(app, "components"), { recursive: true });
|
|
writeFileSync(join(app, "pages/index.ts"), "export default () => '';");
|
|
writeFileSync(
|
|
join(app, "components/Button.wrn"),
|
|
"component Button { view { <button>Review</button> } }",
|
|
);
|
|
const mount = '<div data-component="Button"></div>';
|
|
const inner = encode([{ cond: "review.active", body: mount }]);
|
|
const list = encode({ list: "items", item: "item", body: "<p>Item</p>", empty: mount });
|
|
const outer = encode([
|
|
{
|
|
cond: "review",
|
|
body: `<template data-wrn-if="${inner}"></template><template data-wrn-control-end></template><template data-wrn-each="${list}"></template><template data-wrn-control-end></template>`,
|
|
},
|
|
]);
|
|
const handlers = createHandlers({
|
|
mode: "development",
|
|
hmr: false,
|
|
router: buildRouter(app),
|
|
loadModule: async (file) =>
|
|
basename(file) === "Button.wrn"
|
|
? { render: () => "<button>Review</button>" }
|
|
: {
|
|
default: () =>
|
|
`<template data-wrn-if="${encodeControlTemplate(JSON.parse(Buffer.from(outer, "base64").toString()))}"></template><template data-wrn-control-end></template>`,
|
|
},
|
|
getMiddleware: async () => [],
|
|
assets: { serve: async () => null },
|
|
});
|
|
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
|
upgrade: () => false,
|
|
}))!.text();
|
|
const registry = /data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html);
|
|
expect(registry).not.toBeNull();
|
|
const definitions = JSON.parse(registry![1]!);
|
|
const decode = (markup: string, kind: string) => {
|
|
const reference = new RegExp(`data-wrn-${kind}="(@[A-Za-z0-9-]+:[0-9]+)"`).exec(markup)![1]!;
|
|
return definitions[Number(reference.split(":")[1])];
|
|
};
|
|
const branch = definitions[0][0].body;
|
|
expect(decode(branch, "if")[0].body).toBe("<button>Review</button>");
|
|
expect(decode(branch, "each").empty).toBe("<button>Review</button>");
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("shared branch references are prepared once across a branching control graph", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "wrn-shared-controls-"));
|
|
try {
|
|
const app = join(root, "app");
|
|
mkdirSync(join(app, "pages"), { recursive: true });
|
|
writeFileSync(join(app, "pages/index.ts"), "export default () => ''; ");
|
|
let reads = 0;
|
|
const handlers = createHandlers({
|
|
mode: "development",
|
|
hmr: false,
|
|
router: buildRouter(app),
|
|
loadModule: async () => ({
|
|
default: () => {
|
|
let body = "<p>Leaf</p>";
|
|
for (let depth = 0; depth < 12; depth++) {
|
|
let storedBody = body;
|
|
const entry = {
|
|
cond: "open",
|
|
get body() {
|
|
reads++;
|
|
return storedBody;
|
|
},
|
|
set body(value: string) {
|
|
storedBody = value;
|
|
},
|
|
};
|
|
const reference = encodeControlTemplate([entry]);
|
|
body =
|
|
`<template data-wrn-if="${reference}"></template><template data-wrn-control-end></template>`.repeat(
|
|
2,
|
|
);
|
|
}
|
|
return body;
|
|
},
|
|
}),
|
|
getMiddleware: async () => [],
|
|
assets: { serve: async () => null },
|
|
});
|
|
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
|
upgrade: () => false,
|
|
}))!.text();
|
|
expect(reads).toBeLessThan(200);
|
|
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
|
expect(definitions).toHaveLength(12);
|
|
expect(definitions[0][0].body).toBe("<p>Leaf</p>");
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("deep inline loop definitions stay bounded after shared component preparation", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "wrn-loop-size-"));
|
|
try {
|
|
const app = join(root, "app");
|
|
mkdirSync(join(app, "pages"), { recursive: true });
|
|
mkdirSync(join(app, "components"), { recursive: true });
|
|
writeFileSync(join(app, "pages/index.ts"), "export default () => ''; ");
|
|
writeFileSync(join(app, "components/Button.wrn"), "component Button { view { <button /> } }");
|
|
let body = '<div data-component="Button"></div>';
|
|
for (let depth = 0; depth < 12; depth++) {
|
|
const encoded = Buffer.from(
|
|
JSON.stringify({ list: "items", item: "item", body, empty: "" }),
|
|
).toString("base64");
|
|
body = `<template data-wrn-each="${encoded}"></template><template data-wrn-control-end></template>`;
|
|
}
|
|
const handlers = createHandlers({
|
|
mode: "development",
|
|
hmr: false,
|
|
router: buildRouter(app),
|
|
loadModule: async (file) =>
|
|
basename(file) === "Button.wrn"
|
|
? { render: () => `<button>${"Review".repeat(200)}</button>` }
|
|
: { default: () => body },
|
|
getMiddleware: async () => [],
|
|
assets: { serve: async () => null },
|
|
});
|
|
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
|
upgrade: () => false,
|
|
}))!.text();
|
|
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
|
expect(definitions).toHaveLength(12);
|
|
expect(html.length).toBeLessThan(12000);
|
|
expect(
|
|
definitions.filter((definition: { body: string }) => definition.body.includes("<button>")),
|
|
).toHaveLength(1);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("inert control nesting does not consume the component composition budget", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "wrn-control-budget-"));
|
|
try {
|
|
const app = join(root, "app");
|
|
mkdirSync(join(app, "pages"), { recursive: true });
|
|
mkdirSync(join(app, "components"), { recursive: true });
|
|
writeFileSync(join(app, "pages/index.ts"), "export default ()=>'';");
|
|
writeFileSync(
|
|
join(app, "components/Button.wrn"),
|
|
"component Button { view { <button>Nested field</button> } }",
|
|
);
|
|
let body = '<div data-component="Button"></div>';
|
|
for (let depth = 0; depth < 20; depth++) {
|
|
const encoded = Buffer.from(JSON.stringify([{ cond: "open", body }])).toString("base64");
|
|
body = `<template data-wrn-if="${encoded}"></template><template data-wrn-control-end></template>`;
|
|
}
|
|
const handlers = createHandlers({
|
|
mode: "development",
|
|
hmr: false,
|
|
router: buildRouter(app),
|
|
loadModule: async (file) =>
|
|
basename(file) === "Button.wrn"
|
|
? { render: () => "<button>Nested field</button>" }
|
|
: { default: () => body },
|
|
getMiddleware: async () => [],
|
|
assets: { serve: async () => null },
|
|
});
|
|
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
|
upgrade: () => false,
|
|
}))!.text();
|
|
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
|
expect(JSON.stringify(definitions)).toContain("<button>Nested field</button>");
|
|
expect(JSON.stringify(definitions)).not.toContain("data-component");
|
|
expect(html.length).toBeLessThan(20000);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|