150 lines
6.9 KiB
TypeScript
150 lines
6.9 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|