Add toast notification system with sonner library
- Create Toaster component wrapping sonner at bottom-right position - Add showToast utility with success/error/info methods - Error toasts persist until dismissed, others auto-dismiss after 5s - Migrate error handling to toasts across all pages: - Dashboard (override toggle errors) - Settings (save/load success/error) - Garmin settings (connection success/error) - Calendar (load errors) - Period History (load/delete errors) - Add dark mode support for toast styling - Add Toaster provider to root layout - 27 new tests (23 toaster component + 4 integration) - Total: 977 unit tests passing P5.2 COMPLETE - All P0-P5 items now complete. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,16 @@ vi.mock("next/navigation", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock showToast utility with vi.hoisted to avoid hoisting issues
|
||||
const mockShowToast = vi.hoisted(() => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/components/ui/toaster", () => ({
|
||||
showToast: mockShowToast,
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
@@ -31,6 +41,9 @@ describe("SettingsPage", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockShowToast.success.mockClear();
|
||||
mockShowToast.error.mockClear();
|
||||
mockShowToast.info.mockClear();
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
@@ -302,7 +315,7 @@ describe("SettingsPage", () => {
|
||||
resolveSave(mockUser);
|
||||
});
|
||||
|
||||
it("shows success message on save", async () => {
|
||||
it("shows success toast on save", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -323,11 +336,13 @@ describe("SettingsPage", () => {
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/settings saved/i)).toBeInTheDocument();
|
||||
expect(mockShowToast.success).toHaveBeenCalledWith(
|
||||
"Settings saved successfully",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error on save failure", async () => {
|
||||
it("shows error toast on save failure", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -349,10 +364,9 @@ describe("SettingsPage", () => {
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/cycleLength must be between 21 and 45/i),
|
||||
).toBeInTheDocument();
|
||||
expect(mockShowToast.error).toHaveBeenCalledWith(
|
||||
"cycleLength must be between 21 and 45",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -377,7 +391,7 @@ describe("SettingsPage", () => {
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(mockShowToast.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText(/cycle length/i)).not.toBeDisabled();
|
||||
@@ -444,65 +458,20 @@ describe("SettingsPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("clears error when user starts typing", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: "Failed to save" }),
|
||||
});
|
||||
describe("toast notifications", () => {
|
||||
it("shows toast with fetch error on load failure", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: "Failed to fetch user" }),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
|
||||
expect(mockShowToast.error).toHaveBeenCalledWith(
|
||||
"Unable to fetch data. Retry?",
|
||||
);
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cycleLengthInput = screen.getByLabelText(/cycle length/i);
|
||||
fireEvent.change(cycleLengthInput, { target: { value: "30" } });
|
||||
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears success message when user modifies form", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/settings saved/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cycleLengthInput = screen.getByLabelText(/cycle length/i);
|
||||
fireEvent.change(cycleLengthInput, { target: { value: "30" } });
|
||||
|
||||
expect(screen.queryByText(/settings saved/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -643,7 +612,7 @@ describe("SettingsPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error if logout fails", async () => {
|
||||
it("shows error toast if logout fails", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -666,8 +635,7 @@ describe("SettingsPage", () => {
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByText(/logout failed/i)).toBeInTheDocument();
|
||||
expect(mockShowToast.error).toHaveBeenCalledWith("Logout failed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user