Files
Metamorph/e2e/tests/m0-smoke.spec.ts

128 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-05-11 06:05:27 +02:00
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();
});
});