/** * Shared browser-test helpers. * * Import this in every `*.integration.test.ts` file. * Provides login, API calling, and page-navigation utilities * that work inside the Vitest browser environment. */ import { expect } from 'vitest'; import { CREDENTIALS } from './fixtures'; import { BASE } from './setup'; // ── Types ─────────────────────────────────────────────────────────── interface ApiResponse { code: number; data: T; message?: string; } type Role = keyof typeof CREDENTIALS; // ── Auth helpers ──────────────────────────────────────────────────── /** * Login as a specific role and store the token in localStorage. * Returns the parsed response data. */ export async function loginAs(role: Role): Promise<{ token: string; user: Record }> { const creds = CREDENTIALS[role]; const res = await fetch(`${BASE}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(creds), }); expect(res.status).toBe(201); const json = (await res.json()) as ApiResponse<{ token: string; user: Record }>; expect(json.code).toBe(0); localStorage.setItem('token', json.data.token); localStorage.setItem('user', JSON.stringify(json.data.user)); return json.data; } /** * Logout: clear localStorage. */ export function logout(): void { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('permissions'); localStorage.removeItem('currentCampusId'); } // ── API helpers (authenticated) ───────────────────────────────────── function authHeaders(): Record { const token = localStorage.getItem('token'); return { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } export async function apiGet(url: string): Promise> { const res = await fetch(`${BASE}${url}`, { headers: authHeaders() }); return (await res.json()) as ApiResponse; } export async function apiPost(url: string, body?: unknown): Promise> { const res = await fetch(`${BASE}${url}`, { method: 'POST', headers: authHeaders(), body: body ? JSON.stringify(body) : undefined, }); return (await res.json()) as ApiResponse; } export async function apiPut(url: string, body?: unknown): Promise> { const res = await fetch(`${BASE}${url}`, { method: 'PUT', headers: authHeaders(), body: body ? JSON.stringify(body) : undefined, }); return (await res.json()) as ApiResponse; } export async function apiDelete(url: string): Promise> { const res = await fetch(`${BASE}${url}`, { method: 'DELETE', headers: authHeaders(), }); return (await res.json()) as ApiResponse; } // ── Page helpers ──────────────────────────────────────────────────── /** * Navigate to a page and wait for it to load. */ export async function goTo(path: string): Promise { document.location.href = `${BASE}${path}`; // Wait for React to render await new Promise((r) => setTimeout(r, 500)); } /** * Assert the current page URL contains the given path. */ export async function assertOnPage(path: string): Promise { // Wait a tick for SPA routing await new Promise((r) => setTimeout(r, 300)); expect(window.location.pathname).toContain(path); } // ── Wait helpers ──────────────────────────────────────────────────── /** Poll until a condition is true or timeout. */ export async function waitFor( condition: () => boolean | Promise, timeout = 5000, interval = 200, ): Promise { const start = Date.now(); while (Date.now() - start < timeout) { if (await condition()) return; await new Promise((r) => setTimeout(r, interval)); } throw new Error(`waitFor timed out after ${timeout}ms`); } // ── Assertion helpers ─────────────────────────────────────────────── /** Assert an API response is successful (code === 0). */ export function assertOk(res: ApiResponse, msg?: string): T { expect(res.code, msg ?? 'API should return code 0').toBe(0); return res.data; } /** Assert an API response is an error (code !== 0). */ export function assertError(res: ApiResponse, expectedCode?: number): void { expect(res.code).not.toBe(0); if (expectedCode !== undefined) { expect(res.code).toBe(expectedCode); } } /** Assert a 403 is returned (permission denied). */ export async function assertForbidden(promise: Promise): Promise { const res = await promise; expect(res.status).toBe(403); } /** Assert a 401 is returned (unauthenticated). */ export async function assertUnauthenticated(promise: Promise): Promise { const res = await promise; expect(res.status).toBe(401); } // ── Sensitive data helpers (PRD §3.3) ─────────────────────────────── /** Assert phone number is masked: 138****0001 */ export function assertPhoneMasked(displayed: string): void { expect(displayed).toMatch(/^\d{3}\*{4}\d{4}$/); } /** Assert ID card is masked: 110101********1234 */ export function assertIdCardMasked(displayed: string): void { expect(displayed).toMatch(/^\d{6}\*{8}\d{4}$/); }