diff --git a/src/__tests__/unit/components/whiteboard/WhiteboardsPage.test.tsx b/src/__tests__/unit/components/whiteboard/WhiteboardsPage.test.tsx
new file mode 100644
index 0000000000..fa10a98e96
--- /dev/null
+++ b/src/__tests__/unit/components/whiteboard/WhiteboardsPage.test.tsx
@@ -0,0 +1,285 @@
+// @vitest-environment jsdom
+// Tests for WhiteboardsPage — delete button, pagination, and navigation (v2)
+import React from "react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
+
+globalThis.React = React;
+
+const mockRouterPush = vi.fn();
+const mockRouterReplace = vi.fn();
+const mockSearchParamsGet = vi.fn(() => null);
+const mockSearchParamsToString = vi.fn(() => "");
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: mockRouterPush, replace: mockRouterReplace }),
+ useSearchParams: () => ({
+ get: mockSearchParamsGet,
+ toString: mockSearchParamsToString,
+ }),
+ usePathname: () => "/w/test-workspace/whiteboards",
+}));
+
+vi.mock("next-auth/react", () => ({
+ useSession: vi.fn(() => ({ data: { user: { id: "user-1", name: "Test User" } } })),
+}));
+
+vi.mock("@/hooks/useWorkspace", () => ({
+ useWorkspace: vi.fn(() => ({ id: "workspace-1", slug: "test-workspace", role: "OWNER" })),
+}));
+
+vi.mock("@/components/ui/page-header", () => ({
+ PageHeader: ({ title, actions }: { title: string; actions?: React.ReactNode }) => (
+
+
{title}
+ {actions}
+
+ ),
+}));
+
+vi.mock("@/components/ui/button", () => ({
+ Button: ({ children, onClick, disabled, variant, size, className }: any) => (
+
+ ),
+}));
+
+vi.mock("@/components/ui/badge", () => ({
+ Badge: ({ children }: any) => {children},
+}));
+
+vi.mock("@/components/ui/card", () => ({
+ Card: ({ children, className }: any) => {children}
,
+ CardHeader: ({ children, className }: any) => {children}
,
+ CardTitle: ({ children }: any) => {children}
,
+ CardDescription: ({ children }: any) => {children}
,
+}));
+
+vi.mock("@/components/features/TableColumnHeaders", () => ({
+ FilterDropdownHeader: ({ label, onChange, value }: any) => (
+
+ ),
+ SortableColumnHeader: ({ label, onSort }: any) => (
+
+ ),
+}));
+
+vi.mock("@/components/whiteboard/MoveWhiteboardDialog", () => ({
+ MoveWhiteboardDialog: () => null,
+}));
+
+vi.mock("@/components/ui/pagination", () => ({
+ Pagination: ({ children }: any) => ,
+ PaginationContent: ({ children }: any) => ,
+ PaginationItem: ({ children }: any) => {children},
+ PaginationLink: ({ children, onClick }: any) => {children},
+ PaginationPrevious: ({ onClick }: any) => Prev,
+ PaginationNext: ({ onClick }: any) => Next,
+ PaginationEllipsis: () => ...,
+}));
+
+// Shared ref so AlertDialogCancel can call the parent's onOpenChange
+let _alertDialogOnOpenChange: ((open: boolean) => void) | null = null;
+
+vi.mock("@/components/ui/alert-dialog", () => ({
+ AlertDialog: ({ children, open, onOpenChange }: any) => {
+ _alertDialogOnOpenChange = onOpenChange;
+ return open ? {children}
: null;
+ },
+ AlertDialogContent: ({ children }: any) => {children}
,
+ AlertDialogHeader: ({ children }: any) => {children}
,
+ AlertDialogTitle: ({ children }: any) => {children}
,
+ AlertDialogDescription: ({ children }: any) => {children}
,
+ AlertDialogFooter: ({ children }: any) => {children}
,
+ AlertDialogAction: ({ children, onClick, disabled }: any) => (
+
+ ),
+ AlertDialogCancel: ({ children, disabled }: any) => (
+
+ ),
+}));
+
+vi.mock("@/components/ui/dropdown-menu", () => ({
+ DropdownMenu: ({ children }: any) => {children}
,
+ DropdownMenuTrigger: ({ children }: any) => {children}
,
+ DropdownMenuContent: ({ children, onClick }: any) => {children}
,
+ DropdownMenuItem: ({ children, onClick, className }: any) => (
+
+ ),
+}));
+
+vi.mock("@/components/ui/avatar", () => ({
+ Avatar: ({ children }: any) => {children}
,
+ AvatarImage: ({ src }: any) =>
,
+ AvatarFallback: ({ children }: any) => {children},
+}));
+
+const mockWhiteboards = [
+ {
+ id: "wb-1",
+ name: "Test Whiteboard 1",
+ featureId: null,
+ feature: null,
+ createdAt: "2024-01-01T00:00:00Z",
+ updatedAt: "2024-01-02T00:00:00Z",
+ createdBy: { id: "user-1", name: "Test User", image: null },
+ },
+ {
+ id: "wb-2",
+ name: "Test Whiteboard 2",
+ featureId: null,
+ feature: null,
+ createdAt: "2024-01-03T00:00:00Z",
+ updatedAt: "2024-01-04T00:00:00Z",
+ createdBy: { id: "user-2", name: "Another User", image: null },
+ },
+];
+
+const pagination = { totalPages: 1, total: 2, page: 1, limit: 24 };
+
+function makeFetchMock(deleteOk = true) {
+ return vi.fn().mockImplementation((url: string, options?: any) => {
+ if (options?.method === "DELETE") {
+ return Promise.resolve({ ok: deleteOk, json: () => Promise.resolve({ success: deleteOk }) });
+ }
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ success: true, data: mockWhiteboards, pagination }),
+ });
+ });
+}
+
+async function renderPage() {
+ const { default: WhiteboardsPage } = await import("@/app/w/[slug]/whiteboards/page");
+ let result: ReturnType;
+ await act(async () => {
+ result = render();
+ });
+ await waitFor(() => {
+ expect(screen.queryByText("Test Whiteboard 1")).toBeTruthy();
+ });
+ return result!;
+}
+
+describe("WhiteboardsPage — delete button", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSearchParamsGet.mockReturnValue(null);
+ mockSearchParamsToString.mockReturnValue("");
+ global.fetch = makeFetchMock() as any;
+ Object.defineProperty(global, "localStorage", {
+ value: {
+ getItem: vi.fn(() => null),
+ setItem: vi.fn(),
+ removeItem: vi.fn(),
+ clear: vi.fn(),
+ length: 0,
+ key: vi.fn(),
+ },
+ writable: true,
+ });
+ });
+
+ it("calls e.preventDefault() and e.stopPropagation() and sets deleteId when delete button is clicked", async () => {
+ await renderPage();
+
+ const deleteButtons = screen.getAllByText("Delete");
+ expect(deleteButtons.length).toBeGreaterThan(0);
+
+ await act(async () => {
+ fireEvent.click(deleteButtons[0]);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole("dialog")).toBeTruthy();
+ });
+ });
+
+ it("opens delete dialog without navigating when delete button is clicked", async () => {
+ await renderPage();
+
+ const deleteButtons = screen.getAllByText("Delete");
+ await act(async () => {
+ fireEvent.click(deleteButtons[0]);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Delete whiteboard?")).toBeTruthy();
+ });
+ expect(mockRouterPush).not.toHaveBeenCalled();
+ });
+
+ it("does not open delete dialog when clicking the card body link", async () => {
+ await renderPage();
+
+ const cardLink = screen.getByText("Test Whiteboard 1").closest("a");
+ expect(cardLink).toBeTruthy();
+
+ await act(async () => {
+ fireEvent.click(cardLink!);
+ });
+
+ expect(screen.queryByText("Delete whiteboard?")).toBeNull();
+ });
+
+ it("removes the whiteboard from the list after confirming deletion", async () => {
+ global.fetch = makeFetchMock(true) as any;
+
+ await renderPage();
+
+ const deleteButtons = screen.getAllByText("Delete");
+ await act(async () => {
+ fireEvent.click(deleteButtons[0]);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("confirm-delete")).toBeTruthy();
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId("confirm-delete"));
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText("Test Whiteboard 1")).toBeNull();
+ });
+ });
+
+ it("keeps the whiteboard list intact and closes dialog on cancel", async () => {
+ await renderPage();
+
+ const deleteButtons = screen.getAllByText("Delete");
+ await act(async () => {
+ fireEvent.click(deleteButtons[0]);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("cancel-delete")).toBeTruthy();
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId("cancel-delete"));
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText("Delete whiteboard?")).toBeNull();
+ });
+
+ expect(screen.getByText("Test Whiteboard 1")).toBeTruthy();
+ });
+});