-
+
Emanation
@@ -36,15 +40,15 @@ describe("SigSelect", () => {
Reversal
-
-
-
-
@@ -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
Card effect.' }
+ it("renders first energy effect HTML including .card-ref spans", () => {
+ card.dataset.energies = JSON.stringify([
+ { type: "LIBIDO", effect: 'First
Card 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
The Occultist.' }
- ]);
- 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);
- });
- });
});
diff --git a/src/static_src/scss/_button-pad.scss b/src/static_src/scss/_button-pad.scss
index 9d5e7be..1957890 100644
--- a/src/static_src/scss/_button-pad.scss
+++ b/src/static_src/scss/_button-pad.scss
@@ -143,7 +143,7 @@
}
// FYI btn
- &.btn-caution {
+ &.btn-info {
color: rgba(var(--priYl), 1);
border-color: rgba(var(--priYl), 1);
background-color: rgba(var(--terYl), 1);
diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss
index 2d95063..cbf45ca 100644
--- a/src/static_src/scss/_card-deck.scss
+++ b/src/static_src/scss/_card-deck.scss
@@ -277,7 +277,7 @@ html:has(.sig-backdrop) {
z-index: 50;
}
- .sig-caution-btn {
+ .sig-info-btn {
position: absolute;
top: 1.25rem;
right: -1rem;
@@ -286,7 +286,7 @@ html:has(.sig-backdrop) {
}
// Caution tooltip — covers the entire stat block (inset: 0), z-index above buttons.
- .sig-caution-tooltip {
+ .sig-info-tooltip {
display: none;
position: absolute;
inset: 0;
@@ -301,20 +301,21 @@ html:has(.sig-backdrop) {
overflow-y: auto;
}
- .sig-caution-header {
+ .sig-info-header {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
- .sig-caution-title {
+ .sig-info-title {
font-size: calc(var(--sig-card-w, 120px) * 0.093);
font-weight: 700;
margin: 0;
- color: rgba(var(--priYl), 1);
+ &--energies { color: rgba(var(--terUser), 1); }
+ &--operations { color: rgba(var(--quaUser), 1); }
}
- .sig-caution-type {
+ .sig-info-type {
font-size: calc(var(--sig-card-w, 120px) * 0.058);
opacity: 0.7;
text-transform: uppercase;
@@ -322,14 +323,7 @@ html:has(.sig-backdrop) {
flex-shrink: 0;
}
- .sig-caution-shoptalk {
- font-size: calc(var(--sig-card-w, 120px) * 0.063);
- opacity: 0.55;
- margin: 0;
- font-style: italic;
- }
-
- .sig-caution-effect {
+ .sig-info-effect {
flex: 1;
font-size: calc(var(--sig-card-w, 120px) * 0.075);
margin: 0;
@@ -341,22 +335,22 @@ html:has(.sig-backdrop) {
}
}
- .sig-caution-index {
+ .sig-info-index {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
}
// Nav arrows portaled out of tooltip — sit at bottom corners above tooltip (z-70)
- .sig-caution-prev,
- .sig-caution-next {
+ .sig-info-prev,
+ .sig-info-next {
display: none;
position: absolute;
bottom: -1rem;
margin: 0;
z-index: 70;
}
- .sig-caution-prev { left: -1rem; }
- .sig-caution-next { right: -1rem; }
+ .sig-info-prev { left: -1rem; }
+ .sig-info-next { right: -1rem; }
.stat-face {
display: none;
@@ -395,9 +389,9 @@ html:has(.sig-backdrop) {
}
&.sig-stage--frozen .sig-stat-block { display: block; }
- &.sig-caution-open .sig-stat-block {
- .sig-caution-tooltip { display: flex; }
- .sig-caution-prev, .sig-caution-next { display: inline-flex; }
+ &.sig-info-open .sig-stat-block {
+ .sig-info-tooltip { display: flex; }
+ .sig-info-prev, .sig-info-next { display: inline-flex; }
}
}
@@ -613,8 +607,8 @@ html:has(.sig-backdrop) {
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
// card-ref spans inside the caution tooltip — must match the base rule's
- // .sig-stat-block .sig-caution-effect .card-ref specificity (0,3,0) to win.
- .sig-caution-effect .card-ref { color: rgba(var(--quiUser), 1); }
+ // .sig-stat-block .sig-info-effect .card-ref specificity (0,3,0) to win.
+ .sig-info-effect .card-ref { color: rgba(var(--quiUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
.sig-overlay[data-polarity="gravity"] {
@@ -626,7 +620,7 @@ html:has(.sig-backdrop) {
}
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
// override to secUser (light) so body text reads against the dark backdrop.
- .sig-caution-tooltip { color: rgba(var(--secUser), 1); }
+ .sig-info-tooltip { color: rgba(var(--secUser), 1); }
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
diff --git a/src/static_src/tests/NoteSpec.js b/src/static_src/tests/NoteSpec.js
index 8029e45..8e750b1 100644
--- a/src/static_src/tests/NoteSpec.js
+++ b/src/static_src/tests/NoteSpec.js
@@ -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/');
});
diff --git a/src/static_src/tests/RoleSelectSpec.js b/src/static_src/tests/RoleSelectSpec.js
index 3e39fff..aae6fdf 100644
--- a/src/static_src/tests/RoleSelectSpec.js
+++ b/src/static_src/tests/RoleSelectSpec.js
@@ -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/");
diff --git a/src/static_src/tests/SigSelectSpec.js b/src/static_src/tests/SigSelectSpec.js
index cdfd2ec..d84ff77 100644
--- a/src/static_src/tests/SigSelectSpec.js
+++ b/src/static_src/tests/SigSelectSpec.js
@@ -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 = `
{
-
+
Emanation
@@ -36,15 +40,15 @@ describe("SigSelect", () => {
Reversal
-
-
-
-
@@ -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
Card effect.' }
+ it("renders first energy effect HTML including .card-ref spans", () => {
+ card.dataset.energies = JSON.stringify([
+ { type: "LIBIDO", effect: 'First
Card 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
The Occultist.' }
- ]);
- 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);
- });
- });
});
diff --git a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
index f100a2f..36cf1d9 100644
--- a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
+++ b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
@@ -43,7 +43,7 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
-
+
Emanation
@@ -52,16 +52,16 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
Reversal
-
-
@@ -77,8 +77,8 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
data-correspondence="{{ card.correspondence|default:'' }}"
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
- data-mechanisms="{{ card.mechanisms_json }}"
- data-articulations="{{ card.articulations_json }}"
+ data-energies="{{ card.energies_json }}"
+ data-operations="{{ card.operations_json }}"
data-levity-qualifier="{{ card.levity_qualifier }}"
data-gravity-qualifier="{{ card.gravity_qualifier }}"
data-reversal="{{ card.reversal }}">