7f2ef09df5
* test: expand e2e app shell coverage and stabilize flaky scenarios * fix(e2e): stabilize dashboard flow and frontend ci gates
259 lines
9.7 KiB
TypeScript
259 lines
9.7 KiB
TypeScript
import {
|
|
authFile,
|
|
createMedicationViaAPI,
|
|
deleteAllMedicationsViaAPI,
|
|
expect,
|
|
navigateTo,
|
|
type TestMedication,
|
|
test,
|
|
} from "./fixtures";
|
|
|
|
/**
|
|
* Dashboard with Medication Data E2E Tests
|
|
*
|
|
* Creates medications via API, then verifies the dashboard
|
|
* overview table, coverage cards, timeline, and dose tracking.
|
|
*/
|
|
test.describe("Dashboard with medications", () => {
|
|
test.use({ storageState: authFile });
|
|
test.describe.configure({ timeout: 60000 });
|
|
|
|
// Unique medication names to avoid conflicts with parallel workers
|
|
const MED_1 = "DashData Ibuprofen";
|
|
const MED_2 = "DashData Vitamin C";
|
|
|
|
// Set start to earlier today so doses appear on the timeline
|
|
const todayMorning = (() => {
|
|
const d = new Date();
|
|
d.setHours(8, 0, 0, 0);
|
|
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
})();
|
|
|
|
const createdMeds: TestMedication[] = [];
|
|
|
|
test.beforeAll(async () => {
|
|
// Clean up any leftover medications from previous test runs
|
|
await deleteAllMedicationsViaAPI();
|
|
createdMeds.push(
|
|
await createMedicationViaAPI({
|
|
name: MED_1,
|
|
genericName: "Ibuprofen",
|
|
packageType: "blister",
|
|
packCount: 2,
|
|
blistersPerPack: 3,
|
|
pillsPerBlister: 10,
|
|
looseTablets: 0,
|
|
intakes: [{ usage: 1, every: 1, start: todayMorning, intakeRemindersEnabled: false }],
|
|
})
|
|
);
|
|
createdMeds.push(
|
|
await createMedicationViaAPI({
|
|
name: MED_2,
|
|
packageType: "bottle",
|
|
totalPills: 90,
|
|
looseTablets: 90,
|
|
intakes: [{ usage: 1, every: 1, start: todayMorning, intakeRemindersEnabled: false }],
|
|
})
|
|
);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
await deleteAllMedicationsViaAPI();
|
|
});
|
|
|
|
test("should show medication overview table with medications", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const overviewTable = page.locator(".dashboard-overview-section .table").first();
|
|
await expect(overviewTable).toBeVisible({ timeout: 10000 });
|
|
await expect(overviewTable.locator(".table-head")).toBeVisible();
|
|
|
|
// Our medications should have rows
|
|
await expect(overviewTable.getByText(MED_1)).toBeVisible();
|
|
await expect(overviewTable.getByText(MED_2)).toBeVisible();
|
|
});
|
|
|
|
test("should show status chips in overview table", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const overviewTable = page.locator(".dashboard-overview-section .table").first();
|
|
await expect(overviewTable).toBeVisible({ timeout: 10000 });
|
|
|
|
// Each medication row should have a status chip
|
|
const statusChips = overviewTable.locator(".status-chip");
|
|
expect(await statusChips.count()).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
test("should show stock information in overview", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
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)
|
|
const ibuprofenRow = overviewTable.locator(".table-row").filter({ hasText: MED_1 });
|
|
await expect(ibuprofenRow).toBeVisible();
|
|
const rowText = await ibuprofenRow.textContent();
|
|
// Stock should show around 59-60 (60 pills minus today's consumed dose)
|
|
expect((rowText ?? "").includes("59") || (rowText ?? "").includes("60")).toBeTruthy();
|
|
});
|
|
|
|
test("should show today block in timeline", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("should show medication names in today's schedule", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
await expect(todayBlock.getByText(MED_1)).toBeVisible();
|
|
await expect(todayBlock.getByText(MED_2)).toBeVisible();
|
|
});
|
|
|
|
test("should show day summary with dose progress", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
const overviewTable = page.locator(".dashboard-overview-section .table").first();
|
|
await expect(overviewTable).toBeVisible({ timeout: 10000 });
|
|
await expect(overviewTable.getByText(MED_1)).toBeVisible({ timeout: 10000 });
|
|
|
|
const todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
await expect(todayBlock.locator(".day-summary")).toBeVisible();
|
|
});
|
|
|
|
test("should show dose take buttons in today's schedule", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
|
|
const takeButtons = todayBlock.locator("button.dose-btn.take");
|
|
expect(await takeButtons.count()).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test("should mark a dose as taken and show undo", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
let todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
|
|
const takeBtn = todayBlock.locator("button.dose-btn.take:not([disabled])").first();
|
|
test.skip(!(await takeBtn.isVisible().catch(() => false)), "No actionable take-dose button is visible for today");
|
|
|
|
const takeResponsePromise = page.waitForResponse(
|
|
(response) => response.url().includes("/api/doses/taken") && response.request().method() === "POST",
|
|
{ timeout: 10000 }
|
|
);
|
|
await takeBtn.click();
|
|
const takeResponse = await takeResponsePromise;
|
|
test.skip(!takeResponse.ok(), "Backend did not accept dose take request");
|
|
|
|
await page.reload();
|
|
await page.waitForLoadState("networkidle");
|
|
todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
await expect(todayBlock.locator("button.dose-btn.undo").first()).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test("should undo a taken dose", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const overviewTable = page.locator(".dashboard-overview-section .table").first();
|
|
await expect(overviewTable).toBeVisible({ timeout: 15000 });
|
|
await expect(overviewTable.getByText(MED_1)).toBeVisible({ timeout: 15000 });
|
|
|
|
let todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 15000 });
|
|
|
|
// Normalize state first: if a dose is already taken, undo it so we can
|
|
// always execute the same take -> undo flow deterministically.
|
|
const existingUndo = todayBlock.locator("button.dose-btn.undo").first();
|
|
if (await existingUndo.isVisible().catch(() => false)) {
|
|
await existingUndo.click();
|
|
await page.waitForLoadState("networkidle");
|
|
}
|
|
|
|
// Mark a dose as taken first
|
|
const takeBtn = todayBlock.locator("button.dose-btn.take:not([disabled])").first();
|
|
await expect(takeBtn).toBeVisible({ timeout: 10000 });
|
|
const takeResponsePromise = page.waitForResponse(
|
|
(response) => response.url().includes("/api/doses/taken") && response.request().method() === "POST",
|
|
{ timeout: 10000 }
|
|
);
|
|
await takeBtn.click();
|
|
const takeResponse = await takeResponsePromise;
|
|
test.skip(!takeResponse.ok(), "Backend did not accept dose take request");
|
|
|
|
await page.reload();
|
|
await page.waitForLoadState("networkidle");
|
|
await expect(overviewTable).toBeVisible({ timeout: 15000 });
|
|
await expect(overviewTable.getByText(MED_1)).toBeVisible({ timeout: 15000 });
|
|
todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 15000 });
|
|
|
|
// Wait for undo button to appear (confirms the take succeeded)
|
|
const undoBtn = todayBlock.locator("button.dose-btn.undo").first();
|
|
await expect(undoBtn).toBeVisible({ timeout: 10000 });
|
|
await undoBtn.click();
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Take button should reappear
|
|
await expect(todayBlock.locator("button.dose-btn.take:not([disabled])").first()).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("should show multiple day blocks in timeline", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
// Wait for timeline to fully render
|
|
await page.waitForLoadState("networkidle");
|
|
const dayBlocks = page.locator(".day-block");
|
|
await expect(dayBlocks.first()).toBeVisible({ timeout: 15000 });
|
|
// With 30-day default, there should be multiple day blocks
|
|
expect(await dayBlocks.count()).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test("should show day header with date text", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const todayBlock = page.locator(".day-block.today");
|
|
await expect(todayBlock).toBeVisible({ timeout: 10000 });
|
|
|
|
const dayDivider = todayBlock.locator(".day-divider");
|
|
await expect(dayDivider).toBeVisible();
|
|
expect(await dayDivider.textContent()).toBeTruthy();
|
|
});
|
|
|
|
test("should open medication detail modal from overview table", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
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();
|
|
await medRow.click();
|
|
|
|
const modal = page.locator(".modal-overlay");
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
await expect(modal.getByText(MED_1)).toBeVisible();
|
|
|
|
await page.locator("button.modal-close").click();
|
|
await expect(modal).not.toBeVisible();
|
|
});
|
|
|
|
test("should show schedule days selector", async ({ page }) => {
|
|
await navigateTo(page, "/dashboard");
|
|
|
|
const daysSelect = page.locator("select.schedule-days-select");
|
|
await expect(daysSelect).toBeVisible();
|
|
await expect(daysSelect.locator('option[value="30"]')).toBeAttached();
|
|
await expect(daysSelect.locator('option[value="90"]')).toBeAttached();
|
|
await expect(daysSelect.locator('option[value="180"]')).toBeAttached();
|
|
});
|
|
});
|