Files
HealthLog/e2e/setup/setup-flow-fixture.ts
T
Marc-André Bombeck 2686da2803 Add the setup flow's browser journeys, axe sweep and string-headroom check
Four journeys, one account each, from the welcome screen to the
dashboard: blood pressure and weight typed in, a daily medication added
inline, a visit within a month with labs, and a child's managed profile
created on the confirm screen. Each asserts what the walk wrote — the
module map, the tile order, the checklist, the settled ledger — and the
first proves that leaving after Q3 resumes at Q4.

Two sweeps on accounts of their own drive the state through the answers
route and visit every screen by URL: axe at both widths plus one
keyboard-only answer, and every locale with every text node lengthened
by 30% before the page, the shell, every clipping box and every control
are measured. All six run in one project because each mutates the
account it walks.

The medication wizard learns to stay where it was mounted after a
create, which the first-result screen needs and the medications list
does not.
2026-09-10 15:57:41 +02:00

119 lines
3.9 KiB
TypeScript

/**
* v1.39 (C2) — the state the setup-flow journeys start from, and nothing else.
*
* Each journey owns one account and walks it from the welcome screen to the
* dashboard. What a walk leaves behind — the answers row, the completion
* stamp, the module map, the seeded dashboard, the reading or medication the
* first-result step produced, a managed profile — is exactly what the next
* run would then find already done, so every journey resets its own account
* before it starts. Seeded through `pg`, the same transport `global-setup.ts`
* and the other fixtures use, because there is no product endpoint that
* un-completes an account.
*
* Idempotent, and scoped to the one account it is given.
*/
import pg from "pg";
function connect(): pg.Pool {
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error("[setup-flow-fixture] DATABASE_URL is not set");
}
return new pg.Pool({ connectionString: url });
}
/**
* Put one account back before its first run: no answers, no completion, no
* module map, no dashboard seed, no rows the first-result step could have
* written, and a clean write bucket for the answers route.
*/
export async function resetSetupFlow(username: string): Promise<void> {
const pool = connect();
try {
const { rows } = await pool.query<{ id: string }>(
`SELECT id FROM users WHERE username = $1`,
[username],
);
const id = rows[0]?.id;
if (!id) {
throw new Error(`[setup-flow-fixture] no account named ${username}`);
}
await pool.query(`DELETE FROM onboarding_records WHERE user_id = $1`, [id]);
await pool.query(
`UPDATE users
SET onboarding_completed_at = NULL,
module_preferences_json = NULL,
dashboard_widgets_json = NULL,
glucose_unit = NULL,
unit_preference = NULL
WHERE id = $1`,
[id],
);
await pool.query(`DELETE FROM measurements WHERE user_id = $1`, [id]);
await pool.query(`DELETE FROM medications WHERE user_id = $1`, [id]);
await pool.query(`DELETE FROM encounters WHERE user_id = $1`, [id]);
await pool.query(
`DELETE FROM rate_limits
WHERE key IN (
'onboarding-answers:' || $1,
'onboarding-restart:' || $1,
'managed-profile:create:' || $1
)`,
[id],
);
} finally {
await pool.end();
}
}
/**
* Remove every managed profile this account looks after. A managed profile
* is a real account row, so a journey that created one and left it behind
* would find the guardian looking after two records on the next run.
*/
export async function clearManagedProfilesOf(username: string): Promise<void> {
const pool = connect();
try {
await pool.query(
`DELETE FROM users
WHERE managed_profile_at IS NOT NULL
AND id IN (
SELECT g.grantor_id FROM account_grants g
JOIN users u ON u.id = g.grantee_id
WHERE u.username = $1
)`,
[username],
);
} finally {
await pool.end();
}
}
/**
* Make the account's step-up fresh, exactly as `managed-profile-fixture.ts`
* does for the guardian: creating a managed profile resolves
* `requireFreshMfa`, which needs a confirmed factor on the account and a
* session stamp newer than five minutes. Not a bypass — the route still runs
* its gate; this is the state in which the surface is usable at all.
*/
export async function stampFreshMfa(username: string): Promise<void> {
const pool = connect();
try {
const stamped = await pool.query(
`UPDATE sessions SET mfa_verified_at = NOW()
WHERE user_id = (SELECT id FROM users WHERE username = $1)`,
[username],
);
if ((stamped.rowCount ?? 0) === 0) {
throw new Error(`[setup-flow-fixture] no session for ${username}`);
}
await pool.query(
`UPDATE users SET totp_confirmed_at = COALESCE(totp_confirmed_at, NOW())
WHERE username = $1`,
[username],
);
} finally {
await pool.end();
}
}