/** * Render smoke test — mounts every route (all app routes + 4 auth) into jsdom * and fails on any thrown error, console.error, or empty render. * * npm run smoke * * Bundled with esbuild (not Vite's SSR loader) because several dependencies * ship CJS and esbuild's interop handles that cleanly. The render itself lives * in src/__smoke__/entry.jsx so it exercises the real component tree. */ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' import esbuild from 'esbuild' import { JSDOM } from 'jsdom' // ---------------------------------------------------------------- environment const dom = new JSDOM('
', { url: 'http://localhost:5173/', pretendToBeVisual: true, }) globalThis.window = dom.window globalThis.document = dom.window.document // Node 24 defines `navigator` as a getter-only global. Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true }) globalThis.HTMLElement = dom.window.HTMLElement globalThis.Element = dom.window.Element globalThis.Node = dom.window.Node globalThis.getComputedStyle = dom.window.getComputedStyle globalThis.localStorage = dom.window.localStorage globalThis.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 0) globalThis.cancelAnimationFrame = clearTimeout globalThis.IS_REACT_ACT_ENVIRONMENT = true class RO { observe() {} unobserve() {} disconnect() {} } class MO { observe() {} disconnect() {} takeRecords() { return [] } } globalThis.ResizeObserver = RO globalThis.MutationObserver = MO dom.window.ResizeObserver = RO dom.window.MutationObserver = MO dom.window.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, }) // jsdom has no canvas backend; the retained chart engine only needs a context object. dom.window.HTMLCanvasElement.prototype.getContext = () => new Proxy({}, { get: (_, k) => (k === 'canvas' ? {} : () => {}) }) // A signed-in session holding every permission tag, so no route is gated away. const MODULES = ['dashboard', 'inbox', 'jobs', 'candidates', 'pipeline', 'interviews', 'assessments', 'offers', 'reports', 'analytics', 'job_board', 'settings', 'rbac_users', 'tasks', 'talent'] const ACTIONS = ['view', 'create', 'edit', 'delete', 'approve', 'export', 'manage', 'configure'] const permissions = MODULES.flatMap((m) => ACTIONS.map((a) => `${m}.${a}`)) dom.window.localStorage.setItem('tf-auth', JSON.stringify({ access_token: 'test', refresh_token: 'test', expires_in: 1800, expires_at: Date.now() + 1800_000, data: { id: 1, name: 'Test User', email: 't@example.com', role_name: 'system_administrator', permissions }, })) // The real-API screens must not hit the network here. globalThis.fetch = async () => ({ ok: true, status: 200, statusText: 'OK', text: async () => JSON.stringify({ data: [], status_code: 200 }), }) // ---------------------------------------------------------------- bundle const outDir = mkdtempSync(join(tmpdir(), 'tf-smoke-')) const outFile = join(outDir, 'entry.mjs') await esbuild.build({ entryPoints: ['src/__smoke__/entry.jsx'], outfile: outFile, bundle: true, format: 'esm', platform: 'node', target: 'node20', jsx: 'automatic', loader: { '.js': 'jsx', '.jsx': 'jsx' }, logLevel: 'error', define: { 'process.env.NODE_ENV': '"development"', 'import.meta.env': JSON.stringify({ DEV: true, PROD: false, MODE: 'development', VITE_API_BASE: '' }), }, }) // ---------------------------------------------------------------- run const errors = [] const origError = console.error console.error = (...args) => { const msg = args.map((a) => (a instanceof Error ? a.stack : String(a))).join(' ') if (msg.includes('React Router Future Flag')) return // advisory, not a defect errors.push(msg) } let failed = 0 let total = 0 try { const mod = await import(pathToFileURL(outFile).href) mod.boot() total = mod.ALL_ROUTES.length for (const path of mod.ALL_ROUTES) { errors.length = 0 const container = dom.window.document.createElement('div') dom.window.document.body.appendChild(container) try { const text = (await mod.renderRoute(path, container)).trim() if (errors.length) { console.log(`FAIL ${path}\n ${errors[0].split('\n').slice(0, 3).join(' | ').slice(0, 260)}`) failed++ } else if (text.length < 5) { console.log(`FAIL ${path} (rendered empty)`) failed++ } else { console.log(`ok ${path} (${text.length} chars)`) } } catch (err) { console.log(`FAIL ${path}\n ${String(err.message).split('\n')[0].slice(0, 260)}`) failed++ } finally { container.remove() } } } finally { console.error = origError rmSync(outDir, { recursive: true, force: true }) } console.log(failed ? `\n${failed}/${total} routes FAILED` : `\nAll ${total} routes rendered clean`) process.exit(failed ? 1 : 0)