434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
|
|
import { expect } from "@esm-bundle/chai";
|
||
|
|
import { DeploymentHUD } from "../../src/ui/deployment-hud.js";
|
||
|
|
|
||
|
|
describe("UI: DeploymentHUD", () => {
|
||
|
|
let element;
|
||
|
|
let container;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
container = document.createElement("div");
|
||
|
|
document.body.appendChild(container);
|
||
|
|
element = document.createElement("deployment-hud");
|
||
|
|
container.appendChild(element);
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
if (container.parentNode) {
|
||
|
|
container.parentNode.removeChild(container);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Helper to wait for LitElement update
|
||
|
|
async function waitForUpdate() {
|
||
|
|
await element.updateComplete;
|
||
|
|
// Give a small delay for DOM updates
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper to query shadow DOM
|
||
|
|
function queryShadow(selector) {
|
||
|
|
return element.shadowRoot?.querySelector(selector);
|
||
|
|
}
|
||
|
|
|
||
|
|
function queryShadowAll(selector) {
|
||
|
|
return element.shadowRoot?.querySelectorAll(selector) || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("CoA 1: Basic Rendering", () => {
|
||
|
|
it("should render deployment HUD with squad units", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
{ id: "u2", name: "Weaver", classId: "CLASS_AETHER_WEAVER", icon: "✨" },
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const header = queryShadow(".header");
|
||
|
|
expect(header).to.exist;
|
||
|
|
expect(header.textContent).to.include("MISSION DEPLOYMENT");
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards.length).to.equal(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should hide when not in deployment state", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Test" }];
|
||
|
|
element.currentState = "STATE_COMBAT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const header = queryShadow(".header");
|
||
|
|
expect(header).to.be.null;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 2: Tutorial Hints", () => {
|
||
|
|
it("should display tutorial hint when missionDef provides one", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Test" }];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
tutorial_hint: "Drag units from the bench to the Green Zone.",
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const tutorialHint = queryShadow(".tutorial-hint");
|
||
|
|
expect(tutorialHint).to.exist;
|
||
|
|
expect(tutorialHint.textContent.trim()).to.equal(
|
||
|
|
"Drag units from the bench to the Green Zone."
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should display default hint when no tutorial hint provided", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Test" }];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = null;
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const tutorialHint = queryShadow(".tutorial-hint");
|
||
|
|
expect(tutorialHint).to.be.null;
|
||
|
|
|
||
|
|
const header = queryShadow(".header");
|
||
|
|
expect(header.textContent).to.include(
|
||
|
|
"Select a unit below, then click a green tile to place."
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should not display tutorial hint overlay when hint is empty", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Test" }];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
tutorial_hint: undefined,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const tutorialHint = queryShadow(".tutorial-hint");
|
||
|
|
expect(tutorialHint).to.be.null;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 3: Suggested Units", () => {
|
||
|
|
it("should highlight suggested units with suggested class", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
{ id: "u2", name: "Weaver", classId: "CLASS_AETHER_WEAVER", icon: "✨" },
|
||
|
|
{ id: "u3", name: "Scavenger", classId: "CLASS_SCAVENGER", icon: "🔧" },
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
suggested_units: ["CLASS_VANGUARD", "CLASS_AETHER_WEAVER"],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards.length).to.equal(3);
|
||
|
|
|
||
|
|
// Check that suggested units have the 'suggested' attribute
|
||
|
|
const vanguardCard = Array.from(unitCards).find((card) =>
|
||
|
|
card.textContent.includes("Vanguard")
|
||
|
|
);
|
||
|
|
const weaverCard = Array.from(unitCards).find((card) =>
|
||
|
|
card.textContent.includes("Weaver")
|
||
|
|
);
|
||
|
|
const scavengerCard = Array.from(unitCards).find((card) =>
|
||
|
|
card.textContent.includes("Scavenger")
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(vanguardCard?.hasAttribute("suggested")).to.be.true;
|
||
|
|
expect(weaverCard?.hasAttribute("suggested")).to.be.true;
|
||
|
|
expect(scavengerCard?.hasAttribute("suggested")).to.be.false;
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should display RECOMMENDED label on suggested units", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
suggested_units: ["CLASS_VANGUARD"],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
expect(unitCard.textContent).to.include("RECOMMENDED");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should not show RECOMMENDED on deployed suggested units", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [0]; // Unit is deployed
|
||
|
|
element.deployedIds = []; // Initialize empty, will be updated from indices
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
suggested_units: ["CLASS_VANGUARD"],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
expect(unitCard.textContent).to.include("DEPLOYED");
|
||
|
|
expect(unitCard.textContent).to.not.include("RECOMMENDED");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should handle empty suggested_units array", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
suggested_units: [],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
expect(unitCard?.hasAttribute("suggested")).to.be.false;
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should handle missing deployment config gracefully", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard", classId: "CLASS_VANGUARD", icon: "🛡️" },
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {}; // No deployment config
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
expect(unitCard?.hasAttribute("suggested")).to.be.false;
|
||
|
|
|
||
|
|
const tutorialHint = queryShadow(".tutorial-hint");
|
||
|
|
expect(tutorialHint).to.be.null;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 4: Deployment State", () => {
|
||
|
|
it("should show deployment count and max units", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Vanguard" },
|
||
|
|
{ id: "u2", name: "Weaver" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [0]; // Deploy first unit
|
||
|
|
element.deployedIds = []; // Initialize empty, will be updated from indices
|
||
|
|
element.maxUnits = 4;
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const statusBar = queryShadow(".status-bar");
|
||
|
|
expect(statusBar.textContent).to.include("Squad Size: 1 / 4");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should disable start button when no units deployed", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Vanguard" }];
|
||
|
|
element.deployedIndices = [];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const startBtn = queryShadow(".start-btn");
|
||
|
|
expect(startBtn?.hasAttribute("disabled")).to.be.true;
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should enable start button when units are deployed", async () => {
|
||
|
|
element.squad = [{ id: "u1", name: "Vanguard" }];
|
||
|
|
element.deployedIndices = [0]; // Deploy first unit
|
||
|
|
element.deployedIds = []; // Initialize empty, will be updated from indices
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const startBtn = queryShadow(".start-btn");
|
||
|
|
expect(startBtn?.hasAttribute("disabled")).to.be.false;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 5: Unit Name and Class Display", () => {
|
||
|
|
it("should display character name and class name separately", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{
|
||
|
|
id: "u1",
|
||
|
|
name: "Valerius",
|
||
|
|
className: "Vanguard",
|
||
|
|
classId: "CLASS_VANGUARD",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
const unitName = unitCard?.querySelector(".unit-name");
|
||
|
|
const unitClass = unitCard?.querySelector(".unit-class");
|
||
|
|
|
||
|
|
expect(unitName?.textContent.trim()).to.equal("Valerius");
|
||
|
|
expect(unitClass?.textContent.trim()).to.equal("Vanguard");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should format classId to className when className is missing", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{
|
||
|
|
id: "u1",
|
||
|
|
name: "Aria",
|
||
|
|
classId: "CLASS_AETHER_WEAVER",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
const unitClass = unitCard?.querySelector(".unit-class");
|
||
|
|
|
||
|
|
expect(unitClass?.textContent.trim()).to.equal("Aether Weaver");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should handle missing name gracefully", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{
|
||
|
|
id: "u1",
|
||
|
|
classId: "CLASS_VANGUARD",
|
||
|
|
className: "Vanguard",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
const unitName = unitCard?.querySelector(".unit-name");
|
||
|
|
|
||
|
|
expect(unitName?.textContent.trim()).to.equal("Unknown");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 6: Deployed Units", () => {
|
||
|
|
it("should convert deployed indices to IDs and apply deployed styling", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Valerius", className: "Vanguard" },
|
||
|
|
{ id: "u2", name: "Aria", className: "Weaver" },
|
||
|
|
{ id: "u3", name: "Kael", className: "Scavenger" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [0, 2]; // Deploy units at indices 0 and 2
|
||
|
|
element.deployedIds = []; // Initialize empty
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards.length).to.equal(3);
|
||
|
|
|
||
|
|
// Check deployed attribute
|
||
|
|
expect(unitCards[0].hasAttribute("deployed")).to.be.true;
|
||
|
|
expect(unitCards[1].hasAttribute("deployed")).to.be.false;
|
||
|
|
expect(unitCards[2].hasAttribute("deployed")).to.be.true;
|
||
|
|
|
||
|
|
// Check deployed count
|
||
|
|
const statusBar = queryShadow(".status-bar");
|
||
|
|
expect(statusBar.textContent).to.include("Squad Size: 2 / 4");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should update deployedIds when squad changes", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Valerius" },
|
||
|
|
{ id: "u2", name: "Aria" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [0];
|
||
|
|
element.deployedIds = []; // Initialize empty
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
// Change squad
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u3", name: "Kael" },
|
||
|
|
{ id: "u4", name: "Lyra" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [1];
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards[0].hasAttribute("deployed")).to.be.false;
|
||
|
|
expect(unitCards[1].hasAttribute("deployed")).to.be.true;
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should handle deployment-update event", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Valerius" },
|
||
|
|
{ id: "u2", name: "Aria" },
|
||
|
|
];
|
||
|
|
element.deployedIndices = [];
|
||
|
|
element.deployedIds = []; // Initialize empty
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
// Simulate deployment-update event
|
||
|
|
window.dispatchEvent(
|
||
|
|
new CustomEvent("deployment-update", {
|
||
|
|
detail: { deployedIndices: [0] },
|
||
|
|
})
|
||
|
|
);
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards[0].hasAttribute("deployed")).to.be.true;
|
||
|
|
expect(unitCards[1].hasAttribute("deployed")).to.be.false;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("CoA 7: Selected Units", () => {
|
||
|
|
it("should highlight selected unit", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Valerius", className: "Vanguard" },
|
||
|
|
{ id: "u2", name: "Aria", className: "Weaver" },
|
||
|
|
];
|
||
|
|
element.selectedId = "u1";
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCards = queryShadowAll(".unit-card");
|
||
|
|
expect(unitCards[0].hasAttribute("selected")).to.be.true;
|
||
|
|
expect(unitCards[1].hasAttribute("selected")).to.be.false;
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should prioritize selected styling over suggested", async () => {
|
||
|
|
element.squad = [
|
||
|
|
{ id: "u1", name: "Valerius", className: "Vanguard", classId: "CLASS_VANGUARD" },
|
||
|
|
];
|
||
|
|
element.selectedId = "u1";
|
||
|
|
element.deployedIds = [];
|
||
|
|
element.currentState = "STATE_DEPLOYMENT";
|
||
|
|
element.missionDef = {
|
||
|
|
deployment: {
|
||
|
|
suggested_units: ["CLASS_VANGUARD"],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await waitForUpdate();
|
||
|
|
|
||
|
|
const unitCard = queryShadow(".unit-card");
|
||
|
|
expect(unitCard.hasAttribute("selected")).to.be.true;
|
||
|
|
expect(unitCard.hasAttribute("suggested")).to.be.true;
|
||
|
|
// Both attributes should be present, CSS will handle priority
|
||
|
|
// We can't easily test computed styles in this environment, so just verify attributes
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Note: Portrait display tests are skipped because image pathing doesn't work
|
||
|
|
// correctly in the test environment (404 errors). The portrait functionality
|
||
|
|
// is tested through manual/integration testing.
|
||
|
|
});
|
||
|
|
|