More tests in pages

This commit is contained in:
Arthur Belleville 2025-10-27 20:58:01 +01:00
parent a18489c056
commit 0d0abaf945
No known key found for this signature in database
13 changed files with 1465 additions and 14 deletions

View file

@ -1,15 +1,29 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import { NotFoundPage } from "./NotFoundPage";
const mockNavigate = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => mockNavigate,
};
});
describe("NotFoundPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = render(
<BrowserRouter>
@ -27,4 +41,53 @@ describe("NotFoundPage", () => {
);
expect(screen.getByText("404")).toBeInTheDocument();
});
it("displays page not found title", () => {
render(
<BrowserRouter>
<NotFoundPage />
</BrowserRouter>
);
expect(screen.getByText(/pages:notFound.title/i)).toBeInTheDocument();
});
it("displays description message", () => {
render(
<BrowserRouter>
<NotFoundPage />
</BrowserRouter>
);
expect(screen.getByText(/pages:notFound.description/i)).toBeInTheDocument();
});
it("displays go back button", () => {
render(
<BrowserRouter>
<NotFoundPage />
</BrowserRouter>
);
expect(screen.getByRole("button", { name: /pages:notFound.goBack/i })).toBeInTheDocument();
});
it("navigates back when go back button is clicked", () => {
render(
<BrowserRouter>
<NotFoundPage />
</BrowserRouter>
);
const goBackButton = screen.getByRole("button", { name: /pages:notFound.goBack/i });
fireEvent.click(goBackButton);
expect(mockNavigate).toHaveBeenCalledWith("/");
});
it("displays go home link", () => {
render(
<BrowserRouter>
<NotFoundPage />
</BrowserRouter>
);
expect(screen.getByText(/pages:notFound.goHome/i)).toBeInTheDocument();
});
});

View file

@ -1,13 +1,17 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { PublicBookingPage } from "./PublicBookingPage";
const mockNavigate = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useParams: () => ({ username_id: "test-user", event_type: "test-event" }),
useSearchParams: () => [new URLSearchParams(), vi.fn()],
useSearchParams: () => [new URLSearchParams("date=2024-01-15&time=14:00"), vi.fn()],
useNavigate: () => mockNavigate,
};
});
@ -17,9 +21,110 @@ vi.mock("react-i18next", () => ({
}),
}));
vi.mock("../hooks/eventTypes", () => ({
usePublicEventType: () => ({
eventType: {
id: "test-event-id",
name: "Test Event Type",
duration: 30,
description: "Test event description",
},
isLoading: false,
error: null,
}),
}));
vi.mock("../hooks/availability", () => ({
usePublicAvailability: () => ({
availableSlots: [
{ date: "2024-01-15", time: "14:00" },
{ date: "2024-01-15", time: "15:00" },
{ date: "2024-01-16", time: "10:00" },
],
isLoading: false,
}),
}));
vi.mock("../hooks/bookings", () => ({
useCreateBooking: () => ({
mutate: vi.fn(),
isPending: false,
}),
}));
describe("PublicBookingPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<PublicBookingPage />);
expect(container).toBeInTheDocument();
});
it("displays event type name", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByText("Test Event Type")).toBeInTheDocument();
});
it("displays event type description", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByText("Test event description")).toBeInTheDocument();
});
it("displays duration information", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByText(/30/i)).toBeInTheDocument();
});
it("renders calendar for date selection", () => {
renderWithProviders(<PublicBookingPage />);
// Look for calendar component
expect(screen.getByText(/pages:booking.selectDate/i)).toBeInTheDocument();
});
it("renders time slot selection", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByText(/pages:booking.selectTime/i)).toBeInTheDocument();
});
it("displays available time slots", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByText("14:00")).toBeInTheDocument();
expect(screen.getByText("15:00")).toBeInTheDocument();
});
});
// Note: Testing loading and error states would require re-mocking the hook with different values.
// The current test suite covers the happy path. State testing is better handled with integration tests.
describe("PublicBookingPage - Booking Flow", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("has booking form", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByRole("form", { name: /pages:booking.form/i })).toBeInTheDocument();
});
it("requires user information for booking", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByLabelText(/pages:booking.name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/pages:booking.email/i)).toBeInTheDocument();
});
it("has submit button for booking", () => {
renderWithProviders(<PublicBookingPage />);
expect(screen.getByRole("button", { name: /pages:booking.submit/i })).toBeInTheDocument();
});
});

View file

@ -1,3 +1,4 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { PublicNotePage } from "./PublicNotePage";
@ -16,9 +17,41 @@ vi.mock("react-i18next", () => ({
}),
}));
vi.mock("../hooks/notes", () => ({
usePublicNote: () => ({
note: {
id: "test-note-id",
title: "Test Public Note",
content: "This is the content of the public note",
created_at: "2023-01-01",
},
isLoading: false,
error: null,
}),
}));
describe("PublicNotePage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<PublicNotePage />);
expect(container).toBeInTheDocument();
});
it("displays note title", () => {
renderWithProviders(<PublicNotePage />);
expect(screen.getByText("Test Public Note")).toBeInTheDocument();
});
it("displays note content", () => {
renderWithProviders(<PublicNotePage />);
expect(screen.getByText(/This is the content of the public note/i)).toBeInTheDocument();
});
});
// Note: Testing loading and error states would require re-mocking the hook with different values.
// The current test suite covers the happy path. State testing is better handled with integration tests.

View file

@ -1,7 +1,10 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { ChatPage } from "./chat";
const mockSetActiveChannel = vi.fn();
vi.mock("../hooks/channel", () => ({
useChannelFromUrl: () => ({
channel: null,
@ -11,7 +14,10 @@ vi.mock("../hooks/channel", () => ({
vi.mock("../hooks/tablos", () => ({
useTablosList: () => ({
data: [],
data: [
{ id: "tablo-1", name: "Test Tablo 1" },
{ id: "tablo-2", name: "Test Tablo 2" },
],
}),
}));
@ -26,9 +32,9 @@ vi.mock("../providers/UserStoreProvider", () => ({
vi.mock("../providers/ChatProvider", () => ({
useChatClient: () => null,
useChatContext: () => ({
client: null,
client: { id: "test-client" },
channel: null,
setActiveChannel: vi.fn(),
setActiveChannel: mockSetActiveChannel,
}),
}));
@ -49,15 +55,65 @@ vi.mock("stream-chat-react", () => ({
useChannelStateContext: () => ({ channel: null }),
useCreateChatClient: () => null,
useChatContext: () => ({
client: null,
client: { id: "test-client" },
channel: null,
setActiveChannel: vi.fn(),
setActiveChannel: mockSetActiveChannel,
}),
}));
describe("ChatPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<ChatPage />);
expect(container).toBeInTheDocument();
});
it("renders channel list", () => {
renderWithProviders(<ChatPage />);
expect(screen.getByTestId("channel-list")).toBeInTheDocument();
});
it("renders channel window", () => {
renderWithProviders(<ChatPage />);
expect(screen.getByTestId("channel")).toBeInTheDocument();
expect(screen.getByTestId("window")).toBeInTheDocument();
});
it("renders message list and input", () => {
renderWithProviders(<ChatPage />);
expect(screen.getByTestId("message-list")).toBeInTheDocument();
expect(screen.getByTestId("message-input")).toBeInTheDocument();
});
it("applies correct filters for channel list", () => {
renderWithProviders(<ChatPage />);
// ChannelList should be rendered with proper filters
expect(screen.getByTestId("channel-list")).toBeInTheDocument();
});
});
// Note: Testing channel from URL would require re-mocking the hook with different values
// This is better tested with integration tests or by testing the hook separately.
describe("ChatPage - Channel List Toggle", () => {
it("starts with channel list expanded when no channel in URL", () => {
renderWithProviders(<ChatPage />);
const channelListContainer = screen.getByTestId("channel-list").parentElement;
expect(channelListContainer?.className).toContain("w-80");
});
it("has collapsible channel list", () => {
renderWithProviders(<ChatPage />);
const channelListContainer = screen.getByTestId("channel-list").parentElement;
expect(channelListContainer).toBeInTheDocument();
});
});

View file

@ -1,16 +1,133 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { FeedbackPage } from "./feedback";
const mockNavigate = vi.fn();
const mockCreateFeedback = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => mockNavigate,
};
});
vi.mock("../hooks/feedback", () => ({
useCreateFeedback: () => ({
createFeedback: mockCreateFeedback,
isSuccess: false,
isPending: false,
}),
}));
describe("FeedbackPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<FeedbackPage />);
expect(container).toBeInTheDocument();
});
it("renders form with all elements", () => {
renderWithProviders(<FeedbackPage />);
expect(screen.getByText(/pages:feedback.title/i)).toBeInTheDocument();
expect(screen.getByLabelText(/pages:feedback.form.type.label/i)).toBeInTheDocument();
expect(screen.getByLabelText(/pages:feedback.form.message.label/i)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /pages:feedback.buttons.send/i })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /pages:feedback.buttons.cancel/i })
).toBeInTheDocument();
});
it("shows back button that navigates back", () => {
renderWithProviders(<FeedbackPage />);
const backButton = screen.getAllByRole("button")[0]; // First button is the back button
fireEvent.click(backButton);
expect(mockNavigate).toHaveBeenCalledWith(-1);
});
it("allows selecting feedback type", async () => {
renderWithProviders(<FeedbackPage />);
const selectTrigger = screen.getByRole("combobox");
fireEvent.click(selectTrigger);
await waitFor(() => {
const bugOption = screen.getByRole("option", { name: /pages:feedback.form.type.bug/i });
expect(bugOption).toBeInTheDocument();
});
});
it("updates message textarea on change", () => {
renderWithProviders(<FeedbackPage />);
const messageTextarea = screen.getByPlaceholderText(
/pages:feedback.form.message.placeholder/i
) as HTMLTextAreaElement;
fireEvent.change(messageTextarea, { target: { value: "This is my feedback" } });
expect(messageTextarea.value).toBe("This is my feedback");
});
it("disables submit button when message is empty", () => {
renderWithProviders(<FeedbackPage />);
const submitButton = screen.getByRole("button", { name: /pages:feedback.buttons.send/i });
expect(submitButton).toBeDisabled();
});
it("enables submit button when message is filled", () => {
renderWithProviders(<FeedbackPage />);
const messageTextarea = screen.getByPlaceholderText(/pages:feedback.form.message.placeholder/i);
fireEvent.change(messageTextarea, { target: { value: "This is my feedback" } });
const submitButton = screen.getByRole("button", { name: /pages:feedback.buttons.send/i });
expect(submitButton).not.toBeDisabled();
});
it("submits form with feedback data", async () => {
renderWithProviders(<FeedbackPage />);
const messageTextarea = screen.getByPlaceholderText(/pages:feedback.form.message.placeholder/i);
fireEvent.change(messageTextarea, { target: { value: "This is my feedback" } });
const submitButton = screen.getByRole("button", { name: /pages:feedback.buttons.send/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockCreateFeedback).toHaveBeenCalledWith({
fd_type: "improvement",
message: "This is my feedback",
});
});
});
it("navigates back when cancel button is clicked", () => {
renderWithProviders(<FeedbackPage />);
const cancelButton = screen.getByRole("button", { name: /pages:feedback.buttons.cancel/i });
fireEvent.click(cancelButton);
expect(mockNavigate).toHaveBeenCalledWith(-1);
});
});
// Note: Testing success state would require re-rendering with different mock values
// which is complex with vi.mock at the module level. This would be better tested
// with integration tests or by splitting the success view into a separate component.

View file

@ -1,13 +1,18 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { JoinPage } from "./join";
const mockNavigate = vi.fn();
const mockJoinTablo = vi.fn();
const mockSearchParams = new URLSearchParams("tablo_name=Test%20Tablo&token=test-token");
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useParams: () => ({ invite_code: "test-invite" }),
useNavigate: () => vi.fn(),
useSearchParams: () => [mockSearchParams],
useNavigate: () => mockNavigate,
};
});
@ -17,9 +22,77 @@ vi.mock("react-i18next", () => ({
}),
}));
vi.mock("../hooks/invite", () => ({
useJoinTablo: () => mockJoinTablo,
}));
vi.mock("@xtablo/shared", () => ({
toast: {
add: vi.fn(),
},
}));
describe("JoinPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<JoinPage />);
expect(container).toBeInTheDocument();
});
it("displays the tablo name from query params", () => {
renderWithProviders(<JoinPage />);
expect(screen.getByText(/Test Tablo/i)).toBeInTheDocument();
});
it("renders accept and reject buttons", () => {
renderWithProviders(<JoinPage />);
expect(screen.getByRole("button", { name: /Accepter l'invitation/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Refuser/i })).toBeInTheDocument();
});
it("navigates to home when reject button is clicked", () => {
renderWithProviders(<JoinPage />);
const rejectButton = screen.getByRole("button", { name: /Refuser/i });
fireEvent.click(rejectButton);
expect(mockNavigate).toHaveBeenCalledWith("/");
});
it("calls joinTablo when accept button is clicked", async () => {
renderWithProviders(<JoinPage />);
const acceptButton = screen.getByRole("button", { name: /Accepter l'invitation/i });
fireEvent.click(acceptButton);
await waitFor(() => {
expect(mockJoinTablo).toHaveBeenCalledWith(
{ token: "test-token" },
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
})
);
});
});
// Note: Testing successful join navigation would require the callback to be triggered
// which is complex to test in isolation. This is better tested with integration tests.
// Note: Testing error toast scenarios with missing user/token would require
// dynamic mocking which is complex. These scenarios are better tested with integration tests.
it("displays invitation message", () => {
renderWithProviders(<JoinPage />);
expect(screen.getByText(/Vous avez été invité\(e\) à rejoindre ce tablo/i)).toBeInTheDocument();
});
// Note: Testing URL decoding with different params would require re-rendering with new mocks
// This is covered by the existing test that shows the tablo name correctly.
});

View file

@ -1,7 +1,11 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { LoginPage } from "./login";
const mockNavigate = vi.fn();
const mockLogin = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
@ -12,16 +16,110 @@ vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
<a href={to}>{children}</a>
),
};
});
vi.mock("../hooks/auth", () => ({
useLoginEmail: () => ({
mutate: mockLogin,
isPending: false,
errors: null,
}),
}));
describe("LoginPage", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<LoginPage />);
expect(container).toBeInTheDocument();
});
it("renders all form elements", () => {
renderWithProviders(<LoginPage />);
expect(screen.getByLabelText(/common:labels.email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/common:labels.password/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /auth:login.loginButton/i })).toBeInTheDocument();
});
it("displays theme toggle button", () => {
renderWithProviders(<LoginPage />);
const themeButton = screen.getByRole("button", { name: /auth:common.themeToggle/i });
expect(themeButton).toBeInTheDocument();
});
it("shows link to signup page", () => {
renderWithProviders(<LoginPage />);
const signupLink = screen.getByText(/auth:login.signupLink/i);
expect(signupLink).toBeInTheDocument();
});
it("updates email input on change", () => {
renderWithProviders(<LoginPage />);
const emailInput = screen.getByPlaceholderText(
/auth:login.emailPlaceholder/i
) as HTMLInputElement;
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
expect(emailInput.value).toBe("test@example.com");
});
it("updates password input on change", () => {
renderWithProviders(<LoginPage />);
const passwordInput = screen.getByPlaceholderText(
/auth:login.passwordPlaceholder/i
) as HTMLInputElement;
fireEvent.change(passwordInput, { target: { value: "password123" } });
expect(passwordInput.value).toBe("password123");
});
it("submits form with email and password", async () => {
renderWithProviders(<LoginPage />);
const emailInput = screen.getByPlaceholderText(/auth:login.emailPlaceholder/i);
const passwordInput = screen.getByPlaceholderText(/auth:login.passwordPlaceholder/i);
const submitButton = screen.getByRole("button", { name: /auth:login.loginButton/i });
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
it("uses redirectUrl from localStorage when available", () => {
localStorage.setItem("redirectUrl", "/dashboard");
renderWithProviders(<LoginPage />);
// Component should read from localStorage
expect(localStorage.getItem("redirectUrl")).toBe("/dashboard");
});
it("prevents form submission when fields are empty", () => {
renderWithProviders(<LoginPage />);
const submitButton = screen.getByRole("button", { name: /auth:login.loginButton/i });
fireEvent.click(submitButton);
// Form should not submit due to HTML5 validation (required fields)
expect(mockLogin).not.toHaveBeenCalled();
});
});

View file

@ -1,12 +1,20 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import NotesPage from "./notes";
const mockNavigate = vi.fn();
const mockCreateNote = vi.fn();
const mockUpdateNote = vi.fn();
const mockDeleteNote = vi.fn();
const mockUpdateSharing = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
useParams: () => ({ noteId: undefined }),
};
});
@ -16,9 +24,146 @@ vi.mock("react-i18next", () => ({
}),
}));
describe("NotesPage", () => {
vi.mock("../hooks/notes", () => ({
useNotes: () => ({
notes: [
{ id: "note-1", title: "Test Note 1", content: "Content 1", created_at: "2023-01-01" },
{ id: "note-2", title: "Test Note 2", content: "Content 2", created_at: "2023-01-02" },
],
isLoading: false,
}),
useNote: () => ({
note: null,
isLoading: false,
}),
useCreateNote: () => ({
mutate: mockCreateNote,
isPending: false,
}),
useUpdateNote: () => ({
mutate: mockUpdateNote,
isPending: false,
}),
useDeleteNote: () => ({
mutate: mockDeleteNote,
isPending: false,
}),
useNoteSharing: () => ({
isPublic: false,
isSharedWithAllTablos: false,
isLoading: false,
}),
useUpdateNoteSharing: () => ({
mutate: mockUpdateSharing,
isPending: false,
}),
}));
vi.mock("../providers/UserStoreProvider", () => ({
useIsReadOnlyUser: () => false,
useUser: () => ({
id: "test-user-id",
name: "Test User",
}),
TestUserStoreProvider: ({ children }: { children: React.ReactNode }) => children,
}));
vi.mock("@xtablo/shared", () => ({
toast: {
add: vi.fn(),
},
}));
describe("NotesPage - Create Mode", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<NotesPage mode="create" />);
expect(container).toBeInTheDocument();
});
it("displays page title", () => {
renderWithProviders(<NotesPage mode="create" />);
expect(screen.getByText(/notes:title/i)).toBeInTheDocument();
});
it("renders notes sidebar with list of notes", () => {
renderWithProviders(<NotesPage mode="create" />);
expect(screen.getByText("Test Note 1")).toBeInTheDocument();
expect(screen.getByText("Test Note 2")).toBeInTheDocument();
});
it("renders new note button", () => {
renderWithProviders(<NotesPage mode="create" />);
const newNoteButtons = screen.getAllByRole("button", { name: /notes:newNote/i });
expect(newNoteButtons.length).toBeGreaterThan(0);
});
it("has title input field", () => {
renderWithProviders(<NotesPage mode="create" />);
expect(screen.getByPlaceholderText(/notes:titlePlaceholder/i)).toBeInTheDocument();
});
it("updates title input on change", () => {
renderWithProviders(<NotesPage mode="create" />);
const titleInput = screen.getByPlaceholderText(/notes:titlePlaceholder/i) as HTMLInputElement;
fireEvent.change(titleInput, { target: { value: "My New Note" } });
expect(titleInput.value).toBe("My New Note");
});
it("renders save button", () => {
renderWithProviders(<NotesPage mode="create" />);
expect(screen.getByRole("button", { name: /notes:save/i })).toBeInTheDocument();
});
it("calls createNote when save button is clicked", async () => {
renderWithProviders(<NotesPage mode="create" />);
const titleInput = screen.getByPlaceholderText(/notes:titlePlaceholder/i);
fireEvent.change(titleInput, { target: { value: "My New Note" } });
const saveButton = screen.getByRole("button", { name: /notes:save/i });
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockCreateNote).toHaveBeenCalled();
});
});
});
describe("NotesPage - Edit Mode", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders in edit mode", () => {
const { container } = renderWithProviders(<NotesPage mode="edit" />);
expect(container).toBeInTheDocument();
});
// Note: Testing delete button visibility in edit mode with a specific noteId would require
// re-mocking useParams with different values, which is complex with module-level mocks.
});
// Note: Testing empty state and loading state would require re-mocking the hooks with different values.
// The current test suite already covers the main happy path with notes present.
// Additional state testing is better handled with integration tests.
describe("NotesPage - Sidebar Toggle", () => {
it("has sidebar toggle button", () => {
renderWithProviders(<NotesPage mode="create" />);
// Sidebar should be visible by default, look for collapse button
const toggleButtons = screen.getAllByRole("button");
expect(toggleButtons.length).toBeGreaterThan(0);
});
});

View file

@ -2,18 +2,92 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { OAuthSigninPage } from "./oauth-signin";
const mockNavigate = vi.fn();
const mockSignUpToStream = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
useSearchParams: () => [new URLSearchParams(), vi.fn()],
};
});
vi.mock("@xtablo/shared/hooks/auth", () => ({
useSignUpToStream: () => ({
signUpToStream: mockSignUpToStream,
}),
}));
vi.mock("../lib/api", () => ({
api: {},
}));
describe("OAuthSigninPage", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<OAuthSigninPage />);
expect(container).toBeInTheDocument();
});
it("renders empty component", () => {
const { container } = renderWithProviders(<OAuthSigninPage />);
expect(container.firstChild).toBeEmptyDOMElement();
});
it("navigates to home when session exists without redirectUrl", () => {
renderWithProviders(<OAuthSigninPage />);
vi.advanceTimersByTime(150);
expect(mockSignUpToStream).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith("/");
});
it("navigates to redirectUrl when session exists with redirectUrl", () => {
localStorage.setItem("redirectUrl", "/dashboard");
renderWithProviders(<OAuthSigninPage />);
vi.advanceTimersByTime(150);
expect(mockSignUpToStream).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith("/dashboard");
expect(localStorage.getItem("redirectUrl")).toBeNull();
});
it("decodes redirectUrl before navigation", () => {
localStorage.setItem("redirectUrl", "%2Fdashboard%2Ftest");
renderWithProviders(<OAuthSigninPage />);
vi.advanceTimersByTime(150);
expect(mockNavigate).toHaveBeenCalledWith("/dashboard/test");
});
it("signs up to stream with access token", () => {
renderWithProviders(<OAuthSigninPage />);
vi.advanceTimersByTime(150);
expect(mockSignUpToStream).toHaveBeenCalled();
});
it("clears interval on unmount", () => {
const { unmount } = renderWithProviders(<OAuthSigninPage />);
unmount();
// If interval isn't cleared, this would cause issues
vi.advanceTimersByTime(150);
});
});

View file

@ -1,16 +1,122 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { ResetPasswordPage } from "./reset-password";
const mockNavigate = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => mockNavigate,
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
<a href={to}>{children}</a>
),
};
});
describe("ResetPasswordPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<ResetPasswordPage />);
expect(container).toBeInTheDocument();
});
it("renders form with email input", () => {
renderWithProviders(<ResetPasswordPage />);
expect(screen.getByText(/Mot de passe oublié/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Email/i)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Réinitialiser le mot de passe/i })
).toBeInTheDocument();
});
it("displays help text", () => {
renderWithProviders(<ResetPasswordPage />);
expect(screen.getByText(/Entrez votre adresse email/i)).toBeInTheDocument();
});
it("shows link back to login", () => {
renderWithProviders(<ResetPasswordPage />);
const loginLink = screen.getByText(/Retour à la connexion/i);
expect(loginLink).toBeInTheDocument();
});
it("updates email input on change", () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i) as HTMLInputElement;
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
expect(emailInput.value).toBe("test@example.com");
});
it("submits form and shows success message", async () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i);
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/Email envoyé/i)).toBeInTheDocument();
});
});
it("displays email in success message", async () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i);
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/test@example.com/i)).toBeInTheDocument();
});
});
it("shows return to login button in success state", async () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i);
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByRole("button", { name: /Retour à la connexion/i })).toBeInTheDocument();
});
});
it("requires email field to be filled", () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i);
expect(emailInput).toHaveAttribute("required");
});
it("requires valid email format", () => {
renderWithProviders(<ResetPasswordPage />);
const emailInput = screen.getByLabelText(/Email/i);
expect(emailInput).toHaveAttribute("type", "email");
});
});

View file

@ -1,7 +1,14 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import SettingsPage from "./settings";
const mockUpdateProfile = vi.fn();
const mockUploadAvatar = vi.fn();
const mockRemoveAvatar = vi.fn();
const mockUpdateIntroduction = vi.fn();
const mockSetDraftIntroduction = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
@ -27,9 +34,262 @@ vi.mock("react-router-dom", async () => {
};
});
vi.mock("../hooks/profile", () => ({
useUpdateProfile: () => ({
mutate: mockUpdateProfile,
isPending: false,
}),
useUploadAvatar: () => ({
mutate: mockUploadAvatar,
}),
useRemoveAvatar: () => ({
mutateAsync: mockRemoveAvatar,
isPending: false,
}),
}));
vi.mock("../hooks/intros", () => ({
useIntroduction: () => ({
introduction: { intro_email: "Test introduction" },
updateIntroduction: mockUpdateIntroduction,
setDraftIntroduction: mockSetDraftIntroduction,
isPending: false,
}),
}));
vi.mock("@xtablo/shared", () => ({
toast: {
add: vi.fn(),
},
}));
describe("SettingsPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<SettingsPage />);
expect(container).toBeInTheDocument();
});
it("renders all settings sections", () => {
renderWithProviders(<SettingsPage />);
expect(screen.getByText(/settings:avatar.title/i)).toBeInTheDocument();
expect(screen.getByText(/settings:personalInfo.title/i)).toBeInTheDocument();
expect(screen.getByText(/settings:introduction.title/i)).toBeInTheDocument();
});
it("displays user information in form fields", () => {
renderWithProviders(<SettingsPage />);
const firstNameInput = screen.getByLabelText(
/settings:personalInfo.firstName/i
) as HTMLInputElement;
const lastNameInput = screen.getByLabelText(
/settings:personalInfo.lastName/i
) as HTMLInputElement;
const emailInput = screen.getByLabelText(/settings:personalInfo.email/i) as HTMLInputElement;
expect(firstNameInput.value).toBe("John");
expect(lastNameInput.value).toBe("Doe");
expect(emailInput.value).toBe("john@example.com");
});
it("email input is disabled", () => {
renderWithProviders(<SettingsPage />);
const emailInput = screen.getByLabelText(/settings:personalInfo.email/i);
expect(emailInput).toBeDisabled();
});
it("updates first name on change", () => {
renderWithProviders(<SettingsPage />);
const firstNameInput = screen.getByLabelText(
/settings:personalInfo.firstName/i
) as HTMLInputElement;
fireEvent.change(firstNameInput, { target: { value: "Jane" } });
expect(firstNameInput.value).toBe("Jane");
});
it("updates last name on change", () => {
renderWithProviders(<SettingsPage />);
const lastNameInput = screen.getByLabelText(
/settings:personalInfo.lastName/i
) as HTMLInputElement;
fireEvent.change(lastNameInput, { target: { value: "Smith" } });
expect(lastNameInput.value).toBe("Smith");
});
it("calls updateProfile with new values when save button is clicked", async () => {
renderWithProviders(<SettingsPage />);
const firstNameInput = screen.getByLabelText(/settings:personalInfo.firstName/i);
const lastNameInput = screen.getByLabelText(/settings:personalInfo.lastName/i);
fireEvent.change(firstNameInput, { target: { value: "Jane" } });
fireEvent.change(lastNameInput, { target: { value: "Smith" } });
const saveButton = screen.getByRole("button", { name: /settings:personalInfo.save/i });
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockUpdateProfile).toHaveBeenCalledWith({
firstName: "Jane",
lastName: "Smith",
});
});
});
it("displays introduction text", () => {
renderWithProviders(<SettingsPage />);
const introTextarea = screen.getByLabelText(
/settings:introduction.title/i
) as HTMLTextAreaElement;
expect(introTextarea.value).toBe("Test introduction");
});
it("updates introduction text on change", () => {
renderWithProviders(<SettingsPage />);
const introTextarea = screen.getByLabelText(/settings:introduction.title/i);
fireEvent.change(introTextarea, { target: { value: "New introduction" } });
expect(mockSetDraftIntroduction).toHaveBeenCalled();
});
it("calls updateIntroduction when save button is clicked", async () => {
renderWithProviders(<SettingsPage />);
const saveButtons = screen.getAllByRole("button", { name: /common:buttons.save/i });
const introSaveButton = saveButtons[saveButtons.length - 1]; // Last save button is for introduction
fireEvent.click(introSaveButton);
await waitFor(() => {
expect(mockUpdateIntroduction).toHaveBeenCalledWith({
intro_email: "Test introduction",
});
});
});
it("renders avatar with user initials fallback", () => {
renderWithProviders(<SettingsPage />);
const avatar = screen.getByAltText("Avatar");
expect(avatar).toBeInTheDocument();
});
it("has file input for avatar upload", () => {
renderWithProviders(<SettingsPage />);
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
expect(fileInput).toBeInTheDocument();
expect(fileInput).toHaveAttribute("accept", "image/*");
});
it("shows choose file button", () => {
renderWithProviders(<SettingsPage />);
expect(screen.getByRole("button", { name: /settings:avatar.chooseFile/i })).toBeInTheDocument();
});
it("validates file size on avatar upload", async () => {
const { toast } = await import("@xtablo/shared");
renderWithProviders(<SettingsPage />);
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
// Create a large file (> 5MB)
const largeFile = new File(["x".repeat(6 * 1024 * 1024)], "large.jpg", { type: "image/jpeg" });
Object.defineProperty(fileInput, "files", {
value: [largeFile],
writable: false,
});
fireEvent.change(fileInput);
await waitFor(() => {
expect(toast.add).toHaveBeenCalledWith(
expect.objectContaining({
title: "settings:toasts.error",
description: "settings:toasts.fileTooLarge",
type: "error",
})
);
});
});
it("validates file type on avatar upload", async () => {
const { toast } = await import("@xtablo/shared");
renderWithProviders(<SettingsPage />);
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
// Create a non-image file
const textFile = new File(["test"], "test.txt", { type: "text/plain" });
Object.defineProperty(fileInput, "files", {
value: [textFile],
writable: false,
});
fireEvent.change(fileInput);
await waitFor(() => {
expect(toast.add).toHaveBeenCalledWith(
expect.objectContaining({
title: "settings:toasts.error",
description: "settings:toasts.invalidImage",
type: "error",
})
);
});
});
it("shows delete avatar button when avatar exists", () => {
renderWithProviders(<SettingsPage />);
expect(screen.getByRole("button", { name: /settings:avatar.delete/i })).toBeInTheDocument();
});
it("opens confirmation dialog when delete button is clicked", () => {
renderWithProviders(<SettingsPage />);
const deleteButton = screen.getByRole("button", { name: /settings:avatar.delete/i });
fireEvent.click(deleteButton);
expect(screen.getByText(/settings:avatar.deleteTitle/i)).toBeInTheDocument();
expect(screen.getByText(/settings:avatar.deleteDescription/i)).toBeInTheDocument();
});
it("calls removeAvatar when deletion is confirmed", async () => {
renderWithProviders(<SettingsPage />);
const deleteButton = screen.getByRole("button", { name: /settings:avatar.delete/i });
fireEvent.click(deleteButton);
// Find and click confirm button in dialog
const confirmButtons = screen.getAllByRole("button", { name: /settings:avatar.delete/i });
const confirmButton = confirmButtons[confirmButtons.length - 1]; // Last one is in the dialog
fireEvent.click(confirmButton);
await waitFor(() => {
expect(mockRemoveAvatar).toHaveBeenCalled();
});
});
it("renders language selector", () => {
renderWithProviders(<SettingsPage />);
// LanguageSelector is a separate component, just verify it's rendered
expect(screen.getByText(/settings:title/i)).toBeInTheDocument();
});
});

View file

@ -1,7 +1,11 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { SignUpPage } from "./signup";
const mockNavigate = vi.fn();
const mockSignUp = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
@ -12,16 +16,214 @@ vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
<a href={to}>{children}</a>
),
};
});
vi.mock("../hooks/auth", () => ({
useSignUp: () => ({
mutate: mockSignUp,
isPending: false,
}),
}));
describe("SignUpPage", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<SignUpPage />);
expect(container).toBeInTheDocument();
});
it("renders all form fields", () => {
renderWithProviders(<SignUpPage />);
expect(screen.getByLabelText(/auth:signup.firstName/i)).toBeInTheDocument();
expect(screen.getByLabelText(/auth:signup.lastName/i)).toBeInTheDocument();
expect(screen.getByLabelText(/auth:signup.email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/common:labels.password/i)).toBeInTheDocument();
expect(screen.getByLabelText(/auth:signup.confirmPassword/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /auth:signup.signupButton/i })).toBeInTheDocument();
});
it("shows link to login page", () => {
renderWithProviders(<SignUpPage />);
const loginLink = screen.getByText(/auth:signup.loginLink/i);
expect(loginLink).toBeInTheDocument();
});
it("updates form fields on change", () => {
renderWithProviders(<SignUpPage />);
const firstNameInput = screen.getByPlaceholderText(
/auth:signup.firstNamePlaceholder/i
) as HTMLInputElement;
const lastNameInput = screen.getByPlaceholderText(
/auth:signup.lastNamePlaceholder/i
) as HTMLInputElement;
const emailInput = screen.getByPlaceholderText(
/auth:signup.emailPlaceholder/i
) as HTMLInputElement;
const passwordInput = screen.getByPlaceholderText(
/auth:signup.passwordPlaceholder/i
) as HTMLInputElement;
const confirmPasswordInput = screen.getByPlaceholderText(
/auth:signup.confirmPasswordPlaceholder/i
) as HTMLInputElement;
fireEvent.change(firstNameInput, { target: { value: "John" } });
fireEvent.change(lastNameInput, { target: { value: "Doe" } });
fireEvent.change(emailInput, { target: { value: "john@example.com" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.change(confirmPasswordInput, { target: { value: "password123" } });
expect(firstNameInput.value).toBe("John");
expect(lastNameInput.value).toBe("Doe");
expect(emailInput.value).toBe("john@example.com");
expect(passwordInput.value).toBe("password123");
expect(confirmPasswordInput.value).toBe("password123");
});
it("shows error when password is too short", async () => {
renderWithProviders(<SignUpPage />);
const passwordInput = screen.getByPlaceholderText(/auth:signup.passwordPlaceholder/i);
const confirmPasswordInput = screen.getByPlaceholderText(
/auth:signup.confirmPasswordPlaceholder/i
);
const submitButton = screen.getByRole("button", { name: /auth:signup.signupButton/i });
const termsCheckbox = screen.getByRole("checkbox", { name: /auth:signup.termsAccept/i });
fireEvent.change(screen.getByPlaceholderText(/auth:signup.firstNamePlaceholder/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.lastNamePlaceholder/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.emailPlaceholder/i), {
target: { value: "john@example.com" },
});
fireEvent.change(passwordInput, { target: { value: "short" } });
fireEvent.change(confirmPasswordInput, { target: { value: "short" } });
fireEvent.click(termsCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/auth:signup.errors.passwordLength/i)).toBeInTheDocument();
});
expect(mockSignUp).not.toHaveBeenCalled();
});
it("shows error when passwords don't match", async () => {
renderWithProviders(<SignUpPage />);
const passwordInput = screen.getByPlaceholderText(/auth:signup.passwordPlaceholder/i);
const confirmPasswordInput = screen.getByPlaceholderText(
/auth:signup.confirmPasswordPlaceholder/i
);
const submitButton = screen.getByRole("button", { name: /auth:signup.signupButton/i });
const termsCheckbox = screen.getByRole("checkbox", { name: /auth:signup.termsAccept/i });
fireEvent.change(screen.getByPlaceholderText(/auth:signup.firstNamePlaceholder/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.lastNamePlaceholder/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.emailPlaceholder/i), {
target: { value: "john@example.com" },
});
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.change(confirmPasswordInput, { target: { value: "different123" } });
fireEvent.click(termsCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/auth:signup.errors.passwordMatch/i)).toBeInTheDocument();
});
expect(mockSignUp).not.toHaveBeenCalled();
});
it("shows error for invalid email", async () => {
renderWithProviders(<SignUpPage />);
const submitButton = screen.getByRole("button", { name: /auth:signup.signupButton/i });
const termsCheckbox = screen.getByRole("checkbox", { name: /auth:signup.termsAccept/i });
fireEvent.change(screen.getByPlaceholderText(/auth:signup.firstNamePlaceholder/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.lastNamePlaceholder/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.emailPlaceholder/i), {
target: { value: "invalid-email" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.passwordPlaceholder/i), {
target: { value: "password123" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.confirmPasswordPlaceholder/i), {
target: { value: "password123" },
});
fireEvent.click(termsCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/auth:signup.errors.invalidEmail/i)).toBeInTheDocument();
});
expect(mockSignUp).not.toHaveBeenCalled();
});
it("submits form with valid data", async () => {
renderWithProviders(<SignUpPage />);
const submitButton = screen.getByRole("button", { name: /auth:signup.signupButton/i });
const termsCheckbox = screen.getByRole("checkbox", { name: /auth:signup.termsAccept/i });
fireEvent.change(screen.getByPlaceholderText(/auth:signup.firstNamePlaceholder/i), {
target: { value: "John" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.lastNamePlaceholder/i), {
target: { value: "Doe" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.emailPlaceholder/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.passwordPlaceholder/i), {
target: { value: "password123" },
});
fireEvent.change(screen.getByPlaceholderText(/auth:signup.confirmPasswordPlaceholder/i), {
target: { value: "password123" },
});
fireEvent.click(termsCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith({
email: "john@example.com",
password: "password123",
first_name: "John",
last_name: "Doe",
confirm_password: "password123",
business_name: "",
});
});
});
it("requires terms checkbox to be checked", () => {
renderWithProviders(<SignUpPage />);
const termsCheckbox = screen.getByRole("checkbox", { name: /auth:signup.termsAccept/i });
expect(termsCheckbox).toHaveAttribute("required");
});
});

View file

@ -1,16 +1,135 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { TabloPage } from "./tablo";
const mockNavigate = vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => mockNavigate,
useParams: () => ({ tabloId: "test-tablo-id" }),
};
});
vi.mock("../hooks/tablos", () => ({
useTablo: () => ({
tablo: {
id: "test-tablo-id",
name: "Test Tablo",
owner_id: "test-owner-id",
},
isLoading: false,
error: null,
}),
useTablosList: () => ({
data: [{ id: "test-tablo-id", name: "Test Tablo" }],
}),
}));
vi.mock("../hooks/tabloData", () => ({
useTabloData: () => ({
data: [],
isLoading: false,
}),
}));
vi.mock("../providers/UserStoreProvider", () => ({
useUser: () => ({
id: "test-user-id",
name: "Test User",
}),
useIsReadOnlyUser: () => false,
TestUserStoreProvider: ({ children }: { children: React.ReactNode }) => children,
}));
describe("TabloPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders without crashing", () => {
const { container } = renderWithProviders(<TabloPage />);
expect(container).toBeInTheDocument();
});
it("displays tablo name", () => {
renderWithProviders(<TabloPage />);
expect(screen.getByText("Test Tablo")).toBeInTheDocument();
});
it("renders data grid for tablo", () => {
renderWithProviders(<TabloPage />);
// AG Grid should be rendered (look for grid container)
const gridContainer = document.querySelector(".ag-root-wrapper");
expect(gridContainer).toBeInTheDocument();
});
});
describe("TabloPage - Loading State", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("shows loading state when tablo is being fetched", () => {
vi.mocked(vi.importMock("../hooks/tablos")).useTablo = () => ({
tablo: null,
isLoading: true,
error: null,
});
vi.doMock("../hooks/tablos", () => ({
useTablo: () => ({
tablo: null,
isLoading: true,
error: null,
}),
useTablosList: () => ({
data: [],
}),
}));
renderWithProviders(<TabloPage />);
expect(screen.getByText(/pages:tablo.loading/i)).toBeInTheDocument();
});
});
describe("TabloPage - Error State", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("shows error message when tablo cannot be loaded", () => {
vi.mocked(vi.importMock("../hooks/tablos")).useTablo = () => ({
tablo: null,
isLoading: false,
error: new Error("Tablo not found"),
});
vi.doMock("../hooks/tablos", () => ({
useTablo: () => ({
tablo: null,
isLoading: false,
error: new Error("Tablo not found"),
}),
useTablosList: () => ({
data: [],
}),
}));
renderWithProviders(<TabloPage />);
expect(screen.getByText(/pages:tablo.error/i)).toBeInTheDocument();
});
});