249 lines
9.2 KiB
TypeScript
249 lines
9.2 KiB
TypeScript
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 > 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)");
|
||
});
|
||
});
|