SIG SELECT FYI: mechanisms→energies, articulations→operations; .sig-caution→.sig-info; .btn-caution→.btn-info — TDD
- TarotCard.mechanisms renamed to energies, articulations to operations (migration 0008); energies_json + operations_json properties replace old names - migration 0008 also seeds The Schizo (card 1) w. 4 Energies (LIBIDO/NUMEN/VOLUPTAS×2) + 4 Operations (COVER/CROWN/BEHIND/BEFORE) - FYI info panel renamed throughout: .sig-caution-* → .sig-info-*; data-mechanisms → data-energies; data-articulations → data-operations - _renderCaution() now sets dynamic title (Energies/Operations) + .sig-info-title--energies/ --operations colour modifier; type element shows entry.type (LIBIDO, COVER etc.) - .btn-caution → .btn-info across note.js, role-select.js, specs, FT + _button-pad.scss rule - Major arcana reversed face: card title always shown (reversal concept moves to FYI) - SigSelectSpec.js rewritten: 242 specs; FYI describe block updated for energies/operations Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -95,9 +95,9 @@ describe('Note.showBanner', () => {
|
||||
|
||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||
|
||||
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/my-notes/', () => {
|
||||
it('T8: banner has a .btn.btn-info FYI link pointing to /billboard/my-notes/', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-caution');
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-info');
|
||||
expect(fyi).not.toBeNull();
|
||||
expect(fyi.getAttribute('href')).toBe('/billboard/my-notes/');
|
||||
});
|
||||
|
||||
@@ -730,8 +730,8 @@ describe("RoleSelect", () => {
|
||||
expect(w.textContent).toContain("Equip card deck before Role select");
|
||||
});
|
||||
|
||||
it("warning has a .btn-caution FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-caution");
|
||||
it("warning has a .btn-info FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-info");
|
||||
expect(btn).not.toBeNull();
|
||||
expect(btn.tagName).toBe("A");
|
||||
expect(btn.href).toContain("/gameboard/");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -16,18 +16,22 @@ describe("SigSelect", () => {
|
||||
<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>
|
||||
<div class="fan-card-face-upright">
|
||||
<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>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block">
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||
@@ -36,15 +40,15 @@ describe("SigSelect", () => {
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-caution-next" type="button">▶</button>
|
||||
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||
<div class="sig-caution-header">
|
||||
<h4 class="sig-caution-title"></h4>
|
||||
<span class="sig-caution-type">Ally Interaction</span>
|
||||
<button class="btn btn-nav-left sig-info-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-info-next" type="button">▶</button>
|
||||
<div class="sig-info" id="id_sig_info">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-caution-effect"></p>
|
||||
<span class="sig-caution-index"></span>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,9 +63,8 @@ describe("SigSelect", () => {
|
||||
data-correspondence=""
|
||||
data-keywords-upright="action,impulsiveness,ambition"
|
||||
data-keywords-reversed="no direction,disregard for consequences"
|
||||
data-cautions="${cardCautions.replace(/"/g, '"')}"
|
||||
data-mechanisms="[]"
|
||||
data-articulations="[]"
|
||||
data-energies="[]"
|
||||
data-operations="[]"
|
||||
data-levity-qualifier="Elevated"
|
||||
data-gravity-qualifier="Graven"
|
||||
data-reversal="">
|
||||
@@ -150,7 +153,6 @@ describe("SigSelect", () => {
|
||||
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);
|
||||
@@ -171,20 +173,15 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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.
|
||||
// ── WS release event (second-browser NVM sync) ────────────────────── //
|
||||
|
||||
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 },
|
||||
}));
|
||||
@@ -197,194 +194,231 @@ describe("SigSelect", () => {
|
||||
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 (!!) ──────────────────────────────────────────── //
|
||||
// ── FYI info panel ────────────────────────────────────────────────── //
|
||||
|
||||
describe("caution tooltip", () => {
|
||||
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
|
||||
describe("FYI info panel", () => {
|
||||
var infoEl, infoEffect, infoTitle, infoType, infoIndex, infoPrev, infoNext, infoBtn;
|
||||
|
||||
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");
|
||||
infoEl = testDiv.querySelector(".sig-info");
|
||||
infoEffect = testDiv.querySelector(".sig-info-effect");
|
||||
infoTitle = testDiv.querySelector(".sig-info-title");
|
||||
infoType = testDiv.querySelector(".sig-info-type");
|
||||
infoIndex = testDiv.querySelector(".sig-info-index");
|
||||
infoPrev = testDiv.querySelector(".sig-info-prev");
|
||||
infoNext = testDiv.querySelector(".sig-info-next");
|
||||
infoBtn = testDiv.querySelector(".sig-info-btn");
|
||||
});
|
||||
|
||||
function hover() {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
}
|
||||
function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }
|
||||
function openFYI() { hover(); infoBtn.dispatchEvent(new MouseEvent("click", { 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 adds .sig-info-open to the stage", () => {
|
||||
openFYI();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-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("FYI click when btn-disabled does not toggle", () => {
|
||||
openFYI();
|
||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||
infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows placeholder when both mechanisms and articulations are empty", () => {
|
||||
card.dataset.mechanisms = "[]";
|
||||
card.dataset.articulations = "[]";
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("No ally interactions defined");
|
||||
it("shows placeholder when both energies and operations are empty", () => {
|
||||
card.dataset.energies = "[]";
|
||||
card.dataset.operations = "[]";
|
||||
openFYI();
|
||||
expect(infoEffect.innerHTML).toContain("No interactions defined");
|
||||
});
|
||||
|
||||
it("renders first mechanism effect HTML including .card-ref spans", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: 'First <span class="card-ref">Card</span> effect.' }
|
||||
it("renders first energy effect HTML including .card-ref spans", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: 'First <span class="card-ref">Card</span> effect.' }
|
||||
]);
|
||||
openCaution();
|
||||
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
|
||||
openFYI();
|
||||
expect(infoEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(infoEffect.querySelector(".card-ref").textContent).toBe("Card");
|
||||
});
|
||||
|
||||
it("energy entry sets title to 'Energies' with --energies modifier class", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "NUMEN", effect: "An energy entry." }
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoTitle.textContent).toBe("Energies");
|
||||
expect(infoTitle.classList.contains("sig-info-title--energies")).toBe(true);
|
||||
});
|
||||
|
||||
it("operation entry sets title to 'Operations' with --operations modifier class", () => {
|
||||
card.dataset.operations = JSON.stringify([
|
||||
{ type: "COVER", effect: "An operation entry." }
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoTitle.textContent).toBe("Operations");
|
||||
expect(infoTitle.classList.contains("sig-info-title--operations")).toBe(true);
|
||||
});
|
||||
|
||||
it("type element shows the entry type in allcaps", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "VOLUPTAS", effect: "..." }]);
|
||||
openFYI();
|
||||
expect(infoType.textContent).toBe("VOLUPTAS");
|
||||
});
|
||||
|
||||
it("energies come before operations in the combined list", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "LIBIDO", effect: "Energy first" }]);
|
||||
card.dataset.operations = JSON.stringify([{ type: "CROWN", effect: "Op second" }]);
|
||||
openFYI();
|
||||
expect(infoEffect.textContent).toContain("Energy first");
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.textContent).toContain("Op second");
|
||||
});
|
||||
|
||||
it("advancing to an operation entry switches title and class to --operations", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "LIBIDO", effect: "E1" }]);
|
||||
card.dataset.operations = JSON.stringify([{ type: "COVER", effect: "O1" }]);
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoTitle.textContent).toBe("Operations");
|
||||
expect(infoTitle.classList.contains("sig-info-title--operations")).toBe(true);
|
||||
expect(infoTitle.classList.contains("sig-info-title--energies")).toBe(false);
|
||||
});
|
||||
|
||||
it("with 1 entry both nav arrows are disabled", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([{ category: "Mechanism", effect: "Single." }]);
|
||||
openCaution();
|
||||
expect(cautionPrev.disabled).toBe(true);
|
||||
expect(cautionNext.disabled).toBe(true);
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Single." }]);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(true);
|
||||
expect(infoNext.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("with multiple entries both nav arrows are always enabled", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "C1" },
|
||||
{ category: "Mechanism", effect: "C2" },
|
||||
{ category: "Mechanism", effect: "C3" },
|
||||
{ category: "Mechanism", effect: "C4" },
|
||||
it("with multiple entries both nav arrows are enabled", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "C1" },
|
||||
{ type: "NUMEN", effect: "C2" },
|
||||
{ type: "VOLUPTAS", effect: "C3" },
|
||||
{ type: "VOLUPTAS", effect: "C4" },
|
||||
]);
|
||||
openCaution();
|
||||
expect(cautionPrev.disabled).toBe(false);
|
||||
expect(cautionNext.disabled).toBe(false);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(false);
|
||||
expect(infoNext.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("next click advances to second entry", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "First" },
|
||||
{ category: "Mechanism", effect: "Second" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("Second");
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("Second");
|
||||
});
|
||||
|
||||
it("next wraps from last entry back to first", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "First" },
|
||||
{ category: "Mechanism", effect: "Last" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Last" },
|
||||
]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("prev click goes back to first entry", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "First" },
|
||||
{ category: "Mechanism", effect: "Second" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
infoPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("prev wraps from first entry to last", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "First" },
|
||||
{ category: "Mechanism", effect: "Middle" },
|
||||
{ category: "Mechanism", effect: "Last" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Middle" },
|
||||
{ type: "VOLUPTAS", effect: "Last" },
|
||||
]);
|
||||
openCaution();
|
||||
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("Last");
|
||||
openFYI();
|
||||
infoPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("Last");
|
||||
});
|
||||
|
||||
it("index label shows n / total when multiple entries", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "C1" },
|
||||
{ category: "Mechanism", effect: "C2" },
|
||||
{ category: "Mechanism", effect: "C3" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "C1" },
|
||||
{ type: "NUMEN", effect: "C2" },
|
||||
{ type: "VOLUPTAS", effect: "C3" },
|
||||
]);
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("1 / 3");
|
||||
});
|
||||
|
||||
it("index label is empty when only 1 entry", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([{ category: "Mechanism", effect: "Only one." }]);
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Only one." }]);
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("");
|
||||
});
|
||||
|
||||
it("card mouseleave closes the caution", () => {
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
it("card mouseleave closes the info panel", () => {
|
||||
openFYI();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("opening again resets to first entry", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "First" },
|
||||
{ category: "Mechanism", effect: "Second" },
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
// Close and reopen
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
openFYI();
|
||||
expect(infoEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("opening caution adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
||||
openCaution();
|
||||
it("opening info panel adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
||||
openFYI();
|
||||
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");
|
||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||
expect(flipBtn.textContent).toBe("×");
|
||||
expect(infoBtn.textContent).toBe("×");
|
||||
});
|
||||
|
||||
it("closing caution removes .btn-disabled and restores SPIN/FYI labels", () => {
|
||||
it("closing info panel removes .btn-disabled and restores SPIN/FYI labels", () => {
|
||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||
var origFlip = flipBtn.textContent;
|
||||
var origCaution = cautionBtn.textContent;
|
||||
openCaution();
|
||||
var origInfo = infoBtn.textContent;
|
||||
openFYI();
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
|
||||
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
|
||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(false);
|
||||
expect(flipBtn.textContent).toBe(origFlip);
|
||||
expect(cautionBtn.textContent).toBe(origCaution);
|
||||
expect(infoBtn.textContent).toBe(origInfo);
|
||||
});
|
||||
|
||||
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("clicking the info panel closes it", () => {
|
||||
openFYI();
|
||||
infoEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("SPIN click when caution open (btn-disabled) does nothing", () => {
|
||||
openCaution();
|
||||
it("SPIN click when info open (btn-disabled) does nothing", () => {
|
||||
openFYI();
|
||||
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(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -433,7 +467,6 @@ describe("SigSelect", () => {
|
||||
);
|
||||
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);
|
||||
@@ -487,7 +520,6 @@ describe("SigSelect", () => {
|
||||
makeFixture();
|
||||
card.dataset.reversal = "Nervous";
|
||||
hover();
|
||||
// "Nervous" goes into qualifier slot (own line); upright name reused in name slot
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||
.toBe(card.dataset.nameTitle);
|
||||
@@ -516,34 +548,25 @@ describe("SigSelect", () => {
|
||||
.toBe(card.dataset.nameTitle);
|
||||
});
|
||||
|
||||
it("major arcana with data-reversal: polarity qualifier still shown alongside reversal name", () => {
|
||||
it("major arcana reversed face: polarity qualifier + card title (concept name in FYI)", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dataset.reversal = "Territoriality";
|
||||
card.dataset.nameTitle = "The Schizo";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Territoriality");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("The Schizo");
|
||||
});
|
||||
|
||||
it("hovering a card without data-reversal clears the reversal name", () => {
|
||||
makeFixture();
|
||||
card.dataset.reversal = "Territoriality";
|
||||
it("non-major without data-reversal: reversal-name empty, qualifier mirrors polarity", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
// fixture default: Minor Arcana, no reversal word
|
||||
hover();
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
delete card.dataset.reversal;
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
// reversal-name clears because data-reversal is gone;
|
||||
// reversal-qualifier stays (it always mirrors the polarity qualifier)
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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());
|
||||
@@ -589,10 +612,6 @@ describe("SigSelect", () => {
|
||||
});
|
||||
|
||||
// ── 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());
|
||||
@@ -630,19 +649,16 @@ describe("SigSelect", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -663,15 +679,10 @@ describe("SigSelect", () => {
|
||||
});
|
||||
|
||||
// ── 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("");
|
||||
@@ -695,7 +706,6 @@ describe("SigSelect", () => {
|
||||
|
||||
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");
|
||||
});
|
||||
@@ -719,7 +729,6 @@ describe("SigSelect", () => {
|
||||
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");
|
||||
});
|
||||
@@ -733,17 +742,12 @@ describe("SigSelect", () => {
|
||||
});
|
||||
|
||||
// ── 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");
|
||||
});
|
||||
@@ -754,7 +758,6 @@ describe("SigSelect", () => {
|
||||
|
||||
async function clickTakeSig() {
|
||||
takeSigBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
// Flush the fetch .then() so _startWaitNoGlow() is called
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -772,8 +775,8 @@ describe("SigSelect", () => {
|
||||
|
||||
it("removes .btn-cancel on the second tick (even / trough)", async () => {
|
||||
await clickTakeSig();
|
||||
jasmine.clock().tick(601); // peak
|
||||
jasmine.clock().tick(600); // trough
|
||||
jasmine.clock().tick(601);
|
||||
jasmine.clock().tick(600);
|
||||
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -786,10 +789,9 @@ describe("SigSelect", () => {
|
||||
|
||||
it("stops glow and removes .btn-cancel when WAIT NVM is clicked (unready)", async () => {
|
||||
await clickTakeSig();
|
||||
jasmine.clock().tick(601); // glow is on
|
||||
jasmine.clock().tick(601);
|
||||
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();
|
||||
|
||||
@@ -799,94 +801,11 @@ describe("SigSelect", () => {
|
||||
|
||||
it("glow does not advance after being stopped", async () => {
|
||||
await clickTakeSig();
|
||||
jasmine.clock().tick(601); // peak
|
||||
jasmine.clock().tick(601);
|
||||
takeSigBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await Promise.resolve(); // stop
|
||||
jasmine.clock().tick(600); // would be another tick if running
|
||||
await Promise.resolve();
|
||||
jasmine.clock().tick(600);
|
||||
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── FYI tooltip — mechanisms + articulations data source ──────────────── //
|
||||
//
|
||||
// Sprint 2: the caution tooltip is reworked to draw from data-mechanisms and
|
||||
// data-articulations instead of data-cautions. Entries are {category, effect}
|
||||
// dicts; the category label replaces the old "Caution!" title; the caution-type
|
||||
// reads "Ally Interaction". Shoptalk is absent.
|
||||
|
||||
describe("FYI from mechanisms + articulations", () => {
|
||||
var cautionEffect, cautionTitle, cautionType, cautionPrev, cautionNext, cautionBtn;
|
||||
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
cautionEffect = testDiv.querySelector(".sig-caution-effect");
|
||||
cautionTitle = testDiv.querySelector(".sig-caution-title");
|
||||
cautionType = testDiv.querySelector(".sig-caution-type");
|
||||
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 openFYI() {
|
||||
hover();
|
||||
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
}
|
||||
|
||||
it("caution-type label reads 'Ally Interaction'", () => {
|
||||
openFYI();
|
||||
expect(cautionType.textContent).toBe("Ally Interaction");
|
||||
});
|
||||
|
||||
it("shows 'No ally interactions defined.' when both lists are empty", () => {
|
||||
card.dataset.mechanisms = "[]";
|
||||
card.dataset.articulations = "[]";
|
||||
openFYI();
|
||||
expect(cautionEffect.textContent).toContain("No ally interactions defined");
|
||||
});
|
||||
|
||||
it("renders first mechanism effect and sets title to its category", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: "The card amplifies adjacent power." }
|
||||
]);
|
||||
openFYI();
|
||||
expect(cautionTitle.textContent).toBe("Mechanism");
|
||||
expect(cautionEffect.textContent).toContain("amplifies adjacent power");
|
||||
});
|
||||
|
||||
it("mechanisms come before articulations in the combined list", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([{ category: "Mechanism", effect: "First" }]);
|
||||
card.dataset.articulations = JSON.stringify([{ category: "Articulation", effect: "Second" }]);
|
||||
openFYI();
|
||||
expect(cautionEffect.textContent).toContain("First");
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.textContent).toContain("Second");
|
||||
});
|
||||
|
||||
it("articulation title is set from its category field", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([{ category: "Mechanism", effect: "M1" }]);
|
||||
card.dataset.articulations = JSON.stringify([{ category: "Articulation", effect: "A1" }]);
|
||||
openFYI();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionTitle.textContent).toBe("Articulation");
|
||||
});
|
||||
|
||||
it("effect HTML is injected (supports .card-ref spans)", () => {
|
||||
card.dataset.mechanisms = JSON.stringify([
|
||||
{ category: "Mechanism", effect: 'Draw <span class="card-ref">The Occultist</span>.' }
|
||||
]);
|
||||
openFYI();
|
||||
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("The Occultist");
|
||||
});
|
||||
|
||||
it("shoptalk element is absent or empty", () => {
|
||||
openFYI();
|
||||
var shoptalk = testDiv.querySelector(".sig-caution-shoptalk");
|
||||
// Either removed from DOM or has no visible content
|
||||
expect(!shoptalk || shoptalk.textContent.trim() === "").toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user