import "@testing-library/jest-dom"; import React, { createRef, forwardRef } from "react"; import { NextImage } from "./index"; import { render, screen } from "@testing-library/react"; // Mock next/image - must use require-style reference due to hoisting const mockNextImage = jest.fn(); jest.mock("next/image", () => { const MockNextImage = forwardRef(function MockNextImage( props: any, ref: any ) { mockNextImage(props); const rest = { ...props }; const loader = rest.loader; delete rest.loader; delete rest.unoptimized; delete rest.priority; delete rest.fill; delete rest.sizes; return ( ); }); return { __esModule: true, default: MockNextImage }; }); jest.mock("@shared/utils", () => ({ cx: (...args: any[]) => args.filter(Boolean).join(" "), })); describe("NextImage", () => { beforeEach(() => { mockNextImage.mockClear(); }); it("has displayName set to NextImage", () => { expect(NextImage.displayName).toBe("NextImage"); }); describe("basic rendering", () => { it("renders an image element", () => { render(); expect(screen.getByTestId("next-image")).toBeInTheDocument(); }); it("passes src, alt, width, height props", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("src", "/photo.jpg"); expect(img).toHaveAttribute("alt", "Photo"); expect(img).toHaveAttribute("width", "200"); expect(img).toHaveAttribute("height", "150"); }); it("passes className to image", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveClass("rounded-lg"); }); it("passes additional props through", () => { render( ); expect(mockNextImage).toHaveBeenCalledWith( expect.objectContaining({ priority: true }) ); }); }); describe("ref forwarding", () => { it("forwards ref to the underlying image", () => { const ref = createRef(); render( ); expect(ref.current).toBeInstanceOf(HTMLImageElement); }); }); describe("Contentful image detection", () => { it("detects Contentful images by domain", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-has-loader", "true"); expect(img).toHaveAttribute("data-unoptimized", "false"); }); it("does not use contentful loader for non-Contentful images", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-has-loader", "false"); }); it("does not use contentful loader for relative paths", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-has-loader", "false"); }); it("handles non-string src gracefully", () => { const staticImport = { src: "/static.png", height: 100, width: 100 }; render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-has-loader", "false"); }); }); describe("custom loader behavior", () => { it("passes a loader function for Contentful images", () => { render( ); expect(mockNextImage).toHaveBeenCalledWith( expect.objectContaining({ loader: expect.any(Function), unoptimized: false, }) ); }); it("loader appends width, quality, and format params", () => { render( ); const callProps = mockNextImage.mock.calls[0][0]; const loader = callProps.loader; const result = loader({ src: "https://images.ctfassets.net/space/photo.jpg", width: 800, quality: 75, }); expect(result).toContain("w=800"); expect(result).toContain("q=75"); expect(result).toContain("fm=webp"); }); it("loader uses default quality of 90 when not specified", () => { render( ); const callProps = mockNextImage.mock.calls[0][0]; const loader = callProps.loader; const result = loader({ src: "https://images.ctfassets.net/space/photo.jpg", width: 400, quality: undefined, }); expect(result).toContain("q=90"); }); it("loader preserves existing URL params", () => { render( ); const callProps = mockNextImage.mock.calls[0][0]; const loader = callProps.loader; const result = loader({ src: "https://images.ctfassets.net/space/photo.jpg?fit=fill", width: 600, quality: 80, }); expect(result).toContain("fit=fill"); expect(result).toContain("w=600"); expect(result).toContain("q=80"); expect(result).toContain("fm=webp"); }); }); describe("SVG optimization handling", () => { it("marks SVG from Contentful as unoptimized", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-unoptimized", "true"); expect(img).toHaveAttribute("data-has-loader", "false"); }); it("does not use contentful loader for SVG images", () => { render( ); expect(mockNextImage).toHaveBeenCalledWith( expect.objectContaining({ unoptimized: true, }) ); expect(mockNextImage).not.toHaveBeenCalledWith( expect.objectContaining({ loader: expect.any(Function), }) ); }); it("handles SVG with query params from Contentful", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-unoptimized", "true"); expect(img).toHaveAttribute("data-has-loader", "false"); }); it("handles SVG with uppercase extension from Contentful", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-unoptimized", "true"); }); it("does not treat non-Contentful SVG specially", () => { render( ); const img = screen.getByTestId("next-image"); // Non-contentful SVG: no loader, unoptimized is false (falsy) expect(img).toHaveAttribute("data-has-loader", "false"); expect(img).toHaveAttribute("data-unoptimized", "false"); }); }); describe("non-Contentful images", () => { it("does not set unoptimized for regular external images", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-has-loader", "false"); }); it("does not set unoptimized for local images", () => { render( ); const img = screen.getByTestId("next-image"); expect(img).toHaveAttribute("data-unoptimized"); }); }); }); describe("resolveDefaultExport (CJS/ESM interop)", () => { afterEach(() => { jest.resetModules(); }); it("unwraps doubly-wrapped default export", async () => { jest.doMock("next/image", () => { function MockImage(props: any) { return ; } return { __esModule: true, default: { default: MockImage } }; }); jest.doMock("@shared/utils", () => ({ cx: (...args: any[]) => args.filter(Boolean).join(" "), })); const { NextImage } = await import("./index"); expect(NextImage).toBeDefined(); }); it("resolves when default export is already a function", async () => { jest.doMock("next/image", () => { function MockImage(props: any) { return ; } return { __esModule: true, default: MockImage }; }); jest.doMock("@shared/utils", () => ({ cx: (...args: any[]) => args.filter(Boolean).join(" "), })); const { NextImage } = await import("./index"); expect(NextImage).toBeDefined(); }); it("returns current when loop exhausts iterations", async () => { jest.doMock("next/image", () => { const nested = { default: { default: { default: { default: { default: { default: "not-a-function" }, }, }, }, }, }; return { __esModule: true, default: nested }; }); jest.doMock("@shared/utils", () => ({ cx: (...args: any[]) => args.filter(Boolean).join(" "), })); const { NextImage } = await import("./index"); expect(NextImage).toBeDefined(); }); });