aether-shards/test/ui/mission-board.test.js
Matthew Mone 2c86d674f4 Add mission debrief and procedural mission generation features
- Introduce the MissionDebrief component to display after-action reports, including XP, rewards, and squad status.
- Implement the MissionGenerator class to create procedural side missions, enhancing replayability and resource management.
- Update mission schema to include mission objects for INTERACT objectives, improving mission complexity.
- Enhance GameLoop and MissionManager to support new mission features and interactions.
- Add tests for MissionDebrief and MissionGenerator to ensure functionality and integration within the game architecture.
2026-01-01 16:08:54 -08:00

646 lines
20 KiB
JavaScript

import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
import { MissionBoard } from "../../src/ui/components/mission-board.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(),
_ensureMissionsLoaded: sinon.stub().resolves(), // Mock lazy loading
};
gameStateManager.missionManager = mockMissionManager;
});
afterEach(() => {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
});
// Helper to wait for LitElement update
async function waitForUpdate() {
await element.updateComplete;
// Give time for async operations to complete
await new Promise((resolve) => setTimeout(resolve, 50));
await element.updateComplete;
}
// 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;
});
});
describe("Mission Prerequisites", () => {
it("should show mission as available when no prerequisites", 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");
expect(missionCard).to.exist;
expect(missionCard.classList.contains("locked")).to.be.false;
});
it("should show mission as locked when prerequisites not met", async () => {
const mission1 = {
id: "MISSION_01",
type: "SIDE_QUEST",
config: { title: "First Mission", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "SIDE_QUEST",
config: {
title: "Second Mission",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
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 mission2Card = Array.from(missionCards).find((card) =>
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
);
expect(mission2Card).to.exist;
expect(mission2Card.classList.contains("locked")).to.be.true;
});
it("should show mission as available when prerequisites are met", async () => {
const mission1 = {
id: "MISSION_01",
type: "SIDE_QUEST",
config: { title: "First Mission", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "SIDE_QUEST",
config: {
title: "Second Mission",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
mockMissionManager.missionRegistry.set(mission1.id, mission1);
mockMissionManager.missionRegistry.set(mission2.id, mission2);
mockMissionManager.completedMissions.add("MISSION_01");
await waitForUpdate();
const missionCards = queryShadowAll(".mission-card");
const mission2Card = Array.from(missionCards).find((card) =>
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
);
expect(mission2Card).to.exist;
expect(mission2Card.classList.contains("locked")).to.be.false;
});
it("should display prerequisite requirements for locked missions", async () => {
const mission1 = {
id: "MISSION_01",
type: "SIDE_QUEST",
config: { title: "First Mission", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "SIDE_QUEST",
config: {
title: "Second Mission",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
mockMissionManager.missionRegistry.set(mission1.id, mission1);
mockMissionManager.missionRegistry.set(mission2.id, mission2);
await waitForUpdate();
const missionCards = queryShadowAll(".mission-card");
const mission2Card = Array.from(missionCards).find((card) =>
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
);
expect(mission2Card).to.exist;
expect(mission2Card.textContent).to.include("Requires");
expect(mission2Card.textContent).to.include("First Mission");
});
});
describe("Mission Visibility", () => {
it("should hide STORY missions when prerequisites not met", async () => {
const mission1 = {
id: "MISSION_01",
type: "STORY",
config: { title: "First Story", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "STORY",
config: {
title: "Second Story",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
mockMissionManager.missionRegistry.set(mission1.id, mission1);
mockMissionManager.missionRegistry.set(mission2.id, mission2);
await waitForUpdate();
const missionCards = queryShadowAll(".mission-card");
expect(missionCards.length).to.equal(1);
const titles = Array.from(missionCards).map((card) =>
card.querySelector(".mission-title")?.textContent.trim()
);
expect(titles).to.include("First Story");
expect(titles).to.not.include("Second Story");
});
it("should show SIDE_QUEST missions as locked when prerequisites not met", async () => {
const mission1 = {
id: "MISSION_01",
type: "SIDE_QUEST",
config: { title: "First Quest", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "SIDE_QUEST",
config: {
title: "Second Quest",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
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("First Quest");
expect(titles).to.include("Second Quest");
const mission2Card = Array.from(missionCards).find((card) =>
card.querySelector(".mission-title")?.textContent.includes("Second Quest")
);
expect(mission2Card.classList.contains("locked")).to.be.true;
});
it("should show STORY mission when prerequisites are met", async () => {
const mission1 = {
id: "MISSION_01",
type: "STORY",
config: { title: "First Story", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "STORY",
config: {
title: "Second Story",
description: "Test",
prerequisites: ["MISSION_01"],
},
rewards: {},
};
mockMissionManager.missionRegistry.set(mission1.id, mission1);
mockMissionManager.missionRegistry.set(mission2.id, mission2);
mockMissionManager.completedMissions.add("MISSION_01");
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("First Story");
expect(titles).to.include("Second Story");
});
it("should respect explicit visibility_when_locked setting", async () => {
const mission1 = {
id: "MISSION_01",
type: "STORY",
config: { title: "First Story", description: "Test" },
rewards: {},
};
const mission2 = {
id: "MISSION_02",
type: "STORY",
config: {
title: "Second Story",
description: "Test",
prerequisites: ["MISSION_01"],
visibility_when_locked: "locked", // Override default hidden behavior
},
rewards: {},
};
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("Second Story");
});
});
});