Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / e2e-smoke (pull_request) Failing after 1m31s
CI/CD / test-runners (pull_request) Failing after 2m40s
CI/CD / test-go-pg (pull_request) Successful in 3m17s
CI/CD / test-sqlite (pull_request) Successful in 3m27s
CI/CD / build-and-deploy (pull_request) Has been skipped
Playwright's addCookies (both domain-based and url-based) fails to send SameSite=Strict cookies in Docker DNS environments. The server middleware reads arm_token from cookies, but the cookie is originally set by client-side JS (document.cookie with SameSite=Strict). New approach: navigate to /login first, then inject the token into localStorage (key: arm_auth) and set the cookie via document.cookie in page context — exactly how the real login flow works. This ensures the cookie attributes match what the server expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
JavaScript
147 lines
4.7 KiB
JavaScript
#!/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);
|
|
}
|
|
})();
|