import type { Page } from "@playwright/test"; import { authFile, createMedicationViaAPI, deleteAllMedicationsViaAPI, deleteMedicationViaAPI, expect, navigateTo, type TestMedication, test, } from "./fixtures"; /** * Medication CRUD E2E Tests * * Tests creating, editing, and deleting medications via the UI form. * Each test cleans up after itself to avoid side effects. */ /** * Helper: fill the medication form and save. Waits for the successful * API response and verifies the medication appears in the list. */ async function fillAndSaveMedication( page: Page, opts: { name: string; genericName?: string; packageType?: "blister" | "bottle" | "tube" | "liquid_container" | "inhaler" | "injection"; packs?: string; blistersPerPack?: string; pillsPerBlister?: string; loosePills?: string; totalCapacity?: string; currentPills?: string; expiryDate?: string; notes?: string; intakes?: { usage: string; every: string }[]; } ): Promise { const openCreateBtn = page.getByRole("button", { name: /New medication|New entry|form\.newEntry/i }).first(); if (await openCreateBtn.isVisible().catch(() => false)) { await openCreateBtn.click(); } const form = page.locator("form.form-grid:visible").first(); await expect(form.getByLabel(/(Commercial Name|form\.commercialName)/i)).toBeVisible({ timeout: 10000 }); await form.getByLabel(/(Commercial Name|form\.commercialName)/i).fill(opts.name); if (opts.genericName) { await form.getByLabel(/(Generic Name|form\.genericName)/i).fill(opts.genericName); } const packageTypeSelect = form.locator("select.package-type-select"); if (opts.packageType === "bottle" || opts.packageType === "inhaler" || opts.packageType === "injection") { await packageTypeSelect.selectOption(opts.packageType ?? "bottle"); await page.getByRole("tab", { name: /Package/i }).click(); if (opts.totalCapacity) await form .getByLabel(/(Total Capacity|form\.totalCapacity|Total \(pills\)|Total \(count\)|form\.totalCount)/i) .fill(opts.totalCapacity); if (opts.currentPills) await form .getByLabel(/(Current Pills|form\.currentPills|Current Stock|form\.currentStockCount)/i) .fill(opts.currentPills); } else if (opts.packageType === "tube") { await packageTypeSelect.selectOption("tube"); await page.getByRole("tab", { name: /Package/i }).click(); if (opts.totalCapacity) { await form.getByLabel(/(Amount per tube|form\.packageAmountPerTube)/i).fill(opts.totalCapacity); } } else if (opts.packageType === "liquid_container") { await packageTypeSelect.selectOption("liquid_container"); await page.getByRole("tab", { name: /Package/i }).click(); if (opts.totalCapacity) { await form.getByLabel(/(Package amount|form\.packageAmount)/i).fill(opts.totalCapacity); } } else { await packageTypeSelect.selectOption("blister"); await page.getByRole("tab", { name: /Package/i }).click(); if (opts.packs) await form.getByLabel(/(^Packs$|form\.packs)/i).fill(opts.packs); if (opts.blistersPerPack) await form.getByLabel(/(Blisters per pack|form\.blistersPerPack)/i).fill(opts.blistersPerPack); if (opts.pillsPerBlister) await form.getByLabel(/(Pills per blister|form\.pillsPerBlister)/i).fill(opts.pillsPerBlister); if (opts.loosePills) { const looseField = form.getByLabel(/(Loose pills|form\.loosePills)/i); if (await looseField.isVisible().catch(() => false)) { await looseField.fill(opts.loosePills); } } } if (opts.expiryDate) await form.getByLabel(/(Expiry Date|form\.expiryDate)/i).fill(opts.expiryDate); if (opts.notes) await form.getByLabel(/(Notes|form\.notes)/i).fill(opts.notes); // Fill intake schedules const intakes = opts.intakes ?? [{ usage: "1", every: "1" }]; await page.getByRole("tab", { name: /Schedule/i }).click(); for (let i = 0; i < intakes.length; i++) { if (i > 0) { await form.getByRole("button", { name: /(Intake|form\.blisters\.addIntake)/i }).click(); } const row = form.locator(".blister-row").nth(i); const usageField = row.getByRole("textbox", { name: /(Usage|Tablets|Capsules|Applications|Puffs|Injections|Ml|form\.blisters\.usage|common\.(puffs|injections))/i, }); const everyField = row.getByLabel(/(Every \(days\)|form\.blisters\.everyDays)/i); await usageField.fill(intakes[i].usage); await everyField.fill(intakes[i].every); } await page.waitForLoadState("networkidle"); await form.locator("button[type='submit']").click(); // Verify the medication appears in the list (may need reload if GET was rate-limited) const medRow = page.locator(".med-row").filter({ hasText: opts.name }); try { await expect(medRow).toBeVisible({ timeout: 5000 }); } catch { await page.reload(); await page.waitForLoadState("networkidle"); await expect(medRow).toBeVisible({ timeout: 10000 }); } } /** * Helper: save after editing (PUT) and wait for success. */ async function saveEdit(page: Page, medName: string): Promise { const form = page.locator("form.form-grid:visible").first(); await page.waitForLoadState("networkidle"); const submitBtn = form.locator("button[type='submit']"); if ( (await submitBtn.count()) > 0 && (await submitBtn .first() .isVisible() .catch(() => false)) ) { await submitBtn.first().click(); } else { const closeBtn = form.getByRole("button", { name: /Close|Cancel/i }).first(); if (await closeBtn.isVisible().catch(() => false)) { await closeBtn.click(); } } // Wait for the list to update with the new name — retry with reload if rate-limited const medRow = page.locator(".med-row").filter({ hasText: medName }); try { await expect(medRow).toBeVisible({ timeout: 15000 }); } catch { await page.reload(); await page.waitForLoadState("networkidle"); await expect(medRow).toBeVisible({ timeout: 10000 }); } } test.describe("Medication CRUD", () => { test.use({ storageState: authFile }); // Clean up any leftover medications before and after all tests test.beforeAll(async () => { await deleteAllMedicationsViaAPI(); }); test.afterAll(async () => { await deleteAllMedicationsViaAPI(); }); test.describe("Create medication", () => { // Clean up after each create test to avoid state leakage to later test blocks test.afterEach(async () => { await deleteAllMedicationsViaAPI(); }); test("should create a blister-pack medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Ibuprofen", genericName: "Ibuprofen", packageType: "blister", packs: "2", blistersPerPack: "3", pillsPerBlister: "10", loosePills: "5", }); // Verify medication details in the list const medRow = page.locator(".med-row").filter({ hasText: "Test Ibuprofen" }); await expect(medRow.locator(".med-name")).toContainText("Test Ibuprofen"); }); test("should create a bottle medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Vitamin D Drops", packageType: "bottle", totalCapacity: "60", currentPills: "45", }); }); test("should create an inhaler medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Rescue Inhaler", packageType: "inhaler", totalCapacity: "200", currentPills: "120", intakes: [{ usage: "2", every: "1" }], }); const medRow = page.locator(".med-row").filter({ hasText: "Test Rescue Inhaler" }); await expect(medRow.locator(".med-details")).toContainText(/Inhaler|form\.packageTypeInhaler/i); await expect(medRow.locator(".med-total")).toContainText("120 / 200"); }); test("should create an injection medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Weekly Injection", packageType: "injection", totalCapacity: "12", currentPills: "4", intakes: [{ usage: "1", every: "7" }], }); const medRow = page.locator(".med-row").filter({ hasText: "Test Weekly Injection" }); await expect(medRow.locator(".med-details")).toContainText(/Injection|form\.packageTypeInjection/i); await expect(medRow.locator(".med-total")).toContainText("4 / 12"); }); test("should create medication with multiple intake schedules", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Multi-Intake Med", packs: "1", blistersPerPack: "2", pillsPerBlister: "14", intakes: [ { usage: "1", every: "1" }, { usage: "0.5", every: "7" }, ], }); }); test("should create a tube medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Tube Cream", packageType: "tube", totalCapacity: "50", }); }); test("should create a liquid-container medication via the form", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Liquid Syrup", packageType: "liquid_container", totalCapacity: "120", }); }); test("should create medication with notes and expiry date", async ({ page }) => { await navigateTo(page, "/medications"); const expiryDate = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]; await fillAndSaveMedication(page, { name: "Test Aspirin", packs: "1", blistersPerPack: "1", pillsPerBlister: "20", expiryDate, notes: "Take with food. Do not exceed 3 per day.", }); }); test("should not save with empty commercial name", async ({ page }) => { await navigateTo(page, "/medications"); await page .getByRole("button", { name: /New medication|New entry|form\.newEntry/i }) .first() .click(); // Saving without name should not create a medication row. const saveBtn = page.locator("form.form-grid button[type='submit']"); await expect(saveBtn).toBeVisible(); await saveBtn.click(); await expect(page.locator(".med-row")).toHaveCount(0); }); test("should reset form after saving a medication", async ({ page }) => { await navigateTo(page, "/medications"); await fillAndSaveMedication(page, { name: "Test Reset Check", packs: "1", blistersPerPack: "1", pillsPerBlister: "10", }); // Opening a fresh form after save should start with an empty commercial name. await page .getByRole("button", { name: /New medication|New entry|form\.newEntry/i }) .first() .click(); await expect(page.getByLabel(/(Commercial Name|form\.commercialName)/i)).toHaveValue(""); }); }); test.describe("Edit medication", () => { test.describe.configure({ timeout: 60000 }); const createdMeds: TestMedication[] = []; test.afterEach(async () => { for (const med of createdMeds) { await deleteMedicationViaAPI(med.id); } createdMeds.length = 0; }); test("should edit an existing medication", async ({ page }) => { // Create prerequisite via API (faster, no rate-limit issues) createdMeds.push(await createMedicationViaAPI({ name: "Before Edit" })); await navigateTo(page, "/medications"); // Click Edit const medRow = page.locator(".med-row").filter({ hasText: "Before Edit" }); await expect(medRow).toBeVisible({ timeout: 10000 }); await medRow.locator("button.info").click(); // Form title should say "Edit entry" (or legacy "Edit medication"). await expect( page.locator("h2").filter({ hasText: /(Edit(:| (entry|medication))|form\.editEntry)/i }) ).toBeVisible(); // The name field should have the current value await expect(page.getByLabel(/(Commercial Name|form\.commercialName)/i)).toHaveValue("Before Edit"); // Change the name await page.getByLabel(/(Commercial Name|form\.commercialName)/i).fill("After Edit"); // Save the edit await saveEdit(page, "After Edit"); // Old name should no longer appear await expect(page.locator(".med-row").filter({ hasText: "Before Edit" })).not.toBeVisible(); // Update tracked ID for cleanup createdMeds[0].name = "After Edit"; }); test("should cancel editing and discard changes", async ({ page }) => { createdMeds.push(await createMedicationViaAPI({ name: "Cancel Test Med" })); await navigateTo(page, "/medications"); // Click Edit const medRow = page.locator(".med-row").filter({ hasText: "Cancel Test Med" }); await expect(medRow).toBeVisible({ timeout: 10000 }); await medRow.locator("button.info").click(); // Change the name await page.getByLabel(/(Commercial Name|form\.commercialName)/i).fill("Modified Name"); // Click Cancel await page .getByRole("button", { name: /Close|Cancel/i }) .first() .click(); // Original name should still be in the list await expect(page.locator(".med-row").filter({ hasText: "Cancel Test Med" })).toBeVisible(); }); }); test.describe("Delete medication", () => { test.describe.configure({ timeout: 60000 }); const createdMeds: TestMedication[] = []; test.afterEach(async () => { for (const med of createdMeds) { await deleteMedicationViaAPI(med.id); } createdMeds.length = 0; }); test("should delete a medication after confirming", async ({ page }) => { createdMeds.push(await createMedicationViaAPI({ name: "Delete Me Med" })); await navigateTo(page, "/medications"); const medRow = page.locator(".med-row").filter({ hasText: "Delete Me Med" }); await expect(medRow).toBeVisible({ timeout: 10000 }); await medRow.locator("button.danger").click(); await page .locator(".confirm-modal-overlay, .modal-overlay") .getByRole("button", { name: /Delete/i }) .click(); // Medication should be removed await expect(medRow).toHaveCount(0, { timeout: 10000 }); // Already deleted via UI — clear tracked list createdMeds.length = 0; }); test("should not delete when confirm dialog is dismissed", async ({ page }) => { createdMeds.push(await createMedicationViaAPI({ name: "Keep Me Med" })); await navigateTo(page, "/medications"); const medRow = page.locator(".med-row").filter({ hasText: "Keep Me Med" }); await expect(medRow).toBeVisible({ timeout: 10000 }); // Dismiss the native confirm() page.on("dialog", (dialog) => dialog.dismiss()); await medRow.locator("button.danger").click(); // Medication should still be there await expect(medRow).toBeVisible(); }); }); test.describe("Medication list", () => { test.describe.configure({ timeout: 60000 }); const createdMeds: TestMedication[] = []; test.afterEach(async () => { for (const med of createdMeds) { await deleteMedicationViaAPI(med.id); } createdMeds.length = 0; }); test("should display multiple medications in the list", async ({ page }) => { createdMeds.push(await createMedicationViaAPI({ name: "Med Alpha" })); createdMeds.push( await createMedicationViaAPI({ name: "Med Beta", packCount: 2, blistersPerPack: 2, pillsPerBlister: 14, intakes: [ { usage: 2, every: 1, start: new Date().toISOString().slice(0, 16), intakeRemindersEnabled: false }, ], }) ); await navigateTo(page, "/medications"); // Both medications should be in the list await expect(page.locator(".med-row").filter({ hasText: "Med Alpha" })).toBeVisible({ timeout: 10000 }); await expect(page.locator(".med-row").filter({ hasText: "Med Beta" })).toBeVisible(); expect(await page.locator(".med-row").count()).toBeGreaterThanOrEqual(2); }); test("should show stock details on medication row", async ({ page }) => { createdMeds.push( await createMedicationViaAPI({ name: "Stock Detail Med", packCount: 3, blistersPerPack: 2, pillsPerBlister: 10, looseTablets: 3, }) ); await navigateTo(page, "/medications"); const medRow = page.locator(".med-row").filter({ hasText: "Stock Detail Med" }); try { await expect(medRow).toBeVisible({ timeout: 10000 }); } catch { // Reload in case the list didn't include the newly created med await page.reload(); await page.waitForLoadState("networkidle"); await expect(medRow).toBeVisible({ timeout: 10000 }); } // Should display stock details const medDetails = medRow.locator(".med-details, .med-total"); expect(await medDetails.count()).toBeGreaterThan(0); }); }); test.describe("Intake schedule management", () => { test("should add and remove intake schedule rows", async ({ page }) => { await navigateTo(page, "/medications"); await page .getByRole("button", { name: /New medication|New entry|form\.newEntry/i }) .first() .click(); await page.getByRole("tab", { name: /Schedule/i }).click(); const form = page.locator("form.form-grid:visible").first(); expect(await form.locator(".blister-row").count()).toBe(1); await form.getByRole("button", { name: /(Intake|form\.blisters\.addIntake)/i }).click(); expect(await form.locator(".blister-row").count()).toBe(2); await form.getByRole("button", { name: /(Intake|form\.blisters\.addIntake)/i }).click(); expect(await form.locator(".blister-row").count()).toBe(3); const removeBtn = page .locator("form.form-grid:visible .blister-row") .last() .getByRole("button", { name: /Remove/i }); await removeBtn.click(); expect(await form.locator(".blister-row").count()).toBe(2); }); }); });