/** * Navigation Discovery — scans the cPad sidebar and collects all /cp links. * * This test acts as both: * 1. A standalone spec that validates the nav scan returns ≥ 1 link. * 2. A data producer: it writes the discovered URLs to * tests/.discovered/cpad-links.json so that navigation.spec.ts can * consume them dynamically. * * Ignored links: * • /cp/logout * • javascript:void / # anchors * • External URLs (not starting with /cp) * * Run: * npx playwright test tests/cpad/navigation-discovery.spec.ts --project=cpad */ import { test, expect } from '@playwright/test'; import { DASHBOARD_PATH, CP_PATH } from '../helpers/auth'; import fs from 'fs'; import path from 'path'; const DISCOVERED_DIR = path.join('tests', '.discovered'); const DISCOVERED_FILE = path.join(DISCOVERED_DIR, 'cpad-links.json'); // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── /** Returns true if the href should be included in navigation tests */ function isNavigableLink(href: string | null): boolean { if (!href) return false; if (!href.startsWith(CP_PATH)) return false; if (href.includes('/logout')) return false; if (href.startsWith('javascript:')) return false; if (href === CP_PATH || href === CP_PATH + '/') return false; // exclude root (same as dashboard) return true; } /** Remove query strings and anchors for clean deduplication */ function normalise(href: string): string { try { const u = new URL(href, 'http://placeholder'); return u.pathname; } catch { return href.split('?')[0].split('#')[0]; } } // ───────────────────────────────────────────────────────────────────────────── // Discovery test // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Navigation Discovery', () => { test('scan sidebar and collect all /cp navigation links', async ({ page }) => { await page.goto(DASHBOARD_PATH); await page.waitForLoadState('networkidle'); // Ensure we're not on the login page expect(page.url()).not.toContain('/login'); // ── Widen the scan to cover lazy-loaded submenu items ────────────────── // Hover over sidebar nav items to expand hidden submenus const menuItems = page.locator('.nav-item, .sidebar-item, .menu-item, li.nav-item'); const menuCount = await menuItems.count(); for (let i = 0; i < Math.min(menuCount, 30); i++) { await menuItems.nth(i).hover().catch(() => null); } // ── Collect all anchor hrefs ─────────────────────────────────────────── const rawHrefs: string[] = await page.$$eval('a[href]', (anchors) => anchors.map((a) => (a as HTMLAnchorElement).getAttribute('href') ?? ''), ); const discovered = [...new Set( rawHrefs .map((h) => normalise(h)) .filter(isNavigableLink), )].sort(); console.log(`[discovery] Found ${discovered.length} navigable /cp links`); discovered.forEach((l) => console.log(' •', l)); // Must find at least a few links — if not, something is wrong with auth expect(discovered.length, 'Expected to find /cp navigation links').toBeGreaterThanOrEqual(1); // ── Persist for navigation.spec.ts ──────────────────────────────────── if (!fs.existsSync(DISCOVERED_DIR)) { fs.mkdirSync(DISCOVERED_DIR, { recursive: true }); } fs.writeFileSync(DISCOVERED_FILE, JSON.stringify(discovered, null, 2), 'utf8'); console.log(`[discovery] Links saved to ${DISCOVERED_FILE}`); }); });