candidates and board ui agent issue
Some checks failed
CI / check (push) Failing after 4m58s

This commit is contained in:
2026-09-05 10:46:06 +05:30
parent e02a0c23d4
commit 6249e00a3a
78 changed files with 15071 additions and 1998 deletions

115
scripts/render-page.mjs Normal file
View File

@@ -0,0 +1,115 @@
/**
* Render an Admin page to static markup, outside a browser.
*
* The migration to the UI node tree has to be provable rather than asserted:
* a page is rendered before it is touched, rendered again afterwards, and the
* two are compared. This is the thing that renders it, used both to capture a
* baseline and, from the check script, to compare against one.
*
* node scripts/render-page.mjs src/pages/admin/HiredHistory.jsx out.html
*
* The page is loaded through a real Vite server, so `@/` aliases, Markdown
* imports and `import.meta.glob` behave exactly as they do in the app. Queries
* are disabled rather than mocked: every page then renders its empty state,
* deterministically, which is all a structural comparison needs.
*/
import { createServer } from 'vite';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
/**
* The design system reads `window` when its modules evaluate, which is a
* pre-existing SSR limitation and not what any of this is testing. The shim is
* the same one the citation tests use.
*/
export function shimWindow() {
const had = 'window' in globalThis;
const hadSvg = 'SVGElement' in globalThis;
if (!had) {
globalThis.window = {
matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
addEventListener() {}, removeEventListener() {},
};
}
/* Recharts tests `instanceof SVGElement` while measuring, which is a browser
global with no Node equivalent. A bare class is enough: nothing is ever an
instance of it, which is the correct answer outside a browser. */
if (!hadSvg) globalThis.SVGElement = class SVGElement {};
return () => {
if (!had) delete globalThis.window;
if (!hadSvg) delete globalThis.SVGElement;
};
}
/**
* Render one page.
*
* `providers` are supplied by the caller rather than assumed here, because the
* editing session belongs to the layout and a baseline captured before a
* migration must be rendered without it.
*/
export async function renderPage(server, modulePath, { route = '/', wrap = null } = {}) {
/* Imported through Node rather than the Vite graph so the instance matches
the one the page itself resolves — loading them as SSR modules creates a
second copy, and a second QueryClientProvider provides nothing. */
const { QueryClient, QueryClientProvider } = await import('@tanstack/react-query');
const { MemoryRouter } = await import('react-router-dom');
const Page = (await server.ssrLoadModule(modulePath)).default;
const client = new QueryClient({ defaultOptions: { queries: { retry: false, enabled: false } } });
const inner = wrap ? wrap(React.createElement(Page)) : React.createElement(Page);
return renderToStaticMarkup(
React.createElement(MemoryRouter, { initialEntries: [route] },
React.createElement(QueryClientProvider, { client }, inner))
);
}
/**
* Two renderings are the same page when they paint the same styled boxes, in
* the same order, around the same words.
*
* Compared this way rather than byte-for-byte because a migration legitimately
* adds `data-ui-*` identity attributes, and for components that cannot forward
* unknown props a wrapper element carrying nothing else. Both are invisible.
* A changed utility class, a reordered section or altered text moves one of
* these and fails.
*/
export const classSignature = (html) => (html.match(/class="[^"]*"/g) || []).join('\n');
export const wordSignature = (html) => html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
/** Tag counts, so an added element is visible and can be characterised. */
export const tagCounts = (html) => (html.match(/<\/?[a-z][a-z0-9-]*/gi) || [])
.map((t) => t.toLowerCase())
.reduce((acc, t) => ({ ...acc, [t]: (acc[t] || 0) + 1 }), {});
/* Run directly: capture a baseline. */
if (process.argv[1] && process.argv[1].endsWith('render-page.mjs')) {
const [modulePath, out, route] = process.argv.slice(2);
const restore = shimWindow();
const server = await createServer({
root: process.cwd(),
server: { middlewareMode: true },
appType: 'custom',
logLevel: 'error',
resolve: { alias: { 'react-hot-toast': join(process.cwd(), 'scripts/stubs/react-hot-toast.js') } },
});
try {
const html = await renderPage(server, `/${modulePath.replace(/^\//, '')}`, { route: route || '/' });
writeFileSync(out, html);
console.log(`rendered ${html.length} chars -> ${out}`);
} catch (error) {
console.error('RENDER FAILED:', error.message);
process.exitCode = 1;
} finally {
await server.close();
restore();
/* `server.close()` leaves a handle open often enough that the process hangs
without this, and a capture script that never exits is a capture script
nobody runs. */
process.exit(process.exitCode || 0);
}
}