test(e2e): log a mood at a chosen time against the real server

The mood timestamp spec answered the POST and the list read itself. It now
runs on the mood journey's account: the instant typed into the form is the
instant stored, and the entry is in the list, also after a reload.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 12:58:39 +02:00
1 parent 7445b69842
commit f185855d76
2 files changed
+67 -33

No files matched your search

+66 -32
View File
@@ -1,39 +1,53 @@
import type { Page } from "@playwright/test";
import { expect, test } from "./setup/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import {
criticalAccount,
criticalStorageState,
resetCriticalAccount,
} from "./setup/critical-fixture";
/**
* Logging a mood through the form, against the real server: the time stays
* visible while the details are collapsed, the instant typed into it is the
* instant stored, and the entry is in the list afterwards.
*
* Until v1.46 this spec answered the POST and the list read itself, so the
* list was always empty and the POST always succeeded. The account is now the
* mood journey's own, one per project (`critical-fixture.ts`), emptied before
* the test; the empty state the form opens from is the account's real one.
*/
/** The account's mood entries, as the list route answers them. */
async function storedMoods(
page: Page,
): Promise<Array<{ mood: string; moodLoggedAt: string }>> {
const res = await page.request.get("/api/mood-entries");
expect(res.status()).toBe(200);
const { data } = (await res.json()) as {
data: { entries: Array<{ mood: string; moodLoggedAt: string }> };
};
return data.entries.map(({ mood, moodLoggedAt }) => ({ mood, moodLoggedAt }));
}
test.describe("mood timestamp capture", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test.use({ storageState: criticalStorageState("mood") });
test("keeps Time visible while details are collapsed and submits the selected instant", async ({
test.beforeEach(async ({}, testInfo) => {
// Fixture hygiene: what a previous run or a retry left on the account.
await resetCriticalAccount(criticalAccount("mood", testInfo), [
"DELETE FROM mood_entry_rollups WHERE user_id = $1",
"DELETE FROM mood_entries WHERE user_id = $1",
]);
});
test("a mood logged at a chosen time is stored at that instant and listed @critical", async ({
page,
}) => {
let submitted: Record<string, unknown> | null = null;
await page.route("**/api/mood-entries*", async (route) => {
if (route.request().method() === "POST") {
submitted = route.request().postDataJSON() as Record<string, unknown>;
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify({ data: { id: "created-mood" }, error: null }),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { entries: [], meta: { total: 0, limit: 50, offset: 0 } },
error: null,
}),
});
});
await page.goto("/mood", { waitUntil: "domcontentloaded" });
// The list is mocked empty, so its empty state carries the add action
// (the header drops its own while there is nothing to list).
// An empty list carries the add action in its empty state (the header
// drops its own while there is nothing to list).
await page.getByRole("button", { name: "Log your first mood" }).click();
await page.getByRole("radio", { name: "Amazing" }).click();
@@ -53,12 +67,32 @@ test.describe("mood timestamp capture", () => {
await timeInput.fill("09:45");
await timeInput.blur();
const created = page.waitForResponse(
(res) =>
new URL(res.url()).pathname === "/api/mood-entries" &&
res.request().method() === "POST",
);
await page.getByRole("button", { name: "Save", exact: true }).click();
expect((await created).status()).toBe(201);
await expect.poll(() => submitted).not.toBeNull();
expect(submitted).toMatchObject({
mood: "SUPER_GUT",
moodLoggedAt: "2026-01-15T08:45:00.000Z",
// 09:45 in Berlin in January is 08:45 UTC.
expect(await storedMoods(page)).toEqual([
{ mood: "SUPER_GUT", moodLoggedAt: "2026-01-15T08:45:00.000Z" },
]);
// The list shows it, at either width (the table and the phone cards each
// carry the marker; one of them is on screen).
const rows = page.locator('[data-slot="mood-rows"]').filter({
visible: true,
});
await expect(rows).toBeVisible();
await expect(rows).toContainText("Amazing");
await expect(rows).toContainText("09:45");
// And still after a reload, which reads the list from the server again
// rather than from the cache the mutation updated.
await page.reload({ waitUntil: "domcontentloaded" });
await expect(rows).toContainText("Amazing");
await expect(rows).toContainText("09:45");
});
});
@@ -49,7 +49,7 @@ const E2E = join(process.cwd(), "e2e");
* the set is the release gate, so a journey that loses its tag is a change
* somebody should see here.
*/
const CRITICAL_TEST_FLOOR = 11;
const CRITICAL_TEST_FLOOR = 12;
/** A test whose title literal carries the tag, however the call is spelled. */
const CRITICAL_TEST =