diff --git a/src/app/dashboard/contributor/ContributorDashboardClient.test.tsx b/src/app/dashboard/contributor/ContributorDashboardClient.test.tsx index 7519c16..ef1c8a4 100644 --- a/src/app/dashboard/contributor/ContributorDashboardClient.test.tsx +++ b/src/app/dashboard/contributor/ContributorDashboardClient.test.tsx @@ -24,10 +24,11 @@ jest.mock("@/lib/api", () => ({ fetchBounties: jest.fn(), })); jest.mock("@/context/AuthContext", () => ({ useAuth: jest.fn() })); +let mockSearchParams = new URLSearchParams(); jest.mock("next/navigation", () => ({ useRouter: () => ({ replace: jest.fn(), refresh: jest.fn() }), usePathname: () => "/dashboard/contributor", - useSearchParams: () => new URLSearchParams(), + useSearchParams: () => mockSearchParams, })); const mockUseAuth = useAuth as jest.MockedFunction; @@ -63,6 +64,7 @@ function deferred() { beforeEach(() => { jest.clearAllMocks(); + mockSearchParams = new URLSearchParams(); mockFetchBounties.mockResolvedValue({ data: mockBounties, source: "live", @@ -165,3 +167,44 @@ describe("ContributorDashboardClient — out-of-order response guard", () => { expect(await screen.findByText("Live data")).toBeInTheDocument(); }); }); + +describe("ContributorDashboardClient — tab query param validation (#692)", () => { + it("defaults to active tab when ?tab= is missing", async () => { + mockSearchParams = new URLSearchParams(); + await act(async () => { + render(); + }); + expect(screen.getByRole("tab", { name: /Active/ })).toHaveAttribute( + "aria-selected", + "true", + ); + }); + + it("selects completed tab when ?tab=completed is passed", async () => { + mockSearchParams = new URLSearchParams({ tab: "completed" }); + await act(async () => { + render(); + }); + expect(screen.getByRole("tab", { name: /Completed/ })).toHaveAttribute( + "aria-selected", + "true", + ); + }); + + it("falls back to active tab when an invalid ?tab= value is provided", async () => { + mockSearchParams = new URLSearchParams({ tab: "foo" }); + await act(async () => { + render(); + }); + expect(screen.getByRole("tab", { name: /Active/ })).toHaveAttribute( + "aria-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /Completed/ })).toHaveAttribute( + "aria-selected", + "false", + ); + }); +}); + + diff --git a/src/app/dashboard/contributor/ContributorDashboardClient.tsx b/src/app/dashboard/contributor/ContributorDashboardClient.tsx index 5343c3a..4bc550d 100644 --- a/src/app/dashboard/contributor/ContributorDashboardClient.tsx +++ b/src/app/dashboard/contributor/ContributorDashboardClient.tsx @@ -71,8 +71,9 @@ export default function ContributorDashboardClient() { const [bounties, setBounties] = useState(mockBounties); const [statsSource, setStatsSource] = useState("loading"); const [bountiesSource, setBountiesSource] = useState("loading"); + const tabParam = searchParams.get("tab"); const [tab, setTabState] = useState<"active" | "completed">( - (searchParams.get("tab") as "active" | "completed") || "active" + tabParam === "completed" ? "completed" : "active" ); // Explicit fetch status: starts "loading" so cards shimmer rather than // flashing zeroes while the auth check + API call are in flight.