feat(cms): recover drafts and failed imports with verified UI workflows
This commit is contained in:
1 parent
410a1e466c
commit
88790d1a0d
84 files changed
+3952
-288
No files matched your search
@@ -0,0 +1,224 @@
|
||||
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 =
|
||||
'<!doctype html><html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="csrf-token" content="' +
|
||||
"a".repeat(64) +
|
||||
'"><title>UI verification fixture</title><link rel="stylesheet" href="/fixture/styles.css"></head><body><div id="root"></div><script src="/fixture/app.js"></script></body></html>';
|
||||
const cover =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="720" height="160" viewBox="0 0 720 160"><rect width="720" height="160" fill="#334155"/><circle cx="620" cy="80" r="48" fill="#f59e0b"/></svg>';
|
||||
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)));
|
||||
Reference in new issue
Block a user