Files
medassist-ng/frontend/e2e/medication-edit.spec.ts
T
Daniel Volz 2f5fc2d9e9 fix: stabilize medication Playwright gate
* fix: stabilize medication Playwright gate

* fix: satisfy medication Playwright frontend gate
2026-05-15 20:20:18 +02:00

536 lines
20 KiB
TypeScript

import type { Page } from "@playwright/test";
import {
authFile,
createMedicationViaAPI,
deleteAllMedicationsViaAPI,
expect,
navigateTo,
type TestMedication,
test,
} from "./fixtures";
/**
* Medication Edit E2E Tests
*
* Tests editing medications: changing fields, adding notes, taken-by persons,
* generic name, refill stock, intake reminders, and intake schedule changes.
* Each test creates a medication via API, edits it via the UI, and verifies the change.
*/
/** Helper: click Edit button on a medication row */
async function clickEditMed(page: Page, medName: string): Promise<void> {
const medRow = page.locator(".med-row").filter({ hasText: medName });
for (let attempt = 0; attempt < 3; attempt++) {
if (await medRow.isVisible().catch(() => false)) break;
await page.reload();
await page.waitForLoadState("networkidle");
await page.waitForTimeout(1000);
}
await expect(medRow).toBeVisible({ timeout: 10000 });
await medRow.locator("button.info").click();
await expect(page.locator("h2").filter({ hasText: /(Edit(:| (entry|medication))|form\.editEntry)/i })).toBeVisible({
timeout: 5000,
});
}
async function openMedicationDetailFromDashboard(page: Page, medName: string) {
const overviewTable = page.locator(".dashboard-overview-section .table").first();
for (let attempt = 0; attempt < 3; attempt++) {
try {
await expect(overviewTable).toBeVisible({ timeout: 10000 });
const medRow = overviewTable.locator(".table-row").filter({ hasText: medName });
await expect(medRow).toBeVisible({ timeout: 10000 });
await medRow.click();
const modal = page.locator(".modal-content.med-detail-modal");
await expect(modal).toBeVisible({ timeout: 5000 });
await expect(modal.getByText(medName)).toBeVisible({ timeout: 5000 });
return modal;
} catch {
if (attempt === 2) throw new Error(`Failed to open dashboard medication detail for ${medName}`);
await page.reload();
await page.waitForLoadState("networkidle");
}
}
throw new Error(`Failed to open dashboard medication detail for ${medName}`);
}
/** Helper: save edit and verify success */
async function saveEditAndVerify(page: Page, medName: string): Promise<void> {
const form = page.locator("form.form-grid:visible").first();
// Wait for any pending network before clicking save
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 save request + re-fetch to complete
await page.waitForLoadState("networkidle");
// Reload page to get fresh data from the backend
// This ensures the meds array passed to startEdit has the saved changes
await page.reload();
await page.waitForLoadState("networkidle");
// Verify the med row is visible in the list
const medRow = page.locator(".med-row").filter({ hasText: medName });
await expect(medRow).toBeVisible({ timeout: 10000 });
}
test.describe("Medication Editing", () => {
test.use({ storageState: authFile });
test.describe.configure({ timeout: 60000 });
const createdMeds: TestMedication[] = [];
test.beforeAll(async () => {
await deleteAllMedicationsViaAPI();
});
test.afterAll(async () => {
await deleteAllMedicationsViaAPI();
});
test("should edit generic name on an existing medication", async ({ page }) => {
createdMeds.push(await createMedicationViaAPI({ name: "Edit GenName Med" }));
await navigateTo(page, "/medications");
await clickEditMed(page, "Edit GenName Med");
// Generic name should be empty initially
const genericField = page.getByLabel(/(Generic Name|form\.genericName)/i);
await expect(genericField).toHaveValue("");
// Add a generic name
await genericField.fill("Acetylsalicylic acid");
await expect(genericField).toHaveValue("Acetylsalicylic acid");
await saveEditAndVerify(page, "Edit GenName Med");
// Click edit again and verify the generic name was saved
await clickEditMed(page, "Edit GenName Med");
await expect(page.getByLabel(/(Generic Name|form\.genericName)/i)).toHaveValue("Acetylsalicylic acid");
});
test("should add notes to an existing medication", async ({ page }) => {
createdMeds.push(await createMedicationViaAPI({ name: "Edit Notes Med" }));
await navigateTo(page, "/medications");
await clickEditMed(page, "Edit Notes Med");
await page.getByRole("tab", { name: /Package/i }).click();
// Notes should be empty initially
const notesField = page.getByLabel(/(Notes|form\.notes)/i);
await expect(notesField).toHaveValue("");
// Add notes text
await notesField.fill("Take with food after breakfast. Do not exceed 3 per day. Store below 25°C.");
await expect(notesField).toContainText("Take with food after breakfast");
await saveEditAndVerify(page, "Edit Notes Med");
// Verify notes were saved by clicking edit again
await clickEditMed(page, "Edit Notes Med");
await expect(page.getByLabel(/(Notes|form\.notes)/i)).toContainText("Take with food after breakfast");
});
test("should add taken-by person to a medication", async ({ page }) => {
createdMeds.push(await createMedicationViaAPI({ name: "TakenBy Med" }));
await navigateTo(page, "/medications");
await clickEditMed(page, "TakenBy Med");
// Find the taken-by input field inside the tag-input-container
const takenByContainer = page.locator(".tag-input-container");
await expect(takenByContainer).toBeVisible();
const takenByInput = takenByContainer.locator("input");
// Add a person name
await takenByInput.fill("Alice");
await takenByInput.press("Enter");
// Tag should appear
await expect(takenByContainer.locator(".tag").filter({ hasText: "Alice" })).toBeVisible();
// Add another person
await takenByInput.fill("Bob");
await takenByInput.press("Enter");
await expect(takenByContainer.locator(".tag").filter({ hasText: "Bob" })).toBeVisible();
await saveEditAndVerify(page, "TakenBy Med");
// Verify tags are persisted
await clickEditMed(page, "TakenBy Med");
await expect(page.locator(".tag-input-container .tag").filter({ hasText: "Alice" })).toBeVisible();
await expect(page.locator(".tag-input-container .tag").filter({ hasText: "Bob" })).toBeVisible();
});
test("should remove a taken-by person from a medication", async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: "Remove TakenBy Med",
takenBy: ["Alice", "Bob"],
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, "Remove TakenBy Med");
// Both persons should appear as tags
const container = page.locator(".tag-input-container");
await expect(container.locator(".tag")).toHaveCount(2, { timeout: 5000 });
// Use Backspace in the empty input to remove the last tag (Bob)
// The app handles this: if input empty + backspace → remove last takenBy person
const takenByInput = container.locator("input");
await takenByInput.click();
await takenByInput.press("Backspace");
// After backspace, Bob (the last tag) should be removed, leaving Alice
await expect(container.locator(".tag")).toHaveCount(1, { timeout: 5000 });
await expect(container.locator(".tag").filter({ hasText: "Alice" })).toBeVisible();
await saveEditAndVerify(page, "Remove TakenBy Med");
// Verify only Alice remains after save
await clickEditMed(page, "Remove TakenBy Med");
await expect(container.locator(".tag")).toHaveCount(1, { timeout: 5000 });
await expect(container.locator(".tag").filter({ hasText: "Alice" })).toBeVisible();
});
test("should add an expiry date to a medication", async ({ page }) => {
createdMeds.push(await createMedicationViaAPI({ name: "Expiry Date Med" }));
await navigateTo(page, "/medications");
await clickEditMed(page, "Expiry Date Med");
await page.getByRole("tab", { name: /Package/i }).click();
// Set expiry date to 6 months from now
const expiryDate = new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
const expiryField = page.getByLabel(/(Expiry Date|form\.expiryDate)/i);
await expiryField.fill(expiryDate);
await expect(expiryField).toHaveValue(expiryDate);
// Also touch the name field to ensure form is dirty
// Expiry change itself is enough to persist in the current edit flow.
await saveEditAndVerify(page, "Expiry Date Med");
// Verify expiry date was saved
await clickEditMed(page, "Expiry Date Med");
await expect(page.getByLabel(/(Expiry Date|form\.expiryDate)/i)).toHaveValue(expiryDate);
});
test("should edit intake schedule usage and interval", async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: "Edit Intake Med",
intakes: [
{
usage: 1,
every: 1,
start: new Date().toISOString().slice(0, 16),
intakeRemindersEnabled: false,
},
],
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, "Edit Intake Med");
await page.getByRole("tab", { name: /Schedule/i }).click();
// Change intake from 1 pill daily to 2 pills every 7 days
const intakeRow = page.locator(".blister-row").first();
const usageField = intakeRow.getByLabel(/(Usage|form\.blisters\.usage)/i);
const everyField = intakeRow.getByLabel(/(Every \(days\)|form\.blisters\.everyDays)/i);
await usageField.fill("2");
await everyField.fill("7");
await expect(usageField).toHaveValue("2");
await expect(everyField).toHaveValue("7");
await saveEditAndVerify(page, "Edit Intake Med");
// Verify the changes persisted
await clickEditMed(page, "Edit Intake Med");
const savedRow = page.locator(".blister-row").first();
await expect(savedRow.getByLabel(/(Usage|form\.blisters\.usage)/i)).toHaveValue("2");
await expect(savedRow.getByLabel(/(Every \(days\)|form\.blisters\.everyDays)/i)).toHaveValue("7");
});
test("should add a second intake schedule row", async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: "Add Intake Med",
intakes: [
{
usage: 1,
every: 1,
start: new Date().toISOString().slice(0, 16),
intakeRemindersEnabled: false,
},
],
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, "Add Intake Med");
await page.getByRole("tab", { name: /Schedule/i }).click();
// Should have 1 intake row initially
await expect(page.locator(".blister-row")).toHaveCount(1);
// Add a second intake
await page.getByRole("button", { name: /(Intake|form\.blisters\.addIntake)/i }).click();
await expect(page.locator(".blister-row")).toHaveCount(2);
// Fill the new intake row
const secondRow = page.locator(".blister-row").nth(1);
await secondRow.getByLabel(/(Usage|form\.blisters\.usage)/i).fill("0.5");
await secondRow.getByLabel(/(Every \(days\)|form\.blisters\.everyDays)/i).fill("7");
await saveEditAndVerify(page, "Add Intake Med");
// Verify 2 intakes persisted
await clickEditMed(page, "Add Intake Med");
await expect(page.locator(".blister-row")).toHaveCount(2, { timeout: 10000 });
});
test("should toggle intake reminder on a medication", async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: "Reminder Toggle Med",
intakes: [
{
usage: 1,
every: 1,
start: new Date().toISOString().slice(0, 16),
intakeRemindersEnabled: false,
},
],
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, "Reminder Toggle Med");
await page.getByRole("tab", { name: /Schedule/i }).click();
// Find the remind checkbox in the intake row
const intakeRow = page.locator(".blister-row").first();
const remindToggle = intakeRow.locator(".toggle-switch");
const remindCheckbox = intakeRow.locator('.toggle-switch input[type="checkbox"]');
await expect(remindCheckbox).not.toBeChecked();
await remindToggle.click();
await expect(remindCheckbox).toBeChecked();
await saveEditAndVerify(page, "Reminder Toggle Med");
// Verify reminder was saved
await clickEditMed(page, "Reminder Toggle Med");
const savedCheckbox = page.locator(".blister-row").first().locator('.toggle-switch input[type="checkbox"]');
await expect(savedCheckbox).toBeChecked();
});
for (const scenario of [
{
name: "Inhaler Reminder Refill Med",
packageType: "inhaler" as const,
totalCapacity: 200,
currentStock: 120,
refillAmount: 30,
expectedStock: 150,
unitLabel: /puffs?|common\.puffs?/i,
},
{
name: "Injection Reminder Refill Med",
packageType: "injection" as const,
totalCapacity: 12,
currentStock: 4,
refillAmount: 3,
expectedStock: 7,
unitLabel: /injections?|common\.injections?/i,
},
]) {
test(`should persist reminders and refill ${scenario.packageType} stock without drift`, async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: scenario.name,
packageType: scenario.packageType,
totalPills: scenario.totalCapacity,
looseTablets: scenario.currentStock,
intakes: [
{
usage: 1,
every: 1,
start: new Date().toISOString().slice(0, 16),
intakeRemindersEnabled: false,
},
],
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, scenario.name);
await page.getByRole("tab", { name: /Schedule/i }).click();
const intakeRow = page.locator(".blister-row").first();
const remindToggle = intakeRow.locator(".toggle-switch");
const remindCheckbox = intakeRow.locator('.toggle-switch input[type="checkbox"]');
await expect(remindCheckbox).not.toBeChecked();
await remindToggle.click();
await expect(remindCheckbox).toBeChecked();
await saveEditAndVerify(page, scenario.name);
await clickEditMed(page, scenario.name);
await page.getByRole("tab", { name: /Schedule/i }).click();
await expect(page.locator(".blister-row").first().locator('.toggle-switch input[type="checkbox"]')).toBeChecked();
await navigateTo(page, "/dashboard");
const modal = await openMedicationDetailFromDashboard(page, scenario.name);
await modal.getByRole("button", { name: /Refill|refill\.button/i }).click();
const refillModal = page.locator(".modal-content.refill-modal");
await expect(refillModal).toBeVisible({ timeout: 5000 });
const refillInput = refillModal.locator('input[type="number"]').first();
await refillInput.fill(String(scenario.refillAmount));
await expect(refillModal.locator(".refill-preview")).toContainText(`+${scenario.refillAmount}`);
await expect(refillModal.locator(".refill-preview")).toContainText(scenario.unitLabel);
await refillModal.locator(".modal-footer .success").click();
await expect(refillModal).not.toBeVisible({ timeout: 10000 });
const refillHistoryHeader = modal.locator(".med-detail-section h3").filter({
hasText: /Refill History|refill\.history/i,
});
await expect(refillHistoryHeader).toBeVisible({ timeout: 10000 });
await refillHistoryHeader.click();
const refillAmount = modal.locator(".refill-history-item .refill-amount").first();
await expect(refillAmount).toContainText(`+${scenario.refillAmount}`);
await expect(refillAmount).toContainText(scenario.unitLabel);
await page.locator("button.modal-close").click();
await expect(modal).not.toBeVisible({ timeout: 5000 });
await navigateTo(page, "/medications");
const medRow = page.locator(".med-row").filter({ hasText: scenario.name });
await expect(medRow.locator(".med-total")).toContainText(`${scenario.expectedStock} / ${scenario.totalCapacity}`);
});
}
test("should change package type across all supported profiles", async ({ page }) => {
createdMeds.push(
await createMedicationViaAPI({
name: "PackType Change Med",
packageType: "blister",
packCount: 2,
blistersPerPack: 3,
pillsPerBlister: 10,
})
);
await navigateTo(page, "/medications");
await clickEditMed(page, "PackType Change Med");
const form = page.locator("form.form-grid:visible").first();
// Should be blister type initially
const packageSelect = form.locator("select.package-type-select");
await expect(packageSelect).toHaveValue("blister");
// Blister-specific fields are shown in the Package tab.
await page.getByRole("tab", { name: /Package/i }).click();
await expect(form.getByLabel(/(Blisters per pack|form\.blistersPerPack)/i)).toBeVisible();
await page.getByRole("tab", { name: /General/i }).click();
// Switch to bottle
await packageSelect.selectOption("bottle");
await page.getByRole("tab", { name: /Package/i }).click();
await expect(form.getByLabel(/(Total Capacity|form\.totalCapacity|Total \(pills\))/i)).toBeVisible();
await page.getByRole("tab", { name: /General/i }).click();
// Switch to tube
await packageSelect.selectOption("tube");
await page.getByRole("tab", { name: /Package/i }).click();
await expect(form.getByLabel(/(Amount per tube|form\.packageAmountPerTube)/i)).toBeVisible();
await page.getByRole("tab", { name: /General/i }).click();
// Switch to liquid container and persist this final state
await packageSelect.selectOption("liquid_container");
await page.getByRole("tab", { name: /Package/i }).click();
await expect(form.getByLabel(/(Package amount|form\.packageAmount)/i)).toBeVisible();
await page.getByRole("tab", { name: /General/i }).click();
// Switch to inhaler
await packageSelect.selectOption("inhaler");
await page.getByRole("tab", { name: /Package/i }).click();
await expect(
form.getByLabel(/(Total Capacity|form\.totalCapacity|Total \(count\)|form\.totalCount)/i)
).toBeVisible();
await expect(form.getByLabel(/(Current Stock|form\.currentStockCount)/i)).toBeVisible();
await page.getByRole("tab", { name: /General/i }).click();
// Switch to injection and persist this final state
await packageSelect.selectOption("injection");
await page.getByRole("tab", { name: /Package/i }).click();
await expect(
form.getByLabel(/(Total Capacity|form\.totalCapacity|Total \(count\)|form\.totalCount)/i)
).toBeVisible();
await expect(form.getByLabel(/(Current Stock|form\.currentStockCount)/i)).toBeVisible();
await saveEditAndVerify(page, "PackType Change Med");
// Verify final package type persisted
await clickEditMed(page, "PackType Change Med");
await expect(page.locator("select.package-type-select")).toHaveValue("injection");
});
test("should edit multiple fields at once (name, notes, generic, taken-by)", async ({ page }) => {
createdMeds.push(await createMedicationViaAPI({ name: "Multi Edit Med" }));
await navigateTo(page, "/medications");
await clickEditMed(page, "Multi Edit Med");
// Change the name
await page.getByLabel(/(Commercial Name|form\.commercialName)/i).fill("Fully Edited Med");
// Add generic name
await page.getByLabel(/(Generic Name|form\.genericName)/i).fill("Ibuprofen Lysinate");
// Add notes
await page.getByRole("tab", { name: /Package/i }).click();
await page.getByLabel(/(Notes|form\.notes)/i).fill("Morning dose only. Take with plenty of water.");
await page.getByRole("tab", { name: /General/i }).click();
// Add a taken-by person
const takenByInput = page.locator(".tag-input-container input");
await takenByInput.fill("Charlie");
await takenByInput.press("Enter");
await expect(page.locator(".tag-input-container .tag").filter({ hasText: "Charlie" })).toBeVisible();
await saveEditAndVerify(page, "Fully Edited Med");
// Verify all changes persisted
await clickEditMed(page, "Fully Edited Med");
await expect(page.getByLabel(/(Commercial Name|form\.commercialName)/i)).toHaveValue("Fully Edited Med");
await expect(page.getByLabel(/(Generic Name|form\.genericName)/i)).toHaveValue("Ibuprofen Lysinate");
await expect(page.getByLabel(/(Notes|form\.notes)/i)).toContainText("Morning dose only");
await expect(page.locator(".tag-input-container .tag").filter({ hasText: "Charlie" })).toBeVisible();
});
});