Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-ui-hot-swap.ts
T

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";
}