Add dashboard onboarding banners (P4.1)
All checks were successful
Deploy / deploy (push) Successful in 2m29s
All checks were successful
Deploy / deploy (push) Successful in 2m29s
Implement OnboardingBanner component that prompts new users to complete setup with contextual banners for: - Garmin connection (links to /settings/garmin) - Period date (button with callback for date picker) - Notification time (links to /settings) Banners display at the top of the dashboard when setup is incomplete, with icons and styled action buttons. Each banner uses role="alert" for accessibility. - Add OnboardingBanner component (16 tests) - Integrate into dashboard page (5 new tests, 28 total) - Update UserData interface to include garminConnected, notificationTime - Test count: 770 tests across 43 files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,9 @@ const mockUserResponse = {
|
||||
activeOverrides: [],
|
||||
cycleLength: 31,
|
||||
lastPeriodDate: "2024-01-01",
|
||||
garminConnected: true,
|
||||
notificationTime: "07:00",
|
||||
timezone: "America/New_York",
|
||||
};
|
||||
|
||||
describe("Dashboard", () => {
|
||||
@@ -572,4 +575,152 @@ describe("Dashboard", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onboarding banners", () => {
|
||||
it("shows no onboarding banners when setup is complete", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockTodayResponse),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUserResponse),
|
||||
});
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("TRAIN")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Should not have any onboarding messages
|
||||
expect(
|
||||
screen.queryByText(/Connect your Garmin to get started/i),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Set your last period date/i),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Set your preferred notification time/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Garmin banner when not connected", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockTodayResponse),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
...mockUserResponse,
|
||||
garminConnected: false,
|
||||
}),
|
||||
});
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Connect your Garmin to get started/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the link points to Garmin settings
|
||||
const link = screen.getByRole("link", { name: /Connect/i });
|
||||
expect(link).toHaveAttribute("href", "/settings/garmin");
|
||||
});
|
||||
|
||||
it("shows notification time banner when not set", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockTodayResponse),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
...mockUserResponse,
|
||||
notificationTime: "",
|
||||
}),
|
||||
});
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Set your preferred notification time/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the link points to settings
|
||||
const link = screen.getByRole("link", { name: /Configure/i });
|
||||
expect(link).toHaveAttribute("href", "/settings");
|
||||
});
|
||||
|
||||
it("shows multiple banners when multiple items need setup", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockTodayResponse),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
...mockUserResponse,
|
||||
garminConnected: false,
|
||||
notificationTime: "",
|
||||
}),
|
||||
});
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Connect your Garmin to get started/i),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Set your preferred notification time/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows period date banner with action button", async () => {
|
||||
// Note: When lastPeriodDate is null, /api/today returns 400 error
|
||||
// But we still want to show the onboarding banner if userData shows no period
|
||||
// This test checks that the banner appears when userData indicates no period
|
||||
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 />);
|
||||
|
||||
// The error state handles this case with a specific message
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/please log your period start date to get started/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user