Files
healer-man/src/game/uiLayout.test.ts
T
2026-09-14 13:20:20 -04:00

132 lines
7.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
actionKeyForEvent, assignDesktopAbility, bindDesktopSlot, defaultUiLayout, desktopSlotForKey,
exportUiPreferences, importUiPreferences, normalizeActionKey, normalizeUiPreferences, resetDesktopAbilities, resolveDpsMeterDisplay,
HUD_ELEMENTS, resolveHudScreen,
} from "./uiLayout";
describe("personal UI layouts", () => {
it("assigns every element one display, saves routing, and keeps single-display widgets available", () => {
const preferences = normalizeUiPreferences(null);
const layout = preferences.current.mobile;
expect(resolveHudScreen("party", layout, true)).toBe("top");
for (const id of ["actions", "player", "cast", "experience"] as const) expect(resolveHudScreen(id, layout, true)).toBe("bottom");
for (const id of Object.keys(HUD_ELEMENTS) as (keyof typeof HUD_ELEMENTS)[]) {
layout.elements[id].screen = "bottom";
expect(resolveHudScreen(id, layout, true)).toBe("bottom");
expect(resolveHudScreen(id, layout, false)).toBe("top");
if (id === "dps") {
expect(resolveDpsMeterDisplay(layout, false)).toBe("main");
expect(resolveDpsMeterDisplay(layout, true)).toBe("companion");
}
layout.elements[id].screen = "top";
expect(resolveHudScreen(id, layout, true)).toBe("top");
layout.elements[id].screen = "hidden";
expect(resolveHudScreen(id, layout, true)).toBe("hidden");
}
expect(importUiPreferences(exportUiPreferences(preferences))).toEqual(preferences);
});
it("saves and clears abilities on inactive WoW bars without changing keys, templates, or mobile controls", () => {
const ui = normalizeUiPreferences(null);
ui.slots.pc[0] = { name: "Original", layout: ui.current.pc };
const original = exportUiPreferences(ui);
ui.current.pc = assignDesktopAbility(ui.current.pc, 4, 11, "test-heal");
const saved = importUiPreferences(exportUiPreferences(ui));
expect(saved.current.pc.bars[4].slots[11]).toEqual({ abilityId: "test-heal", key: null });
expect(saved.current.pc.bars[0]).toEqual(JSON.parse(original).current.pc.bars[0]);
expect(saved.current.mobile).toEqual(ui.current.mobile);
expect(saved.slots.pc[0]?.layout.bars[4].slots[11].abilityId).toBeNull();
expect(assignDesktopAbility(saved.current.pc, 4, 11, null).bars[4].slots[11].abilityId).toBeNull();
});
it("restores one or all WoW ability bars with the correct offset and retains custom keys and geometry", () => {
let layout = defaultUiLayout();
layout.wowBars = true; layout.barCount = 2; layout.bars[1].columns = 3;
layout = bindDesktopSlot(layout, 1, 0, "Ctrl+KeyQ");
layout = assignDesktopAbility(assignDesktopAbility(layout, 0, 0, "custom-one"), 1, 0, "custom-two");
const defaults = Array.from({ length: 14 }, (_, i) => `spell-${i}`);
const bar = resetDesktopAbilities(layout, defaults, 1);
expect(bar.bars[0]).toEqual(layout.bars[0]);
expect(bar.bars[1].slots[0]).toEqual({ abilityId: "spell-12", key: "Ctrl+KeyQ" });
expect(bar.bars[1].slots[2].abilityId).toBeNull();
const all = resetDesktopAbilities(bar, defaults);
expect(all.bars[0].slots[0]).toEqual({ abilityId: "spell-0", key: "Digit1" });
expect(all.bars[1].columns).toBe(3);
expect(all.wowBars).toBe(true); expect(all.barCount).toBe(2);
});
it("defaults DPS to PC lower-left flow and the Thor companion, with saved overrides", () => {
const ui = normalizeUiPreferences(null);
expect(ui.current.pc.elements.dps).toEqual({ visible: true, position: null, width: null });
expect(resolveDpsMeterDisplay(ui.current.pc, false)).toBe("main");
expect(resolveDpsMeterDisplay(ui.current.mobile, true)).toBe("companion");
expect(resolveDpsMeterDisplay(ui.current.pc, true)).toBe("companion");
ui.current.mobile.dpsMeterDisplay = "main";
ui.current.mobile.elements.dps = { visible: false, position: { x: .6, y: .7 }, width: 300 };
expect(importUiPreferences(exportUiPreferences(ui))).toEqual(ui);
expect(resolveDpsMeterDisplay(ui.current.mobile, true)).toBe("main");
});
it("imports older UI templates without a DPS meter", () => {
const exported = JSON.parse(exportUiPreferences(normalizeUiPreferences(null)));
for (const device of ["pc", "mobile"]) {
delete exported.current[device].elements.dps;
delete exported.current[device].dpsMeterDisplay;
}
expect(importUiPreferences(JSON.stringify(exported)).current.pc.elements.dps.visible).toBe(true);
exported.current.pc.dpsMeterDisplay = "invalid";
expect(() => importUiPreferences(JSON.stringify(exported))).toThrow("DPS meter display");
});
it("migrates old accounts to independent Mobile and PC layouts with exactly six empty slots", () => {
const ui = normalizeUiPreferences(undefined);
expect(ui.slots.mobile).toEqual([null, null, null]);
expect(ui.slots.pc).toEqual([null, null, null]);
ui.current.pc.elements.party.visible = false;
expect(ui.current.mobile.elements.party.visible).toBe(true);
expect(ui.current.pc.bars).toHaveLength(5);
expect(ui.current.pc.bars.every((bar) => bar.slots.length === 12)).toBe(true);
});
it("round trips named slots, positions, hidden elements, geometry, assignments, and key chords", () => {
const ui = normalizeUiPreferences(null);
const layout = ui.current.pc;
layout.wowBars = true; layout.barCount = 5;
layout.elements.party = { visible: false, position: { x: .32, y: .45 }, width: 200 };
layout.bars[4].columns = 3;
layout.bars[4].slots[11] = { abilityId: "test-heal", key: "Ctrl+Shift+KeyQ" };
ui.slots.pc[2] = { name: "Raid night", layout };
ui.slots.mobile[0] = { name: "Travel", layout: defaultUiLayout() };
expect(importUiPreferences(exportUiPreferences(ui))).toEqual(ui);
});
it("reassigns conflicts even from hidden bars, accepts modifiers and clears a binding", () => {
let layout = defaultUiLayout(); layout.wowBars = true; layout.barCount = 5;
layout = bindDesktopSlot(layout, 4, 11, "Digit1");
expect(layout.bars[0].slots[0].key).toBeNull();
expect(desktopSlotForKey(layout, "Digit1")).toBe(layout.bars[4].slots[11]);
layout.elements["bar-5"].visible = false;
expect(desktopSlotForKey(layout, "Digit1")).not.toBeNull();
layout.barCount = 4;
expect(desktopSlotForKey(layout, "Digit1")).toBeNull();
layout = bindDesktopSlot(layout, 4, 11, null);
expect(layout.bars[4].slots[11].key).toBeNull();
expect(actionKeyForEvent({ code: "KeyQ", ctrlKey: true, shiftKey: true, altKey: false, metaKey: false })).toBe("Ctrl+Shift+KeyQ");
expect(normalizeActionKey("Escape")).toBe("Escape");
expect(normalizeActionKey("ShiftLeft")).toBeNull();
});
it.each([
["unsupported versions", (data: any) => { data.version = 99; }],
["extra slots", (data: any) => { data.slots.mobile.push(null); }],
["invalid coordinates", (data: any) => { data.current.pc.elements.party.position = { x: "bad", y: 0 }; }],
["invalid bar geometry", (data: any) => { data.current.pc.bars[0].columns = 7; }],
["invalid keys", (data: any) => { data.current.pc.bars[0].slots[0].key = "NotAKey"; }],
["duplicate chords", (data: any) => { data.current.pc.bars[0].slots[1].key = "Digit1"; }],
["missing buttons", (data: any) => { data.current.pc.bars[0].slots.pop(); }],
])("rejects %s without mutating the original preferences", (_, mutate) => {
const ui = normalizeUiPreferences(null);
const original = exportUiPreferences(ui);
const data = JSON.parse(original); mutate(data);
expect(() => importUiPreferences(JSON.stringify(data))).toThrow();
expect(exportUiPreferences(ui)).toBe(original);
});
it("rejects malformed and oversized imports", () => {
expect(() => importUiPreferences("{")).toThrow("valid JSON");
expect(() => importUiPreferences(" ".repeat(256001))).toThrow("256 KB");
});
});