Files
OpenFrontIO/tests/NameLayer.test.ts
T
2026-05-09 02:14:44 +02:00

163 lines
5.4 KiB
TypeScript

import {
computeAllianceClipPath,
computeAllianceTopCutPercent,
} from "../src/client/graphics/PlayerIcons";
import {
computeNameLayerLayout,
computeNameLayerScreenMetrics,
computeNameLayerWorldScale,
computeTraitorFlashAlpha,
computeTraitorFlashDurationSeconds,
replaceUnsupportedNameGlyphs,
resetNameLayerGlyphWarningsForTests,
} from "../src/client/graphics/layers/NameLayerLayout";
describe("PlayerIcons", () => {
describe("computeAllianceClipPath", () => {
test("returns full visibility (20% top cut) when alliance time is at 100%", () => {
const result = computeAllianceClipPath(1.0);
// topCut = 20 + (1 - 1.0) * 80 * 0.78 = 20 + 0 = 20.00
expect(result).toBe("inset(20.00% -2px 0 -2px)");
});
test("returns maximum cut (82.40% top cut) when alliance time is at 0%", () => {
const result = computeAllianceClipPath(0.0);
// topCut = 20 + (1 - 0.0) * 80 * 0.78 = 20 + 62.4 = 82.40
expect(result).toBe("inset(82.40% -2px 0 -2px)");
});
test("returns 51.20% top cut when alliance time is at 50%", () => {
const result = computeAllianceClipPath(0.5);
// topCut = 20 + (1 - 0.5) * 80 * 0.78 = 20 + 31.2 = 51.20
expect(result).toBe("inset(51.20% -2px 0 -2px)");
});
test("returns 27.80% top cut when alliance time is at 87.5%", () => {
const result = computeAllianceClipPath(0.875);
// topCut = 20 + (1 - 0.875) * 80 * 0.78 = 20 + 7.8 = 27.80
expect(result).toBe("inset(27.80% -2px 0 -2px)");
});
test("returns 74.60% top cut when alliance time is at 12.5%", () => {
const result = computeAllianceClipPath(0.125);
// topCut = 20 + (1 - 0.125) * 80 * 0.78 = 20 + 54.6 = 74.60
expect(result).toBe("inset(74.60% -2px 0 -2px)");
});
test("includes -2px horizontal overscan to prevent subpixel gaps", () => {
const result = computeAllianceClipPath(0.5);
expect(result).toContain("-2px");
expect(result.match(/-2px/g)).toHaveLength(2); // Should appear twice (left and right)
});
test("shares numeric top-cut helper with Pixi masks", () => {
expect(computeAllianceTopCutPercent(1.0)).toBeCloseTo(20);
expect(computeAllianceTopCutPercent(0.5)).toBeCloseTo(51.2);
expect(computeAllianceTopCutPercent(0.0)).toBeCloseTo(82.4);
});
});
});
describe("NameLayerLayout", () => {
test("computes DOM-compatible local row positions with flag and icon gaps", () => {
const layout = computeNameLayerLayout({
fontSize: 10,
iconSize: 15,
iconCount: 2,
centeredIconCount: 1,
hasFlag: true,
flagAspectRatio: 2,
nameWidth: 40,
troopWidth: 30,
});
expect(layout.iconPositions).toEqual([
{ x: -9.5, y: -9.75 },
{ x: 9.5, y: -9.75 },
]);
expect(layout.flag).toEqual({ x: -20, y: 2.75, width: 20, height: 10 });
expect(layout.nameText).toEqual({ x: 10, y: 2.75 });
expect(layout.troopText).toEqual({ x: 0, y: 12.25 });
expect(layout.centeredIconPositions).toEqual([{ x: 0, y: 2.75 }]);
});
test("keeps no-flag names centered on the text width", () => {
const layout = computeNameLayerLayout({
fontSize: 12,
iconSize: 18,
iconCount: 0,
centeredIconCount: 0,
hasFlag: false,
flagAspectRatio: 1,
nameWidth: 60,
troopWidth: 24,
});
expect(layout.flag).toBeNull();
expect(layout.nameText.x).toBe(0);
expect(layout.width).toBe(60);
});
test("combines local label scale with camera scale for world-stable labels", () => {
expect(computeNameLayerWorldScale(8, 2)).toBeCloseTo(4);
expect(computeNameLayerWorldScale(20, 2)).toBeCloseTo(6);
});
test("computes final screen-space text and icon sizes", () => {
expect(computeNameLayerScreenMetrics(8, 2)).toEqual({
fontSize: 16,
iconSize: 24,
});
expect(computeNameLayerScreenMetrics(20, 2)).toEqual({
fontSize: 48,
iconSize: 72,
});
});
test("matches traitor flash duration thresholds and alpha extrema", () => {
expect(computeTraitorFlashDurationSeconds(156)).toBeNull();
expect(computeTraitorFlashDurationSeconds(150)).toBeCloseTo(1);
expect(computeTraitorFlashDurationSeconds(0)).toBeCloseTo(0.2);
expect(computeTraitorFlashAlpha(150, 0)).toBeCloseTo(1);
expect(computeTraitorFlashAlpha(150, 250)).toBeCloseTo(0.65);
expect(computeTraitorFlashAlpha(150, 500)).toBeCloseTo(0.3);
});
test("spreads multiple centered icons instead of stacking them", () => {
const layout = computeNameLayerLayout({
fontSize: 10,
iconSize: 15,
iconCount: 0,
centeredIconCount: 2,
hasFlag: false,
flagAspectRatio: 1,
nameWidth: 40,
troopWidth: 30,
});
expect(layout.centeredIconPositions).toEqual([
{ x: -9.5, y: -4.75 },
{ x: 9.5, y: -4.75 },
]);
});
test("replaces unsupported glyphs once per glyph", () => {
resetNameLayerGlyphWarningsForTests();
const warn = vi.fn();
expect(replaceUnsupportedNameGlyphs("A🙂🙂B", warn)).toBe("A??B");
expect(replaceUnsupportedNameGlyphs("🙂", warn)).toBe("?");
expect(warn).toHaveBeenCalledTimes(1);
});
test("replaces unsupported grapheme clusters with one fallback glyph", () => {
resetNameLayerGlyphWarningsForTests();
const warn = vi.fn();
expect(
replaceUnsupportedNameGlyphs("A\u{1F469}\u200D\u{1F4BB}B", warn),
).toBe("A?B");
expect(warn).toHaveBeenCalledTimes(1);
});
});