#!/usr/bin/env node /** * Surface Test Driver — Playwright-based CI runner * * Navigates to /s/test-runners as admin, clicks "Run All", * waits for completion, then fetches results from the API. * * Usage: * node ci/surface-test-driver.js --server=http://localhost:3000 --token=TOKEN * * Exit codes: 0 = all passed, 1 = failures */ 'use strict'; const { chromium } = require('playwright'); // Parse args const args = {}; process.argv.slice(2).forEach(a => { const [k, v] = a.replace(/^--/, '').split('='); args[k] = v; }); const SERVER = args.server || 'http://localhost:3000'; const TOKEN = args.token || ''; if (!TOKEN) { console.error('ERROR: --token is required'); process.exit(1); } (async () => { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage(); // Authenticate via page context — inject tokens into localStorage // and set the arm_token cookie via JS. Playwright's addCookies // doesn't reliably work with SameSite=Strict cookies in Docker. console.log('Setting up auth...'); await page.goto(SERVER + '/login', { waitUntil: 'networkidle', timeout: 30000 }); await page.evaluate((token) => { localStorage.setItem('arm_auth', JSON.stringify({ accessToken: token, refreshToken: token, user: null, cookieMaxAge: 604800, })); document.cookie = `arm_token=${token}; path=/; max-age=604800; SameSite=Strict`; }, TOKEN); // Collect console errors const consoleErrors = []; page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); try { // Navigate to test runners surface console.log('Navigating to test-runners surface...'); await page.goto(SERVER + '/s/test-runners', { waitUntil: 'networkidle', timeout: 30000 }); // Wait for the "Run All" button — suites load asynchronously so the // button only appears once runners have registered their suites. console.log('Waiting for Run All button...'); const runAllBtn = await page.waitForSelector('button:has-text("Run All")', { timeout: 60000 }) .catch(() => null); if (!runAllBtn) { // Dump page content for debugging const text = await page.textContent('body').catch(() => '(empty)'); console.error('ERROR: "Run All" button not found. Page text:', text.substring(0, 500)); await browser.close(); process.exit(1); } console.log('Clicking Run All...'); await runAllBtn.click(); // Wait for results — poll until running state clears console.log('Waiting for test completion...'); // The running indicator disappears when tests finish // Max wait: 5 minutes const maxWait = 300000; const start = Date.now(); let done = false; while (!done && (Date.now() - start) < maxWait) { await page.waitForTimeout(2000); // Check if results are available via API try { const resp = await page.evaluate(async (serverUrl) => { const r = await fetch(serverUrl + '/api/v1/admin/test-runners/results', { credentials: 'include' }); if (r.status === 200) return await r.json(); return null; }, SERVER); if (resp && resp.summary) { done = true; console.log('\n═══ Results ═══'); console.log(` Total: ${resp.summary.total}`); console.log(` Passed: ${resp.summary.passed}`); console.log(` Failed: ${resp.summary.failed}`); console.log(` Warned: ${resp.summary.warned}`); console.log(` Skipped: ${resp.summary.skipped}`); console.log(` Duration: ${resp.duration_ms}ms`); if (resp.summary.failed > 0) { console.log('\n═══ Failures ═══'); for (const suite of (resp.suites || [])) { for (const test of (suite.tests || [])) { if (test.status === 'failed') { console.log(` ✗ ${suite.name} > ${test.name}: ${test.detail || 'failed'}`); } } } await browser.close(); process.exit(1); } await browser.close(); process.exit(0); } } catch (e) { // Results not ready yet } } if (!done) { console.error('ERROR: Tests did not complete within timeout'); await browser.close(); process.exit(1); } } catch (e) { console.error('Driver error:', e.message); if (consoleErrors.length > 0) { console.error('\nBrowser console errors:'); consoleErrors.forEach(e => console.error(' ' + e)); } await browser.close(); process.exit(1); } })();