geocrop-platform./apps/nextgen/client/e2e/helpers/auth.ts

118 lines
4.4 KiB
TypeScript

/**
* E2E auth helpers.
*
* Strategy: API-login + localStorage injection. Faster and more reliable
* than driving the Login form, and decouples route-coverage tests from
* form-rendering flakiness. The Login form itself is exercised by the
* existing manual / smoke flows.
*
* Zustand's `persist` middleware stores auth state under the
* `auth-storage` key in localStorage. The shape is:
* { state: { token, user, offline }, version: 0 }
*
* Source of truth for demo credentials: server/src/database/init.js
* (kept in sync with client/src/pages/Login.tsx's demoLogin() map).
*/
import { Page, request as pwRequest } from '@playwright/test';
export type PortalRole =
| 'school_admin'
| 'systems_admin'
| 'principal'
| 'hr'
| 'bursar'
| 'accountant'
| 'nurse'
| 'dining_staff'
| 'teacher'
| 'librarian'
| 'clubs_head'
| 'student'
| 'parent'
| 'driver'
| 'groundsman'
| 'matron'
| 'boarding_master'
| 'security'
| 'janitor';
export const DEMO_ACCOUNTS: Record<PortalRole, { email: string; password: string }> = {
systems_admin: { email: 'sysadmin@school.com', password: 'admin123' },
school_admin: { email: 'admin@school.com', password: 'admin123' },
principal: { email: 'principal@school.com', password: 'principal123' },
hr: { email: 'hr@school.com', password: 'hr123' },
bursar: { email: 'bursar@school.com', password: 'bursar123' },
accountant: { email: 'bursar@school.com', password: 'bursar123' }, // no seed; borrow bursar
nurse: { email: 'nurse@school.com', password: 'nurse123' },
dining_staff: { email: 'dining@school.com', password: 'dining123' },
teacher: { email: 'teacher@school.com', password: 'teacher123' },
librarian: { email: 'librarian@school.com', password: 'librarian123' },
clubs_head: { email: 'clubs_head@school.com', password: 'clubs123' },
student: { email: 'student@school.com', password: 'student123' },
parent: { email: 'parent@school.com', password: 'parent123' },
driver: { email: 'driver@school.com', password: 'driver123' },
groundsman: { email: 'groundsman@school.com', password: 'grounds123' },
matron: { email: 'matron@school.com', password: 'matron123' },
boarding_master: { email: 'boarding@school.com', password: 'boarding123' },
security: { email: 'security@school.com', password: 'security123' },
janitor: { email: 'janitor@school.com', password: 'janitor123' },
};
const API = 'http://localhost:3001';
export async function apiLogin(role: PortalRole): Promise<{ token: string; user: any }> {
const account = DEMO_ACCOUNTS[role];
if (!account) throw new Error(`No demo account for role ${role}`);
const ctx = await pwRequest.newContext({ baseURL: API });
try {
const res = await ctx.post('/api/auth/login', { data: account });
if (res.status() !== 200) {
throw new Error(`Login failed for ${role} (${account.email}): ${res.status()} ${await res.text()}`);
}
const body = await res.json();
return { token: body.token, user: body.user };
} finally {
await ctx.dispose();
}
}
/**
* Probe a demo account to see if it actually exists in the seed.
* Used by specs to skip cleanly when the seed is minimal — the
* helper itself doesn't fail, only the spec does.
*/
export async function isRoleAvailable(role: PortalRole): Promise<boolean> {
try {
await apiLogin(role);
return true;
} catch {
return false;
}
}
/**
* Log `role` in by injecting the auth state into localStorage and
* reloading the app. The baseURL is read from the Playwright config
* (http://localhost:3000).
*
* The `?e2e=1` flag on the first navigation is honoured by AppLayout to
* suppress notification permission requests and other side effects that
* would otherwise interrupt the test run.
*/
export async function loginAs(page: Page, role: PortalRole): Promise<void> {
const { token, user } = await apiLogin(role);
await page.addInitScript(({ token, user }) => {
const payload = { state: { token, user, offline: false }, version: 0 };
window.localStorage.setItem('auth-storage', JSON.stringify(payload));
// Suppress Notification.requestPermission() in AppLayout's effect.
// The shim is a no-op so global notification polling becomes inert.
(window as any).__E2E__ = true;
}, { token, user });
}
export async function logout(page: Page): Promise<void> {
await page.evaluate(() => {
window.localStorage.removeItem('auth-storage');
});
}