diff --git a/src/apps/dashboard/static/apps/dashboard/note.js b/src/apps/dashboard/static/apps/dashboard/note.js index adeec52..8669bb3 100644 --- a/src/apps/dashboard/static/apps/dashboard/note.js +++ b/src/apps/dashboard/static/apps/dashboard/note.js @@ -21,7 +21,7 @@ const Note = (() => { '' + '
' + '' + - 'FYI'; + 'FYI'; banner.querySelector('.note-banner__nvm').addEventListener('click', function () { banner.remove(); diff --git a/src/apps/epic/migrations/0008_rename_energies_operations_seed_schizo.py b/src/apps/epic/migrations/0008_rename_energies_operations_seed_schizo.py new file mode 100644 index 0000000..d7e827a --- /dev/null +++ b/src/apps/epic/migrations/0008_rename_energies_operations_seed_schizo.py @@ -0,0 +1,55 @@ +"""Rename mechanisms→energies and articulations→operations on TarotCard; +seed The Schizo (Earthman major arcana card 1) with Energy and Operation entries. +""" +from django.db import migrations + +SCHIZO_ENERGIES = [ + {"type": "LIBIDO", "effect": "When encountering territorial Libido, may convert Emanation into 1. The Priest."}, + {"type": "NUMEN", "effect": "When encountering despotic Numen, may convert Emanation into 1. The Powerful."}, + {"type": "VOLUPTAS", "effect": "When encountering axiomatic Voluptas, may convert Emanation into 1. The Normal."}, + {"type": "VOLUPTAS", "effect": "When encountering annihilating Voluptas, may convert Emanation into 1. The Surrendered."}, +] + +SCHIZO_OPERATIONS = [ + {"type": "COVER", "effect": "When covering 2. The Occultist she may choose, by converting her own Reversal into 2. Pestilence, to convert this Reversal into 1. The Pervert."}, + {"type": "CROWN", "effect": "When crowning 3. The Despot she may choose, by converting her own Reversal into 3. War, to convert this Reversal into 1. The Paranoiac."}, + {"type": "BEHIND", "effect": "When behind 4. The Capitalist he may choose, by converting his own Reversal into 4. Famine, to convert this Reversal into 1. The Neurotic."}, + {"type": "BEFORE", "effect": "When before 5. The Fascist he may choose, by converting his own Reversal into 5. Death, to convert this Reversal into 1. The Suicidal."}, +] + + +def seed_schizo(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + try: + earthman = DeckVariant.objects.get(slug="earthman") + except DeckVariant.DoesNotExist: + return + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=1, + ).update(energies=SCHIZO_ENERGIES, operations=SCHIZO_OPERATIONS) + + +def clear_schizo(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + try: + earthman = DeckVariant.objects.get(slug="earthman") + except DeckVariant.DoesNotExist: + return + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=1, + ).update(energies=[], operations=[]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0007_populate_middle_arcana_reversals"), + ] + + operations = [ + migrations.RenameField("TarotCard", "mechanisms", "energies"), + migrations.RenameField("TarotCard", "articulations", "operations"), + migrations.RunPython(seed_schizo, reverse_code=clear_schizo), + ] diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index c14a269..0a20f9a 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -257,8 +257,8 @@ class TarotCard(models.Model): gravity_emanation = models.CharField(max_length=200, blank=True, default='') levity_reversal = models.CharField(max_length=200, blank=True, default='') # polarity-split reversal (card 48) gravity_reversal = models.CharField(max_length=200, blank=True, default='') - mechanisms = models.JSONField(default=list) # list of dicts; in-game effects - articulations = models.JSONField(default=list) # list of dicts; combinatory effects + energies = models.JSONField(default=list) # list of {type, effect} dicts — Energy interactions + operations = models.JSONField(default=list) # list of {type, effect} dicts — Operation interactions keywords_upright = models.JSONField(default=list) keywords_reversed = models.JSONField(default=list) cautions = models.JSONField(default=list) @@ -342,14 +342,14 @@ class TarotCard(models.Model): return json.dumps(self.cautions) @property - def mechanisms_json(self): + def energies_json(self): import json - return json.dumps(self.mechanisms) + return json.dumps(self.energies) @property - def articulations_json(self): + def operations_json(self): import json - return json.dumps(self.articulations) + return json.dumps(self.operations) def __str__(self): return self.name diff --git a/src/apps/epic/static/apps/epic/role-select.js b/src/apps/epic/static/apps/epic/role-select.js index 401a439..454d567 100644 --- a/src/apps/epic/static/apps/epic/role-select.js +++ b/src/apps/epic/static/apps/epic/role-select.js @@ -48,7 +48,7 @@ var RoleSelect = (function () { el.innerHTML = "

Equip card deck before Role select

" + "
" + - "FYI" + + "FYI" + "" + "
"; el.querySelector(".btn-cancel").addEventListener("click", function () { diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js index ef277c3..f3c76a6 100644 --- a/src/apps/epic/static/apps/epic/sig-select.js +++ b/src/apps/epic/static/apps/epic/sig-select.js @@ -6,7 +6,7 @@ var SigSelect = (function () { }; var overlay, deckGrid, stage, stageCard, statBlock; - var cautionEl, cautionEffect, cautionTitle, cautionPrev, cautionNext, cautionIndexEl; + var cautionEl, cautionEffect, cautionTitle, cautionTypeEl, cautionPrev, cautionNext, cautionIndexEl; var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel; var reserveUrl, readyUrl, userRole, userPolarity; @@ -47,15 +47,20 @@ var SigSelect = (function () { function _renderCaution() { if (_cautionData.length === 0) { - if (cautionTitle) cautionTitle.textContent = 'Ally Interaction'; - cautionEffect.innerHTML = 'No ally interactions defined.'; + cautionTitle.textContent = 'Energies'; + cautionTitle.className = 'sig-info-title sig-info-title--energies'; + if (cautionTypeEl) cautionTypeEl.textContent = ''; + cautionEffect.innerHTML = 'No interactions defined.'; cautionPrev.disabled = true; cautionNext.disabled = true; cautionIndexEl.textContent = ''; return; } var entry = _cautionData[_cautionIdx]; - if (cautionTitle) cautionTitle.textContent = entry.category || ''; + var isEnergies = entry.category === 'energies'; + cautionTitle.textContent = isEnergies ? 'Energies' : 'Operations'; + cautionTitle.className = 'sig-info-title sig-info-title--' + entry.category; + if (cautionTypeEl) cautionTypeEl.textContent = entry.type || ''; cautionEffect.innerHTML = entry.effect || ''; cautionPrev.disabled = (_cautionData.length <= 1); cautionNext.disabled = (_cautionData.length <= 1); @@ -67,9 +72,13 @@ var SigSelect = (function () { function _openCaution() { if (!_focusedCardEl) return; try { - var mechanisms = JSON.parse(_focusedCardEl.dataset.mechanisms || '[]'); - var articulations = JSON.parse(_focusedCardEl.dataset.articulations || '[]'); - _cautionData = mechanisms.concat(articulations); + var energies = JSON.parse(_focusedCardEl.dataset.energies || '[]'); + var operations = JSON.parse(_focusedCardEl.dataset.operations || '[]'); + _cautionData = energies.map(function (e) { + return { type: e.type, effect: e.effect, category: 'energies' }; + }).concat(operations.map(function (o) { + return { type: o.type, effect: o.effect, category: 'operations' }; + })); } catch (e) { _cautionData = []; } @@ -79,11 +88,11 @@ var SigSelect = (function () { _cautionBtn.classList.add('btn-disabled'); _flipBtn.textContent = '\u00D7'; _cautionBtn.textContent = '\u00D7'; - stage.classList.add('sig-caution-open'); + stage.classList.add('sig-info-open'); } function _closeCaution() { - stage.classList.remove('sig-caution-open'); + stage.classList.remove('sig-info-open'); if (_flipBtn) { _flipBtn.classList.remove('btn-disabled'); _cautionBtn.classList.remove('btn-disabled'); @@ -140,7 +149,7 @@ var SigSelect = (function () { var reversal = cardEl.dataset.reversal || ''; if (isMajor) { stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier; - stageCard.querySelector('.fan-card-reversal-name').textContent = reversal; + stageCard.querySelector('.fan-card-reversal-name').textContent = title; } else if (reversal) { stageCard.querySelector('.fan-card-reversal-qualifier').textContent = reversal; stageCard.querySelector('.fan-card-reversal-name').textContent = title; @@ -629,7 +638,7 @@ var SigSelect = (function () { statBlock = stage.querySelector('.sig-stat-block'); _flipBtn = statBlock.querySelector('.sig-flip-btn'); - _cautionBtn = statBlock.querySelector('.sig-caution-btn'); + _cautionBtn = statBlock.querySelector('.sig-info-btn'); _flipOrigLabel = _flipBtn.textContent; _cautionOrigLabel = _cautionBtn.textContent; @@ -639,12 +648,13 @@ var SigSelect = (function () { stageCard.classList.toggle('stage-card--reversed'); }); - cautionEl = stage.querySelector('.sig-caution-tooltip'); - cautionEffect = cautionEl.querySelector('.sig-caution-effect'); - cautionTitle = cautionEl.querySelector('.sig-caution-title'); - cautionPrev = statBlock.querySelector('.sig-caution-prev'); - cautionNext = statBlock.querySelector('.sig-caution-next'); - cautionIndexEl = cautionEl.querySelector('.sig-caution-index'); + cautionEl = stage.querySelector('.sig-info'); + cautionEffect = cautionEl.querySelector('.sig-info-effect'); + cautionTitle = cautionEl.querySelector('.sig-info-title'); + cautionTypeEl = cautionEl.querySelector('.sig-info-type'); + cautionPrev = statBlock.querySelector('.sig-info-prev'); + cautionNext = statBlock.querySelector('.sig-info-next'); + cautionIndexEl = cautionEl.querySelector('.sig-info-index'); // Clicking the tooltip (not nav buttons) dismisses it cautionEl.addEventListener('click', function () { @@ -653,7 +663,7 @@ var SigSelect = (function () { _cautionBtn.addEventListener('click', function () { if (_cautionBtn.classList.contains('btn-disabled')) return; - stage.classList.contains('sig-caution-open') ? _closeCaution() : _openCaution(); + stage.classList.contains('sig-info-open') ? _closeCaution() : _openCaution(); }); cautionPrev.addEventListener('click', function () { _cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length; diff --git a/src/functional_tests/test_applet_my_notes.py b/src/functional_tests/test_applet_my_notes.py index 76e65b2..68eee0c 100644 --- a/src/functional_tests/test_applet_my_notes.py +++ b/src/functional_tests/test_applet_my_notes.py @@ -152,7 +152,7 @@ class StargazerNoteFromDashboardTest(FunctionalTest): banner.find_element(By.CSS_SELECTOR, ".note-banner__timestamp") banner.find_element(By.CSS_SELECTOR, ".note-banner__image") banner.find_element(By.CSS_SELECTOR, ".btn.btn-cancel") # NVM - fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-caution") # FYI + fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-info") # FYI # FYI navigates to Note page fyi.click() diff --git a/src/static/tests/NoteSpec.js b/src/static/tests/NoteSpec.js index 8029e45..8e750b1 100644 --- a/src/static/tests/NoteSpec.js +++ b/src/static/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/tests/RoleSelectSpec.js b/src/static/tests/RoleSelectSpec.js index 3e39fff..aae6fdf 100644 --- a/src/static/tests/RoleSelectSpec.js +++ b/src/static/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/tests/SigSelectSpec.js b/src/static/tests/SigSelectSpec.js index cdfd2ec..d84ff77 100644 --- a/src/static/tests/SigSelectSpec.js +++ b/src/static/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

      - - -
      -
      -

      - Ally Interaction + + +
      +
      +

      +

      -

      - +

      +
      @@ -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

          - - -
          -
          -

          - Ally Interaction + + +
          +
          +

          +

          -

          - +

          +
          @@ -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

              -
              -
              -

              -

              Ally Interaction

              +
              +
              +

              +

              -

              - +

              +
              - - + +
              @@ -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 }}">