room ?seat=N seat-switch: CARTE gamer previews an owned seat's card-stack — data-active-slot + ineligible .fa-ban when it's not the table's turn — TDD

Workstream B of the position-circle tooltips sprint.

_role_select_context consumes ?seat=N (threaded from room_view): a multi-seat (CARTE) gamer previews a specific owned seat. The card-stack's active slot becomes that seat; it stays 'eligible' only when the previewed seat is also the table's current turn (lowest unassigned seat), else it renders the ineligible .fa-ban. Strictly additive — a one-seat gamer never passes ?seat, and an unowned/garbage param is ignored, so the normal role-select flow (RoleSelectRenderingTest) is untouched.

Tests: CarteSeatSwitchTest.test_switching_seat_loads_that_seats_role_view FT green; 2 new render ITs (seat-param-previews + no-seat-keeps-canonical); RoleSelectRenderingTest still green.

[[project-position-circle-tooltips]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-01 12:29:08 -04:00
parent 30246cc94a
commit d190b37149
3 changed files with 38 additions and 1 deletions

View File

@@ -745,6 +745,23 @@ class PositionTooltipCarteRenderTest(TestCase):
chunk = content[idx:idx + 800] chunk = content[idx:idx + 800]
self.assertIn("seat=4", chunk) self.assertIn("seat=4", chunk)
def test_seat_param_previews_that_seat_in_card_stack(self):
# Un-assigned owned seats → ?seat=4 previews seat 4, which is not the
# table's current turn (slot 1) → card-stack ineligible (.fa-ban).
for n in range(1, 7):
TableSeat.objects.create(room=self.room, gamer=self.viewer, slot_number=n)
content = self.client.get(self.room_url + "?seat=4").content.decode()
self.assertIn('data-active-slot="4"', content)
self.assertIn('data-state="ineligible"', content)
def test_no_seat_param_keeps_canonical_active_slot(self):
for n in range(1, 7):
TableSeat.objects.create(room=self.room, gamer=self.viewer, slot_number=n)
content = self.client.get(self.room_url).content.decode()
# Default: the table's turn is slot 1, and the viewer owns it → eligible.
self.assertIn('data-active-slot="1"', content)
self.assertIn('data-state="eligible"', content)
class PickRolesViewTest(TestCase): class PickRolesViewTest(TestCase):
def setUp(self): def setUp(self):

View File

@@ -428,6 +428,27 @@ def _role_select_context(room, user, seat_param=None):
if user.is_authenticated else [] if user.is_authenticated else []
) )
active_slot = active_seat.slot_number if active_seat else None active_slot = active_seat.slot_number if active_seat else None
# CARTE seat-switch (?seat=N): a multi-seat gamer previews a specific owned
# seat. The card-stack's active slot becomes that seat; it stays "eligible"
# (role-pickable now) only when the previewed seat is also the table's
# current turn (the lowest unassigned seat), else it shows the ineligible
# .fa-ban. No-op for the normal flow — a one-seat gamer never passes ?seat,
# and an unowned/garbage param is ignored.
if seat_param and user.is_authenticated and card_stack_state is not None:
try:
_seat_n = int(seat_param)
except (TypeError, ValueError):
_seat_n = None
if _seat_n is not None and room.table_seats.filter(
gamer=user, slot_number=_seat_n
).exists():
active_slot = _seat_n
_turn = unassigned.first() if unassigned.exists() else None
card_stack_state = (
"eligible"
if _turn is not None and _turn.slot_number == _seat_n
else "ineligible"
)
_my_role = assigned_seats[0].role if assigned_seats else None _my_role = assigned_seats[0].role if assigned_seats else None
# `equipped_deck_id` gates the JS role-select guard (role-select.js L165). # `equipped_deck_id` gates the JS role-select guard (role-select.js L165).
# Falls back to ANY of the user's seats in this room w. deck_variant set # Falls back to ANY of the user's seats in this room w. deck_variant set

View File

@@ -226,7 +226,6 @@ class CarteSeatSwitchTest(FunctionalTest):
) )
self.assertEqual(circle.get_attribute("data-tt-tokens"), "6") self.assertEqual(circle.get_attribute("data-tt-tokens"), "6")
@skip(_RED + " [workstream B — ?seat card-stack consumption]")
def test_switching_seat_loads_that_seats_role_view(self): def test_switching_seat_loads_that_seats_role_view(self):
# Clicking the me-also seat-4 circle loads ?seat=4 and the card-stack # Clicking the me-also seat-4 circle loads ?seat=4 and the card-stack
# reflects seat 4 (a non-active seat → ineligible / .fa-ban atop deck). # reflects seat 4 (a non-active seat → ineligible / .fa-ban atop deck).