114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
#!/usr/bin/env tsx
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
import { AppShell } from "@toolctrl/ui-app";
|
|
import {
|
|
createHotSwapDrillPage,
|
|
createMockPage,
|
|
createMockRegistry,
|
|
generateHotSwapMock,
|
|
generateMockPageHtml,
|
|
} from "@toolctrl/ui-mock";
|
|
|
|
export async function verifyUiHotSwap() {
|
|
const page = createHotSwapDrillPage();
|
|
const replacementPage = createMockPage({
|
|
...page,
|
|
id: "phase-c-hot-swap-replacement",
|
|
titleKey: "mock.phase-c.hot-swap.replacement.title",
|
|
state: {
|
|
status: "ready",
|
|
data: {
|
|
cards: [
|
|
{ labelKey: "mock.phase-c.hot-swap.metrics.modules", value: "4", trend: "up" },
|
|
{ labelKey: "mock.phase-c.hot-swap.metrics.swap", value: "active", trend: "flat" },
|
|
],
|
|
activity: [{ labelKey: "mock.phase-c.hot-swap.activity.registrySwap", meta: "replacement" }],
|
|
},
|
|
},
|
|
registries: {
|
|
...page.registries,
|
|
sidebar: [
|
|
...(page.registries?.sidebar ?? []),
|
|
{ id: "phase-c-replacement", href: page.route, labelKey: "mock.phase-c.hot-swap.replacement.label" },
|
|
],
|
|
breadcrumbs: [
|
|
...(page.registries?.breadcrumbs ?? []),
|
|
{ id: "replacement", href: page.route, labelKey: "mock.phase-c.hot-swap.replacement.label" },
|
|
],
|
|
},
|
|
});
|
|
const registry = createMockRegistry([page, replacementPage]);
|
|
const activePage = registry.get(page.route);
|
|
|
|
assert.equal(activePage?.id, "phase-c-hot-swap-replacement");
|
|
|
|
const mockModule = generateHotSwapMock(page);
|
|
const mockHtml = generateMockPageHtml(page);
|
|
const replacementHtml = generateMockPageHtml(replacementPage);
|
|
const shellHtml = renderToStaticMarkup(
|
|
createElement(
|
|
AppShell,
|
|
{ registries: activePage?.registries },
|
|
createElement("main", {
|
|
"data-hot-swap-route": activePage?.route,
|
|
"data-hot-swap-status": activePage?.state.status,
|
|
"data-hot-swap-page": activePage?.id,
|
|
dangerouslySetInnerHTML: { __html: replacementHtml },
|
|
}),
|
|
),
|
|
);
|
|
|
|
assert.match(mockModule, /export const mockPage =/);
|
|
assert.match(mockModule, /phase-c-hot-swap/);
|
|
const importedMockPage = await importGeneratedHotSwapModule(mockModule);
|
|
assert.equal(importedMockPage.id, "phase-c-hot-swap");
|
|
assert.equal(importedMockPage.route, "/mock/phase-c/hot-swap");
|
|
assert.equal(importedMockPage.state.status, "ready");
|
|
assert.match(mockHtml, /data-ui-mock-route="\/mock\/phase-c\/hot-swap"/);
|
|
assert.match(replacementHtml, /phase-c-hot-swap-replacement/);
|
|
assert.notEqual(replacementHtml, mockHtml);
|
|
assert.match(shellHtml, /data-ui-shell="app"/);
|
|
assert.match(shellHtml, /data-sidebar-count="3"/);
|
|
assert.match(shellHtml, /data-breadcrumb-count="4"/);
|
|
assert.match(shellHtml, /data-mobile-count="1"/);
|
|
assert.match(shellHtml, /data-portal-count="1"/);
|
|
assert.match(shellHtml, /data-hot-swap-route="\/mock\/phase-c\/hot-swap"/);
|
|
assert.match(shellHtml, /data-hot-swap-status="ready"/);
|
|
assert.match(shellHtml, /data-hot-swap-page="phase-c-hot-swap-replacement"/);
|
|
}
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
|
|
if (isCli) {
|
|
await verifyUiHotSwap();
|
|
process.stdout.write("UI hot-swap drill verified.\n");
|
|
}
|
|
|
|
async function importGeneratedHotSwapModule(source: string) {
|
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "toolctrl-ui-hot-swap-"));
|
|
const modulePath = path.join(tempDir, "hot-swap.mjs");
|
|
|
|
try {
|
|
await writeFile(modulePath, source, "utf8");
|
|
const imported = (await import(`${modulePath}?t=${Date.now()}`)) as { default?: unknown; mockPage?: unknown };
|
|
assert.deepEqual(imported.default, imported.mockPage);
|
|
assert.ok(isMockPage(imported.mockPage));
|
|
return imported.mockPage;
|
|
} finally {
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function isMockPage(value: unknown): value is ReturnType<typeof createHotSwapDrillPage> {
|
|
if (!value || typeof value !== "object") return false;
|
|
const candidate = value as Partial<ReturnType<typeof createHotSwapDrillPage>>;
|
|
return typeof candidate.id === "string" && typeof candidate.route === "string" && typeof candidate.state === "object";
|
|
}
|