mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
The water-capture journey clicked the dashboard quick-add trigger after
`waitForLoadState("networkidle")` and then waited thirty seconds for a menu
item that never arrived. The failure snapshot shows the page rendered, the
trigger present, and no menu anywhere: the click landed on server HTML that
React had not attached a handler to yet, and the one click the test had was
spent before hydration.
`networkidle` cannot close that window, because the trigger is in the first
HTML and exists long before the client bundle finishes. The gate has to be the
element's own state. `openMenu` clicks and confirms the trigger reports itself
open — Radix stamps `data-state`, the hand-rolled mobile capture button carries
`aria-expanded` off React state — and spends another click if it does not,
because the problem is a lost event and waiting longer for a swallowed click
achieves nothing. Out of attempts it fails naming the state the trigger was
actually in, rather than letting the caller time out on a menu item that was
never going to appear.
This is a race the spec always had; it surfaces when the machine is loaded
enough to push hydration past the click, which is why it reads as a
branch-specific regression when it is not one. The tour passthrough spec keeps
its single un-retried click on the same trigger: proving that exactly one click
reaches the button through the overlay is that test's entire point.
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
import { expect, test } from "./setup/test";
|
|
|
|
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
import { openMenu } from "./open-menu";
|
|
import {
|
|
mockDashboardSnapshot,
|
|
WEIGHT_ONLY_SUMMARIES,
|
|
} from "./utils/mock-dashboard-snapshot";
|
|
|
|
test.describe("water capture", () => {
|
|
test.use({ storageState: STORAGE_STATE_PATH });
|
|
|
|
test("opens globally, waits for success, and retains failed input", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES });
|
|
await page.route(/\/api\/analytics(\?|$)/, (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
data: { summaries: {}, bpInTargetPct: null, glucoseByContext: {} },
|
|
error: null,
|
|
}),
|
|
}),
|
|
);
|
|
await page.route("**/api/mood/analytics", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
data: { entries: [], summary: { count: 0 } },
|
|
error: null,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
let writeCount = 0;
|
|
let confirmSuccess: (() => void) | undefined;
|
|
const successConfirmed = new Promise<void>((resolve) => {
|
|
confirmSuccess = resolve;
|
|
});
|
|
await page.route("**/api/nutrients/water", async (route) => {
|
|
writeCount += 1;
|
|
if (writeCount === 1) {
|
|
await successConfirmed;
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ data: { amountMl: 250 }, error: null }),
|
|
});
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 500,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ data: null, error: "write failed" }),
|
|
});
|
|
});
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const openWater = async (toastMayOverlap = false) => {
|
|
if (testInfo.project.name === "chromium-mobile") {
|
|
const capture = page.getByTestId("bottom-nav-capture");
|
|
if (toastMayOverlap) {
|
|
await capture.dispatchEvent("click");
|
|
} else {
|
|
await openMenu(page, capture);
|
|
}
|
|
const option = page.getByTestId("capture-picker-water");
|
|
await expect(option).toBeVisible();
|
|
const box = await option.boundingBox();
|
|
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
|
|
if (toastMayOverlap) {
|
|
await option.dispatchEvent("click");
|
|
} else {
|
|
await option.click();
|
|
}
|
|
} else {
|
|
await openMenu(
|
|
page,
|
|
page.locator('[data-tour-id="dashboard-quick-add"]'),
|
|
);
|
|
await page.getByRole("menuitem", { name: "Log water" }).click();
|
|
}
|
|
await expect(
|
|
page.getByRole("heading", { name: "Add water" }),
|
|
).toBeVisible();
|
|
if (testInfo.project.name === "chromium-mobile") {
|
|
const actions = page
|
|
.getByRole("dialog")
|
|
.locator(
|
|
'[data-slot="water-quick-add-chips"] button, form button[type="submit"]',
|
|
);
|
|
await expect(actions).toHaveCount(4);
|
|
for (const action of await actions.all()) {
|
|
await expect(action).toHaveAccessibleName(/.+/);
|
|
const actionBox = await action.boundingBox();
|
|
expect(actionBox?.height ?? 0).toBeGreaterThanOrEqual(44);
|
|
}
|
|
}
|
|
};
|
|
|
|
await openWater();
|
|
const amount = page.getByRole("spinbutton", {
|
|
name: "Custom amount (mL)",
|
|
});
|
|
await amount.fill("250");
|
|
const submit = page
|
|
.getByRole("dialog")
|
|
.getByRole("button", { name: "Add", exact: true });
|
|
await amount.press("Enter");
|
|
|
|
await expect(submit).toBeDisabled();
|
|
await expect(amount).toHaveValue("250");
|
|
await expect(page.getByText("250 mL water added.")).toHaveCount(0);
|
|
|
|
confirmSuccess?.();
|
|
await expect(page.getByText("250 mL water added.")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Add water" })).toHaveCount(
|
|
0,
|
|
);
|
|
expect(writeCount).toBe(1);
|
|
|
|
await openWater(true);
|
|
const failedAmount = page.getByRole("spinbutton", {
|
|
name: "Custom amount (mL)",
|
|
});
|
|
await expect(failedAmount).toHaveValue("");
|
|
await failedAmount.fill("375");
|
|
await page
|
|
.getByRole("dialog")
|
|
.getByRole("button", { name: "Add", exact: true })
|
|
.click();
|
|
|
|
await expect(
|
|
page.getByText("Couldn't save that — try again."),
|
|
).toBeVisible();
|
|
await expect(failedAmount).toHaveValue("375");
|
|
await expect(
|
|
page.getByRole("heading", { name: "Add water" }),
|
|
).toBeVisible();
|
|
await expect(page.getByText("375 mL water added.")).toHaveCount(0);
|
|
expect(writeCount).toBe(2);
|
|
});
|
|
});
|