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); }); }); });