Add comprehensive tests for the InventoryManager and InventoryContainer to validate item management functionalities. Implement integration tests for the CharacterSheet component, ensuring proper interaction with the inventory system. Update the Explorer class to support new inventory features and maintain backward compatibility. Refactor related components for improved clarity and performance.
905 lines
28 KiB
JavaScript
905 lines
28 KiB
JavaScript
import { expect } from "@esm-bundle/chai";
|
|
import { CharacterSheet } from "../../src/ui/components/CharacterSheet.js";
|
|
import { Explorer } from "../../src/units/Explorer.js";
|
|
import { Item } from "../../src/items/Item.js";
|
|
import vanguardDef from "../../src/assets/data/classes/vanguard.json" with {
|
|
type: "json",
|
|
};
|
|
|
|
// Import SkillTreeUI to register the custom element
|
|
import "../../src/ui/components/SkillTreeUI.js";
|
|
|
|
describe("UI: CharacterSheet", () => {
|
|
let element;
|
|
let container;
|
|
let testUnit;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
element = document.createElement("character-sheet");
|
|
container.appendChild(element);
|
|
|
|
// Create a test Explorer unit
|
|
testUnit = new Explorer("test-unit-1", "Test Vanguard", "CLASS_VANGUARD", vanguardDef);
|
|
testUnit.classMastery["CLASS_VANGUARD"] = {
|
|
level: 5,
|
|
xp: 250,
|
|
skillPoints: 2,
|
|
unlockedNodes: [],
|
|
};
|
|
testUnit.recalculateBaseStats(vanguardDef);
|
|
testUnit.currentHealth = 100;
|
|
testUnit.maxHealth = 120;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (container.parentNode) {
|
|
container.parentNode.removeChild(container);
|
|
}
|
|
});
|
|
|
|
// Helper to wait for LitElement update
|
|
async function waitForUpdate() {
|
|
await element.updateComplete;
|
|
// Give a small delay for DOM updates
|
|
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: Stat Rendering", () => {
|
|
it("should render stats with effective values", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
// Check that stat values are displayed
|
|
const statValues = queryShadowAll('.stat-value');
|
|
expect(statValues.length).to.be.greaterThan(0);
|
|
});
|
|
|
|
it("should show stat breakdown in tooltip on hover", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const statItem = queryShadow(".stat-item");
|
|
expect(statItem).to.exist;
|
|
|
|
const tooltip = statItem.querySelector(".tooltip");
|
|
expect(tooltip).to.exist;
|
|
});
|
|
|
|
it("should display debuffed stats in red", async () => {
|
|
testUnit.statusEffects = [
|
|
{
|
|
id: "WEAKNESS",
|
|
name: "Weakness",
|
|
statModifiers: { attack: -5 },
|
|
},
|
|
];
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const attackStat = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Attack")
|
|
);
|
|
expect(attackStat).to.exist;
|
|
expect(attackStat.classList.contains("debuffed")).to.be.true;
|
|
|
|
const statValue = attackStat.querySelector(".stat-value");
|
|
expect(statValue.classList.contains("debuffed")).to.be.true;
|
|
});
|
|
|
|
it("should display buffed stats in green", async () => {
|
|
testUnit.statusEffects = [
|
|
{
|
|
id: "STRENGTH",
|
|
name: "Strength",
|
|
statModifiers: { attack: +5 },
|
|
},
|
|
];
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const attackStat = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Attack")
|
|
);
|
|
expect(attackStat).to.exist;
|
|
expect(attackStat.classList.contains("buffed")).to.be.true;
|
|
|
|
const statValue = attackStat.querySelector(".stat-value");
|
|
expect(statValue.classList.contains("buffed")).to.be.true;
|
|
});
|
|
|
|
it("should calculate effective stats from base + equipment + buffs", async () => {
|
|
const weapon = new Item({
|
|
id: "ITEM_TEST_SWORD",
|
|
name: "Test Sword",
|
|
type: "WEAPON",
|
|
stats: { attack: 10 },
|
|
});
|
|
|
|
// Reset to level 1 to get base stats
|
|
testUnit.classMastery["CLASS_VANGUARD"].level = 1;
|
|
testUnit.recalculateBaseStats(vanguardDef);
|
|
|
|
testUnit.equipment.weapon = weapon;
|
|
testUnit.statusEffects = [
|
|
{
|
|
id: "BUFF",
|
|
name: "Power Boost",
|
|
statModifiers: { attack: 3 },
|
|
},
|
|
];
|
|
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
// Base attack from vanguard level 1 is 12, +10 from weapon, +3 from buff = 25
|
|
const attackStat = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Attack")
|
|
);
|
|
const statValue = attackStat.querySelector(".stat-value");
|
|
const totalValue = parseInt(statValue.textContent.trim());
|
|
expect(totalValue).to.equal(25);
|
|
});
|
|
|
|
it("should display health bar with current/max values", async () => {
|
|
testUnit.currentHealth = 80;
|
|
testUnit.maxHealth = 120;
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const healthBar = queryShadow(".health-bar-container");
|
|
expect(healthBar).to.exist;
|
|
|
|
const healthLabel = queryShadow(".health-label");
|
|
expect(healthLabel.textContent).to.include("80");
|
|
expect(healthLabel.textContent).to.include("120");
|
|
});
|
|
|
|
it("should display AP icons based on speed", async () => {
|
|
// Speed = 8, so AP = 3 + floor(8/5) = 3 + 1 = 4
|
|
testUnit.baseStats.speed = 8;
|
|
testUnit.currentAP = 3;
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
// Find the AP stat item (which shows AP icons)
|
|
const apStat = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("AP")
|
|
);
|
|
expect(apStat).to.exist;
|
|
|
|
const apIcons = apStat.querySelectorAll(".ap-icon");
|
|
expect(apIcons.length).to.equal(4); // Max AP
|
|
|
|
const emptyIcons = apStat.querySelectorAll(".ap-icon.empty");
|
|
expect(emptyIcons.length).to.equal(1); // One empty (4 total - 3 used)
|
|
});
|
|
});
|
|
|
|
describe("CoA 2: Equipment Swapping", () => {
|
|
it("should show equipment slots in paper doll", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
// Use the new class names (mainHand, offHand, body, accessory)
|
|
const weaponSlot = queryShadow(".equipment-slot.mainHand");
|
|
const armorSlot = queryShadow(".equipment-slot.body");
|
|
const relicSlot = queryShadow(".equipment-slot.accessory");
|
|
const utilitySlot = queryShadow(".equipment-slot.offHand");
|
|
|
|
expect(weaponSlot).to.exist;
|
|
expect(armorSlot).to.exist;
|
|
expect(relicSlot).to.exist;
|
|
expect(utilitySlot).to.exist;
|
|
});
|
|
|
|
it("should show ghost icon for empty slots", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const weaponSlot = queryShadow(".equipment-slot.mainHand");
|
|
const slotIcon = weaponSlot.querySelector(".slot-icon");
|
|
expect(slotIcon).to.exist;
|
|
});
|
|
|
|
it("should show item icon for equipped items", async () => {
|
|
// Use the new loadout system
|
|
testUnit.loadout.mainHand = {
|
|
uid: "ITEM_TEST_SWORD_1",
|
|
defId: "ITEM_TEST_SWORD",
|
|
isNew: false,
|
|
quantity: 1,
|
|
};
|
|
|
|
// Mock inventoryManager with item registry
|
|
const mockItemRegistry = new Map();
|
|
mockItemRegistry.set("ITEM_TEST_SWORD", {
|
|
id: "ITEM_TEST_SWORD",
|
|
name: "Test Sword",
|
|
type: "WEAPON",
|
|
icon: "⚔️",
|
|
});
|
|
|
|
element.unit = testUnit;
|
|
element.inventoryManager = {
|
|
itemRegistry: mockItemRegistry,
|
|
};
|
|
await waitForUpdate();
|
|
|
|
const weaponSlot = queryShadow(".equipment-slot.mainHand");
|
|
const itemIcon = weaponSlot.querySelector(".item-icon");
|
|
expect(itemIcon).to.exist;
|
|
});
|
|
|
|
it("should switch to inventory tab when slot is clicked", async () => {
|
|
element.unit = testUnit;
|
|
element.inventory = [];
|
|
await waitForUpdate();
|
|
|
|
const weaponSlot = queryShadow(".equipment-slot.mainHand");
|
|
weaponSlot.click();
|
|
await waitForUpdate();
|
|
|
|
expect(element.activeTab).to.equal("INVENTORY");
|
|
expect(element.selectedSlot).to.equal("MAIN_HAND");
|
|
});
|
|
|
|
it("should filter inventory by slot type when slot is selected", async () => {
|
|
const weapon1 = new Item({
|
|
id: "ITEM_SWORD",
|
|
name: "Sword",
|
|
type: "WEAPON",
|
|
});
|
|
const weapon2 = new Item({
|
|
id: "ITEM_AXE",
|
|
name: "Axe",
|
|
type: "WEAPON",
|
|
});
|
|
const armor = new Item({
|
|
id: "ITEM_PLATE",
|
|
name: "Plate",
|
|
type: "ARMOR",
|
|
});
|
|
|
|
element.unit = testUnit;
|
|
element.inventory = [weapon1, weapon2, armor];
|
|
element.selectedSlot = "WEAPON";
|
|
await waitForUpdate();
|
|
|
|
const itemCards = queryShadowAll(".item-card");
|
|
expect(itemCards.length).to.equal(2); // Only weapons
|
|
});
|
|
|
|
it("should equip item when clicked in inventory", async () => {
|
|
const weapon = new Item({
|
|
id: "ITEM_SWORD",
|
|
name: "Sword",
|
|
type: "WEAPON",
|
|
stats: { attack: 10 },
|
|
});
|
|
const oldWeapon = new Item({
|
|
id: "ITEM_OLD_SWORD",
|
|
name: "Old Sword",
|
|
type: "WEAPON",
|
|
});
|
|
|
|
testUnit.equipment.weapon = oldWeapon;
|
|
element.unit = testUnit;
|
|
element.inventory = [weapon];
|
|
element.selectedSlot = "WEAPON";
|
|
|
|
let equipEventFired = false;
|
|
let equipEventDetail = null;
|
|
element.addEventListener("equip-item", (e) => {
|
|
equipEventFired = true;
|
|
equipEventDetail = e.detail;
|
|
});
|
|
|
|
await waitForUpdate();
|
|
|
|
const itemCard = queryShadow(".item-card");
|
|
itemCard.click();
|
|
await waitForUpdate();
|
|
|
|
expect(equipEventFired).to.be.true;
|
|
expect(equipEventDetail.unitId).to.equal(testUnit.id);
|
|
expect(equipEventDetail.slot).to.equal("WEAPON");
|
|
expect(equipEventDetail.item.id).to.equal("ITEM_SWORD");
|
|
expect(equipEventDetail.oldItem.id).to.equal("ITEM_OLD_SWORD");
|
|
|
|
// Old item should be in inventory
|
|
expect(element.inventory.some((item) => item.id === "ITEM_OLD_SWORD")).to.be
|
|
.true;
|
|
// New item should be equipped
|
|
expect(testUnit.equipment.weapon.id).to.equal("ITEM_SWORD");
|
|
});
|
|
|
|
it("should update stats immediately after equipping", async () => {
|
|
const weapon = new Item({
|
|
id: "ITEM_SWORD",
|
|
name: "Sword",
|
|
type: "WEAPON",
|
|
stats: { attack: 15 },
|
|
});
|
|
|
|
element.unit = testUnit;
|
|
element.inventory = [weapon];
|
|
element.selectedSlot = "WEAPON";
|
|
await waitForUpdate();
|
|
|
|
const initialAttack = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Attack")
|
|
);
|
|
const initialValue = parseInt(
|
|
initialAttack.querySelector(".stat-value").textContent.trim()
|
|
);
|
|
|
|
const itemCard = queryShadow(".item-card");
|
|
itemCard.click();
|
|
await waitForUpdate();
|
|
|
|
const updatedAttack = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Attack")
|
|
);
|
|
const updatedValue = parseInt(
|
|
updatedAttack.querySelector(".stat-value").textContent.trim()
|
|
);
|
|
|
|
expect(updatedValue).to.equal(initialValue + 15);
|
|
});
|
|
|
|
it("should not allow equipping in read-only mode", async () => {
|
|
element.unit = testUnit;
|
|
element.readOnly = true;
|
|
element.inventory = [
|
|
new Item({
|
|
id: "ITEM_SWORD",
|
|
name: "Sword",
|
|
type: "WEAPON",
|
|
}),
|
|
];
|
|
element.selectedSlot = "MAIN_HAND";
|
|
await waitForUpdate();
|
|
|
|
const weaponSlot = queryShadow(".equipment-slot.mainHand");
|
|
expect(weaponSlot.hasAttribute("disabled")).to.be.true;
|
|
|
|
const itemCard = queryShadow(".item-card");
|
|
const initialWeapon = testUnit.equipment.weapon;
|
|
itemCard.click();
|
|
await waitForUpdate();
|
|
|
|
// Equipment should not have changed
|
|
expect(testUnit.equipment.weapon).to.equal(initialWeapon);
|
|
});
|
|
});
|
|
|
|
describe("CoA 3: Skill Interaction", () => {
|
|
it("should display skill tree tab", async () => {
|
|
element.unit = testUnit;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
const skillsTab = Array.from(queryShadowAll(".tab-button")).find((btn) =>
|
|
btn.textContent.includes("Skills")
|
|
);
|
|
expect(skillsTab).to.exist;
|
|
|
|
const skillsContainer = queryShadow(".skills-container");
|
|
expect(skillsContainer).to.exist;
|
|
});
|
|
|
|
it("should embed skill-tree-ui component", async () => {
|
|
element.unit = testUnit;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
const skillTree = queryShadow("skill-tree-ui");
|
|
expect(skillTree).to.exist;
|
|
expect(skillTree.unit).to.equal(testUnit);
|
|
});
|
|
|
|
it("should display SP badge when skill points are available", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 3;
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const spBadge = queryShadow(".sp-badge");
|
|
expect(spBadge).to.exist;
|
|
expect(spBadge.textContent).to.include("SP: 3");
|
|
});
|
|
|
|
it("should not display SP badge when no skill points", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 0;
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const spBadge = queryShadow(".sp-badge");
|
|
expect(spBadge).to.be.null;
|
|
});
|
|
|
|
it("should handle unlock-request and update unit stats", async () => {
|
|
// Set up unit with skill points and mock recalculateStats
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 2;
|
|
testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];
|
|
testUnit.maxHealth = 100;
|
|
testUnit.currentHealth = 100;
|
|
|
|
let recalculateStatsCalled = false;
|
|
let recalculateStatsArgs = null;
|
|
testUnit.recalculateStats = (itemRegistry, treeDef) => {
|
|
recalculateStatsCalled = true;
|
|
recalculateStatsArgs = { itemRegistry, treeDef };
|
|
// Simulate stat boost application
|
|
testUnit.maxHealth = 110; // Base 100 + 10 from health boost
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
// Create mock tree definition
|
|
const mockTreeDef = {
|
|
id: "TREE_TEST",
|
|
nodes: {
|
|
ROOT: {
|
|
id: "ROOT",
|
|
tier: 1,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 10 },
|
|
req: 1,
|
|
cost: 1,
|
|
},
|
|
},
|
|
};
|
|
element.treeDef = mockTreeDef;
|
|
await waitForUpdate();
|
|
|
|
// Call the handler directly (since it's a private method, we'll simulate the event)
|
|
const unlockEvent = new CustomEvent("unlock-request", {
|
|
detail: { nodeId: "ROOT", cost: 1 },
|
|
bubbles: true,
|
|
composed: true,
|
|
});
|
|
// Simulate the event being handled by calling the method directly
|
|
element._handleUnlockRequest(unlockEvent);
|
|
await waitForUpdate();
|
|
|
|
// Verify node was unlocked
|
|
expect(testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes).to.include("ROOT");
|
|
expect(testUnit.classMastery["CLASS_VANGUARD"].skillPoints).to.equal(1);
|
|
|
|
// Verify recalculateStats was called with correct args
|
|
expect(recalculateStatsCalled).to.be.true;
|
|
expect(recalculateStatsArgs.treeDef).to.exist;
|
|
});
|
|
|
|
it("should dispatch skill-unlocked event after unlocking", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 2;
|
|
testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];
|
|
testUnit.recalculateStats = () => {}; // Mock function
|
|
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
let skillUnlockedEventFired = false;
|
|
let skillUnlockedEventDetail = null;
|
|
element.addEventListener("skill-unlocked", (e) => {
|
|
skillUnlockedEventFired = true;
|
|
skillUnlockedEventDetail = e.detail;
|
|
});
|
|
|
|
const unlockEvent = new CustomEvent("unlock-request", {
|
|
detail: { nodeId: "ROOT", cost: 1 },
|
|
bubbles: true,
|
|
composed: true,
|
|
});
|
|
// Call the handler directly
|
|
element._handleUnlockRequest(unlockEvent);
|
|
await waitForUpdate();
|
|
|
|
expect(skillUnlockedEventFired).to.be.true;
|
|
expect(skillUnlockedEventDetail.unitId).to.equal(testUnit.id);
|
|
expect(skillUnlockedEventDetail.nodeId).to.equal("ROOT");
|
|
expect(skillUnlockedEventDetail.cost).to.equal(1);
|
|
});
|
|
|
|
it("should update SkillTreeUI after unlocking", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 2;
|
|
testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];
|
|
testUnit.recalculateStats = () => {}; // Mock function
|
|
|
|
element.unit = testUnit;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
const skillTree = queryShadow("skill-tree-ui");
|
|
expect(skillTree).to.exist;
|
|
|
|
const initialUpdateTrigger = skillTree.updateTrigger || 0;
|
|
|
|
const unlockEvent = new CustomEvent("unlock-request", {
|
|
detail: { nodeId: "ROOT", cost: 1 },
|
|
bubbles: true,
|
|
composed: true,
|
|
});
|
|
// Call the handler directly
|
|
element._handleUnlockRequest(unlockEvent);
|
|
|
|
// Wait for setTimeout to execute
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
await waitForUpdate();
|
|
|
|
// Verify updateTrigger was incremented
|
|
expect(skillTree.updateTrigger).to.be.greaterThan(initialUpdateTrigger);
|
|
});
|
|
|
|
it("should include skill tree stat boosts in stat breakdown", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
|
|
testUnit.baseStats.health = 100;
|
|
testUnit.maxHealth = 100;
|
|
|
|
// Create mock tree definition with health boost
|
|
const mockTreeDef = {
|
|
id: "TREE_TEST",
|
|
nodes: {
|
|
ROOT: {
|
|
id: "ROOT",
|
|
tier: 1,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 10 },
|
|
req: 1,
|
|
cost: 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.treeDef = mockTreeDef;
|
|
await waitForUpdate();
|
|
|
|
// Get health stat breakdown - health uses a health bar, not stat-value
|
|
const healthStat = Array.from(queryShadowAll(".stat-item")).find((item) =>
|
|
item.textContent.includes("Health")
|
|
);
|
|
expect(healthStat).to.exist;
|
|
|
|
// Health shows as "current / max" in the health label
|
|
const healthLabel = healthStat.querySelector(".health-label");
|
|
if (healthLabel) {
|
|
// Health bar shows current/max, so we check the max value
|
|
const healthText = healthLabel.textContent;
|
|
const match = healthText.match(/\/(\d+)/);
|
|
if (match) {
|
|
const maxHealth = parseInt(match[1]);
|
|
// Should be 100 base + 10 boost = 110
|
|
expect(maxHealth).to.equal(110);
|
|
}
|
|
} else {
|
|
// Fallback: check if the breakdown tooltip would show the boost
|
|
// This test verifies the calculation happens, even if we can't easily test the UI
|
|
const { total } = element._getEffectiveStat("health");
|
|
expect(total).to.equal(110);
|
|
}
|
|
});
|
|
|
|
describe("30-Node Skill Tree Integration", () => {
|
|
it("should receive and pass full 30-node tree to SkillTreeUI", async () => {
|
|
// Create a mock 30-node tree (simplified version)
|
|
const mock30NodeTree = {
|
|
id: "TREE_CLASS_VANGUARD",
|
|
nodes: {},
|
|
};
|
|
|
|
// Generate 30 node IDs
|
|
for (let i = 1; i <= 30; i++) {
|
|
mock30NodeTree.nodes[`NODE_${i}`] = {
|
|
id: `NODE_${i}`,
|
|
tier: Math.ceil(i / 6),
|
|
type: i % 3 === 0 ? "STAT_BOOST" : "ACTIVE_SKILL",
|
|
data: i % 3 === 0
|
|
? { stat: "health", value: i }
|
|
: { id: `SKILL_${i}`, name: `Skill ${i}` },
|
|
req: Math.ceil(i / 6),
|
|
cost: Math.ceil(i / 10),
|
|
children: i < 30 ? [`NODE_${i + 1}`] : [],
|
|
};
|
|
}
|
|
|
|
element.unit = testUnit;
|
|
element.treeDef = mock30NodeTree;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
const skillTree = queryShadow("skill-tree-ui");
|
|
expect(skillTree).to.exist;
|
|
expect(skillTree.treeDef).to.exist;
|
|
expect(skillTree.treeDef.id).to.equal("TREE_CLASS_VANGUARD");
|
|
expect(Object.keys(skillTree.treeDef.nodes)).to.have.length(30);
|
|
});
|
|
|
|
it("should handle treeDef with all node types from template", async () => {
|
|
const mockFullTree = {
|
|
id: "TREE_TEST",
|
|
nodes: {
|
|
NODE_T1_1: {
|
|
tier: 1,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 2 },
|
|
req: 1,
|
|
cost: 1,
|
|
children: ["NODE_T2_1", "NODE_T2_2"],
|
|
},
|
|
NODE_T2_1: {
|
|
tier: 2,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "defense", value: 2 },
|
|
req: 2,
|
|
cost: 1,
|
|
children: ["NODE_T3_1"],
|
|
},
|
|
NODE_T2_2: {
|
|
tier: 2,
|
|
type: "ACTIVE_SKILL",
|
|
data: { id: "SKILL_1", name: "Shield Bash" },
|
|
req: 2,
|
|
cost: 1,
|
|
children: ["NODE_T3_2"],
|
|
},
|
|
NODE_T3_1: {
|
|
tier: 3,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 6 },
|
|
req: 3,
|
|
cost: 1,
|
|
children: [],
|
|
},
|
|
NODE_T3_2: {
|
|
tier: 3,
|
|
type: "ACTIVE_SKILL",
|
|
data: { id: "SKILL_2", name: "Taunt" },
|
|
req: 3,
|
|
cost: 1,
|
|
children: [],
|
|
},
|
|
NODE_T4_1: {
|
|
tier: 4,
|
|
type: "ACTIVE_SKILL",
|
|
data: { id: "SKILL_3", name: "Skill 3" },
|
|
req: 4,
|
|
cost: 2,
|
|
children: [],
|
|
},
|
|
NODE_T4_2: {
|
|
tier: 4,
|
|
type: "PASSIVE_ABILITY",
|
|
data: { effect_id: "PASSIVE_1", name: "Passive 1" },
|
|
req: 4,
|
|
cost: 2,
|
|
children: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.treeDef = mockFullTree;
|
|
element.activeTab = "SKILLS";
|
|
await waitForUpdate();
|
|
|
|
const skillTree = queryShadow("skill-tree-ui");
|
|
expect(skillTree).to.exist;
|
|
expect(skillTree.treeDef).to.equal(mockFullTree);
|
|
|
|
// Verify all node types are present
|
|
const nodes = skillTree.treeDef.nodes;
|
|
expect(nodes.NODE_T1_1.type).to.equal("STAT_BOOST");
|
|
expect(nodes.NODE_T2_2.type).to.equal("ACTIVE_SKILL");
|
|
expect(nodes.NODE_T4_2.type).to.equal("PASSIVE_ABILITY");
|
|
});
|
|
|
|
it("should use treeDef in _getTreeDefinition method", async () => {
|
|
const mockTree = {
|
|
id: "TREE_TEST",
|
|
nodes: {
|
|
NODE_1: {
|
|
tier: 1,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 2 },
|
|
req: 1,
|
|
cost: 1,
|
|
children: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.treeDef = mockTree;
|
|
await waitForUpdate();
|
|
|
|
const treeDef = element._getTreeDefinition();
|
|
expect(treeDef).to.equal(mockTree);
|
|
expect(treeDef.id).to.equal("TREE_TEST");
|
|
});
|
|
|
|
it("should pass treeDef to recalculateStats when unlocking nodes", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 2;
|
|
testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];
|
|
testUnit.recalculateStats = () => {}; // Mock function
|
|
|
|
const mockTree = {
|
|
id: "TREE_TEST",
|
|
nodes: {
|
|
NODE_1: {
|
|
tier: 1,
|
|
type: "STAT_BOOST",
|
|
data: { stat: "health", value: 10 },
|
|
req: 1,
|
|
cost: 1,
|
|
children: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
let recalculateStatsCalledWith = null;
|
|
testUnit.recalculateStats = (itemRegistry, treeDef) => {
|
|
recalculateStatsCalledWith = { itemRegistry, treeDef };
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.treeDef = mockTree;
|
|
await waitForUpdate();
|
|
|
|
const unlockEvent = new CustomEvent("unlock-request", {
|
|
detail: { nodeId: "NODE_1", cost: 1 },
|
|
bubbles: true,
|
|
composed: true,
|
|
});
|
|
element._handleUnlockRequest(unlockEvent);
|
|
await waitForUpdate();
|
|
|
|
expect(recalculateStatsCalledWith).to.exist;
|
|
expect(recalculateStatsCalledWith.treeDef).to.equal(mockTree);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CoA 4: Context Awareness", () => {
|
|
it("should display inventory tab", async () => {
|
|
element.unit = testUnit;
|
|
element.inventory = [];
|
|
element.activeTab = "INVENTORY";
|
|
await waitForUpdate();
|
|
|
|
const inventoryGrid = queryShadow(".inventory-grid");
|
|
expect(inventoryGrid).to.exist;
|
|
});
|
|
|
|
it("should show empty message when inventory is empty", async () => {
|
|
element.unit = testUnit;
|
|
element.inventory = [];
|
|
element.activeTab = "INVENTORY";
|
|
await waitForUpdate();
|
|
|
|
const emptyMessage = queryShadow(".inventory-grid p");
|
|
expect(emptyMessage).to.exist;
|
|
expect(emptyMessage.textContent).to.include("No items available");
|
|
});
|
|
|
|
it("should display mastery tab", async () => {
|
|
element.unit = testUnit;
|
|
element.activeTab = "MASTERY";
|
|
await waitForUpdate();
|
|
|
|
const masteryContainer = queryShadow(".mastery-container");
|
|
expect(masteryContainer).to.exist;
|
|
});
|
|
|
|
it("should show mastery progress for all classes", async () => {
|
|
testUnit.classMastery["CLASS_VANGUARD"] = {
|
|
level: 5,
|
|
xp: 250,
|
|
skillPoints: 2,
|
|
unlockedNodes: [],
|
|
};
|
|
testUnit.classMastery["CLASS_WEAVER"] = {
|
|
level: 2,
|
|
xp: 50,
|
|
skillPoints: 0,
|
|
unlockedNodes: [],
|
|
};
|
|
|
|
element.unit = testUnit;
|
|
element.activeTab = "MASTERY";
|
|
await waitForUpdate();
|
|
|
|
const masteryClasses = queryShadowAll(".mastery-class");
|
|
expect(masteryClasses.length).to.equal(2);
|
|
});
|
|
});
|
|
|
|
describe("Header Rendering", () => {
|
|
it("should display unit name, class, and level", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const name = queryShadow(".name");
|
|
expect(name.textContent).to.include("Test Vanguard");
|
|
|
|
const classTitle = queryShadow(".class-title");
|
|
expect(classTitle.textContent).to.include("Vanguard");
|
|
|
|
const level = queryShadow(".level");
|
|
expect(level.textContent).to.include("Level 5");
|
|
});
|
|
|
|
it("should display XP bar", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const xpBar = queryShadow(".xp-bar-container");
|
|
expect(xpBar).to.exist;
|
|
|
|
const xpLabel = queryShadow(".xp-label");
|
|
expect(xpLabel.textContent).to.include("250");
|
|
});
|
|
|
|
it("should display close button", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const closeButton = queryShadow(".close-button");
|
|
expect(closeButton).to.exist;
|
|
});
|
|
|
|
it("should dispatch close event when close button is clicked", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
let closeEventFired = false;
|
|
element.addEventListener("close", () => {
|
|
closeEventFired = true;
|
|
});
|
|
|
|
const closeButton = queryShadow(".close-button");
|
|
closeButton.click();
|
|
|
|
expect(closeEventFired).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe("Tab Switching", () => {
|
|
it("should switch between tabs", async () => {
|
|
element.unit = testUnit;
|
|
await waitForUpdate();
|
|
|
|
const inventoryTab = queryShadowAll(".tab-button")[0];
|
|
const skillsTab = queryShadowAll(".tab-button")[1];
|
|
const masteryTab = queryShadowAll(".tab-button")[2];
|
|
|
|
expect(inventoryTab.classList.contains("active")).to.be.true;
|
|
|
|
skillsTab.click();
|
|
await waitForUpdate();
|
|
|
|
expect(element.activeTab).to.equal("SKILLS");
|
|
expect(skillsTab.classList.contains("active")).to.be.true;
|
|
|
|
masteryTab.click();
|
|
await waitForUpdate();
|
|
|
|
expect(element.activeTab).to.equal("MASTERY");
|
|
expect(masteryTab.classList.contains("active")).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
|