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:
@@ -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 2–6 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):
|
||||
|
||||
Reference in New Issue
Block a user