From dc730c3bb033b55515246d34230911fb2a817cf9 Mon Sep 17 00:00:00 2001
From: Seneca Artemis <77749468+belhajManel@users.noreply.github.com>
Date: Fri, 20 Dec 2024 15:54:01 +0100
Subject: [PATCH] test: Write unit tests for settings page and components
(#290)
---
src/components/common/Banner/Banner.tsx | 2 +-
.../DeleteAccountSetting.test.tsx | 104 +++++++++++++++++
.../EditBirthplaceSetting.test.tsx | 88 +++++++++++++++
.../EditMinorSetting.test.tsx | 46 ++++++++
.../EditNameSetting/EditNameSetting.test.tsx | 105 ++++++++++++++++++
.../EditNameSetting/EditNameSetting.tsx | 2 +-
.../EditResidenceSetting.test.tsx | 89 +++++++++++++++
.../EditThemeSetting.test.tsx | 49 ++++++++
tests/vitest.setup.ts | 10 ++
9 files changed, 493 insertions(+), 2 deletions(-)
create mode 100644 src/components/settings/DeleteAccountSetting/DeleteAccountSetting.test.tsx
create mode 100644 src/components/settings/EditBirthplaceSetting/EditBirthplaceSetting.test.tsx
create mode 100644 src/components/settings/EditMinorSetting/EditMinorSetting.test.tsx
create mode 100644 src/components/settings/EditNameSetting/EditNameSetting.test.tsx
create mode 100644 src/components/settings/EditResidenceSetting/EditResidenceSetting.test.tsx
create mode 100644 src/components/settings/EditThemeSetting/EditThemeSetting.test.tsx
diff --git a/src/components/common/Banner/Banner.tsx b/src/components/common/Banner/Banner.tsx
index 89fc623..ecb16f5 100644
--- a/src/components/common/Banner/Banner.tsx
+++ b/src/components/common/Banner/Banner.tsx
@@ -63,7 +63,7 @@ export function Banner({ children, icon: Icon, variant }: BannerProps) {
Icon = Icon ?? DefaultIcon();
return (
-
+
{children}
diff --git a/src/components/settings/DeleteAccountSetting/DeleteAccountSetting.test.tsx b/src/components/settings/DeleteAccountSetting/DeleteAccountSetting.test.tsx
new file mode 100644
index 0000000..fecd4d1
--- /dev/null
+++ b/src/components/settings/DeleteAccountSetting/DeleteAccountSetting.test.tsx
@@ -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
).mockReturnValue(
+ mockDeleteAccount,
+ );
+ (useAuthActions as unknown as ReturnType).mockReturnValue({
+ signOut: mockSignOut,
+ });
+ });
+
+ it("renders the DeleteAccountSetting component", () => {
+ render();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ });
+ });
+});
diff --git a/src/components/settings/EditBirthplaceSetting/EditBirthplaceSetting.test.tsx b/src/components/settings/EditBirthplaceSetting/EditBirthplaceSetting.test.tsx
new file mode 100644
index 0000000..ddac8d3
--- /dev/null
+++ b/src/components/settings/EditBirthplaceSetting/EditBirthplaceSetting.test.tsx
@@ -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).mockReturnValue(
+ mockSetBirthplace,
+ );
+ });
+
+ it("renders correct jurisdiction if it exists", () => {
+ render();
+ expect(screen.getByText(JURISDICTIONS.CA)).toBeInTheDocument();
+ });
+
+ it("renders 'Set birthplace' if birthplace is not set", () => {
+ render(
+ ,
+ );
+ expect(
+ screen.getByRole("button", { name: "Set birthplace" }),
+ ).toBeInTheDocument();
+ });
+
+ it("populates correct jurisdiction when modal is opened", async () => {
+ const user = userEvent.setup();
+ render();
+ 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();
+ 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();
+
+ 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.",
+ );
+ });
+});
diff --git a/src/components/settings/EditMinorSetting/EditMinorSetting.test.tsx b/src/components/settings/EditMinorSetting/EditMinorSetting.test.tsx
new file mode 100644
index 0000000..bd53705
--- /dev/null
+++ b/src/components/settings/EditMinorSetting/EditMinorSetting.test.tsx
@@ -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();
+
+ 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).mockReturnValue(
+ updateIsMinorMock,
+ );
+
+ render();
+
+ const freeSwitch = screen.getByRole("switch", { name: "Is minor" });
+ await user.click(freeSwitch);
+
+ expect(updateIsMinorMock).toHaveBeenCalledWith({ isMinor: true });
+ });
+});
diff --git a/src/components/settings/EditNameSetting/EditNameSetting.test.tsx b/src/components/settings/EditNameSetting/EditNameSetting.test.tsx
new file mode 100644
index 0000000..6be7cce
--- /dev/null
+++ b/src/components/settings/EditNameSetting/EditNameSetting.test.tsx
@@ -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();
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
+ });
+
+ it("renders 'Set name' if name is not set", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: "Set name" }),
+ ).toBeInTheDocument();
+ });
+
+ it("populates correct username when modal is opened", async () => {
+ const user = userEvent.setup();
+ render();
+ 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();
+ 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).mockReturnValue(updateName);
+ render();
+ 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).mockReturnValue(updateName);
+ render();
+ 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();
+ 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();
+ });
+});
diff --git a/src/components/settings/EditNameSetting/EditNameSetting.tsx b/src/components/settings/EditNameSetting/EditNameSetting.tsx
index 8b1df2d..38a3ce4 100644
--- a/src/components/settings/EditNameSetting/EditNameSetting.tsx
+++ b/src/components/settings/EditNameSetting/EditNameSetting.tsx
@@ -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;
}
diff --git a/src/components/settings/EditResidenceSetting/EditResidenceSetting.test.tsx b/src/components/settings/EditResidenceSetting/EditResidenceSetting.test.tsx
new file mode 100644
index 0000000..70931b1
--- /dev/null
+++ b/src/components/settings/EditResidenceSetting/EditResidenceSetting.test.tsx
@@ -0,0 +1,89 @@
+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 { EditResidenceSetting } from "./EditResidenceSetting";
+
+describe("EditResidenceSetting", () => {
+ const mockUser: Doc<"users"> = {
+ _id: "user123" as Id<"users">,
+ _creationTime: 123,
+ role: "user",
+ residence: "CA",
+ };
+
+ const mockSetResidence = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ (useMutation as unknown as ReturnType).mockReturnValue(
+ mockSetResidence,
+ );
+ });
+
+ it("renders correct jurisdiction if it exists", () => {
+ render();
+ expect(screen.getByText(JURISDICTIONS.CA)).toBeInTheDocument();
+ });
+
+ it("renders 'Set residence' if residence is not set", () => {
+ render(
+ ,
+ );
+ expect(
+ screen.getByRole("button", { name: "Set residence" }),
+ ).toBeInTheDocument();
+ });
+
+ it("populates correct jurisdiction when modal is opened", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.click(screen.getByRole("button", { name: JURISDICTIONS.CA }));
+ expect(
+ screen.getByRole("button", { name: `${JURISDICTIONS.CA} State` }),
+ ).toBeInTheDocument();
+ });
+
+ it("updates residence and submits the form", async () => {
+ const user = userEvent.setup();
+ mockSetResidence.mockResolvedValueOnce(undefined);
+
+ render();
+ 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(mockSetResidence).toHaveBeenCalledWith({
+ residence: "NY",
+ });
+
+ expect(toast.success).toHaveBeenCalledWith("Residence updated.");
+ });
+
+ it("displays an error message if the update fails", async () => {
+ const user = userEvent.setup();
+ mockSetResidence.mockRejectedValueOnce(new Error("Update failed"));
+
+ render();
+
+ 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.getByText("Failed to update residence. Please try again."),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/settings/EditThemeSetting/EditThemeSetting.test.tsx b/src/components/settings/EditThemeSetting/EditThemeSetting.test.tsx
new file mode 100644
index 0000000..8853a03
--- /dev/null
+++ b/src/components/settings/EditThemeSetting/EditThemeSetting.test.tsx
@@ -0,0 +1,49 @@
+import { useTheme } from "@/utils/useTheme";
+import { THEMES } from "@convex/constants";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { EditThemeSetting } from "./EditThemeSetting";
+
+describe("EditThemeSetting", () => {
+ const mockUpdateTheme = vi.fn();
+ const mockSetNextTheme = vi.fn();
+ const mockThemeSelection = new Set(["light"]);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ (useTheme as unknown as ReturnType).mockReturnValue({
+ theme: "light",
+ themeSelection: mockThemeSelection,
+ setTheme: vi.fn((theme) => {
+ const selectedTheme = [...theme][0];
+ mockUpdateTheme({ theme: selectedTheme });
+ mockSetNextTheme(selectedTheme);
+ }),
+ });
+ });
+
+ it("renders component correctly with correct initial theme", () => {
+ render();
+ expect(screen.getByText("Theme")).toBeInTheDocument();
+ expect(screen.getByText("Adjust your display.")).toBeInTheDocument();
+ for (const theme of Object.values(THEMES)) {
+ expect(screen.getByText(theme.label)).toBeInTheDocument();
+ }
+ const lightThemeButton = screen.getByRole("radio", {
+ name: THEMES.light.label,
+ });
+ expect(lightThemeButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("calls setTheme when a theme is selected", async () => {
+ const user = userEvent.setup();
+ render();
+ const darkThemeButton = screen.getByRole("radio", {
+ name: THEMES.dark.label,
+ });
+ await user.click(darkThemeButton);
+ expect(mockUpdateTheme).toHaveBeenCalledWith({ theme: "dark" });
+ expect(mockSetNextTheme).toHaveBeenCalledWith("dark");
+ });
+});
diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts
index 2bd3841..6681f6f 100644
--- a/tests/vitest.setup.ts
+++ b/tests/vitest.setup.ts
@@ -13,3 +13,13 @@ vi.mock("sonner", () => ({
error: vi.fn(),
},
}));
+
+// Mock the auth hook
+vi.mock("@convex-dev/auth/react", () => ({
+ useAuthActions: vi.fn(),
+}));
+
+// Mock the useTheme hook
+vi.mock("@/utils/useTheme", () => ({
+ useTheme: vi.fn(),
+}));