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.
399 lines
12 KiB
JavaScript
399 lines
12 KiB
JavaScript
import { expect } from "@esm-bundle/chai";
|
|
import { MissionBoard } from "../../src/ui/components/MissionBoard.js";
|
|
import { gameStateManager } from "../../src/core/GameStateManager.js";
|
|
|
|
describe("UI: MissionBoard", () => {
|
|
let element;
|
|
let container;
|
|
let mockMissionManager;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
element = document.createElement("mission-board");
|
|
container.appendChild(element);
|
|
|
|
// Mock MissionManager
|
|
mockMissionManager = {
|
|
missionRegistry: new Map(),
|
|
completedMissions: new Set(),
|
|
};
|
|
|
|
gameStateManager.missionManager = mockMissionManager;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (container && container.parentNode) {
|
|
container.parentNode.removeChild(container);
|
|
}
|
|
});
|
|
|
|
// 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("Mission Display", () => {
|
|
it("should display registered missions", async () => {
|
|
const mission1 = {
|
|
id: "MISSION_TUTORIAL_01",
|
|
type: "TUTORIAL",
|
|
config: {
|
|
title: "Protocol: First Descent",
|
|
description: "Establish a foothold in the Rusting Wastes.",
|
|
difficulty_tier: 1,
|
|
},
|
|
rewards: {
|
|
guaranteed: {
|
|
xp: 100,
|
|
currency: {
|
|
aether_shards: 50,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const mission2 = {
|
|
id: "MISSION_01",
|
|
type: "STORY",
|
|
config: {
|
|
title: "The First Strike",
|
|
description: "Strike back at the enemy.",
|
|
recommended_level: 2,
|
|
},
|
|
rewards: {
|
|
guaranteed: {
|
|
xp: 200,
|
|
currency: {
|
|
aether_shards: 100,
|
|
ancient_cores: 1,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockMissionManager.missionRegistry.set(mission1.id, mission1);
|
|
mockMissionManager.missionRegistry.set(mission2.id, mission2);
|
|
|
|
await waitForUpdate();
|
|
|
|
const missionCards = queryShadowAll(".mission-card");
|
|
expect(missionCards.length).to.equal(2);
|
|
|
|
const titles = Array.from(missionCards).map((card) =>
|
|
card.querySelector(".mission-title")?.textContent.trim()
|
|
);
|
|
expect(titles).to.include("Protocol: First Descent");
|
|
expect(titles).to.include("The First Strike");
|
|
});
|
|
|
|
it("should show empty state when no missions available", async () => {
|
|
mockMissionManager.missionRegistry.clear();
|
|
await waitForUpdate();
|
|
|
|
const emptyState = queryShadow(".empty-state");
|
|
expect(emptyState).to.exist;
|
|
expect(emptyState.textContent).to.include("No missions available");
|
|
});
|
|
});
|
|
|
|
describe("Mission Card Details", () => {
|
|
it("should display mission type badge", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "STORY",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const typeBadge = missionCard.querySelector(".mission-type.STORY");
|
|
expect(typeBadge).to.exist;
|
|
expect(typeBadge.textContent).to.include("STORY");
|
|
});
|
|
|
|
it("should display mission description", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: {
|
|
title: "Test Mission",
|
|
description: "This is a test mission description.",
|
|
},
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const description = missionCard.querySelector(".mission-description");
|
|
expect(description).to.exist;
|
|
expect(description.textContent).to.include("This is a test mission description.");
|
|
});
|
|
|
|
it("should display difficulty information", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: {
|
|
title: "Test Mission",
|
|
difficulty_tier: 3,
|
|
},
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const difficulty = missionCard.querySelector(".difficulty");
|
|
expect(difficulty).to.exist;
|
|
expect(difficulty.textContent).to.include("Tier 3");
|
|
});
|
|
});
|
|
|
|
describe("Rewards Display", () => {
|
|
it("should display currency rewards", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {
|
|
guaranteed: {
|
|
currency: {
|
|
aether_shards: 150,
|
|
ancient_cores: 2,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const rewards = missionCard.querySelector(".mission-rewards");
|
|
expect(rewards).to.exist;
|
|
expect(rewards.textContent).to.include("150");
|
|
expect(rewards.textContent).to.include("2");
|
|
});
|
|
|
|
it("should display XP rewards", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {
|
|
guaranteed: {
|
|
xp: 250,
|
|
},
|
|
},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const rewards = missionCard.querySelector(".mission-rewards");
|
|
expect(rewards).to.exist;
|
|
expect(rewards.textContent).to.include("250");
|
|
expect(rewards.textContent).to.include("XP");
|
|
});
|
|
|
|
it("should handle both snake_case and camelCase currency", async () => {
|
|
// Test snake_case (from JSON)
|
|
const mission1 = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test 1", description: "Test" },
|
|
rewards: {
|
|
guaranteed: {
|
|
currency: {
|
|
aether_shards: 100,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
// Test camelCase (from code)
|
|
const mission2 = {
|
|
id: "MISSION_02",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test 2", description: "Test" },
|
|
rewards: {
|
|
guaranteed: {
|
|
currency: {
|
|
aetherShards: 200,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockMissionManager.missionRegistry.set(mission1.id, mission1);
|
|
mockMissionManager.missionRegistry.set(mission2.id, mission2);
|
|
await waitForUpdate();
|
|
|
|
const missionCards = queryShadowAll(".mission-card");
|
|
expect(missionCards.length).to.equal(2);
|
|
|
|
// Both should display rewards correctly
|
|
const rewards1 = missionCards[0].querySelector(".mission-rewards");
|
|
const rewards2 = missionCards[1].querySelector(".mission-rewards");
|
|
expect(rewards1.textContent).to.include("100");
|
|
expect(rewards2.textContent).to.include("200");
|
|
});
|
|
});
|
|
|
|
describe("Completed Missions", () => {
|
|
it("should mark completed missions", async () => {
|
|
const mission = {
|
|
id: "MISSION_TUTORIAL_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
mockMissionManager.completedMissions.add(mission.id);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
expect(missionCard.classList.contains("completed")).to.be.true;
|
|
expect(missionCard.textContent).to.include("Completed");
|
|
});
|
|
|
|
it("should not show select button for completed missions", async () => {
|
|
const mission = {
|
|
id: "MISSION_TUTORIAL_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
mockMissionManager.completedMissions.add(mission.id);
|
|
await waitForUpdate();
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
const selectButton = missionCard.querySelector(".select-button");
|
|
expect(selectButton).to.be.null;
|
|
});
|
|
});
|
|
|
|
describe("Mission Selection", () => {
|
|
it("should dispatch mission-selected event when mission is clicked", async () => {
|
|
const mission = {
|
|
id: "MISSION_TUTORIAL_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
let eventDispatched = false;
|
|
let eventData = null;
|
|
|
|
element.addEventListener("mission-selected", (e) => {
|
|
eventDispatched = true;
|
|
eventData = e.detail;
|
|
});
|
|
|
|
const missionCard = queryShadow(".mission-card");
|
|
missionCard.click();
|
|
await waitForUpdate();
|
|
|
|
expect(eventDispatched).to.be.true;
|
|
expect(eventData.missionId).to.equal("MISSION_TUTORIAL_01");
|
|
});
|
|
|
|
it("should dispatch mission-selected event when select button is clicked", async () => {
|
|
const mission = {
|
|
id: "MISSION_TUTORIAL_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
let eventDispatched = false;
|
|
let eventData = null;
|
|
|
|
element.addEventListener("mission-selected", (e) => {
|
|
eventDispatched = true;
|
|
eventData = e.detail;
|
|
});
|
|
|
|
const selectButton = queryShadow(".select-button");
|
|
expect(selectButton).to.exist;
|
|
|
|
selectButton.click();
|
|
await waitForUpdate();
|
|
|
|
expect(eventDispatched).to.be.true;
|
|
expect(eventData.missionId).to.equal("MISSION_TUTORIAL_01");
|
|
});
|
|
});
|
|
|
|
describe("Close Button", () => {
|
|
it("should dispatch close event when close button is clicked", async () => {
|
|
const mission = {
|
|
id: "MISSION_01",
|
|
type: "TUTORIAL",
|
|
config: { title: "Test Mission", description: "Test" },
|
|
rewards: {},
|
|
};
|
|
mockMissionManager.missionRegistry.set(mission.id, mission);
|
|
await waitForUpdate();
|
|
|
|
let closeEventDispatched = false;
|
|
|
|
element.addEventListener("close", () => {
|
|
closeEventDispatched = true;
|
|
});
|
|
|
|
const closeButton = queryShadow(".close-button");
|
|
expect(closeButton).to.exist;
|
|
|
|
closeButton.click();
|
|
await waitForUpdate();
|
|
|
|
expect(closeEventDispatched).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe("Mission Type Styling", () => {
|
|
it("should apply correct styling for different mission types", async () => {
|
|
const missions = [
|
|
{ id: "M1", type: "STORY", config: { title: "Story", description: "Test" }, rewards: {} },
|
|
{ id: "M2", type: "SIDE_QUEST", config: { title: "Side", description: "Test" }, rewards: {} },
|
|
{ id: "M3", type: "TUTORIAL", config: { title: "Tutorial", description: "Test" }, rewards: {} },
|
|
{ id: "M4", type: "PROCEDURAL", config: { title: "Proc", description: "Test" }, rewards: {} },
|
|
];
|
|
|
|
missions.forEach((m) => mockMissionManager.missionRegistry.set(m.id, m));
|
|
await waitForUpdate();
|
|
|
|
const missionCards = queryShadowAll(".mission-card");
|
|
expect(missionCards.length).to.equal(4);
|
|
|
|
const typeBadges = Array.from(missionCards).map((card) =>
|
|
card.querySelector(".mission-type")
|
|
);
|
|
|
|
expect(typeBadges[0].classList.contains("STORY")).to.be.true;
|
|
expect(typeBadges[1].classList.contains("SIDE_QUEST")).to.be.true;
|
|
expect(typeBadges[2].classList.contains("TUTORIAL")).to.be.true;
|
|
expect(typeBadges[3].classList.contains("PROCEDURAL")).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
|