Add period date setup modal for new users
All checks were successful
Deploy / deploy (push) Successful in 2m27s

Users without a lastPeriodDate can now set it via a modal opened from
the onboarding banner. The dashboard now fetches user data independently
so the banner shows even when /api/today fails due to missing period date.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 14:28:49 +00:00
parent 72706bb91b
commit 0e585e6bb4
4 changed files with 771 additions and 34 deletions

View File

@@ -500,11 +500,16 @@ describe("Dashboard", () => {
describe("error handling", () => {
it("shows error message when /api/today fails", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve({ error: "Internal server error" }),
});
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve({ error: "Internal server error" }),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUserResponse),
});
render(<Dashboard />);
@@ -515,20 +520,31 @@ describe("Dashboard", () => {
});
it("shows setup message when user has no lastPeriodDate", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
});
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
// Multiple alerts may be present (error alert + onboarding banner)
const alerts = screen.getAllByRole("alert");
expect(alerts.length).toBeGreaterThan(0);
// Check for the specific help text about getting started
expect(
screen.getByText(/please log your period start date to get started/i),
@@ -723,4 +739,295 @@ describe("Dashboard", () => {
});
});
});
describe("period date modal flow", () => {
it("shows onboarding banner when todayData fails but userData shows no lastPeriodDate", async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(
screen.getByText(/Set your last period date for accurate tracking/i),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /set date/i }),
).toBeInTheDocument();
});
});
it("opens period date modal when clicking Set date button", async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(
screen.getByRole("button", { name: /set date/i }),
).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /set date/i }));
await waitFor(() => {
expect(
screen.getByRole("dialog", { name: /set period date/i }),
).toBeInTheDocument();
});
});
it("calls POST /api/cycle/period when submitting date in modal", async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(
screen.getByRole("button", { name: /set date/i }),
).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /set date/i }));
await waitFor(() => {
expect(
screen.getByRole("dialog", { name: /set period date/i }),
).toBeInTheDocument();
});
// Set up mock for the period POST and subsequent refetch
mockFetch.mockClear();
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
message: "Period start date logged successfully",
lastPeriodDate: "2024-01-15",
cycleDay: 1,
phase: "MENSTRUAL",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTodayResponse),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: "2024-01-15",
}),
});
const dateInput = screen.getByLabelText(
/when did your last period start/i,
);
fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
fireEvent.click(screen.getByRole("button", { name: /save/i }));
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("/api/cycle/period", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ startDate: "2024-01-15" }),
});
});
});
it("closes modal and refetches data after successful submission", async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(
screen.getByRole("button", { name: /set date/i }),
).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /set date/i }));
await waitFor(() => {
expect(
screen.getByRole("dialog", { name: /set period date/i }),
).toBeInTheDocument();
});
// Set up mock for the period POST and successful refetch
mockFetch.mockClear();
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
message: "Period start date logged successfully",
lastPeriodDate: "2024-01-15",
cycleDay: 1,
phase: "MENSTRUAL",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTodayResponse),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: "2024-01-15",
}),
});
const dateInput = screen.getByLabelText(
/when did your last period start/i,
);
fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
fireEvent.click(screen.getByRole("button", { name: /save/i }));
// Modal should close and dashboard should show normal content
await waitFor(() => {
expect(
screen.queryByRole("dialog", { name: /set period date/i }),
).not.toBeInTheDocument();
// Dashboard should now show the decision card
expect(screen.getByText("TRAIN")).toBeInTheDocument();
});
});
it("shows error in modal when API call fails", async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 400,
json: () =>
Promise.resolve({
error:
"User has no lastPeriodDate set. Please log your period start date first.",
}),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
...mockUserResponse,
lastPeriodDate: null,
}),
});
render(<Dashboard />);
await waitFor(() => {
expect(
screen.getByRole("button", { name: /set date/i }),
).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /set date/i }));
await waitFor(() => {
expect(
screen.getByRole("dialog", { name: /set period date/i }),
).toBeInTheDocument();
});
// Set up mock for failed API call
mockFetch.mockClear();
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve({ error: "Failed to update period date" }),
});
const dateInput = screen.getByLabelText(
/when did your last period start/i,
);
fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
fireEvent.click(screen.getByRole("button", { name: /save/i }));
// Error should appear in modal (there may be multiple alerts - dashboard error + modal error)
await waitFor(() => {
const alerts = screen.getAllByRole("alert");
const modalError = alerts.find((alert) =>
alert.textContent?.includes("Failed to update period date"),
);
expect(modalError).toBeInTheDocument();
});
// Modal should still be open
expect(
screen.getByRole("dialog", { name: /set period date/i }),
).toBeInTheDocument();
});
});
});