import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { LocaleBoundary } from "@/components/i18n/locale-provider"; import { GuideImageExport } from "@/components/public/guide-image-export"; import { ConstellationImage } from "@/components/public/constellation-image"; import type { PublicGuide } from "@/lib/guides/queries"; import { exportMessages, exportStatLabel, exportText, withoutNotesFileName, type ExportMessageKey } from "@/lib/guides/export-text"; describe("export translations", () => { it("keeps matching keys and named placeholders in both languages", () => { expect(Object.keys(exportMessages.th).sort()).toEqual(Object.keys(exportMessages.en).sort()); for (const key of Object.keys(exportMessages.en) as ExportMessageKey[]) { const placeholders = (text: string) => [...text.matchAll(/\{(\w+)\}/gu)].map((match) => match[1]).sort(); expect(placeholders(exportMessages.th[key]), key).toEqual(placeholders(exportMessages.en[key])); expect(exportMessages.en[key], key).not.toMatch(/[\u0e00-\u0e7f]/u); } }); it("defaults to English and interpolates names without interpreting their contents", () => { expect(exportText("weaponTitle", { name: "Raiden" })).toBe("Raiden Weapons"); expect(exportText("rotation", { seconds: "20.5" })).toBe("Rotation duration: 20.5 seconds"); expect(exportText("weaponTitle", { name: "{seconds}" })).toBe("{seconds} Weapons"); expect(exportText("condition", { index: 2 }, "th")).toBe("Condition 2"); }); it("keeps technical labels in English while localising export controls", () => { for (const key of ["materials", "materialLevel", "weaponTitle", "talents", "mainStats", "teamDamage"] as const) { expect(exportText(key, { name: "Raiden", level: 90 }, "th")).toBe(exportText(key, { name: "Raiden", level: 90 }, "en")); } expect(exportText("materials", {}, "th")).toBe("MATERIALS"); expect(exportText("materialLevel", { level: 90 }, "th")).toBe("LV.90"); expect(exportText("exportImage", {}, "th")).toBe("ส่งออกภาพ"); }); it("uses English stat names for exports while retaining Thai page labels and custom values", () => { expect(exportStatLabel("EM")).toBe("Elemental Mastery"); expect(exportStatLabel("CRIT DAMAGE")).toBe("CRIT DMG"); expect(exportStatLabel("EM", "th")).toBe("ความชำนาญธาตุ"); expect(exportStatLabel("Pyro DMG%")).toBe("Pyro DMG%"); expect(exportStatLabel("Custom value")).toBe("Custom value"); }); it("distinguishes no-note downloads while preserving the image extension", () => { expect(withoutNotesFileName("guide.png")).toBe("guide-without-notes.png"); expect(withoutNotesFileName("guide.v2.webp")).toBe("guide.v2-without-notes.webp"); expect(withoutNotesFileName("guide")).toBe("guide-without-notes"); }); }); describe("image export controls", () => { it("always offers both export modes even without notes", () => { const html = renderToStaticMarkup(No notes); expect(html).toContain("Export image"); expect(html).toContain("Export without note"); expect(html.match(/ { const html = renderToStaticMarkup(Overview); expect(html.match(/ { const html = renderToStaticMarkup(No sections); expect(html.match(/\sdisabled=""/gu)).toHaveLength(2); }); it.each(["English note", "โน้ตภาษาไทย"])("marks constellation notes for omission regardless of language: %s", (note) => { const guide = { character: { name: "Raiden", splashImageUrl: "/splash.png" }, coverUrl: "/cover.png" } as PublicGuide; const group = { rows: [], note } as unknown as PublicGuide["constellationGroups"][number]; const html = renderToStaticMarkup({createElement(ConstellationImage, { guide, group })}); expect(html).toContain("Raiden’s Constellations"); expect(html).toContain("Team Damage"); expect(html).toContain('data-export-note="true"'); expect(html).toContain(note); }); });