Skip to content

Commit

Permalink
test: Write unit tests for settings page and components (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
belhajManel authored Dec 20, 2024
1 parent 4b29956 commit dc730c3
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/components/common/Banner/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function Banner({ children, icon: Icon, variant }: BannerProps) {
Icon = Icon ?? DefaultIcon();

return (
<div className={bannerStyles({ variant })}>
<div role="alert" className={bannerStyles({ variant })}>
<Icon size={20} className={iconStyles({ variant })} />
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useAuthActions } from "@convex-dev/auth/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useMutation } from "convex/react";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DeleteAccountSetting } from "./DeleteAccountSetting";

describe("DeleteAccountSetting", () => {
const mockSignOut = vi.fn();
const mockDeleteAccount = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
(useMutation as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
mockDeleteAccount,
);
(useAuthActions as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
signOut: mockSignOut,
});
});

it("renders the DeleteAccountSetting component", () => {
render(<DeleteAccountSetting />);
expect(
screen.getByRole("button", { name: "Delete account" }),
).toBeInTheDocument();
expect(
screen.getByText("Permanently delete your Namesake account and data."),
).toBeInTheDocument();
});

it("opens the delete account modal when the button is clicked", async () => {
const user = userEvent.setup();
render(<DeleteAccountSetting />);
await user.click(screen.getByRole("button", { name: "Delete account" }));
expect(screen.getByText("Delete account?")).toBeInTheDocument();
expect(
screen.getByText(
"This will permanently erase your account and all data.",
),
).toBeInTheDocument();
});

it("shows an error if the confirmation text is incorrect", async () => {
const user = userEvent.setup();
render(<DeleteAccountSetting />);
await user.click(screen.getByRole("button", { name: "Delete account" }));

const input = screen.getByLabelText("Type DELETE to confirm");
await user.type(input, "WRONG_TEXT");

await user.click(screen.getByRole("button", { name: "Delete account" }));
expect(screen.getByRole("alert")).toHaveTextContent(
"Please type DELETE to confirm.",
);

expect(mockDeleteAccount).not.toHaveBeenCalled();
});

it("submits the form successfully", async () => {
const user = userEvent.setup();
render(<DeleteAccountSetting />);
await user.click(screen.getByRole("button", { name: "Delete account" }));

const input = screen.getByLabelText("Type DELETE to confirm");
await user.type(input, "DELETE");
await user.click(screen.getByRole("button", { name: "Delete account" }));

await waitFor(() => {
expect(mockDeleteAccount).toHaveBeenCalled();
expect(mockSignOut).toHaveBeenCalled();
expect(toast.success).toHaveBeenCalledWith("Account deleted.");
});
});

it("displays an error if account deletion fails", async () => {
const user = userEvent.setup();
mockDeleteAccount.mockRejectedValue(new Error("Deletion failed"));

render(<DeleteAccountSetting />);
await user.click(screen.getByRole("button", { name: "Delete account" }));

const input = screen.getByLabelText("Type DELETE to confirm");
await user.type(input, "DELETE");
await user.click(screen.getByRole("button", { name: "Delete account" }));

expect(
await screen.findByText("Failed to delete account. Please try again."),
).toBeInTheDocument();
expect(toast.success).not.toHaveBeenCalled();
});

it("closes the modal when 'Cancel' is clicked", async () => {
const user = userEvent.setup();
render(<DeleteAccountSetting />);
await user.click(screen.getByRole("button", { name: "Delete account" }));

await user.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => {
expect(screen.queryByText("Delete account?")).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Doc, Id } from "@convex/_generated/dataModel";
import { JURISDICTIONS } from "@convex/constants";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useMutation } from "convex/react";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EditBirthplaceSetting } from "./EditBirthplaceSetting";

describe("EditBirthplaceSetting", () => {
const mockUser: Doc<"users"> = {
_id: "user123" as Id<"users">,
_creationTime: 123,
role: "user",
birthplace: "CA",
};
const mockSetBirthplace = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
(useMutation as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
mockSetBirthplace,
);
});

it("renders correct jurisdiction if it exists", () => {
render(<EditBirthplaceSetting user={mockUser} />);
expect(screen.getByText(JURISDICTIONS.CA)).toBeInTheDocument();
});

it("renders 'Set birthplace' if birthplace is not set", () => {
render(
<EditBirthplaceSetting user={{ ...mockUser, birthplace: undefined }} />,
);
expect(
screen.getByRole("button", { name: "Set birthplace" }),
).toBeInTheDocument();
});

it("populates correct jurisdiction when modal is opened", async () => {
const user = userEvent.setup();
render(<EditBirthplaceSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: JURISDICTIONS.CA }));
expect(
screen.getByRole("button", { name: `${JURISDICTIONS.CA} State` }),
).toBeInTheDocument();
});

it("updates birthplace and submits the form", async () => {
const user = userEvent.setup();
mockSetBirthplace.mockResolvedValueOnce(undefined);

render(<EditBirthplaceSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: JURISDICTIONS.CA }));
const stateSelect = screen.getByLabelText("State");

await user.click(stateSelect);

await user.click(screen.getByRole("option", { name: JURISDICTIONS.NY }));

await user.click(screen.getByRole("button", { name: "Save" }));

expect(mockSetBirthplace).toHaveBeenCalledWith({
birthplace: "NY",
});

expect(toast.success).toHaveBeenCalledWith("Birthplace updated.");
});

it("displays an error message if the update fails", async () => {
const user = userEvent.setup();
mockSetBirthplace.mockRejectedValueOnce(new Error("Update failed"));

render(<EditBirthplaceSetting user={mockUser} />);

await user.click(screen.getByRole("button", { name: JURISDICTIONS.CA }));
const stateSelect = screen.getByLabelText("State");
await user.click(stateSelect);

await user.click(screen.getByRole("option", { name: JURISDICTIONS.NY }));

await user.click(screen.getByRole("button", { name: "Save" }));

expect(screen.getByRole("alert")).toHaveTextContent(
"Failed to update birthplace. Please try again.",
);
});
});
46 changes: 46 additions & 0 deletions src/components/settings/EditMinorSetting/EditMinorSetting.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Doc, Id } from "@convex/_generated/dataModel";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useMutation } from "convex/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EditMinorSetting } from "./EditMinorSetting";

describe("EditMinorSetting", () => {
const mockUser: Doc<"users"> = {
_id: "user123" as Id<"users">,
isMinor: false,
_creationTime: 123,
role: "user",
};

beforeEach(() => {
vi.clearAllMocks();
});

it("renders correctly with initial state", () => {
render(<EditMinorSetting user={mockUser} />);

expect(screen.getByText("Under 18")).toBeInTheDocument();
expect(
screen.getByText(
"Are you under 18 years old or applying on behalf of someone who is?",
),
).toBeInTheDocument();
expect(screen.getByRole("switch", { name: "Is minor" })).not.toBeChecked();
});

it("toggles the switch and calls updateIsMinor mutation", async () => {
const user = userEvent.setup();
const updateIsMinorMock = vi.fn();
(useMutation as ReturnType<typeof vi.fn>).mockReturnValue(
updateIsMinorMock,
);

render(<EditMinorSetting user={mockUser} />);

const freeSwitch = screen.getByRole("switch", { name: "Is minor" });
await user.click(freeSwitch);

expect(updateIsMinorMock).toHaveBeenCalledWith({ isMinor: true });
});
});
105 changes: 105 additions & 0 deletions src/components/settings/EditNameSetting/EditNameSetting.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Doc, Id } from "@convex/_generated/dataModel";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useMutation } from "convex/react";
import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EditNameSetting } from "./EditNameSetting";

describe("EditNameSetting", () => {
const mockUser: Doc<"users"> = {
_id: "123" as Id<"users">,
name: "John Doe",
role: "user",
_creationTime: 123,
};

beforeEach(() => {
vi.clearAllMocks();
});

it("renders correct username if exists", () => {
render(<EditNameSetting user={mockUser} />);
expect(screen.getByText("John Doe")).toBeInTheDocument();
});

it("renders 'Set name' if name is not set", () => {
render(<EditNameSetting user={{ ...mockUser, name: undefined }} />);
expect(
screen.getByRole("button", { name: "Set name" }),
).toBeInTheDocument();
});

it("populates correct username when modal is opened", async () => {
const user = userEvent.setup();
render(<EditNameSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: "John Doe" }));
expect(screen.getByRole("textbox")).toHaveValue("John Doe");
});

it("displays an error when the name is too long", async () => {
const user = userEvent.setup();
render(<EditNameSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: "John Doe" }));
const input = screen.getByLabelText("Name");

await user.type(input, "a".repeat(101));
await user.click(screen.getByRole("button", { name: "Save" }));

expect(
await screen.findByText("Name must be less than 100 characters."),
).toBeInTheDocument();
});

it("submits the form successfully", async () => {
const user = userEvent.setup();

const updateName = vi.fn();
(useMutation as ReturnType<typeof vi.fn>).mockReturnValue(updateName);
render(<EditNameSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: "John Doe" }));

const input = screen.getByLabelText("Name");
await user.clear(input);
await user.type(input, "Jane Doe");
await user.click(screen.getByRole("button", { name: "Save" }));

await waitFor(() =>
expect(updateName).toHaveBeenCalledWith({ name: "Jane Doe" }),
);
expect(toast.success).toHaveBeenCalledWith("Name updated.");
});

it("displays an error when the form submission fails", async () => {
const user = userEvent.setup();

const updateName = vi
.fn()
.mockRejectedValue(new Error("Failed to update name"));
(useMutation as ReturnType<typeof vi.fn>).mockReturnValue(updateName);
render(<EditNameSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: "John Doe" }));

const input = screen.getByLabelText("Name");
await user.clear(input);
await user.type(input, "Jane Doe");
await user.click(screen.getByRole("button", { name: "Save" }));
expect(
await screen.findByText("Failed to update name. Please try again."),
).toBeInTheDocument();
});

it("closes the modal without saving when the cancel button is clicked", async () => {
const user = userEvent.setup();

render(<EditNameSetting user={mockUser} />);
await user.click(screen.getByRole("button", { name: "John Doe" }));
const input = screen.getByLabelText("Name");
await user.clear(input);
await user.type(input, "Jane Doe");
await user.click(screen.getByRole("button", { name: "Cancel" }));

expect(screen.queryByText("Edit name")).not.toBeInTheDocument();
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const EditNameModal = ({
setError(undefined);

if (name.length > 100) {
setError("Name must be less than 100 characters");
setError("Name must be less than 100 characters.");
return;
}

Expand Down
Loading

0 comments on commit dc730c3

Please sign in to comment.