import { test, expect, type Request } from '@playwright/test'; /** * M0 — Bootstrap smoke checks. * Validates what M0 actually delivers: * 1. The 3-container stack is reachable (front + api proxy). * 2. The home page renders the RTOps design system primitives. * 3. Self-hosted webfonts (no Google Fonts CDN — spec §7). * 4. No JS console errors on first load. * 5. API health endpoint returns the expected JSON. */ test.describe('M0 — bootstrap smoke', () => { const consoleErrors: string[] = []; const externalRequests: string[] = []; test.beforeEach(({ page }) => { consoleErrors.length = 0; externalRequests.length = 0; page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`)); page.on('request', (req: Request) => { const url = req.url(); if ( url.includes('fonts.googleapis.com') || url.includes('fonts.gstatic.com') || url.includes('cdn.jsdelivr.net') || url.includes('unpkg.com') ) { externalRequests.push(url); } }); }); test('home page loads and renders the RTOps header', async ({ page }) => { const resp = await page.goto('/'); expect(resp?.status(), 'home page should respond 200').toBe(200); await expect(page).toHaveTitle(/Metamorph/); const h1 = page.getByRole('heading', { level: 1 }); await expect(h1).toContainText('Metamorph'); await expect(h1).toContainText('Purple Team Platform'); }); test('API health card eventually shows OK', async ({ page }) => { await page.goto('/'); // The "API" card binds the health probe; wait for the green-accent state. const apiCard = page.locator('h3', { hasText: /^API$/ }).locator('..'); await expect(apiCard).toContainText(/version\s+\d+/i, { timeout: 10_000 }); await expect(apiCard).toContainText('ok'); }); test('design system primitives render with the expected accent classes', async ({ page }) => { await page.goto('/'); // Tags from the demo row. await expect(page.getByText('EVASION', { exact: true })).toBeVisible(); await expect(page.getByText('C2', { exact: true })).toBeVisible(); await expect(page.getByText('LATERAL', { exact: true })).toBeVisible(); // Flow nodes. await expect(page.getByText('recon', { exact: true })).toBeVisible(); await expect(page.getByText('impact', { exact: true })).toBeVisible(); // Buttons. await expect(page.getByRole('button', { name: /primary/i })).toBeVisible(); }); test('body uses self-hosted IBM Plex Sans, no Google Fonts requests', async ({ page }) => { await page.goto('/'); // Wait for fonts to settle. await page.evaluate(() => document.fonts.ready); const bodyFont = await page.evaluate(() => window.getComputedStyle(document.body).fontFamily.toLowerCase(), ); expect(bodyFont).toContain('ibm plex sans'); // Header is mono. const h1Font = await page.evaluate(() => { const h1 = document.querySelector('h1'); return h1 ? window.getComputedStyle(h1).fontFamily.toLowerCase() : ''; }); expect(h1Font).toContain('jetbrains mono'); // No request must hit Google Fonts or any other CDN — see spec §7. expect(externalRequests, `unexpected CDN traffic: ${externalRequests.join(', ')}`).toEqual([]); }); test('background uses the RTOps deep navy token', async ({ page }) => { await page.goto('/'); const bg = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor); // tasks/design.md: --bg = #0a0e1a → rgb(10, 14, 26) expect(bg).toBe('rgb(10, 14, 26)'); }); test('no JS console errors on first load', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); // The auth provider attempts a silent /auth/refresh at mount; without a // refresh cookie the server returns 401 and the browser logs a generic // "Failed to load resource" warning. That's expected for unauthenticated // visitors and doesn't constitute a real error. const realErrors = consoleErrors.filter( (e) => !/Failed to load resource.*401/i.test(e), ); expect(realErrors, `console errors: ${realErrors.join(' | ')}`).toEqual([]); }); test('API health endpoint returns the expected JSON shape', async ({ request }) => { const resp = await request.get('/api/v1/health'); expect(resp.status()).toBe(200); const body = (await resp.json()) as { status: string; version: string }; expect(body.status).toBe('ok'); expect(body.version).toMatch(/^\d+\.\d+\.\d+/); }); test('CORS headers are set when the SPA origin asks for them', async ({ request }) => { const resp = await request.get('/api/v1/health', { headers: { Origin: 'http://localhost:8080' }, }); expect(resp.status()).toBe(200); // flask-cors echoes back the configured origin when allowed. const allowed = resp.headers()['access-control-allow-origin']; expect(allowed === 'http://localhost:8080' || allowed === '*').toBeTruthy(); }); });