Files
buzz-sheet/lib/guides/image-export.test.tsx
2026-10-07 17:03:15 +07:00

249 lines
9.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import {
GuideImageExport,
imageExportFileName,
scaledExportFontSize,
} from "@/components/public/guide-image-export";
import { MaterialPoster } from "@/components/public/material-poster";
import { ConstellationImage } from "@/components/public/constellation-image";
import { WeaponComparisonImage } from "@/components/public/weapon-image-export";
import { TeamExportImage, type TeamExportData } from "@/components/public/team-search";
import type { PublicGuide } from "@/lib/guides/queries";
describe("image export controls", () => {
it.each([1, 4])("renders the shared Team Calc image with %i member(s)", (memberCount) => {
const members: TeamExportData["members"] = Array.from({ length: memberCount }, (_, position) => ({
position,
constellation: String(position),
refinement: "1",
damage: "1000",
character: {
key: `character-${position}`,
name: `Character ${position + 1}`,
imageUrl: "/character.png",
element: "Fire",
rarity: 5,
},
weapon: {
key: `weapon-${position}`,
name: `Weapon ${position + 1}`,
imageUrl: "/weapon.png",
rarity: 5,
},
artifact: {
key: `artifact-${position}`,
name: `Artifact ${position + 1}`,
imageUrl: "/artifact.png",
rarity: 5,
},
secondArtifact: null,
}));
const team: TeamExportData = {
title: "Generator Team",
rotationSeconds: "20",
combo: "E > Q",
members,
};
const html = renderToStaticMarkup(<TeamExportImage team={team} />);
expect(html).toContain("Generator Team");
expect(html).toContain("Rotation duration: 20 seconds");
expect(html).toContain("E &gt; Q");
expect(html).toContain(`DPS: ${memberCount * 50}`);
expect(html.match(/Character \d/gu)).toHaveLength(memberCount);
});
it("omits the placeholder condition from weapon export images", () => {
const guide = {
character: { name: "Raiden", splashImageUrl: "/splash.png" },
coverUrl: "/cover.png",
} as PublicGuide;
const group = { rows: [], name: null, note: null } as unknown as PublicGuide["weaponGroups"][number];
const html = renderToStaticMarkup(
<WeaponComparisonImage
guide={guide}
group={group}
metric="overallPercent"
conditionLabel={group.name}
/>,
);
expect(html).toContain("เปรียบเทียบอาวุธ Raiden");
expect(html).toContain("Raiden’s Weapon Comparison");
expect(html).not.toContain("Condition 1");
expect(html).not.toContain("เงื่อนไข 1");
});
it("shows one export trigger and keeps settings in its dialog", () => {
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png">No notes</GuideImageExport>);
expect(html).toContain("บันทึกภาพ");
expect(html.match(/<button\b/gu)).toHaveLength(1);
});
it("shows only one export action when no-note export is disabled", () => {
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" allowWithoutNotes={false}>Content</GuideImageExport>);
expect(html).toContain("บันทึกภาพ");
expect(html.match(/<button\b/gu)).toHaveLength(1);
});
it("renders a direct export trigger without settings UI", () => {
const html = renderToStaticMarkup(
<GuideImageExport fileName="poster.png" modeSelection="direct">
Poster
</GuideImageExport>,
);
expect(html).toContain("บันทึกภาพ");
expect(html).not.toContain("ตั้งค่าภาพที่จะบันทึก");
expect(html.match(/<button\b/gu)).toHaveLength(1);
});
it("shows all settings inline with one export action inside an existing dialog", () => {
const html = renderToStaticMarkup(
<GuideImageExport fileName="guide.png" modeSelection="inline">
Content
</GuideImageExport>,
);
expect(html).toContain("รวมโน้ต");
expect(html).toContain("ไม่รวมโน้ต");
expect(html).toContain("ไทย");
expect(html).toContain("English");
expect(html).toContain("ตัวอย่างภาพส่งออก");
expect(html).toContain("h-[clamp(8rem,50svh,32rem)]");
expect(html).toContain('data-slot="dialog-footer"');
expect(html).toContain("sticky bottom-0");
expect(html.match(/>บันทึกภาพ<\/button>/gu)).toHaveLength(1);
});
it("hides the persistent language chooser when language is prompted on export", () => {
const html = renderToStaticMarkup(
<GuideImageExport
fileName="guide.png"
allowWithoutNotes={false}
showLanguageSelector={false}
promptForLanguage
>
Content
</GuideImageExport>,
);
expect(html).not.toContain("ภาษาในภาพส่งออก");
expect(html).not.toContain("English");
expect(html.match(/<button\b/gu)).toHaveLength(1);
});
it("disables export when an export selection is empty", () => {
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" disabled>No sections</GuideImageExport>);
expect(html.match(/\sdisabled=""/gu)).toHaveLength(1);
});
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("กลุ่มดาว Raiden");
expect(html).toContain("Team Damage");
expect(html).toContain("ดาเมจทั้งทีม");
expect(html).toContain('data-export-note="true"');
expect(html).toContain(note);
});
});
describe("image export filenames", () => {
it("adds the selected language after the note variant", () => {
expect(imageExportFileName("Raiden.png", "th", false)).toBe("Raiden-th.png");
expect(imageExportFileName("Raiden.png", "en", true)).toBe("Raiden-without-notes-en.png");
});
it("can preserve English-only export filenames", () => {
expect(imageExportFileName("Raiden-materials.png", "th", false, false)).toBe("Raiden-materials.png");
});
});
describe("image export text fitting", () => {
it("keeps the original font size when text already fits", () => {
expect(scaledExportFontSize(32, 180, 240)).toBe(32);
});
it("scales overflowing text to the available width", () => {
expect(scaledExportFontSize(32, 320, 240)).toBe(24);
});
it("has no minimum font-size floor for exceptionally long text", () => {
expect(scaledExportFontSize(32, 3200, 100)).toBe(1);
});
});
describe("material poster text", () => {
const props = {
character: { name: "Raiden", nameCardImageUrl: null, baseStats: [], ascension: [], leveling: [] },
characterArtUrl: null,
artworkOffsetX: 0,
artworkOffsetY: 0,
artworkScale: 100,
artworkRotation: 0,
topColors: ["#000000", "#000000"] as [string, string],
bottomColors: ["#000000", "#000000"] as [string, string],
};
it("always uses English, including on the website", () => {
const html = renderToStaticMarkup(<MaterialPoster {...props} />);
expect(html).toContain("MATERIALS");
expect(html).toContain("LV.90");
expect(html).not.toContain("วัสดุ");
expect(html).not.toContain("เลเวล 90");
});
it("shows a fifth base stat in its own box", () => {
const html = renderToStaticMarkup(
<MaterialPoster
{...props}
character={{
...props.character,
baseStats: [
{ key: "hp", value: 12000 },
{ key: "atk", value: 300 },
{ key: "def", value: 700 },
{ key: "CRIT Rate%", value: 24.2 },
{ key: "Energy Recharge%", value: 132 },
],
}}
/>,
);
expect(html).toContain("Energy Recharge%");
expect(html).toContain("132%");
expect(html).toContain("h-[7cqw]");
expect(html).toContain("top-[49%]");
});
it("optimizes scale-aware character artwork and bleeding layers for export", () => {
const characterArtUrl = "https://buzz-cdn.astrxl.dev/posters/raiden.png";
const html = renderToStaticMarkup(
<MaterialPoster
{...props}
characterArtUrl={characterArtUrl}
artworkScale={200}
bleeding={[{ id: "bleed", imageUrl: characterArtUrl, amount: 50, blendMode: "screen" }]}
/>,
);
expect(html).toContain("/_next/image?url=https%3A%2F%2Fbuzz-cdn.astrxl.dev%2Fposters%2Fraiden.png");
expect(html).toContain("w=3840");
expect(html).toContain('sizes="3200px"');
expect(html).not.toContain(`src="${characterArtUrl}"`);
});
it("rotates the character artwork and bleeding layers together", () => {
const html = renderToStaticMarkup(
<MaterialPoster {...props} characterArtUrl="/raiden.png" artworkRotation={45} />,
);
expect(html).toContain("rotate(45deg)");
});
});