Skip to content
40 changes: 34 additions & 6 deletions __tests__/components/nft-image/renderers/NFTImageRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,28 @@ jest.mock("@/components/nft-image/NFTImageBalance", () => {
const createMockNFT = (overrides: Partial<BaseNFT> = {}): BaseNFT => ({
id: 1,
contract: "0x123",
token_id: "1",
name: "Test NFT",
image: "https://example.com/image.png",
collection: "Test Collection",
token_type: "ERC721",
description: "Test description",
artist: "Test Artist",
artist_seize_handle: "testartist",
uri: "https://example.com/token/1",
icon: "https://example.com/icon.png",
thumbnail: "https://example.com/thumb.png",
scaled: "https://example.com/scaled.png",
image: "https://example.com/image.png",
animation: "https://example.com/animation.mp4",
market_cap: 0,
floor_price: 0.05,
total_volume_last_24_hours: 50,
total_volume_last_7_days: 200,
total_volume_last_1_month: 500,
total_volume: 1000,
highest_offer: 0.08,
mint_price: 0.06529,
supply: 10,
name: "Test NFT",
created_at: new Date(),
metadata: {
image: "https://example.com/metadata-image.png",
name: "Test NFT",
Expand All @@ -107,8 +124,6 @@ const createDefaultProps = (
): BaseRendererProps => ({
nft: createMockNFT(),
height: 300,
showOwnedIfLoggedIn: false,
showUnseizedIfLoggedIn: false,
heightStyle: "height-300",
imageStyle: "image-style",
bgStyle: "bg-style",
Expand Down Expand Up @@ -389,10 +404,23 @@ describe("NFTImageRenderer", () => {
});

describe("Performance Attributes", () => {
it("sets correct loading and priority attributes", () => {
it("uses lazy loading for grid-sized images", () => {
const props = createDefaultProps();
render(<NFTImageRenderer {...props} />);

const image = screen.getByRole("img");
expect(image).toHaveAttribute("data-loading", "lazy");
expect(image).toHaveAttribute("data-priority", "false");
expect(image).toHaveAttribute("data-fetch-priority", "auto");
});

it("prioritizes larger feature images", () => {
const props = createDefaultProps({
height: 650,
heightStyle: "height-650",
});
render(<NFTImageRenderer {...props} />);

const image = screen.getByRole("img");
expect(image).toHaveAttribute("data-loading", "eager");
expect(image).toHaveAttribute("data-priority", "true");
Expand Down
89 changes: 60 additions & 29 deletions __tests__/components/the-memes/MemePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React from "react";
jest.mock("next/navigation", () => ({
useRouter: jest.fn(),
useSearchParams: jest.fn(),
usePathname: jest.fn(),
}));

jest.mock("@/services/6529api", () => ({
Expand Down Expand Up @@ -62,31 +63,57 @@ jest.mock("@/components/the-memes/MemePageTimeline", () => ({
show ? <div data-testid="timeline">Timeline</div> : null,
}));

jest.mock("@/components/the-memes/MemePageMintCountdown", () => () => (
<div data-testid="mint-countdown" />
));
jest.mock("@/components/the-memes/MemePageMintCountdown", () => {
const MockMemePageMintCountdown = () => <div data-testid="mint-countdown" />;
MockMemePageMintCountdown.displayName = "MockMemePageMintCountdown";
return MockMemePageMintCountdown;
});

jest.mock("@/components/nft-image/NFTImage", () => () => (
<div data-testid="nft-image" />
));
jest.mock("@/components/nft-image/NFTImage", () => {
const MockNFTImage = () => <div data-testid="nft-image" />;
MockNFTImage.displayName = "MockNFTImage";
return MockNFTImage;
});

jest.mock("@/components/nft-navigation/NftNavigation", () => () => (
<div data-testid="nft-navigation" />
));
jest.mock("@/components/nft-navigation/NftNavigation", () => {
const MockNftNavigation = () => <div data-testid="nft-navigation" />;
MockNftNavigation.displayName = "MockNftNavigation";
return MockNftNavigation;
});

const mockPush = jest.fn();
const mockReplace = jest.fn((url: string, _options?: { scroll?: boolean }) => {
const parsedUrl = new URL(url, "https://example.com");
currentFocus = parsedUrl.searchParams.get("focus");
});
let currentFocus: string | null = null;
const mockSearchParams = {
get: jest.fn().mockReturnValue(null),
get: jest.fn((key: string) => (key === "focus" ? currentFocus : null)),
toString: jest.fn(() => {
const params = new URLSearchParams();
if (currentFocus) {
params.set("focus", currentFocus);
}
return params.toString();
}),
};

const useSearchParamsMock = require("next/navigation").useSearchParams;
useSearchParamsMock.mockReturnValue(mockSearchParams);
const usePathnameMock = require("next/navigation").usePathname;
usePathnameMock.mockReturnValue("/the-memes/1");

(useRouter as jest.Mock).mockReturnValue({
query: { id: "1" },
isReady: true,
push: mockPush,
replace: jest.fn(),
push: jest.fn(),
replace: mockReplace,
});

beforeEach(() => {
currentFocus = null;
mockReplace.mockClear();
mockSearchParams.get.mockClear();
mockSearchParams.toString.mockClear();
});

const nftMeta = {
Expand Down Expand Up @@ -189,7 +216,7 @@ function renderPage() {
};

return render(
<AuthContext.Provider value={mockAuthContext}>
<AuthContext.Provider value={mockAuthContext as any}>
<MemePage nftId="1" />
</AuthContext.Provider>
);
Expand All @@ -214,8 +241,8 @@ jest.mock("@/contexts/TitleContext", () => ({

describe("MemePage tab navigation", () => {
beforeEach(() => {
mockPush.mockClear();
mockSearchParams.get.mockReturnValue(null);
currentFocus = null;
mockReplace.mockClear();
});

it.each([
Expand All @@ -233,7 +260,7 @@ describe("MemePage tab navigation", () => {
expect(screen.getByTestId("mint-countdown")).toBeInTheDocument()
);

mockPush.mockClear();
mockReplace.mockClear();
const btn = screen.getByRole("button", { name: label });
await userEvent.click(btn);

Expand All @@ -242,8 +269,9 @@ describe("MemePage tab navigation", () => {
});

if (label !== "Live") {
expect(mockPush).toHaveBeenLastCalledWith(
`/the-memes/1?focus=${focus}`
expect(mockReplace).toHaveBeenLastCalledWith(
`/the-memes/1?focus=${focus}`,
{ scroll: false }
);
}
}
Expand All @@ -252,28 +280,29 @@ describe("MemePage tab navigation", () => {

describe("MemePage search params handling", () => {
beforeEach(() => {
mockPush.mockClear();
currentFocus = null;
mockReplace.mockClear();
mockSearchParams.get.mockClear();
});

it("defaults to LIVE focus when no focus param", async () => {
mockSearchParams.get.mockReturnValue(null);
currentFocus = null;
renderPage();
await waitFor(() => {
expect(screen.getByTestId("live-right")).toBeInTheDocument();
});
});

it("sets focus from valid search param", async () => {
mockSearchParams.get.mockReturnValue(MEME_FOCUS.ACTIVITY);
currentFocus = MEME_FOCUS.ACTIVITY;
renderPage();
await waitFor(() => {
expect(screen.getByTestId("activity")).toBeInTheDocument();
});
});

it("ignores invalid focus param and defaults to LIVE", async () => {
mockSearchParams.get.mockReturnValue("invalid-focus");
currentFocus = "invalid-focus";
renderPage();
await waitFor(() => {
expect(screen.getByTestId("live-right")).toBeInTheDocument();
Expand All @@ -290,8 +319,9 @@ describe("MemePage search params handling", () => {
const artButton = screen.getByRole("button", { name: "The Art" });
await userEvent.click(artButton);

expect(mockPush).toHaveBeenCalledWith(
`/the-memes/1?focus=${MEME_FOCUS.THE_ART}`
expect(mockReplace).toHaveBeenCalledWith(
`/the-memes/1?focus=${MEME_FOCUS.THE_ART}`,
{ scroll: false }
);
});
});
Expand Down Expand Up @@ -329,12 +359,13 @@ describe("MemePage API interactions", () => {

renderPage();

// Should not make the second NFT call when metadata is empty
await waitFor(() => {
const calls = (fetchUrl as jest.Mock).mock.calls;
const nftCalls = calls.filter((call) => call[0].includes("/api/nfts?"));
expect(nftCalls).toHaveLength(0);
expect(nftCalls).toHaveLength(1);
});

expect(screen.queryByTestId("nft-navigation")).not.toBeInTheDocument();
});
});

Expand Down Expand Up @@ -391,7 +422,7 @@ describe("MemePage wallet integration", () => {
};

render(
<AuthContext.Provider value={mockAuthContext}>
<AuthContext.Provider value={mockAuthContext as any}>
<MemePage nftId="1" />
</AuthContext.Provider>
);
Expand Down Expand Up @@ -441,7 +472,7 @@ describe("MemePage wallet integration", () => {
};

render(
<AuthContext.Provider value={mockAuthContext}>
<AuthContext.Provider value={mockAuthContext as any}>
<MemePage nftId="1" />
</AuthContext.Provider>
);
Expand Down
14 changes: 11 additions & 3 deletions __tests__/components/the-memes/MemePageActivity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ jest.mock("@/services/6529api", () => ({
fetchUrl: (...args: any[]) => fetchUrlMock(...args),
}));

jest.mock("@/components/latest-activity/LatestActivityRow", () => () => (
<tr data-testid="activity-row" />
));
jest.mock("@/components/latest-activity/LatestActivityRow", () => {
const MockLatestActivityRow = () => <tr data-testid="activity-row" />;
MockLatestActivityRow.displayName = "MockLatestActivityRow";
return MockLatestActivityRow;
});

// Utility NFT object with required fields only
const nft = {
Expand Down Expand Up @@ -101,6 +103,12 @@ describe("MemePageActivity", () => {
});

describe("Activity Fetching", () => {
it("skips fetching when tab is hidden", () => {
render(<MemePageActivity show={false} nft={nft} pageSize={10} />);

expect(fetchUrlMock).not.toHaveBeenCalled();
});

it("fetches activity with correct base url", async () => {
render(<MemePageActivity show nft={nft} pageSize={10} />);

Expand Down
7 changes: 4 additions & 3 deletions components/nft-image/renderers/NFTImageRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ function getSrc(

export default function NFTImageRenderer(props: Readonly<BaseRendererProps>) {
const src = getSrc(props.nft, !!props.showThumbnail, !!props.showOriginal);
const shouldLazyLoad = !!props.showThumbnail || props.height === 300;

return (
<Col
xs={12}
className={`mb-2 text-center d-flex align-items-center justify-content-center ${styles.imageWrapper} ${props.heightStyle} ${props.bgStyle}`}>
<Image
loading="eager"
priority
loading={shouldLazyLoad ? "lazy" : "eager"}
priority={!shouldLazyLoad}
width="0"
height="0"
fetchPriority="high"
fetchPriority={shouldLazyLoad ? "auto" : "high"}
unoptimized
className={props.imageStyle}
style={{
Expand Down
Loading