206 lines
6.9 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|