diff --git a/src/apps/epic/static/apps/epic/role-select.js b/src/apps/epic/static/apps/epic/role-select.js
index 454d567..9719016 100644
--- a/src/apps/epic/static/apps/epic/role-select.js
+++ b/src/apps/epic/static/apps/epic/role-select.js
@@ -120,7 +120,7 @@ var RoleSelect = (function () {
}
openFan();
} 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
// queued in _pendingTurnChange and will run after onComplete.
if (typeof Tray !== "undefined") {
diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js
index b8105b3..4109582 100644
--- a/src/apps/epic/static/apps/epic/sig-select.js
+++ b/src/apps/epic/static/apps/epic/sig-select.js
@@ -529,8 +529,19 @@ var SigSelect = (function () {
if (!overlay) return;
if (e.detail.polarity !== userPolarity) return;
var pendingPolarity = userPolarity === 'levity' ? 'gravity' : 'levity';
- _dismissSigOverlay();
- _showWaitingMsg(pendingPolarity);
+ // Tray-place sequence first (visually anchors the sig stage's exit);
+ // 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 () {
diff --git a/src/apps/epic/static/apps/epic/tray.js b/src/apps/epic/static/apps/epic/tray.js
index 6db925b..ec0bb26 100644
--- a/src/apps/epic/static/apps/epic/tray.js
+++ b/src/apps/epic/static/apps/epic/tray.js
@@ -243,18 +243,18 @@ var Tray = (function () {
});
}
- // _arcIn — add .arc-in to cardEl, wait for animationend, remove it, call onComplete.
- function _arcIn(cardEl, onComplete) {
- cardEl.classList.add('arc-in');
+ // _fadeIn — add .fade-in to cardEl, wait for animationend, remove it, call onComplete.
+ function _fadeIn(cardEl, onComplete) {
+ cardEl.classList.add('fade-in');
cardEl.addEventListener('animationend', function handler() {
cardEl.removeEventListener('animationend', handler);
- cardEl.classList.remove('arc-in');
+ cardEl.classList.remove('fade-in');
if (onComplete) onComplete();
});
}
// 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
// CSS transitions are disabled (e.g. test environments).
// The grid always contains exactly 8 .tray-cell elements (from the template);
@@ -276,8 +276,56 @@ var Tray = (function () {
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:
…
).
+ // 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();
- _arcIn(firstCell, function () {
+ _fadeIn(cardEl, function () {
close();
if (!onComplete) return;
if (!_wrap) { onComplete(); return; }
@@ -518,7 +566,7 @@ var Tray = (function () {
// Clear any role-card state from tray cells (Jasmine afterEach)
if (_grid) {
_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.textContent = '';
delete el.dataset.role;
@@ -543,6 +591,7 @@ var Tray = (function () {
forceClose: forceClose,
isOpen: isOpen,
placeCard: placeCard,
+ placeSig: placeSig,
reset: reset,
_testSetLandscape: function (v) { _landscapeOverride = v; },
};
diff --git a/src/functional_tests/test_room_role_select.py b/src/functional_tests/test_room_role_select.py
index 011c090..09c7bb9 100644
--- a/src/functional_tests/test_room_role_select.py
+++ b/src/functional_tests/test_room_role_select.py
@@ -564,7 +564,7 @@ class RoleSelectTest(FunctionalTest):
self.wait_for(
lambda: self.assertFalse(
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):
"""Portrait: after confirming a role the first .tray-cell gets
.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)
room = self._make_room()
self.create_pre_authenticated_session("slot1@test.io")
@@ -669,7 +669,7 @@ class RoleSelectTrayTest(FunctionalTest):
self.wait_for(
lambda: self.assertFalse(
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(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
- "Tray should close after the arc-in sequence"
+ "Tray should close after the fade-in sequence"
)
)
diff --git a/src/static/tests/SigSelectSpec.js b/src/static/tests/SigSelectSpec.js
index 68c4e54..cf3da29 100644
--- a/src/static/tests/SigSelectSpec.js
+++ b/src/static/tests/SigSelectSpec.js
@@ -809,4 +809,63 @@ describe("SigSelect", () => {
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);
+ });
+ });
});
diff --git a/src/static/tests/TraySpec.js b/src/static/tests/TraySpec.js
index 1e876e1..6058e06 100644
--- a/src/static/tests/TraySpec.js
+++ b/src/static/tests/TraySpec.js
@@ -430,7 +430,7 @@ describe("Tray", () => {
// placeCard(roleCode, onComplete):
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
// 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.
// 5. Calls onComplete.
//
@@ -485,16 +485,16 @@ describe("Tray", () => {
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);
- 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);
expect(Tray.isOpen()).toBe(true);
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);
});
@@ -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 =
+ 'XCVIII' +
+ '';
+ });
+
+ 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 //
// ---------------------------------------------------------------------- //
diff --git a/src/static_src/scss/_tray.scss b/src/static_src/scss/_tray.scss
index 8f8709e..115bb1e 100644
--- a/src/static_src/scss/_tray.scss
+++ b/src/static_src/scss/_tray.scss
@@ -123,7 +123,7 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this
}
// ─── Role card: scrawl fade-in ───────────────────────────────────────────────
-@keyframes tray-role-arc-in {
+@keyframes tray-role-fade-in {
from { opacity: 0; }
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.
- &.arc-in img {
- animation: tray-role-arc-in 1s ease forwards;
+ &.fade-in img {
+ 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 {
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 {
diff --git a/src/static_src/tests/SigSelectSpec.js b/src/static_src/tests/SigSelectSpec.js
index 68c4e54..cf3da29 100644
--- a/src/static_src/tests/SigSelectSpec.js
+++ b/src/static_src/tests/SigSelectSpec.js
@@ -809,4 +809,63 @@ describe("SigSelect", () => {
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);
+ });
+ });
});
diff --git a/src/static_src/tests/TraySpec.js b/src/static_src/tests/TraySpec.js
index 1e876e1..6058e06 100644
--- a/src/static_src/tests/TraySpec.js
+++ b/src/static_src/tests/TraySpec.js
@@ -430,7 +430,7 @@ describe("Tray", () => {
// placeCard(roleCode, onComplete):
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
// 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.
// 5. Calls onComplete.
//
@@ -485,16 +485,16 @@ describe("Tray", () => {
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);
- 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);
expect(Tray.isOpen()).toBe(true);
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);
});
@@ -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 =
+ 'XCVIII' +
+ '';
+ });
+
+ 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 //
// ---------------------------------------------------------------------- //