fix: stabilize frontend e2e selectors and auth/session reliability (#373)

This commit is contained in:
Daniel Volz
2026-03-02 23:21:57 +01:00
committed by GitHub
parent 1a348c62f5
commit 56d244aa61
19 changed files with 485 additions and 43 deletions
+4 -4
View File
@@ -65,7 +65,7 @@ test.describe("Dashboard with medications", () => {
test("should show medication overview table with medications", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
await expect(overviewTable.locator(".table-head")).toBeVisible();
@@ -77,7 +77,7 @@ test.describe("Dashboard with medications", () => {
test("should show status chips in overview table", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Each medication row should have a status chip
@@ -88,7 +88,7 @@ test.describe("Dashboard with medications", () => {
test("should show stock information in overview", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// The Ibuprofen row should show stock info (60 pills minus today's usage = 59)
@@ -202,7 +202,7 @@ test.describe("Dashboard with medications", () => {
test("should open medication detail modal from overview table", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const medRow = overviewTable.locator(".table-row").filter({ hasText: MED_1 }).first();
+70 -5
View File
@@ -177,7 +177,9 @@ export { expect };
// ---------------------------------------------------------------------------
const API_BASE = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:5173";
function getAuthCookie(): string | null {
let cachedAuthCookie: string | null = null;
function readAuthCookieFromFile(): string | null {
try {
const state = JSON.parse(fs.readFileSync(authFile, "utf-8"));
return state.cookies?.find((c: { name: string }) => c.name === "access_token")?.value ?? null;
@@ -186,6 +188,49 @@ function getAuthCookie(): string | null {
}
}
function extractCookieValue(setCookieHeaders: string[], name: string): string | null {
for (const header of setCookieHeaders) {
const [pair] = header.split(";");
if (!pair) continue;
const [cookieName, ...valueParts] = pair.split("=");
if (cookieName?.trim() !== name) continue;
const value = valueParts.join("=").trim();
if (value) return value;
}
return null;
}
async function refreshAuthCookieViaLogin(): Promise<string | null> {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: TEST_USER.username,
password: TEST_USER.password,
rememberMe: false,
}),
});
if (!res.ok) return null;
const getSetCookie = (res.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie;
const setCookieHeaders = typeof getSetCookie === "function" ? getSetCookie.call(res.headers) : [];
const fallback = res.headers.get("set-cookie");
if (fallback) setCookieHeaders.push(fallback);
const accessToken = extractCookieValue(setCookieHeaders, "access_token");
if (accessToken) {
cachedAuthCookie = accessToken;
}
return accessToken;
}
function getAuthCookie(): string | null {
if (cachedAuthCookie) return cachedAuthCookie;
cachedAuthCookie = readAuthCookieFromFile();
return cachedAuthCookie;
}
/** Typed medication response (subset of fields we care about) */
export interface TestMedication {
id: number;
@@ -229,7 +274,7 @@ export async function createMedicationViaAPI(data: {
takenBy?: string | null;
}[];
}): Promise<TestMedication> {
const token = getAuthCookie();
let token = getAuthCookie();
const isBottle = data.packageType === "bottle";
const body = {
packageType: isBottle ? "bottle" : "blister",
@@ -261,6 +306,10 @@ export async function createMedicationViaAPI(data: {
},
body: JSON.stringify(body),
});
if (res.status === 401) {
token = await refreshAuthCookieViaLogin();
if (token) continue;
}
if (res.status === 429) {
// Rate limited — exponential backoff: 3s, 6s, 9s, 12s, 15s
await new Promise((r) => setTimeout(r, 3000 * (attempt + 1)));
@@ -280,12 +329,16 @@ export async function createMedicationViaAPI(data: {
* Includes retry for rate-limited responses.
*/
export async function deleteMedicationViaAPI(id: number): Promise<void> {
const token = getAuthCookie();
let token = getAuthCookie();
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(`${API_BASE}/api/medications/${id}`, {
method: "DELETE",
headers: token ? { Cookie: `access_token=${token}` } : {},
});
if (res.status === 401) {
token = await refreshAuthCookieViaLogin();
if (token) continue;
}
if (res.status === 429) {
await new Promise((r) => setTimeout(r, 3000 * (attempt + 1)));
continue;
@@ -299,11 +352,15 @@ export async function deleteMedicationViaAPI(id: number): Promise<void> {
* Includes retry logic for rate-limited responses.
*/
export async function deleteAllMedicationsViaAPI(): Promise<void> {
const token = getAuthCookie();
let token = getAuthCookie();
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(`${API_BASE}/api/medications`, {
headers: token ? { Cookie: `access_token=${token}` } : {},
});
if (res.status === 401) {
token = await refreshAuthCookieViaLogin();
if (token) continue;
}
if (res.status === 429) {
await new Promise((r) => setTimeout(r, 3000 * (attempt + 1)));
continue;
@@ -316,6 +373,10 @@ export async function deleteAllMedicationsViaAPI(): Promise<void> {
method: "DELETE",
headers: token ? { Cookie: `access_token=${token}` } : {},
});
if (delRes.status === 401) {
token = await refreshAuthCookieViaLogin();
if (token) continue;
}
if (delRes.status === 429) {
await new Promise((r) => setTimeout(r, 3000));
continue;
@@ -332,7 +393,7 @@ export async function deleteAllMedicationsViaAPI(): Promise<void> {
* Requires a medication with takenBy to exist first.
*/
export async function createShareTokenViaAPI(takenBy: string, scheduleDays = 30): Promise<TestShareToken> {
const token = getAuthCookie();
let token = getAuthCookie();
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`${API_BASE}/api/share`, {
method: "POST",
@@ -342,6 +403,10 @@ export async function createShareTokenViaAPI(takenBy: string, scheduleDays = 30)
},
body: JSON.stringify({ takenBy, scheduleDays }),
});
if (res.status === 401) {
token = await refreshAuthCookieViaLogin();
if (token) continue;
}
if (res.status === 429) {
await new Promise((r) => setTimeout(r, 3000 * (attempt + 1)));
continue;
+1 -1
View File
@@ -83,7 +83,7 @@ async function fillAndSaveMedication(
await form.getByRole("button", { name: /(Intake|form\.blisters\.addIntake)/i }).click();
}
const row = form.locator(".blister-row").nth(i);
await row.getByLabel(/(Usage \(pills\)|form\.blisters\.usage)/i).fill(intakes[i].usage);
await row.getByLabel(/(Usage \((pills|tablets)\)|form\.blisters\.usage)/i).fill(intakes[i].usage);
await row.getByLabel(/(Every \(days\)|form\.blisters\.everyDays)/i).fill(intakes[i].every);
}
+3 -3
View File
@@ -72,7 +72,7 @@ test.describe("Share Schedule", () => {
test("should show taken-by badges on dashboard overview table", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Alice's medication should show "Alice" badge
@@ -253,7 +253,7 @@ test.describe("Share Schedule", () => {
test("should show notes icon on dashboard for medication with notes", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Alice's med has notes — should show the 📝 icon
@@ -265,7 +265,7 @@ test.describe("Share Schedule", () => {
test("should show notes in medication detail modal", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Click on Alice's med to open detail modal
+10 -10
View File
@@ -125,7 +125,7 @@ test.describe("Stock Status Levels", () => {
test("should show all medications in overview table", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// All 5 medications should appear
@@ -139,7 +139,7 @@ test.describe("Stock Status Levels", () => {
test("should show High status chip for well-stocked medication", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// High stock med row should have a .status-chip.high
@@ -151,7 +151,7 @@ test.describe("Stock Status Levels", () => {
test("should show Normal status chip for moderate stock medication", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const normalRow = overviewTable.locator(".table-row").filter({ hasText: MED_NORMAL });
@@ -162,7 +162,7 @@ test.describe("Stock Status Levels", () => {
test("should show Warning status chip for low stock medication", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const lowRow = overviewTable.locator(".table-row").filter({ hasText: MED_LOW });
@@ -173,7 +173,7 @@ test.describe("Stock Status Levels", () => {
test("should show Danger status chip for critical stock medication", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const criticalRow = overviewTable.locator(".table-row").filter({ hasText: MED_CRITICAL });
@@ -184,7 +184,7 @@ test.describe("Stock Status Levels", () => {
test("should show Danger status chip for depleted medication", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const depletedRow = overviewTable.locator(".table-row").filter({ hasText: MED_DEPLETED });
@@ -195,7 +195,7 @@ test.describe("Stock Status Levels", () => {
test("should show days-left and runs-out date in overview", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// High stock should show many days (around 299)
@@ -227,7 +227,7 @@ test.describe("Stock Status Levels", () => {
test("should color-code stock values depending on status", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// High stock row should have success-text class on stock cells
@@ -255,7 +255,7 @@ test.describe("Stock Status Levels", () => {
test("should open medication detail modal showing stock info", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Click on the critical stock medication row
@@ -278,7 +278,7 @@ test.describe("Stock Status Levels", () => {
test("should show generic name in overview for medications that have one", async ({ page }) => {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
// Click on the normal stock med (has generic name "Ibuprofen 400mg")
+1 -1
View File
@@ -54,7 +54,7 @@ test.describe("MedDetail footer tooltip visibility", () => {
*/
async function openMedDetailModal(page: import("@playwright/test").Page) {
await navigateTo(page, "/dashboard");
const overviewTable = page.locator(".table.table-7");
const overviewTable = page.locator(".dashboard-overview-section .table").first();
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const medRow = overviewTable.locator(".table-row").filter({ hasText: MED_NAME }).first();