role-select UX: tray timing delays, seat/circle state polish, 394 ITs green
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:
Disco DeDisco
2026-03-31 00:01:04 -04:00
parent a8592aeaec
commit 736b59b5c0
13 changed files with 833 additions and 400 deletions

View File

@@ -402,7 +402,9 @@ class RoleSelectRenderingTest(TestCase):
response = self.client.get(
self.url
)
self.assertNotContains(response, "fa-ban")
# Seat ban icons carry "position-status-icon"; card-stack ban does not.
# Assert the bare "fa-solid fa-ban" (card-stack form) is absent.
self.assertNotContains(response, 'class="fa-solid fa-ban"')
def test_gatekeeper_overlay_absent_when_role_select(self):
self.client.force_login(self.founder)
@@ -411,6 +413,21 @@ class RoleSelectRenderingTest(TestCase):
)
self.assertNotContains(response, "gate-overlay")
def test_tray_wrap_has_role_select_phase_class(self):
# Tray handle hidden until gamer confirms a role pick
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertContains(response, 'id="id_tray_wrap" class="role-select-phase"')
def test_tray_absent_during_gatekeeper_phase(self):
# Tray must not render before the gamer occupies a seat
room = Room.objects.create(name="Gate Room", owner=self.founder)
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": room.id})
)
self.assertNotContains(response, 'id="id_tray_wrap"')
def test_six_table_seats_rendered(self):
self.client.force_login(self.founder)
response = self.client.get(
@@ -418,20 +435,66 @@ class RoleSelectRenderingTest(TestCase):
)
self.assertContains(response, "table-seat", count=6)
def test_active_table_seat_has_active_class(self):
self.client.force_login(self.founder) # slot 1 is active
response = self.client.get(
self.url
)
self.assertContains(response, 'class="table-seat active"')
def test_inactive_table_seat_lacks_active_class(self):
def test_table_seats_never_active_on_load(self):
# Seat glow is JS-only (during tray animation); never server-rendered
self.client.force_login(self.founder)
response = self.client.get(
self.url
)
# Slots 26 are not active, so at least one plain table-seat exists
self.assertContains(response, 'class="table-seat"')
response = self.client.get(self.url)
self.assertNotContains(response, 'class="table-seat active"')
def test_assigned_seat_renders_role_confirmed_class(self):
# A seat with a role already picked must load as role-confirmed (opaque chair)
self.gamers[0].refresh_from_db()
seat = self.room.table_seats.get(slot_number=1)
seat.role = "PC"
seat.save()
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertContains(response, 'table-seat role-confirmed')
def test_unassigned_seat_lacks_role_confirmed_class(self):
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertNotContains(response, 'table-seat role-confirmed')
def test_assigned_slot_circle_renders_role_assigned_class(self):
# Slot 1 circle hidden because 1 role was assigned (count-based, not role-label-based)
seat = self.room.table_seats.get(slot_number=1)
seat.role = "PC"
seat.save()
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertContains(response, 'gate-slot filled role-assigned')
def test_slot_circle_hides_by_count_not_role_label(self):
# Gamer in slot 1 picks NC (not PC) — slot 1 circle must still hide, not slot 2's
seat = self.room.table_seats.get(slot_number=1)
seat.role = "NC"
seat.save()
self.client.force_login(self.founder)
response = self.client.get(self.url)
content = response.content.decode()
import re
# Template renders class before data-slot; capture both orderings
circles = re.findall(r'class="([^"]*gate-slot[^"]*)"[^>]*data-slot="(\d)"', content)
slot1_classes = next((cls for cls, slot in circles if slot == "1"), "")
slot2_classes = next((cls for cls, slot in circles if slot == "2"), "")
self.assertIn("role-assigned", slot1_classes)
self.assertNotIn("role-assigned", slot2_classes)
def test_unassigned_slot_circle_lacks_role_assigned_class(self):
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertNotContains(response, 'role-assigned')
def test_position_strip_rendered_during_role_select(self):
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertContains(response, "position-strip")
def test_position_strip_has_six_gate_slots(self):
self.client.force_login(self.founder)
response = self.client.get(self.url)
self.assertContains(response, "gate-slot", count=6)
def test_card_stack_has_data_user_slots_for_eligible_gamer(self):
self.client.force_login(self.founder) # founder is slot 1 only
@@ -447,6 +510,29 @@ class RoleSelectRenderingTest(TestCase):
)
self.assertContains(response, 'data-user-slots="2"')
def test_assigned_seat_renders_check_icon(self):
seat = self.room.table_seats.get(slot_number=1)
seat.role = "PC"
seat.save()
self.client.force_login(self.founder)
response = self.client.get(self.url)
content = response.content.decode()
# The PC seat should have fa-circle-check, not fa-ban
pc_seat_start = content.index('data-role="PC"')
pc_seat_chunk = content[pc_seat_start:pc_seat_start + 300]
self.assertIn("fa-circle-check", pc_seat_chunk)
self.assertNotIn("fa-ban", pc_seat_chunk)
def test_unassigned_seat_renders_ban_icon(self):
# slot 2's role is still null
self.client.force_login(self.founder)
response = self.client.get(self.url)
content = response.content.decode()
nc_seat_start = content.index('data-role="NC"')
nc_seat_chunk = content[nc_seat_start:nc_seat_start + 300]
self.assertIn("fa-ban", nc_seat_chunk)
self.assertNotIn("fa-circle-check", nc_seat_chunk)
class PickRolesViewTest(TestCase):
def setUp(self):