import { expect } from "@esm-bundle/chai"; import sinon from "sinon"; // Import to register custom element import "../../src/ui/screens/BarracksScreen.js"; import { gameStateManager } from "../../src/core/GameStateManager.js"; import vanguardDef from "../../src/assets/data/classes/vanguard.json" with { type: "json", }; describe("UI: BarracksScreen", () => { let element; let container; let mockPersistence; let mockRosterManager; let mockHubStash; let mockGameLoop; beforeEach(async () => { // Set up mocks BEFORE creating the element // Create mock hub stash mockHubStash = { currency: { aetherShards: 1000, ancientCores: 0, }, }; // Create mock persistence mockPersistence = { loadRun: sinon.stub().resolves({ inventory: { runStash: { currency: { aetherShards: 500, ancientCores: 0, }, }, }, }), saveRoster: sinon.stub().resolves(), saveHubStash: sinon.stub().resolves(), }; // Create mock class registry const mockClassRegistry = new Map(); mockClassRegistry.set("CLASS_VANGUARD", vanguardDef); // Create mock game loop with class registry mockGameLoop = { classRegistry: mockClassRegistry, }; // Create mock roster with test units const testRoster = [ { id: "UNIT_1", name: "Valerius", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "READY", classMastery: { CLASS_VANGUARD: { level: 3, xp: 150, skillPoints: 2, unlockedNodes: [], }, }, history: { missions: 2, kills: 5 }, }, { id: "UNIT_2", name: "Aria", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "INJURED", currentHealth: 60, // Injured unit with stored HP classMastery: { CLASS_VANGUARD: { level: 2, xp: 80, skillPoints: 1, unlockedNodes: [], }, }, history: { missions: 1, kills: 2 }, }, { id: "UNIT_3", name: "Kael", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "READY", classMastery: { CLASS_VANGUARD: { level: 5, xp: 300, skillPoints: 3, unlockedNodes: [], }, }, history: { missions: 5, kills: 12 }, }, ]; // Create mock roster manager mockRosterManager = { roster: testRoster, rosterLimit: 12, getDeployableUnits: sinon.stub().returns(testRoster.filter((u) => u.status === "READY")), save: sinon.stub().returns({ roster: testRoster, graveyard: [], }), }; // Replace gameStateManager properties with mocks gameStateManager.persistence = mockPersistence; gameStateManager.rosterManager = mockRosterManager; gameStateManager.hubStash = mockHubStash; gameStateManager.gameLoop = mockGameLoop; // NOW create the element after mocks are set up container = document.createElement("div"); document.body.appendChild(container); element = document.createElement("barracks-screen"); container.appendChild(element); // Wait for element to be defined and connected await element.updateComplete; // Create mock hub stash mockHubStash = { currency: { aetherShards: 1000, ancientCores: 0, }, }; // Create mock persistence mockPersistence = { loadRun: sinon.stub().resolves({ inventory: { runStash: { currency: { aetherShards: 500, ancientCores: 0, }, }, }, }), saveRoster: sinon.stub().resolves(), saveHubStash: sinon.stub().resolves(), }; // Create mock class registry const mockClassRegistry = new Map(); mockClassRegistry.set("CLASS_VANGUARD", vanguardDef); // Create mock game loop with class registry mockGameLoop = { classRegistry: mockClassRegistry, }; // Create mock roster with test units const testRoster = [ { id: "UNIT_1", name: "Valerius", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "READY", classMastery: { CLASS_VANGUARD: { level: 3, xp: 150, skillPoints: 2, unlockedNodes: [], }, }, history: { missions: 2, kills: 5 }, }, { id: "UNIT_2", name: "Aria", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "INJURED", currentHealth: 60, // Injured unit with stored HP classMastery: { CLASS_VANGUARD: { level: 2, xp: 80, skillPoints: 1, unlockedNodes: [], }, }, history: { missions: 1, kills: 2 }, }, { id: "UNIT_3", name: "Kael", classId: "CLASS_VANGUARD", activeClassId: "CLASS_VANGUARD", status: "READY", classMastery: { CLASS_VANGUARD: { level: 5, xp: 300, skillPoints: 3, unlockedNodes: [], }, }, history: { missions: 5, kills: 12 }, }, ]; // Create mock roster manager mockRosterManager = { roster: testRoster, rosterLimit: 12, getDeployableUnits: sinon.stub().returns(testRoster.filter((u) => u.status === "READY")), save: sinon.stub().returns({ roster: testRoster, graveyard: [], }), }; // Replace gameStateManager properties with mocks gameStateManager.persistence = mockPersistence; gameStateManager.rosterManager = mockRosterManager; gameStateManager.hubStash = mockHubStash; gameStateManager.gameLoop = mockGameLoop; }); 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("CoA 1: Roster Synchronization", () => { it("should load roster from RosterManager on connectedCallback", async () => { // Ensure element is connected and roster is loaded await waitForUpdate(); // Give _loadRoster time to complete (it's synchronous but triggers update) await new Promise((resolve) => setTimeout(resolve, 50)); await waitForUpdate(); expect(element.units.length).to.equal(3); const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.equal(3); }); it("should display roster count correctly", async () => { await waitForUpdate(); const rosterCount = queryShadow(".roster-count"); expect(rosterCount).to.exist; expect(rosterCount.textContent).to.include("3/12"); }); it("should update roster when unit is dismissed", async () => { await waitForUpdate(); // Select a unit first const unitCards = queryShadowAll(".unit-card"); unitCards[0].click(); await waitForUpdate(); // Mock confirm to return true const originalConfirm = window.confirm; window.confirm = sinon.stub().returns(true); // Click dismiss button const dismissButton = queryShadow(".btn-danger"); expect(dismissButton).to.exist; dismissButton.click(); await waitForUpdate(); // Restore confirm window.confirm = originalConfirm; // Verify unit was removed const updatedCards = queryShadowAll(".unit-card"); expect(updatedCards).to.have.length(2); expect(mockRosterManager.roster).to.have.length(2); expect(mockPersistence.saveRoster.called).to.be.true; }); }); describe("CoA 2: Healing Transaction", () => { it("should calculate heal cost correctly", async () => { await waitForUpdate(); // Select injured unit const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); expect(injuredCard).to.exist; injuredCard.click(); await waitForUpdate(); // Check heal button exists and shows cost const healButton = queryShadow(".btn-primary"); expect(healButton).to.exist; expect(healButton.textContent).to.include("Treat Wounds"); }); it("should heal unit when heal button is clicked", async () => { await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); // Select injured unit (UNIT_2 with 60 HP) const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); expect(injuredCard).to.exist; injuredCard.click(); await waitForUpdate(); // Get the selected unit to check actual maxHp const selectedUnit = element._getSelectedUnit(); expect(selectedUnit).to.exist; const expectedCost = Math.ceil((selectedUnit.maxHp - selectedUnit.currentHp) * 0.5); const healButton = queryShadow(".btn-primary"); expect(healButton).to.exist; expect(healButton.textContent).to.include(expectedCost.toString()); // Click heal healButton.click(); await waitForUpdate(); // Verify unit was healed const injuredUnit = mockRosterManager.roster.find((u) => u.id === "UNIT_2"); expect(injuredUnit.currentHealth).to.equal(selectedUnit.maxHp); // Max HP expect(injuredUnit.status).to.equal("READY"); // Verify currency was deducted const expectedRemaining = 1000 - expectedCost; expect(mockHubStash.currency.aetherShards).to.equal(expectedRemaining); // Verify save was called expect(mockPersistence.saveRoster.called).to.be.true; expect(mockPersistence.saveHubStash.called).to.be.true; }); it("should disable heal button when unit has full HP", async () => { await waitForUpdate(); // Select healthy unit const unitCards = queryShadowAll(".unit-card"); const readyCard = Array.from(unitCards).find( (card) => !card.classList.contains("injured") ); readyCard.click(); await waitForUpdate(); // Check that heal button is disabled or shows "Full Health" const healButton = queryShadow(".btn:disabled"); expect(healButton).to.exist; expect(healButton.textContent).to.include("Full Health"); }); it("should show insufficient funds message when wallet is too low", async () => { // Set low wallet balance mockHubStash.currency.aetherShards = 10; element.wallet = { aetherShards: 10, ancientCores: 0 }; await waitForUpdate(); // Select injured unit const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); injuredCard.click(); await waitForUpdate(); // Check for insufficient funds message const healCost = queryShadow(".heal-cost"); expect(healCost).to.exist; expect(healCost.textContent).to.include("Insufficient funds"); // Heal button should be disabled const healButton = queryShadow(".btn-primary"); expect(healButton).to.exist; expect(healButton.disabled).to.be.true; }); it("should dispatch wallet-updated event after healing", async () => { await waitForUpdate(); // Wait for units to load let attempts = 0; while (element.units.length === 0 && attempts < 10) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; } let walletUpdatedEvent = null; const handler = (e) => { walletUpdatedEvent = e; }; window.addEventListener("wallet-updated", handler); // Select injured unit and heal const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); expect(injuredCard).to.exist; injuredCard.click(); await waitForUpdate(); const healButton = queryShadow(".btn-primary"); expect(healButton).to.exist; healButton.click(); await waitForUpdate(); // Wait for async event dispatch (Promise.resolve().then()) await new Promise((resolve) => setTimeout(resolve, 50)); // Wait for event dispatch attempts = 0; while (!walletUpdatedEvent && attempts < 20) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; } expect(walletUpdatedEvent).to.exist; expect(walletUpdatedEvent.detail.wallet).to.exist; expect(walletUpdatedEvent.detail.wallet.aetherShards).to.be.lessThan(1000); window.removeEventListener("wallet-updated", handler); }); }); describe("CoA 3: Navigation", () => { it("should dispatch open-character-sheet event when Inspect is clicked", async () => { await waitForUpdate(); // Wait for units to load let attempts = 0; while (element.units.length === 0 && attempts < 10) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; } let characterSheetEvent = null; const handler = (e) => { characterSheetEvent = e; }; window.addEventListener("open-character-sheet", handler); // Find UNIT_1 (Valerius) in the list const valeriusIndex = element.units.findIndex((u) => u.id === "UNIT_1"); expect(valeriusIndex).to.be.greaterThan(-1); // Select UNIT_1 const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.be.greaterThan(0); // Find the card that contains "Valerius" const valeriusCard = Array.from(unitCards).find((card) => card.textContent.includes("Valerius") ); expect(valeriusCard).to.exist; valeriusCard.click(); await waitForUpdate(); // Verify selection expect(element.selectedUnitId).to.equal("UNIT_1"); // Click inspect button (first action button) const actionButtons = queryShadowAll(".action-button"); expect(actionButtons.length).to.be.greaterThan(0); actionButtons[0].click(); await waitForUpdate(); // Wait for event attempts = 0; while (!characterSheetEvent && attempts < 20) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; } expect(characterSheetEvent).to.exist; expect(characterSheetEvent.detail.unitId).to.equal("UNIT_1"); expect(characterSheetEvent.detail.unit).to.exist; window.removeEventListener("open-character-sheet", handler); }); it("should maintain selection state when character sheet is opened", async () => { await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); // Find injured unit (UNIT_2) in the list const injuredUnitIndex = element.units.findIndex((u) => u.id === "UNIT_2"); expect(injuredUnitIndex).to.be.greaterThan(-1); // Select the injured unit const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); expect(injuredCard).to.exist; injuredCard.click(); await waitForUpdate(); expect(element.selectedUnitId).to.equal("UNIT_2"); // Open character sheet (doesn't change selection) const actionButtons = queryShadowAll(".action-button"); expect(actionButtons.length).to.be.greaterThan(0); actionButtons[0].click(); // Inspect button // Selection should remain expect(element.selectedUnitId).to.equal("UNIT_2"); }); }); describe("CoA 4: Selection Persistence", () => { it("should maintain selection when roster is re-sorted", async () => { await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); // Find injured unit (UNIT_2) and select it const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); expect(injuredCard).to.exist; injuredCard.click(); await waitForUpdate(); expect(element.selectedUnitId).to.equal("UNIT_2"); // Change sort const sortButtons = queryShadowAll(".sort-button"); expect(sortButtons.length).to.be.greaterThan(0); sortButtons[1].click(); // Name sort await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); // Selection should persist expect(element.selectedUnitId).to.equal("UNIT_2"); // Verify selected card is still highlighted const selectedCard = queryShadow(".unit-card.selected"); expect(selectedCard).to.exist; }); }); describe("Filtering", () => { it("should filter units by READY status", async () => { await waitForUpdate(); const filterButtons = queryShadowAll(".filter-button"); const readyFilter = Array.from(filterButtons).find((btn) => btn.textContent.includes("Ready") ); readyFilter.click(); await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); expect(unitCards).to.have.length(2); // Only READY units }); it("should filter units by INJURED status", async () => { await waitForUpdate(); const filterButtons = queryShadowAll(".filter-button"); const injuredFilter = Array.from(filterButtons).find((btn) => btn.textContent.includes("Injured") ); injuredFilter.click(); await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); expect(unitCards).to.have.length(1); // Only INJURED unit expect(unitCards[0].classList.contains("injured")).to.be.true; }); it("should show all units when ALL filter is selected", async () => { await waitForUpdate(); // First filter to INJURED const filterButtons = queryShadowAll(".filter-button"); const injuredFilter = Array.from(filterButtons).find((btn) => btn.textContent.includes("Injured") ); injuredFilter.click(); await waitForUpdate(); // Then switch to ALL const allFilter = Array.from(filterButtons).find((btn) => btn.textContent.includes("All") ); allFilter.click(); await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); expect(unitCards).to.have.length(3); // All units }); }); describe("Sorting", () => { it("should sort units by level (descending)", async () => { await waitForUpdate(); // Default sort should be LEVEL_DESC const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.be.greaterThan(0); // First unit should be highest level (UNIT_3, level 5) const firstCard = unitCards[0]; expect(firstCard.textContent).to.include("Kael"); }); it("should sort units by name (ascending)", async () => { await waitForUpdate(); const sortButtons = queryShadowAll(".sort-button"); const nameSort = Array.from(sortButtons).find((btn) => btn.textContent.includes("Name") ); nameSort.click(); await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.be.greaterThan(0); // First should be alphabetically first (Aria) expect(unitCards[0].textContent).to.include("Aria"); }); it("should sort units by HP (ascending)", async () => { await waitForUpdate(); const sortButtons = queryShadowAll(".sort-button"); const hpSort = Array.from(sortButtons).find((btn) => btn.textContent.includes("HP") ); hpSort.click(); await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.be.greaterThan(0); // First should be lowest HP (injured unit) expect(unitCards[0].classList.contains("injured")).to.be.true; }); }); describe("Unit Card Rendering", () => { it("should render unit cards with correct information", async () => { await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); const unitCards = queryShadowAll(".unit-card"); expect(unitCards.length).to.equal(3); // Check that all units are rendered (order may vary by sort) const allNames = Array.from(unitCards).map((card) => card.textContent); expect(allNames.some((text) => text.includes("Valerius"))).to.be.true; expect(allNames.some((text) => text.includes("Aria"))).to.be.true; expect(allNames.some((text) => text.includes("Kael"))).to.be.true; }); it("should show HP bar with correct percentage", async () => { await waitForUpdate(); // Find injured unit card const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); const hpBar = injuredCard.querySelector(".progress-bar-fill"); expect(hpBar).to.exist; // Should be around 50% (60/120) const width = hpBar.style.width; expect(width).to.include("%"); }); it("should highlight selected unit card", async () => { await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); unitCards[0].click(); await waitForUpdate(); const selectedCard = queryShadow(".unit-card.selected"); expect(selectedCard).to.exist; expect(selectedCard).to.equal(unitCards[0]); }); }); describe("Detail Sidebar", () => { it("should show empty state when no unit is selected", async () => { await waitForUpdate(); const emptyState = queryShadow(".empty-state"); expect(emptyState).to.exist; expect(emptyState.textContent).to.include("Select a unit"); }); it("should display unit details when unit is selected", async () => { await waitForUpdate(); await new Promise((resolve) => setTimeout(resolve, 50)); // Find Valerius unit const valeriusUnit = element.units.find((u) => u.name === "Valerius"); expect(valeriusUnit).to.exist; // Find and click the card for Valerius const unitCards = queryShadowAll(".unit-card"); const valeriusCard = Array.from(unitCards).find((card) => card.textContent.includes("Valerius") ); expect(valeriusCard).to.exist; valeriusCard.click(); await waitForUpdate(); const detailSidebar = queryShadow(".detail-sidebar"); expect(detailSidebar).to.exist; expect(detailSidebar.textContent).to.include("Valerius"); expect(detailSidebar.textContent).to.include("Level"); }); it("should show heal button for injured units", async () => { await waitForUpdate(); // Select injured unit const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); injuredCard.click(); await waitForUpdate(); const healButton = queryShadow(".btn-primary"); expect(healButton).to.exist; expect(healButton.textContent).to.include("Treat Wounds"); }); it("should show dismiss button", async () => { await waitForUpdate(); const unitCards = queryShadowAll(".unit-card"); unitCards[0].click(); await waitForUpdate(); const dismissButton = queryShadow(".btn-danger"); expect(dismissButton).to.exist; expect(dismissButton.textContent).to.include("Dismiss"); }); }); describe("Close Functionality", () => { it("should dispatch close-barracks event when close button is clicked", async () => { await waitForUpdate(); let closeEvent = null; element.addEventListener("close-barracks", (e) => { closeEvent = e; }); const closeButton = queryShadow(".btn-close"); expect(closeButton).to.exist; closeButton.click(); expect(closeEvent).to.exist; }); }); describe("HP Calculation", () => { it("should calculate maxHp from class definition and level", async () => { await waitForUpdate(); // UNIT_1 is level 3, should have base + (3-1) * growth // Vanguard base health: 120, growth: 10 // Expected: 120 + (3-1) * 10 = 140 const unitCards = queryShadowAll(".unit-card"); unitCards[0].click(); await waitForUpdate(); const detailSidebar = queryShadow(".detail-sidebar"); // Should show HP values expect(detailSidebar.textContent).to.match(/\d+\s*\/\s*\d+/); }); it("should use stored currentHealth if available", async () => { await waitForUpdate(); // UNIT_2 has currentHealth: 60 stored const unitCards = queryShadowAll(".unit-card"); const injuredCard = Array.from(unitCards).find((card) => card.classList.contains("injured") ); injuredCard.click(); await waitForUpdate(); const detailSidebar = queryShadow(".detail-sidebar"); // Should show 60 in HP display expect(detailSidebar.textContent).to.include("60"); }); }); });