demo'd old inventory area in room.html to make way for new content (hex table now centered in view); old test suite now targets Role card in #id_tray cells where appropriate, or skips Sig card select until aforementioned new feature deployed; new scripts & jasmine tests too; removed one irrelevant test case from apps.epic.tests.ITs.test_views.SelectRoleViewTest
This commit is contained in:
@@ -3,6 +3,11 @@ var RoleSelect = (function () {
|
||||
// ahead of the fetch response doesn't get overridden by Tray.open().
|
||||
var _turnChangedBeforeFetch = false;
|
||||
|
||||
// Set to true while placeCard animation is running. handleTurnChanged
|
||||
// defers its work until the animation completes.
|
||||
var _animationPending = false;
|
||||
var _pendingTurnChange = null;
|
||||
|
||||
var ROLES = [
|
||||
{ code: "PC", name: "Player", element: "Fire" },
|
||||
{ code: "BC", name: "Builder", element: "Stone" },
|
||||
@@ -27,18 +32,10 @@ var RoleSelect = (function () {
|
||||
if (backdrop) backdrop.remove();
|
||||
}
|
||||
|
||||
function selectRole(roleCode, cardEl) {
|
||||
function selectRole(roleCode) {
|
||||
_turnChangedBeforeFetch = false; // fresh selection, reset the race flag
|
||||
var invCard = cardEl.cloneNode(true);
|
||||
invCard.classList.add("flipped");
|
||||
// strip old event listeners from the clone by replacing with a clean copy
|
||||
var clean = invCard.cloneNode(true);
|
||||
|
||||
closeFan();
|
||||
|
||||
var invSlot = document.getElementById("id_inv_role_card");
|
||||
if (invSlot) invSlot.appendChild(clean);
|
||||
|
||||
// Immediately lock the stack — do not wait for WS turn_changed
|
||||
var stack = document.querySelector(".card-stack[data-starter-roles]");
|
||||
if (stack) {
|
||||
@@ -60,24 +57,27 @@ var RoleSelect = (function () {
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
// Server rejected (role already taken) — undo optimistic update
|
||||
if (invSlot && invSlot.contains(clean)) invSlot.removeChild(clean);
|
||||
if (stack) {
|
||||
stack.dataset.starterRoles = stack.dataset.starterRoles
|
||||
.split(",").filter(function (r) { return r.trim() !== roleCode; }).join(",");
|
||||
}
|
||||
openFan();
|
||||
} else {
|
||||
// Place role card in tray grid and open the tray
|
||||
var grid = document.getElementById("id_tray_grid");
|
||||
if (grid) {
|
||||
var trayCard = document.createElement("div");
|
||||
trayCard.className = "tray-cell tray-role-card";
|
||||
trayCard.dataset.role = roleCode;
|
||||
grid.insertBefore(trayCard, grid.firstChild);
|
||||
// Always animate the role card into the tray, even if turn_changed
|
||||
// already arrived. placeCard opens the tray, arcs the card in,
|
||||
// then force-closes — so the user always sees their role card land.
|
||||
// If turn_changed arrived before the fetch, handleTurnChanged already
|
||||
// ran; _pendingTurnChange will be null and onComplete is a no-op.
|
||||
if (typeof Tray !== "undefined") {
|
||||
_animationPending = true;
|
||||
Tray.placeCard(roleCode, function () {
|
||||
_animationPending = false;
|
||||
if (_pendingTurnChange) {
|
||||
var ev = _pendingTurnChange;
|
||||
_pendingTurnChange = null;
|
||||
handleTurnChanged(ev);
|
||||
}
|
||||
// Only open if turn_changed hasn't already arrived and closed it.
|
||||
if (typeof Tray !== "undefined" && !_turnChangedBeforeFetch) {
|
||||
Tray.open();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -135,7 +135,7 @@ var RoleSelect = (function () {
|
||||
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?",
|
||||
function () { // confirm
|
||||
card.classList.remove("guard-active");
|
||||
selectRole(role.code, card);
|
||||
selectRole(role.code);
|
||||
},
|
||||
function () { // dismiss (NVM / outside click)
|
||||
card.classList.remove("guard-active");
|
||||
@@ -165,9 +165,13 @@ var RoleSelect = (function () {
|
||||
}
|
||||
|
||||
function handleTurnChanged(event) {
|
||||
// If a placeCard animation is running, defer until it completes.
|
||||
if (_animationPending) {
|
||||
_pendingTurnChange = event;
|
||||
return;
|
||||
}
|
||||
|
||||
var active = String(event.detail.active_slot);
|
||||
var invSlot = document.getElementById("id_inv_role_card");
|
||||
if (invSlot) invSlot.innerHTML = "";
|
||||
|
||||
// Force-close tray instantly so it never obscures the next player's card-stack.
|
||||
// Also set the race flag so the fetch .then() doesn't re-open if it arrives late.
|
||||
@@ -223,5 +227,10 @@ var RoleSelect = (function () {
|
||||
openFan: openFan,
|
||||
closeFan: closeFan,
|
||||
setReload: function (fn) { _reload = fn; },
|
||||
// Testing hook — resets animation-pause state between Jasmine specs
|
||||
_testReset: function () {
|
||||
_animationPending = false;
|
||||
_pendingTurnChange = null;
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
@@ -224,6 +224,37 @@ 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');
|
||||
cardEl.addEventListener('animationend', function handler() {
|
||||
cardEl.removeEventListener('animationend', handler);
|
||||
cardEl.classList.remove('arc-in');
|
||||
if (onComplete) onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
// placeCard(roleCode, onComplete) — mark the first tray cell with the role,
|
||||
// open the tray, arc-in the cell, then force-close. Calls onComplete after.
|
||||
// The grid always contains exactly 8 .tray-cell elements (from the template);
|
||||
// the first one receives .tray-role-card and data-role instead of a new element
|
||||
// being inserted, so the cell count never changes.
|
||||
function placeCard(roleCode, onComplete) {
|
||||
if (!_grid) { if (onComplete) onComplete(); return; }
|
||||
var firstCell = _grid.querySelector('.tray-cell');
|
||||
if (!firstCell) { if (onComplete) onComplete(); return; }
|
||||
|
||||
firstCell.classList.add('tray-role-card');
|
||||
firstCell.dataset.role = roleCode;
|
||||
firstCell.textContent = roleCode;
|
||||
|
||||
open();
|
||||
_arcIn(firstCell, function () {
|
||||
forceClose();
|
||||
if (onComplete) onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function _startDrag(clientX, clientY) {
|
||||
_dragHandled = false;
|
||||
if (_wrap) _wrap.classList.add('tray-dragging');
|
||||
@@ -428,6 +459,14 @@ var Tray = (function () {
|
||||
_onBtnClick = null;
|
||||
}
|
||||
_cancelPendingHide();
|
||||
// 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.textContent = '';
|
||||
delete el.dataset.role;
|
||||
});
|
||||
}
|
||||
_wrap = null;
|
||||
_btn = null;
|
||||
_tray = null;
|
||||
@@ -446,6 +485,7 @@ var Tray = (function () {
|
||||
close: close,
|
||||
forceClose: forceClose,
|
||||
isOpen: isOpen,
|
||||
placeCard: placeCard,
|
||||
reset: reset,
|
||||
_testSetLandscape: function (v) { _landscapeOverride = v; },
|
||||
};
|
||||
|
||||
@@ -655,43 +655,6 @@ class SelectRoleViewTest(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class RevealPhaseRenderingTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
|
||||
for i, (gamer, role) in enumerate(zip(gamers, roles), start=1):
|
||||
TableSeat.objects.create(
|
||||
room=self.room, gamer=gamer, slot_number=i,
|
||||
role=role, role_revealed=True,
|
||||
)
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.SIG_SELECT
|
||||
self.room.save()
|
||||
self.client.force_login(self.founder)
|
||||
|
||||
def test_face_up_role_cards_rendered_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "face-up")
|
||||
|
||||
def test_inv_role_card_slot_present(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "id_inv_role_card")
|
||||
|
||||
def test_partner_indicator_present_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "partner-indicator")
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
|
||||
@@ -216,14 +216,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
)
|
||||
)
|
||||
|
||||
# 7. Role card appears in inventory
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_inv_role_card .card"
|
||||
)
|
||||
)
|
||||
|
||||
# 8. Card stack returns to table centre
|
||||
# 7. Card stack returns to table centre
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
|
||||
)
|
||||
@@ -323,46 +316,6 @@ class RoleSelectTest(FunctionalTest):
|
||||
cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
|
||||
self.assertEqual(len(cards), 5)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 3d — Previously selected roles appear in inventory on re-entry#
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_previously_selected_roles_shown_in_inventory_on_re_entry(self):
|
||||
"""A multi-slot gamer who already chose some roles should see those
|
||||
role cards pre-populated in the inventory when they re-enter the room."""
|
||||
from apps.epic.models import TableSeat
|
||||
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
||||
room = Room.objects.create(name="Inventory Re-entry Test", owner=founder)
|
||||
_fill_room_via_orm(room, [
|
||||
"founder@test.io", "founder@test.io",
|
||||
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
|
||||
])
|
||||
room.table_status = Room.ROLE_SELECT
|
||||
room.save()
|
||||
|
||||
for slot in room.gate_slots.order_by("slot_number"):
|
||||
TableSeat.objects.create(
|
||||
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
|
||||
)
|
||||
# Founder's first slot has already chosen BC
|
||||
TableSeat.objects.filter(room=room, slot_number=1).update(role="BC")
|
||||
|
||||
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(room_url)
|
||||
|
||||
# Inventory should contain exactly one pre-rendered card for BC
|
||||
inv_cards = self.wait_for(
|
||||
lambda: self.browser.find_elements(
|
||||
By.CSS_SELECTOR, "#id_inv_role_card .card"
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(inv_cards), 1)
|
||||
self.assertIn(
|
||||
"BUILDER",
|
||||
inv_cards[0].text.upper(),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 4 — Click-away dismisses fan without selecting #
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -392,17 +345,13 @@ class RoleSelectTest(FunctionalTest):
|
||||
# Click the backdrop (outside the fan)
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop").click()
|
||||
|
||||
# Modal closes; stack still present; inventory still empty
|
||||
# Modal closes; stack still present
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
len(self.browser.find_elements(By.ID, "id_role_select")), 0
|
||||
)
|
||||
)
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
|
||||
self.assertEqual(
|
||||
len(self.browser.find_elements(By.CSS_SELECTOR, "#id_inv_role_card .card")),
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -529,17 +478,9 @@ class RoleSelectTest(FunctionalTest):
|
||||
|
||||
self.browser.refresh()
|
||||
|
||||
# All role cards in inventory are face-up
|
||||
face_up_cards = self.wait_for(
|
||||
lambda: self.browser.find_elements(
|
||||
By.CSS_SELECTOR, "#id_inv_role_card .card.face-up"
|
||||
)
|
||||
)
|
||||
self.assertGreater(len(face_up_cards), 0)
|
||||
|
||||
# Partner indicator is visible
|
||||
# Sig deck is present (page has transitioned to SIG_SELECT)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".partner-indicator")
|
||||
lambda: self.browser.find_element(By.ID, "id_sig_deck")
|
||||
)
|
||||
|
||||
|
||||
@@ -590,12 +531,13 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
self.confirm_guard()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T1 — Portrait: role card at topmost grid square, tray opens #
|
||||
# T1 — Portrait: role card marks first cell; tray opens then closes #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_portrait_role_card_enters_topmost_grid_square(self):
|
||||
"""Portrait: after confirming a role, a .tray-role-card is the first child
|
||||
of #id_tray_grid (topmost cell) and the tray is open."""
|
||||
"""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."""
|
||||
self.browser.set_window_size(390, 844)
|
||||
room = self._make_room()
|
||||
self.create_pre_authenticated_session("slot1@test.io")
|
||||
@@ -604,34 +546,42 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
|
||||
self._select_role()
|
||||
|
||||
# Card appears in the grid.
|
||||
# First cell receives the role card class.
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
|
||||
)
|
||||
)
|
||||
|
||||
# It is the first child — topmost in portrait.
|
||||
is_first = self.browser.execute_script("""
|
||||
var card = document.querySelector('#id_tray_grid .tray-role-card');
|
||||
return card !== null && card === card.parentElement.firstElementChild;
|
||||
result = self.browser.execute_script("""
|
||||
var grid = document.getElementById('id_tray_grid');
|
||||
var card = grid.querySelector('.tray-role-card');
|
||||
return {
|
||||
isFirst: card !== null && card === grid.firstElementChild,
|
||||
count: grid.children.length,
|
||||
role: card ? card.dataset.role : null
|
||||
};
|
||||
""")
|
||||
self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid")
|
||||
self.assertTrue(result["isFirst"], "Role card should be the first cell")
|
||||
self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
|
||||
self.assertTrue(result["role"], "First cell should carry data-role")
|
||||
|
||||
# Tray is open.
|
||||
self.assertTrue(
|
||||
# Tray closes after the animation sequence.
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.execute_script("return Tray.isOpen()"),
|
||||
"Tray should be open after role selection"
|
||||
"Tray should close after the arc-in sequence"
|
||||
)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T2 — Landscape: role card at leftmost grid square, tray opens #
|
||||
# T2 — Landscape: same contract in landscape #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@tag('two-browser')
|
||||
def test_landscape_role_card_enters_leftmost_grid_square(self):
|
||||
"""Landscape: after confirming a role, a .tray-role-card is the first child
|
||||
of #id_tray_grid (leftmost cell) and the tray is open."""
|
||||
"""Landscape: the first .tray-cell gets .tray-role-card; grid has
|
||||
8 cells; tray opens then closes."""
|
||||
self.browser.set_window_size(844, 390)
|
||||
room = self._make_room()
|
||||
self.create_pre_authenticated_session("slot1@test.io")
|
||||
@@ -640,24 +590,28 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
|
||||
self._select_role()
|
||||
|
||||
# Card appears in the grid.
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
|
||||
)
|
||||
)
|
||||
|
||||
# It is the first child — leftmost in landscape.
|
||||
is_first = self.browser.execute_script("""
|
||||
var card = document.querySelector('#id_tray_grid .tray-role-card');
|
||||
return card !== null && card === card.parentElement.firstElementChild;
|
||||
result = self.browser.execute_script("""
|
||||
var grid = document.getElementById('id_tray_grid');
|
||||
var card = grid.querySelector('.tray-role-card');
|
||||
return {
|
||||
isFirst: card !== null && card === grid.firstElementChild,
|
||||
count: grid.children.length
|
||||
};
|
||||
""")
|
||||
self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid")
|
||||
self.assertTrue(result["isFirst"], "Role card should be the first cell")
|
||||
self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
|
||||
|
||||
# Tray is open.
|
||||
self.assertTrue(
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.execute_script("return Tray.isOpen()"),
|
||||
"Tray should be open after role selection"
|
||||
"Tray should close after the arc-in sequence"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -189,12 +189,9 @@ class SigSelectTest(FunctionalTest):
|
||||
)
|
||||
)
|
||||
|
||||
# Founder's significator appears in their inventory
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_inv_sig_card .card"
|
||||
)
|
||||
)
|
||||
# TODO: sig card should appear in the tray (tray.placeCard for sig phase)
|
||||
# once sig-select.js is updated to call Tray.placeCard instead of
|
||||
# appending to the removed #id_inv_sig_card inventory element.
|
||||
|
||||
# Active seat advances to NC
|
||||
self.wait_for(
|
||||
|
||||
@@ -7,7 +7,6 @@ describe("RoleSelect", () => {
|
||||
<div class="room-page"
|
||||
data-select-role-url="/epic/room/test-uuid/select-role">
|
||||
</div>
|
||||
<div id="id_inv_role_card"></div>
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
@@ -102,12 +101,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card appends a .card to #id_inv_role_card", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card POSTs to the select_role URL", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
@@ -117,11 +110,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("clicking a card results in exactly one card in inventory", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelectorAll("#id_inv_role_card .card").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -135,11 +123,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector(".role-select-backdrop").click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -259,20 +242,13 @@ describe("RoleSelect", () => {
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("tray card after successful role selection", () => {
|
||||
let grid, guardConfirm;
|
||||
let guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Minimal tray grid matching room.html structure
|
||||
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);
|
||||
}
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
spyOn(Tray, "open");
|
||||
// Spy on Tray.placeCard: call the onComplete callback immediately.
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
if (cb) cb();
|
||||
});
|
||||
|
||||
// Capturing guard spy — holds onConfirm so we can fire it per-test
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
@@ -283,54 +259,128 @@ describe("RoleSelect", () => {
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
});
|
||||
|
||||
it("prepends a .tray-role-card to #id_tray_grid on success", async () => {
|
||||
it("calls Tray.placeCard() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).not.toBeNull();
|
||||
expect(Tray.placeCard).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tray-role-card is the first child of #id_tray_grid", async () => {
|
||||
it("passes the role code string to Tray.placeCard", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.firstElementChild.classList.contains("tray-role-card")).toBe(true);
|
||||
const roleCode = Tray.placeCard.calls.mostRecent().args[0];
|
||||
expect(typeof roleCode).toBe("string");
|
||||
expect(roleCode.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("tray-role-card carries the selected role as data-role", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
const trayCard = grid.querySelector(".tray-role-card");
|
||||
expect(trayCard.dataset.role).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls Tray.open() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not prepend a tray-role-card on server rejection", async () => {
|
||||
it("does not call Tray.placeCard() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).toBeNull();
|
||||
expect(Tray.placeCard).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Tray.open() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
// ------------------------------------------------------------------ //
|
||||
// WS turn_changed pause during animation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("WS turn_changed pause during placeCard animation", () => {
|
||||
let stack, guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Six table seats, slot 1 starts active
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat" + (i === 1 ? " active" : "");
|
||||
seat.dataset.slot = String(i);
|
||||
testDiv.appendChild(seat);
|
||||
}
|
||||
stack = document.createElement("div");
|
||||
stack.className = "card-stack";
|
||||
stack.dataset.state = "ineligible";
|
||||
stack.dataset.userSlots = "1";
|
||||
stack.dataset.starterRoles = "";
|
||||
testDiv.appendChild(stack);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.id = "id_tray_grid";
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
// placeCard spy that holds the onComplete callback
|
||||
let heldCallback = null;
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
heldCallback = cb; // don't call immediately — simulate animation in-flight
|
||||
});
|
||||
spyOn(Tray, "forceClose");
|
||||
|
||||
// Expose heldCallback so tests can fire it
|
||||
Tray._testFirePlaceCardComplete = () => {
|
||||
if (heldCallback) heldCallback();
|
||||
};
|
||||
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
(anchor, message, onConfirm) => { guardConfirm = onConfirm; }
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("grid grows by exactly 1 on success", async () => {
|
||||
const before = grid.children.length;
|
||||
afterEach(() => {
|
||||
delete Tray._testFirePlaceCardComplete;
|
||||
RoleSelect._testReset();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not call Tray.forceClose immediately", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve(); // fetch resolves; placeCard called; animation pending
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not immediately move the active seat", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.children.length).toBe(before + 1);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("1"); // still slot 1
|
||||
});
|
||||
|
||||
it("deferred turn_changed is processed when animation completes", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
|
||||
// Fire onComplete — deferred turn_changed should now run
|
||||
Tray._testFirePlaceCardComplete();
|
||||
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
|
||||
it("turn_changed after animation completes is processed immediately", () => {
|
||||
// No animation in flight — turn_changed should run right away
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).toHaveBeenCalled();
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,9 +483,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("appends a .card to #id_inv_role_card", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissing the guard (NVM or outside click)", () => {
|
||||
@@ -463,10 +510,6 @@ describe("RoleSelect", () => {
|
||||
expect(window.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores normal mouseleave behaviour on the card", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter"));
|
||||
card.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
|
||||
@@ -422,4 +422,97 @@ describe("Tray", () => {
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// placeCard() //
|
||||
// ---------------------------------------------------------------------- //
|
||||
//
|
||||
// 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).
|
||||
// 4. forceClose() — tray closes instantly.
|
||||
// 5. Calls onComplete.
|
||||
//
|
||||
// The grid always has exactly 8 .tray-cell elements (from the template);
|
||||
// no new elements are inserted.
|
||||
//
|
||||
// ---------------------------------------------------------------------- //
|
||||
|
||||
describe("placeCard()", () => {
|
||||
let grid, firstCell;
|
||||
|
||||
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);
|
||||
// Re-init so _grid is set (reset() in outer afterEach clears it)
|
||||
Tray.init();
|
||||
firstCell = grid.querySelector(".tray-cell");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
grid.remove();
|
||||
});
|
||||
|
||||
it("adds .tray-role-card to the first .tray-cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
});
|
||||
|
||||
it("sets data-role on the first cell", () => {
|
||||
Tray.placeCard("NC", null);
|
||||
expect(firstCell.dataset.role).toBe("NC");
|
||||
});
|
||||
|
||||
it("grid cell count stays at 8", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(grid.children.length).toBe(8);
|
||||
});
|
||||
|
||||
it("opens the tray", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("adds .arc-in to the first cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("arc-in")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes .arc-in and force-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(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("calls onComplete after the tray closes", () => {
|
||||
let called = false;
|
||||
Tray.placeCard("PC", () => { called = true; });
|
||||
firstCell.dispatchEvent(new Event("animationend"));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("landscape: same behaviour — first cell gets role card", () => {
|
||||
Tray._testSetLandscape(true);
|
||||
Tray.init();
|
||||
Tray.placeCard("EC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
expect(firstCell.dataset.role).toBe("EC");
|
||||
});
|
||||
|
||||
it("reset() removes .tray-role-card and data-role from cells", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
Tray.reset();
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(false);
|
||||
expect(firstCell.dataset.role).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -419,17 +419,6 @@ $seat-r-y: round($seat-r * 0.5); // 65px
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-inventory {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(var(--terUser), 0.3) transparent;
|
||||
}
|
||||
|
||||
.table-seat {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@@ -617,56 +606,6 @@ $card-h: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Inventory role card hand ───────────────────────────────────────────────
|
||||
//
|
||||
// Cards are stacked vertically: only a $strip-height peek of each card below
|
||||
// the first is visible by default, showing the role name at the top of the
|
||||
// card face. Hovering any card slides it right to pop it clear of the stack.
|
||||
|
||||
$inv-card-w: 100px;
|
||||
$inv-card-h: 150px;
|
||||
$inv-strip: 30px; // visible height of each stacked card after the first
|
||||
|
||||
#id_inv_role_card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card {
|
||||
width: $inv-card-w;
|
||||
height: $inv-card-h;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
// Every card after the first overlaps the one above it
|
||||
& + .card {
|
||||
margin-top: -($inv-card-h - $inv-strip);
|
||||
}
|
||||
|
||||
// Role name pinned to the top of the face so it reads in the strip
|
||||
.card-front {
|
||||
justify-content: flex-start;
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
|
||||
// Pop the hovered card to the right, above siblings
|
||||
&:hover {
|
||||
transform: translateX(1.5rem);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Partner indicator ─────────────────────────────────────────────────────
|
||||
|
||||
.partner-indicator {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Landscape mobile — aggressively scale down to fit short viewport
|
||||
@media (orientation: landscape) and (max-width: 1440px) {
|
||||
.gate-modal {
|
||||
|
||||
@@ -117,6 +117,27 @@ $handle-r: 1rem;
|
||||
&::before { border-color: rgba(var(--quaUser), 1); }
|
||||
}
|
||||
|
||||
// ─── Role card: arc-in animation (portrait) ─────────────────────────────────
|
||||
@keyframes tray-role-arc-in {
|
||||
from { opacity: 0; transform: scale(0.3) translate(-40%, -40%); }
|
||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
}
|
||||
|
||||
.tray-role-card {
|
||||
background: rgba(var(--quaUser), 0.25);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 0.2em;
|
||||
font-size: 0.65rem;
|
||||
color: rgba(var(--quaUser), 1);
|
||||
font-weight: 600;
|
||||
|
||||
&.arc-in {
|
||||
animation: tray-role-arc-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tray-wobble {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-8px); }
|
||||
@@ -276,6 +297,16 @@ $handle-r: 1rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// Role card arc-in for landscape
|
||||
@keyframes tray-role-arc-in-landscape {
|
||||
from { opacity: 0; transform: scale(0.3) translate(-40%, 40%); }
|
||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
}
|
||||
|
||||
.tray-role-card.arc-in {
|
||||
animation: tray-role-arc-in-landscape 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes tray-wobble-landscape {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
20% { transform: translateY(-8px); }
|
||||
|
||||
@@ -7,7 +7,6 @@ describe("RoleSelect", () => {
|
||||
<div class="room-page"
|
||||
data-select-role-url="/epic/room/test-uuid/select-role">
|
||||
</div>
|
||||
<div id="id_inv_role_card"></div>
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
@@ -102,12 +101,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card appends a .card to #id_inv_role_card", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card POSTs to the select_role URL", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
@@ -117,11 +110,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("clicking a card results in exactly one card in inventory", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelectorAll("#id_inv_role_card .card").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -135,11 +123,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector(".role-select-backdrop").click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -259,20 +242,13 @@ describe("RoleSelect", () => {
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("tray card after successful role selection", () => {
|
||||
let grid, guardConfirm;
|
||||
let guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Minimal tray grid matching room.html structure
|
||||
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);
|
||||
}
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
spyOn(Tray, "open");
|
||||
// Spy on Tray.placeCard: call the onComplete callback immediately.
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
if (cb) cb();
|
||||
});
|
||||
|
||||
// Capturing guard spy — holds onConfirm so we can fire it per-test
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
@@ -283,54 +259,128 @@ describe("RoleSelect", () => {
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
});
|
||||
|
||||
it("prepends a .tray-role-card to #id_tray_grid on success", async () => {
|
||||
it("calls Tray.placeCard() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).not.toBeNull();
|
||||
expect(Tray.placeCard).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tray-role-card is the first child of #id_tray_grid", async () => {
|
||||
it("passes the role code string to Tray.placeCard", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.firstElementChild.classList.contains("tray-role-card")).toBe(true);
|
||||
const roleCode = Tray.placeCard.calls.mostRecent().args[0];
|
||||
expect(typeof roleCode).toBe("string");
|
||||
expect(roleCode.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("tray-role-card carries the selected role as data-role", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
const trayCard = grid.querySelector(".tray-role-card");
|
||||
expect(trayCard.dataset.role).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls Tray.open() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not prepend a tray-role-card on server rejection", async () => {
|
||||
it("does not call Tray.placeCard() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).toBeNull();
|
||||
expect(Tray.placeCard).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Tray.open() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
// ------------------------------------------------------------------ //
|
||||
// WS turn_changed pause during animation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("WS turn_changed pause during placeCard animation", () => {
|
||||
let stack, guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Six table seats, slot 1 starts active
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat" + (i === 1 ? " active" : "");
|
||||
seat.dataset.slot = String(i);
|
||||
testDiv.appendChild(seat);
|
||||
}
|
||||
stack = document.createElement("div");
|
||||
stack.className = "card-stack";
|
||||
stack.dataset.state = "ineligible";
|
||||
stack.dataset.userSlots = "1";
|
||||
stack.dataset.starterRoles = "";
|
||||
testDiv.appendChild(stack);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.id = "id_tray_grid";
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
// placeCard spy that holds the onComplete callback
|
||||
let heldCallback = null;
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
heldCallback = cb; // don't call immediately — simulate animation in-flight
|
||||
});
|
||||
spyOn(Tray, "forceClose");
|
||||
|
||||
// Expose heldCallback so tests can fire it
|
||||
Tray._testFirePlaceCardComplete = () => {
|
||||
if (heldCallback) heldCallback();
|
||||
};
|
||||
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
(anchor, message, onConfirm) => { guardConfirm = onConfirm; }
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("grid grows by exactly 1 on success", async () => {
|
||||
const before = grid.children.length;
|
||||
afterEach(() => {
|
||||
delete Tray._testFirePlaceCardComplete;
|
||||
RoleSelect._testReset();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not call Tray.forceClose immediately", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve(); // fetch resolves; placeCard called; animation pending
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not immediately move the active seat", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.children.length).toBe(before + 1);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("1"); // still slot 1
|
||||
});
|
||||
|
||||
it("deferred turn_changed is processed when animation completes", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
|
||||
// Fire onComplete — deferred turn_changed should now run
|
||||
Tray._testFirePlaceCardComplete();
|
||||
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
|
||||
it("turn_changed after animation completes is processed immediately", () => {
|
||||
// No animation in flight — turn_changed should run right away
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).toHaveBeenCalled();
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,9 +483,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("appends a .card to #id_inv_role_card", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissing the guard (NVM or outside click)", () => {
|
||||
@@ -463,10 +510,6 @@ describe("RoleSelect", () => {
|
||||
expect(window.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores normal mouseleave behaviour on the card", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter"));
|
||||
card.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
|
||||
@@ -422,4 +422,97 @@ describe("Tray", () => {
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// placeCard() //
|
||||
// ---------------------------------------------------------------------- //
|
||||
//
|
||||
// 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).
|
||||
// 4. forceClose() — tray closes instantly.
|
||||
// 5. Calls onComplete.
|
||||
//
|
||||
// The grid always has exactly 8 .tray-cell elements (from the template);
|
||||
// no new elements are inserted.
|
||||
//
|
||||
// ---------------------------------------------------------------------- //
|
||||
|
||||
describe("placeCard()", () => {
|
||||
let grid, firstCell;
|
||||
|
||||
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);
|
||||
// Re-init so _grid is set (reset() in outer afterEach clears it)
|
||||
Tray.init();
|
||||
firstCell = grid.querySelector(".tray-cell");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
grid.remove();
|
||||
});
|
||||
|
||||
it("adds .tray-role-card to the first .tray-cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
});
|
||||
|
||||
it("sets data-role on the first cell", () => {
|
||||
Tray.placeCard("NC", null);
|
||||
expect(firstCell.dataset.role).toBe("NC");
|
||||
});
|
||||
|
||||
it("grid cell count stays at 8", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(grid.children.length).toBe(8);
|
||||
});
|
||||
|
||||
it("opens the tray", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("adds .arc-in to the first cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("arc-in")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes .arc-in and force-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(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("calls onComplete after the tray closes", () => {
|
||||
let called = false;
|
||||
Tray.placeCard("PC", () => { called = true; });
|
||||
firstCell.dispatchEvent(new Event("animationend"));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("landscape: same behaviour — first cell gets role card", () => {
|
||||
Tray._testSetLandscape(true);
|
||||
Tray.init();
|
||||
Tray.placeCard("EC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
expect(firstCell.dataset.role).toBe("EC");
|
||||
});
|
||||
|
||||
it("reset() removes .tray-role-card and data-role from cells", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
Tray.reset();
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(false);
|
||||
expect(firstCell.dataset.role).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,32 +47,6 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="id_inventory" class="room-inventory">
|
||||
<div id="id_inv_sig_card"></div>
|
||||
<div id="id_inv_role_card">
|
||||
{% if room.table_status == "ROLE_SELECT" %}
|
||||
{% for seat in assigned_seats %}
|
||||
<div class="card flipped">
|
||||
<div class="card-back">?</div>
|
||||
<div class="card-front">
|
||||
<div class="card-role-name">{{ seat.get_role_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif room.table_status == "SIG_SELECT" and user_seat %}
|
||||
<div class="card face-up">
|
||||
<div class="card-front">
|
||||
<div class="card-role-name">{{ user_seat.get_role_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if partner_seat %}
|
||||
<div class="partner-indicator">
|
||||
Partner: {{ partner_seat.get_role_display }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if room.table_status == "SIG_SELECT" and sig_cards %}
|
||||
|
||||
Reference in New Issue
Block a user