diff --git a/__tests__/app/tools/subscriptions-report.test.tsx b/__tests__/app/tools/subscriptions-report.test.tsx
index 3ce32da603..e3f8fc11ab 100644
--- a/__tests__/app/tools/subscriptions-report.test.tsx
+++ b/__tests__/app/tools/subscriptions-report.test.tsx
@@ -6,13 +6,9 @@ import React from "react";
jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({
__esModule: true,
- displayedSeasonNumberFromIndex: jest.fn(() => 1),
- formatFullDate: jest.fn((date: Date) => date.toISOString()),
getCardsRemainingUntilEndOf: jest.fn(() => 2),
- getSeasonIndexForDate: jest.fn(() => 0),
- getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: [] })),
+ getUpcomingMintsAcrossSeasons: jest.fn(() => []),
isMintingToday: jest.fn(() => false),
- nextMintDateOnOrAfter: jest.fn(() => new Date("2024-01-01T00:00:00Z")),
}));
jest.mock("@/services/api/common-api", () => ({
@@ -53,11 +49,10 @@ jest.mock("@/contexts/TitleContext", () => ({
describe("Subscriptions report page", () => {
it("sets title and renders component", async () => {
- const props = { szn: 1, upcoming: [], redeemed: [] } as any;
const { container } = render(
-
+
);
diff --git a/__tests__/components/latest-activity/ActivityFilters.test.tsx b/__tests__/components/latest-activity/ActivityFilters.test.tsx
index 830c71c74e..1d05bf91d2 100644
--- a/__tests__/components/latest-activity/ActivityFilters.test.tsx
+++ b/__tests__/components/latest-activity/ActivityFilters.test.tsx
@@ -1,35 +1,22 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
import ActivityFilters from "@/components/latest-activity/ActivityFilters";
-import { TypeFilter, ContractFilter } from "@/hooks/useActivityData";
+import { ContractFilter, TypeFilter } from "@/hooks/useActivityData";
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
-// Mock react-bootstrap components to simplify testing
-jest.mock("react-bootstrap", () => {
- const MockDropdown = ({ children, drop, className, ...props }: any) => (
-
+jest.mock("react-bootstrap", () => ({
+ Col: ({ children, sm, md, className }: any) => (
+
{children}
- );
- MockDropdown.Toggle = ({ children, ...props }: any) => (
-
- );
- MockDropdown.Menu = ({ children, ...props }: any) => (
-
{children}
- );
- MockDropdown.Item = ({ children, onClick, ...props }: any) => (
-
- );
-
- return {
- Col: ({ children, ...props }: any) =>
{children}
,
- Dropdown: MockDropdown,
- };
-});
+ ),
+}));
-// Mock the SCSS module
-jest.mock("@/components/latest-activity/LatestActivity.module.scss", () => ({
- filterDropdown: "mock-filter-dropdown",
+jest.mock("framer-motion", () => ({
+ useAnimate: () => [{ current: null }, jest.fn()],
+ AnimatePresence: ({ children }: any) => <>{children}>,
+ motion: {
+ div: ({ children, ...props }: any) =>
{children}
,
+ },
}));
describe("ActivityFilters", () => {
@@ -53,14 +40,24 @@ describe("ActivityFilters", () => {
it("renders both dropdown filters", () => {
render(
);
- const dropdowns = screen.getAllByTestId("dropdown");
- expect(dropdowns).toHaveLength(2);
+ expect(
+ screen.getByRole("button", { name: /Collection:/i })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /Filter:/i })
+ ).toBeInTheDocument();
});
- it("displays current filter values in dropdown toggles", () => {
+ it("displays current filter values in dropdown buttons", () => {
render(
);
- expect(screen.getByText("Collection: All")).toBeInTheDocument();
- expect(screen.getByText("Filter: All")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
+ })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: `Filter: ${TypeFilter.ALL}` })
+ ).toBeInTheDocument();
});
it("displays custom filter values when provided", () => {
@@ -70,8 +67,14 @@ describe("ActivityFilters", () => {
selectedContract: ContractFilter.MEMES,
};
render(
);
- expect(screen.getByText("Collection: Memes")).toBeInTheDocument();
- expect(screen.getByText("Filter: Sales")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.MEMES}`,
+ })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: `Filter: ${TypeFilter.SALES}` })
+ ).toBeInTheDocument();
});
});
@@ -91,174 +94,216 @@ describe("ActivityFilters", () => {
});
});
- describe("Dropdown Options", () => {
- it("renders all TypeFilter options", () => {
- const { container } = render(
);
-
- // Check that all enum values would be rendered
- const typeFilterValues = Object.values(TypeFilter);
- expect(typeFilterValues).toContain(TypeFilter.ALL);
- expect(typeFilterValues).toContain(TypeFilter.SALES);
- expect(typeFilterValues).toContain(TypeFilter.TRANSFERS);
- expect(typeFilterValues).toContain(TypeFilter.AIRDROPS);
- expect(typeFilterValues).toContain(TypeFilter.MINTS);
- expect(typeFilterValues).toContain(TypeFilter.BURNS);
- });
-
- it("renders all ContractFilter options", () => {
- const { container } = render(
);
-
- // Check that all enum values would be rendered
- const contractFilterValues = Object.values(ContractFilter);
- expect(contractFilterValues).toContain(ContractFilter.ALL);
- expect(contractFilterValues).toContain(ContractFilter.MEMES);
- expect(contractFilterValues).toContain(ContractFilter.NEXTGEN);
- expect(contractFilterValues).toContain(ContractFilter.GRADIENTS);
- });
- });
-
describe("CSS Classes", () => {
- it("applies correct CSS classes to dropdowns", () => {
- render(
);
- const dropdowns = screen.getAllByTestId("dropdown");
- dropdowns.forEach(dropdown => {
- expect(dropdown.className).toContain("mock-filter-dropdown");
- expect(dropdown).toHaveAttribute("drop", "down-centered");
- });
- });
-
it("applies correct Bootstrap classes to Col", () => {
render(
);
const col = screen.getByTestId("col");
- expect(col).toHaveAttribute("sm", "12");
- expect(col).toHaveAttribute("md", "6");
+ expect(col).toHaveAttribute("data-sm", "12");
+ expect(col).toHaveAttribute("data-md", "6");
expect(col.className).toContain("d-flex");
expect(col.className).toContain("align-items-center");
expect(col.className).toContain("gap-4");
});
+
+ it("applies tailwind-scope class to Col", () => {
+ render(
);
+ const col = screen.getByTestId("col");
+ expect(col.className).toContain("tailwind-scope");
+ });
});
describe("Props Validation", () => {
it("handles all TypeFilter enum values correctly", () => {
- Object.values(TypeFilter).forEach((typeFilter) => {
+ for (const typeFilter of Object.values(TypeFilter)) {
const props = { ...mockProps, typeFilter };
- render(
);
- expect(screen.getByText(`Filter: ${typeFilter}`)).toBeInTheDocument();
- });
+ const { unmount } = render(
);
+ expect(
+ screen.getByRole("button", { name: `Filter: ${typeFilter}` })
+ ).toBeInTheDocument();
+ unmount();
+ }
});
it("handles all ContractFilter enum values correctly", () => {
- Object.values(ContractFilter).forEach((selectedContract) => {
+ for (const selectedContract of Object.values(ContractFilter)) {
const props = { ...mockProps, selectedContract };
- render(
);
- expect(screen.getByText(`Collection: ${selectedContract}`)).toBeInTheDocument();
+ const { unmount } = render(
);
+ expect(
+ screen.getByRole("button", {
+ name: `Collection: ${selectedContract}`,
+ })
+ ).toBeInTheDocument();
+ unmount();
+ }
+ });
+ });
+
+ describe("User Interactions", () => {
+ it("opens contract dropdown when clicked", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const contractButton = screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
});
+ await user.click(contractButton);
+
+ const menuItems = screen.getAllByRole("menuitem");
+ expect(menuItems.length).toBeGreaterThan(0);
});
- it("preserves callback function references", () => {
- const onTypeFilterChange = jest.fn();
+ it("opens type filter dropdown when clicked", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const typeButton = screen.getByRole("button", {
+ name: `Filter: ${TypeFilter.ALL}`,
+ });
+ await user.click(typeButton);
+
+ const menuItems = screen.getAllByRole("menuitem");
+ expect(menuItems.length).toBeGreaterThan(0);
+ });
+
+ it("calls onContractFilterChange when contract option is selected", async () => {
+ const user = userEvent.setup();
const onContractFilterChange = jest.fn();
- const props = {
- ...mockProps,
- onTypeFilterChange,
- onContractFilterChange,
- };
-
- render(
);
-
- // Callbacks should be preserved (this tests the component doesn't recreate them)
- expect(typeof props.onTypeFilterChange).toBe("function");
- expect(typeof props.onContractFilterChange).toBe("function");
+ render(
+
+ );
+
+ const contractButton = screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
+ });
+ await user.click(contractButton);
+
+ const memesOption = screen.getByRole("menuitem", {
+ name: ContractFilter.MEMES,
+ });
+ await user.click(memesOption);
+
+ expect(onContractFilterChange).toHaveBeenCalledWith(ContractFilter.MEMES);
+ });
+
+ it("calls onTypeFilterChange when type option is selected", async () => {
+ const user = userEvent.setup();
+ const onTypeFilterChange = jest.fn();
+ render(
+
+ );
+
+ const typeButton = screen.getByRole("button", {
+ name: `Filter: ${TypeFilter.ALL}`,
+ });
+ await user.click(typeButton);
+
+ const salesOption = screen.getByRole("menuitem", {
+ name: TypeFilter.SALES,
+ });
+ await user.click(salesOption);
+
+ expect(onTypeFilterChange).toHaveBeenCalledWith(TypeFilter.SALES);
+ });
+
+ it("shows all ContractFilter options in dropdown", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const contractButton = screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
+ });
+ await user.click(contractButton);
+
+ for (const contract of Object.values(ContractFilter)) {
+ expect(
+ screen.getByRole("menuitem", { name: contract })
+ ).toBeInTheDocument();
+ }
+ });
+
+ it("shows all TypeFilter options in dropdown", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const typeButton = screen.getByRole("button", {
+ name: `Filter: ${TypeFilter.ALL}`,
+ });
+ await user.click(typeButton);
+
+ for (const type of Object.values(TypeFilter)) {
+ expect(
+ screen.getByRole("menuitem", { name: type })
+ ).toBeInTheDocument();
+ }
});
});
describe("Component Integration", () => {
it("maintains proper component structure", () => {
const { container } = render(
);
-
- // Should have proper nesting: Col > (Dropdown * 2)
+
const col = container.querySelector('[data-testid="col"]');
- const dropdowns = col?.querySelectorAll('[data-testid="dropdown"]');
-
expect(col).toBeInTheDocument();
- expect(dropdowns).toHaveLength(2);
+
+ const buttons = within(col as HTMLElement).getAllByRole("button", {
+ name: /Collection:|Filter:/i,
+ });
+ expect(buttons).toHaveLength(2);
});
it("renders consistently with different boolean values", () => {
- // Test with isMobile true
- const { rerender } = render(
);
+ const { rerender } = render(
+
+ );
expect(screen.getByTestId("col")).toBeInTheDocument();
-
- // Test with isMobile false
+
rerender(
);
expect(screen.getByTestId("col")).toBeInTheDocument();
});
});
- describe("User Interactions", () => {
- it("calls onContractFilterChange when contract dropdown item is clicked", async () => {
- const onContractFilterChange = jest.fn();
- const props = {
- ...mockProps,
- onContractFilterChange,
- };
-
- render(
);
-
- // Find all dropdown items and click one that should be a ContractFilter
- const dropdownItems = screen.getAllByTestId("dropdown-item");
-
- // The first dropdown is for contracts, so we'll find items there
- // Since we're using mocks, we can simulate clicking on a known filter
- await userEvent.click(dropdownItems[0]); // This should trigger onClick
-
- expect(onContractFilterChange).toHaveBeenCalled();
- });
+ describe("Accessibility", () => {
+ it("dropdown buttons have correct aria-haspopup attribute", () => {
+ render(
);
- it("calls onTypeFilterChange when type dropdown item is clicked", async () => {
- const onTypeFilterChange = jest.fn();
- const props = {
- ...mockProps,
- onTypeFilterChange,
- };
-
- render(
);
-
- // Find all dropdown items
- const dropdownItems = screen.getAllByTestId("dropdown-item");
-
- // The second set of dropdown items are for type filters
- // Since ContractFilter has 4 values (All, Memes, NextGen, Gradients)
- // TypeFilter items start after that
- const typeFilterItemIndex = Object.values(ContractFilter).length; // Should be 4
- if (dropdownItems[typeFilterItemIndex]) {
- await userEvent.click(dropdownItems[typeFilterItemIndex]);
+ const buttons = screen.getAllByRole("button", {
+ name: /Collection:|Filter:/i,
+ });
+ for (const button of buttons) {
+ expect(button).toHaveAttribute("aria-haspopup", "true");
}
-
- expect(onTypeFilterChange).toHaveBeenCalled();
});
- });
- describe("Error Boundaries and Edge Cases", () => {
- it("handles undefined enum values gracefully", () => {
- // This tests that the component doesn't break with unexpected enum values
- const propsWithUndefined = {
- ...mockProps,
- typeFilter: undefined as any,
- selectedContract: undefined as any,
- };
-
- expect(() => render(
)).not.toThrow();
+ it("dropdown buttons have correct aria-expanded attribute", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const contractButton = screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
+ });
+ expect(contractButton).toHaveAttribute("aria-expanded", "false");
+
+ await user.click(contractButton);
+ expect(contractButton).toHaveAttribute("aria-expanded", "true");
});
- it("handles missing callback functions", () => {
- const propsWithoutCallbacks = {
- ...mockProps,
- onTypeFilterChange: undefined as any,
- onContractFilterChange: undefined as any,
- };
-
- expect(() => render(
)).not.toThrow();
+ it("dropdown items have correct role", async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ const contractButton = screen.getByRole("button", {
+ name: `Collection: ${ContractFilter.ALL}`,
+ });
+ await user.click(contractButton);
+
+ const menuItems = screen.getAllByRole("menuitem");
+ expect(menuItems.length).toBe(Object.values(ContractFilter).length);
});
});
});
diff --git a/__tests__/components/latest-activity/ActivityHeader.test.tsx b/__tests__/components/latest-activity/ActivityHeader.test.tsx
index f0e665615a..da11c8f42d 100644
--- a/__tests__/components/latest-activity/ActivityHeader.test.tsx
+++ b/__tests__/components/latest-activity/ActivityHeader.test.tsx
@@ -1,143 +1,166 @@
-import { render, screen } from '@testing-library/react';
-import React from 'react';
-import ActivityHeader from '@/components/latest-activity/ActivityHeader';
+import ActivityHeader from "@/components/latest-activity/ActivityHeader";
+import { render, screen } from "@testing-library/react";
// Mock the DotLoader component
-jest.mock('@/components/dotLoader/DotLoader', () => {
+jest.mock("@/components/dotLoader/DotLoader", () => {
return function MockDotLoader() {
return
Loading...
;
};
});
-// Mock the SCSS module
-jest.mock('@/styles/Home.module.scss', () => ({
- viewAllLink: 'mocked-view-all-link-class',
-}));
-
-describe('ActivityHeader', () => {
- it('renders the NFT Activity header text', () => {
+describe("ActivityHeader", () => {
+ it("renders the NFT Activity header text", () => {
render(
);
-
- expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+
+ expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
expect(screen.getByText(/Activity/)).toBeInTheDocument();
});
- it('renders with correct Bootstrap column classes', () => {
- const { container } = render(
);
-
+ it("renders with correct Bootstrap column classes", () => {
+ const { container } = render(
+
+ );
+
const colElement = container.firstChild as HTMLElement;
- expect(colElement).toHaveClass('col-sm-12');
- expect(colElement).toHaveClass('col-md-6');
- expect(colElement).toHaveClass('d-flex');
- expect(colElement).toHaveClass('align-items-center');
- expect(colElement).toHaveClass('justify-content-between');
+ expect(colElement).toHaveClass("col-sm-12");
+ expect(colElement).toHaveClass("col-md-6");
+ expect(colElement).toHaveClass("d-flex");
+ expect(colElement).toHaveClass("align-items-center");
+ expect(colElement).toHaveClass("justify-content-between");
});
- describe('when showViewAll is true', () => {
- it('renders the View All link', () => {
+ describe("when showViewAll is true", () => {
+ it("renders the View All link", () => {
render(
);
-
- const link = screen.getByRole('link', { name: 'View All' });
+
+ const link = screen.getByRole("link", { name: "View All" });
expect(link).toBeInTheDocument();
- expect(link).toHaveAttribute('href', '/nft-activity');
- expect(link).toHaveClass('mocked-view-all-link-class');
+ expect(link).toHaveAttribute("href", "/nft-activity");
+ const span = link.querySelector("span");
+ expect(span).toHaveClass(
+ "tw-whitespace-nowrap",
+ "tw-text-sm",
+ "tw-font-semibold"
+ );
});
- it('does not render DotLoader even when fetching is true', () => {
+ it("does not render DotLoader even when fetching is true", () => {
render(
);
-
- expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument();
- expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument();
+
+ expect(
+ screen.getByRole("link", { name: "View All" })
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument();
});
});
- describe('when showViewAll is false', () => {
- it('does not render the View All link', () => {
+ describe("when showViewAll is false", () => {
+ it("does not render the View All link", () => {
render(
);
-
- expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument();
+
+ expect(
+ screen.queryByRole("link", { name: "View All" })
+ ).not.toBeInTheDocument();
});
- it('renders DotLoader when fetching is true', () => {
+ it("renders DotLoader when fetching is true", () => {
render(
);
-
- expect(screen.getByTestId('dot-loader')).toBeInTheDocument();
- expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument();
+
+ expect(screen.getByTestId("dot-loader")).toBeInTheDocument();
+ expect(
+ screen.queryByRole("link", { name: "View All" })
+ ).not.toBeInTheDocument();
});
- it('does not render DotLoader when fetching is false', () => {
+ it("does not render DotLoader when fetching is false", () => {
render(
);
-
- expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument();
- expect(screen.queryByRole('link', { name: 'View All' })).not.toBeInTheDocument();
+
+ expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole("link", { name: "View All" })
+ ).not.toBeInTheDocument();
});
});
- describe('prop combinations', () => {
- it('handles showViewAll=false and fetching=false (minimal state)', () => {
+ describe("prop combinations", () => {
+ it("handles showViewAll=false and fetching=false (minimal state)", () => {
render(
);
-
- expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument();
- expect(screen.queryByRole('link')).not.toBeInTheDocument();
- expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument();
+
+ expect(
+ screen.getByRole("heading", { name: "NFT Activity" })
+ ).toBeInTheDocument();
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument();
});
- it('handles showViewAll=false and fetching=true (loading state)', () => {
+ it("handles showViewAll=false and fetching=true (loading state)", () => {
render(
);
-
- expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument();
- expect(screen.queryByRole('link')).not.toBeInTheDocument();
- expect(screen.getByTestId('dot-loader')).toBeInTheDocument();
+
+ expect(
+ screen.getByRole("heading", { name: "NFT Activity" })
+ ).toBeInTheDocument();
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
+ expect(screen.getByTestId("dot-loader")).toBeInTheDocument();
});
- it('handles showViewAll=true and fetching=false (view all state)', () => {
+ it("handles showViewAll=true and fetching=false (view all state)", () => {
render(
);
-
- expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument();
- expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument();
- expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument();
+
+ expect(
+ screen.getByRole("heading", { name: "NFT Activity" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "View All" })
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument();
});
- it('handles showViewAll=true and fetching=true (view all takes precedence)', () => {
+ it("handles showViewAll=true and fetching=true (view all takes precedence)", () => {
render(
);
-
- expect(screen.getByRole('heading', { name: 'NFT Activity' })).toBeInTheDocument();
- expect(screen.getByRole('link', { name: 'View All' })).toBeInTheDocument();
- expect(screen.queryByTestId('dot-loader')).not.toBeInTheDocument();
+
+ expect(
+ screen.getByRole("heading", { name: "NFT Activity" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "View All" })
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("dot-loader")).not.toBeInTheDocument();
});
});
- describe('accessibility', () => {
- it('has proper heading structure', () => {
+ describe("accessibility", () => {
+ it("has proper heading structure", () => {
render(
);
-
- const heading = screen.getByRole('heading', { level: 1 });
- expect(heading).toHaveTextContent('NFT Activity');
+
+ const heading = screen.getByRole("heading", { level: 1 });
+ expect(heading).toHaveTextContent("NFT Activity");
});
- it('link has proper accessible text', () => {
+ it("link has proper accessible text", () => {
render(
);
-
- const link = screen.getByRole('link', { name: 'View All' });
- expect(link).toHaveAccessibleName('View All');
+
+ const link = screen.getByRole("link", { name: "View All" });
+ expect(link).toHaveAccessibleName("View All");
});
});
- describe('styling and layout', () => {
- it('has correct flex container structure', () => {
- const { container } = render(
);
-
- const spanElement = container.querySelector('span.d-flex');
- expect(spanElement).toHaveClass('d-flex');
- expect(spanElement).toHaveClass('flex-wrap');
- expect(spanElement).toHaveClass('align-items-center');
- expect(spanElement).toHaveClass('gap-3');
+ describe("styling and layout", () => {
+ it("has correct flex container structure", () => {
+ const { container } = render(
+
+ );
+
+ const spanElement = container.querySelector("span.d-flex");
+ expect(spanElement).toHaveClass("d-flex");
+ expect(spanElement).toHaveClass("flex-wrap");
+ expect(spanElement).toHaveClass("align-items-center");
+ expect(spanElement).toHaveClass("gap-3");
});
- it('displays NFT Activity heading text', () => {
+ it("displays NFT Activity heading text", () => {
render(
);
expect(
- screen.getByRole('heading', { name: 'NFT Activity' })
+ screen.getByRole("heading", { name: "NFT Activity" })
).toBeInTheDocument();
});
});
diff --git a/__tests__/components/latest-activity/LatestActivity.test.tsx b/__tests__/components/latest-activity/LatestActivity.test.tsx
index ff7d8d7c7e..7afc1cca39 100644
--- a/__tests__/components/latest-activity/LatestActivity.test.tsx
+++ b/__tests__/components/latest-activity/LatestActivity.test.tsx
@@ -113,8 +113,8 @@ describe("LatestActivity", () => {
expect(fetchUrl).toHaveBeenCalledWith(
"https://api.test.6529.io/api/transactions?page_size=10&page=1"
);
- await userEvent.click(screen.getByText("Collection: All"));
- await userEvent.click(screen.getByText("Memes"));
+ await userEvent.click(screen.getByText("All Collections"));
+ await userEvent.click(screen.getByText("The Memes"));
await waitFor(() =>
expect(fetchUrl).toHaveBeenLastCalledWith(
"https://api.test.6529.io/api/transactions?page_size=10&page=1&contract=0x33FD426905F149f8376e227d0C9D3340AaD17aF1"
@@ -170,7 +170,7 @@ describe("LatestActivity", () => {
);
// Change filter - this should trigger a fetch
- await userEvent.click(screen.getByText("Filter: All"));
+ await userEvent.click(screen.getByText("All Transactions"));
await userEvent.click(screen.getByText("Sales"));
await waitFor(() => {
@@ -406,7 +406,7 @@ describe("LatestActivity", () => {
});
// Change type filter
- await userEvent.click(screen.getByText("Filter: All"));
+ await userEvent.click(screen.getByText("All Transactions"));
await userEvent.click(screen.getByText("Sales"));
await waitFor(() => {
@@ -426,8 +426,8 @@ describe("LatestActivity", () => {
});
// Change contract filter
- await userEvent.click(screen.getByText("Collection: All"));
- await userEvent.click(screen.getByText("Memes"));
+ await userEvent.click(screen.getByText("All Collections"));
+ await userEvent.click(screen.getByText("The Memes"));
await waitFor(() => {
expect(fetchUrl).toHaveBeenCalledWith(
@@ -452,8 +452,8 @@ describe("LatestActivity", () => {
expect(
screen.getByRole("heading", { name: "NFT Activity" })
).toBeInTheDocument();
- expect(screen.getByText("Collection: All")).toBeInTheDocument();
- expect(screen.getByText("Filter: All")).toBeInTheDocument();
+ expect(screen.getByText("All Collections")).toBeInTheDocument();
+ expect(screen.getByText("All Transactions")).toBeInTheDocument();
});
it("starts with empty activity array when no data provided", async () => {
diff --git a/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx b/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx
deleted file mode 100644
index 103d6cf6ca..0000000000
--- a/__tests__/components/seasons-dropdown/SeasonsDropdown.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import SeasonsDropdown from '@/components/seasons-dropdown/SeasonsDropdown';
-
-const seasons = [1, 2, 3];
-
-it('displays selected season and calls setter', async () => {
- const setSelected = jest.fn();
- const user = userEvent.setup();
- render(
);
- expect(screen.getByText('SZN: 2')).toBeInTheDocument();
-
- // Click on the dropdown toggle to open the menu
- await user.click(screen.getByRole('button'));
-
- // Then click on the SZN3 option
- await user.click(screen.getByText('SZN3'));
- expect(setSelected).toHaveBeenCalledWith(3);
-});
diff --git a/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx b/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx
index ad1c4fb44d..629265ecd4 100644
--- a/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx
+++ b/__tests__/components/user/collected/cards/UserPageCollectedCardsNoCards.test.tsx
@@ -1,31 +1,61 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import UserPageCollectedCardsNoCards from '@/components/user/collected/cards/UserPageCollectedCardsNoCards';
-import { CollectedCollectionType, CollectionSeized } from '@/entities/IProfile';
-import { MEMES_SEASON } from '@/enums';
+import UserPageCollectedCardsNoCards from "@/components/user/collected/cards/UserPageCollectedCardsNoCards";
+import { CollectedCollectionType, CollectionSeized } from "@/entities/IProfile";
+import { MemeSeason } from "@/entities/ISeason";
+import { render, screen } from "@testing-library/react";
-describe('UserPageCollectedCardsNoCards messages', () => {
+describe("UserPageCollectedCardsNoCards messages", () => {
function renderComponent(filters: any) {
return render(
);
}
- it('shows generic message when seized filter active', () => {
- renderComponent({ seized: CollectionSeized.SEIZED, collection: null, szn: null });
- expect(screen.getByText('No cards to display')).toBeInTheDocument();
+ const mockSeason: MemeSeason = {
+ id: 4,
+ start_index: 1,
+ end_index: 100,
+ count: 100,
+ name: "SZN4",
+ display: "SZN 4",
+ };
+
+ it("shows generic message when seized filter active", () => {
+ renderComponent({
+ seized: CollectionSeized.SEIZED,
+ collection: null,
+ szn: null,
+ });
+ expect(screen.getByText("No cards to display")).toBeInTheDocument();
});
- it('shows full setter when no collection selected', () => {
- renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: null, szn: null });
- expect(screen.getByText('Congratulations, full setter!')).toBeInTheDocument();
+ it("shows full setter when no collection selected", () => {
+ renderComponent({
+ seized: CollectionSeized.NOT_SEIZED,
+ collection: null,
+ szn: null,
+ });
+ expect(
+ screen.getByText("Congratulations, full setter!")
+ ).toBeInTheDocument();
});
- it('shows season specific message for memes season', () => {
- renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: CollectedCollectionType.MEMES, szn: MEMES_SEASON.SZN4 });
- expect(screen.getByText('Congratulations, SZN4 full setter!')).toBeInTheDocument();
+ it("shows season specific message for memes season", () => {
+ renderComponent({
+ seized: CollectionSeized.NOT_SEIZED,
+ collection: CollectedCollectionType.MEMES,
+ szn: mockSeason,
+ });
+ expect(
+ screen.getByText("Congratulations, SZN 4 full setter!")
+ ).toBeInTheDocument();
});
- it('shows gradient message', () => {
- renderComponent({ seized: CollectionSeized.NOT_SEIZED, collection: CollectedCollectionType.GRADIENTS, szn: null });
- expect(screen.getByText('Congratulations, Gradient full setter!')).toBeInTheDocument();
+ it("shows gradient message", () => {
+ renderComponent({
+ seized: CollectionSeized.NOT_SEIZED,
+ collection: CollectedCollectionType.GRADIENTS,
+ szn: null,
+ });
+ expect(
+ screen.getByText("Congratulations, Gradient full setter!")
+ ).toBeInTheDocument();
});
});
diff --git a/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx b/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx
index 77886affab..04ca2ead4d 100644
--- a/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx
+++ b/__tests__/components/user/collected/filters/UserPageCollectedFilters.test.tsx
@@ -1,97 +1,144 @@
-import React from 'react';
-import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
-import { RefObject } from 'react';
-import UserPageCollectedFilters from '@/components/user/collected/filters/UserPageCollectedFilters';
-import { CollectedCollectionType, CollectionSeized, CollectionSort } from '@/entities/IProfile';
-import { SortDirection } from '@/entities/ISort';
-import { MEMES_SEASON } from '@/enums';
-import { ApiIdentity } from '@/generated/models/ApiIdentity';
-import { ApiProfileClassification } from '@/generated/models/ApiProfileClassification';
-
-// Mock the child components
-jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersCollection', () => {
- return function MockUserPageCollectedFiltersCollection({ selected, setSelected }: any) {
- return (
-
-
- Current: {selected || 'none'}
-
- );
- };
-});
-
-jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSortBy', () => {
- return function MockUserPageCollectedFiltersSortBy({ selected, setSelected }: any) {
- return (
-
-
- Current: {selected || 'none'}
-
- );
- };
-});
-
-jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSeized', () => {
- return function MockUserPageCollectedFiltersSeized({ selected, setSelected }: any) {
- return (
-
-
- Current: {selected || 'none'}
-
- );
- };
-});
-
-jest.mock('@/components/user/collected/filters/UserPageCollectedFiltersSzn', () => {
- return function MockUserPageCollectedFiltersSzn({ selected, setSelected }: any) {
- return (
-
-
- Current: {selected || 'none'}
-
- );
- };
-});
-
-jest.mock('@/components/user/utils/addresses-select/UserAddressesSelectDropdown', () => {
- return function MockUserAddressesSelectDropdown({ wallets, onActiveAddress }: any) {
- return (
-
-
- Wallets: {wallets.length}
-
- );
- };
-});
-
-// Mock the helpers
-jest.mock('@/components/user/collected/filters/user-page-collected-filters.helpers', () => ({
- COLLECTED_COLLECTIONS_META: {
- [CollectedCollectionType.MEMES]: {
- filters: {
- seized: true,
- szn: true,
+import UserPageCollectedFilters from "@/components/user/collected/filters/UserPageCollectedFilters";
+import {
+ CollectedCollectionType,
+ CollectionSeized,
+ CollectionSort,
+} from "@/entities/IProfile";
+import { MemeSeason } from "@/entities/ISeason";
+import { SortDirection } from "@/entities/ISort";
+import { ApiIdentity } from "@/generated/models/ApiIdentity";
+import { ApiProfileClassification } from "@/generated/models/ApiProfileClassification";
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@testing-library/react";
+import { RefObject } from "react";
+
+jest.mock(
+ "@/components/user/collected/filters/UserPageCollectedFiltersCollection",
+ () => {
+ return function MockUserPageCollectedFiltersCollection({
+ selected,
+ setSelected,
+ }: any) {
+ return (
+
+
+ Current: {selected || "none"}
+
+ );
+ };
+ }
+);
+
+jest.mock(
+ "@/components/user/collected/filters/UserPageCollectedFiltersSortBy",
+ () => {
+ return function MockUserPageCollectedFiltersSortBy({
+ selected,
+ setSelected,
+ }: any) {
+ return (
+
+
+ Current: {selected || "none"}
+
+ );
+ };
+ }
+);
+
+jest.mock(
+ "@/components/user/collected/filters/UserPageCollectedFiltersSeized",
+ () => {
+ return function MockUserPageCollectedFiltersSeized({
+ selected,
+ setSelected,
+ }: any) {
+ return (
+
+
+ Current: {selected || "none"}
+
+ );
+ };
+ }
+);
+
+jest.mock(
+ "@/components/user/collected/filters/UserPageCollectedFiltersSzn",
+ () => {
+ return function MockUserPageCollectedFiltersSzn({
+ selected,
+ setSelected,
+ }: any) {
+ return (
+
+
+ Current: {selected?.display || "none"}
+
+ );
+ };
+ }
+);
+
+jest.mock(
+ "@/components/user/utils/addresses-select/UserAddressesSelectDropdown",
+ () => {
+ return function MockUserAddressesSelectDropdown({
+ wallets,
+ onActiveAddress,
+ }: any) {
+ return (
+
+
+ Wallets: {wallets.length}
+
+ );
+ };
+ }
+);
+
+jest.mock(
+ "@/components/user/collected/filters/user-page-collected-filters.helpers",
+ () => ({
+ COLLECTED_COLLECTIONS_META: {
+ [CollectedCollectionType.MEMES]: {
+ filters: {
+ seized: true,
+ szn: true,
+ },
},
- },
- [CollectedCollectionType.GRADIENTS]: {
- filters: {
- seized: false,
- szn: false,
+ [CollectedCollectionType.GRADIENTS]: {
+ filters: {
+ seized: false,
+ szn: false,
+ },
},
- },
- [CollectedCollectionType.NETWORK]: {
- filters: {
- seized: false,
- szn: false,
+ [CollectedCollectionType.NETWORK]: {
+ filters: {
+ seized: false,
+ szn: false,
+ },
},
},
- },
-}));
+ })
+);
-// Mock window.matchMedia
-Object.defineProperty(window, 'matchMedia', {
+Object.defineProperty(globalThis, "matchMedia", {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
@@ -103,13 +150,13 @@ Object.defineProperty(window, 'matchMedia', {
})),
});
-describe('UserPageCollectedFilters', () => {
+describe("UserPageCollectedFilters", () => {
const mockProfile: ApiIdentity = {
- id: '1',
- handle: 'testuser',
- normalised_handle: 'testuser',
- wallet: '0x123',
- display: 'Test User',
+ id: "1",
+ handle: "testuser",
+ normalised_handle: "testuser",
+ wallet: "0x123",
+ display: "Test User",
pfp: null,
pfp_url: null,
cic: 0,
@@ -122,15 +169,15 @@ describe('UserPageCollectedFilters', () => {
consolidation_key: null,
classification: ApiProfileClassification.Pseudonym,
sub_classification: null,
- primary_wallet: '0x123',
+ primary_wallet: "0x123",
banner1: null,
banner2: null,
active_main_stage_submission_ids: [],
winner_main_stage_drop_ids: [],
wallets: [
- { wallet: '0x123', display: '0x123', tdh: 0 },
- { wallet: '0x456', display: '0x456', tdh: 0 }
- ]
+ { wallet: "0x123", display: "0x123", tdh: 0 },
+ { wallet: "0x456", display: "0x456", tdh: 0 },
+ ],
} as unknown as ApiIdentity;
const mockFilters = {
@@ -138,11 +185,13 @@ describe('UserPageCollectedFilters', () => {
sortBy: CollectionSort.TOKEN_ID,
sortDirection: SortDirection.ASC,
seized: null as CollectionSeized | null,
- szn: null as MEMES_SEASON | null,
- handleOrWallet: 'testuser',
+ szn: null as MemeSeason | null,
+ initialSznId: null as number | null,
+ handleOrWallet: "testuser",
accountForConsolidations: false,
page: 1,
pageSize: 20,
+ subcollection: null,
};
const mockSetters = {
@@ -150,6 +199,7 @@ describe('UserPageCollectedFilters', () => {
setSortBy: jest.fn(),
setSeized: jest.fn(),
setSzn: jest.fn(),
+ setSubcollection: jest.fn(),
showTransfer: false,
};
@@ -158,17 +208,17 @@ describe('UserPageCollectedFilters', () => {
beforeEach(() => {
jest.clearAllMocks();
mockContainerRef = {
- current: document.createElement('div')
+ current: document.createElement("div"),
};
- globalThis.ResizeObserver = jest.fn().mockImplementation((callback) => ({
+ globalThis.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
});
- it('renders filters correctly', () => {
+ it("renders filters correctly", () => {
render(
{
/>
);
- expect(screen.getByTestId('collection-filter')).toBeInTheDocument();
- expect(screen.getByTestId('sort-by-filter')).toBeInTheDocument();
- expect(screen.getByTestId('address-select')).toBeInTheDocument();
+ expect(screen.getByTestId("sort-by-filter")).toBeInTheDocument();
+ expect(screen.getByTestId("address-select")).toBeInTheDocument();
});
- it('shows seized filter when collection supports it', () => {
- const filtersWithMemes = { ...mockFilters, collection: CollectedCollectionType.MEMES };
+ it("shows seized filter when collection supports it", () => {
+ const filtersWithMemes = {
+ ...mockFilters,
+ collection: CollectedCollectionType.MEMES,
+ };
render(
{
/>
);
- expect(screen.getByTestId('seized-filter')).toBeInTheDocument();
+ expect(screen.getByTestId("seized-filter")).toBeInTheDocument();
});
- it('shows season filter when collection supports it', () => {
- const filtersWithMemes = { ...mockFilters, collection: CollectedCollectionType.MEMES };
+ it("shows season filter when collection supports it", () => {
+ const filtersWithMemes = {
+ ...mockFilters,
+ collection: CollectedCollectionType.MEMES,
+ };
render(
{
/>
);
- expect(screen.getByTestId('szn-filter')).toBeInTheDocument();
+ expect(screen.getByTestId("szn-filter")).toBeInTheDocument();
});
- it('hides seized and season filters when collection does not support them', () => {
- const filtersWithGradients = { ...mockFilters, collection: CollectedCollectionType.GRADIENTS };
+ it("hides seized and season filters when collection does not support them", () => {
+ const filtersWithGradients = {
+ ...mockFilters,
+ collection: CollectedCollectionType.GRADIENTS,
+ };
render(
{
/>
);
- expect(screen.queryByTestId('seized-filter')).not.toBeInTheDocument();
- expect(screen.queryByTestId('szn-filter')).not.toBeInTheDocument();
+ expect(screen.queryByTestId("seized-filter")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("szn-filter")).not.toBeInTheDocument();
});
- it('calls setCollection when collection filter is used', () => {
+ it("calls setSortBy when sort filter is used", () => {
render(
{
/>
);
- fireEvent.click(screen.getByText('Collection Filter'));
- expect(mockSetters.setCollection).toHaveBeenCalledWith(CollectedCollectionType.MEMES);
- });
-
- it('calls setSortBy when sort filter is used', () => {
- render(
-
- );
-
- fireEvent.click(screen.getByText('Sort By Filter'));
+ fireEvent.click(screen.getByText("Sort By Filter"));
expect(mockSetters.setSortBy).toHaveBeenCalledWith(CollectionSort.TOKEN_ID);
});
- it('shows scroll arrows when filters are not fully visible', async () => {
+ it("shows scroll arrows when filters are not fully visible", async () => {
const { container } = render(
{
);
await waitFor(() => {
- const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement;
+ const scrollContainer = container.querySelector(
+ '[class*="tw-overflow-x-auto"]'
+ ) as HTMLDivElement;
expect(scrollContainer).toBeTruthy();
});
- const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement;
+ const scrollContainer = container.querySelector(
+ '[class*="tw-overflow-x-auto"]'
+ ) as HTMLDivElement;
if (!scrollContainer) {
- throw new Error('Scroll container not found');
+ throw new Error("Scroll container not found");
}
await act(async () => {
- Object.defineProperty(scrollContainer, 'scrollLeft', {
+ Object.defineProperty(scrollContainer, "scrollLeft", {
writable: true,
configurable: true,
value: 50,
});
- Object.defineProperty(scrollContainer, 'scrollWidth', {
+ Object.defineProperty(scrollContainer, "scrollWidth", {
writable: true,
configurable: true,
value: 300,
});
- Object.defineProperty(scrollContainer, 'clientWidth', {
+ Object.defineProperty(scrollContainer, "clientWidth", {
writable: true,
configurable: true,
value: 100,
});
- const scrollEvent = new Event('scroll', { bubbles: true });
+ const scrollEvent = new Event("scroll", { bubbles: true });
scrollContainer.dispatchEvent(scrollEvent);
- await new Promise(resolve => setTimeout(resolve, 0));
+ await new Promise((resolve) => setTimeout(resolve, 0));
});
await waitFor(() => {
- expect(screen.getByLabelText('Scroll filters left')).toBeInTheDocument();
- expect(screen.getByLabelText('Scroll filters right')).toBeInTheDocument();
+ expect(screen.getByLabelText("Scroll filters left")).toBeInTheDocument();
+ expect(screen.getByLabelText("Scroll filters right")).toBeInTheDocument();
});
});
- it('calls scrollHorizontally when scroll arrows are clicked', async () => {
+ it("calls scrollHorizontally when scroll arrows are clicked", async () => {
const scrollBySpy = jest.fn();
const { container } = render(
{
);
await waitFor(() => {
- const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement;
+ const scrollContainer = container.querySelector(
+ '[class*="tw-overflow-x-auto"]'
+ ) as HTMLDivElement;
expect(scrollContainer).toBeTruthy();
});
- const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement;
+ const scrollContainer = container.querySelector(
+ '[class*="tw-overflow-x-auto"]'
+ ) as HTMLDivElement;
if (!scrollContainer) {
- throw new Error('Scroll container not found');
+ throw new Error("Scroll container not found");
}
scrollContainer.scrollBy = scrollBySpy;
await act(async () => {
- Object.defineProperty(scrollContainer, 'scrollLeft', {
+ Object.defineProperty(scrollContainer, "scrollLeft", {
writable: true,
configurable: true,
value: 50,
});
- Object.defineProperty(scrollContainer, 'scrollWidth', {
+ Object.defineProperty(scrollContainer, "scrollWidth", {
writable: true,
configurable: true,
value: 300,
});
- Object.defineProperty(scrollContainer, 'clientWidth', {
+ Object.defineProperty(scrollContainer, "clientWidth", {
writable: true,
configurable: true,
value: 100,
});
- const scrollEvent = new Event('scroll', { bubbles: true });
+ const scrollEvent = new Event("scroll", { bubbles: true });
scrollContainer.dispatchEvent(scrollEvent);
- await new Promise(resolve => setTimeout(resolve, 0));
+ await new Promise((resolve) => setTimeout(resolve, 0));
});
await waitFor(() => {
- expect(screen.getByLabelText('Scroll filters left')).toBeInTheDocument();
- expect(screen.getByLabelText('Scroll filters right')).toBeInTheDocument();
+ expect(screen.getByLabelText("Scroll filters left")).toBeInTheDocument();
+ expect(screen.getByLabelText("Scroll filters right")).toBeInTheDocument();
});
- const leftArrow = screen.getByLabelText('Scroll filters left');
- const rightArrow = screen.getByLabelText('Scroll filters right');
+ const leftArrow = screen.getByLabelText("Scroll filters left");
+ const rightArrow = screen.getByLabelText("Scroll filters right");
fireEvent.click(leftArrow);
- expect(scrollBySpy).toHaveBeenCalledWith({ left: -150, behavior: 'smooth' });
+ expect(scrollBySpy).toHaveBeenCalledWith({
+ left: -150,
+ behavior: "smooth",
+ });
fireEvent.click(rightArrow);
- expect(scrollBySpy).toHaveBeenCalledWith({ left: 150, behavior: 'smooth' });
+ expect(scrollBySpy).toHaveBeenCalledWith({ left: 150, behavior: "smooth" });
});
- it('sets up event listeners on mount and cleans up on unmount', async () => {
- const addEventListenerSpy = jest.spyOn(HTMLDivElement.prototype, 'addEventListener');
- const removeEventListenerSpy = jest.spyOn(HTMLDivElement.prototype, 'removeEventListener');
- const windowAddEventListenerSpy = jest.spyOn(window, 'addEventListener');
- const windowRemoveEventListenerSpy = jest.spyOn(window, 'removeEventListener');
+ it("sets up event listeners on mount and cleans up on unmount", async () => {
+ const addEventListenerSpy = jest.spyOn(
+ HTMLDivElement.prototype,
+ "addEventListener"
+ );
+ const removeEventListenerSpy = jest.spyOn(
+ HTMLDivElement.prototype,
+ "removeEventListener"
+ );
+ const windowAddEventListenerSpy = jest.spyOn(
+ globalThis,
+ "addEventListener"
+ );
+ const windowRemoveEventListenerSpy = jest.spyOn(
+ globalThis,
+ "removeEventListener"
+ );
const { container, unmount } = render(
{
);
await waitFor(() => {
- const scrollContainer = container.querySelector('[class*="tw-overflow-x-auto"]') as HTMLDivElement;
+ const scrollContainer = container.querySelector(
+ '[class*="tw-overflow-x-auto"]'
+ ) as HTMLDivElement;
expect(scrollContainer).toBeTruthy();
});
await waitFor(() => {
- expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
- expect(windowAddEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ "scroll",
+ expect.any(Function)
+ );
+ expect(windowAddEventListenerSpy).toHaveBeenCalledWith(
+ "resize",
+ expect.any(Function)
+ );
});
unmount();
- expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
- expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "scroll",
+ expect.any(Function)
+ );
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith(
+ "resize",
+ expect.any(Function)
+ );
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
windowAddEventListenerSpy.mockRestore();
windowRemoveEventListenerSpy.mockRestore();
});
-
-});
\ No newline at end of file
+});
diff --git a/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx b/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx
index 1921d9e808..17180feaf9 100644
--- a/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx
+++ b/__tests__/components/user/collected/filters/UserPageCollectedFiltersSzn.test.tsx
@@ -1,29 +1,58 @@
-import React from 'react';
-import { render } from '@testing-library/react';
-import UserPageCollectedFiltersSzn from '@/components/user/collected/filters/UserPageCollectedFiltersSzn';
-import { MEMES_SEASON } from '@/enums';
+import UserPageCollectedFiltersSzn from "@/components/user/collected/filters/UserPageCollectedFiltersSzn";
+import { MemeSeason } from "@/entities/ISeason";
+import { render } from "@testing-library/react";
let capturedProps: any = null;
-jest.mock('@/components/utils/select/dropdown/CommonDropdown', () => (props: any) => {
- capturedProps = props;
- return ;
-});
+jest.mock(
+ "@/components/utils/select/dropdown/SeasonsGridDropdown",
+ () => (props: any) => {
+ capturedProps = props;
+ return ;
+ }
+);
-describe('UserPageCollectedFiltersSzn', () => {
+describe("UserPageCollectedFiltersSzn", () => {
beforeEach(() => {
capturedProps = null;
});
- it('passes items and active selection to dropdown', () => {
- const ref = { current: null } as React.RefObject;
+ it("passes props to SeasonsGridDropdown", () => {
+ const mockSeason: MemeSeason = {
+ id: 1,
+ start_index: 1,
+ end_index: 100,
+ count: 100,
+ name: "SZN1",
+ display: "SZN 1",
+ };
+ const setSelected = jest.fn();
+
render(
-
+
);
- const values = capturedProps.items.map((i: any) => i.value);
- expect(values).toEqual([null, ...Object.values(MEMES_SEASON)]);
- expect(capturedProps.activeItem).toBe(MEMES_SEASON.SZN1);
- expect(capturedProps.filterLabel).toBe('Season');
- expect(capturedProps.containerRef).toBe(ref);
+
+ expect(capturedProps.selected).toBe(mockSeason);
+ expect(capturedProps.initialSeasonId).toBe(1);
+ expect(capturedProps.setSelected).toBe(setSelected);
+ });
+
+ it("passes null when no season selected", () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+
+ expect(capturedProps.selected).toBeNull();
+ expect(capturedProps.initialSeasonId).toBeNull();
});
});
diff --git a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx
index 00fb91c976..b0d9b0fccf 100644
--- a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx
+++ b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.test.tsx
@@ -1,52 +1,57 @@
-import { render, screen, fireEvent } from '@testing-library/react';
-import { useQuery } from '@tanstack/react-query';
-import { AuthContext } from '@/components/auth/Auth';
-import UserPageSubscriptionsUpcoming from '@/components/user/subscriptions/UserPageSubscriptionsUpcoming';
-import { NFTSubscription, SubscriptionDetails } from '@/entities/ISubscription';
-import { createMockAuthContext } from '@/__tests__/utils/testContexts';
-
-jest.mock('@tanstack/react-query', () => ({
+import { createMockAuthContext } from "@/__tests__/utils/testContexts";
+import { AuthContext } from "@/components/auth/Auth";
+import UserPageSubscriptionsUpcoming from "@/components/user/subscriptions/UserPageSubscriptionsUpcoming";
+import { NFTSubscription, SubscriptionDetails } from "@/entities/ISubscription";
+import { useQuery } from "@tanstack/react-query";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+jest.mock("@tanstack/react-query", () => ({
useQuery: jest.fn(),
}));
const mockUpcomingRows = [
{
- utcDay: new Date('2024-01-01T00:00:00Z'),
- instantUtc: new Date('2024-01-01T15:40:00Z'),
+ utcDay: new Date("2024-01-01T00:00:00Z"),
+ instantUtc: new Date("2024-01-01T15:40:00Z"),
meme: 201,
+ seasonIndex: 1,
},
{
- utcDay: new Date('2024-01-02T00:00:00Z'),
- instantUtc: new Date('2024-01-02T15:40:00Z'),
+ utcDay: new Date("2024-01-02T00:00:00Z"),
+ instantUtc: new Date("2024-01-02T15:40:00Z"),
meme: 202,
+ seasonIndex: 1,
},
{
- utcDay: new Date('2024-01-03T00:00:00Z'),
- instantUtc: new Date('2024-01-03T15:40:00Z'),
+ utcDay: new Date("2024-01-03T00:00:00Z"),
+ instantUtc: new Date("2024-01-03T15:40:00Z"),
meme: 203,
+ seasonIndex: 1,
},
{
- utcDay: new Date('2024-01-04T00:00:00Z'),
- instantUtc: new Date('2024-01-04T15:40:00Z'),
+ utcDay: new Date("2024-01-04T00:00:00Z"),
+ instantUtc: new Date("2024-01-04T15:40:00Z"),
meme: 204,
+ seasonIndex: 1,
},
];
-jest.mock('@/components/meme-calendar/meme-calendar.helpers', () => ({
+jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({
__esModule: true,
formatFullDate: jest.fn((date: Date) => {
- const iso = date.toISOString().split('T')[0];
- const day = date.toLocaleDateString('en-US', {
- weekday: 'long',
- timeZone: 'UTC',
+ const iso = date.toISOString().split("T")[0];
+ const day = date.toLocaleDateString("en-US", {
+ weekday: "long",
+ timeZone: "UTC",
});
return `${iso} / ${day}`;
}),
- getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: mockUpcomingRows })),
+ getUpcomingMintsAcrossSeasons: jest.fn(() => mockUpcomingRows),
isMintingToday: jest.fn(() => false),
+ displayedSeasonNumberFromIndex: jest.fn((idx: number) => idx + 1),
}));
-jest.mock('@/services/api/common-api', () => ({
+jest.mock("@/services/api/common-api", () => ({
commonApiFetch: jest.fn(),
commonApiPost: jest.fn(),
}));
@@ -54,31 +59,31 @@ jest.mock('@/services/api/common-api', () => ({
const mockSubscriptions: NFTSubscription[] = [
{
token_id: 1,
- contract: '0x123',
+ contract: "0x123",
subscribed: true,
} as NFTSubscription,
{
token_id: 2,
- contract: '0x123',
+ contract: "0x123",
subscribed: false,
} as NFTSubscription,
{
token_id: 3,
- contract: '0x123',
+ contract: "0x123",
subscribed: true,
} as NFTSubscription,
{
token_id: 4,
- contract: '0x123',
+ contract: "0x123",
subscribed: false,
} as NFTSubscription,
];
const mockDetails: SubscriptionDetails = {
- profile: 'testuser',
+ profile: "testuser",
} as SubscriptionDetails;
-describe('UserPageSubscriptionsUpcoming', () => {
+describe("UserPageSubscriptionsUpcoming", () => {
const useQueryMock = useQuery as jest.Mock;
const mockRefresh = jest.fn();
const mockRequestAuth = jest.fn();
@@ -86,12 +91,14 @@ describe('UserPageSubscriptionsUpcoming', () => {
beforeEach(() => {
useQueryMock.mockImplementation(({ enabled }) => ({
- data: enabled ? {
- phase: 'Phase 1',
- phase_position: 100,
- phase_subscriptions: 500,
- airdrop_address: '0xabc123',
- } : null,
+ data: enabled
+ ? {
+ phase: "Phase 1",
+ phase_position: 100,
+ phase_subscriptions: 500,
+ airdrop_address: "0xabc123",
+ }
+ : null,
}));
mockRequestAuth.mockResolvedValue({ success: true });
});
@@ -107,7 +114,7 @@ describe('UserPageSubscriptionsUpcoming', () => {
});
const defaultProps = {
- profileKey: 'testuser',
+ profileKey: "testuser",
details: mockDetails,
memes_subscriptions: mockSubscriptions,
readonly: false,
@@ -122,125 +129,118 @@ describe('UserPageSubscriptionsUpcoming', () => {
);
};
- it('renders upcoming drops title', () => {
+ it("renders upcoming drops title", () => {
renderComponent();
- expect(screen.getByText('Upcoming Drops')).toBeInTheDocument();
+ expect(screen.getByText("Upcoming Drops")).toBeInTheDocument();
});
- it('displays first 3 subscriptions by default', () => {
+ it("displays first 3 subscriptions by default", () => {
renderComponent();
- expect(screen.getByText('The Memes #1')).toBeInTheDocument();
- expect(screen.getByText('The Memes #2')).toBeInTheDocument();
- expect(screen.getByText('The Memes #3')).toBeInTheDocument();
- expect(screen.queryByText('The Memes #4')).not.toBeInTheDocument();
+ expect(screen.getByText("The Memes #1")).toBeInTheDocument();
+ expect(screen.getByText("The Memes #2")).toBeInTheDocument();
+ expect(screen.getByText("The Memes #3")).toBeInTheDocument();
+ expect(screen.queryByText("The Memes #4")).not.toBeInTheDocument();
});
- it('shows expand button when there are more than 3 subscriptions', () => {
+ it("shows expand button when there are more than 3 subscriptions", () => {
renderComponent();
- expect(screen.getByText('Show More')).toBeInTheDocument();
+ expect(screen.getByText("Show More")).toBeInTheDocument();
});
- it('expands to show all subscriptions when Show More clicked', () => {
+ it("expands to show all subscriptions when Show More clicked", () => {
renderComponent();
-
- fireEvent.click(screen.getByText('Show More'));
-
- expect(screen.getByText('The Memes #4')).toBeInTheDocument();
- expect(screen.getByText('Show Less')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText("Show More"));
+
+ expect(screen.getByText("The Memes #4")).toBeInTheDocument();
+ expect(screen.getByText("Show Less")).toBeInTheDocument();
});
- it('collapses back to 3 subscriptions when Show Less clicked', () => {
+ it("collapses back to 3 subscriptions when Show Less clicked", () => {
renderComponent();
-
- fireEvent.click(screen.getByText('Show More'));
- fireEvent.click(screen.getByText('Show Less'));
-
- expect(screen.queryByText('The Memes #4')).not.toBeInTheDocument();
- expect(screen.getByText('Show More')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText("Show More"));
+ fireEvent.click(screen.getByText("Show Less"));
+
+ expect(screen.queryByText("The Memes #4")).not.toBeInTheDocument();
+ expect(screen.getByText("Show More")).toBeInTheDocument();
});
- it('displays subscription toggles in correct state', () => {
+ it("displays subscription toggles in correct state", () => {
renderComponent();
-
- const toggles = screen.getAllByRole('button');
- const subscriptionToggles = toggles.filter(toggle =>
- toggle.getAttribute('aria-label')?.includes('Toggle subscription')
- );
-
- // Since the component may not render subscription toggles in this test setup,
- // we'll just verify that buttons exist on the page (Show More, etc.)
+
+ const toggles = screen.getAllByRole("button");
expect(toggles.length).toBeGreaterThan(0);
});
- it('shows phase information for first subscription', () => {
+ it("shows phase information for first subscription", () => {
renderComponent();
-
+
expect(screen.getByText(/Phase: Phase 1/)).toBeInTheDocument();
expect(screen.getByText(/Subscription Position: 100/)).toBeInTheDocument();
expect(screen.getByText(/Airdrop Address: 0xabc123/)).toBeInTheDocument();
});
- it('does not show phase information when not available', () => {
+ it("does not show phase information when not available", () => {
useQueryMock.mockReturnValue({
data: null,
});
-
+
renderComponent();
-
+
expect(screen.queryByText(/Phase:/)).not.toBeInTheDocument();
});
- it('displays dates for subscriptions', () => {
+ it("displays dates for subscriptions", () => {
renderComponent();
-
- expect(screen.getByText('2024-01-01 / Monday')).toBeInTheDocument();
- expect(screen.getByText('2024-01-02 / Tuesday')).toBeInTheDocument();
+
+ expect(screen.getByText("2024-01-01 / Monday")).toBeInTheDocument();
+ expect(screen.getByText("2024-01-02 / Tuesday")).toBeInTheDocument();
});
- it('shows minting today message when applicable', () => {
- const { isMintingToday } = require('@/components/meme-calendar/meme-calendar.helpers');
+ it("shows minting today message when applicable", () => {
+ const {
+ isMintingToday,
+ } = require("@/components/meme-calendar/meme-calendar.helpers");
isMintingToday.mockReturnValue(true);
-
+
renderComponent();
-
- expect(screen.getByText('- Minting Today')).toBeInTheDocument();
+
+ expect(screen.getByText("Minting Today")).toBeInTheDocument();
});
- it('disables toggles in readonly mode', () => {
+ it("disables toggles in readonly mode", () => {
renderComponent({ readonly: true });
-
- const toggles = screen.getAllByRole('button');
- const subscriptionToggles = toggles.filter(toggle =>
- toggle.getAttribute('aria-label')?.includes('Toggle subscription')
+
+ const toggles = screen.getAllByRole("button");
+ const subscriptionToggles = toggles.filter((toggle) =>
+ toggle.getAttribute("aria-label")?.includes("Toggle subscription")
);
-
- subscriptionToggles.forEach(toggle => {
+
+ subscriptionToggles.forEach((toggle) => {
expect(toggle).toBeDisabled();
});
});
- it('handles empty subscriptions list', () => {
+ it("handles empty subscriptions list", () => {
renderComponent({ memes_subscriptions: [] });
-
- expect(screen.getByText('Upcoming Drops')).toBeInTheDocument();
- expect(screen.queryByText('Show More')).not.toBeInTheDocument();
+
+ expect(screen.getByText("Upcoming Drops")).toBeInTheDocument();
+ expect(screen.queryByText("Show More")).not.toBeInTheDocument();
});
- it('does not show expand button when no subscriptions', () => {
+ it("does not show expand button when no subscriptions", () => {
renderComponent({ memes_subscriptions: [] });
-
- expect(screen.queryByText('Show More')).not.toBeInTheDocument();
+
+ expect(screen.queryByText("Show More")).not.toBeInTheDocument();
});
- it('calls query for final subscription data only for first subscription', () => {
+ it("calls query for final subscription data only for first subscription", () => {
renderComponent();
-
+
expect(useQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
- queryKey: [
- 'consolidation-final-subscription',
- 'testuser-0x123-1',
- ],
+ queryKey: ["consolidation-final-subscription", "testuser-0x123-1"],
enabled: true,
})
);
diff --git a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx
index 558d6e75b3..4b3acc3bf7 100644
--- a/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx
+++ b/__tests__/components/user/subscriptions/UserPageSubscriptionsUpcoming.toggle.test.tsx
@@ -1,46 +1,51 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { useQuery } from '@tanstack/react-query';
-import UserPageSubscriptionsUpcoming from '@/components/user/subscriptions/UserPageSubscriptionsUpcoming';
-import { AuthContext } from '@/components/auth/Auth';
-import { createMockAuthContext } from '@/__tests__/utils/testContexts';
-
-jest.mock('react-toggle', () => (props: any) => (
+import { createMockAuthContext } from "@/__tests__/utils/testContexts";
+import { AuthContext } from "@/components/auth/Auth";
+import UserPageSubscriptionsUpcoming from "@/components/user/subscriptions/UserPageSubscriptionsUpcoming";
+import { useQuery } from "@tanstack/react-query";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+jest.mock("react-toggle", () => (props: any) => (
));
-jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() }));
+jest.mock("@tanstack/react-query", () => ({ useQuery: jest.fn() }));
const mockUpcomingRows = [
{
- utcDay: new Date('2024-01-01T00:00:00Z'),
- instantUtc: new Date('2024-01-01T15:40:00Z'),
+ utcDay: new Date("2024-01-01T00:00:00Z"),
+ instantUtc: new Date("2024-01-01T15:40:00Z"),
meme: 201,
+ seasonIndex: 1,
},
];
-jest.mock('@/components/meme-calendar/meme-calendar.helpers', () => ({
+jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({
__esModule: true,
- formatFullDate: jest.fn(() => '2024-01-01 / Monday'),
- getUpcomingMintsForCurrentOrNextSeason: jest.fn(() => ({ rows: mockUpcomingRows })),
+ getUpcomingMintsAcrossSeasons: jest.fn(() => mockUpcomingRows),
isMintingToday: jest.fn(() => false),
+ displayedSeasonNumberFromIndex: jest.fn((idx: number) => idx + 1),
+ formatFullDate: jest.fn(() => "Mon, 1 Jan 2024"),
}));
-jest.mock('@/services/api/common-api', () => ({
+jest.mock("@/services/api/common-api", () => ({
commonApiFetch: jest.fn(),
commonApiPost: jest.fn(),
}));
-const { commonApiPost } = require('@/services/api/common-api');
+const { commonApiPost } = require("@/services/api/common-api");
-const sub = { token_id:1, contract:'0x123', subscribed:true } as any;
-const details = { profile:'test' } as any;
+const sub = { token_id: 1, contract: "0x123", subscribed: true } as any;
+const details = { profile: "test" } as any;
const useQueryMock = useQuery as jest.Mock;
function renderComp() {
useQueryMock.mockReturnValue({ data: null });
- const auth = createMockAuthContext({ requestAuth: jest.fn(async () => ({ success: true })), setToast: jest.fn() });
+ const auth = createMockAuthContext({
+ requestAuth: jest.fn(async () => ({ success: true })),
+ setToast: jest.fn(),
+ });
return render(
{
+test("toggles subscription and posts update", async () => {
const user = userEvent.setup();
- (commonApiPost as jest.Mock).mockResolvedValue({ token_id:1, subscribed:false });
+ (commonApiPost as jest.Mock).mockResolvedValue({
+ token_id: 1,
+ subscribed: false,
+ });
const { container } = renderComp();
- await user.click(screen.getByTestId('toggle'));
+ await user.click(screen.getByTestId("toggle"));
expect(commonApiPost).toHaveBeenCalled();
});
diff --git a/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx b/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx
new file mode 100644
index 0000000000..69ac316026
--- /dev/null
+++ b/__tests__/components/utils/select/dropdown/SeasonsGridDropdown.test.tsx
@@ -0,0 +1,299 @@
+import SeasonsGridDropdown from "@/components/utils/select/dropdown/SeasonsGridDropdown";
+import { MemeSeason } from "@/entities/ISeason";
+import { act, fireEvent, render, screen } from "@testing-library/react";
+
+const mockSeasons: MemeSeason[] = [
+ {
+ id: 1,
+ start_index: 1,
+ end_index: 100,
+ count: 100,
+ name: "SZN1",
+ display: "SZN 1",
+ },
+ {
+ id: 2,
+ start_index: 101,
+ end_index: 200,
+ count: 100,
+ name: "SZN2",
+ display: "SZN 2",
+ },
+ {
+ id: 3,
+ start_index: 201,
+ end_index: 300,
+ count: 100,
+ name: "SZN3",
+ display: "SZN 3",
+ },
+];
+
+const flushPromises = () => act(() => Promise.resolve());
+
+jest.mock("@/services/api/common-api", () => ({
+ commonApiFetch: jest.fn(() => Promise.resolve(mockSeasons)),
+}));
+
+jest.mock("framer-motion", () => ({
+ useAnimate: () => [{ current: null }, jest.fn()],
+ AnimatePresence: ({ children }: any) => <>{children}>,
+ motion: {
+ div: ({ children, ...props }: any) => {children}
,
+ },
+}));
+
+jest.mock("react-use", () => ({
+ createBreakpoint: () => () => "LG",
+ useClickAway: jest.fn(),
+ useKeyPressEvent: jest.fn(),
+}));
+
+Object.defineProperty(globalThis, "matchMedia", {
+ writable: true,
+ value: jest.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+});
+
+describe("SeasonsGridDropdown", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders with 'All Seasons' label when nothing is selected", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ expect(
+ screen.getByRole("button", { name: /Season: All Seasons/i })
+ ).toBeInTheDocument();
+ });
+
+ it("displays selected season label when a season is selected", async () => {
+ const setSelected = jest.fn();
+ const selectedSeason = mockSeasons[1];
+
+ render(
+
+ );
+ await flushPromises();
+
+ expect(
+ screen.getByRole("button", { name: /Season: SZN 2/i })
+ ).toBeInTheDocument();
+ });
+
+ it("opens dropdown on button click", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ fireEvent.click(button);
+
+ expect(screen.getByRole("menu")).toBeInTheDocument();
+ });
+
+ it("fetches and displays seasons from API", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ fireEvent.click(button);
+
+ expect(screen.getByRole("menu")).toBeInTheDocument();
+
+ const menuItems = screen.getAllByRole("menuitem");
+ expect(menuItems).toHaveLength(4);
+ expect(menuItems[0]).toHaveTextContent("All Seasons");
+ expect(menuItems[1]).toHaveTextContent("SZN 1");
+ expect(menuItems[2]).toHaveTextContent("SZN 2");
+ expect(menuItems[3]).toHaveTextContent("SZN 3");
+ });
+
+ it("calls setSelected with null when 'All Seasons' is clicked", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: SZN 1/i });
+ fireEvent.click(button);
+
+ const allSeasonsButton = screen.getByRole("menuitem", {
+ name: /All Seasons/i,
+ });
+ fireEvent.click(allSeasonsButton);
+
+ expect(setSelected).toHaveBeenCalledWith(null);
+ });
+
+ it("calls setSelected with season when a season is clicked", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ fireEvent.click(button);
+
+ const szn2Button = screen.getByRole("menuitem", { name: /SZN 2/i });
+ fireEvent.click(szn2Button);
+
+ expect(setSelected).toHaveBeenCalledWith(mockSeasons[1]);
+ });
+
+ it("applies selected styles to 'All Seasons' when selected is null", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ fireEvent.click(button);
+
+ const allSeasonsButton = screen.getByRole("menuitem", {
+ name: /All Seasons/i,
+ });
+ expect(allSeasonsButton).toHaveClass("tw-bg-primary-500/20");
+ expect(allSeasonsButton).toHaveClass("tw-border-primary-500");
+ expect(allSeasonsButton).toHaveClass("tw-text-primary-300");
+ });
+
+ it("applies selected styles to the selected season", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: SZN 1/i });
+ fireEvent.click(button);
+
+ const szn1Button = screen.getByRole("menuitem", { name: /SZN 1/i });
+ expect(szn1Button).toHaveClass("tw-bg-primary-500/20");
+ expect(szn1Button).toHaveClass("tw-border-primary-500");
+ expect(szn1Button).toHaveClass("tw-text-primary-300");
+ });
+
+ it("applies non-selected styles to unselected items", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: SZN 1/i });
+ fireEvent.click(button);
+
+ const szn2Button = screen.getByRole("menuitem", { name: /SZN 2/i });
+ expect(szn2Button).toHaveClass("tw-bg-transparent");
+ expect(szn2Button).toHaveClass("tw-border-iron-700");
+ expect(szn2Button).toHaveClass("tw-text-iron-200");
+ });
+
+ it("applies initial season from initialSeasonId prop", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ expect(setSelected).toHaveBeenCalledWith(mockSeasons[1]);
+ });
+
+ it("does not apply initial season when initialSeasonId is null", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ expect(
+ screen.getByRole("button", { name: /Season: All Seasons/i })
+ ).toBeInTheDocument();
+ expect(setSelected).not.toHaveBeenCalled();
+ });
+
+ it("disables button when disabled prop is true", async () => {
+ const setSelected = jest.fn();
+
+ render(
+
+ );
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ expect(button).toBeDisabled();
+ expect(button).toHaveClass("tw-opacity-50");
+ });
+
+ it("closes dropdown when selecting an item", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ fireEvent.click(button);
+
+ const szn1Button = screen.getByRole("menuitem", { name: /SZN 1/i });
+ fireEvent.click(szn1Button);
+
+ expect(button).toHaveAttribute("aria-expanded", "false");
+ });
+
+ it("has correct aria attributes", async () => {
+ const setSelected = jest.fn();
+
+ render();
+ await flushPromises();
+
+ const button = screen.getByRole("button", { name: /Season: All Seasons/i });
+ expect(button).toHaveAttribute("aria-haspopup", "true");
+ expect(button).toHaveAttribute("aria-expanded", "false");
+
+ fireEvent.click(button);
+
+ expect(button).toHaveAttribute("aria-expanded", "true");
+ });
+});
diff --git a/__tests__/hooks/useActivityFilters.test.ts b/__tests__/hooks/useActivityFilters.test.ts
index 595894b432..144c779c88 100644
--- a/__tests__/hooks/useActivityFilters.test.ts
+++ b/__tests__/hooks/useActivityFilters.test.ts
@@ -1,208 +1,208 @@
-import { renderHook, act } from '@testing-library/react';
-import { useActivityFilters } from '@/hooks/useActivityFilters';
-import { TypeFilter, ContractFilter } from '@/hooks/useActivityData';
+import { ContractFilter, TypeFilter } from "@/hooks/useActivityData";
+import { useActivityFilters } from "@/hooks/useActivityFilters";
+import { act, renderHook } from "@testing-library/react";
-describe('useActivityFilters', () => {
- describe('Initial State', () => {
- it('initializes with correct default state', () => {
+describe("useActivityFilters", () => {
+ describe("Initial State", () => {
+ it("initializes with correct default state", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
- expect(typeof result.current.setTypeFilter).toBe('function');
- expect(typeof result.current.setSelectedContract).toBe('function');
- expect(typeof result.current.resetFilters).toBe('function');
+ expect(typeof result.current.setTypeFilter).toBe("function");
+ expect(typeof result.current.setSelectedContract).toBe("function");
+ expect(typeof result.current.resetFilters).toBe("function");
});
- it('returns consistent object reference structure', () => {
+ it("returns consistent object reference structure", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
const returnedKeys = Object.keys(result.current);
const expectedKeys = [
- 'typeFilter',
- 'selectedContract',
- 'setTypeFilter',
- 'setSelectedContract',
- 'resetFilters'
+ "typeFilter",
+ "selectedContract",
+ "setTypeFilter",
+ "setSelectedContract",
+ "resetFilters",
];
-
+
expect(returnedKeys.sort()).toEqual(expectedKeys.sort());
});
});
- describe('TypeFilter State Management', () => {
- it('updates typeFilter when setTypeFilter is called', () => {
+ describe("TypeFilter State Management", () => {
+ it("updates typeFilter when setTypeFilter is called", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
});
- it('handles all TypeFilter enum values correctly', () => {
+ it("handles all TypeFilter enum values correctly", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
const allTypeFilters = [
TypeFilter.ALL,
TypeFilter.AIRDROPS,
TypeFilter.MINTS,
TypeFilter.SALES,
TypeFilter.TRANSFERS,
- TypeFilter.BURNS
+ TypeFilter.BURNS,
];
-
- allTypeFilters.forEach(filterValue => {
+
+ allTypeFilters.forEach((filterValue) => {
act(() => {
result.current.setTypeFilter(filterValue);
});
-
+
expect(result.current.typeFilter).toBe(filterValue);
});
});
- it('maintains selectedContract state when typeFilter changes', () => {
+ it("maintains selectedContract state when typeFilter changes", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// First set a contract filter
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
-
+
// Then change type filter
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
-
+
// Contract filter should remain unchanged
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
});
- it('allows rapid consecutive typeFilter updates', () => {
+ it("allows rapid consecutive typeFilter updates", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
result.current.setTypeFilter(TypeFilter.TRANSFERS);
result.current.setTypeFilter(TypeFilter.MINTS);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.MINTS);
});
});
- describe('ContractFilter State Management', () => {
- it('updates selectedContract when setSelectedContract is called', () => {
+ describe("ContractFilter State Management", () => {
+ it("updates selectedContract when setSelectedContract is called", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
-
+
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('handles all ContractFilter enum values correctly', () => {
+ it("handles all ContractFilter enum values correctly", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
const allContractFilters = [
ContractFilter.ALL,
ContractFilter.MEMES,
ContractFilter.NEXTGEN,
- ContractFilter.GRADIENTS
+ ContractFilter.GRADIENTS,
];
-
- allContractFilters.forEach(filterValue => {
+
+ allContractFilters.forEach((filterValue) => {
act(() => {
result.current.setSelectedContract(filterValue);
});
-
+
expect(result.current.selectedContract).toBe(filterValue);
});
});
- it('maintains typeFilter state when selectedContract changes', () => {
+ it("maintains typeFilter state when selectedContract changes", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// First set a type filter
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
-
+
// Then change contract filter
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
// Type filter should remain unchanged
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('allows rapid consecutive selectedContract updates', () => {
+ it("allows rapid consecutive selectedContract updates", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
result.current.setSelectedContract(ContractFilter.NEXTGEN);
result.current.setSelectedContract(ContractFilter.GRADIENTS);
});
-
+
expect(result.current.selectedContract).toBe(ContractFilter.GRADIENTS);
});
});
- describe('Reset Functionality', () => {
- it('resets both filters to default state', () => {
+ describe("Reset Functionality", () => {
+ it("resets both filters to default state", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Set both filters to non-default values
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
-
+
// Reset filters
act(() => {
result.current.resetFilters();
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
});
- it('resets filters even when already at default state', () => {
+ it("resets filters even when already at default state", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Filters are already at default state
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
-
+
// Reset should still work without issues
act(() => {
result.current.resetFilters();
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
});
- it('can reset after multiple filter changes', () => {
+ it("can reset after multiple filter changes", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Make multiple changes
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
@@ -211,190 +211,216 @@ describe('useActivityFilters', () => {
result.current.setSelectedContract(ContractFilter.NEXTGEN);
result.current.setTypeFilter(TypeFilter.BURNS);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.BURNS);
expect(result.current.selectedContract).toBe(ContractFilter.NEXTGEN);
-
+
// Reset should bring everything back to defaults
act(() => {
result.current.resetFilters();
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
});
});
- describe('Callback Integration', () => {
- describe('setTypeFilter with resetPage callback', () => {
- it('calls resetPage callback when provided to setTypeFilter', () => {
+ describe("Callback Integration", () => {
+ describe("setTypeFilter with resetPage callback", () => {
+ it("calls resetPage callback when provided to setTypeFilter", () => {
const { result } = renderHook(() => useActivityFilters());
const mockResetPage = jest.fn();
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, mockResetPage);
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(1);
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
});
- it('does not crash when no resetPage callback provided to setTypeFilter', () => {
+ it("does not crash when no resetPage callback provided to setTypeFilter", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
expect(() => {
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
}).not.toThrow();
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
});
- it('calls resetPage before state setter is called', () => {
+ it("calls resetPage before state setter is called", () => {
const { result } = renderHook(() => useActivityFilters());
const executionOrder: string[] = [];
-
+
const mockResetPage = jest.fn(() => {
// Capture the typeFilter state at the moment resetPage is called
- executionOrder.push(`resetPage called, typeFilter: ${result.current.typeFilter}`);
+ executionOrder.push(
+ `resetPage called, typeFilter: ${result.current.typeFilter}`
+ );
});
-
+
// Initial state
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, mockResetPage);
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(1);
// resetPage is called while state is still at old value
expect(executionOrder).toEqual([
- 'resetPage called, typeFilter: All'
+ "resetPage called, typeFilter: All Transactions",
]);
// But after the act() block, state should be updated
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
});
- it('handles resetPage callback that throws an error', () => {
+ it("handles resetPage callback that throws an error", () => {
const { result } = renderHook(() => useActivityFilters());
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
+ const consoleErrorSpy = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+
const mockResetPageWithError = jest.fn(() => {
- throw new Error('Reset page failed');
+ throw new Error("Reset page failed");
});
-
+
// The implementation calls resetPage first, then setTypeFilterState
// If resetPage throws, the setTypeFilterState never gets called
expect(() => {
act(() => {
- result.current.setTypeFilter(TypeFilter.SALES, mockResetPageWithError);
+ result.current.setTypeFilter(
+ TypeFilter.SALES,
+ mockResetPageWithError
+ );
});
- }).toThrow('Reset page failed');
-
+ }).toThrow("Reset page failed");
+
expect(mockResetPageWithError).toHaveBeenCalledTimes(1);
// State should NOT be updated if callback throws before state setter is called
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
-
+
consoleErrorSpy.mockRestore();
});
});
- describe('setSelectedContract with resetPage callback', () => {
- it('calls resetPage callback when provided to setSelectedContract', () => {
+ describe("setSelectedContract with resetPage callback", () => {
+ it("calls resetPage callback when provided to setSelectedContract", () => {
const { result } = renderHook(() => useActivityFilters());
const mockResetPage = jest.fn();
-
+
act(() => {
- result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage);
+ result.current.setSelectedContract(
+ ContractFilter.MEMES,
+ mockResetPage
+ );
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(1);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('does not crash when no resetPage callback provided to setSelectedContract', () => {
+ it("does not crash when no resetPage callback provided to setSelectedContract", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
expect(() => {
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
});
}).not.toThrow();
-
+
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('calls resetPage before state setter is called', () => {
+ it("calls resetPage before state setter is called", () => {
const { result } = renderHook(() => useActivityFilters());
const executionOrder: string[] = [];
-
+
const mockResetPage = jest.fn(() => {
- executionOrder.push(`resetPage called, selectedContract: ${result.current.selectedContract}`);
+ executionOrder.push(
+ `resetPage called, selectedContract: ${result.current.selectedContract}`
+ );
});
-
+
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
-
+
act(() => {
- result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage);
+ result.current.setSelectedContract(
+ ContractFilter.MEMES,
+ mockResetPage
+ );
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(1);
expect(executionOrder).toEqual([
- 'resetPage called, selectedContract: All'
+ "resetPage called, selectedContract: All Collections",
]);
// But after the act() block, state should be updated
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('handles resetPage callback that throws an error', () => {
+ it("handles resetPage callback that throws an error", () => {
const { result } = renderHook(() => useActivityFilters());
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
+ const consoleErrorSpy = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+
const mockResetPageWithError = jest.fn(() => {
- throw new Error('Reset page failed');
+ throw new Error("Reset page failed");
});
-
+
expect(() => {
act(() => {
- result.current.setSelectedContract(ContractFilter.MEMES, mockResetPageWithError);
+ result.current.setSelectedContract(
+ ContractFilter.MEMES,
+ mockResetPageWithError
+ );
});
- }).toThrow('Reset page failed');
-
+ }).toThrow("Reset page failed");
+
expect(mockResetPageWithError).toHaveBeenCalledTimes(1);
// State should NOT be updated if callback throws before state setter is called
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
-
+
consoleErrorSpy.mockRestore();
});
});
- describe('Callback with both filters', () => {
- it('calls resetPage callback for both filter setters independently', () => {
+ describe("Callback with both filters", () => {
+ it("calls resetPage callback for both filter setters independently", () => {
const { result } = renderHook(() => useActivityFilters());
const mockResetPage1 = jest.fn();
const mockResetPage2 = jest.fn();
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, mockResetPage1);
- result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage2);
+ result.current.setSelectedContract(
+ ContractFilter.MEMES,
+ mockResetPage2
+ );
});
-
+
expect(mockResetPage1).toHaveBeenCalledTimes(1);
expect(mockResetPage2).toHaveBeenCalledTimes(1);
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('can use the same callback function for both filters', () => {
+ it("can use the same callback function for both filters", () => {
const { result } = renderHook(() => useActivityFilters());
const mockResetPage = jest.fn();
-
+
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, mockResetPage);
- result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage);
+ result.current.setSelectedContract(
+ ContractFilter.MEMES,
+ mockResetPage
+ );
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(2);
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
@@ -402,193 +428,198 @@ describe('useActivityFilters', () => {
});
});
- describe('Combined Filter Operations', () => {
- it('can set both filters and then reset them', () => {
+ describe("Combined Filter Operations", () => {
+ it("can set both filters and then reset them", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Set both filters
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
-
+
// Reset
act(() => {
result.current.resetFilters();
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
});
- it('handles complex filter sequences correctly', () => {
+ it("handles complex filter sequences correctly", () => {
const { result } = renderHook(() => useActivityFilters());
const mockResetPage = jest.fn();
-
+
// Complex sequence of operations
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, mockResetPage);
result.current.setSelectedContract(ContractFilter.MEMES, mockResetPage);
result.current.setTypeFilter(TypeFilter.TRANSFERS, mockResetPage);
result.current.resetFilters();
- result.current.setSelectedContract(ContractFilter.NEXTGEN, mockResetPage);
+ result.current.setSelectedContract(
+ ContractFilter.NEXTGEN,
+ mockResetPage
+ );
});
-
+
expect(mockResetPage).toHaveBeenCalledTimes(4); // 3 setter calls + 0 for resetFilters
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.NEXTGEN);
});
});
- describe('Edge Cases and Error Scenarios', () => {
- it('maintains referential stability for function references across renders', () => {
+ describe("Edge Cases and Error Scenarios", () => {
+ it("maintains referential stability for function references across renders", () => {
const { result, rerender } = renderHook(() => useActivityFilters());
-
+
const firstRenderFunctions = {
setTypeFilter: result.current.setTypeFilter,
setSelectedContract: result.current.setSelectedContract,
- resetFilters: result.current.resetFilters
+ resetFilters: result.current.resetFilters,
};
-
+
// Trigger re-render by changing some internal state
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
-
+
rerender();
-
+
// Function references should remain the same (though with React hooks this might not be guaranteed without useCallback)
// This test documents current behavior - if functions are recreated on each render, this test will fail
// and that's actually expected behavior for useState-based hooks without memoization
- expect(typeof result.current.setTypeFilter).toBe('function');
- expect(typeof result.current.setSelectedContract).toBe('function');
- expect(typeof result.current.resetFilters).toBe('function');
+ expect(typeof result.current.setTypeFilter).toBe("function");
+ expect(typeof result.current.setSelectedContract).toBe("function");
+ expect(typeof result.current.resetFilters).toBe("function");
});
- it('handles undefined resetPage callback gracefully using optional chaining', () => {
+ it("handles undefined resetPage callback gracefully using optional chaining", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Pass undefined explicitly (though TypeScript should prevent this in real usage)
act(() => {
result.current.setTypeFilter(TypeFilter.SALES, undefined);
result.current.setSelectedContract(ContractFilter.MEMES, undefined);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
});
- it('works correctly when hook is used multiple times in same component', () => {
+ it("works correctly when hook is used multiple times in same component", () => {
// Simulate multiple instances of the hook
const { result: instance1 } = renderHook(() => useActivityFilters());
const { result: instance2 } = renderHook(() => useActivityFilters());
-
+
// Each instance should be independent
act(() => {
instance1.current.setTypeFilter(TypeFilter.SALES);
instance2.current.setTypeFilter(TypeFilter.TRANSFERS);
});
-
+
expect(instance1.current.typeFilter).toBe(TypeFilter.SALES);
expect(instance2.current.typeFilter).toBe(TypeFilter.TRANSFERS);
-
+
// Reset one instance should not affect the other
act(() => {
instance1.current.resetFilters();
});
-
+
expect(instance1.current.typeFilter).toBe(TypeFilter.ALL);
expect(instance2.current.typeFilter).toBe(TypeFilter.TRANSFERS);
});
});
- describe('TypeScript Type Safety', () => {
- it('returns correct TypeScript types for all properties', () => {
+ describe("TypeScript Type Safety", () => {
+ it("returns correct TypeScript types for all properties", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Test that the return values are of correct types
expect(Object.values(TypeFilter)).toContain(result.current.typeFilter);
- expect(Object.values(ContractFilter)).toContain(result.current.selectedContract);
- expect(typeof result.current.setTypeFilter).toBe('function');
- expect(typeof result.current.setSelectedContract).toBe('function');
- expect(typeof result.current.resetFilters).toBe('function');
+ expect(Object.values(ContractFilter)).toContain(
+ result.current.selectedContract
+ );
+ expect(typeof result.current.setTypeFilter).toBe("function");
+ expect(typeof result.current.setSelectedContract).toBe("function");
+ expect(typeof result.current.resetFilters).toBe("function");
});
- it('ensures enum values are properly typed (not strings)', () => {
+ it("ensures enum values are properly typed (not strings)", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Verify that the returned enum values are actual enum members
expect(result.current.typeFilter).toBe(TypeFilter.ALL);
expect(result.current.selectedContract).toBe(ContractFilter.ALL);
-
+
// The enum values should be equal to their string representations
- expect(TypeFilter.ALL).toBe('All');
- expect(ContractFilter.ALL).toBe('All');
-
+ expect(TypeFilter.ALL).toBe("All Transactions");
+ expect(ContractFilter.ALL).toBe("All Collections");
+
// But when we set them, we should use the enum, not strings
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
expect(result.current.typeFilter).toBe(TypeFilter.SALES);
expect(result.current.selectedContract).toBe(ContractFilter.MEMES);
- expect(result.current.typeFilter).toBe('Sales');
- expect(result.current.selectedContract).toBe('Memes');
+ expect(result.current.typeFilter).toBe("Sales");
+ expect(result.current.selectedContract).toBe("The Memes");
});
});
- describe('Hook Interface Contract', () => {
- it('implements the UseActivityFiltersReturn interface correctly', () => {
+ describe("Hook Interface Contract", () => {
+ it("implements the UseActivityFiltersReturn interface correctly", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
// Verify all required properties exist
const requiredProperties = [
- 'typeFilter',
- 'selectedContract',
- 'setTypeFilter',
- 'setSelectedContract',
- 'resetFilters'
+ "typeFilter",
+ "selectedContract",
+ "setTypeFilter",
+ "setSelectedContract",
+ "resetFilters",
];
-
- requiredProperties.forEach(prop => {
+
+ requiredProperties.forEach((prop) => {
expect(result.current).toHaveProperty(prop);
});
-
+
// Verify no unexpected properties exist
const actualProperties = Object.keys(result.current);
expect(actualProperties.sort()).toEqual(requiredProperties.sort());
});
- it('maintains consistent return object shape across state changes', () => {
+ it("maintains consistent return object shape across state changes", () => {
const { result } = renderHook(() => useActivityFilters());
-
+
const initialShape = Object.keys(result.current).sort();
-
+
// Make various state changes
act(() => {
result.current.setTypeFilter(TypeFilter.SALES);
});
-
+
const afterTypeFilterChange = Object.keys(result.current).sort();
expect(afterTypeFilterChange).toEqual(initialShape);
-
+
act(() => {
result.current.setSelectedContract(ContractFilter.MEMES);
});
-
+
const afterContractFilterChange = Object.keys(result.current).sort();
expect(afterContractFilterChange).toEqual(initialShape);
-
+
act(() => {
result.current.resetFilters();
});
-
+
const afterReset = Object.keys(result.current).sort();
expect(afterReset).toEqual(initialShape);
});
});
-});
\ No newline at end of file
+});
diff --git a/components/home/Home.tsx b/components/home/Home.tsx
index 81ad8b7de5..13ab12a930 100644
--- a/components/home/Home.tsx
+++ b/components/home/Home.tsx
@@ -39,16 +39,17 @@ export default function Home({
{featuredNextgen && !isEmptyObject(featuredNextgen) && (
-
-
- Discover NextGen -{" "}
- {featuredNextgen.name}{" "}
+
+
+ Discover NextGen - {featuredNextgen.name}{" "}
- View Collection
+
+ View Collection
+
diff --git a/components/latest-activity/ActivityFilters.tsx b/components/latest-activity/ActivityFilters.tsx
index f3849cfcf4..db24e2e2e8 100644
--- a/components/latest-activity/ActivityFilters.tsx
+++ b/components/latest-activity/ActivityFilters.tsx
@@ -1,8 +1,9 @@
"use client";
-import { Col, Dropdown } from "react-bootstrap";
-import styles from "./LatestActivity.module.scss";
-import { TypeFilter, ContractFilter } from "@/hooks/useActivityData";
+import CommonDropdown from "@/components/utils/select/dropdown/CommonDropdown";
+import { ContractFilter, TypeFilter } from "@/hooks/useActivityData";
+import { useMemo } from "react";
+import { Col } from "react-bootstrap";
interface ActivityFiltersProps {
readonly typeFilter: TypeFilter;
@@ -19,40 +20,45 @@ export default function ActivityFilters({
onContractFilterChange,
isMobile,
}: ActivityFiltersProps) {
+ const contractItems = useMemo(
+ () =>
+ Object.values(ContractFilter).map((contract) => ({
+ key: contract,
+ label: contract,
+ value: contract,
+ })),
+ []
+ );
+
+ const typeItems = useMemo(
+ () =>
+ Object.values(TypeFilter).map((type) => ({
+ key: type,
+ label: type,
+ value: type,
+ })),
+ []
+ );
+
return (
-
- Collection: {selectedContract}
-
- {Object.values(ContractFilter).map((contract) => (
- onContractFilterChange(contract)}
- >
- {contract}
-
- ))}
-
-
-
- Filter: {typeFilter}
-
- {Object.values(TypeFilter).map((filter) => (
- onTypeFilterChange(filter)}
- >
- {filter}
-
- ))}
-
-
+ }`}>
+
+
);
}
diff --git a/components/latest-activity/ActivityHeader.tsx b/components/latest-activity/ActivityHeader.tsx
index 503d088a85..71f50ddbb9 100644
--- a/components/latest-activity/ActivityHeader.tsx
+++ b/components/latest-activity/ActivityHeader.tsx
@@ -2,7 +2,6 @@
import Link from "next/link";
import { Col } from "react-bootstrap";
-import homeStyles from "@/styles/Home.module.scss";
import DotLoader from "../dotLoader/DotLoader";
interface ActivityHeaderProps {
@@ -18,13 +17,14 @@ export default function ActivityHeader({
+ className="tw-py-2 d-flex align-items-center justify-content-between">
- NFT Activity
+ NFT Activity
{showViewAll ? (
-
- View All
+
+
+ View All
+
) : (
fetching &&
diff --git a/components/latest-activity/LatestActivity.module.scss b/components/latest-activity/LatestActivity.module.scss
index 081085766f..19fb2b6e46 100644
--- a/components/latest-activity/LatestActivity.module.scss
+++ b/components/latest-activity/LatestActivity.module.scss
@@ -65,7 +65,6 @@
}
.filterDropdown {
- line-height: 48px;
text-align: center;
background-color: transparent;
border: none;
diff --git a/components/meme-calendar/MemeCalendarOverview.tsx b/components/meme-calendar/MemeCalendarOverview.tsx
index 4258c2d936..4c6d112336 100644
--- a/components/meme-calendar/MemeCalendarOverview.tsx
+++ b/components/meme-calendar/MemeCalendarOverview.tsx
@@ -25,6 +25,7 @@ import {
getMintTimelineDetails,
getNextMintStart,
getUpcomingMintsForCurrentOrNextSeason,
+ getUpcomingMintsForSeasonIndex,
printCalendarInvites,
ymd,
} from "./meme-calendar.helpers";
@@ -44,12 +45,10 @@ export default function MemeCalendarOverview({
return (
-
- The Memes Minting Calendar
-
+
The Memes Minting Calendar
{showViewAll && (
-
-
+
+
View Full Calendar
@@ -414,38 +413,61 @@ function MemeCalendarOverviewUpcomingMints({
}: MemeCalendarOverviewUpcomingMintsProps) {
const [now] = useState(new Date());
- const { seasonStart, seasonEndInclusive, seasonIndex, rows } =
- useMemo(
- () => getUpcomingMintsForCurrentOrNextSeason(now),
- [now]
- );
+ const currentSeason = useMemo(
+ () => getUpcomingMintsForCurrentOrNextSeason(now),
+ [now]
+ );
const canonicalNextMintNumber = useMemo(
() => getCanonicalNextMintNumber(now),
[now]
);
- const { filteredRows, hasCanonicalNext } = useMemo(() => {
- const containsCanonical = rows.some(
+ const {
+ seasonStart,
+ seasonEndInclusive,
+ seasonIndex,
+ filteredRows,
+ isNextSeason,
+ } = useMemo(() => {
+ const containsCanonical = currentSeason.rows.some(
(row) => row.meme === canonicalNextMintNumber
);
+ const filtered = containsCanonical
+ ? currentSeason.rows.filter((row) => row.meme !== canonicalNextMintNumber)
+ : currentSeason.rows;
+
+ if (filtered.length === 0 && containsCanonical) {
+ const nextSeason = getUpcomingMintsForSeasonIndex(
+ currentSeason.seasonIndex + 1,
+ now
+ );
+ return {
+ seasonStart: nextSeason.seasonStart,
+ seasonEndInclusive: nextSeason.seasonEndInclusive,
+ seasonIndex: nextSeason.seasonIndex,
+ filteredRows: nextSeason.rows,
+ isNextSeason: true,
+ };
+ }
+
return {
- filteredRows: containsCanonical
- ? rows.filter((row) => row.meme !== canonicalNextMintNumber)
- : rows,
- hasCanonicalNext: containsCanonical,
- } as const;
- }, [rows, canonicalNextMintNumber]);
+ seasonStart: currentSeason.seasonStart,
+ seasonEndInclusive: currentSeason.seasonEndInclusive,
+ seasonIndex: currentSeason.seasonIndex,
+ filteredRows: filtered,
+ isNextSeason: false,
+ };
+ }, [currentSeason, canonicalNextMintNumber, now]);
- const emptyStateCopy = hasCanonicalNext
- ? "No additional mints scheduled in this season."
- : "No upcoming mints in this season.";
+ const emptyStateCopy = "No upcoming mints in this season.";
return (
- Upcoming Mints for SZN {displayedSeasonNumberFromIndex(seasonIndex)}
+ {isNextSeason ? "Upcoming SZN" : "Upcoming Mints for SZN"}{" "}
+ {displayedSeasonNumberFromIndex(seasonIndex)}
{formatFullDate(seasonStart, displayTz)} -{" "}
diff --git a/components/meme-calendar/meme-calendar.helpers.tsx b/components/meme-calendar/meme-calendar.helpers.tsx
index f78e74a7e3..1837f5f938 100644
--- a/components/meme-calendar/meme-calendar.helpers.tsx
+++ b/components/meme-calendar/meme-calendar.helpers.tsx
@@ -847,6 +847,7 @@ export interface SeasonMintRow {
utcDay: Date;
instantUtc: Date;
meme: number;
+ seasonIndex: number;
}
export interface SeasonMintScanResult {
@@ -886,6 +887,7 @@ function getUpcomingMintsBetween(
Math.max(todayUtcDay.getTime(), seasonStart.getTime())
);
+ const seasonIndex = getSeasonIndexForDate(seasonStart);
const out: SeasonMintRow[] = [];
const cursor = new Date(scanStart);
while (+cursor <= +seasonEndInclusive) {
@@ -895,13 +897,13 @@ function getUpcomingMintsBetween(
utcDay: new Date(cursor),
instantUtc: mintInstant,
meme: getMintNumberForMintDate(cursor),
+ seasonIndex,
});
}
cursor.setUTCDate(cursor.getUTCDate() + 1);
}
const rows = out.filter((x) => x.instantUtc.getTime() > now.getTime());
- const seasonIndex = getSeasonIndexForDate(seasonStart);
return { seasonStart, seasonEndInclusive, seasonIndex, rows };
}
@@ -934,6 +936,32 @@ export function getUpcomingMintsForCurrentOrNextSeason(
};
}
+export function getUpcomingMintsForSeasonIndex(
+ seasonIndex: number,
+ now: Date = new Date()
+): SeasonMintScanResult {
+ const start = getSeasonStartDate(seasonIndex);
+ const end = addMonths(start, 2);
+ return getUpcomingMintsBetween(start, end, now);
+}
+
+export function getUpcomingMintsAcrossSeasons(
+ minCount: number,
+ now: Date = new Date()
+): SeasonMintRow[] {
+ const result: SeasonMintRow[] = [];
+ let idx = getSeasonIndexForDate(now);
+
+ while (result.length < minCount) {
+ const season = getUpcomingMintsForSeasonIndex(idx, now);
+ result.push(...season.rows);
+ idx++;
+ if (idx > getSeasonIndexForDate(now) + 4) break;
+ }
+
+ return result.slice(0, minCount);
+}
+
export function getCanonicalNextMintNumber(now: Date = new Date()): number {
const upcomingInstant = getNextMintStart(now);
const upcomingUtcDay = new Date(
diff --git a/components/seasons-dropdown/SeasonsDropdown.module.scss b/components/seasons-dropdown/SeasonsDropdown.module.scss
deleted file mode 100644
index 316b1a8ae2..0000000000
--- a/components/seasons-dropdown/SeasonsDropdown.module.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-@use "../../styles/variables.scss";
-
-.seasonDropdown {
- text-align: left;
- background-color: transparent;
- border: none;
- padding-right: 25px;
- a {
- line-height: 30px;
- }
- button {
- font-size: larger !important;
- font-weight: bolder !important;
- cursor: copy;
- padding: 0;
- text-align: left;
- background-color: transparent !important;
- border-color: transparent !important;
- position: relative;
- &::after {
- position: absolute;
- margin-top: 0;
- top: 50%;
- margin-left: 0.5rem;
- transform: translateY(-50%);
- font-size: 1.2em; /* Make the arrow bigger */
- }
- }
- div {
- width: 60%;
- }
-}
-
-@media only screen and (max-width: 800px) {
- .seasonDropdown {
- width: auto; /* Changed from 100% to auto to prevent full width */
- display: inline-block; /* Keeps it to the size of its content */
-
- button {
- text-align: left;
- width: auto; /* Changed from 100% to auto */
- position: relative;
- padding-right: 1.75rem; /* Make space for the arrow */
-
- &::after {
- position: absolute;
- right: 0; /* Position at the right edge of button */
- top: 50%;
- transform: translateY(-50%);
- font-size: 1.2em; /* Make the arrow bigger */
- }
- }
-
- div {
- width: auto;
- min-width: 120px; /* Ensure dropdown menu has reasonable width */
- }
- }
-}
diff --git a/components/seasons-dropdown/SeasonsDropdown.tsx b/components/seasons-dropdown/SeasonsDropdown.tsx
deleted file mode 100644
index 231c88a39e..0000000000
--- a/components/seasons-dropdown/SeasonsDropdown.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import styles from "./SeasonsDropdown.module.scss";
-import { Dropdown } from "react-bootstrap";
-
-interface Props {
- seasons: number[];
- selectedSeason: number;
- setSelectedSeason(season: number): void;
-}
-
-export default function SeasonsDropdown(props: Readonly
) {
- return (
-
-
- SZN: {props.selectedSeason === 0 ? `All` : props.selectedSeason}
-
-
- {
- props.setSelectedSeason(0);
- }}>
- All
-
- {props.seasons.map((s) => (
- {
- props.setSelectedSeason(s);
- }}>
- SZN{s}
-
- ))}
-
-
- );
-}
diff --git a/components/subscriptions-report/SubscriptionsReport.tsx b/components/subscriptions-report/SubscriptionsReport.tsx
index ccd00d812c..1da57aaf4e 100644
--- a/components/subscriptions-report/SubscriptionsReport.tsx
+++ b/components/subscriptions-report/SubscriptionsReport.tsx
@@ -9,12 +9,9 @@ import {
displayedSeasonNumberFromIndex,
formatFullDate,
getCardsRemainingUntilEndOf,
- getSeasonIndexForDate,
- getUpcomingMintsForCurrentOrNextSeason,
+ getUpcomingMintsAcrossSeasons,
isMintingToday,
- nextMintDateOnOrAfter,
SeasonMintRow,
- SeasonMintScanResult,
} from "@/components/meme-calendar/meme-calendar.helpers";
import Pagination, { Paginated } from "@/components/pagination/Pagination";
import ShowMoreButton from "@/components/show-more-button/ShowMoreButton";
@@ -69,14 +66,10 @@ export default function SubscriptionsReportComponent() {
prevUpcomingExpandedRef.current = upcomingExpanded;
}, [upcomingExpanded, upcomingCounts.length]);
- const nextMintDate = nextMintDateOnOrAfter();
- const idx = getSeasonIndexForDate(nextMintDate);
- const szn = displayedSeasonNumberFromIndex(idx);
-
const [now] = useState(new Date());
- const { rows } = useMemo(
- () => getUpcomingMintsForCurrentOrNextSeason(now),
- [now]
+ const rows = useMemo(
+ () => getUpcomingMintsAcrossSeasons(upcomingCounts.length || 50, now),
+ [now, upcomingCounts.length]
);
async function fetchUpcomingCounts(count: number) {
@@ -149,12 +142,10 @@ export default function SubscriptionsReportComponent() {
}
return (
-
+
-
- Subscriptions Report
-
+ Subscriptions Report
{connectedProfile && (!capacitor.isIos || country === "US") && (
- Upcoming Drops for SZN{szn}
+ Upcoming Drops
{upcomingLoading &&
}
@@ -284,7 +275,7 @@ export default function SubscriptionsReportComponent() {
{totalRedeemed > PAGE_SIZE && redeemedPage !== null && (
The Memes #{props.count.token_id}
+ SZN {displayedSeasonNumberFromIndex(props.date.seasonIndex)}
+ {" / "}
{formatFullDate(props.date.utcDay)}
@@ -355,8 +348,9 @@ function RedeemedSubscriptionDetails(
#{props.count.token_id} - {props.count.name}
- {dateTime.toIsoDateString()} / {dateTime.toDayName()} / SZN
- {props.count.szn}
+ SZN {props.count.szn}
+ {" / "}
+ {formatFullDate(dateTime.toDate())}
diff --git a/components/the-memes/TheMemes.tsx b/components/the-memes/TheMemes.tsx
index 7d6bd0af64..0de1cf3b6e 100644
--- a/components/the-memes/TheMemes.tsx
+++ b/components/the-memes/TheMemes.tsx
@@ -1,5 +1,13 @@
"use client";
+import { AuthContext } from "@/components/auth/Auth";
+import CollectionsDropdown from "@/components/collections-dropdown/CollectionsDropdown";
+import DotLoader from "@/components/dotLoader/DotLoader";
+import { LFGButton } from "@/components/lfg-slideshow/LFGSlideshow";
+import NFTImage from "@/components/nft-image/NFTImage";
+import { VolumeTypeDropdown } from "@/components/the-memes/MemeShared";
+import styles from "@/components/the-memes/TheMemes.module.scss";
+import SeasonsGridDropdown from "@/components/utils/select/dropdown/SeasonsGridDropdown";
import { publicEnv } from "@/config/env";
import { MEMES_CONTRACT } from "@/constants";
import { useSetTitle } from "@/contexts/TitleContext";
@@ -10,7 +18,6 @@ import { SortDirection } from "@/entities/ISort";
import { MemeLabSort, MEMES_EXTENDED_SORT, MemesSort } from "@/enums";
import { numberWithCommas, printMintDate } from "@/helpers/Helpers";
import { fetchUrl } from "@/services/6529api";
-import { commonApiFetch } from "@/services/api/common-api";
import {
faChevronCircleDown,
faChevronCircleUp,
@@ -20,14 +27,6 @@ import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useContext, useEffect, useState } from "react";
import { Col, Container, Row } from "react-bootstrap";
-import { AuthContext } from "../auth/Auth";
-import CollectionsDropdown from "../collections-dropdown/CollectionsDropdown";
-import DotLoader from "../dotLoader/DotLoader";
-import { LFGButton } from "../lfg-slideshow/LFGSlideshow";
-import NFTImage from "../nft-image/NFTImage";
-import SeasonsDropdown from "../seasons-dropdown/SeasonsDropdown";
-import { VolumeTypeDropdown } from "./MemeShared";
-import styles from "./TheMemes.module.scss";
interface Meme {
meme: number;
@@ -102,8 +101,8 @@ export default function TheMemesComponent() {
const { connectedProfile } = useContext(AuthContext);
- const [selectedSeason, setSelectedSeason] = useState(0);
- const [seasons, setSeasons] = useState
([]);
+ const [selectedSeason, setSelectedSeason] = useState(null);
+ const [initialSeasonId, setInitialSeasonId] = useState(null);
const [routerLoaded, setRouterLoaded] = useState(false);
@@ -113,7 +112,7 @@ export default function TheMemesComponent() {
let initialSortDir = SortDirection.ASC;
let initialSort = MemesSort.AGE;
let initialVolume = VolumeType.ALL_TIME;
- let initialSzn = 0;
+ let initialSznId: number | null = null;
const routerSortDir = searchParams?.get("sort_dir");
if (routerSortDir) {
@@ -150,16 +149,15 @@ export default function TheMemesComponent() {
const routerSzn = searchParams?.get("szn");
if (routerSzn) {
- if (Array.isArray(routerSzn)) {
- initialSzn = parseInt(routerSzn[0]);
- } else {
- initialSzn = parseInt(routerSzn);
+ const parsed = Number.parseInt(routerSzn);
+ if (!Number.isNaN(parsed) && parsed > 0) {
+ initialSznId = parsed;
}
}
setSort(initialSort);
setSortDir(initialSortDir);
- setSelectedSeason(initialSzn);
+ setInitialSeasonId(initialSznId);
setVolumeType(initialVolume);
setRouterLoaded(true);
}, [searchParams]);
@@ -167,8 +165,8 @@ export default function TheMemesComponent() {
const getNftsNextPage = () => {
const mySort = getApiSort(sort, volumeType);
let seasonFilter = "";
- if (selectedSeason > 0) {
- seasonFilter = `&season=${selectedSeason}`;
+ if (selectedSeason) {
+ seasonFilter = `&season=${selectedSeason.id}`;
}
return `${publicEnv.API_ENDPOINT}/api/memes_extended_data?page_size=48&sort_direction=${sortDir}&sort=${mySort}${seasonFilter}`;
};
@@ -187,14 +185,6 @@ export default function TheMemesComponent() {
Map
>(new Map());
- useEffect(() => {
- commonApiFetch({
- endpoint: "new_memes_seasons",
- }).then((response) => {
- setSeasons(response);
- });
- }, []);
-
useEffect(() => {
let sortParam: string;
@@ -216,11 +206,11 @@ export default function TheMemesComponent() {
}
let queryString = `sort=${sortParam}&sort_dir=${sortDir.toLowerCase()}`;
- if (selectedSeason > 0) {
- queryString += `&szn=${selectedSeason}`;
+ if (selectedSeason) {
+ queryString += `&szn=${selectedSeason.id}`;
}
router.push(`the-memes?${queryString}`);
- }, [sort, sortDir, selectedSeason, volumeType]);
+ }, [sort, sortDir, selectedSeason, volumeType, router]);
useEffect(() => {
const memesMap = new Map<
@@ -461,16 +451,14 @@ export default function TheMemesComponent() {
-
- The Memes
-
+ The Memes
-
-
s.id)}
- selectedSeason={selectedSeason}
- setSelectedSeason={setSelectedSeason}
+
+
@@ -486,11 +474,11 @@ export default function TheMemesComponent() {
-
-
s.id)}
- selectedSeason={selectedSeason}
- setSelectedSeason={setSelectedSeason}
+
+
@@ -591,4 +579,4 @@ export function printVolumeTypeDropdown(
setVolumeSort={setVolumeSort}
/>
);
-}
\ No newline at end of file
+}
diff --git a/components/user/collected/UserPageCollected.tsx b/components/user/collected/UserPageCollected.tsx
index be52b1e6da..fb29303dba 100644
--- a/components/user/collected/UserPageCollected.tsx
+++ b/components/user/collected/UserPageCollected.tsx
@@ -11,8 +11,8 @@ import {
CollectionSeized,
CollectionSort,
} from "@/entities/IProfile";
+import { MemeSeason } from "@/entities/ISeason";
import { SortDirection } from "@/entities/ISort";
-import { MEMES_SEASON } from "@/enums";
import { ApiIdentity } from "@/generated/models/ObjectSerializer";
import { areEqualAddresses } from "@/helpers/Helpers";
import { Page } from "@/helpers/Types";
@@ -42,7 +42,8 @@ export interface ProfileCollectedFilters {
readonly collection: CollectedCollectionType | null;
readonly subcollection: string | null;
readonly seized: CollectionSeized | null;
- readonly szn: MEMES_SEASON | null;
+ readonly szn: MemeSeason | null;
+ readonly initialSznId: number | null;
readonly page: number;
readonly pageSize: number;
readonly sortBy: CollectionSort;
@@ -65,22 +66,6 @@ const SEARCH_PARAMS_FIELDS = {
sortDirection: "sort-direction",
} as const;
-const SZN_TO_SEARCH_PARAMS: Record = {
- [MEMES_SEASON.SZN1]: "1",
- [MEMES_SEASON.SZN2]: "2",
- [MEMES_SEASON.SZN3]: "3",
- [MEMES_SEASON.SZN4]: "4",
- [MEMES_SEASON.SZN5]: "5",
- [MEMES_SEASON.SZN6]: "6",
- [MEMES_SEASON.SZN7]: "7",
- [MEMES_SEASON.SZN8]: "8",
- [MEMES_SEASON.SZN9]: "9",
- [MEMES_SEASON.SZN10]: "10",
- [MEMES_SEASON.SZN11]: "11",
- [MEMES_SEASON.SZN12]: "12",
- [MEMES_SEASON.SZN13]: "13",
-};
-
export default function UserPageCollected({
profile,
}: {
@@ -116,20 +101,18 @@ export default function UserPageCollected({
);
};
- const convertSzn = ({
+ const convertSznId = ({
szn,
collection,
}: {
readonly szn: string | null;
readonly collection: CollectedCollectionType | null;
- }): MEMES_SEASON | null => {
+ }): number | null => {
if (!collection) return null;
if (!COLLECTED_COLLECTIONS_META[collection].filters.szn) return null;
if (!szn) return null;
- const entry = Object.entries(SZN_TO_SEARCH_PARAMS).find(
- ([k, v]) => v === szn
- );
- return entry ? (entry[0] as MEMES_SEASON) : null;
+ const parsed = Number.parseInt(szn, 10);
+ return Number.isNaN(parsed) ? null : parsed;
};
const convertCollection = (
@@ -196,8 +179,12 @@ export default function UserPageCollected({
seized: seized ?? null,
collection: convertedCollection,
}),
- szn: convertSzn({ szn: szn ?? null, collection: convertedCollection }),
- page: page ? parseInt(page) : 1,
+ szn: null,
+ initialSznId: convertSznId({
+ szn: szn ?? null,
+ collection: convertedCollection,
+ }),
+ page: page ? Number.parseInt(page, 10) : 1,
pageSize: PAGE_SIZE,
sortBy: convertSortedBy({
sortBy: sortBy ?? null,
@@ -362,11 +349,12 @@ export default function UserPageCollected({
await updateFields(items);
};
- const setSzn = async (szn: MEMES_SEASON | null): Promise => {
+ const setSzn = async (szn: MemeSeason | null): Promise => {
+ setFilters((prev) => ({ ...prev, szn }));
const items: QueryUpdateInput[] = [
{
name: "szn",
- value: szn ? SZN_TO_SEARCH_PARAMS[szn] : null,
+ value: szn ? szn.id.toString() : null,
},
{
name: "page",
@@ -434,7 +422,7 @@ export default function UserPageCollected({
}
if (filters.szn) {
- params.szn = SZN_TO_SEARCH_PARAMS[filters.szn];
+ params.szn = filters.szn.id.toString();
}
return await commonApiFetch>({
diff --git a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx
index 39a55c969e..cbeda1fee2 100644
--- a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx
+++ b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx
@@ -1,12 +1,8 @@
"use client";
-import { useEffect, useState } from "react";
-import {
- CollectedCollectionType,
- CollectionSeized,
-} from "@/entities/IProfile";
-import { MEMES_SEASON } from "@/enums";
+import { CollectedCollectionType, CollectionSeized } from "@/entities/IProfile";
import { assertUnreachable } from "@/helpers/AllowlistToolHelpers";
+import { useEffect, useState } from "react";
import { ProfileCollectedFilters } from "../UserPageCollected";
export default function UserPageCollectedCardsNoCards({
@@ -22,27 +18,10 @@ export default function UserPageCollectedCardsNoCards({
case null:
return "Congratulations, full setter!";
case CollectedCollectionType.MEMES:
- switch (filters.szn) {
- case null:
- return "Congratulations, The Memes full setter!";
- case MEMES_SEASON.SZN1:
- case MEMES_SEASON.SZN2:
- case MEMES_SEASON.SZN3:
- case MEMES_SEASON.SZN4:
- case MEMES_SEASON.SZN5:
- case MEMES_SEASON.SZN6:
- case MEMES_SEASON.SZN7:
- case MEMES_SEASON.SZN8:
- case MEMES_SEASON.SZN9:
- case MEMES_SEASON.SZN10:
- case MEMES_SEASON.SZN11:
- case MEMES_SEASON.SZN12:
- case MEMES_SEASON.SZN13:
- return `Congratulations, ${filters.szn} full setter!`;
- default:
- assertUnreachable(filters.szn);
- return "";
+ if (filters.szn === null) {
+ return "Congratulations, The Memes full setter!";
}
+ return `Congratulations, ${filters.szn.display} full setter!`;
case CollectedCollectionType.GRADIENTS:
return "Congratulations, Gradient full setter!";
case CollectedCollectionType.MEMELAB:
diff --git a/components/user/collected/filters/UserPageCollectedFilters.tsx b/components/user/collected/filters/UserPageCollectedFilters.tsx
index d1c74ef494..48bc30d16d 100644
--- a/components/user/collected/filters/UserPageCollectedFilters.tsx
+++ b/components/user/collected/filters/UserPageCollectedFilters.tsx
@@ -10,7 +10,7 @@ import {
CollectionSeized,
CollectionSort,
} from "@/entities/IProfile";
-import { MEMES_SEASON } from "@/enums";
+import { MemeSeason } from "@/entities/ISeason";
import { ApiIdentity } from "@/generated/models/ApiIdentity";
import {
faChevronLeft,
@@ -48,7 +48,7 @@ export default function UserPageCollectedFilters({
readonly setCollection: (collection: CollectedCollectionType | null) => void;
readonly setSortBy: (sortBy: CollectionSort) => void;
readonly setSeized: (seized: CollectionSeized | null) => void;
- readonly setSzn: (szn: MEMES_SEASON | null) => void;
+ readonly setSzn: (szn: MemeSeason | null) => void;
readonly setSubcollection: (subcollection: string | null) => void;
readonly showTransfer: boolean;
}) {
@@ -151,12 +151,10 @@ export default function UserPageCollectedFilters({
+ className="tw-w-full tw-overflow-x-auto [&::-webkit-scrollbar]:tw-hidden [-ms-overflow-style:none] [scrollbar-width:none]">
+ className="tw-flex tw-nowrap tw-justify-between tw-gap-x-3 lg:tw-gap-x-4 tw-items-center tw-w-full tw-min-w-max">
{showTransfer &&
}
@@ -198,7 +196,7 @@ export default function UserPageCollectedFilters({
{getShowSzn(filters.collection) && (
)}
@@ -220,8 +218,7 @@ export default function UserPageCollectedFilters({