/** * Authentication tests for the cPad Control Panel. * * Coverage: * • Login page renders correctly * • Login with valid credentials → redirected to /cp/dashboard (or /cp/) * • Login with invalid credentials → error message shown, no redirect * • Logout → session is destroyed, login page is shown * * These tests do NOT use the pre-saved storageState; they exercise the actual * login/logout flow from scratch. * * Run: * npx playwright test tests/cpad/auth.spec.ts --project=chromium */ import { test, expect } from '@playwright/test'; import { ADMIN_EMAIL, ADMIN_PASSWORD, CP_PATH, DASHBOARD_PATH, attachErrorListeners, } from '../helpers/auth'; // ───────────────────────────────────────────────────────────────────────────── // Login page // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Login Page', () => { test('login page loads and shows email + password fields', async ({ page }) => { const { consoleErrors, networkErrors } = attachErrorListeners(page); await page.goto(CP_PATH + '/login'); // Basic page health await expect(page.locator('body')).toBeVisible(); const title = await page.title(); expect(title.length, 'Page title should not be empty').toBeGreaterThan(0); // Form fields const emailField = page.locator('input[name="email"], input[type="email"]').first(); const passwordField = page.locator('input[name="password"], input[type="password"]').first(); const submitButton = page.locator('button[type="submit"], input[type="submit"]').first(); await expect(emailField).toBeVisible(); await expect(passwordField).toBeVisible(); await expect(submitButton).toBeVisible(); // No JS crashes on page load expect(consoleErrors.length, `Console errors: ${consoleErrors.join(' | ')}`).toBe(0); expect(networkErrors.length, `Network errors: ${networkErrors.join(' | ')}`).toBe(0); }); }); // ───────────────────────────────────────────────────────────────────────────── // Successful login // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Successful Login', () => { test('admin can log in and is redirected to dashboard', async ({ page }) => { const { consoleErrors, networkErrors } = attachErrorListeners(page); await page.goto(CP_PATH + '/login'); await page.locator('input[name="email"], input[type="email"]').first().fill(ADMIN_EMAIL); await page.locator('input[name="password"], input[type="password"]').first().fill(ADMIN_PASSWORD); await page.locator('button[type="submit"], input[type="submit"]').first().click(); // After successful login the URL should be within /cp and not be /login await page.waitForURL( (url) => url.pathname.startsWith(CP_PATH) && !url.pathname.endsWith('/login'), { timeout: 20_000 }, ); await expect(page.locator('body')).toBeVisible(); // No server errors const body = await page.locator('body').textContent() ?? ''; expect(/Whoops|Server Error|500/.test(body), 'Server error page after login').toBe(false); expect(networkErrors.length, `HTTP 5xx errors: ${networkErrors.join(' | ')}`).toBe(0); }); }); // ───────────────────────────────────────────────────────────────────────────── // Failed login // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Failed Login', () => { test('wrong password shows error and stays on login page', async ({ page }) => { const { networkErrors } = attachErrorListeners(page); await page.goto(CP_PATH + '/login'); await page.locator('input[name="email"], input[type="email"]').first().fill(ADMIN_EMAIL); await page.locator('input[name="password"], input[type="password"]').first().fill('WrongPassword999!'); await page.locator('button[type="submit"], input[type="submit"]').first().click(); // Should stay on the login page await page.waitForLoadState('networkidle'); expect(page.url()).toContain('/login'); // No 5xx errors from a bad credentials attempt expect(networkErrors.length, `HTTP 5xx errors: ${networkErrors.join(' | ')}`).toBe(0); }); test('empty credentials show validation errors', async ({ page }) => { await page.goto(CP_PATH + '/login'); // Submit without filling in anything await page.locator('button[type="submit"], input[type="submit"]').first().click(); await page.waitForLoadState('networkidle'); // Still on login page expect(page.url()).toContain(CP_PATH); }); }); // ───────────────────────────────────────────────────────────────────────────── // Logout // ───────────────────────────────────────────────────────────────────────────── test.describe('cPad Logout', () => { test('logout destroys session and shows login page', async ({ page }) => { // Log in first await page.goto(CP_PATH + '/login'); await page.locator('input[name="email"], input[type="email"]').first().fill(ADMIN_EMAIL); await page.locator('input[name="password"], input[type="password"]').first().fill(ADMIN_PASSWORD); await page.locator('button[type="submit"], input[type="submit"]').first().click(); await page.waitForURL( (url) => url.pathname.startsWith(CP_PATH) && !url.pathname.endsWith('/login'), { timeout: 20_000 }, ); // Perform logout via the /cp/logout route await page.goto(CP_PATH + '/logout'); await page.waitForLoadState('networkidle'); // Should land on the login page again expect(page.url()).toContain('/login'); // Attempting to access dashboard now should redirect back to login await page.goto(DASHBOARD_PATH); await page.waitForLoadState('networkidle'); expect(page.url()).toContain('/login'); }); });