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