Sig select: caution tooltip, FLIP/FYI stat block, keyword display
- TarotCard.cautions JSONField + cautions_json property; migrations 0027–0029 seed The Schizo (number=1) with 4 rival-interaction cautions (Roman-numeral card refs: I. The Pervert / II. The Occultist, etc.) - Sig-select overlay: FLIP (stat-block toggle) + FYI (caution tooltip) buttons; nav PRV/NXT portaled outside tooltip at bottom corners (z-70); caution tooltip covers stat block (inset:0, z-60, Gaussian blur); tooltip click dismisses; FLIP/FYI fully dead while btn-disabled; nav wraps circularly (4/4 → 1/4, 1/4 → 4/4) - SCSS: btn-disabled specificity fix (!important); btn-nav-left/right classes; sig-caution-* layout; stat-face keyword lists - Jasmine suite expanded: stat block + FLIP (5 specs), caution tooltip (16 specs) including wrap-around and disabled-button behaviour - IT tests: TarotCardCautionsTest (5), SigSelectRenderingTest (8) - Role-card SVG icons added to static/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card;
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}' } = {}) {
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]' } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -19,6 +19,29 @@ describe("SigSelect", () => {
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>
|
||||
</div>
|
||||
<div class="sig-stat-block">
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Upright</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversed</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</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">Caution!</h4>
|
||||
<span class="sig-caution-type">Rival Interaction</span>
|
||||
</div>
|
||||
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||
<p class="sig-caution-effect"></p>
|
||||
<span class="sig-caution-index"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-deck-grid">
|
||||
<div class="sig-card"
|
||||
@@ -28,7 +51,10 @@ describe("SigSelect", () => {
|
||||
data-name-group="Pentacles"
|
||||
data-name-title="King of Pentacles"
|
||||
data-arcana="Minor Arcana"
|
||||
data-correspondence="">
|
||||
data-correspondence=""
|
||||
data-keywords-upright="action,impulsiveness,ambition"
|
||||
data-keywords-reversed="no direction,disregard for consequences"
|
||||
data-cautions="${cardCautions.replace(/"/g, '"')}">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">K</span>
|
||||
</div>
|
||||
@@ -48,6 +74,7 @@ describe("SigSelect", () => {
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
stageCard = testDiv.querySelector(".sig-stage-card");
|
||||
statBlock = testDiv.querySelector(".sig-stat-block");
|
||||
card = testDiv.querySelector(".sig-card");
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: true })
|
||||
@@ -252,4 +279,222 @@ describe("SigSelect", () => {
|
||||
expect(card.classList.contains("sig-focused")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
|
||||
|
||||
describe("caution tooltip", () => {
|
||||
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
|
||||
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
|
||||
cautionEffect = testDiv.querySelector(".sig-caution-effect");
|
||||
cautionPrev = testDiv.querySelector(".sig-caution-prev");
|
||||
cautionNext = testDiv.querySelector(".sig-caution-next");
|
||||
cautionBtn = testDiv.querySelector(".sig-caution-btn");
|
||||
});
|
||||
|
||||
function hover() {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
}
|
||||
|
||||
function openCaution() {
|
||||
hover();
|
||||
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
}
|
||||
|
||||
it("!! click adds .sig-caution-open to the stage", () => {
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
});
|
||||
|
||||
it("second !! click removes .sig-caution-open (toggle)", () => {
|
||||
openCaution();
|
||||
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("shows placeholder text when cautions list is empty", () => {
|
||||
card.dataset.cautions = "[]";
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("pending");
|
||||
});
|
||||
|
||||
it("renders first caution effect HTML including .card-ref spans", () => {
|
||||
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
|
||||
openCaution();
|
||||
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
|
||||
});
|
||||
|
||||
it("with 1 caution both nav arrows are disabled", () => {
|
||||
card.dataset.cautions = JSON.stringify(["Single caution."]);
|
||||
openCaution();
|
||||
expect(cautionPrev.disabled).toBe(true);
|
||||
expect(cautionNext.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("with multiple cautions both nav arrows are always enabled", () => {
|
||||
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
|
||||
openCaution();
|
||||
expect(cautionPrev.disabled).toBe(false);
|
||||
expect(cautionNext.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("next click advances to second caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("Second");
|
||||
});
|
||||
|
||||
it("next wraps from last caution back to first", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Last"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("prev click goes back to first caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("prev wraps from first caution to last", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
|
||||
openCaution();
|
||||
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(cautionEffect.innerHTML).toContain("Last");
|
||||
});
|
||||
|
||||
it("index label shows n / total when multiple cautions", () => {
|
||||
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
|
||||
});
|
||||
|
||||
it("index label is empty when only 1 caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["Only one."]);
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
|
||||
});
|
||||
|
||||
it("card mouseleave closes the caution", () => {
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("opening again resets to first caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
// Close and reopen
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("First");
|
||||
});
|
||||
|
||||
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
|
||||
openCaution();
|
||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
||||
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
|
||||
expect(flipBtn.textContent).toBe("\u00D7");
|
||||
expect(cautionBtn.textContent).toBe("\u00D7");
|
||||
});
|
||||
|
||||
it("closing caution removes .btn-disabled and restores original labels", () => {
|
||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||
var origFlip = flipBtn.textContent;
|
||||
var origCaution = cautionBtn.textContent;
|
||||
openCaution();
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
|
||||
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
|
||||
expect(flipBtn.textContent).toBe(origFlip);
|
||||
expect(cautionBtn.textContent).toBe(origCaution);
|
||||
});
|
||||
|
||||
it("clicking the tooltip closes caution", () => {
|
||||
openCaution();
|
||||
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("FLIP click when caution open (btn-disabled) does nothing", () => {
|
||||
openCaution();
|
||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Stat block: keyword population and FLIP toggle ────────────────── //
|
||||
|
||||
describe("stat block and FLIP", () => {
|
||||
beforeEach(() => makeFixture());
|
||||
|
||||
it("populates upright keywords when a card is hovered", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
|
||||
expect(items.length).toBe(3);
|
||||
expect(items[0].textContent).toBe("action");
|
||||
expect(items[1].textContent).toBe("impulsiveness");
|
||||
expect(items[2].textContent).toBe("ambition");
|
||||
});
|
||||
|
||||
it("populates reversed keywords when a card is hovered", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].textContent).toBe("no direction");
|
||||
expect(items[1].textContent).toBe("disregard for consequences");
|
||||
});
|
||||
|
||||
it("FLIP click adds .is-reversed to the stat block", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("second FLIP click removes .is-reversed", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("hovering a new card resets .is-reversed", () => {
|
||||
// Add a second card to the grid so we can hover it
|
||||
var secondCard = card.cloneNode(true);
|
||||
secondCard.dataset.cardId = "99";
|
||||
testDiv.querySelector(".sig-deck-grid").appendChild(secondCard);
|
||||
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true })
|
||||
);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||
|
||||
secondCard.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("card with no keywords yields empty lists", () => {
|
||||
card.dataset.keywordsUpright = "";
|
||||
card.dataset.keywordsReversed = "";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
|
||||
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user