import { readFile } from "node:fs/promises"; import { createServer } from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; import tailwind from "@tailwindcss/postcss"; import { build } from "esbuild"; import postcss from "postcss"; const root = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../..", ); const fixtures = path.join(root, "e2e/ui/fixtures"); const port = Number(process.env.UI_TEST_PORT || 3107); if (!Number.isInteger(port) || port < 1024 || port > 65535) throw Error("Invalid UI_TEST_PORT"); const actions = new Set([ "@/actions/events", "@/actions/admin-settings", "@/actions/admin-table-views", "@/actions/article-recovery", "@/actions/admin-media", ]); const bundle = await build({ entryPoints: [path.join(fixtures, "entry.tsx")], bundle: true, write: false, outdir: path.join(root, "test-results/ui-fixture"), format: "iife", jsx: "automatic", alias: { "@": path.join(root, "src") }, define: { "process.env.NODE_ENV": '"production"' }, plugins: [ { name: "server-contracts", setup(build) { build.onResolve( { filter: /^next\/navigation$|^next\/link$|^@\/actions\// }, (args) => { if (args.path === "next/navigation") return { path: path.join(fixtures, "navigation.ts") }; if (args.path === "next/link") return { path: path.join(fixtures, "next-link.tsx") }; if (actions.has(args.path)) return { path: path.join(fixtures, "server-actions.ts") }; throw Error(`Unexpected server action in UI fixture: ${args.path}`); }, ); build.onResolve( { filter: /^@\/lib\/db$|^mysql2|^next\/headers$/ }, (args) => { throw Error( `Database or server dependency forbidden in UI fixture: ${args.path}`, ); }, ); }, }, ], }); const globalCss = ( await postcss([tailwind({ base: root })]).process( await readFile(path.join(root, "src/app/globals.css"), "utf8"), { from: path.join(root, "src/app/globals.css") }, ) ).css; const css = globalCss + "\n" + bundle.outputFiles .filter((file) => file.path.endsWith(".css")) .map((file) => file.text) .join("\n"); const js = bundle.outputFiles.find((file) => file.path.endsWith(".js")).text; const html = 'UI verification fixture
'; const cover = ''; const jobs = new Map(); const views = []; const json = (res, value, status = 200) => { res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", }); res.end(JSON.stringify(value)); }; const server = createServer(async (req, res) => { try { const url = new URL(req.url, "http://127.0.0.1"); if (url.pathname === "/fixture/reset") { jobs.clear(); views.length = 0; return json(res, { ok: true }); } if (url.pathname === "/health") return json(res, { ready: true }); if (url.pathname === "/fixture/styles.css") { res.setHeader("content-type", "text/css"); return res.end(css); } if (url.pathname === "/fixture/app.js") { res.setHeader("content-type", "application/javascript"); return res.end(js); } if (url.pathname === "/fixture/cover.svg") { res.setHeader("content-type", "image/svg+xml"); return res.end(cover); } if (url.pathname.startsWith("/vendor/tinymce/")) { const assetRoot = path.join(root, "node_modules/tinymce"); const asset = path.resolve( assetRoot, url.pathname.slice("/vendor/tinymce/".length), ); if (!asset.startsWith(assetRoot + path.sep)) { res.writeHead(400); return res.end(); } res.setHeader( "content-type", asset.endsWith(".css") ? "text/css" : asset.endsWith(".js") ? "application/javascript" : asset.endsWith(".woff") ? "font/woff" : "application/octet-stream", ); return res.end(await readFile(asset)); } let body = {}; if (req.method === "POST" || req.method === "PATCH") { const chunks = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > 1000000) throw Error("Fixture payload too large"); chunks.push(chunk); } body = JSON.parse(Buffer.concat(chunks).toString() || "{}"); } if (url.pathname === "/api/admin/studio/import-jobs") { if (req.method === "PATCH") { const job = jobs.get(body.id); if (job) job.state = "cancelled"; return json(res, { ok: true, job }); } return json(res, { ok: true, jobs: [...jobs.values()], nextCursor: null, }); } if (url.pathname === "/api/admin/import/official/sync-all") { if (!jobs.has(body.id)) jobs.set(body.id, { id: body.id, userId: 7, createdAt: "2030-01-01T12:00:00.000Z", updatedAt: "2030-01-01T12:00:00.000Z", state: "queued", translate: false, syncKind: "official", items: [ { id: 0, classname: "fixture_chair", name: "Fixture chair", description: "", type: "flooritem", category: "other", revision: 1, state: "pending", }, ], }); await new Promise((resolve) => setTimeout(resolve, 150)); return json(res, { ok: true, job: jobs.get(body.id) }); } if (url.pathname === "/api/media") return json(res, { files: [] }); if (url.pathname.startsWith("/fixture/actions/")) { const name = url.pathname.split("/").at(-1); if (name === "load-recovery") return json(res, { version: 0, draft: null, revisions: [] }); if (name === "autosave" || name === "clear-recovery") return json(res, { ok: true, version: 1 }); if (name === "list-views") return json(res, { ok: true, data: { views } }); if (name === "save-view") { views.push(body.view ?? body); return json(res, { ok: true }); } return json(res, { ok: true, data: {} }); } if ( [ "/admin/attachment-harness", "/admin/jobs-harness", "/admin/settings", "/admin/articles/new", "/admin/articles/recovery", "/admin/events/recovery", "/admin/tickets/desk", "/admin/import/official", ].includes(url.pathname) ) { res.setHeader("content-type", "text/html"); return res.end(html); } res.writeHead(404); res.end("Not found"); } catch (error) { console.error(error); res.writeHead(500); res.end("Fixture error"); } }); server.listen(port, "127.0.0.1", () => console.log(`Isolated UI fixture ready at http://127.0.0.1:${port}`), ); for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => server.close(() => process.exit(0)));