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>
212 lines
6.0 KiB
TypeScript
212 lines
6.0 KiB
TypeScript
// ABOUTME: Tests for OnboardingBanner component that prompts new users to complete setup.
|
|
// ABOUTME: Covers Garmin connection, period date, and notification time banners.
|
|
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { OnboardingBanner, type OnboardingStatus } from "./onboarding-banner";
|
|
|
|
// Mock next/link
|
|
vi.mock("next/link", () => ({
|
|
default: ({
|
|
children,
|
|
href,
|
|
}: {
|
|
children: React.ReactNode;
|
|
href: string;
|
|
}) => <a href={href}>{children}</a>,
|
|
}));
|
|
|
|
describe("OnboardingBanner", () => {
|
|
const defaultStatus: OnboardingStatus = {
|
|
garminConnected: true,
|
|
lastPeriodDate: "2024-01-15",
|
|
notificationTime: "07:00",
|
|
};
|
|
|
|
describe("rendering conditions", () => {
|
|
it("renders nothing when all setup is complete", () => {
|
|
const { container } = render(<OnboardingBanner status={defaultStatus} />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it("renders Garmin banner when not connected", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, garminConnected: false }}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByText(/Connect your Garmin to get started/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders period banner when lastPeriodDate is null", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, lastPeriodDate: null }}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByText(/Set your last period date for accurate tracking/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders period banner when lastPeriodDate is empty string", () => {
|
|
render(
|
|
<OnboardingBanner status={{ ...defaultStatus, lastPeriodDate: "" }} />,
|
|
);
|
|
expect(
|
|
screen.getByText(/Set your last period date for accurate tracking/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders notification banner when notificationTime is missing", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, notificationTime: "" }}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByText(/Set your preferred notification time/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders multiple banners when multiple items are missing", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{
|
|
garminConnected: false,
|
|
lastPeriodDate: null,
|
|
notificationTime: "",
|
|
}}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByText(/Connect your Garmin to get started/i),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(/Set your last period date for accurate tracking/i),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(/Set your preferred notification time/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Garmin banner", () => {
|
|
it("links to /settings/garmin", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, garminConnected: false }}
|
|
/>,
|
|
);
|
|
const link = screen.getByRole("link", { name: /Connect/i });
|
|
expect(link).toHaveAttribute("href", "/settings/garmin");
|
|
});
|
|
|
|
it("has proper accessibility attributes", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, garminConnected: false }}
|
|
/>,
|
|
);
|
|
const banner = screen.getByRole("alert");
|
|
expect(banner).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("period date banner", () => {
|
|
it("calls onSetPeriodDate callback when button is clicked", () => {
|
|
const onSetPeriodDate = vi.fn();
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, lastPeriodDate: null }}
|
|
onSetPeriodDate={onSetPeriodDate}
|
|
/>,
|
|
);
|
|
const button = screen.getByRole("button", { name: /Set date/i });
|
|
fireEvent.click(button);
|
|
expect(onSetPeriodDate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("shows 'Set date' button for period banner", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, lastPeriodDate: null }}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByRole("button", { name: /Set date/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("notification time banner", () => {
|
|
it("links to /settings", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, notificationTime: "" }}
|
|
/>,
|
|
);
|
|
const link = screen.getByRole("link", { name: /Configure/i });
|
|
expect(link).toHaveAttribute("href", "/settings");
|
|
});
|
|
});
|
|
|
|
describe("styling", () => {
|
|
it("uses alert styling with appropriate colors", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, garminConnected: false }}
|
|
/>,
|
|
);
|
|
const banner = screen.getByRole("alert");
|
|
expect(banner.className).toContain("border");
|
|
expect(banner.className).toContain("rounded");
|
|
});
|
|
|
|
it("has proper spacing between multiple banners", () => {
|
|
const { container } = render(
|
|
<OnboardingBanner
|
|
status={{
|
|
garminConnected: false,
|
|
lastPeriodDate: null,
|
|
notificationTime: "",
|
|
}}
|
|
/>,
|
|
);
|
|
const wrapper = container.firstChild as HTMLElement;
|
|
expect(wrapper.className).toContain("space-y");
|
|
});
|
|
});
|
|
|
|
describe("icons", () => {
|
|
it("shows watch icon for Garmin banner", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, garminConnected: false }}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("⌚")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows calendar icon for period banner", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, lastPeriodDate: null }}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("📅")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows bell icon for notification banner", () => {
|
|
render(
|
|
<OnboardingBanner
|
|
status={{ ...defaultStatus, notificationTime: "" }}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("🔔")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|