aether-shards/test/ui/mission-board.test.js

725 lines
23 KiB
JavaScript
Raw Normal View History

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;
let originalMissionManager;
beforeEach(() => {
originalMissionManager = gameStateManager.missionManager;
// Mock MissionManager - set up BEFORE creating element
mockMissionManager = {
missionRegistry: new Map(),
completedMissions: new Set(),
_ensureMissionsLoaded: sinon.stub().resolves(), // Mock lazy loading
areProceduralMissionsUnlocked: sinon.stub().returns(false),
refreshProceduralMissions: sinon.stub(),
};
gameStateManager.missionManager = mockMissionManager;
container = document.createElement("div");
document.body.appendChild(container);
element = document.createElement("mission-board");
container.appendChild(element);
});
afterEach(() => {
gameStateManager.missionManager = originalMissionManager;
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);
// Wait for initial load to complete, then trigger a reload
await new Promise((resolve) => setTimeout(resolve, 100));
// Dispatch event to trigger reload
window.dispatchEvent(new CustomEvent("missions-updated"));
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 new Promise((resolve) => setTimeout(resolve, 100));
window.dispatchEvent(new CustomEvent("missions-updated"));
await waitForUpdate();
// Switch to completed tab
const completedTab = queryShadow(".tab-button:last-child");
if (completedTab) {
completedTab.click();
await waitForUpdate();
}
const missionCard = queryShadow(".mission-card");
expect(missionCard).to.exist;
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 new Promise((resolve) => setTimeout(resolve, 100));
window.dispatchEvent(new CustomEvent("missions-updated"));
await waitForUpdate();
// Switch to completed tab
const completedTab = queryShadow(".tab-button:last-child");
if (completedTab) {
completedTab.click();
await waitForUpdate();
}
const missionCard = queryShadow(".mission-card");
expect(missionCard).to.exist;
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 new Promise((resolve) => setTimeout(resolve, 100));
window.dispatchEvent(new CustomEvent("missions-updated"));
await waitForUpdate();
// Wait a bit more for the component to process the update
await new Promise((resolve) => setTimeout(resolve, 50));
await waitForUpdate();
// Check active tab - should show MISSION_02 (prerequisites met)
const activeCards = queryShadowAll(".missions-grid .mission-card");
expect(activeCards.length).to.equal(1);
const activeTitle = activeCards[0]
.querySelector(".mission-title")
?.textContent.trim();
expect(activeTitle).to.equal("Second Story");
// Check completed tab - should show MISSION_01 (completed)
const completedTab = queryShadow(".tab-button:last-child");
if (completedTab) {
completedTab.click();
await waitForUpdate();
const completedCards = queryShadowAll(".missions-grid .mission-card");
expect(completedCards.length).to.equal(1);
const completedTitle = completedCards[0]
.querySelector(".mission-title")
?.textContent.trim();
expect(completedTitle).to.equal("First 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");
});
});
});