Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/__tests__/integration/ExploreClient.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ExplorePage from "@/app/[locale]/explore/ExploreClient";
import { Category, type Campaign } from "@/types";

const mockUseCampaigns = jest.fn();

jest.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));

jest.mock("@/i18n/routing", () => ({
Link: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<a href={String(href)} {...props}>
{children}
</a>
),
}));

jest.mock("@/hooks/useCampaigns", () => ({
useCampaigns: () => mockUseCampaigns(),
}));

function makeCampaign(overrides: Partial<Campaign> = {}): Campaign {
return {
id: 1,
creator: "GCREATOR1111111111111111111111111111111111111111111111111",
title: "Campaign",
description: "Desc",
created_at: 1,
status: "active",
funding_goal: BigInt(100_000_000),
deadline: 9_999_999_999,
amount_raised: BigInt(10_000_000),
is_active: true,
funds_withdrawn: false,
is_cancelled: false,
is_verified: true,
category: Category.Educator,
has_revenue_sharing: false,
revenue_share_percentage: 0,
...overrides,
};
}

describe("ExploreClient category filter", () => {
const scrollToSpy = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
window.scrollTo = scrollToSpy;
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
}));
mockUseCampaigns.mockReturnValue({
campaigns: [
makeCampaign({ id: 1, title: "Learner campaign", category: Category.Learner }),
makeCampaign({ id: 2, title: "Educator campaign", category: Category.Educator }),
],
isLoading: false,
error: null,
refetch: jest.fn(),
});
});

it("scrolls back to the top when the active category changes", async () => {
const user = userEvent.setup();
render(<ExplorePage />);

scrollToSpy.mockClear();

const educatorPill = screen.getByRole("button", { name: /Educator/i });
await user.click(educatorPill);

expect(scrollToSpy).toHaveBeenCalledWith({ top: 0 });
expect(screen.getByText("Educator campaign")).toBeInTheDocument();
expect(screen.queryByText("Learner campaign")).not.toBeInTheDocument();
});

it("does not scroll again for renders that don't change the active category", async () => {
const user = userEvent.setup();
render(<ExplorePage />);

scrollToSpy.mockClear();

const allPill = screen.getByRole("button", { name: "all" });
await user.click(allPill);

expect(scrollToSpy).not.toHaveBeenCalled();
});
});
9 changes: 8 additions & 1 deletion src/app/[locale]/explore/ExploreClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Image from "next/image";
import { Link } from "@/i18n/routing";
import { useTranslations, useLocale } from "next-intl";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import CampaignStatusBadge from "@/components/CampaignStatusBadge";
import FundingProgressBar from "@/components/FundingProgressBar";
import { CampaignRowSkeleton } from "@/components/Skeleton";
Expand All @@ -25,6 +25,13 @@ export default function ExplorePage() {
const { campaigns, isLoading, error, refetch } = useCampaigns();
const [activeCategory, setActiveCategory] = useState<"all" | Category>("all");

// Scroll back to the top of the list whenever the category filter changes,
// so switching categories doesn't leave the user stranded deep in the
// previous (now stale) scroll position.
useEffect(() => {
window.scrollTo({ top: 0 });
}, [activeCategory]);

const categories = useMemo(() => {
const seen = new Set(campaigns.map((c) => c.category));
return ["all" as const, ...Array.from(seen).sort((a, b) => a - b)];
Expand Down