2026-01-01 04:11:00 +00:00
|
|
|
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 () => {
|
2026-01-02 00:08:54 +00:00
|
|
|
// 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
|
2026-01-01 04:11:00 +00:00
|
|
|
container = document.createElement("div");
|
|
|
|
|
document.body.appendChild(container);
|
|
|
|
|
element = document.createElement("barracks-screen");
|
|
|
|
|
container.appendChild(element);
|
|
|
|
|
|
2026-01-02 00:08:54 +00:00
|
|
|
// Wait for element to be defined and connected
|
2026-01-01 04:11:00 +00:00
|
|
|
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 () => {
|
2026-01-02 00:08:54 +00:00
|
|
|
// 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));
|
2026-01-01 04:11:00 +00:00
|
|
|
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();
|
|
|
|
|
|
2026-01-02 00:08:54 +00:00
|
|
|
// Wait for async event dispatch (Promise.resolve().then())
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
|
|
2026-01-01 04:11:00 +00:00
|
|
|
// 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");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|