Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env tsx
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Use a relative path so vitest's Vite-based resolver can find the workspace package
|
||||
// even when it has not been hoisted into the top-level node_modules/@toolctrl directory.
|
||||
import { createHotSwapDrillPage, generateMockArtifacts } from "../packages/ui-mock/src/index.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { JSDOM } = require("jsdom") as {
|
||||
JSDOM: new (html: string) => { window: { document: Document } };
|
||||
};
|
||||
|
||||
const BASELINE_DIR = "packages/ui-mock/visual-baseline";
|
||||
const BASELINE_FILES = {
|
||||
module: "hot-swap.mock.ts",
|
||||
html: "hot-swap.html",
|
||||
snapshot: "hot-swap.snapshot.json",
|
||||
} as const;
|
||||
|
||||
export type UiMockVisualResult = {
|
||||
readonly passes: readonly string[];
|
||||
readonly failures: readonly string[];
|
||||
readonly observations: readonly string[];
|
||||
};
|
||||
|
||||
export function buildUiMockVisualDrill() {
|
||||
const page = createHotSwapDrillPage();
|
||||
const artifacts = generateMockArtifacts(page);
|
||||
return {
|
||||
module: artifacts.module,
|
||||
html: artifacts.html,
|
||||
snapshot: `${JSON.stringify(artifacts.snapshot, null, 2)}\n`,
|
||||
};
|
||||
}
|
||||
|
||||
export function writeUiMockVisualBaseline(rootDir = process.cwd()) {
|
||||
const drill = buildUiMockVisualDrill();
|
||||
const baselineDir = path.join(rootDir, BASELINE_DIR);
|
||||
fs.mkdirSync(baselineDir, { recursive: true });
|
||||
|
||||
for (const [artifactName, fileName] of Object.entries(BASELINE_FILES)) {
|
||||
fs.writeFileSync(path.join(baselineDir, fileName), drill[artifactName as keyof typeof drill], "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyUiMockVisualBaseline(rootDir = process.cwd()): UiMockVisualResult {
|
||||
const drill = buildUiMockVisualDrill();
|
||||
const passes: string[] = [];
|
||||
const failures: string[] = [];
|
||||
const observations: string[] = [];
|
||||
|
||||
assert.equal(drill.module.includes("export const mockPage"), true);
|
||||
assert.equal(drill.html.includes('data-ui-mock-route="/mock/phase-c/hot-swap"'), true);
|
||||
const domSmoke = verifyRenderedHotSwapDrill(drill.html);
|
||||
passes.push(...domSmoke.passes);
|
||||
failures.push(...domSmoke.failures);
|
||||
|
||||
for (const [artifactName, fileName] of Object.entries(BASELINE_FILES)) {
|
||||
const relativePath = `${BASELINE_DIR}/${fileName}`;
|
||||
const baselinePath = path.join(rootDir, relativePath);
|
||||
if (!fs.existsSync(baselinePath)) {
|
||||
failures.push(`${relativePath}: missing UI mock static artifact baseline`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const expected = normalizeNewlines(fs.readFileSync(baselinePath, "utf8"));
|
||||
const actual = normalizeNewlines(drill[artifactName as keyof typeof drill]);
|
||||
if (actual === expected) {
|
||||
passes.push(`${relativePath}: matches generated hot-swap drill artifact`);
|
||||
} else {
|
||||
failures.push(`${relativePath}: static artifact baseline drifted; run pnpm run verify:ui-visual -- --update after reviewing the change`);
|
||||
}
|
||||
}
|
||||
|
||||
observations.push("UI mock static drill renders through JSDOM in CI; browser screenshot and E2E commands remain intentionally outside this gate.");
|
||||
|
||||
return { passes, failures, observations };
|
||||
}
|
||||
|
||||
function verifyRenderedHotSwapDrill(html: string): { passes: string[]; failures: string[] } {
|
||||
const passes: string[] = [];
|
||||
const failures: string[] = [];
|
||||
const document = new JSDOM(html).window.document;
|
||||
const body = document.body;
|
||||
const shell = document.querySelector(".mock-shell");
|
||||
const activeLinks = [...document.querySelectorAll(".mock-link")].filter((item) => item.getAttribute("data-active") === "true");
|
||||
const cards = [...document.querySelectorAll('section[aria-label="Mock metrics"] .mock-card')];
|
||||
const stateScript = document.querySelector("#mock-page-state");
|
||||
|
||||
if (body?.getAttribute("data-ui-mock-route") === "/mock/phase-c/hot-swap" && shell?.getAttribute("data-ui-mock-page") === "phase-c-hot-swap") {
|
||||
passes.push("rendered hot-swap HTML exposes canonical mock route and page id");
|
||||
} else {
|
||||
failures.push("rendered hot-swap HTML is missing canonical mock route or page id");
|
||||
}
|
||||
|
||||
if (activeLinks.length === 1) {
|
||||
passes.push("rendered hot-swap navigation has exactly one active route");
|
||||
} else {
|
||||
failures.push(`rendered hot-swap navigation expected one active route but found ${activeLinks.length}`);
|
||||
}
|
||||
|
||||
if (cards.length >= 3 && document.body.textContent?.includes("Serialized state")) {
|
||||
passes.push("rendered hot-swap page includes metric cards and serialized state");
|
||||
} else {
|
||||
failures.push("rendered hot-swap page is missing metric cards or serialized state");
|
||||
}
|
||||
|
||||
try {
|
||||
const state = stateScript?.textContent ? JSON.parse(decodeHtmlEntities(stateScript.textContent)) : null;
|
||||
if (state?.status === "ready") {
|
||||
passes.push("rendered hot-swap state payload parses as ready");
|
||||
} else {
|
||||
failures.push("rendered hot-swap state payload is missing or invalid");
|
||||
}
|
||||
} catch {
|
||||
failures.push("rendered hot-swap state payload is missing or invalid");
|
||||
}
|
||||
|
||||
return { passes, failures };
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(value: string) {
|
||||
return value.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
||||
}
|
||||
|
||||
export function formatUiMockVisualResult(result: UiMockVisualResult): string {
|
||||
const status = result.failures.length > 0 ? "FAIL" : "PASS";
|
||||
return [
|
||||
`${status} UI mock static artifact baseline verification`,
|
||||
...result.passes.map((pass) => `PASS ${pass}`),
|
||||
...result.observations.map((observation) => `OBSERVE ${observation}`),
|
||||
...result.failures.map((failure) => `FAIL ${failure}`),
|
||||
`Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function normalizeNewlines(value: string): string {
|
||||
return value.replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
export function assertVisualBaselineUpdateAllowed(env: NodeJS.ProcessEnv = process.env): void {
|
||||
if (env.CI) {
|
||||
throw new Error("Refusing to update UI mock static artifact baselines while CI is set.");
|
||||
}
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
|
||||
if (isCli) {
|
||||
if (process.argv.includes("--update")) {
|
||||
assertVisualBaselineUpdateAllowed();
|
||||
writeUiMockVisualBaseline(process.cwd());
|
||||
}
|
||||
|
||||
const result = verifyUiMockVisualBaseline(process.cwd());
|
||||
const output = formatUiMockVisualResult(result);
|
||||
|
||||
if (result.failures.length > 0) {
|
||||
process.stderr.write(output);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(output);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user