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

688 lines
33 KiB
JavaScript
Raw Normal View History

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;')}"
data-levity-qualifier="Elevated"
data-gravity-qualifier="Graven">
<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 "Elevated" 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 'Elevated' 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("Elevated");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Elevated' 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("Elevated");
});
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("Elevated");
});
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);
});
});
});