import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import { resolve, extname, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; // Run after npm run build, with an existing Playwright/Chromium installation: // PLAYWRIGHT_MODULE=/path/to/playwright-core/index.mjs CHROME_EXECUTABLE=/path/to/chrome // node tests/pwa.browser.mjs // This serves the real production bundle and service worker on loopback. No live API is used. const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright-core'); const root = fileURLToPath(new URL('../dist/', import.meta.url)); await readFile(resolve(root, 'sw.js')); const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.webmanifest': 'application/manifest+json', '.png': 'image/png', '.ttf': 'font/ttf' }; let sessionMode = 'token'; const server = createServer(async (request, response) => { const path = new URL(request.url, 'http://localhost').pathname; if (path.startsWith('/api/')) { let status = 200; let body; if (sessionMode === 'unavailable') { status = 503; body = { message: 'Service temporarily unavailable' }; } else if (sessionMode !== 'open' && request.headers.authorization !== 'Bearer local-login-fixture') { status = request.headers.authorization ? 403 : 401; body = { message: status === 401 ? 'missing bearer token' : 'invalid bearer token' }; } else { body = { '/api/v1/auth/session': { authenticated: true, mode: sessionMode }, '/api/v1/workspace-items': { items: [] }, '/api/v1/models': { providers: {} }, '/api/v1/chat-tools': { tools: [] }, '/api/v1/active-runs': { chats: [], searches: [] }, }[path] ?? {}; } response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); response.end(JSON.stringify(body)); return; } const file = resolve(root, '.' + (path === '/' ? '/index.html' : path)); if (!file.startsWith(root.endsWith(sep) ? root : root + sep)) { response.writeHead(404); response.end(); return; } try { const bytes = await readFile(file); response.writeHead(200, { 'Content-Type': types[extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-store' }); response.end(bytes); } catch { response.writeHead(404); response.end(); } }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const browser = await chromium.launch({ executablePath: process.env.CHROME_EXECUTABLE || undefined, headless: true, ...(process.env.BROWSER_NO_SANDBOX === '1' ? { args: ['--no-sandbox'] } : {}) }); try { const context = await browser.newContext({ serviceWorkers: 'allow' }); await context.route('https://fonts.googleapis.com/**', route => route.abort()); await context.route('https://fonts.gstatic.com/**', route => route.abort()); const page = await context.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto(`http://127.0.0.1:${server.address().port}/`, { waitUntil: 'domcontentloaded', timeout: 15_000 }); // Activation must settle before a worker-controlled navigation can finish. await page.waitForFunction(() => navigator.serviceWorker.controller?.state === 'activated', undefined, { timeout: 15_000 }); await page.getByPlaceholder('ADMIN_TOKEN').waitFor({ timeout: 5_000 }); assert.equal(await page.getByRole('alert').count(), 0); assert.equal(await page.getByRole('button', { name: 'Continue without token' }).count(), 0); assert.equal(await page.getByText(/If.*ADMIN_TOKEN/).count(), 0); assert.equal(await page.getByRole('button', { name: 'Sign in', exact: true }).isDisabled(), true); await page.getByPlaceholder('ADMIN_TOKEN').fill('incorrect-login-fixture'); assert.equal(await page.getByRole('button', { name: 'Sign in', exact: true }).isEnabled(), true); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.getByRole('alert').waitFor(); assert.equal(await page.getByRole('alert').textContent(), 'Incorrect token. Please try again.'); await page.reload({ waitUntil: 'domcontentloaded', timeout: 10_000 }); await page.getByPlaceholder('ADMIN_TOKEN').waitFor({ timeout: 5_000 }); assert.equal(await page.getByRole('alert').count(), 0); assert.equal(await page.evaluate(() => navigator.serviceWorker.controller?.state), 'activated'); // Invalid saved credentials are a real failure, unlike a first visit without credentials. await page.evaluate(() => localStorage.setItem('sybil_admin_token', 'outdated-login-fixture')); await page.reload({ waitUntil: 'domcontentloaded' }); await page.getByRole('alert').waitFor(); assert.equal(await page.getByRole('alert').textContent(), 'Incorrect token. Please try again.'); await page.getByPlaceholder('ADMIN_TOKEN').fill('local-login-fixture'); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.locator('.workspace-shell').waitFor(); assert.equal(await page.evaluate(() => localStorage.getItem('sybil_admin_token')), 'local-login-fixture'); await page.evaluate(() => localStorage.removeItem('sybil_admin_token')); sessionMode = 'open'; await page.reload({ waitUntil: 'domcontentloaded' }); await page.locator('.workspace-shell').waitFor(); assert.equal(await page.getByPlaceholder('ADMIN_TOKEN').count(), 0); // Suppress only the expected missing-token response, never connectivity/server failures. sessionMode = 'unavailable'; await page.reload({ waitUntil: 'domcontentloaded' }); await page.getByRole('alert').waitFor(); assert.equal(await page.getByRole('alert').textContent(), 'Service temporarily unavailable'); assert.deepEqual(errors, []); console.log('PASS service-worker activation/reload, neutral first sign-in, rejected tokens, valid sign-in, automatic open-mode entry, and visible server failures'); } finally { await browser.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }