Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests/e2e/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

## Synchronization

- Server-rendered visibility does not prove hydration. Call `waitForDashboardReady(page)` before dashboard input, keyboard/focus behavior, or client-router interaction after a full navigation or reload. Shared interaction helpers may own this wait. Preserve deliberate pre-hydration assertions and server-rendered-only checks.

- Prefer route barriers (`page.route`, `route.fetch`, `route.fulfill`) and `Promise.withResolvers()` over sleeps.
- Match API waits by HTTP method plus exact absolute URL, especially for period/date/history race tests.
- Verify state both while a request is blocked and after it is released.
Expand Down
15 changes: 14 additions & 1 deletion tests/e2e/dashboard-calendar.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect, test } from "@playwright/test";
import { configureDashboardDayEntryE2E } from "./dashboard-day-entry-helpers";
import { seedPeriod } from "./helpers/db";
import { addDays, getBaseUrl, getCurrentJstDate } from "./dashboard-shared";
import {
addDays,
getBaseUrl,
getCurrentJstDate,
waitForDashboardReady,
} from "./dashboard-shared";

configureDashboardDayEntryE2E();

Expand All @@ -16,6 +21,7 @@ test("moves one roving calendar focus across months with keyboard", async ({
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-keys`);
await waitForDashboardReady(page);

const januaryEnd = page.getByTestId("calendar-day-2026-01-31");
const februaryStart = page.getByTestId("calendar-day-2026-02-01");
Expand Down Expand Up @@ -77,6 +83,7 @@ test("calendar handoff opens the focused date once with Enter and retains select
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-keys`);
await waitForDashboardReady(page);

const date = "2026-02-01";
const day = page.getByTestId(`calendar-day-${date}`);
Expand Down Expand Up @@ -114,6 +121,7 @@ test("calendar handoff opens the focused date once with Space", async ({
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-keys`);
await waitForDashboardReady(page);

const date = "2026-02-01";
const day = page.getByTestId(`calendar-day-${date}`);
Expand Down Expand Up @@ -150,6 +158,7 @@ test("keyboard keeps disabled calendar dates focusable without opening history",
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-current`);
await waitForDashboardReady(page);

const periodSummaryBarrier = Promise.withResolvers<void>();
const nextSummaryUrl = `${getBaseUrl()}/api/periods/p-calendar-next`;
Expand Down Expand Up @@ -213,6 +222,7 @@ test("keeps selection separate from keyboard focus and resets it for another per
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-current`);
await waitForDashboardReady(page);

const todayDay = page.getByTestId(`calendar-day-${today}`);
const tomorrowDay = page.getByTestId(`calendar-day-${addDays(today, 1)}`);
Expand Down Expand Up @@ -257,6 +267,7 @@ test("keeps future allowance unchanged while future calendar input remains avail
budgetYen: 90000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-future`);
await waitForDashboardReady(page);

const futureDay = page.getByTestId(`calendar-day-${futureStart}`);
await expect(page.getByTestId("today-food-allowance")).toContainText("0 円");
Expand All @@ -279,6 +290,7 @@ test("blocks calendar activation while a period save is in flight, then restores
budgetYen: 120000,
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-save`);
await waitForDashboardReady(page);

const saveBarrier = Promise.withResolvers<void>();
const periodUrl = `${getBaseUrl()}/api/periods/p-calendar-save`;
Expand Down Expand Up @@ -340,6 +352,7 @@ test("keeps the calendar inside every supported viewport without clipping state
],
});
await page.goto(`${getBaseUrl()}/?periodId=p-calendar-responsive`);
await waitForDashboardReady(page);
const todayDay = page.getByTestId(`calendar-day-${today}`);
await todayDay.click();
await page
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/dashboard-create-period.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getBaseUrl,
getCurrentJstDate,
resetTestData,
waitForDashboardReady,
} from "./dashboard-shared";

test.describe.configure({ mode: "serial", timeout: 120_000 });
Expand All @@ -24,6 +25,7 @@ for (const failureStage of ["summary", "list"] as const) {
let posts = 0;
let gets = 0;
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
page.on("request", (request) => {
if (request.method() === "POST") posts += 1;
});
Expand Down Expand Up @@ -97,6 +99,7 @@ for (const variant of ["initial", "additional"] as const) {
});
}
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
if (variant === "additional") {
await page.getByText("次の予算期間を作成する").click();
}
Expand Down Expand Up @@ -156,6 +159,7 @@ test("guards duplicate creation through a held POST and list GET", async ({
let posts = 0;
let gets = 0;
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
await page.route(`${getBaseUrl()}/api/periods`, async (route) => {
if (route.request().method() === "POST") {
posts += 1;
Expand Down Expand Up @@ -221,6 +225,7 @@ test("additional recovery does not reopen or steal focus after closing during re
let gets = 0;
let posts = 0;
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
page.on("request", (request) => {
if (request.method() === "POST") posts += 1;
});
Expand Down Expand Up @@ -280,6 +285,7 @@ test("validates fields in form order and submits corrected drafts with Enter", a
const today = getCurrentJstDate();
const writes: string[] = [];
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
page.on("request", (request) => {
if (["POST", "PUT"].includes(request.method())) writes.push(request.url());
});
Expand Down Expand Up @@ -326,6 +332,7 @@ test("initial cancel restores defaults and clears field errors without writing",
}, testInfo) => {
const today = getCurrentJstDate();
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
const writes: string[] = [];
page.on("request", (request) => {
if (["POST", "PUT"].includes(request.method())) writes.push(request.url());
Expand Down Expand Up @@ -372,6 +379,7 @@ test("additional cancel closes the form and keeps drafts while clearing server f
budgetYen: 120000,
});
await page.goto(getBaseUrl());
await waitForDashboardReady(page);
const opener = page.getByText("次の予算期間を作成する");
await opener.click();
await page.getByLabel("期間ID", { exact: true }).fill("p-retained");
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/dashboard-day-entry-failure-handling.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
configureDashboardDayEntryE2E,
seedCurrentPeriod,
} from "./dashboard-day-entry-helpers";
import { getBaseUrl } from "./dashboard-shared";
import { getBaseUrl, waitForDashboardReady } from "./dashboard-shared";

configureDashboardDayEntryE2E();

Expand All @@ -14,6 +14,7 @@ test("rejects malformed day-entry yen values before requests", async ({
const { periodId, todayDate } = await seedCurrentPeriod(request);

await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
let addRequestCount = 0;
await page.route(
`**/api/periods/${periodId}/days/${todayDate}/add`,
Expand Down Expand Up @@ -71,6 +72,7 @@ test("shows save error and keeps input on failed day entry update", async ({
const { periodId, todayDate } = await seedCurrentPeriod(request);

await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
const retryBarrier = Promise.withResolvers<void>();
let addRequestCount = 0;
await page.route(
Expand Down Expand Up @@ -136,6 +138,7 @@ test("shows history load error while keeping the day entry modal usable", async
const { periodId, todayDate } = await seedCurrentPeriod(request);

await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await page.route(
`**/api/periods/${periodId}/days/${todayDate}/history`,
async (route) => {
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/dashboard-day-entry-surface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
openDayEntryAndWaitForHistory,
seedCurrentPeriod,
} from "./dashboard-day-entry-helpers";
import { getBaseUrl } from "./dashboard-shared";
import { getBaseUrl, waitForDashboardReady } from "./dashboard-shared";

configureDashboardDayEntryE2E();

Expand Down Expand Up @@ -190,6 +190,7 @@ test("returns focus to the calendar origin and announces an accepted save", asyn

await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await origin.focus();
const initialHistoryResponse = page.waitForResponse(
(response) =>
Expand Down Expand Up @@ -244,6 +245,7 @@ test("falls back to the calendar heading when its origin is removed", async ({
const { periodId, todayDate } = await seedCurrentPeriod(request);
const origin = page.getByTestId(`calendar-day-${todayDate}`);
await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await origin.focus();
await origin.press("Enter");
const modal = page.getByTestId("day-entry-modal");
Expand Down
6 changes: 5 additions & 1 deletion tests/e2e/dashboard-history-surface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
configureDashboardDayEntryE2E,
seedCurrentPeriod,
} from "./dashboard-day-entry-helpers";
import { getBaseUrl } from "./dashboard-shared";
import { getBaseUrl, waitForDashboardReady } from "./dashboard-shared";

configureDashboardDayEntryE2E();

Expand All @@ -31,6 +31,7 @@ async function openHistoryWithEntries(
periodId: string,
date: string,
): Promise<Locator> {
await waitForDashboardReady(page);
const historyUrl = new URL(
`/api/periods/${encodeURIComponent(periodId)}/days/${encodeURIComponent(date)}/history`,
getBaseUrl(),
Expand Down Expand Up @@ -61,6 +62,7 @@ test("keeps loading and empty history states exclusive", async ({
await route.continue();
});
await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await page.getByTestId(`calendar-day-${todayDate}`).click();
await historyRequested.promise;

Expand Down Expand Up @@ -122,6 +124,7 @@ test("keeps retry focused through a failed history GET and moves it after succes
});
});
await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await page.getByTestId(`calendar-day-${todayDate}`).click();

const modal = page.getByTestId("day-entry-modal");
Expand Down Expand Up @@ -171,6 +174,7 @@ test("loads a readable history row with wrapping memo", async ({
);
expect(response.ok()).toBe(true);
await page.goto(`${getBaseUrl()}/?periodId=${encodeURIComponent(periodId)}`);
await waitForDashboardReady(page);
await page.getByTestId(`calendar-day-${todayDate}`).click();

const modal = page.getByTestId("day-entry-modal");
Expand Down
Loading
Loading