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();
});
});