aether-shards/test/ui/hub-screen.test.js

359 lines
11 KiB
JavaScript
Raw Normal View History

import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
import { HubScreen } from "../../src/ui/screens/HubScreen.js";
import { gameStateManager } from "../../src/core/GameStateManager.js";
describe("UI: HubScreen", () => {
let element;
let container;
let mockPersistence;
let mockRosterManager;
let mockMissionManager;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
element = document.createElement("hub-screen");
container.appendChild(element);
// Mock gameStateManager dependencies
mockPersistence = {
loadRun: sinon.stub().resolves(null),
};
mockRosterManager = {
roster: [],
getDeployableUnits: sinon.stub().returns([]),
};
mockMissionManager = {
completedMissions: new Set(),
};
// Replace gameStateManager properties with mocks
gameStateManager.persistence = mockPersistence;
gameStateManager.rosterManager = mockRosterManager;
gameStateManager.missionManager = mockMissionManager;
});
afterEach(() => {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
// Clean up event listeners (element handles its own cleanup in disconnectedCallback)
});
// Helper to wait for LitElement update
async function waitForUpdate() {
await element.updateComplete;
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: Live Data Binding", () => {
it("should fetch wallet and roster data on mount", async () => {
const runData = {
inventory: {
runStash: {
currency: {
aetherShards: 450,
ancientCores: 12,
},
},
},
};
mockPersistence.loadRun.resolves(runData);
mockRosterManager.roster = [
{ id: "u1", status: "READY" },
{ id: "u2", status: "READY" },
{ id: "u3", status: "INJURED" },
];
mockRosterManager.getDeployableUnits.returns([
{ id: "u1", status: "READY" },
{ id: "u2", status: "READY" },
]);
await waitForUpdate();
expect(mockPersistence.loadRun.called).to.be.true;
expect(element.wallet.aetherShards).to.equal(450);
expect(element.wallet.ancientCores).to.equal(12);
expect(element.rosterSummary.total).to.equal(3);
expect(element.rosterSummary.ready).to.equal(2);
expect(element.rosterSummary.injured).to.equal(1);
});
it("should display correct currency values in top bar", async () => {
const runData = {
inventory: {
runStash: {
currency: {
aetherShards: 450,
ancientCores: 12,
},
},
},
};
mockPersistence.loadRun.resolves(runData);
await waitForUpdate();
const resourceStrip = queryShadow(".resource-strip");
expect(resourceStrip).to.exist;
expect(resourceStrip.textContent).to.include("450");
expect(resourceStrip.textContent).to.include("12");
});
it("should handle missing wallet data gracefully", async () => {
mockPersistence.loadRun.resolves(null);
await waitForUpdate();
expect(element.wallet.aetherShards).to.equal(0);
expect(element.wallet.ancientCores).to.equal(0);
});
});
describe("CoA 2: Hotspot & Dock Sync", () => {
it("should open MISSIONS overlay when clicking missions hotspot", async () => {
await waitForUpdate();
const missionsHotspot = queryShadow(".hotspot.missions");
expect(missionsHotspot).to.exist;
missionsHotspot.click();
await waitForUpdate();
expect(element.activeOverlay).to.equal("MISSIONS");
});
it("should open MISSIONS overlay when clicking missions dock button", async () => {
await waitForUpdate();
const missionsButton = queryShadowAll(".dock-button")[1]; // Second button is MISSIONS
expect(missionsButton).to.exist;
missionsButton.click();
await waitForUpdate();
expect(element.activeOverlay).to.equal("MISSIONS");
});
it("should open BARRACKS overlay from both hotspot and button", async () => {
await waitForUpdate();
// Test hotspot
const barracksHotspot = queryShadow(".hotspot.barracks");
barracksHotspot.click();
await waitForUpdate();
expect(element.activeOverlay).to.equal("BARRACKS");
// Close and test button
element.activeOverlay = "NONE";
await waitForUpdate();
const barracksButton = queryShadowAll(".dock-button")[0]; // First button is BARRACKS
barracksButton.click();
await waitForUpdate();
expect(element.activeOverlay).to.equal("BARRACKS");
});
});
describe("CoA 3: Overlay Management", () => {
it("should render mission-board component when activeOverlay is MISSIONS", async () => {
element.activeOverlay = "MISSIONS";
await waitForUpdate();
// Import MissionBoard dynamically
await import("../../src/ui/components/MissionBoard.js");
await waitForUpdate();
const overlayContainer = queryShadow(".overlay-container.active");
expect(overlayContainer).to.exist;
const missionBoard = queryShadow("mission-board");
expect(missionBoard).to.exist;
});
it("should close overlay when close event is dispatched", async () => {
element.activeOverlay = "MISSIONS";
await waitForUpdate();
// Simulate close event
const closeEvent = new CustomEvent("close", { bubbles: true, composed: true });
element.dispatchEvent(closeEvent);
await waitForUpdate();
expect(element.activeOverlay).to.equal("NONE");
});
it("should close overlay when backdrop is clicked", async () => {
element.activeOverlay = "BARRACKS";
await waitForUpdate();
const backdrop = queryShadow(".overlay-backdrop");
expect(backdrop).to.exist;
backdrop.click();
await waitForUpdate();
expect(element.activeOverlay).to.equal("NONE");
});
it("should show different overlays for different types", async () => {
const overlayTypes = ["BARRACKS", "MISSIONS", "MARKET", "RESEARCH", "SYSTEM"];
for (const type of overlayTypes) {
element.activeOverlay = type;
await waitForUpdate();
const overlayContainer = queryShadow(".overlay-container.active");
expect(overlayContainer).to.exist;
expect(element.activeOverlay).to.equal(type);
}
});
});
describe("CoA 4: Mission Handoff", () => {
it("should dispatch request-team-builder event when mission is selected", async () => {
let eventDispatched = false;
let eventData = null;
window.addEventListener("request-team-builder", (e) => {
eventDispatched = true;
eventData = e.detail;
});
element.activeOverlay = "MISSIONS";
await waitForUpdate();
// Import MissionBoard
await import("../../src/ui/components/MissionBoard.js");
await waitForUpdate();
const missionBoard = queryShadow("mission-board");
expect(missionBoard).to.exist;
// Simulate mission selection
const missionEvent = new CustomEvent("mission-selected", {
detail: { missionId: "MISSION_TUTORIAL_01" },
bubbles: true,
composed: true,
});
missionBoard.dispatchEvent(missionEvent);
await waitForUpdate();
expect(eventDispatched).to.be.true;
expect(eventData.missionId).to.equal("MISSION_TUTORIAL_01");
expect(element.activeOverlay).to.equal("NONE");
});
});
describe("Unlock System", () => {
it("should unlock market after first mission", async () => {
mockMissionManager.completedMissions = new Set(["MISSION_TUTORIAL_01"]);
await waitForUpdate();
expect(element.unlocks.market).to.be.true;
expect(element.unlocks.research).to.be.false;
});
it("should unlock research after 3 missions", async () => {
mockMissionManager.completedMissions = new Set([
"MISSION_1",
"MISSION_2",
"MISSION_3",
]);
await waitForUpdate();
expect(element.unlocks.research).to.be.true;
});
it("should disable locked facilities in dock", async () => {
mockMissionManager.completedMissions = new Set(); // No missions completed
await waitForUpdate();
const marketButton = queryShadowAll(".dock-button")[2]; // MARKET is third button
expect(marketButton.hasAttribute("disabled")).to.be.true;
const researchButton = queryShadowAll(".dock-button")[3]; // RESEARCH is fourth button
expect(researchButton.hasAttribute("disabled")).to.be.true;
});
it("should hide market hotspot when locked", async () => {
mockMissionManager.completedMissions = new Set(); // No missions completed
await waitForUpdate();
const marketHotspot = queryShadow(".hotspot.market");
expect(marketHotspot.hasAttribute("hidden")).to.be.true;
});
});
describe("Roster Summary Display", () => {
it("should calculate roster summary correctly", async () => {
mockRosterManager.roster = [
{ id: "u1", status: "READY" },
{ id: "u2", status: "READY" },
{ id: "u3", status: "INJURED" },
{ id: "u4", status: "READY" },
];
mockRosterManager.getDeployableUnits.returns([
{ id: "u1", status: "READY" },
{ id: "u2", status: "READY" },
{ id: "u4", status: "READY" },
]);
await waitForUpdate();
expect(element.rosterSummary.total).to.equal(4);
expect(element.rosterSummary.ready).to.equal(3);
expect(element.rosterSummary.injured).to.equal(1);
});
});
describe("State Change Handling", () => {
it("should reload data when gamestate-changed event fires", async () => {
const initialShards = 100;
const runData1 = {
inventory: {
runStash: {
currency: { aetherShards: initialShards, ancientCores: 0 },
},
},
};
mockPersistence.loadRun.resolves(runData1);
await waitForUpdate();
expect(element.wallet.aetherShards).to.equal(initialShards);
// Change the data
const newShards = 200;
const runData2 = {
inventory: {
runStash: {
currency: { aetherShards: newShards, ancientCores: 0 },
},
},
};
mockPersistence.loadRun.resolves(runData2);
// Simulate state change
window.dispatchEvent(
new CustomEvent("gamestate-changed", {
detail: { oldState: "STATE_COMBAT", newState: "STATE_MAIN_MENU" },
})
);
await waitForUpdate();
expect(element.wallet.aetherShards).to.equal(newShards);
});
});
});