/** * Navigation health test for the cPad Control Panel. * * Two operating modes: * * 1. Dynamic — reads tests/.discovered/cpad-links.json (written by * navigation-discovery.spec.ts) and visits every link. * Run the discovery spec first to populate this file. * * 2. Fallback — when the discovery file is absent, falls back to a static * list of known /cp routes so the suite never fails silently. * * For every visited page the test asserts: * • No redirect to /cp/login (i.e. session is still valid) * • HTTP status not 5xx (detected via response interception) * • No uncaught JavaScript exceptions * • No browser console errors * • Page body is visible and non-empty * • Page does not contain Laravel error text * * Run: * npx playwright test tests/cpad/navigation.spec.ts --project=cpad */ import { test, expect } from '@playwright/test'; import { CP_PATH, attachErrorListeners } from '../helpers/auth'; import { hasForm } from '../helpers/formFiller'; import fs from 'fs'; import path from 'path'; // ───────────────────────────────────────────────────────────────────────────── // Link source // ───────────────────────────────────────────────────────────────────────────── const DISCOVERED_FILE = path.join('tests', '.discovered', 'cpad-links.json'); /** Well-known /cp pages used when the discovery file is missing */ const STATIC_FALLBACK_LINKS: string[] = [ '/cp/dashboard', '/cp/configuration', '/cp/config', '/cp/language/app', '/cp/language/system', '/cp/translation/app', '/cp/security/access', '/cp/security/roles', '/cp/security/permissions', '/cp/security/login', '/cp/plugins', '/cp/user/profile', '/cp/messages', '/cp/api/keys', '/cp/friendly-url', ]; function loadLinks(): string[] { if (fs.existsSync(DISCOVERED_FILE)) { try { const links: string[] = JSON.parse(fs.readFileSync(DISCOVERED_FILE, 'utf8')); if (links.length > 0) return links; } catch { /* fall through to static list */ } } console.warn('[navigation] discovery file not found — using static fallback list'); return STATIC_FALLBACK_LINKS; } const CP_LINKS = loadLinks(); // ───────────────────────────────────────────────────────────────────────────── // Main navigation suite // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Navigation Health', () => { for (const linkPath of CP_LINKS) { test(`page loads: ${linkPath}`, async ({ page }) => { const { consoleErrors, networkErrors } = attachErrorListeners(page); await page.goto(linkPath); await page.waitForLoadState('networkidle', { timeout: 20_000 }); // ── Auth check ────────────────────────────────────────────────────── expect(page.url(), `${linkPath} should not redirect to login`).not.toContain('/login'); // ── HTTP errors ───────────────────────────────────────────────────── expect(networkErrors.length, `HTTP 5xx on ${linkPath}: ${networkErrors.join(' | ')}`).toBe(0); // ── Body visibility ───────────────────────────────────────────────── await expect(page.locator('body')).toBeVisible(); const bodyText = await page.locator('body').textContent() ?? ''; expect(bodyText.trim().length, `Body should not be empty on ${linkPath}`).toBeGreaterThan(0); // ── Laravel / server error page ────────────────────────────────────── const hasServerError = /Whoops[,!]|Server Error|Call to undefined function|SQLSTATE/.test(bodyText); expect(hasServerError, `Server/Laravel error page at ${linkPath}: ${bodyText.slice(0, 200)}`).toBe(false); // ── JS exceptions ──────────────────────────────────────────────────── expect(consoleErrors.length, `JS console errors on ${linkPath}: ${consoleErrors.join(' | ')}`).toBe(0); // ── Form detection (informational — logged, not asserted) ───────────── const pageHasForm = await hasForm(page); if (pageHasForm) { console.log(` [form] form detected on ${linkPath}`); } }); } });