aether-shards/test/ui/marketplace-screen.test.js

549 lines
16 KiB
JavaScript
Raw Normal View History

import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
// Import to register custom element
import "../../src/ui/screens/marketplace-screen.js";
describe("UI: MarketplaceScreen", () => {
let element;
let container;
let mockMarketManager;
let mockInventoryManager;
let mockHubStash;
beforeEach(async () => {
container = document.createElement("div");
document.body.appendChild(container);
element = document.createElement("marketplace-screen");
container.appendChild(element);
// Wait for element to be defined
await element.updateComplete;
// Create mock hub stash
mockHubStash = {
currency: {
aetherShards: 1000,
ancientCores: 0,
},
};
// Create mock inventory manager
mockInventoryManager = {
hubStash: mockHubStash,
};
// Create mock item registry
const mockItemRegistry = {
get: (defId) => {
const items = {
"ITEM_RUSTY_BLADE": {
id: "ITEM_RUSTY_BLADE",
name: "Rusty Infantry Blade",
type: "WEAPON",
rarity: "COMMON",
},
"ITEM_SCRAP_PLATE": {
id: "ITEM_SCRAP_PLATE",
name: "Scrap Plate Armor",
type: "ARMOR",
rarity: "COMMON",
},
};
return items[defId] || null;
},
};
// Create mock market manager
mockMarketManager = {
inventoryManager: mockInventoryManager,
itemRegistry: mockItemRegistry,
getStockForMerchant: sinon.stub().returns([]),
buyItem: sinon.stub().resolves(true),
getState: sinon.stub().returns({
generationId: "TEST_123",
stock: [],
buyback: [],
}),
};
});
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: Basic Rendering", () => {
it("should render marketplace screen with header", async () => {
await waitForUpdate();
const header = queryShadow(".header");
expect(header).to.exist;
expect(header.textContent).to.include("The Gilded Bazaar");
});
it("should display wallet currency", async () => {
element.marketManager = mockMarketManager;
await waitForUpdate();
const walletDisplay = queryShadow(".wallet-display");
expect(walletDisplay).to.exist;
expect(walletDisplay.textContent).to.include("1000");
expect(walletDisplay.textContent).to.include("Shards");
});
it("should render merchant tabs", async () => {
await waitForUpdate();
const merchantTabs = queryShadowAll(".merchant-tab");
expect(merchantTabs.length).to.equal(4); // Smith, Tailor, Alchemist, Buyback
});
it("should render filter buttons", async () => {
await waitForUpdate();
const filterButtons = queryShadowAll(".filter-button");
expect(filterButtons.length).to.be.greaterThan(0);
});
});
describe("CoA 2: Merchant Tab Switching", () => {
it("should switch active merchant when tab is clicked", async () => {
await waitForUpdate();
const tailorTab = queryShadowAll(".merchant-tab")[1]; // Tailor
tailorTab.click();
await waitForUpdate();
expect(element.activeMerchant).to.equal("TAILOR");
expect(tailorTab.classList.contains("active")).to.be.true;
});
it("should call getStockForMerchant with correct merchant type", async () => {
element.marketManager = mockMarketManager;
mockMarketManager.getStockForMerchant.reset();
mockMarketManager.getStockForMerchant.returns([]);
await waitForUpdate();
const smithTab = queryShadowAll(".merchant-tab")[0];
smithTab.click();
await waitForUpdate();
expect(mockMarketManager.getStockForMerchant.calledWith("SMITH")).to.be
.true;
});
it("should reset filter to ALL when switching merchants", async () => {
element.activeFilter = "WEAPON";
await waitForUpdate();
const tailorTab = queryShadowAll(".merchant-tab")[1];
tailorTab.click();
await waitForUpdate();
expect(element.activeFilter).to.equal("ALL");
});
});
describe("CoA 3: Item Display", () => {
beforeEach(async () => {
// Reset stub
mockMarketManager.getStockForMerchant.reset();
// Set up mock to return items
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_001",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: false,
},
{
id: "STOCK_002",
defId: "ITEM_SCRAP_PLATE",
type: "ARMOR",
rarity: "COMMON",
price: 75,
purchased: false,
},
]);
element.marketManager = mockMarketManager;
await waitForUpdate();
await waitForUpdate(); // Extra update to ensure render completes
});
it("should display items in grid", async () => {
await waitForUpdate();
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.equal(2);
});
it("should display item names", async () => {
const itemNames = queryShadowAll(".item-name");
expect(itemNames.length).to.equal(2);
expect(itemNames[0].textContent).to.include("Rusty Infantry Blade");
});
it("should display item prices", async () => {
const prices = queryShadowAll(".item-price");
expect(prices.length).to.equal(2);
expect(prices[0].textContent).to.include("50");
});
it("should apply rarity classes to item cards", async () => {
await waitForUpdate();
const itemCards = queryShadowAll(".item-card");
itemCards.forEach((card) => {
expect(card.classList.contains("rarity-common")).to.be.true;
});
});
});
describe("CoA 4: Purchase Flow", () => {
beforeEach(async () => {
// Reset stub
mockMarketManager.getStockForMerchant.reset();
// Set up mock to return items
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_001",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: false,
},
]);
element.marketManager = mockMarketManager;
await waitForUpdate();
await waitForUpdate(); // Extra update to ensure render completes
});
it("should open modal when item is clicked", async () => {
const itemCard = queryShadow(".item-card");
itemCard.click();
await waitForUpdate();
const modal = queryShadow(".modal");
expect(modal).to.exist;
expect(element.showModal).to.be.true;
});
it("should display purchase confirmation in modal", async () => {
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
const modalTitle = queryShadow(".modal-title");
expect(modalTitle).to.exist;
expect(modalTitle.textContent).to.include("Confirm Purchase");
});
it("should call buyItem when confirmed", async () => {
await waitForUpdate();
const itemCard = queryShadow(".item-card");
itemCard.click();
await waitForUpdate();
const confirmButton = queryShadow(".btn-primary");
confirmButton.click();
await waitForUpdate();
expect(mockMarketManager.buyItem.calledWith("STOCK_001")).to.be.true;
});
it("should close modal after successful purchase", async () => {
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
const confirmButton = queryShadow(".btn-primary");
expect(confirmButton).to.exist;
confirmButton.click();
await waitForUpdate();
await new Promise((resolve) => setTimeout(resolve, 50)); // Wait for async
expect(element.showModal).to.be.false;
});
it("should close modal when cancel is clicked", async () => {
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
const cancelButton = queryShadowAll(".btn")[1]; // Cancel button
expect(cancelButton).to.exist;
cancelButton.click();
await waitForUpdate();
await new Promise((resolve) => setTimeout(resolve, 10)); // Wait for async
expect(element.showModal).to.be.false;
});
});
describe("CoA 5: Affordability States", () => {
beforeEach(async () => {
// Reset stub
mockMarketManager.getStockForMerchant.reset();
// Set up mock to return items
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_AFFORDABLE",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: false,
},
{
id: "STOCK_UNAFFORDABLE",
defId: "ITEM_SCRAP_PLATE",
type: "ARMOR",
rarity: "COMMON",
price: 2000,
purchased: false,
},
]);
element.marketManager = mockMarketManager;
await waitForUpdate();
await waitForUpdate(); // Extra update to ensure render completes
});
it("should mark affordable items correctly", async () => {
element.marketManager = mockMarketManager;
mockHubStash.currency.aetherShards = 1000;
await waitForUpdate();
const itemCards = queryShadowAll(".item-card");
const affordableCard = Array.from(itemCards).find((card) =>
card.textContent.includes("Rusty Infantry Blade")
);
expect(affordableCard).to.exist;
expect(affordableCard.classList.contains("unaffordable")).to.be.false;
});
it("should mark unaffordable items correctly", async () => {
mockHubStash.currency.aetherShards = 100;
element._updateWallet();
await waitForUpdate();
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.be.greaterThan(0);
const unaffordableCard = Array.from(itemCards).find((card) =>
card.textContent.includes("Scrap Plate")
);
expect(unaffordableCard).to.exist;
expect(unaffordableCard.classList.contains("unaffordable")).to.be.true;
});
it("should disable buy button for unaffordable items", async () => {
mockHubStash.currency.aetherShards = 100;
element._updateWallet();
await waitForUpdate();
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.be.greaterThan(0);
const itemCard = itemCards[1]; // Unaffordable item
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
const confirmButton = queryShadow(".btn-primary");
expect(confirmButton).to.exist;
expect(confirmButton.disabled).to.be.true;
});
});
describe("CoA 6: Sold Out State", () => {
beforeEach(async () => {
// Reset stub
mockMarketManager.getStockForMerchant.reset();
// Set up mock to return sold item
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_SOLD",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: true,
},
]);
element.marketManager = mockMarketManager;
await waitForUpdate();
await waitForUpdate(); // Extra update to ensure render completes
});
it("should display sold out overlay", async () => {
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.be.greaterThan(0);
const soldOverlay = queryShadow(".sold-overlay");
expect(soldOverlay).to.exist;
expect(soldOverlay.textContent).to.include("SOLD");
});
it("should apply sold-out class to card", async () => {
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
expect(itemCard.classList.contains("sold-out")).to.be.true;
});
it("should not open modal when sold item is clicked", async () => {
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
const modal = queryShadow(".modal");
expect(modal).to.be.null;
});
});
describe("CoA 7: Filter Functionality", () => {
beforeEach(async () => {
// Reset stub
mockMarketManager.getStockForMerchant.reset();
// Set up mock to return items
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_WEAPON",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: false,
},
{
id: "STOCK_ARMOR",
defId: "ITEM_SCRAP_PLATE",
type: "ARMOR",
rarity: "COMMON",
price: 75,
purchased: false,
},
]);
element.marketManager = mockMarketManager;
await waitForUpdate();
await waitForUpdate(); // Extra update to ensure render completes
});
it("should filter items by type", async () => {
await waitForUpdate();
const weaponFilter = Array.from(queryShadowAll(".filter-button")).find(
(btn) => btn.textContent.includes("Weapons")
);
weaponFilter.click();
await waitForUpdate();
expect(element.activeFilter).to.equal("WEAPON");
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.equal(1);
});
it("should show all items when ALL filter is selected", async () => {
element.activeFilter = "WEAPON";
await waitForUpdate();
const allFilter = Array.from(queryShadowAll(".filter-button")).find(
(btn) => btn.textContent.includes("All")
);
allFilter.click();
await waitForUpdate();
expect(element.activeFilter).to.equal("ALL");
const itemCards = queryShadowAll(".item-card");
expect(itemCards.length).to.equal(2);
});
});
describe("CoA 8: Event Dispatching", () => {
it("should dispatch market-closed event when close button is clicked", async () => {
const closeSpy = sinon.spy();
element.addEventListener("market-closed", closeSpy);
await waitForUpdate();
const closeButton = queryShadow(".btn-close");
closeButton.click();
await waitForUpdate();
expect(closeSpy.called).to.be.true;
});
});
describe("Empty State", () => {
it("should display empty state when no items available", async () => {
mockMarketManager.getStockForMerchant.returns([]);
await waitForUpdate();
const emptyState = queryShadow(".empty-state");
expect(emptyState).to.exist;
expect(emptyState.textContent).to.include("No items available");
});
});
describe("Wallet Updates", () => {
it("should update wallet display after purchase", async () => {
element.marketManager = mockMarketManager;
mockMarketManager.getStockForMerchant.returns([
{
id: "STOCK_001",
defId: "ITEM_RUSTY_BLADE",
type: "WEAPON",
rarity: "COMMON",
price: 50,
purchased: false,
},
]);
mockHubStash.currency.aetherShards = 1000;
await waitForUpdate();
await new Promise((resolve) => setTimeout(resolve, 10)); // Extra wait for wallet update
const itemCard = queryShadow(".item-card");
expect(itemCard).to.exist;
itemCard.click();
await waitForUpdate();
// Simulate purchase - update currency before clicking
mockHubStash.currency.aetherShards = 950;
const confirmButton = queryShadow(".btn-primary");
expect(confirmButton).to.exist;
confirmButton.click();
await waitForUpdate();
await new Promise((resolve) => setTimeout(resolve, 50)); // Wait for async
// Wallet should be updated (component calls _updateWallet after purchase)
expect(element.wallet.aetherShards).to.equal(950);
});
});
});