Compare commits
5 Commits
759ce8d3e4
...
239da7e5b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
239da7e5b1 | ||
|
|
ed55e4e529 | ||
|
|
2757ae855f | ||
|
|
505744312b | ||
|
|
0522b5c126 |
@@ -21,7 +21,7 @@ const Note = (() => {
|
||||
'</div>' +
|
||||
'<div class="note-banner__image"></div>' +
|
||||
'<button type="button" class="btn btn-cancel note-banner__nvm">NVM</button>' +
|
||||
'<a href="/billboard/my-notes/" class="btn btn-caution note-banner__fyi">FYI</a>';
|
||||
'<a href="/billboard/my-notes/" class="btn btn-info note-banner__fyi">FYI</a>';
|
||||
|
||||
banner.querySelector('.note-banner__nvm').addEventListener('click', function () {
|
||||
banner.remove();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Populate TarotCard.reversal for Earthman Middle Arcana court cards.
|
||||
|
||||
Each suit has a fixed reversal qualifier that replaces the polarity qualifier
|
||||
(Elevated/Graven) when the card is spun to its reversed face:
|
||||
Brands → Seething Grails → Gloomy Blades → Nervous Crowns → Vacant
|
||||
|
||||
Also clears the incorrectly inherited reversal on The Schizo (card 1), which
|
||||
mistakenly carried 'Territoriality' from The Occultist (card 2).
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
SUIT_REVERSAL_QUALIFIER = {
|
||||
"BRANDS": "Seething",
|
||||
"GRAILS": "Gloomy",
|
||||
"BLADES": "Nervous",
|
||||
"CROWNS": "Vacant",
|
||||
}
|
||||
|
||||
RANK_NAMES = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||
|
||||
|
||||
def populate_reversals(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
|
||||
|
||||
# Middle Arcana court cards
|
||||
for suit, qualifier in SUIT_REVERSAL_QUALIFIER.items():
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman,
|
||||
arcana="MIDDLE",
|
||||
suit=suit,
|
||||
number__in=list(RANK_NAMES.keys()),
|
||||
).update(reversal=qualifier)
|
||||
|
||||
# Clear The Schizo's incorrectly inherited reversal (belongs to The Occultist)
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman,
|
||||
arcana="MAJOR",
|
||||
number=1,
|
||||
).update(reversal="")
|
||||
|
||||
|
||||
def clear_reversals(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="MIDDLE",
|
||||
suit__in=list(SUIT_REVERSAL_QUALIFIER.keys()),
|
||||
number__in=list(RANK_NAMES.keys()),
|
||||
).update(reversal="")
|
||||
TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR", number=1).update(
|
||||
reversal="Territoriality"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0006_add_deck_variant_to_tableseat"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_reversals, reverse_code=clear_reversals),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -341,6 +341,16 @@ class TarotCard(models.Model):
|
||||
import json
|
||||
return json.dumps(self.cautions)
|
||||
|
||||
@property
|
||||
def energies_json(self):
|
||||
import json
|
||||
return json.dumps(self.energies)
|
||||
|
||||
@property
|
||||
def operations_json(self):
|
||||
import json
|
||||
return json.dumps(self.operations)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ var RoleSelect = (function () {
|
||||
el.innerHTML =
|
||||
"<p>Equip card deck before Role select</p>" +
|
||||
"<div class=\"guard-actions\">" +
|
||||
"<a class=\"btn btn-caution\" href=\"/gameboard/\">FYI</a>" +
|
||||
"<a class=\"btn btn-info\" href=\"/gameboard/\">FYI</a>" +
|
||||
"<button class=\"btn btn-cancel\">NVM</button>" +
|
||||
"</div>";
|
||||
el.querySelector(".btn-cancel").addEventListener("click", function () {
|
||||
|
||||
@@ -6,7 +6,7 @@ var SigSelect = (function () {
|
||||
};
|
||||
|
||||
var overlay, deckGrid, stage, stageCard, statBlock;
|
||||
var cautionEl, cautionEffect, cautionPrev, cautionNext, cautionIndexEl;
|
||||
var cautionEl, cautionEffect, cautionTitle, cautionTypeEl, cautionPrev, cautionNext, cautionIndexEl;
|
||||
var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel;
|
||||
var reserveUrl, readyUrl, userRole, userPolarity;
|
||||
|
||||
@@ -47,13 +47,21 @@ var SigSelect = (function () {
|
||||
|
||||
function _renderCaution() {
|
||||
if (_cautionData.length === 0) {
|
||||
cautionEffect.innerHTML = '<em>Rival interactions pending.</em>';
|
||||
cautionTitle.textContent = 'Energies';
|
||||
cautionTitle.className = 'sig-info-title sig-info-title--energies';
|
||||
if (cautionTypeEl) cautionTypeEl.textContent = '';
|
||||
cautionEffect.innerHTML = '<em>No interactions defined.</em>';
|
||||
cautionPrev.disabled = true;
|
||||
cautionNext.disabled = true;
|
||||
cautionIndexEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
cautionEffect.innerHTML = _cautionData[_cautionIdx];
|
||||
var entry = _cautionData[_cautionIdx];
|
||||
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);
|
||||
cautionIndexEl.textContent = _cautionData.length > 1
|
||||
@@ -64,7 +72,13 @@ var SigSelect = (function () {
|
||||
function _openCaution() {
|
||||
if (!_focusedCardEl) return;
|
||||
try {
|
||||
_cautionData = JSON.parse(_focusedCardEl.dataset.cautions || '[]');
|
||||
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 = [];
|
||||
}
|
||||
@@ -74,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');
|
||||
@@ -127,8 +141,26 @@ var SigSelect = (function () {
|
||||
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
|
||||
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
|
||||
|
||||
// Reversed face.
|
||||
// - Major arcana: polarity qualifier + reversal concept name
|
||||
// - Non-major w. reversal: suit qualifier word replaces polarity qualifier;
|
||||
// card name (title) stays the same — two separate lines
|
||||
// - Non-major w/o reversal: fall back to mirroring the polarity qualifier
|
||||
var reversal = cardEl.dataset.reversal || '';
|
||||
if (isMajor) {
|
||||
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
|
||||
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;
|
||||
} else {
|
||||
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
|
||||
stageCard.querySelector('.fan-card-reversal-name').textContent = '';
|
||||
}
|
||||
|
||||
// Populate stat block keyword faces and reset to upright
|
||||
statBlock.classList.remove('is-reversed');
|
||||
stageCard.classList.remove('stage-card--reversed');
|
||||
_populateKeywordList(
|
||||
statBlock.querySelector('#id_stat_keywords_upright'),
|
||||
cardEl.dataset.keywordsUpright
|
||||
@@ -606,20 +638,23 @@ 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;
|
||||
|
||||
_flipBtn.addEventListener('click', function () {
|
||||
if (_flipBtn.classList.contains('btn-disabled')) return;
|
||||
statBlock.classList.toggle('is-reversed');
|
||||
stageCard.classList.toggle('stage-card--reversed');
|
||||
});
|
||||
|
||||
cautionEl = stage.querySelector('.sig-caution-tooltip');
|
||||
cautionEffect = cautionEl.querySelector('.sig-caution-effect');
|
||||
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 () {
|
||||
@@ -628,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;
|
||||
|
||||
@@ -1116,18 +1116,19 @@ class SigSelectRenderingTest(TestCase):
|
||||
self.assertContains(response, "stat-face--upright")
|
||||
self.assertContains(response, "stat-face--reversed")
|
||||
|
||||
def test_sig_cards_render_cautions_data_attribute(self):
|
||||
def test_sig_cards_render_energies_operations_data_attributes(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, "data-cautions=")
|
||||
self.assertContains(response, "data-energies=")
|
||||
self.assertContains(response, "data-operations=")
|
||||
|
||||
def test_sig_caution_tooltip_structure_rendered(self):
|
||||
def test_sig_info_panel_structure_rendered(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, "sig-caution-tooltip")
|
||||
self.assertContains(response, "sig-caution-btn")
|
||||
self.assertContains(response, "sig-caution-effect")
|
||||
self.assertContains(response, "sig-caution-index")
|
||||
self.assertContains(response, "sig-caution-prev")
|
||||
self.assertContains(response, "sig-caution-next")
|
||||
self.assertContains(response, "sig-info")
|
||||
self.assertContains(response, "sig-info-btn")
|
||||
self.assertContains(response, "sig-info-effect")
|
||||
self.assertContains(response, "sig-info-index")
|
||||
self.assertContains(response, "sig-info-prev")
|
||||
self.assertContains(response, "sig-info-next")
|
||||
|
||||
|
||||
class SelectSigCardViewTest(TestCase):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -95,9 +95,9 @@ describe('Note.showBanner', () => {
|
||||
|
||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||
|
||||
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/my-notes/', () => {
|
||||
it('T8: banner has a .btn.btn-info FYI link pointing to /billboard/my-notes/', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-caution');
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-info');
|
||||
expect(fyi).not.toBeNull();
|
||||
expect(fyi.getAttribute('href')).toBe('/billboard/my-notes/');
|
||||
});
|
||||
|
||||
@@ -730,8 +730,8 @@ describe("RoleSelect", () => {
|
||||
expect(w.textContent).toContain("Equip card deck before Role select");
|
||||
});
|
||||
|
||||
it("warning has a .btn-caution FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-caution");
|
||||
it("warning has a .btn-info FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-info");
|
||||
expect(btn).not.toBeNull();
|
||||
expect(btn.tagName).toBe("A");
|
||||
expect(btn.href).toContain("/gameboard/");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -16,34 +16,39 @@ describe("SigSelect", () => {
|
||||
<div class="sig-stage-card" style="display:none">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="stage-suit-icon"></i>
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block">
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Upright</p>
|
||||
<p class="stat-face-label">Emanation</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>
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-caution-next" type="button">▶</button>
|
||||
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||
<div class="sig-caution-header">
|
||||
<h4 class="sig-caution-title">Caution!</h4>
|
||||
<span class="sig-caution-type">Rival Interaction</span>
|
||||
<button class="btn btn-nav-left sig-info-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-info-next" type="button">▶</button>
|
||||
<div class="sig-info" id="id_sig_info">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||
<p class="sig-caution-effect"></p>
|
||||
<span class="sig-caution-index"></span>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,9 +63,11 @@ describe("SigSelect", () => {
|
||||
data-correspondence=""
|
||||
data-keywords-upright="action,impulsiveness,ambition"
|
||||
data-keywords-reversed="no direction,disregard for consequences"
|
||||
data-cautions="${cardCautions.replace(/"/g, '"')}"
|
||||
data-energies="[]"
|
||||
data-operations="[]"
|
||||
data-levity-qualifier="Elevated"
|
||||
data-gravity-qualifier="Graven">
|
||||
data-gravity-qualifier="Graven"
|
||||
data-reversal="">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">K</span>
|
||||
</div>
|
||||
@@ -146,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);
|
||||
@@ -167,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 },
|
||||
}));
|
||||
@@ -193,173 +194,238 @@ 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 text when cautions list is empty", () => {
|
||||
card.dataset.cautions = "[]";
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("pending");
|
||||
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 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("renders first energy effect HTML including .card-ref spans", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: 'First <span class="card-ref">Card</span> effect.' }
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(infoEffect.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("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("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("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("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("type element shows the entry type in allcaps", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "VOLUPTAS", effect: "..." }]);
|
||||
openFYI();
|
||||
expect(infoType.textContent).toBe("VOLUPTAS");
|
||||
});
|
||||
|
||||
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("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("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("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("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("with 1 entry both nav arrows are disabled", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Single." }]);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(true);
|
||||
expect(infoNext.disabled).toBe(true);
|
||||
});
|
||||
|
||||
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("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" },
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(false);
|
||||
expect(infoNext.disabled).toBe(false);
|
||||
});
|
||||
|
||||
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("next click advances to second entry", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("Second");
|
||||
});
|
||||
|
||||
it("card mouseleave closes the caution", () => {
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
it("next wraps from last entry back to first", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Last" },
|
||||
]);
|
||||
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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Middle" },
|
||||
{ type: "VOLUPTAS", effect: "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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "C1" },
|
||||
{ type: "NUMEN", effect: "C2" },
|
||||
{ type: "VOLUPTAS", effect: "C3" },
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("1 / 3");
|
||||
});
|
||||
|
||||
it("index label is empty when only 1 entry", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Only one." }]);
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("");
|
||||
});
|
||||
|
||||
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 caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
// Close and reopen
|
||||
it("opening again resets to first entry", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
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 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 original 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("FLIP 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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Stat block: keyword population and FLIP toggle ────────────────── //
|
||||
// ── Stat block: keyword population and SPIN toggle ────────────────── //
|
||||
|
||||
describe("stat block and FLIP", () => {
|
||||
describe("stat block and SPIN", () => {
|
||||
beforeEach(() => makeFixture());
|
||||
|
||||
it("populates upright keywords when a card is hovered", () => {
|
||||
@@ -379,14 +445,14 @@ describe("SigSelect", () => {
|
||||
expect(items[1].textContent).toBe("disregard for consequences");
|
||||
});
|
||||
|
||||
it("FLIP click adds .is-reversed to the stat block", () => {
|
||||
it("SPIN 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", () => {
|
||||
it("second SPIN 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 }));
|
||||
@@ -401,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);
|
||||
@@ -416,12 +481,92 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPIN card animation — stage-card reversed state ──────────────────── //
|
||||
|
||||
describe("SPIN card animation", () => {
|
||||
function hover() {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
}
|
||||
|
||||
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
statBlock.querySelector(".sig-flip-btn")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("second SPIN click removes .stage-card--reversed", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("hovering a new card resets .stage-card--reversed", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
statBlock.querySelector(".sig-flip-btn")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
|
||||
makeFixture();
|
||||
card.dataset.reversal = "Nervous";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||
.toBe(card.dataset.nameTitle);
|
||||
});
|
||||
|
||||
it("updateStage() populates fan-card-reversal-qualifier with levity qualifier", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent)
|
||||
.toBe("Elevated");
|
||||
});
|
||||
|
||||
it("updateStage() populates fan-card-reversal-qualifier with gravity qualifier", () => {
|
||||
makeFixture({ polarity: "gravity", userRole: "BC" });
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent)
|
||||
.toBe("Graven");
|
||||
});
|
||||
|
||||
it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
card.dataset.reversal = "Vacant";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||
.toBe(card.dataset.nameTitle);
|
||||
});
|
||||
|
||||
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.nameTitle = "The Schizo";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("The Schizo");
|
||||
});
|
||||
|
||||
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();
|
||||
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());
|
||||
@@ -467,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());
|
||||
@@ -508,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);
|
||||
@@ -541,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("");
|
||||
@@ -573,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");
|
||||
});
|
||||
@@ -597,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");
|
||||
});
|
||||
@@ -611,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");
|
||||
});
|
||||
@@ -632,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();
|
||||
}
|
||||
|
||||
@@ -650,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);
|
||||
});
|
||||
|
||||
@@ -664,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();
|
||||
|
||||
@@ -677,10 +801,10 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
border: 0.18rem solid rgba(var(--priUser), 1);
|
||||
}
|
||||
|
||||
// BIG btn
|
||||
&.btn-primary {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
@@ -70,6 +71,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// BYE btn
|
||||
&.btn-abandon {
|
||||
color: rgba(var(--priBl), 1);
|
||||
border-color: rgba(var(--priBl), 1);
|
||||
@@ -105,6 +107,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// DEL btn
|
||||
&.btn-cancel {
|
||||
color: rgba(var(--priOr), 1);
|
||||
border-color: rgba(var(--priOr), 1);
|
||||
@@ -139,7 +142,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-caution {
|
||||
// FYI btn
|
||||
&.btn-info {
|
||||
color: rgba(var(--priYl), 1);
|
||||
border-color: rgba(var(--priYl), 1);
|
||||
background-color: rgba(var(--terYl), 1);
|
||||
@@ -174,6 +178,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// OK btn
|
||||
&.btn-confirm {
|
||||
color: rgba(var(--priGn), 1);
|
||||
border-color: rgba(var(--priGn), 1);
|
||||
@@ -209,6 +214,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// DEL btn
|
||||
&.btn-danger {
|
||||
color: rgba(var(--priRd), 1);
|
||||
background-color: rgba(var(--terRd), 1);
|
||||
@@ -286,6 +292,7 @@
|
||||
font-size: 0.75rem; // 0.63rem × 1.2
|
||||
}
|
||||
|
||||
// PRV btn
|
||||
&.btn-nav-left {
|
||||
color: rgba(var(--priFs), 1);
|
||||
border-color: rgba(var(--priFs), 1);
|
||||
@@ -321,6 +328,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// NXT btn
|
||||
&.btn-nav-right {
|
||||
color: rgba(var(--priLm), 1);
|
||||
border-color: rgba(var(--priLm), 1);
|
||||
@@ -356,6 +364,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// DON btn
|
||||
&.btn-equip {
|
||||
color: rgba(var(--priTk), 1);
|
||||
border-color: rgba(var(--priTk), 1);
|
||||
@@ -391,6 +400,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// DOFF btn
|
||||
&.btn-unequip {
|
||||
color: rgba(var(--priMe), 1);
|
||||
border-color: rgba(var(--priMe), 1);
|
||||
@@ -426,7 +436,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-reverse {
|
||||
// FLIP btn
|
||||
&.btn-reveal {
|
||||
color: rgba(var(--priCy), 1);
|
||||
border-color: rgba(var(--priCy), 1);
|
||||
background-color: rgba(var(--terCy), 1);
|
||||
@@ -461,37 +472,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-tip {
|
||||
color: rgba(var(--priLm), 1);
|
||||
border-color: rgba(var(--priLm), 1);
|
||||
background-color: rgba(var(--terLm), 1);
|
||||
// SPIN btn
|
||||
&.btn-reverse {
|
||||
color: rgba(var(--priCy), 1);
|
||||
border-color: rgba(var(--priCy), 1);
|
||||
background-color: rgba(var(--terCy), 1);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terCy), 0.12)
|
||||
;
|
||||
|
||||
&:hover {
|
||||
text-shadow:
|
||||
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||
0 0 1rem rgba(var(--priLm), 1)
|
||||
0 0 1rem rgba(var(--priCy), 1)
|
||||
;
|
||||
box-shadow:
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||
;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 0.18rem solid rgba(var(--priLm), 1);
|
||||
border: 0.18rem solid rgba(var(--priCy), 1);
|
||||
text-shadow:
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.12rem rgba(var(--priLm), 1)
|
||||
0 0 0.12rem rgba(var(--priCy), 1)
|
||||
;
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||
-0.1rem -0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ html:has(.sig-backdrop) {
|
||||
align-items: flex-end;
|
||||
padding-left: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
|
||||
.sig-stage-card {
|
||||
flex-shrink: 0;
|
||||
@@ -188,6 +187,7 @@ html:has(.sig-backdrop) {
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
overflow: hidden;
|
||||
transition: transform 0.4s ease;
|
||||
|
||||
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
|
||||
// so these just need display/font overrides; the corners land at the card edges.
|
||||
@@ -224,12 +224,33 @@ html:has(.sig-backdrop) {
|
||||
padding: 0.25rem 0.15rem;
|
||||
gap: 0.2rem;
|
||||
|
||||
.fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
|
||||
.fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
|
||||
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
|
||||
// Upright qualifier + name share sizing/weight/color with their reversed counterparts
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
|
||||
.fan-card-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
|
||||
.sig-qualifier-below,
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-name,
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
||||
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
|
||||
// Reversed face elements — pre-rotated so they read forward after card spins
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name {
|
||||
transform: rotate(180deg);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
&.stage-card--reversed {
|
||||
transform: rotate(180deg);
|
||||
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name { opacity: 1; }
|
||||
.fan-card-name,
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below { opacity: 0.25; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +277,7 @@ html:has(.sig-backdrop) {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sig-caution-btn {
|
||||
.sig-info-btn {
|
||||
position: absolute;
|
||||
top: 1.25rem;
|
||||
right: -1rem;
|
||||
@@ -265,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;
|
||||
@@ -280,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;
|
||||
@@ -301,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;
|
||||
@@ -320,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;
|
||||
@@ -374,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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,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"] {
|
||||
@@ -605,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); }
|
||||
|
||||
@@ -95,9 +95,9 @@ describe('Note.showBanner', () => {
|
||||
|
||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||
|
||||
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/my-notes/', () => {
|
||||
it('T8: banner has a .btn.btn-info FYI link pointing to /billboard/my-notes/', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-caution');
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-info');
|
||||
expect(fyi).not.toBeNull();
|
||||
expect(fyi.getAttribute('href')).toBe('/billboard/my-notes/');
|
||||
});
|
||||
|
||||
@@ -730,8 +730,8 @@ describe("RoleSelect", () => {
|
||||
expect(w.textContent).toContain("Equip card deck before Role select");
|
||||
});
|
||||
|
||||
it("warning has a .btn-caution FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-caution");
|
||||
it("warning has a .btn-info FYI link to gameboard", () => {
|
||||
const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-info");
|
||||
expect(btn).not.toBeNull();
|
||||
expect(btn.tagName).toBe("A");
|
||||
expect(btn.href).toContain("/gameboard/");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -16,34 +16,39 @@ describe("SigSelect", () => {
|
||||
<div class="sig-stage-card" style="display:none">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="stage-suit-icon"></i>
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block">
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Upright</p>
|
||||
<p class="stat-face-label">Emanation</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>
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-caution-next" type="button">▶</button>
|
||||
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||
<div class="sig-caution-header">
|
||||
<h4 class="sig-caution-title">Caution!</h4>
|
||||
<span class="sig-caution-type">Rival Interaction</span>
|
||||
<button class="btn btn-nav-left sig-info-prev" type="button">◀</button>
|
||||
<button class="btn btn-nav-right sig-info-next" type="button">▶</button>
|
||||
<div class="sig-info" id="id_sig_info">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||
<p class="sig-caution-effect"></p>
|
||||
<span class="sig-caution-index"></span>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,9 +63,11 @@ describe("SigSelect", () => {
|
||||
data-correspondence=""
|
||||
data-keywords-upright="action,impulsiveness,ambition"
|
||||
data-keywords-reversed="no direction,disregard for consequences"
|
||||
data-cautions="${cardCautions.replace(/"/g, '"')}"
|
||||
data-energies="[]"
|
||||
data-operations="[]"
|
||||
data-levity-qualifier="Elevated"
|
||||
data-gravity-qualifier="Graven">
|
||||
data-gravity-qualifier="Graven"
|
||||
data-reversal="">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">K</span>
|
||||
</div>
|
||||
@@ -146,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);
|
||||
@@ -167,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 },
|
||||
}));
|
||||
@@ -193,173 +194,238 @@ 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 text when cautions list is empty", () => {
|
||||
card.dataset.cautions = "[]";
|
||||
openCaution();
|
||||
expect(cautionEffect.innerHTML).toContain("pending");
|
||||
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 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("renders first energy effect HTML including .card-ref spans", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: 'First <span class="card-ref">Card</span> effect.' }
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoEffect.querySelector(".card-ref")).not.toBeNull();
|
||||
expect(infoEffect.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("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("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("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("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("type element shows the entry type in allcaps", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "VOLUPTAS", effect: "..." }]);
|
||||
openFYI();
|
||||
expect(infoType.textContent).toBe("VOLUPTAS");
|
||||
});
|
||||
|
||||
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("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("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("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("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("with 1 entry both nav arrows are disabled", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Single." }]);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(true);
|
||||
expect(infoNext.disabled).toBe(true);
|
||||
});
|
||||
|
||||
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("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" },
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoPrev.disabled).toBe(false);
|
||||
expect(infoNext.disabled).toBe(false);
|
||||
});
|
||||
|
||||
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("next click advances to second entry", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
openFYI();
|
||||
infoNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(infoEffect.innerHTML).toContain("Second");
|
||||
});
|
||||
|
||||
it("card mouseleave closes the caution", () => {
|
||||
openCaution();
|
||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||
it("next wraps from last entry back to first", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Last" },
|
||||
]);
|
||||
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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Middle" },
|
||||
{ type: "VOLUPTAS", effect: "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.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "C1" },
|
||||
{ type: "NUMEN", effect: "C2" },
|
||||
{ type: "VOLUPTAS", effect: "C3" },
|
||||
]);
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("1 / 3");
|
||||
});
|
||||
|
||||
it("index label is empty when only 1 entry", () => {
|
||||
card.dataset.energies = JSON.stringify([{ type: "NUMEN", effect: "Only one." }]);
|
||||
openFYI();
|
||||
expect(infoIndex.textContent).toBe("");
|
||||
});
|
||||
|
||||
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 caution", () => {
|
||||
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||
openCaution();
|
||||
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
// Close and reopen
|
||||
it("opening again resets to first entry", () => {
|
||||
card.dataset.energies = JSON.stringify([
|
||||
{ type: "LIBIDO", effect: "First" },
|
||||
{ type: "NUMEN", effect: "Second" },
|
||||
]);
|
||||
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 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 original 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("FLIP 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);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Stat block: keyword population and FLIP toggle ────────────────── //
|
||||
// ── Stat block: keyword population and SPIN toggle ────────────────── //
|
||||
|
||||
describe("stat block and FLIP", () => {
|
||||
describe("stat block and SPIN", () => {
|
||||
beforeEach(() => makeFixture());
|
||||
|
||||
it("populates upright keywords when a card is hovered", () => {
|
||||
@@ -379,14 +445,14 @@ describe("SigSelect", () => {
|
||||
expect(items[1].textContent).toBe("disregard for consequences");
|
||||
});
|
||||
|
||||
it("FLIP click adds .is-reversed to the stat block", () => {
|
||||
it("SPIN 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", () => {
|
||||
it("second SPIN 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 }));
|
||||
@@ -401,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);
|
||||
@@ -416,12 +481,92 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPIN card animation — stage-card reversed state ──────────────────── //
|
||||
|
||||
describe("SPIN card animation", () => {
|
||||
function hover() {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
}
|
||||
|
||||
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
statBlock.querySelector(".sig-flip-btn")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("second SPIN click removes .stage-card--reversed", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("hovering a new card resets .stage-card--reversed", () => {
|
||||
makeFixture();
|
||||
hover();
|
||||
statBlock.querySelector(".sig-flip-btn")
|
||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
|
||||
makeFixture();
|
||||
card.dataset.reversal = "Nervous";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||
.toBe(card.dataset.nameTitle);
|
||||
});
|
||||
|
||||
it("updateStage() populates fan-card-reversal-qualifier with levity qualifier", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent)
|
||||
.toBe("Elevated");
|
||||
});
|
||||
|
||||
it("updateStage() populates fan-card-reversal-qualifier with gravity qualifier", () => {
|
||||
makeFixture({ polarity: "gravity", userRole: "BC" });
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent)
|
||||
.toBe("Graven");
|
||||
});
|
||||
|
||||
it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
|
||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||
card.dataset.reversal = "Vacant";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||
.toBe(card.dataset.nameTitle);
|
||||
});
|
||||
|
||||
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.nameTitle = "The Schizo";
|
||||
hover();
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("The Schizo");
|
||||
});
|
||||
|
||||
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();
|
||||
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());
|
||||
@@ -467,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());
|
||||
@@ -508,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);
|
||||
@@ -541,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("");
|
||||
@@ -573,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");
|
||||
});
|
||||
@@ -597,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");
|
||||
});
|
||||
@@ -611,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");
|
||||
});
|
||||
@@ -632,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();
|
||||
}
|
||||
|
||||
@@ -650,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);
|
||||
});
|
||||
|
||||
@@ -664,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();
|
||||
|
||||
@@ -677,10 +801,10 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,12 +23,18 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank"></span>
|
||||
@@ -36,27 +42,26 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
||||
</div>
|
||||
</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">FYI</button>
|
||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Upright</p>
|
||||
<p class="stat-face-label">Emanation</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>
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||
<div class="sig-caution-header">
|
||||
<h4 class="sig-caution-title">Caution!</h4>
|
||||
<p class="sig-caution-type">Rival Interaction</p>
|
||||
<div class="sig-info" id="id_sig_tooltip">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||
<p class="sig-caution-effect"></p>
|
||||
<span class="sig-caution-index"></span>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sig-caution-prev" type="button">PRV</button>
|
||||
<button class="btn btn-nav-right sig-caution-next" type="button">NXT</button>
|
||||
<button class="btn btn-nav-left sig-info-prev" type="button">PRV</button>
|
||||
<button class="btn btn-nav-right sig-info-next" type="button">NXT</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,9 +77,11 @@ 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-cautions="{{ card.cautions_json }}"
|
||||
data-energies="{{ card.energies_json }}"
|
||||
data-operations="{{ card.operations_json }}"
|
||||
data-levity-qualifier="{{ card.levity_qualifier }}"
|
||||
data-gravity-qualifier="{{ card.gravity_qualifier }}">
|
||||
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
||||
data-reversal="{{ card.reversal }}">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user