aether-shards/test/ui/hub-screen.test.js
Matthew Mone 5c335b4b3c Add HubScreen and MissionBoard components for campaign management
Introduce the HubScreen as the main interface for managing resources, units, and mission selection, integrating with the GameStateManager for dynamic data binding. Implement the MissionBoard component to display and select available missions, enhancing user interaction with mission details and selection logic. Update the GameStateManager to handle transitions between game states, ensuring a seamless experience for players. Add tests for HubScreen and MissionBoard to validate functionality and integration with the overall game architecture.
2025-12-31 10:49:26 -08:00

358 lines
11 KiB
JavaScript

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