From d739e311961d327bb6b541b1b2b35940e431476b Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 19:47:47 +0000 Subject: [PATCH 1/4] test(frontend): add UserMenu RTL coverage for local-mode menu actions --- .../components/organisms/UserMenu.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 frontend/src/components/organisms/UserMenu.test.tsx diff --git a/frontend/src/components/organisms/UserMenu.test.tsx b/frontend/src/components/organisms/UserMenu.test.tsx new file mode 100644 index 00000000..dd4cde18 --- /dev/null +++ b/frontend/src/components/organisms/UserMenu.test.tsx @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { UserMenu } from "./UserMenu"; + +const useUserMock = vi.hoisted(() => vi.fn()); +const clearLocalAuthTokenMock = vi.hoisted(() => vi.fn()); +const isLocalAuthModeMock = vi.hoisted(() => vi.fn()); + +vi.mock("next/image", () => ({ + default: (props: React.ImgHTMLAttributes) => ( + // eslint-disable-next-line @next/next/no-img-element + {props.alt + ), +})); + +vi.mock("next/link", () => ({ + default: ({ children, href, ...rest }: any) => ( + + {children} + + ), +})); + +vi.mock("@/auth/clerk", () => ({ + useUser: useUserMock, + SignOutButton: ({ children }: { children: React.ReactNode }) => children, +})); + +vi.mock("@/auth/localAuth", () => ({ + clearLocalAuthToken: clearLocalAuthTokenMock, + isLocalAuthMode: isLocalAuthModeMock, +})); + +describe("UserMenu", () => { + it("renders and opens local-mode menu actions", async () => { + const user = userEvent.setup(); + useUserMock.mockReturnValue({ user: null }); + isLocalAuthModeMock.mockReturnValue(true); + + render(); + + await user.click(screen.getByRole("button", { name: /open user menu/i })); + + expect(screen.getByRole("link", { name: /open boards/i })).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /create board/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /sign out/i })).toBeInTheDocument(); + }); +}); From 3dca0fa8138fcc38f28f7d1ff27796de38092c2d Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 19:51:36 +0000 Subject: [PATCH 2/4] test(frontend): fix lint typing in UserMenu test link mock --- frontend/src/components/organisms/UserMenu.test.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/organisms/UserMenu.test.tsx b/frontend/src/components/organisms/UserMenu.test.tsx index dd4cde18..70a056e7 100644 --- a/frontend/src/components/organisms/UserMenu.test.tsx +++ b/frontend/src/components/organisms/UserMenu.test.tsx @@ -16,7 +16,15 @@ vi.mock("next/image", () => ({ })); vi.mock("next/link", () => ({ - default: ({ children, href, ...rest }: any) => ( + default: ({ + children, + href, + ...rest + }: { + children: React.ReactNode; + href?: string; + [key: string]: unknown; + }) => ( {children} From b3b8285a64f2c84e866d77e323713553285abf0a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Mar 2026 22:18:09 +0530 Subject: [PATCH 3/4] test(frontend): harden UserMenu RTL mocks and local sign-out assertions --- .../components/organisms/UserMenu.test.tsx | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/organisms/UserMenu.test.tsx b/frontend/src/components/organisms/UserMenu.test.tsx index 70a056e7..d3dd1fcc 100644 --- a/frontend/src/components/organisms/UserMenu.test.tsx +++ b/frontend/src/components/organisms/UserMenu.test.tsx @@ -1,4 +1,10 @@ -import { describe, expect, it, vi } from "vitest"; +import type { + AnchorHTMLAttributes, + ImgHTMLAttributes, + PropsWithChildren, + ReactNode, +} from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -7,9 +13,13 @@ import { UserMenu } from "./UserMenu"; const useUserMock = vi.hoisted(() => vi.fn()); const clearLocalAuthTokenMock = vi.hoisted(() => vi.fn()); const isLocalAuthModeMock = vi.hoisted(() => vi.fn()); +type LinkProps = PropsWithChildren<{ + href: string | { pathname?: string }; +}> & + Omit, "href">; vi.mock("next/image", () => ({ - default: (props: React.ImgHTMLAttributes) => ( + default: (props: ImgHTMLAttributes) => ( // eslint-disable-next-line @next/next/no-img-element {props.alt ), @@ -20,11 +30,7 @@ vi.mock("next/link", () => ({ children, href, ...rest - }: { - children: React.ReactNode; - href?: string; - [key: string]: unknown; - }) => ( + }: LinkProps) => ( {children} @@ -33,7 +39,7 @@ vi.mock("next/link", () => ({ vi.mock("@/auth/clerk", () => ({ useUser: useUserMock, - SignOutButton: ({ children }: { children: React.ReactNode }) => children, + SignOutButton: ({ children }: { children: ReactNode }) => children, })); vi.mock("@/auth/localAuth", () => ({ @@ -42,6 +48,12 @@ vi.mock("@/auth/localAuth", () => ({ })); describe("UserMenu", () => { + beforeEach(() => { + useUserMock.mockReset(); + clearLocalAuthTokenMock.mockReset(); + isLocalAuthModeMock.mockReset(); + }); + it("renders and opens local-mode menu actions", async () => { const user = userEvent.setup(); useUserMock.mockReturnValue({ user: null }); @@ -51,8 +63,34 @@ describe("UserMenu", () => { await user.click(screen.getByRole("button", { name: /open user menu/i })); - expect(screen.getByRole("link", { name: /open boards/i })).toBeInTheDocument(); - expect(screen.getByRole("link", { name: /create board/i })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /sign out/i })).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /open boards/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /create board/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /sign out/i }), + ).toBeInTheDocument(); + }); + + it("clears local auth token and reloads on local sign out", async () => { + const user = userEvent.setup(); + useUserMock.mockReturnValue({ user: null }); + isLocalAuthModeMock.mockReturnValue(true); + const reloadSpy = vi.fn(); + vi.stubGlobal("location", { + ...window.location, + reload: reloadSpy, + } as Location); + + render(); + + await user.click(screen.getByRole("button", { name: /open user menu/i })); + await user.click(screen.getByRole("button", { name: /sign out/i })); + + expect(clearLocalAuthTokenMock).toHaveBeenCalledTimes(1); + expect(reloadSpy).toHaveBeenCalledTimes(1); + vi.unstubAllGlobals(); }); }); From 3a21c4c20457f07df5a446b0ad60992b1e1b81f6 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Mar 2026 22:19:01 +0530 Subject: [PATCH 4/4] test(frontend): auto-restore location stub in UserMenu tests --- frontend/src/components/organisms/UserMenu.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/organisms/UserMenu.test.tsx b/frontend/src/components/organisms/UserMenu.test.tsx index d3dd1fcc..10553e38 100644 --- a/frontend/src/components/organisms/UserMenu.test.tsx +++ b/frontend/src/components/organisms/UserMenu.test.tsx @@ -4,7 +4,7 @@ import type { PropsWithChildren, ReactNode, } from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -53,6 +53,9 @@ describe("UserMenu", () => { clearLocalAuthTokenMock.mockReset(); isLocalAuthModeMock.mockReset(); }); + afterEach(() => { + vi.unstubAllGlobals(); + }); it("renders and opens local-mode menu actions", async () => { const user = userEvent.setup(); @@ -91,6 +94,5 @@ describe("UserMenu", () => { expect(clearLocalAuthTokenMock).toHaveBeenCalledTimes(1); expect(reloadSpy).toHaveBeenCalledTimes(1); - vi.unstubAllGlobals(); }); });