/** * Focused integration tests for the browser questionnaire server. * Run with: node --test tests/picker-server.test.mjs */ import assert from 'node:assert/strict'; import { execFileSync, spawn } from 'node:child_process'; import { once } from 'node:events'; import { existsSync } from 'node:fs'; import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import http from 'node:http'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { before, test } from 'node:test'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const serverScript = path.join(root, 'skill/scripts/picker-server.mjs'); const paletteScript = path.join(root, 'skill/scripts/palette.mjs'); const colorModule = pathToFileURL(path.join(root, 'picker/scripts/color.js')).href; const pickerIndex = path.join(root, 'skill/scripts/picker/index.html'); const portBase = 18_500 + (process.pid % 500); const cueManifestFixture = { cues: ['hero-01'], palette: { 'hero-01': { primary: { hex: '#1E4A42', snapped: '#1F4B42', at: [168, 252] }, secondary: { hex: '#8C7251', snapped: '#8D7352', at: [424, 318] }, tertiary: { hex: '#D8A82F', snapped: '#D7A930', at: [702, 190] }, neutral: { hex: '#F2EFE8', snapped: '#F1EEE7', at: [86, 94] }, }, }, }; before(() => { if (existsSync(pickerIndex)) return; execFileSync(process.execPath, [path.join(root, 'scripts/build-picker.mjs')], { cwd: root, stdio: 'inherit', }); }); async function createFixture(register = null) { const cwd = await realpath(await mkdtemp(path.join(tmpdir(), 'impeccable-picker-'))); const cuesDir = path.join(cwd, '.impeccable/visual-cues'); await mkdir(cuesDir, { recursive: true }); await writeFile(path.join(cuesDir, 'hero-01.png'), Buffer.from('fake-png')); await writeFile( path.join(cuesDir, 'cues.json'), `${JSON.stringify(cueManifestFixture)}\n`, ); if (register) { await writeFile(path.join(cwd, 'PRODUCT.md'), `# Product\n\n## Register\n\n${register}\n`); } return { cwd, cuesDir }; } async function startPicker(cwd, args = []) { const processHandle = spawn(process.execPath, [serverScript, ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'], }); processHandle.stdout.setEncoding('utf8'); processHandle.stderr.setEncoding('utf8'); let stdout = ''; let stderr = ''; let settled = false; let resolveUrl; let rejectUrl; const urlPromise = new Promise((resolve, reject) => { resolveUrl = resolve; rejectUrl = reject; }); const timer = setTimeout(() => { if (!settled) rejectUrl(new Error(`Server start timeout. stdout=${stdout} stderr=${stderr}`)); }, 5000); processHandle.stdout.on('data', (chunk) => { stdout += chunk; const firstLine = stdout.split(/\r?\n/)[0]; if (!settled && firstLine.startsWith('PICKER_URL ')) { settled = true; clearTimeout(timer); resolveUrl({ firstLine, url: firstLine.slice('PICKER_URL '.length) }); } }); processHandle.stderr.on('data', (chunk) => { stderr += chunk; }); processHandle.once('error', (error) => { if (!settled) rejectUrl(error); }); processHandle.once('exit', (code) => { if (!settled) rejectUrl(new Error(`Server exited ${code}. stdout=${stdout} stderr=${stderr}`)); }); const started = await urlPromise; return { ...started, processHandle, stdout: () => stdout, stderr: () => stderr, }; } async function waitForExit(processHandle) { if (processHandle.exitCode !== null) return [processHandle.exitCode, processHandle.signalCode]; return once(processHandle, 'exit'); } async function cleanup(t, fixture, server) { t.after(async () => { if (server?.processHandle.exitCode === null) server.processHandle.kill('SIGTERM'); await rm(fixture.cwd, { recursive: true, force: true }); }); } function rawGet(baseUrl, requestPath) { const url = new URL(baseUrl); return new Promise((resolve, reject) => { const request = http.get({ hostname: url.hostname, port: url.port, path: requestPath, }, (response) => { response.resume(); response.once('end', () => resolve(response.statusCode)); }); request.once('error', reject); }); } test('serves picker and cues, writes submission, prints answers, and exits 0', async (t) => { const fixture = await createFixture(); const server = await startPicker(fixture.cwd, ['--port', String(portBase)]); await cleanup(t, fixture, server); assert.equal(server.firstLine, `PICKER_URL ${server.url}`); assert.match(server.url, /^http:\/\/127\.0\.0\.1:\d+$/); const pageResponse = await fetch(`${server.url}/`); assert.equal(pageResponse.status, 200); assert.match(pageResponse.headers.get('content-type'), /^text\/html/); const pageHtml = await pageResponse.text(); assert.match(pageHtml, /data-copy-url/); assert.match(pageHtml, /Open this link in your browser at least 1200px wide/); assert.match(pageHtml, /data-copy-url-value aria-label="Picker URL"><\/code>/); assert.match(pageHtml, /aria-label="Copy link"/); assert.match(pageHtml, />Start