Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Write unit tests for settings page and components #290

Merged
merged 9 commits into from
Dec 20, 2024
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,
});
});
evadecker marked this conversation as resolved.
Show resolved Hide resolved

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.");
evadecker marked this conversation as resolved.
Show resolved Hide resolved
});
});

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", () => {
evadecker marked this conversation as resolved.
Show resolved Hide resolved
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));
evadecker marked this conversation as resolved.
Show resolved Hide resolved
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
Loading