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 // // ---------------------------------------------------------------------- //