Sig select: _card-deck.scss extract, WS cursor fixes, own-role indicators, role icon refresh
- New _card-deck.scss: sig select styles moved out of _room.scss + _game-kit.scss - sig-select.js: 3 WS bug fixes — thumbs-up deferred to window.load (layout settled before getBoundingClientRect), hover cursor cleared for all cards on reservation (not just the reserved card), applyHover guards against already-reserved roles - Own-role indicators: gamer now sees their own role-coloured card outline + thumbs-up - Reservation glow: replaced blurry role+ninUser double-shadow with crisp 2px outline - Gravity qualifier: Graven text set to --terUser (matches Leavened/--quiUser pattern) - Role card SVGs refreshed; starter-role-Blank removed - FTs + Jasmine specs extended for sig select WS behaviour - setup_sig_session management command for multi-browser manual testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]' } = {}) {
|
||||
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
data-polarity="levity"
|
||||
data-user-role="PC"
|
||||
data-polarity="${polarity}"
|
||||
data-user-role="${userRole}"
|
||||
data-reserve-url="/epic/room/test/sig-reserve"
|
||||
data-reservations="${reservations.replace(/"/g, '"')}">
|
||||
<div class="sig-modal">
|
||||
@@ -15,7 +15,9 @@ describe("SigSelect", () => {
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="stage-suit-icon"></i>
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<p class="fan-card-correspondence"></p>
|
||||
</div>
|
||||
@@ -409,4 +411,198 @@ describe("SigSelect", () => {
|
||||
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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());
|
||||
|
||||
it("NC hover activates the --mid cursor", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 42, role: "NC", active: true },
|
||||
}));
|
||||
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
it("SC hover activates the --right cursor", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 42, role: "SC", active: true },
|
||||
}));
|
||||
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
it("own role (PC) hover event is ignored — no cursor activates", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 42, role: "PC", active: true },
|
||||
}));
|
||||
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
|
||||
});
|
||||
|
||||
it("hover-off removes .active from the cursor", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 42, role: "NC", active: true },
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 42, role: "NC", active: false },
|
||||
}));
|
||||
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
|
||||
});
|
||||
|
||||
it("hover on unknown card_id is a no-op", () => {
|
||||
expect(() => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||
detail: { card_id: 9999, role: "NC", active: true },
|
||||
}));
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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());
|
||||
|
||||
it("peer reservation sets data-reserved-by to the reserving role", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: true },
|
||||
}));
|
||||
expect(card.dataset.reservedBy).toBe("NC");
|
||||
});
|
||||
|
||||
it("peer reservation also adds .sig-reserved class", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: true },
|
||||
}));
|
||||
expect(card.classList.contains("sig-reserved")).toBe(true);
|
||||
});
|
||||
|
||||
it("release removes data-reserved-by", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: true },
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: false },
|
||||
}));
|
||||
expect(card.dataset.reservedBy).toBeUndefined();
|
||||
});
|
||||
|
||||
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "PC", reserved: true },
|
||||
}));
|
||||
expect(card.dataset.reservedBy).toBe("PC");
|
||||
expect(card.classList.contains("sig-reserved--own")).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
|
||||
});
|
||||
|
||||
it("peer release removes the thumbs-up float", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: true },
|
||||
}));
|
||||
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||
detail: { card_id: 42, role: "NC", reserved: false },
|
||||
}));
|
||||
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Polarity theming — stage qualifier text ────────────────────────────── //
|
||||
//
|
||||
// On mouseenter, updateStage() injects "Leavened" 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 'Leavened' 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("Leavened");
|
||||
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
|
||||
});
|
||||
|
||||
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
|
||||
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
|
||||
});
|
||||
|
||||
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dataset.nameTitle = "The Schizo";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
|
||||
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
|
||||
});
|
||||
|
||||
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
|
||||
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
|
||||
});
|
||||
|
||||
it("hovering clears qualifier slots from the previous card", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
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("Leavened");
|
||||
});
|
||||
|
||||
it("correspondence field is never populated", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dataset.correspondence = "Il Bagatto (Minchiate)";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user