Files
buzz-sheet/lib/image-export/render.test.tsx
T

206 lines
6.9 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { renderToStaticMarkup } from "react-dom/server";
import sharp from "sharp";
import { beforeAll, afterAll, describe, expect, it, vi } from "vitest";
import { init, Renderer } from "takumi-js/wasm/no-init";
import { exportFixtures } from "./fixtures";
import { renderExport } from "./render";
import { exportTemplate } from "./templates";
import { imageSources, loadImageSources } from "./assets";
let renderer: Renderer;
beforeAll(async () => {
await init({ module_or_path: await readFile("public/takumi.wasm") });
renderer = new Renderer();
for (const [name, path] of [
["Anuphan", "Anuphan.ttf"],
["Genshin Overview", "zh-cn.ttf"],
["Kanit Overview", "Kanit-SemiBold.ttf"],
["Kanit Poster", "Kanit-ExtraBoldItalic.ttf"],
]) {
await renderer.registerFont({
name,
data: await readFile(`public/${path}`),
});
}
});
afterAll(() => renderer.free());
describe("browser WASM export templates", () => {
it.each(exportFixtures.map((request) => [request.kind, request] as const))(
"renders %s with intended dimensions and visible content",
async (_, request) => {
const art = await sharp({
create: { width: 500, height: 700, channels: 4, background: "#786a9c" },
})
.png()
.toBuffer();
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input) => {
const path = new URL(String(input)).pathname;
const bytes =
path === "/test-art.png" ? art : await readFile(`public${path}`);
return new Response(bytes);
});
try {
const bytes = await renderExport(
renderer,
request,
"http://export.test",
);
const metadata = await sharp(bytes).metadata();
expect(metadata.width).toBe(request.width);
if (request.height) expect(metadata.height).toBe(request.height);
else expect(metadata.height).toBeGreaterThan(200);
expect(bytes.length).toBeGreaterThan(10000);
const { stdev } = (await sharp(bytes).stats()).channels[0];
expect(stdev).toBeGreaterThan(5);
if (request.kind === "constellation") {
const topBar = await sharp(bytes)
.extract({ left: 200, top: 250, width: 1, height: 1 })
.removeAlpha()
.raw()
.toBuffer();
expect([...topBar]).toEqual([255, 255, 255]);
const highlightedBar = await sharp(bytes)
.extract({ left: 200, top: 750, width: 1, height: 1 })
.removeAlpha()
.raw()
.toBuffer();
expect(highlightedBar[0]).toBeGreaterThan(highlightedBar[1]);
expect(highlightedBar[1]).toBeGreaterThan(highlightedBar[2]);
}
} finally {
fetchMock.mockRestore();
}
},
30000,
);
it.each(["tierlist", "best-stats", "target-stats", "talents"] as const)(
"renders artifact %s selection",
async (section) => {
const fixture = exportFixtures.find(
(request) => request.kind === "artifact",
)!;
if (fixture.kind !== "artifact")
throw new Error("Missing artifact fixture");
const art = await sharp({
create: { width: 50, height: 50, channels: 4, background: "#786a9c" },
})
.png()
.toBuffer();
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input) => {
const path = new URL(String(input)).pathname;
return new Response(
path === "/test-art.png" ? art : await readFile(`public${path}`),
);
});
try {
const width =
section === "best-stats" ? 700 : section === "tierlist" ? 1200 : 800;
const height = section === "best-stats" ? 700 : undefined;
const bytes = await renderExport(
renderer,
{
...fixture,
width,
height,
withoutNotes: true,
data: { ...fixture.data, sections: [section], includeNote: false },
},
"http://export.test",
);
const metadata = await sharp(bytes).metadata();
expect(metadata.width).toBe(width);
if (height) expect(metadata.height).toBe(height);
expect(metadata.height).toBeGreaterThan(150);
} finally {
fetchMock.mockRestore();
}
},
30000,
);
it("encodes a WebP export", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input) => {
const path = new URL(String(input)).pathname;
return new Response(
await readFile(
path === "/test-art.png"
? "public/icon/Watermark.png"
: `public${path}`,
),
);
});
try {
const bytes = await renderExport(
renderer,
{ ...exportFixtures[0], format: "webp", withoutNotes: true },
"http://export.test",
);
expect((await sharp(bytes).metadata()).format).toBe("webp");
} finally {
fetchMock.mockRestore();
}
}, 30000);
it("omits Thai notes before layout", () => {
const request = exportFixtures[0];
expect(renderToStaticMarkup(exportTemplate(request))).toContain(
"โน้ตภาษาไทย",
);
expect(
renderToStaticMarkup(exportTemplate({ ...request, withoutNotes: true })),
).not.toContain("โน้ตภาษาไทย");
});
it("filters artifact sections and conditions before layout", () => {
const request = exportFixtures.find((item) => item.kind === "artifact")!;
if (request.kind !== "artifact")
throw new Error("Missing artifact fixture");
const html = renderToStaticMarkup(
exportTemplate({
...request,
data: {
...request.data,
sections: ["best-stats"],
recommendedCondition: 1,
includeNote: false,
},
}),
);
expect(html).toContain("Second");
expect(html).not.toContain("First");
expect(html).not.toContain("Late Game");
expect(html).not.toContain("Talent Priority");
expect(html).not.toContain("artifact-export-right");
expect(html).not.toContain("โน้ตโปรไฟล์");
});
it("collects mask sources and deduplicates artwork", () => {
expect(
imageSources({
type: "container",
style: { maskImage: "url('/mask.png')" },
children: [{ type: "image", src: "/mask.png" }],
}),
).toEqual(["/mask.png"]);
});
it("rejects missing artwork instead of producing an incomplete image", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(null, { status: 404 }));
try {
await expect(
loadImageSources(
{ type: "image", src: "/missing.png" },
"http://export.test",
),
).rejects.toThrow("Image load failed");
} finally {
fetchMock.mockRestore();
}
});
});