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); }); }); });