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
4 changes: 3 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@
"tryDifferentSearch": "Try different search terms or clear the filters to see all causes.",
"tryAgain": "Try again",
"loadMore": "Load more causes",
"showingRange": "Showing {shown} of {total}"
"showingRange": "Showing {shown} of {total}",
"listView": "List",
"mapView": "Map"
},
"Status": {
"active": "Active",
Expand Down
4 changes: 3 additions & 1 deletion messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@
"tryDifferentSearch": "Intenta con otros términos de búsqueda o borra los filtros para ver todas las causas.",
"tryAgain": "Intentar de nuevo",
"loadMore": "Cargar más causas",
"showingRange": "Mostrando {shown} de {total}"
"showingRange": "Mostrando {shown} de {total}",
"listView": "Lista",
"mapView": "Mapa"
},
"Status": {
"active": "Activa",
Expand Down
5 changes: 1 addition & 4 deletions src/__tests__/hooks/useMultiSigProposals.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ describe("useMultiSigProposals", () => {
});

it("re-filters proposals when campaignId changes", () => {
seed([
proposal({ id: "1-a" }),
proposal({ id: "2-a", campaignId: 2 }),
]);
seed([proposal({ id: "1-a" }), proposal({ id: "2-a", campaignId: 2 })]);

const { result, rerender } = renderHook(
({ cid }: { cid: number }) => useMultiSigProposals(cid, WALLET),
Expand Down
21 changes: 20 additions & 1 deletion src/__tests__/integration/AppPageComponents.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type React from "react";
Expand Down Expand Up @@ -128,6 +128,7 @@ jest.mock("@/components/cancelCampaignModal", () => ({
}));

jest.mock("@/lib/contractClient", () => ({
getAllCampaigns: jest.fn(() => Promise.resolve([])),
getAdmin: jest.fn(() => Promise.resolve(ADMIN)),
getPlatformFee: jest.fn(() => Promise.resolve(300)),
updateAdmin: jest.fn(),
Expand Down Expand Up @@ -216,6 +217,24 @@ describe("app page components", () => {

expect(screen.getByRole("heading", { name: "heroTitle" })).toBeInTheDocument();
expect(mockConnectWallet).toHaveBeenCalledTimes(1);

await waitFor(() => {
expect(screen.getByText("statsRaised")).toBeInTheDocument();
});
expect(screen.getByText("statsCampaigns")).toBeInTheDocument();
});

it("shows a connecting state on the CTA while the wallet is connecting", () => {
mockUseWallet.mockReturnValue({
publicKey: null,
isWalletConnected: false,
connectWallet: mockConnectWallet,
isLoading: true,
});

render(withQueryClient(<HomeClient />));

expect(screen.getByRole("link", { name: /Connecting/i })).toBeInTheDocument();
});

it("renders the cause detail page with campaign data, voting, actions, and refund state", async () => {
Expand Down
102 changes: 102 additions & 0 deletions src/__tests__/integration/ExploreClient.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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("does not scroll on initial mount", async () => {
render(<ExplorePage />);

expect(await screen.findByText("Learner campaign")).toBeInTheDocument();
expect(scrollToSpy).not.toHaveBeenCalled();
});

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();
});
});
16 changes: 15 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, useRef, useState } from "react";
import CampaignStatusBadge from "@/components/CampaignStatusBadge";
import FundingProgressBar from "@/components/FundingProgressBar";
import { CampaignRowSkeleton } from "@/components/Skeleton";
Expand All @@ -25,6 +25,20 @@ 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. Skip the first run so this doesn't
// fight the browser's scroll restoration on initial mount (e.g. navigating
// back to /explore).
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
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
Loading