Files
WRNexusJS/packages/design-studio/test/server.test.ts
T

175 lines
7.6 KiB
TypeScript

import { test, expect, afterAll } from "bun:test";
import { mkdtempSync, readFileSync, existsSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createStudioHandler } from "../src/server.ts";
import { StudioApplication } from "../src/application.ts";
import { CORE_DESIGN_SYSTEMS } from "../../design-system/src/index.ts";
import { resolveThemeConfig } from "../../styles/src/theme.ts";
import { designRevision } from "../src/decisions.ts";
import { DEFAULT_DESIGN_BRIEF } from "../src/generation.ts";
const root = mkdtempSync(join(tmpdir(), "wrn-studio-test-"));
afterAll(() => rmSync(root, { recursive: true, force: true }));
const handler = await createStudioHandler({ applicationRoot: root });
const send = (path: string, body?: unknown, origin?: string) =>
handler(
new Request(
"http://127.0.0.1:4320" + path,
body === undefined
? undefined
: {
method: "POST",
headers: { "content-type": "application/json", ...(origin ? { origin } : {}) },
body: JSON.stringify(body),
},
),
);
test("hosted Studio keeps its path, same-origin checks and export-only boundary", async () => {
const hosted = await createStudioHandler({ hosted: { basePath: "/design-studio" } });
const origin = "https://components.example";
const html = await hosted(new Request(origin + "/design-studio/"));
expect(await html.text()).toContain('<base href="/design-studio/">');
expect((await hosted(new Request(origin + "/api/application"))).status).toBe(404);
const state = await hosted(new Request(origin + "/design-studio/api/application"));
expect((await state.json()).enabled).toBe(false);
const proxied = await hosted(
new Request("http://components.example/design-studio/api/application", {
headers: { origin, "x-forwarded-proto": "https" },
}),
);
expect(proxied.status).toBe(200);
const request = (path: string, body: unknown, from = origin) =>
hosted(
new Request(origin + "/design-studio" + path, {
method: "POST",
headers: { "content-type": "application/json", origin: from },
body: JSON.stringify(body),
}),
);
expect(
(await request("/api/apply", { system: CORE_DESIGN_SYSTEMS.soft, base: "none" })).status,
).toBe(409);
expect(
(await request("/api/preview", { system: CORE_DESIGN_SYSTEMS.soft }, "https://other.example"))
.status,
).toBe(403);
const preview = await request("/api/preview", { system: CORE_DESIGN_SYSTEMS.soft });
const { url } = await preview.json();
expect(url).toStartWith("/design-studio/preview/");
expect((await hosted(new Request(origin + url))).status).toBe(200);
await expect(
createStudioHandler({ applicationRoot: root, hosted: { basePath: "/design-studio" } }),
).rejects.toThrow("export-only");
});
test("generation endpoint exposes only configured metadata and stages validated proposals", async () => {
const meta = await (await send("/api/generation")).json();
expect(meta.provider).toBe("local-rules");
expect(JSON.stringify(meta)).not.toContain("apiKey");
const system = structuredClone(CORE_DESIGN_SYSTEMS.soft),
locks = ["/tokens/shape.control"];
const input = {
kind: "directions",
brief: { ...DEFAULT_DESIGN_BRIEF, product: "A website", audience: "Customers" },
system,
locks,
baseRevision: await designRevision(system, locks),
};
const response = await send("/api/generate", input);
expect(response.status).toBe(200);
const batch = await response.json();
expect(batch.alternatives).toHaveLength(3);
expect(batch.provenance.provider).toBe("local-rules");
expect((await send("/api/generate", { ...input, baseRevision: "a".repeat(64) })).status).toBe(
409,
);
expect((await send("/api/generate", input, "https://example.org")).status).toBe(403);
});
test("local application supports immutable bundles, conflicts, rollback and existing config preservation", () => {
const app = new StudioApplication(root);
writeFileSync(join(root, "wrn.config.ts"), "export default { existing: true };");
const first = app.apply(CORE_DESIGN_SYSTEMS.square, "none", { mode: "light" });
expect(existsSync(join(first.directory, "system.css"))).toBe(true);
const theme = JSON.parse(readFileSync(join(first.directory, "theme.json"), "utf8"));
expect(resolveThemeConfig(theme).recipeCss).toContain("data-wrn-owner");
expect(() => app.apply(CORE_DESIGN_SYSTEMS.soft, "none")).toThrow("WRN-STUDIO-CONFLICT");
const second = app.apply(CORE_DESIGN_SYSTEMS.soft, first.revision);
expect(app.rollback(second.revision).revision).toBe(first.revision);
expect(app.system()!.id).toBe("square");
expect(readFileSync(join(root, "wrn.config.ts"), "utf8")).toContain("existing: true");
const fresh = mkdtempSync(join(root, "first-"));
const initial = new StudioApplication(fresh);
const applied = initial.apply(CORE_DESIGN_SYSTEMS.soft, "none");
expect(initial.rollback(applied.revision).revision).toBe("none");
expect(initial.system()).toBeUndefined();
});
test("server serves a browser bundle, rejects cross-origin, invalid, oversized and unconfigured writes", async () => {
expect((await send("/")).status).toBe(200);
const bundle = await send("/studio.js");
expect(bundle.status).toBe(200);
expect(await bundle.text()).not.toContain("node:fs");
expect(
(
await send(
"/api/apply",
{ system: CORE_DESIGN_SYSTEMS.soft, base: "none" },
"https://other.example",
)
).status,
).toBe(403);
expect((await handler(new Request("http://evil.example/api/application"))).status).toBe(403);
expect((await send("/api/apply", { system: {}, base: "none" })).status).toBe(422);
expect((await send("/api/apply", { data: "x".repeat(2_000_001) })).status).toBe(413);
const readOnly = await createStudioHandler();
expect(
(
await readOnly(
new Request("http://127.0.0.1/api/apply", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ system: CORE_DESIGN_SYSTEMS.soft, base: "none" }),
}),
)
).status,
).toBe(409);
});
test("all presets generate isolated real component previews with light/dark and responsive/RTL controls", async () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS))
for (const mode of ["light", "dark"]) {
const result = await send("/api/preview", { system, mode, direction: "rtl" });
expect(result.status).toBe(200);
const { url } = await result.json();
const preview = await send(url);
const html = await preview.text();
expect(html).toContain(`data-theme="${mode}"`);
expect(html).toContain('dir="rtl"');
expect(html).toContain('data-wrn-component="Radio"');
expect(html).toContain('data-wrn-component="InputNumber"');
expect(html).toContain("data-scope");
expect(html).toContain("data-wrn-controls");
expect(html).not.toContain("failed to load component controllers");
expect(preview.headers.get("content-security-policy")).toContain("connect-src 'none'");
}
});
test("catalog previews cover all 104 component defaults and unknown routes do not expose files", async () => {
let count = 0;
for (let page = 0; page < 7; page++) {
const result = await send("/api/preview", {
system: CORE_DESIGN_SYSTEMS.soft,
content: "catalog",
page,
});
const { url } = await result.json();
const html = await (await send(url)).text();
count += (html.match(/data-wrn-component=/g) ?? []).length;
}
expect(count).toBe(104);
expect((await send("/../../package.json")).status).toBe(404);
});