118 lines
4.4 KiB
TypeScript
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');
|
|
});
|
|
}
|