Files
F-r-ldraledigplannerare/frontend/e2e/support/api.ts
T
jacobh2 ed5680253e Check in a browser what the unit tests cannot see
Every serious defect in this project was found by opening the app, most
recently a feature that rendered blank cards to every user while its tests
passed, and a progress bar labelled in the wrong unit. Five checks now run
without a person: that the age-four figures on screen match what the API
computed, that a multi-week period draws as one bar rather than fragments,
that Sundays and holidays are red where Saturdays are not, that no identifier
leaks into visible text, and that no page logs an error.

Three of them were confirmed by reintroducing the original bug and watching
the right test fail. Dates are derived rather than written down: Easter is
computed independently instead of asked of the system under test, and the
fixture period is placed relative to today, so the suite does not expire.

It runs as its own job and does not block a merge until it has proved itself
stable.
2026-09-17 19:46:17 +02:00

121 lines
3.7 KiB
TypeScript

/**
* Minimal, dependency-free HTTP helpers for talking to the backend directly
* (bypassing the browser) during E2E setup and ground-truth assertions.
*
* Deliberately hits the backend at 127.0.0.1:8000, not through the Vite
* proxy: these calls run in plain Node (global-setup) or alongside a
* Playwright test (spec files), never inside a browser page, so there is no
* CORS concern and no need for the proxy or the httpOnly refresh cookie —
* just a Bearer access token obtained via `apiLogin`.
*/
export const API_BASE = 'http://127.0.0.1:8000'
// Seeded by `backend/scripts/seed_demo.py` — see that file's module
// docstring. Not a secret: dev-only, fixed, printed by the script itself.
export const DEMO_EMAIL = 'demo@example.com'
export const DEMO_PASSWORD = 'DemoPass123!'
export async function waitForBackendHealth(timeoutMs = 60_000): Promise<void> {
const healthUrl = `${API_BASE}/health`
const deadline = Date.now() + timeoutMs
let lastError: unknown = null
while (Date.now() < deadline) {
try {
const res = await fetch(healthUrl)
if (res.ok) return
} catch (err) {
lastError = err
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
throw new Error(`Backend never reported healthy at ${healthUrl}: ${String(lastError)}`)
}
async function parseJsonOrThrow<T>(res: Response, what: string): Promise<T> {
if (!res.ok) {
throw new Error(`${what} failed: ${res.status} ${await res.text()}`)
}
return (await res.json()) as T
}
interface LoginResponse {
access_token: string
}
export async function apiLogin(email: string, password: string): Promise<string> {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await parseJsonOrThrow<LoginResponse>(res, 'login')
return data.access_token
}
export interface ApiChild {
id: string
name: string
birth_date: string
}
export interface ApiUserContext {
personal_data_uuid: string
workspace_id: string
parent_name: string
children: ApiChild[]
}
export async function apiGetContext(token: string): Promise<ApiUserContext> {
const res = await fetch(`${API_BASE}/api/auth/context`, {
headers: { Authorization: `Bearer ${token}` },
})
return parseJsonOrThrow<ApiUserContext>(res, 'get user context')
}
export type PlannedDayType = 'sjukpenning' | 'lagsta' | 'vacation'
export interface PlannedDayPayload {
workspace_id: string
child_id: string
parent_personal_data_uuid: string
date: string
day_type: PlannedDayType
is_dubbeldag: boolean
}
export async function apiCreatePlannedDay(token: string, payload: PlannedDayPayload): Promise<void> {
const res = await fetch(`${API_BASE}/api/planned-days`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
})
if (!res.ok) {
throw new Error(`create planned day ${payload.date} failed: ${res.status} ${await res.text()}`)
}
}
export interface ApiAge4Projection {
personal_data_uuid: string
savable_after_b4: number
expiring_at_b4: number
projected_left_at_b4: number
savable_cap_at_b4: number
}
export interface ApiWorkspaceOverview {
parent_projections: ApiAge4Projection[]
combined_expiring_at_b4: number
combined_days_left: number
}
export async function apiGetOverview(
token: string,
workspaceId: string,
childId: string,
): Promise<ApiWorkspaceOverview> {
const url = `${API_BASE}/api/analytics/${workspaceId}/overview?child_id=${childId}`
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } })
return parseJsonOrThrow<ApiWorkspaceOverview>(res, 'get workspace overview')
}