tray: Tray.placeSig analogue of placeCard for SIG SELECT exit; rename arc-in → fade-in — TDD
After all 3 gamers in a polarity room confirm TAKE SIG and the 12s countdown
expires, sig-select.js's room:polarity_room_done handler now plays the same
tray-open / fade-in / tray-close sequence the role-select uses, then
dismisses the sig overlay & shows the waiting msg ("Gravity settling…" /
"Levity appraising…") on Tray.placeSig's completion callback. Visual order:
sig stage → tray slides in → sig fades into the second tray cell → tray
slides out → table hex w. waiting msg. Cross-polarity events (other room
finishing while we're still in our overlay) are no-op as before.
- tray.js: new Tray.placeSig(sourceEl, onComplete). Mutates the SECOND
.tray-cell in place (sig slot), copies aria-label / data-energies /
data-operations / corner-rank + suit-icon markup from the source
.sig-stage-card, then runs the shared open → fade-in → close sequence.
Extracted _runFadeInSequence helper so placeCard + placeSig share the
same animation glue. reset() now also clears .tray-sig-card from cells.
- _tray.scss: .tray-sig-card.fade-in > .sig-stage-card animates via the
existing tray-role-fade-in keyframes.
- sig-select.js polarity_room_done handler: Tray.placeSig(stageCard,
_settle); _settle runs the existing _dismissSigOverlay + _showWaitingMsg.
Falls back to immediate dismiss when Tray is undefined (test environments
without the tray).
- arc-in → fade-in rename across tray.js, role-select.js, _tray.scss
(incl. @keyframes tray-role-arc-in → tray-role-fade-in), TraySpec.js
spec descriptions + assertions, & test_room_role_select.py docstrings.
The original "arc-in" name suggested a curved-path animation; the actual
behaviour is a 1s opacity fade, so fade-in is the accurate label.
- TraySpec: 10 new placeSig specs mirroring placeCard (second-cell mutation,
data + markup copy, tabIndex, fade-in class, animationend-triggered close,
onComplete callback, landscape parity, reset cleanup).
- SigSelectSpec: 3 new specs (Tray.placeSig called w. stageCard on own
polarity; not called on other polarity; overlay dismiss deferred to the
Tray.placeSig completion callback).
344 specs / 4 pending green; RoleSelectTrayTest FT still green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,7 +120,7 @@ var RoleSelect = (function () {
|
|||||||
}
|
}
|
||||||
openFan();
|
openFan();
|
||||||
} else {
|
} else {
|
||||||
// Animate the role card into the tray: open, arc-in, force-close.
|
// Animate the role card into the tray: open, fade-in, force-close.
|
||||||
// Any turn_changed that arrived while the fetch was in-flight is
|
// Any turn_changed that arrived while the fetch was in-flight is
|
||||||
// queued in _pendingTurnChange and will run after onComplete.
|
// queued in _pendingTurnChange and will run after onComplete.
|
||||||
if (typeof Tray !== "undefined") {
|
if (typeof Tray !== "undefined") {
|
||||||
|
|||||||
@@ -529,8 +529,19 @@ var SigSelect = (function () {
|
|||||||
if (!overlay) return;
|
if (!overlay) return;
|
||||||
if (e.detail.polarity !== userPolarity) return;
|
if (e.detail.polarity !== userPolarity) return;
|
||||||
var pendingPolarity = userPolarity === 'levity' ? 'gravity' : 'levity';
|
var pendingPolarity = userPolarity === 'levity' ? 'gravity' : 'levity';
|
||||||
_dismissSigOverlay();
|
// Tray-place sequence first (visually anchors the sig stage's exit);
|
||||||
_showWaitingMsg(pendingPolarity);
|
// overlay dismissal + waiting msg run on Tray.placeSig's completion
|
||||||
|
// callback so the user sees: stage card → tray slides in → sig fades
|
||||||
|
// into the tray cell → tray slides out → table hex w. waiting msg.
|
||||||
|
function _settle() {
|
||||||
|
_dismissSigOverlay();
|
||||||
|
_showWaitingMsg(pendingPolarity);
|
||||||
|
}
|
||||||
|
if (typeof Tray !== "undefined" && Tray.placeSig) {
|
||||||
|
Tray.placeSig(stageCard, _settle);
|
||||||
|
} else {
|
||||||
|
_settle();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('room:pick_sky_available', function () {
|
window.addEventListener('room:pick_sky_available', function () {
|
||||||
|
|||||||
@@ -243,18 +243,18 @@ var Tray = (function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// _arcIn — add .arc-in to cardEl, wait for animationend, remove it, call onComplete.
|
// _fadeIn — add .fade-in to cardEl, wait for animationend, remove it, call onComplete.
|
||||||
function _arcIn(cardEl, onComplete) {
|
function _fadeIn(cardEl, onComplete) {
|
||||||
cardEl.classList.add('arc-in');
|
cardEl.classList.add('fade-in');
|
||||||
cardEl.addEventListener('animationend', function handler() {
|
cardEl.addEventListener('animationend', function handler() {
|
||||||
cardEl.removeEventListener('animationend', handler);
|
cardEl.removeEventListener('animationend', handler);
|
||||||
cardEl.classList.remove('arc-in');
|
cardEl.classList.remove('fade-in');
|
||||||
if (onComplete) onComplete();
|
if (onComplete) onComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// placeCard(roleCode, onComplete) — mark the first tray cell with the role,
|
// placeCard(roleCode, onComplete) — mark the first tray cell with the role,
|
||||||
// open the tray, arc-in the cell, then animated-close. Calls onComplete after
|
// open the tray, fade-in the cell, then animated-close. Calls onComplete after
|
||||||
// the close slide finishes (transitionend), with a fallback timeout in case
|
// the close slide finishes (transitionend), with a fallback timeout in case
|
||||||
// CSS transitions are disabled (e.g. test environments).
|
// CSS transitions are disabled (e.g. test environments).
|
||||||
// The grid always contains exactly 8 .tray-cell elements (from the template);
|
// The grid always contains exactly 8 .tray-cell elements (from the template);
|
||||||
@@ -276,8 +276,56 @@ var Tray = (function () {
|
|||||||
firstCell.appendChild(img);
|
firstCell.appendChild(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_runFadeInSequence(firstCell, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeSig(sourceEl, onComplete) — analogue of placeCard, applied to the
|
||||||
|
// second tray cell. Copies the user's selected sig-stage-card data + inner
|
||||||
|
// markup so the tray sig card renders identically to the page-load case
|
||||||
|
// (room.html: <div class="sig-stage-card sea-sig-card" aria-label data-energies
|
||||||
|
// data-operations><span class="fan-corner-rank">…</span><i class="fa-solid …"></i></div>).
|
||||||
|
// The grid keeps exactly 8 .tray-cell elements; the second cell is mutated
|
||||||
|
// in place rather than inserting a new element.
|
||||||
|
function placeSig(sourceEl, onComplete) {
|
||||||
|
if (!_grid) { if (onComplete) onComplete(); return; }
|
||||||
|
var secondCell = _grid.children[1];
|
||||||
|
if (!secondCell) { if (onComplete) onComplete(); return; }
|
||||||
|
|
||||||
|
secondCell.classList.add('tray-sig-card');
|
||||||
|
secondCell.tabIndex = 0;
|
||||||
|
secondCell.textContent = '';
|
||||||
|
|
||||||
|
var stage = document.createElement('div');
|
||||||
|
stage.className = 'sig-stage-card sea-sig-card';
|
||||||
|
if (sourceEl) {
|
||||||
|
var aria = sourceEl.getAttribute && sourceEl.getAttribute('aria-label');
|
||||||
|
if (aria) stage.setAttribute('aria-label', aria);
|
||||||
|
if (sourceEl.dataset && sourceEl.dataset.energies) {
|
||||||
|
stage.dataset.energies = sourceEl.dataset.energies;
|
||||||
|
}
|
||||||
|
if (sourceEl.dataset && sourceEl.dataset.operations) {
|
||||||
|
stage.dataset.operations = sourceEl.dataset.operations;
|
||||||
|
}
|
||||||
|
// Carry over rank + suit-icon markup from the source. We copy
|
||||||
|
// those two specific children rather than the whole innerHTML so
|
||||||
|
// the source's larger fan-card-face-* / qualifier markup doesn't
|
||||||
|
// leak into the tray card layout.
|
||||||
|
var rank = sourceEl.querySelector && sourceEl.querySelector('.fan-corner-rank');
|
||||||
|
if (rank) stage.appendChild(rank.cloneNode(true));
|
||||||
|
var icon = sourceEl.querySelector && sourceEl.querySelector('i.fa-solid');
|
||||||
|
if (icon) stage.appendChild(icon.cloneNode(true));
|
||||||
|
}
|
||||||
|
secondCell.appendChild(stage);
|
||||||
|
|
||||||
|
_runFadeInSequence(secondCell, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared open → fade-in → close → onComplete sequence for placeCard /
|
||||||
|
// placeSig. cardEl is the .tray-cell that just received the role / sig
|
||||||
|
// class + content.
|
||||||
|
function _runFadeInSequence(cardEl, onComplete) {
|
||||||
open();
|
open();
|
||||||
_arcIn(firstCell, function () {
|
_fadeIn(cardEl, function () {
|
||||||
close();
|
close();
|
||||||
if (!onComplete) return;
|
if (!onComplete) return;
|
||||||
if (!_wrap) { onComplete(); return; }
|
if (!_wrap) { onComplete(); return; }
|
||||||
@@ -518,7 +566,7 @@ var Tray = (function () {
|
|||||||
// Clear any role-card state from tray cells (Jasmine afterEach)
|
// Clear any role-card state from tray cells (Jasmine afterEach)
|
||||||
if (_grid) {
|
if (_grid) {
|
||||||
_grid.querySelectorAll('.tray-cell').forEach(function (el) {
|
_grid.querySelectorAll('.tray-cell').forEach(function (el) {
|
||||||
el.classList.remove('tray-role-card', 'arc-in');
|
el.classList.remove('tray-role-card', 'tray-sig-card', 'fade-in');
|
||||||
el.removeAttribute('tabindex');
|
el.removeAttribute('tabindex');
|
||||||
el.textContent = '';
|
el.textContent = '';
|
||||||
delete el.dataset.role;
|
delete el.dataset.role;
|
||||||
@@ -543,6 +591,7 @@ var Tray = (function () {
|
|||||||
forceClose: forceClose,
|
forceClose: forceClose,
|
||||||
isOpen: isOpen,
|
isOpen: isOpen,
|
||||||
placeCard: placeCard,
|
placeCard: placeCard,
|
||||||
|
placeSig: placeSig,
|
||||||
reset: reset,
|
reset: reset,
|
||||||
_testSetLandscape: function (v) { _landscapeOverride = v; },
|
_testSetLandscape: function (v) { _landscapeOverride = v; },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ class RoleSelectTest(FunctionalTest):
|
|||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
lambda: self.assertFalse(
|
||||||
self.browser.execute_script("return Tray.isOpen()"),
|
self.browser.execute_script("return Tray.isOpen()"),
|
||||||
"Tray should close after arc-in sequence",
|
"Tray should close after fade-in sequence",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -636,7 +636,7 @@ class RoleSelectTrayTest(FunctionalTest):
|
|||||||
def test_portrait_role_card_enters_topmost_grid_square(self):
|
def test_portrait_role_card_enters_topmost_grid_square(self):
|
||||||
"""Portrait: after confirming a role the first .tray-cell gets
|
"""Portrait: after confirming a role the first .tray-cell gets
|
||||||
.tray-role-card; the grid still has exactly 8 cells; and the tray
|
.tray-role-card; the grid still has exactly 8 cells; and the tray
|
||||||
opens briefly then closes once the arc-in animation completes."""
|
opens briefly then closes once the fade-in animation completes."""
|
||||||
self.browser.set_window_size(390, 844)
|
self.browser.set_window_size(390, 844)
|
||||||
room = self._make_room()
|
room = self._make_room()
|
||||||
self.create_pre_authenticated_session("slot1@test.io")
|
self.create_pre_authenticated_session("slot1@test.io")
|
||||||
@@ -669,7 +669,7 @@ class RoleSelectTrayTest(FunctionalTest):
|
|||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
lambda: self.assertFalse(
|
||||||
self.browser.execute_script("return Tray.isOpen()"),
|
self.browser.execute_script("return Tray.isOpen()"),
|
||||||
"Tray should close after the arc-in sequence"
|
"Tray should close after the fade-in sequence"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -709,7 +709,7 @@ class RoleSelectTrayTest(FunctionalTest):
|
|||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
lambda: self.assertFalse(
|
||||||
self.browser.execute_script("return Tray.isOpen()"),
|
self.browser.execute_script("return Tray.isOpen()"),
|
||||||
"Tray should close after the arc-in sequence"
|
"Tray should close after the fade-in sequence"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -809,4 +809,63 @@ describe("SigSelect", () => {
|
|||||||
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||||
|
//
|
||||||
|
// After all 3 gamers in the user's polarity confirm TAKE SIG and the
|
||||||
|
// 12s countdown expires, the server fires room:polarity_room_done. The
|
||||||
|
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
||||||
|
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
||||||
|
// callback, dismiss the overlay and show the waiting message. Tray runs
|
||||||
|
// FIRST, while the overlay is still up, so the slide is visually anchored
|
||||||
|
// to the sig stage's exit.
|
||||||
|
//
|
||||||
|
// Cross-polarity events (the OTHER room finishing while we're still
|
||||||
|
// selecting) must NOT trigger the sequence.
|
||||||
|
|
||||||
|
describe("polarity_room_done → tray sequence", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// .table-center is appended to by _showWaitingMsg in the dismiss
|
||||||
|
// path; provide one so dismissal doesn't error.
|
||||||
|
const center = document.createElement("div");
|
||||||
|
center.className = "table-center";
|
||||||
|
document.body.appendChild(center);
|
||||||
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
|
spyOn(Tray, "placeSig");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.querySelectorAll(".table-center, #id_hex_waiting_msg")
|
||||||
|
.forEach((el) => el.remove());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls Tray.placeSig with the stage card when own polarity finishes", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "levity" },
|
||||||
|
}));
|
||||||
|
expect(Tray.placeSig).toHaveBeenCalled();
|
||||||
|
const arg = Tray.placeSig.calls.mostRecent().args[0];
|
||||||
|
expect(arg).toBe(stageCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT call Tray.placeSig when the OTHER polarity finishes", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "gravity" },
|
||||||
|
}));
|
||||||
|
expect(Tray.placeSig).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses the overlay only AFTER Tray.placeSig's callback fires", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "levity" },
|
||||||
|
}));
|
||||||
|
// Overlay still mounted — dismissal deferred to tray callback.
|
||||||
|
expect(document.querySelector(".sig-overlay")).not.toBe(null);
|
||||||
|
|
||||||
|
const cb = Tray.placeSig.calls.mostRecent().args[1];
|
||||||
|
cb();
|
||||||
|
expect(document.querySelector(".sig-overlay")).toBe(null);
|
||||||
|
expect(document.getElementById("id_hex_waiting_msg")).not.toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ describe("Tray", () => {
|
|||||||
// placeCard(roleCode, onComplete):
|
// placeCard(roleCode, onComplete):
|
||||||
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
|
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
|
||||||
// 2. Opens the tray.
|
// 2. Opens the tray.
|
||||||
// 3. Arc-in animates the cell (.arc-in class, animationend fires).
|
// 3. Fade-in animates the cell (.fade-in class, animationend fires).
|
||||||
// 4. forceClose() — tray closes instantly.
|
// 4. forceClose() — tray closes instantly.
|
||||||
// 5. Calls onComplete.
|
// 5. Calls onComplete.
|
||||||
//
|
//
|
||||||
@@ -485,16 +485,16 @@ describe("Tray", () => {
|
|||||||
expect(Tray.isOpen()).toBe(true);
|
expect(Tray.isOpen()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds .arc-in to the first cell", () => {
|
it("adds .fade-in to the first cell", () => {
|
||||||
Tray.placeCard("PC", null);
|
Tray.placeCard("PC", null);
|
||||||
expect(firstCell.classList.contains("arc-in")).toBe(true);
|
expect(firstCell.classList.contains("fade-in")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes .arc-in and closes after animationend", () => {
|
it("removes .fade-in and closes after animationend", () => {
|
||||||
Tray.placeCard("PC", null);
|
Tray.placeCard("PC", null);
|
||||||
expect(Tray.isOpen()).toBe(true);
|
expect(Tray.isOpen()).toBe(true);
|
||||||
firstCell.dispatchEvent(new Event("animationend"));
|
firstCell.dispatchEvent(new Event("animationend"));
|
||||||
expect(firstCell.classList.contains("arc-in")).toBe(false);
|
expect(firstCell.classList.contains("fade-in")).toBe(false);
|
||||||
expect(Tray.isOpen()).toBe(false);
|
expect(Tray.isOpen()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,6 +531,116 @@ describe("Tray", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// placeSig() //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
//
|
||||||
|
// placeSig(sourceEl, onComplete) — analogue of placeCard, applied to the
|
||||||
|
// SECOND tray cell (the sig slot). sourceEl is the user's selected sig
|
||||||
|
// .sig-stage-card (during sig-select), whose data-* attrs and inner
|
||||||
|
// markup are copied into the tray's .sig-stage-card.sea-sig-card.
|
||||||
|
//
|
||||||
|
// Sequence: open → fade-in → animationend → close → onComplete.
|
||||||
|
|
||||||
|
describe("placeSig()", () => {
|
||||||
|
let grid, firstCell, secondCell, sourceEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
grid = document.createElement("div");
|
||||||
|
grid.id = "id_tray_grid";
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "tray-cell";
|
||||||
|
grid.appendChild(cell);
|
||||||
|
}
|
||||||
|
document.body.appendChild(grid);
|
||||||
|
Tray.init();
|
||||||
|
firstCell = grid.children[0];
|
||||||
|
secondCell = grid.children[1];
|
||||||
|
|
||||||
|
// Source: the user's selected sig stage card during sig-select.
|
||||||
|
// Carries the same data-* shape that StageCard.fromDataset reads
|
||||||
|
// and the same rank+icon child markup the tray template renders.
|
||||||
|
sourceEl = document.createElement("div");
|
||||||
|
sourceEl.className = "sig-stage-card";
|
||||||
|
sourceEl.setAttribute("aria-label", "The Tester");
|
||||||
|
sourceEl.dataset.energies = '[{"type":"TESTLIBIDO","effect":"e1"}]';
|
||||||
|
sourceEl.dataset.operations = '[{"type":"TESTOP","effect":"o1"}]';
|
||||||
|
sourceEl.innerHTML =
|
||||||
|
'<span class="fan-corner-rank">XCVIII</span>' +
|
||||||
|
'<i class="fa-solid fa-flask"></i>';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => grid.remove());
|
||||||
|
|
||||||
|
it("adds .tray-sig-card to the SECOND .tray-cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(true);
|
||||||
|
expect(firstCell.classList.contains("tray-sig-card")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets tabIndex=0 on the placed cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.tabIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the .sig-stage-card.sea-sig-card child w. copied data + markup", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
const stage = secondCell.querySelector(".sig-stage-card.sea-sig-card");
|
||||||
|
expect(stage).not.toBe(null);
|
||||||
|
expect(stage.getAttribute("aria-label")).toBe("The Tester");
|
||||||
|
expect(stage.dataset.energies).toBe(sourceEl.dataset.energies);
|
||||||
|
expect(stage.dataset.operations).toBe(sourceEl.dataset.operations);
|
||||||
|
expect(stage.querySelector(".fan-corner-rank").textContent).toBe("XCVIII");
|
||||||
|
expect(stage.querySelector("i.fa-solid")).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("grid cell count stays at 8", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(grid.children.length).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the tray", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .fade-in to the placed cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("fade-in")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .fade-in and closes after animationend", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
secondCell.dispatchEvent(new Event("animationend"));
|
||||||
|
expect(secondCell.classList.contains("fade-in")).toBe(false);
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onComplete after the tray closes", () => {
|
||||||
|
let called = false;
|
||||||
|
Tray.placeSig(sourceEl, () => { called = true; });
|
||||||
|
secondCell.dispatchEvent(new Event("animationend"));
|
||||||
|
const te = new Event("transitionend");
|
||||||
|
te.propertyName = "left";
|
||||||
|
wrap.dispatchEvent(te);
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("landscape: same behaviour — second cell gets sig card", () => {
|
||||||
|
Tray._testSetLandscape(true);
|
||||||
|
Tray.init();
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reset() removes .tray-sig-card from cells", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
Tray.reset();
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
// init() — focusable tray cards //
|
// init() — focusable tray cards //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Role card: scrawl fade-in ───────────────────────────────────────────────
|
// ─── Role card: scrawl fade-in ───────────────────────────────────────────────
|
||||||
@keyframes tray-role-arc-in {
|
@keyframes tray-role-fade-in {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
@@ -162,8 +162,8 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cell stays static; only the scrawl image fades in.
|
// Cell stays static; only the scrawl image fades in.
|
||||||
&.arc-in img {
|
&.fade-in img {
|
||||||
animation: tray-role-arc-in 1s ease forwards;
|
animation: tray-role-fade-in 1s ease forwards;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +191,12 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
|
|||||||
&.tt-active > .sig-stage-card {
|
&.tt-active > .sig-stage-card {
|
||||||
rotate: 7deg;
|
rotate: 7deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fade-in mirrors .tray-role-card.fade-in img — the .sig-stage-card child
|
||||||
|
// fades from opacity 0 to 1 once placeSig() lands.
|
||||||
|
&.fade-in > .sig-stage-card {
|
||||||
|
animation: tray-role-fade-in 1s ease forwards;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes tray-wobble {
|
@keyframes tray-wobble {
|
||||||
|
|||||||
@@ -809,4 +809,63 @@ describe("SigSelect", () => {
|
|||||||
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
expect(takeSigBtn.classList.contains("btn-cancel")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||||
|
//
|
||||||
|
// After all 3 gamers in the user's polarity confirm TAKE SIG and the
|
||||||
|
// 12s countdown expires, the server fires room:polarity_room_done. The
|
||||||
|
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
||||||
|
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
||||||
|
// callback, dismiss the overlay and show the waiting message. Tray runs
|
||||||
|
// FIRST, while the overlay is still up, so the slide is visually anchored
|
||||||
|
// to the sig stage's exit.
|
||||||
|
//
|
||||||
|
// Cross-polarity events (the OTHER room finishing while we're still
|
||||||
|
// selecting) must NOT trigger the sequence.
|
||||||
|
|
||||||
|
describe("polarity_room_done → tray sequence", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// .table-center is appended to by _showWaitingMsg in the dismiss
|
||||||
|
// path; provide one so dismissal doesn't error.
|
||||||
|
const center = document.createElement("div");
|
||||||
|
center.className = "table-center";
|
||||||
|
document.body.appendChild(center);
|
||||||
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
|
spyOn(Tray, "placeSig");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.querySelectorAll(".table-center, #id_hex_waiting_msg")
|
||||||
|
.forEach((el) => el.remove());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls Tray.placeSig with the stage card when own polarity finishes", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "levity" },
|
||||||
|
}));
|
||||||
|
expect(Tray.placeSig).toHaveBeenCalled();
|
||||||
|
const arg = Tray.placeSig.calls.mostRecent().args[0];
|
||||||
|
expect(arg).toBe(stageCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT call Tray.placeSig when the OTHER polarity finishes", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "gravity" },
|
||||||
|
}));
|
||||||
|
expect(Tray.placeSig).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses the overlay only AFTER Tray.placeSig's callback fires", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:polarity_room_done", {
|
||||||
|
detail: { polarity: "levity" },
|
||||||
|
}));
|
||||||
|
// Overlay still mounted — dismissal deferred to tray callback.
|
||||||
|
expect(document.querySelector(".sig-overlay")).not.toBe(null);
|
||||||
|
|
||||||
|
const cb = Tray.placeSig.calls.mostRecent().args[1];
|
||||||
|
cb();
|
||||||
|
expect(document.querySelector(".sig-overlay")).toBe(null);
|
||||||
|
expect(document.getElementById("id_hex_waiting_msg")).not.toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ describe("Tray", () => {
|
|||||||
// placeCard(roleCode, onComplete):
|
// placeCard(roleCode, onComplete):
|
||||||
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
|
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
|
||||||
// 2. Opens the tray.
|
// 2. Opens the tray.
|
||||||
// 3. Arc-in animates the cell (.arc-in class, animationend fires).
|
// 3. Fade-in animates the cell (.fade-in class, animationend fires).
|
||||||
// 4. forceClose() — tray closes instantly.
|
// 4. forceClose() — tray closes instantly.
|
||||||
// 5. Calls onComplete.
|
// 5. Calls onComplete.
|
||||||
//
|
//
|
||||||
@@ -485,16 +485,16 @@ describe("Tray", () => {
|
|||||||
expect(Tray.isOpen()).toBe(true);
|
expect(Tray.isOpen()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds .arc-in to the first cell", () => {
|
it("adds .fade-in to the first cell", () => {
|
||||||
Tray.placeCard("PC", null);
|
Tray.placeCard("PC", null);
|
||||||
expect(firstCell.classList.contains("arc-in")).toBe(true);
|
expect(firstCell.classList.contains("fade-in")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes .arc-in and closes after animationend", () => {
|
it("removes .fade-in and closes after animationend", () => {
|
||||||
Tray.placeCard("PC", null);
|
Tray.placeCard("PC", null);
|
||||||
expect(Tray.isOpen()).toBe(true);
|
expect(Tray.isOpen()).toBe(true);
|
||||||
firstCell.dispatchEvent(new Event("animationend"));
|
firstCell.dispatchEvent(new Event("animationend"));
|
||||||
expect(firstCell.classList.contains("arc-in")).toBe(false);
|
expect(firstCell.classList.contains("fade-in")).toBe(false);
|
||||||
expect(Tray.isOpen()).toBe(false);
|
expect(Tray.isOpen()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,6 +531,116 @@ describe("Tray", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// placeSig() //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
//
|
||||||
|
// placeSig(sourceEl, onComplete) — analogue of placeCard, applied to the
|
||||||
|
// SECOND tray cell (the sig slot). sourceEl is the user's selected sig
|
||||||
|
// .sig-stage-card (during sig-select), whose data-* attrs and inner
|
||||||
|
// markup are copied into the tray's .sig-stage-card.sea-sig-card.
|
||||||
|
//
|
||||||
|
// Sequence: open → fade-in → animationend → close → onComplete.
|
||||||
|
|
||||||
|
describe("placeSig()", () => {
|
||||||
|
let grid, firstCell, secondCell, sourceEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
grid = document.createElement("div");
|
||||||
|
grid.id = "id_tray_grid";
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "tray-cell";
|
||||||
|
grid.appendChild(cell);
|
||||||
|
}
|
||||||
|
document.body.appendChild(grid);
|
||||||
|
Tray.init();
|
||||||
|
firstCell = grid.children[0];
|
||||||
|
secondCell = grid.children[1];
|
||||||
|
|
||||||
|
// Source: the user's selected sig stage card during sig-select.
|
||||||
|
// Carries the same data-* shape that StageCard.fromDataset reads
|
||||||
|
// and the same rank+icon child markup the tray template renders.
|
||||||
|
sourceEl = document.createElement("div");
|
||||||
|
sourceEl.className = "sig-stage-card";
|
||||||
|
sourceEl.setAttribute("aria-label", "The Tester");
|
||||||
|
sourceEl.dataset.energies = '[{"type":"TESTLIBIDO","effect":"e1"}]';
|
||||||
|
sourceEl.dataset.operations = '[{"type":"TESTOP","effect":"o1"}]';
|
||||||
|
sourceEl.innerHTML =
|
||||||
|
'<span class="fan-corner-rank">XCVIII</span>' +
|
||||||
|
'<i class="fa-solid fa-flask"></i>';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => grid.remove());
|
||||||
|
|
||||||
|
it("adds .tray-sig-card to the SECOND .tray-cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(true);
|
||||||
|
expect(firstCell.classList.contains("tray-sig-card")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets tabIndex=0 on the placed cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.tabIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the .sig-stage-card.sea-sig-card child w. copied data + markup", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
const stage = secondCell.querySelector(".sig-stage-card.sea-sig-card");
|
||||||
|
expect(stage).not.toBe(null);
|
||||||
|
expect(stage.getAttribute("aria-label")).toBe("The Tester");
|
||||||
|
expect(stage.dataset.energies).toBe(sourceEl.dataset.energies);
|
||||||
|
expect(stage.dataset.operations).toBe(sourceEl.dataset.operations);
|
||||||
|
expect(stage.querySelector(".fan-corner-rank").textContent).toBe("XCVIII");
|
||||||
|
expect(stage.querySelector("i.fa-solid")).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("grid cell count stays at 8", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(grid.children.length).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the tray", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .fade-in to the placed cell", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("fade-in")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .fade-in and closes after animationend", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
secondCell.dispatchEvent(new Event("animationend"));
|
||||||
|
expect(secondCell.classList.contains("fade-in")).toBe(false);
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onComplete after the tray closes", () => {
|
||||||
|
let called = false;
|
||||||
|
Tray.placeSig(sourceEl, () => { called = true; });
|
||||||
|
secondCell.dispatchEvent(new Event("animationend"));
|
||||||
|
const te = new Event("transitionend");
|
||||||
|
te.propertyName = "left";
|
||||||
|
wrap.dispatchEvent(te);
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("landscape: same behaviour — second cell gets sig card", () => {
|
||||||
|
Tray._testSetLandscape(true);
|
||||||
|
Tray.init();
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reset() removes .tray-sig-card from cells", () => {
|
||||||
|
Tray.placeSig(sourceEl, null);
|
||||||
|
Tray.reset();
|
||||||
|
expect(secondCell.classList.contains("tray-sig-card")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
// init() — focusable tray cards //
|
// init() — focusable tray cards //
|
||||||
// ---------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------- //
|
||||||
|
|||||||
Reference in New Issue
Block a user