describe("SigSelect", () => { let testDiv, stageCard, card, statBlock; function makeFixture({ reservations = '{}', cardCautions = '[]' } = {}) { testDiv = document.createElement("div"); testDiv.innerHTML = `
`; document.body.appendChild(testDiv); stageCard = testDiv.querySelector(".sig-stage-card"); statBlock = testDiv.querySelector(".sig-stat-block"); card = testDiv.querySelector(".sig-card"); window.fetch = jasmine.createSpy("fetch").and.returnValue( Promise.resolve({ ok: true }) ); window._roomSocket = { readyState: -1, send: jasmine.createSpy("send") }; SigSelect._testInit(); } afterEach(() => { if (testDiv) testDiv.remove(); delete window._roomSocket; }); // ── Stage reveal on mouseenter ─────────────────────────────────────── // describe("stage preview", () => { beforeEach(() => makeFixture()); it("shows the stage card on mouseenter", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); expect(stageCard.style.display).toBe(""); }); it("hides the stage card on mouseleave when not frozen", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); expect(stageCard.style.display).toBe("none"); }); it("does NOT hide the stage card on mouseleave when frozen (reserved)", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); SigSelect._setFrozen(true); card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); expect(stageCard.style.display).toBe(""); }); }); // ── Card focus (click → OK overlay) ───────────────────────────────── // describe("card click", () => { beforeEach(() => makeFixture()); it("adds .sig-focused to the clicked card", () => { card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(card.classList.contains("sig-focused")).toBe(true); }); it("shows the stage card after click", () => { card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(stageCard.style.display).toBe(""); }); it("does not focus a card reserved by another role", () => { card.dataset.reservedBy = "NC"; card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(card.classList.contains("sig-focused")).toBe(false); }); }); // ── Lock after reservation ─────────────────────────────────────────── // describe("lock after reservation", () => { beforeEach(() => makeFixture()); it("does not focus another card while one is reserved", () => { // Simulate a reservation on some other card (not this one) SigSelect._setReservedCardId("99"); card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(card.classList.contains("sig-focused")).toBe(false); }); it("does not call fetch when OK is clicked while a different card is reserved", () => { SigSelect._setReservedCardId("99"); var okBtn = card.querySelector(".sig-ok-btn"); okBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(window.fetch).not.toHaveBeenCalled(); }); it("allows focus again after reservation is cleared", () => { SigSelect._setReservedCardId("99"); SigSelect._setReservedCardId(null); card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(card.classList.contains("sig-focused")).toBe(true); }); }); // ── WS release clears NVM in a second browser ─────────────────────── // // Simulates the same gamer having two tabs open: tab B must clear its // .sig-reserved--own when tab A presses NVM (WS release event arrives). // The release payload must carry the card_id so the JS can find the element. describe("WS release event (second-browser NVM sync)", () => { beforeEach(() => makeFixture({ reservations: '{"42":"PC"}' })); it("removes .sig-reserved and .sig-reserved--own on WS release", () => { // Confirm reservation was applied on init expect(card.classList.contains("sig-reserved--own")).toBe(true); expect(card.classList.contains("sig-reserved")).toBe(true); // Tab A presses NVM — tab B receives this WS event with the card_id window.dispatchEvent(new CustomEvent("room:sig_reserved", { detail: { card_id: 42, role: "PC", reserved: false }, })); expect(card.classList.contains("sig-reserved--own")).toBe(false); expect(card.classList.contains("sig-reserved")).toBe(false); }); it("unfreezes the stage so other cards can be focused after WS release", () => { window.dispatchEvent(new CustomEvent("room:sig_reserved", { detail: { card_id: 42, role: "PC", reserved: false }, })); // Should now be able to click the card body again card.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(card.classList.contains("sig-focused")).toBe(true); }); }); // ── Caution tooltip (!!) ──────────────────────────────────────────── // describe("caution tooltip", () => { var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn; beforeEach(() => { makeFixture(); cautionTooltip = testDiv.querySelector(".sig-caution-tooltip"); cautionEffect = testDiv.querySelector(".sig-caution-effect"); cautionPrev = testDiv.querySelector(".sig-caution-prev"); cautionNext = testDiv.querySelector(".sig-caution-next"); cautionBtn = testDiv.querySelector(".sig-caution-btn"); }); function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); } function openCaution() { hover(); cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); } it("!! click adds .sig-caution-open to the stage", () => { openCaution(); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true); }); it("FYI click when btn-disabled does not close caution", () => { openCaution(); expect(cautionBtn.classList.contains("btn-disabled")).toBe(true); cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true); }); it("shows placeholder text when cautions list is empty", () => { card.dataset.cautions = "[]"; openCaution(); expect(cautionEffect.innerHTML).toContain("pending"); }); it("renders first caution effect HTML including .card-ref spans", () => { card.dataset.cautions = JSON.stringify(['First Card effect.']); openCaution(); expect(cautionEffect.querySelector(".card-ref")).not.toBeNull(); expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card"); }); it("with 1 caution both nav arrows are disabled", () => { card.dataset.cautions = JSON.stringify(["Single caution."]); openCaution(); expect(cautionPrev.disabled).toBe(true); expect(cautionNext.disabled).toBe(true); }); it("with multiple cautions both nav arrows are always enabled", () => { card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]); openCaution(); expect(cautionPrev.disabled).toBe(false); expect(cautionNext.disabled).toBe(false); }); it("next click advances to second caution", () => { card.dataset.cautions = JSON.stringify(["First", "Second"]); openCaution(); cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(cautionEffect.innerHTML).toContain("Second"); }); it("next wraps from last caution back to first", () => { card.dataset.cautions = JSON.stringify(["First", "Last"]); openCaution(); cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true })); cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(cautionEffect.innerHTML).toContain("First"); }); it("prev click goes back to first caution", () => { card.dataset.cautions = JSON.stringify(["First", "Second"]); openCaution(); cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true })); cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(cautionEffect.innerHTML).toContain("First"); }); it("prev wraps from first caution to last", () => { card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]); openCaution(); cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(cautionEffect.innerHTML).toContain("Last"); }); it("index label shows n / total when multiple cautions", () => { card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]); openCaution(); expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3"); }); it("index label is empty when only 1 caution", () => { card.dataset.cautions = JSON.stringify(["Only one."]); openCaution(); expect(testDiv.querySelector(".sig-caution-index").textContent).toBe(""); }); it("card mouseleave closes the caution", () => { openCaution(); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true); card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false); }); it("opening again resets to first caution", () => { card.dataset.cautions = JSON.stringify(["First", "Second"]); openCaution(); cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true })); // Close and reopen card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); openCaution(); expect(cautionEffect.innerHTML).toContain("First"); }); it("opening caution adds .btn-disabled and swaps labels to ×", () => { openCaution(); var flipBtn = testDiv.querySelector(".sig-flip-btn"); expect(flipBtn.classList.contains("btn-disabled")).toBe(true); expect(cautionBtn.classList.contains("btn-disabled")).toBe(true); expect(flipBtn.textContent).toBe("\u00D7"); expect(cautionBtn.textContent).toBe("\u00D7"); }); it("closing caution removes .btn-disabled and restores original labels", () => { var flipBtn = testDiv.querySelector(".sig-flip-btn"); var origFlip = flipBtn.textContent; var origCaution = cautionBtn.textContent; openCaution(); card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); expect(flipBtn.classList.contains("btn-disabled")).toBe(false); expect(cautionBtn.classList.contains("btn-disabled")).toBe(false); expect(flipBtn.textContent).toBe(origFlip); expect(cautionBtn.textContent).toBe(origCaution); }); it("clicking the tooltip closes caution", () => { openCaution(); cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false); }); it("FLIP click when caution open (btn-disabled) does nothing", () => { openCaution(); var flipBtn = testDiv.querySelector(".sig-flip-btn"); flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true); expect(statBlock.classList.contains("is-reversed")).toBe(false); }); }); // ── Stat block: keyword population and FLIP toggle ────────────────── // describe("stat block and FLIP", () => { beforeEach(() => makeFixture()); it("populates upright keywords when a card is hovered", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); var items = statBlock.querySelectorAll("#id_stat_keywords_upright li"); expect(items.length).toBe(3); expect(items[0].textContent).toBe("action"); expect(items[1].textContent).toBe("impulsiveness"); expect(items[2].textContent).toBe("ambition"); }); it("populates reversed keywords when a card is hovered", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li"); expect(items.length).toBe(2); expect(items[0].textContent).toBe("no direction"); expect(items[1].textContent).toBe("disregard for consequences"); }); it("FLIP click adds .is-reversed to the stat block", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); var flipBtn = statBlock.querySelector(".sig-flip-btn"); flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(statBlock.classList.contains("is-reversed")).toBe(true); }); it("second FLIP click removes .is-reversed", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); var flipBtn = statBlock.querySelector(".sig-flip-btn"); flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(statBlock.classList.contains("is-reversed")).toBe(false); }); it("hovering a new card resets .is-reversed", () => { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); statBlock.querySelector(".sig-flip-btn").dispatchEvent( new MouseEvent("click", { bubbles: true }) ); expect(statBlock.classList.contains("is-reversed")).toBe(true); // Leave and re-enter (simulates moving to a different card) card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true })); card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); expect(statBlock.classList.contains("is-reversed")).toBe(false); }); it("card with no keywords yields empty lists", () => { card.dataset.keywordsUpright = ""; card.dataset.keywordsReversed = ""; card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0); expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0); }); }); });