Files
HealthLog/e2e/setup/mobile-routes-fixture.ts
T
Marc-André Bombeck 567fd8d51c test(e2e): sweep every signed-in route for sideways scroll at 390px
The lab list panned sideways on a phone while the overflow guard was
green, because /labs was not on its hand-kept list. The new sweep reads
the routes from src/app: static routes are visited as is, every dynamic
route needs a resolver to real paths, and routes that are not signed-in
surfaces are listed with their reason. A route added without either
fails the bookkeeping test by name.

It runs on an account of its own, filled once from globalSetup with one
or two of everything a route renders, in German, so the narrow-column
and long-string cases are present. Two lab tests assert at the elements
that broke: the marker name keeps a readable width, and the values
list's actions stay on screen. The dialog spec gains a 360px width,
where the labs sheet's three-button footer overflowed in English too.
2026-10-01 09:45:15 +02:00

205 lines
6.6 KiB
TypeScript

/**
* The record `mobile-route-overflow.spec.ts` measures.
*
* A phone-width overflow guard that visits an empty page proves nothing: an
* empty list fits any viewport, and the lab list that panned sideways on a
* phone only did so once it held a reading. So the account gets one or two of
* everything a signed-in route can render — readings with reference ranges, a
* medication, a mood entry, a visit, a vaccination, an illness episode, a
* custom metric, a workout, a document — and every dynamic route has a real id
* to open.
*
* Written through the app's own routes, once, from `globalSetup`, so the
* parallel workers of the spec never race each other to seed it. An account
* that already holds a lab reading is taken as seeded, which keeps a local
* re-run against a reused database from doubling every row.
*/
import { request as playwrightRequest } from "@playwright/test";
/** Long on purpose: the names are what a narrow column has to give way to. */
export const MOBILE_ROUTES_ANALYTE = "Glomeruläre Filtrationsrate (eGFR)";
export async function seedMobileRoutesRecord(
baseURL: string,
storageStatePath: string,
): Promise<void> {
const ctx = await playwrightRequest.newContext({
baseURL,
storageState: storageStatePath,
});
try {
// The document is the last thing written, so its presence means a
// complete record. A run that died half-way is finished by the next one:
// a row it already wrote answers 409 and is skipped.
const existing = await ctx.get("/api/documents/inbound");
const existingBody = (await existing.json()) as {
data: { documents?: unknown[] } | unknown[] | null;
};
const documents = Array.isArray(existingBody.data)
? existingBody.data
: (existingBody.data?.documents ?? []);
if (documents.length > 0) return;
const post = async (path: string, data: unknown): Promise<unknown> => {
const res = await ctx.post(path, { data });
if (res.status() === 409) return null;
if (!res.ok()) {
throw new Error(
`[mobile-routes-fixture] ${path} answered ${res.status()}: ${(await res.text()).slice(0, 200)}`,
);
}
return ((await res.json()) as { data: unknown }).data;
};
const daysAgo = (days: number, hour = 8) => {
const at = new Date(Date.now() - days * 86_400_000);
at.setUTCHours(hour, 0, 0, 0);
return at.toISOString();
};
for (let day = 1; day <= 6; day += 1) {
await post("/api/measurements", {
type: "WEIGHT",
value: 80 + day / 10,
measuredAt: daysAgo(day, 7),
});
await post("/api/measurements", {
type: "BLOOD_PRESSURE_SYS",
value: 120 + day,
measuredAt: daysAgo(day, 7),
});
await post("/api/measurements", {
type: "BLOOD_PRESSURE_DIA",
value: 78 + (day % 3),
measuredAt: daysAgo(day, 7),
});
await post("/api/measurements", {
type: "PULSE",
value: 60 + day,
measuredAt: daysAgo(day, 7),
});
await post("/api/measurements", {
type: "BLOOD_GLUCOSE",
value: 92 + day,
glucoseContext: "FASTING",
measuredAt: daysAgo(day, 6),
});
await post("/api/mood-entries", {
mood: day % 2 ? "GUT" : "OKAY",
moodLoggedAt: daysAgo(day, 20),
});
}
// Two readings of one marker, so the list row has a trend line and a
// range bar, plus a qualitative one with neither.
for (const [days, value] of [
[40, 78],
[5, 84],
] as const) {
await post("/api/labs", {
analyte: MOBILE_ROUTES_ANALYTE,
panel: "Nierenwerte und Elektrolyte",
value,
unit: "mL/min/1.73m²",
referenceLow: 60,
referenceHigh: 120,
takenAt: daysAgo(days),
});
}
await post("/api/labs", {
analyte: "Hepatitis-B-Oberflächenantigen",
valueText: "negativ",
takenAt: daysAgo(5),
});
await post("/api/medications", {
name: "Ramipril",
dose: "5 mg",
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
});
const practitioner = (await post("/api/practitioners", {
name: "Gemeinschaftspraxis für Allgemeinmedizin",
specialty: "Allgemeinmedizin und Innere Medizin",
})) as { id: string } | null;
await post("/api/encounters", {
occurredAt: daysAgo(20),
practitionerId: practitioner?.id ?? null,
reason: "Routinekontrolle Blutdruck und Blutbild",
});
await post("/api/vaccinations", {
occurredAt: daysAgo(300),
antigenSlug: "tetanus",
doseNumber: 1,
lotNumber: "AB1234-XY",
practitionerId: practitioner?.id ?? null,
});
await post("/api/illness/episodes", {
label: "Erkältung mit Husten",
type: "INFECTION",
onsetAt: daysAgo(12),
resolvedAt: daysAgo(6),
});
const metric = (await post("/api/custom-metrics", {
name: "Augeninnendruck rechts",
unit: "mmHg",
targetLow: 10,
targetHigh: 21,
decimals: 0,
})) as { id: string } | null;
const metricId =
metric?.id ??
(
(await (await ctx.get("/api/custom-metrics")).json()) as {
data: { customMetrics: Array<{ id: string }> };
}
).data.customMetrics[0]?.id;
await post(`/api/custom-metrics/${metricId}/entries`, {
value: 16,
measuredAt: daysAgo(3),
});
await post("/api/workouts/batch", {
workouts: [
{
sportType: "running",
startedAt: daysAgo(2, 6),
endedAt: new Date(
new Date(daysAgo(2, 6)).getTime() + 45 * 60_000,
).toISOString(),
source: "APPLE_HEALTH",
externalId: "e2e-mobile-routes-run",
activeEnergyKcal: 420,
distanceM: 6500,
},
],
});
const pdf =
"%PDF-1.4\n%e2e-mobile-routes\n" +
"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]>>endobj\n" +
"xref\n0 4\ntrailer<</Size 4/Root 1 0 R>>\n%%EOF\n";
const doc = await ctx.post("/api/documents/inbound", {
multipart: {
file: {
name: "laborbefund-hausarzt-mit-langem-dateinamen.pdf",
mimeType: "application/pdf",
buffer: Buffer.from(pdf),
},
title: "Laborbefund Hausarzt mit einem sehr langen Titel",
kind: "LAB_RESULT",
},
});
if (!doc.ok()) {
throw new Error(
`[mobile-routes-fixture] /api/documents/inbound answered ${doc.status()}`,
);
}
} finally {
await ctx.dispose();
}
}