166 lines
5.5 KiB
TypeScript
166 lines
5.5 KiB
TypeScript
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<string, string> },
|
|
) {}
|
|
}
|
|
|
|
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<typeof import("@/lib/og-card")>(
|
|
"@/lib/og-card",
|
|
).readProjectOgImage;
|
|
|
|
afterEach(() => {
|
|
mockedReadProjectOgImage.mockImplementation(actualReadProjectOgImage);
|
|
});
|
|
|
|
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<React.ComponentProps<typeof OgCard>>(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(6);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|