import { readFileSync } from "node:fs"; import { join } from "node:path"; import { notFound } from "next/navigation"; import { ImageResponse } from "next/og"; import { isValidElement } from "react"; import { OG_IMAGE_MAX_BYTES, OG_SIZE, OgCard, readProjectOgImage, } from "@/lib/og-card"; import { caseStudies } from "@/lib/site-content"; import Image, { generateImageMetadata } from "./opengraph-image"; /** * Testes de caminhos-base de Image (imagem de compartilhamento do case): * slug desconhecido, id da imagem diferente do case, case com capa gravada (entregue como está, em JPEG, * abaixo de 150 KB), capa pesada demais (cartão simples) e projeto de laboratório. */ /** O jsdom não tem `Response`; a rota roda no Node e devolve uma. Aqui basta guardar o corpo e os cabeçalhos. */ class FakeResponse { constructor( readonly body: Uint8Array, readonly init: { headers: Record }, ) {} } beforeAll(() => { Object.assign(globalThis, { Response: FakeResponse }); }); jest.mock("@/lib/og-card", () => { const actual = jest.requireActual("@/lib/og-card"); return { ...actual, readProjectOgImage: jest.fn(actual.readProjectOgImage) }; }); const mockedReadProjectOgImage = jest.mocked(readProjectOgImage); const actualReadProjectOgImage = jest.requireActual( "@/lib/og-card", ).readProjectOgImage; afterEach(() => { mockedReadProjectOgImage.mockImplementation(actualReadProjectOgImage); }); jest.mock("next/navigation", () => ({ notFound: jest.fn(() => { throw new Error("NEXT_NOT_FOUND"); }), })); jest.mock("next/og", () => ({ ImageResponse: jest.fn() })); const mockedImageResponse = jest.mocked(ImageResponse); beforeEach(() => { jest.mocked(notFound).mockClear(); mockedImageResponse.mockClear(); }); function getCase(id: string) { const caseStudy = caseStudies.find((item) => item.id === id); if (!caseStudy) throw new Error(`Missing case ${id}`); return caseStudy; } function renderImage(slug: string, imageId: string) { return Image({ params: Promise.resolve({ slug }), id: Promise.resolve(imageId), }); } function sentCard() { const [element, options] = mockedImageResponse.mock.calls[0]; if (!isValidElement>(element)) { throw new Error("ImageResponse did not receive an element"); } return { card: element, options }; } describe("opengraph-image Image", () => { it("path: case with a recorded cover -> the cover itself, in JPEG, under 150 KB, and no generated card", async () => { const caseStudy = getCase("sev-modaintima"); const response = (await renderImage( caseStudy.id, caseStudy.id, )) as unknown as FakeResponse; expect(notFound).not.toHaveBeenCalled(); expect(mockedImageResponse).not.toHaveBeenCalled(); expect(response).toBeInstanceOf(FakeResponse); expect(response.init.headers["Content-Type"]).toBe("image/jpeg"); const cover = readFileSync( join(process.cwd(), "public/videos/sev-modaintima/og.jpg"), ); expect(Buffer.from(response.body).equals(cover)).toBe(true); expect(response.body.length).toBeLessThan(150_000); }); it("path: cover that weighs 150 KB or more -> the plain generated card, with no background", async () => { const caseStudy = getCase("sev-modaintima"); mockedReadProjectOgImage.mockReturnValue(undefined); expect(OG_IMAGE_MAX_BYTES).toBe(150_000); const response = await renderImage(caseStudy.id, caseStudy.id); expect(response).not.toBeInstanceOf(FakeResponse); expect(mockedImageResponse).toHaveBeenCalledTimes(1); const { card, options } = sentCard(); expect(card.type).toBe(OgCard); expect(card.props).toMatchObject({ eyebrow: "Case de cliente", title: caseStudy.title, description: caseStudy.summary, }); expect(card.props.backgroundImage).toBeUndefined(); expect(options).toEqual(OG_SIZE); }); it("path: image metadata -> JPEG for covers, PNG for generated cards, and every case listed without a slug", () => { const [sev] = generateImageMetadata({ params: { slug: "sev-modaintima" } }); const [twins] = generateImageMetadata({ params: { slug: "twins-lab" } }); expect(sev).toMatchObject({ id: "sev-modaintima", contentType: "image/jpeg", size: OG_SIZE, }); expect(twins).toMatchObject({ id: "twins-lab", contentType: "image/png", size: OG_SIZE, }); const all = generateImageMetadata({ params: {} }); expect(all.map(({ id }) => id)).toEqual(caseStudies.map(({ id }) => id)); expect( all.filter(({ contentType }) => contentType === "image/jpeg"), ).toHaveLength(5); }); it("path: unknown slug -> notFound() and no image", async () => { await expect(renderImage("nao-existe", "nao-existe")).rejects.toThrow( "NEXT_NOT_FOUND", ); expect(notFound).toHaveBeenCalledTimes(1); expect(mockedImageResponse).not.toHaveBeenCalled(); }); it("path: known slug but another case's image id -> notFound() and no image", async () => { await expect(renderImage("sev-modaintima", "lavacao-047")).rejects.toThrow( "NEXT_NOT_FOUND", ); expect(notFound).toHaveBeenCalledTimes(1); expect(mockedImageResponse).not.toHaveBeenCalled(); }); it("path: laboratory case -> card labelled as a laboratory project, no cover when none is recorded", async () => { const caseStudy = getCase("twins-lab"); await renderImage(caseStudy.id, caseStudy.id); const { card } = sentCard(); expect(card.props).toMatchObject({ eyebrow: "Projeto de laboratório", title: caseStudy.title, }); expect(card.props.backgroundImage).toBeUndefined(); }); });