More tests in pages
This commit is contained in:
parent
a18489c056
commit
0d0abaf945
13 changed files with 1465 additions and 14 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue