From c0016418ccfaa5788beb9d191cb61b8035790954 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 25 Mar 2026 01:30:18 -0400 Subject: [PATCH] hopefully plugged pipeline fail for FT to assert stock card deck version; 11 new test_models ITs & 12 new test_views ITs in apps.epic.tests --- src/apps/epic/tests/integrated/test_models.py | 170 ++++++++++- src/apps/epic/tests/integrated/test_views.py | 193 +++++++++++- src/functional_tests/test_game_kit.py | 5 +- src/functional_tests/test_room_role_select.py | 277 ++++++++++++++++++ 4 files changed, 642 insertions(+), 3 deletions(-) diff --git a/src/apps/epic/tests/integrated/test_models.py b/src/apps/epic/tests/integrated/test_models.py index 6e32e65..ec67170 100644 --- a/src/apps/epic/tests/integrated/test_models.py +++ b/src/apps/epic/tests/integrated/test_models.py @@ -5,7 +5,10 @@ from django.urls import reverse from django.utils import timezone from apps.lyric.models import Token, User -from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat, debit_token, select_token +from apps.epic.models import ( + DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard, + debit_token, select_token, sig_deck_cards, sig_seat_order, active_sig_seat, +) class RoomCreationTest(TestCase): @@ -214,3 +217,168 @@ class RoomInviteTest(TestCase): Q(invites__invitee_email=friend.email, invites__status=RoomInvite.PENDING) ).distinct() self.assertIn(self.room, rooms) + + +# ── Significator deck helpers ───────────────────────────────────────────────── + +SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"] + + +def _make_sig_cards(deck): + """Create the 18 unique TarotCard types used in the Significator deck.""" + for suit in ["WANDS", "CUPS", "SWORDS", "PENTACLES"]: + for number, court in [(11, "Maid"), (12, "Jack"), (13, "Queen"), (14, "King")]: + TarotCard.objects.create( + deck_variant=deck, arcana="MINOR", suit=suit, number=number, + name=f"{court} of {suit.capitalize()}", + slug=f"{court.lower()}-of-{suit.lower()}-em", + keywords_upright=[], keywords_reversed=[], + ) + TarotCard.objects.create( + deck_variant=deck, arcana="MAJOR", number=0, + name="The Schiz", slug="the-schiz", + keywords_upright=[], keywords_reversed=[], + ) + TarotCard.objects.create( + deck_variant=deck, arcana="MAJOR", number=1, + name="Pope 1: Chancellor", slug="pope-1-chancellor", + keywords_upright=[], keywords_reversed=[], + ) + + +def _full_sig_room(name="Sig Room", role_order=None): + """Return (room, gamers, earthman) with all 6 seats filled, roles assigned, + table_status=SIG_SELECT, and every gamer's equipped_deck set to Earthman.""" + if role_order is None: + role_order = SIG_SEAT_ORDER[:] + earthman = DeckVariant.objects.create( + slug="earthman", name="Earthman Deck", card_count=108, is_default=True + ) + _make_sig_cards(earthman) + owner = User.objects.create(email="founder@sig.io") + gamers = [owner] + for i in range(2, 7): + gamers.append(User.objects.create(email=f"g{i}@sig.io")) + for gamer in gamers: + gamer.equipped_deck = earthman + gamer.save(update_fields=["equipped_deck"]) + room = Room.objects.create(name=name, owner=owner) + for i, (gamer, role) in enumerate(zip(gamers, role_order), start=1): + slot = room.gate_slots.get(slot_number=i) + slot.gamer = gamer + slot.status = GateSlot.FILLED + slot.save() + TableSeat.objects.create( + room=room, gamer=gamer, slot_number=i, + role=role, role_revealed=True, + ) + room.table_status = Room.SIG_SELECT + room.save() + return room, gamers, earthman + + +class SigDeckCompositionTest(TestCase): + """sig_deck_cards(room) returns exactly 36 cards with correct suit/arcana split.""" + + def setUp(self): + self.room, self.gamers, self.earthman = _full_sig_room() + + def test_sig_deck_returns_36_cards(self): + cards = sig_deck_cards(self.room) + self.assertEqual(len(cards), 36) + + def test_sc_ac_contribute_court_cards_of_swords_and_cups(self): + cards = sig_deck_cards(self.room) + sc_ac = [c for c in cards if c.suit in ("SWORDS", "CUPS")] + # M/J/Q/K × 2 suits × 2 roles = 16 + self.assertEqual(len(sc_ac), 16) + self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac)) + + def test_pc_bc_contribute_court_cards_of_wands_and_pentacles(self): + cards = sig_deck_cards(self.room) + pc_bc = [c for c in cards if c.suit in ("WANDS", "PENTACLES")] + self.assertEqual(len(pc_bc), 16) + self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc)) + + def test_nc_ec_contribute_schiz_and_chancellor(self): + cards = sig_deck_cards(self.room) + major = [c for c in cards if c.arcana == "MAJOR"] + self.assertEqual(len(major), 4) + self.assertEqual(sorted(c.number for c in major), [0, 0, 1, 1]) + + def test_each_card_appears_twice_once_per_pile(self): + """18 unique card specs × 2 (levity + gravity) = 36 total.""" + cards = sig_deck_cards(self.room) + slugs = [c.slug for c in cards] + unique_slugs = set(slugs) + self.assertEqual(len(unique_slugs), 18) + self.assertTrue(all(slugs.count(s) == 2 for s in unique_slugs)) + + +class SigSeatOrderTest(TestCase): + """sig_seat_order() and active_sig_seat() return seats in PC→NC→EC→SC→AC→BC order.""" + + def setUp(self): + # Assign roles in reverse of canonical order to prove reordering works + self.room, self.gamers, _ = _full_sig_room( + name="Order Room", + role_order=["BC", "AC", "SC", "EC", "NC", "PC"], + ) + + def test_sig_seat_order_returns_canonical_role_sequence(self): + seats = sig_seat_order(self.room) + self.assertEqual([s.role for s in seats], SIG_SEAT_ORDER) + + def test_active_sig_seat_is_first_seat_without_significator(self): + seat = active_sig_seat(self.room) + self.assertEqual(seat.role, "PC") + + def test_active_sig_seat_advances_after_significator_set(self): + pc_seat = TableSeat.objects.get(room=self.room, role="PC") + earthman = DeckVariant.objects.get(slug="earthman") + card = TarotCard.objects.filter(deck_variant=earthman, arcana="MINOR").first() + pc_seat.significator = card + pc_seat.save() + seat = active_sig_seat(self.room) + self.assertEqual(seat.role, "NC") + + def test_active_sig_seat_is_none_when_all_chosen(self): + earthman = DeckVariant.objects.get(slug="earthman") + cards = list(TarotCard.objects.filter(deck_variant=earthman)) + for i, seat in enumerate(TableSeat.objects.filter(room=self.room)): + seat.significator = cards[i] + seat.save() + self.assertIsNone(active_sig_seat(self.room)) + + +class SigCardFieldTest(TestCase): + """TableSeat.significator FK to TarotCard — default null, assignable.""" + + def setUp(self): + earthman = DeckVariant.objects.create( + slug="earthman", name="Earthman Deck", card_count=108, is_default=True + ) + self.card = TarotCard.objects.create( + deck_variant=earthman, arcana="MINOR", suit="WANDS", number=11, + name="Maid of Wands", slug="maid-of-wands-em", + keywords_upright=[], keywords_reversed=[], + ) + owner = User.objects.create(email="owner@test.io") + room = Room.objects.create(name="Field Test", owner=owner) + self.seat = TableSeat.objects.create(room=room, gamer=owner, slot_number=1, role="PC") + + def test_significator_defaults_to_none(self): + self.assertIsNone(self.seat.significator) + + def test_significator_can_be_assigned(self): + self.seat.significator = self.card + self.seat.save() + self.seat.refresh_from_db() + self.assertEqual(self.seat.significator, self.card) + + def test_significator_nullable_on_delete(self): + self.seat.significator = self.card + self.seat.save() + self.card.delete() + self.seat.refresh_from_db() + self.assertIsNone(self.seat.significator) diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 665d53f..de277e6 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -6,7 +6,9 @@ from django.urls import reverse from django.utils import timezone from apps.lyric.models import Token, User -from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat +from apps.epic.models import ( + DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard, +) class RoomCreationViewTest(TestCase): @@ -766,3 +768,192 @@ class ReleaseSlotViewTest(TestCase): ) self.room.refresh_from_db() self.assertEqual(self.room.gate_status, Room.GATHERING) + + +# ── Significator Selection ──────────────────────────────────────────────────── + +SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"] + + +def _make_sig_cards(deck): + for suit in ["WANDS", "CUPS", "SWORDS", "PENTACLES"]: + for number, court in [(11, "Maid"), (12, "Jack"), (13, "Queen"), (14, "King")]: + TarotCard.objects.create( + deck_variant=deck, arcana="MINOR", suit=suit, number=number, + name=f"{court} of {suit.capitalize()}", + slug=f"{court.lower()}-of-{suit.lower()}-em", + keywords_upright=[], keywords_reversed=[], + ) + TarotCard.objects.create( + deck_variant=deck, arcana="MAJOR", number=0, + name="The Schiz", slug="the-schiz", + keywords_upright=[], keywords_reversed=[], + ) + TarotCard.objects.create( + deck_variant=deck, arcana="MAJOR", number=1, + name="Pope 1: Chancellor", slug="pope-1-chancellor", + keywords_upright=[], keywords_reversed=[], + ) + + +def _full_sig_setUp(test_case, role_order=None): + """Populate test_case with a SIG_SELECT room; return (room, gamers, earthman, card_in_deck).""" + if role_order is None: + role_order = SIG_SEAT_ORDER[:] + earthman = DeckVariant.objects.create( + slug="earthman", name="Earthman Deck", card_count=108, is_default=True + ) + _make_sig_cards(earthman) + founder = User.objects.create(email="founder@test.io") + gamers = [founder] + for i in range(2, 7): + gamers.append(User.objects.create(email=f"g{i}@test.io")) + for gamer in gamers: + gamer.equipped_deck = earthman + gamer.save(update_fields=["equipped_deck"]) + room = Room.objects.create(name="Sig Room", owner=founder) + for i, (gamer, role) in enumerate(zip(gamers, role_order), start=1): + slot = room.gate_slots.get(slot_number=i) + slot.gamer = gamer + slot.status = GateSlot.FILLED + slot.save() + TableSeat.objects.create( + room=room, gamer=gamer, slot_number=i, role=role, role_revealed=True, + ) + room.gate_status = Room.OPEN + room.table_status = Room.SIG_SELECT + room.save() + card_in_deck = TarotCard.objects.get( + deck_variant=earthman, arcana="MINOR", suit="WANDS", number=11 + ) + test_case.client.force_login(founder) + return room, gamers, earthman, card_in_deck + + +class SigSelectRenderingTest(TestCase): + """Gate view at SIG_SELECT renders the Significator deck.""" + + def setUp(self): + self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) + self.url = reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) + + def test_sig_deck_element_present(self): + response = self.client.get(self.url) + self.assertContains(response, "id_sig_deck") + + def test_sig_deck_contains_36_sig_cards(self): + response = self.client.get(self.url) + self.assertEqual(response.content.decode().count('class="sig-card"'), 36) + + def test_seats_rendered_in_pc_nc_ec_sc_ac_bc_order(self): + response = self.client.get(self.url) + content = response.content.decode() + positions = {role: content.find(f'data-role="{role}"') for role in SIG_SEAT_ORDER} + # Every role must appear + self.assertTrue(all(pos != -1 for pos in positions.values())) + # Rendered in canonical sequence + ordered = sorted(SIG_SEAT_ORDER, key=lambda r: positions[r]) + self.assertEqual(ordered, SIG_SEAT_ORDER) + + def test_sig_deck_not_present_during_role_select(self): + self.room.table_status = Room.ROLE_SELECT + self.room.save() + response = self.client.get(self.url) + self.assertNotContains(response, "id_sig_deck") + + +class SelectSigCardViewTest(TestCase): + """select_sig view — records choice, enforces turn order, rejects bad input.""" + + def setUp(self): + self.room, self.gamers, self.earthman, self.card = _full_sig_setUp(self) + # Founder is slot 1, role=PC — active first in canonical order + self.url = reverse("epic:select_sig", kwargs={"room_id": self.room.id}) + + def _post(self, card_id=None, client=None): + c = client or self.client + return c.post(self.url, data={"card_id": card_id or self.card.id}) + + def test_select_sig_records_choice_on_active_seat(self): + self._post() + seat = TableSeat.objects.get(room=self.room, role="PC") + self.assertEqual(seat.significator, self.card) + + def test_select_sig_returns_200(self): + response = self._post() + self.assertEqual(response.status_code, 200) + + def test_select_sig_wrong_turn_makes_no_change(self): + # Gamer 2 is NC — not their turn yet + self.client.force_login(self.gamers[1]) + self._post() + seat = TableSeat.objects.get(room=self.room, role="NC") + self.assertIsNone(seat.significator) + + def test_select_sig_wrong_turn_returns_403(self): + self.client.force_login(self.gamers[1]) + response = self._post() + self.assertEqual(response.status_code, 403) + + def test_select_sig_card_not_in_deck_returns_400(self): + # Create a card that is not in the sig deck (e.g. a pip card) + other = TarotCard.objects.create( + deck_variant=self.earthman, arcana="MINOR", suit="WANDS", number=5, + name="Five of Wands", slug="five-of-wands-em", + keywords_upright=[], keywords_reversed=[], + ) + response = self._post(card_id=other.id) + self.assertEqual(response.status_code, 400) + + def test_select_sig_card_already_taken_returns_409(self): + # Another seat already holds this card as their significator + nc_seat = TableSeat.objects.get(room=self.room, role="NC") + nc_seat.significator = self.card + nc_seat.save() + response = self._post() + self.assertEqual(response.status_code, 409) + + def test_select_sig_advances_active_seat_to_nc(self): + self._post() + from apps.epic.models import active_sig_seat + seat = active_sig_seat(self.room) + self.assertEqual(seat.role, "NC") + + def test_select_sig_notifies_ws(self): + with patch("apps.epic.views._notify_sig_selected") as mock_notify: + self._post() + mock_notify.assert_called_once_with(self.room.id) + + def test_select_sig_requires_login(self): + self.client.logout() + response = self._post() + self.assertEqual(response.status_code, 302) + self.assertIn("/accounts/login/", response.url) + + def test_select_sig_wrong_phase_redirects(self): + self.room.table_status = Room.ROLE_SELECT + self.room.save() + response = self._post() + self.assertRedirects( + response, reverse("epic:gatekeeper", args=[self.room.id]) + ) + + def test_select_sig_last_choice_does_not_advance_to_none(self): + """After all 6 significators chosen, active_sig_seat() is None — + no unhandled AttributeError in the view.""" + cards = list(TarotCard.objects.filter(deck_variant=self.earthman, arcana="MINOR")) + seats_in_order = list( + TableSeat.objects.filter(room=self.room).order_by("slot_number") + ) + # Assign all but the last (BC) manually + for seat, card in zip(seats_in_order[:-1], cards): + seat.significator = card + seat.save() + # BC gamer POSTs the final choice + bc_seat = TableSeat.objects.get(room=self.room, role="BC") + self.client.force_login(bc_seat.gamer) + last_card = TarotCard.objects.filter( + deck_variant=self.earthman, arcana="MAJOR", number=0 + ).first() + response = self.client.post(self.url, data={"card_id": last_card.id}) + self.assertIn(response.status_code, (200, 302)) diff --git a/src/functional_tests/test_game_kit.py b/src/functional_tests/test_game_kit.py index 0964630..935ba7e 100644 --- a/src/functional_tests/test_game_kit.py +++ b/src/functional_tests/test_game_kit.py @@ -100,7 +100,10 @@ class GameKitTest(FunctionalTest): By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck" ) ) - ActionChains(self.browser).move_to_element(deck_el).perform() + # Dispatch mouseenter via JS — more reliable than ActionChains in headless CI + self.browser.execute_script( + "arguments[0].dispatchEvent(new Event('mouseenter'))", deck_el + ) tooltip = self.browser.find_element( By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck .token-tooltip" ) diff --git a/src/functional_tests/test_room_role_select.py b/src/functional_tests/test_room_role_select.py index 51aa2b1..2756e4f 100644 --- a/src/functional_tests/test_room_role_select.py +++ b/src/functional_tests/test_room_role_select.py @@ -682,3 +682,280 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest): )) finally: self.browser2.quit() + + +# ── Significator Selection ──────────────────────────────────────────────────── +# +# After all 6 roles are revealed the room enters SIG_SELECT. A 36-card +# Significator deck appears at the table centre; gamers pick in seat order +# (PC → NC → EC → SC → AC → BC). Selected cards are removed from the shared +# pile in real time via WebSocket, exactly as role selection works. +# +# Deck composition (18 unique cards × 2 — one from levity, one from gravity): +# SC / AC (Shepherd / Alchemist) → M/J/Q/K of Swords & Cups (16 cards) +# PC / BC (Player / Builder) → M/J/Q/K of Wands & Pentacles (16 cards) +# NC / EC (Narrator / Economist) → The Schiz (0) + Chancellor (1) ( 4 cards) +# +# Levity pile: SC, PC, NC contributions. Gravity pile: AC, BC, EC contributions. +# Cards retain the contributor's deck card-back — up to 6 distinct backs active. +# +# ───────────────────────────────────────────────────────────────────────────── + +SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"] + + +def _assign_all_roles(room, role_order=None): + """Assign roles to all slots, reveal them, and advance to SIG_SELECT.""" + if role_order is None: + role_order = SIG_SEAT_ORDER[:] + for slot in room.gate_slots.order_by("slot_number"): + TableSeat.objects.update_or_create( + room=room, + slot_number=slot.slot_number, + defaults={ + "gamer": slot.gamer, + "role": role_order[slot.slot_number - 1], + "role_revealed": True, + }, + ) + room.table_status = Room.SIG_SELECT + room.save() + + +class SigSelectTest(FunctionalTest): + """Significator Selection — non-WebSocket tests.""" + + def setUp(self): + super().setUp() + Applet.objects.get_or_create( + slug="new-game", defaults={"name": "New Game", "context": "gameboard"} + ) + Applet.objects.get_or_create( + slug="my-games", defaults={"name": "My Games", "context": "gameboard"} + ) + + # ------------------------------------------------------------------ # + # Test S1 — Significator deck of 36 cards appears at table centre # + # ------------------------------------------------------------------ # + + def test_sig_deck_appears_with_36_cards_after_all_roles_revealed(self): + founder, _ = User.objects.get_or_create(email="founder@test.io") + room = Room.objects.create(name="Sig Deck 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", + ]) + _assign_all_roles(room) + + self.create_pre_authenticated_session("founder@test.io") + room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" + self.browser.get(room_url) + + # Significator deck is visible at the table centre + sig_deck = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_sig_deck") + ) + self.assertTrue(sig_deck.is_displayed()) + + # It contains exactly 36 cards + cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_sig_deck .sig-card") + self.assertEqual(len(cards), 36) + + # ------------------------------------------------------------------ # + # Test S2 — Seats reorder to canonical role sequence at SIG_SELECT # + # ------------------------------------------------------------------ # + + def test_seats_display_in_pc_nc_ec_sc_ac_bc_order_after_reveal(self): + """Slots were filled in arbitrary token-drop order; after roles are + revealed the seat portraits must appear in PC→NC→EC→SC→AC→BC order.""" + founder, _ = User.objects.get_or_create(email="founder@test.io") + room = Room.objects.create(name="Seat Order Test", owner=founder) + # Assign roles in reverse of canonical order so the reordering is visible + _fill_room_via_orm(room, [ + "founder@test.io", "amigo@test.io", "bud@test.io", + "pal@test.io", "dude@test.io", "bro@test.io", + ]) + _assign_all_roles(room, role_order=["BC", "AC", "SC", "EC", "NC", "PC"]) + + self.create_pre_authenticated_session("founder@test.io") + room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" + self.browser.get(room_url) + + self.wait_for(lambda: self.browser.find_element(By.ID, "id_sig_deck")) + + seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat[data-role]") + self.assertEqual(len(seats), 6) + roles_in_order = [s.get_attribute("data-role") for s in seats] + self.assertEqual(roles_in_order, SIG_SEAT_ORDER) + + # ------------------------------------------------------------------ # + # Test S3 — First seat (PC) can select a significator; deck shrinks # + # ------------------------------------------------------------------ # + + def test_first_seat_pc_can_select_significator_and_deck_shrinks(self): + founder, _ = User.objects.get_or_create(email="founder@test.io") + room = Room.objects.create(name="PC Select Test", owner=founder) + # Founder is assigned PC (slot 1 → first in canonical order → active) + _fill_room_via_orm(room, [ + "founder@test.io", "amigo@test.io", "bud@test.io", + "pal@test.io", "dude@test.io", "bro@test.io", + ]) + _assign_all_roles(room, role_order=["PC", "NC", "EC", "SC", "AC", "BC"]) + + self.create_pre_authenticated_session("founder@test.io") + room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" + self.browser.get(room_url) + + # 36-card sig deck is present and the founder's seat is active + self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_sig_deck .sig-card") + ) + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".table-seat.active[data-role='PC']" + ) + ) + + # Click the first card in the significator deck to select it + first_card = self.browser.find_element( + By.CSS_SELECTOR, "#id_sig_deck .sig-card" + ) + first_card.click() + self.confirm_guard() + + # Deck now has 35 cards — selected card removed + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, "#id_sig_deck .sig-card")), + 35, + ) + ) + + # Founder's significator appears in their inventory + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, "#id_inv_sig_card .card" + ) + ) + + # Active seat advances to NC + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".table-seat.active[data-role='NC']" + ) + ) + + # ------------------------------------------------------------------ # + # Test S4 — Ineligible seat cannot interact with sig deck # + # ------------------------------------------------------------------ # + + def test_non_active_seat_cannot_select_significator(self): + founder, _ = User.objects.get_or_create(email="founder@test.io") + room = Room.objects.create(name="Ineligible Sig Test", owner=founder) + # Founder is NC (second in canonical order) — not first + _fill_room_via_orm(room, [ + "founder@test.io", "amigo@test.io", "bud@test.io", + "pal@test.io", "dude@test.io", "bro@test.io", + ]) + _assign_all_roles(room, role_order=["NC", "PC", "EC", "SC", "AC", "BC"]) + + self.create_pre_authenticated_session("founder@test.io") + room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" + self.browser.get(room_url) + + self.wait_for(lambda: self.browser.find_element(By.ID, "id_sig_deck")) + + # Click a sig card — it must not trigger a selection (deck stays at 36) + self.browser.find_element(By.CSS_SELECTOR, "#id_sig_deck .sig-card").click() + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, "#id_sig_deck .sig-card")), + 36, + ) + ) + + +@tag("channels") +class SigSelectChannelsTest(ChannelsFunctionalTest): + """Significator Selection — WebSocket tests.""" + + def setUp(self): + super().setUp() + Applet.objects.get_or_create( + slug="new-game", defaults={"name": "New Game", "context": "gameboard"} + ) + Applet.objects.get_or_create( + slug="my-games", defaults={"name": "My Games", "context": "gameboard"} + ) + + def _make_browser2(self, email): + session_key = create_pre_authenticated_session(email) + options = webdriver.FirefoxOptions() + if os.environ.get("HEADLESS"): + options.add_argument("--headless") + b = webdriver.Firefox(options=options) + b.get(self.live_server_url + "/404_no_such_url/") + b.add_cookie(dict( + name=django_settings.SESSION_COOKIE_NAME, + value=session_key, + path="/", + )) + return b + + # ------------------------------------------------------------------ # + # Test S5 — Selected sig card disappears for watching gamer (WS) # + # ------------------------------------------------------------------ # + + def test_selected_sig_card_removed_from_deck_for_other_gamers(self): + founder, _ = User.objects.get_or_create(email="founder@test.io") + User.objects.get_or_create(email="watcher@test.io") + room = Room.objects.create(name="Sig WS Test", owner=founder) + _fill_room_via_orm(room, [ + "founder@test.io", "watcher@test.io", "bud@test.io", + "pal@test.io", "dude@test.io", "bro@test.io", + ]) + # Founder is PC (active first); watcher is NC (second) + _assign_all_roles(room, role_order=["PC", "NC", "EC", "SC", "AC", "BC"]) + room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" + + # Watcher loads room, sees 36 cards + self.create_pre_authenticated_session("watcher@test.io") + self.browser.get(room_url) + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, "#id_sig_deck .sig-card")), + 36, + ) + ) + + # Founder picks a significator in second browser + self.browser2 = self._make_browser2("founder@test.io") + try: + self.browser2.get(room_url) + self.wait_for(lambda: self.browser2.find_element( + By.CSS_SELECTOR, ".table-seat.active[data-role='PC']" + )) + self.browser2.find_element( + By.CSS_SELECTOR, "#id_sig_deck .sig-card" + ).click() + self.confirm_guard(browser=self.browser2) + + # Watcher's deck shrinks to 35 without a page reload + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements( + By.CSS_SELECTOR, "#id_sig_deck .sig-card" + )), + 35, + ) + ) + + # Active seat advances to NC in both browsers + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".table-seat.active[data-role='NC']" + )) + self.wait_for(lambda: self.browser2.find_element( + By.CSS_SELECTOR, ".table-seat.active[data-role='NC']" + )) + finally: + self.browser2.quit()