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.
358 lines
11 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
|