role-select UX: tray timing delays, seat/circle state polish, 394 ITs green
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- _animationPending set before fetch (not in .then()) — blocks WS turn advance during in-flight request
- _placeCardDelay (3s) + _postTrayDelay (3s) give gamer time to see each step; both zeroed by _testReset()
- .role-confirmed class: full-opacity chair after placeCard completes; server-rendered on reload
- Slot circles disappear in join order (slot 1 first) via count-based logic, not role-label matching
- data-active-slot on card-stack; handleTurnChanged writes it for selectRole() to read
- #id_tray_wrap not rendered during gate phase ({% if room.table_status %})
- Tray slide/arc-in slowed to 1s for diagnostics; wobble kept at 0.45s
- Obsolete test_roles_revealed_simultaneously FT removed; T8 tray FT uses ROLE_SELECT room
- Jasmine macrotask flush pattern: await new Promise(r => setTimeout(r, 0)) after fetch .then()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -607,20 +607,20 @@ class PositionIndicatorsTest(FunctionalTest):
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test P1 — 6 position indicators present while gatekeeper is open #
|
||||
# Test P1 — 6 position circles present in strip alongside gatekeeper #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_position_indicators_visible_alongside_gatekeeper(self):
|
||||
self.browser.get(self.gate_url)
|
||||
# Gatekeeper modal is open
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
||||
)
|
||||
# Six .table-position elements are rendered outside the modal
|
||||
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
|
||||
self.assertEqual(len(positions), 6)
|
||||
for pos in positions:
|
||||
self.assertTrue(pos.is_displayed())
|
||||
# Six .gate-slot elements are rendered in .position-strip, outside modal
|
||||
strip = self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
|
||||
slots = strip.find_elements(By.CSS_SELECTOR, ".gate-slot")
|
||||
self.assertEqual(len(slots), 6)
|
||||
for slot in slots:
|
||||
self.assertTrue(slot.is_displayed())
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test P2 — URL drops /gate/ after pick_roles #
|
||||
@@ -631,63 +631,57 @@ class PositionIndicatorsTest(FunctionalTest):
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
])
|
||||
# Simulate pick_roles having fired: room advances to ROLE_SELECT
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
|
||||
# Navigating to the /gate/ URL should redirect to the plain room URL
|
||||
self.browser.get(self.gate_url)
|
||||
expected_url = (
|
||||
f"{self.live_server_url}/gameboard/room/{self.room.id}/"
|
||||
)
|
||||
expected_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/"
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(self.browser.current_url, expected_url)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test P3 — Each position has a chair icon and correct role label #
|
||||
# Test P3 — Gate-slot circles live outside the modal #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_position_shows_chair_icon_and_role_label(self):
|
||||
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
||||
def test_position_circles_outside_gatekeeper_modal(self):
|
||||
"""The numbered position circles must NOT be descendants of .gate-modal —
|
||||
they live in .position-strip which sits above the backdrop."""
|
||||
self.browser.get(self.gate_url)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal")
|
||||
)
|
||||
for slot_number, role_label in SLOT_ROLE_LABELS.items():
|
||||
pos = self.browser.find_element(
|
||||
By.CSS_SELECTOR, f".table-position[data-slot='{slot_number}']"
|
||||
)
|
||||
# Chair icon present
|
||||
self.assertTrue(pos.find_elements(By.CSS_SELECTOR, ".fa-chair"))
|
||||
# Role label attribute and visible text
|
||||
self.assertEqual(pos.get_attribute("data-role-label"), role_label)
|
||||
label_el = pos.find_element(By.CSS_SELECTOR, ".position-role-label")
|
||||
self.assertEqual(label_el.text.strip(), role_label)
|
||||
# No .gate-slot inside the modal
|
||||
modal_slots = self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".gate-modal .gate-slot"
|
||||
)
|
||||
self.assertEqual(len(modal_slots), 0)
|
||||
# All 6 live in .position-strip
|
||||
strip_slots = self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".position-strip .gate-slot"
|
||||
)
|
||||
self.assertEqual(len(strip_slots), 6)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test P4 — Unoccupied position shows ban icon #
|
||||
# Test P4 — Each circle displays its slot number #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_unoccupied_position_shows_ban_icon(self):
|
||||
def test_position_circle_shows_slot_number(self):
|
||||
self.browser.get(self.gate_url)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
|
||||
)
|
||||
# All slots are empty — every position should have a ban icon
|
||||
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
|
||||
for pos in positions:
|
||||
self.assertTrue(
|
||||
pos.find_elements(By.CSS_SELECTOR, ".fa-ban"),
|
||||
f"Expected .fa-ban on slot {pos.get_attribute('data-slot')}",
|
||||
for n in range(1, 7):
|
||||
slot_el = self.browser.find_element(
|
||||
By.CSS_SELECTOR, f".position-strip .gate-slot[data-slot='{n}']"
|
||||
)
|
||||
self.assertIn(str(n), slot_el.text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test P5 — Occupied position shows check icon after token confirmed #
|
||||
# Test P5 — Filled slot carries .filled class in strip #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_occupied_position_shows_check_icon_after_token_confirmed(self):
|
||||
# Slot 1 is filled via ORM
|
||||
def test_filled_slot_shown_in_strip(self):
|
||||
from apps.epic.models import GateSlot
|
||||
slot = self.room.gate_slots.get(slot_number=1)
|
||||
slot.gamer = self.founder
|
||||
@@ -696,10 +690,13 @@ class PositionIndicatorsTest(FunctionalTest):
|
||||
|
||||
self.browser.get(self.gate_url)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
|
||||
)
|
||||
pos1 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-position[data-slot='1']"
|
||||
slot1 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".position-strip .gate-slot[data-slot='1']"
|
||||
)
|
||||
self.assertTrue(pos1.find_elements(By.CSS_SELECTOR, ".fa-circle-check"))
|
||||
self.assertFalse(pos1.find_elements(By.CSS_SELECTOR, ".fa-ban"))
|
||||
self.assertIn("filled", slot1.get_attribute("class"))
|
||||
slot2 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".position-strip .gate-slot[data-slot='2']"
|
||||
)
|
||||
self.assertIn("empty", slot2.get_attribute("class"))
|
||||
|
||||
@@ -445,12 +445,14 @@ class RoleSelectTest(FunctionalTest):
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 7 — All roles revealed simultaneously after all gamers select #
|
||||
# Test 8a — Hex seats carry role labels during role select #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_roles_revealed_simultaneously_after_all_select(self):
|
||||
def test_seats_around_hex_have_role_labels(self):
|
||||
"""During role select the 6 .table-seat elements carry data-role
|
||||
attributes matching the fixed slot→role mapping (PC at slot 1, etc.)."""
|
||||
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
||||
room = Room.objects.create(name="Reveal Test", owner=founder)
|
||||
room = Room.objects.create(name="Seat Label Test", owner=founder)
|
||||
_fill_room_via_orm(room, [
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
@@ -462,38 +464,24 @@ class RoleSelectTest(FunctionalTest):
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(room_url)
|
||||
|
||||
# Assign all roles via ORM (simulating all gamers having chosen)
|
||||
from apps.epic.models import TableSeat
|
||||
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
|
||||
for i, slot in enumerate(room.gate_slots.order_by("slot_number")):
|
||||
TableSeat.objects.create(
|
||||
room=room,
|
||||
gamer=slot.gamer,
|
||||
slot_number=slot.slot_number,
|
||||
role=roles[i],
|
||||
role_revealed=True,
|
||||
)
|
||||
room.table_status = Room.SIG_SELECT
|
||||
room.save()
|
||||
|
||||
self.browser.refresh()
|
||||
|
||||
# Sig deck is present (page has transitioned to SIG_SELECT)
|
||||
expected = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_sig_deck")
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
|
||||
)
|
||||
|
||||
for slot_number, role_label in expected.items():
|
||||
seat = self.browser.find_element(
|
||||
By.CSS_SELECTOR, f".table-seat[data-slot='{slot_number}']"
|
||||
)
|
||||
self.assertEqual(seat.get_attribute("data-role"), role_label)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 8a — Position glows while role card is being placed #
|
||||
# Test 8b — Hex seats show .fa-ban when empty #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_position_glows_when_role_card_confirmed(self):
|
||||
"""Immediately after confirming a role pick, the matching
|
||||
.table-position should receive .active (the glow state) before
|
||||
the tray animation completes."""
|
||||
def test_seats_show_ban_icon_when_empty(self):
|
||||
"""All 6 seats carry .fa-ban before any role has been chosen."""
|
||||
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
||||
room = Room.objects.create(name="Position Glow Test", owner=founder)
|
||||
room = Room.objects.create(name="Seat Ban Test", owner=founder)
|
||||
_fill_room_via_orm(room, [
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
@@ -509,32 +497,26 @@ class RoleSelectTest(FunctionalTest):
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(room_url)
|
||||
|
||||
# Open fan, click first card (PC), confirm guard
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
|
||||
)
|
||||
).click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard()
|
||||
|
||||
# PC position gains .active immediately after confirmation
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-position[data-role-label='PC'].active"
|
||||
)
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
|
||||
)
|
||||
seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
|
||||
self.assertEqual(len(seats), 6)
|
||||
for seat in seats:
|
||||
self.assertTrue(
|
||||
seat.find_elements(By.CSS_SELECTOR, ".fa-ban"),
|
||||
f"Expected .fa-ban on seat slot {seat.get_attribute('data-slot')}",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 8b — Position shows check icon after tray sequence ends #
|
||||
# Test 8c — Hex seat gets .fa-circle-check after role selected #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_position_gets_check_when_tray_sequence_ends(self):
|
||||
"""After the tray arc-in animation completes and the tray closes,
|
||||
the PC .table-position should show .fa-circle-check and no .fa-ban."""
|
||||
def test_seat_gets_check_after_role_selected(self):
|
||||
"""After confirming a role pick the corresponding hex seat should
|
||||
show .fa-circle-check and lose .fa-ban."""
|
||||
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
||||
room = Room.objects.create(name="Position Check Test", owner=founder)
|
||||
room = Room.objects.create(name="Seat Check Test", owner=founder)
|
||||
_fill_room_via_orm(room, [
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
@@ -550,7 +532,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(room_url)
|
||||
|
||||
# Open fan, pick PC card, confirm guard
|
||||
# Open fan, pick first card (PC), confirm guard
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
|
||||
@@ -560,23 +542,23 @@ class RoleSelectTest(FunctionalTest):
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard()
|
||||
|
||||
# Wait for tray animation to complete (tray closes)
|
||||
# Wait for tray animation to complete
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.execute_script("return Tray.isOpen()"),
|
||||
"Tray should close after arc-in sequence"
|
||||
"Tray should close after arc-in sequence",
|
||||
)
|
||||
)
|
||||
|
||||
# PC position now shows check icon, ban icon gone
|
||||
# The PC seat (slot 1) now shows check, no ban
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-circle-check"
|
||||
By.CSS_SELECTOR, ".table-seat[data-role='PC'] .fa-circle-check"
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
len(self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-ban"
|
||||
By.CSS_SELECTOR, ".table-seat[data-role='PC'] .fa-ban"
|
||||
)),
|
||||
0,
|
||||
)
|
||||
@@ -745,11 +727,11 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
)
|
||||
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
|
||||
|
||||
# 1. Watcher loads the room — slot 1 is active on initial render
|
||||
# 1. Watcher (slot 2) loads the room
|
||||
self.create_pre_authenticated_session("watcher@test.io")
|
||||
self.browser.get(room_url)
|
||||
self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat.active[data-slot='1']"
|
||||
By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
|
||||
))
|
||||
|
||||
# 2. Founder picks a role in second browser
|
||||
@@ -764,16 +746,10 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard(browser=self.browser2)
|
||||
|
||||
# 3. Watcher's seat arc moves to slot 2 — no page refresh
|
||||
# 3. Watcher's turn arrives via WS — card-stack becomes eligible
|
||||
self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
|
||||
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
|
||||
))
|
||||
self.assertEqual(
|
||||
len(self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".table-seat.active[data-slot='1']"
|
||||
)),
|
||||
0,
|
||||
)
|
||||
finally:
|
||||
self.browser2.quit()
|
||||
|
||||
@@ -836,16 +812,11 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
return card !== null && card === card.parentElement.firstElementChild;
|
||||
""")))
|
||||
|
||||
# Turn advances via WS — seat 2 becomes active.
|
||||
self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
|
||||
))
|
||||
|
||||
# Tray must be closed: forceClose() fires in handleTurnChanged.
|
||||
self.assertFalse(
|
||||
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
|
||||
self.wait_for(lambda: self.assertFalse(
|
||||
self.browser.execute_script("return Tray.isOpen()"),
|
||||
"Tray should be closed after turn advances"
|
||||
)
|
||||
))
|
||||
|
||||
def test_landscape_tray_closes_on_turn_advance(self):
|
||||
"""Landscape: role card at leftmost grid square; tray closes when
|
||||
@@ -869,15 +840,10 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
return card !== null && card === card.parentElement.firstElementChild;
|
||||
""")))
|
||||
|
||||
# Turn advances via WS — seat 2 becomes active.
|
||||
self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
|
||||
))
|
||||
|
||||
# Tray must be closed.
|
||||
self.assertFalse(
|
||||
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
|
||||
self.wait_for(lambda: self.assertFalse(
|
||||
self.browser.execute_script("return Tray.isOpen()"),
|
||||
"Tray should be closed after turn advances"
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,20 @@ class TrayTest(FunctionalTest):
|
||||
document.dispatchEvent(new PointerEvent("pointerup", {clientY: endY, clientX: 0, bubbles: true}));
|
||||
""", btn, start_y, end_y)
|
||||
|
||||
def _make_role_select_room(self, founder_email="founder@test.io"):
|
||||
from apps.epic.models import TableSeat
|
||||
founder, _ = User.objects.get_or_create(email=founder_email)
|
||||
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
||||
emails = [founder_email, "nc@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io"]
|
||||
_fill_room_via_orm(room, emails)
|
||||
room.table_status = Room.ROLE_SELECT
|
||||
room.save()
|
||||
for i, email in enumerate(emails, start=1):
|
||||
gamer, _ = User.objects.get_or_create(email=email)
|
||||
TableSeat.objects.get_or_create(room=room, gamer=gamer, slot_number=i)
|
||||
return room
|
||||
|
||||
def _make_sig_select_room(self, founder_email="founder@test.io"):
|
||||
founder, _ = User.objects.get_or_create(email=founder_email)
|
||||
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
||||
@@ -262,7 +276,7 @@ class TrayTest(FunctionalTest):
|
||||
|
||||
@tag('two-browser')
|
||||
def test_tray_grid_is_1_column_by_8_rows_in_portrait(self):
|
||||
room = self._make_sig_select_room()
|
||||
room = self._make_role_select_room()
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(self._room_url(room))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user