/** * The live acceptance suite: the Owliver UI-editing journey, in a real browser. * * npm run test:browser * * Unit and SSR tests render these same modules and were not enough — every * defect this guards was found in a browser and missed by a green test run. So * this drives the real application: real routing, real React, real network, * real `preferences.uiLayouts`. * * **It never logs in.** The session is an HttpOnly cookie and this script has * no business handling anybody's credentials, so it attaches to a browser that * is *already* signed in and says so plainly when it cannot: * * KROW_E2E_BROWSER_URL=http://127.0.0.1:9222 attach to a running Chrome * (start one with: --remote-debugging-port=9222) * * KROW_E2E_USER_DATA_DIR=/path/to/profile launch Chrome on a profile * that has been signed in once by hand * * With neither set, or with the browser not signed in, it exits **BLOCKED** — * never a silent pass. An acceptance suite that reports green because it never * ran is worse than no suite. * * The journey spans reloads, so the phases live in `browser-flows.js` and the * reloads live here. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import puppeteer from 'puppeteer-core'; const ROOT = process.cwd(); const BASE = process.env.KROW_E2E_BASE_URL || 'http://localhost:5173'; const FLOWS = readFileSync(join(ROOT, 'scripts/browser-flows.js'), 'utf8'); const CHROME = process.env.KROW_E2E_CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; /** * The pages under test, as data. * * `target` is a node the page really composes and `phrase` is how a person * names it — the pair this suite drives every page with. Adding a page is a row. */ const PAGES = [ { page: 'activity', route: '/admin/activity', target: 'audit', phrase: 'the audit log' }, { page: 'hired-history', route: '/admin/hired', target: 'chronology', phrase: 'the recent hiring timeline' }, { page: 'analytics', route: '/admin/analytics', target: 'funnel', phrase: 'the hiring funnel' }, { page: 'positions', route: '/admin/positions', target: 'skill-board-board', phrase: 'the Board card', /* The Board skill's own card: a *skill* node, so the same journey proves built-in and definition-contributed UI move through one engine. Needs the Board definition switched on, which `prepare` below does not do — the page is skipped rather than failed when its node is not there. */ optional: true, data: ['What is on the board?', 'Show me the board activity'], ordinaryQuestion: 'How do I apply for this position?', }, ]; const results = []; const record = (name, pass, detail) => results.push({ name, pass, detail: detail || '' }); async function connect() { if (process.env.KROW_E2E_BROWSER_URL) { return puppeteer.connect({ browserURL: process.env.KROW_E2E_BROWSER_URL, defaultViewport: null }); } if (process.env.KROW_E2E_USER_DATA_DIR) { return puppeteer.launch({ executablePath: CHROME, userDataDir: process.env.KROW_E2E_USER_DATA_DIR, headless: false, defaultViewport: null, }); } return null; } /** Load a route, install the flows, and wait for the page to have composed. */ async function open(page, route) { await page.goto(`${BASE}${route}`, { waitUntil: 'networkidle2', timeout: 45_000 }); await page.evaluate(FLOWS); await page.waitForFunction( () => document.querySelector('textarea') && document.querySelectorAll('[data-ui-node]').length >= 0, { timeout: 30_000 } ); await new Promise((r) => setTimeout(r, 2500)); } const run = (page, fn, args) => page.evaluate( async (name, a) => window.__uiFlows[name](a), fn, args ); async function main() { const browser = await connect(); if (!browser) { console.error( 'BLOCKED — no browser to attach to.\n' + ' This suite does not log in: the session is an HttpOnly cookie and this\n' + ' script does not handle credentials. Point it at a signed-in browser:\n\n' + ' KROW_E2E_BROWSER_URL=http://127.0.0.1:9222 npm run test:browser\n' + ' (start Chrome with --remote-debugging-port=9222)\n\n' + ' KROW_E2E_USER_DATA_DIR=/path/to/profile npm run test:browser\n' ); process.exit(2); } const page = await browser.newPage(); try { await open(page, PAGES[0].route); /* Signed in, or nothing below means anything. */ const authed = await page.evaluate(async () => { const r = await fetch('/api/v1/me/preferences', { credentials: 'include' }); return { status: r.status, composer: Boolean(document.querySelector('textarea')) }; }); if (authed.status !== 200 || !authed.composer) { console.error( `BLOCKED — that browser is not signed in (GET /me/preferences → ${authed.status}).\n` + ' Sign in once in that profile, then run this again.' ); process.exit(2); } for (const spec of PAGES) { await open(page, spec.route); const present = await page.evaluate((t) => window.__uiFlows.nodes().includes(t), spec.target); if (!present) { if (spec.optional) { record(`${spec.page}: SKIPPED — ${spec.target} is not on the page`, true, 'the definition contributing it is switched off'); continue; } record(`${spec.page}: ${spec.target} is on the page`, false, 'not composed'); continue; } results.push(...await run(page, 'nothingPreviewed', { page: spec.page, ordinaryQuestion: spec.ordinaryQuestion || null, })); if (spec.data) results.push(...await run(page, 'dataStaysData', { page: spec.page, questions: spec.data })); results.push(...await run(page, 'phase1', spec)); await open(page, spec.route); results.push(...await run(page, 'phase2', spec)); await open(page, spec.route); results.push(...await run(page, 'phase3', spec)); } } finally { if (process.env.KROW_E2E_BROWSER_URL) browser.disconnect(); else await browser.close(); } const failed = results.filter((r) => !r.pass); for (const r of results) { console.log(`[ ${r.pass ? ' ok ' : 'FAIL'} ] ${r.name}${r.detail ? ` — ${r.detail}` : ''}`); } console.log(`\n${results.length - failed.length}/${results.length} browser checks passed`); if (failed.length) { console.log('\nFailed:'); for (const r of failed) console.log(` - ${r.name} (${r.detail})`); process.exit(1); } } main().catch((error) => { console.error('BROWSER SUITE ERROR:', error); process.exit(1); });