Files
python-tdd/src/static_src/tests/SigSelectSpec.js

686 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

describe("SigSelect", () => {
let testDiv, stageCard, card, statBlock;
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
testDiv = document.createElement("div");
testDiv.innerHTML = `
<div class="sig-overlay"
data-polarity="${polarity}"
data-user-role="${userRole}"
data-reserve-url="/epic/room/test/sig-reserve"
data-ready-url="/epic/room/test/sig-ready"
data-reservations="${reservations.replace(/"/g, '&quot;')}">
<div class="sig-modal">
<div class="sig-stage">
<div class="sig-stage-card" style="display:none">
<span class="fan-corner-rank"></span>
<i class="stage-suit-icon"></i>
<p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p>
</div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">&#9664;</button>
<button class="btn btn-nav-right sig-caution-next" type="button">&#9654;</button>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<span class="sig-caution-type">Rival Interaction</span>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
</div>
</div>
<div class="sig-deck-grid">
<div class="sig-card"
data-card-id="42"
data-corner-rank="K"
data-suit-icon=""
data-name-group="Pentacles"
data-name-title="King of Pentacles"
data-arcana="Minor Arcana"
data-correspondence=""
data-keywords-upright="action,impulsiveness,ambition"
data-keywords-reversed="no direction,disregard for consequences"
data-cautions="${cardCautions.replace(/"/g, '&quot;')}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">K</span>
</div>
<div class="sig-card-actions">
<button class="sig-ok-btn btn btn-confirm">OK</button>
<button class="sig-nvm-btn btn btn-cancel">NVM</button>
</div>
<div class="sig-card-cursors">
<span class="sig-cursor sig-cursor--left"></span>
<span class="sig-cursor sig-cursor--mid"></span>
<span class="sig-cursor sig-cursor--right"></span>
</div>
</div>
</div>
</div>
</div>
`;
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 <span class="card-ref">Card</span> 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);
});
});
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
//
// Fixture polarity = levity, userRole = PC.
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
//
// Only tests the JS position mapping — colour is CSS-only.
describe("WS cursor hover", () => {
beforeEach(() => makeFixture());
it("NC hover activates the --mid cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
});
it("SC hover activates the --right cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "SC", active: true },
}));
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
});
it("own role (PC) hover event is ignored — no cursor activates", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "PC", active: true },
}));
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
});
it("hover-off removes .active from the cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: false },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
});
it("hover on unknown card_id is a no-op", () => {
expect(() => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 9999, role: "NC", active: true },
}));
}).not.toThrow();
});
});
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
//
// applyReservation() sets data-reserved-by so the CSS can glow the card in
// the reserving gamer's role colour. These tests assert the attribute, not
// the colour (CSS variables aren't resolvable in the SpecRunner context).
describe("WS reservation sets data-reserved-by", () => {
beforeEach(() => makeFixture());
it("peer reservation sets data-reserved-by to the reserving role", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("NC");
});
it("peer reservation also adds .sig-reserved class", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.classList.contains("sig-reserved")).toBe(true);
});
it("release removes data-reserved-by", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(card.dataset.reservedBy).toBeUndefined();
});
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "PC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("PC");
expect(card.classList.contains("sig-reserved--own")).toBe(true);
});
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
// First, a hover float exists for NC (mid cursor)
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
// NC then clicks OK — reservation arrives
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
// Thumbs-up replaces hand-pointer
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
expect(floatEl).not.toBeNull();
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
});
it("peer release removes the thumbs-up float", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
});
});
// ── Polarity theming — stage qualifier text ────────────────────────────── //
//
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
// Correspondence field is never populated in sig-select context.
describe("polarity theming — stage qualifier", () => {
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// data-arcana defaults to "Minor Arcana" in fixture → non-major
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dataset.nameTitle = "The Schizo";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
});
it("non-major arcana title has no trailing comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// fixture default: Minor Arcana, "King of Pentacles"
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
});
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
});
it("hovering clears qualifier slots from the previous card", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
// Now major — above should be empty, below filled
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("correspondence field is never populated", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.correspondence = "Il Bagatto (Minchiate)";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
});
});
// ── WAIT NVM glow pulse ────────────────────────────────────────────────────── //
//
// After clicking TAKE SIG (POST ok → isReady=true) a setInterval pulses the
// button at 600ms: odd ticks add .btn-cancel + a --terOr outer box-shadow;
// even ticks remove both. Uses jasmine.clock() to advance the fake timer.
describe("WAIT NVM glow pulse", () => {
let takeSigBtn;
beforeEach(() => {
jasmine.clock().install();
// Pre-reserve card 42 as PC so _showTakeSigBtn() fires during init
makeFixture({ reservations: '{"42":"PC"}' });
takeSigBtn = document.getElementById("id_take_sig_btn");
});
afterEach(() => {
jasmine.clock().uninstall();
});
async function clickTakeSig() {
takeSigBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// Flush the fetch .then() so _startWaitNoGlow() is called
await Promise.resolve();
}
it("adds .btn-cancel after the first pulse tick (600 ms)", async () => {
await clickTakeSig();
jasmine.clock().tick(601);
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(true);
});
it("sets a non-empty box-shadow after the first pulse tick", async () => {
await clickTakeSig();
jasmine.clock().tick(601);
expect(takeSigBtn.style.boxShadow).not.toBe("");
});
it("removes .btn-cancel on the second tick (even / trough)", async () => {
await clickTakeSig();
jasmine.clock().tick(601); // peak
jasmine.clock().tick(600); // trough
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
});
it("clears box-shadow on the trough tick", async () => {
await clickTakeSig();
jasmine.clock().tick(601);
jasmine.clock().tick(600);
expect(takeSigBtn.style.boxShadow).toBe("");
});
it("stops glow and removes .btn-cancel when WAIT NVM is clicked (unready)", async () => {
await clickTakeSig();
jasmine.clock().tick(601); // glow is on
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(true);
// Click again → WAIT NVM → fetch unready → _stopWaitNoGlow()
takeSigBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
expect(takeSigBtn.style.boxShadow).toBe("");
});
it("glow does not advance after being stopped", async () => {
await clickTakeSig();
jasmine.clock().tick(601); // peak
takeSigBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve(); // stop
jasmine.clock().tick(600); // would be another tick if running
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
});
});
});