import os import unittest from django.conf import settings as django_settings from django.test import tag from selenium import webdriver from selenium.webdriver.common.by import By from .base import FunctionalTest, ChannelsFunctionalTest from .management.commands.create_session import create_pre_authenticated_session from apps.applets.models import Applet from apps.epic.models import Room, GateSlot, TableSeat from apps.lyric.models import User def _fill_room_via_orm(room, emails): """Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers.""" gamers = [] for i, email in enumerate(emails, start=1): gamer, _ = User.objects.get_or_create(email=email) slot = room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() gamers.append(gamer) room.gate_status = Room.OPEN room.save() return gamers class RoleSelectTest(FunctionalTest): 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 1 — PICK ROLES dismisses gatekeeper and reveals the table # # ------------------------------------------------------------------ # def test_pick_roles_dismisses_gatekeeper_and_reveals_table(self): # 1. Founder logs in, creates room via UI, fills remaining slots via ORM self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for( lambda: self.browser.find_element(By.ID, "id_new_game_name") ).send_keys("Dragon's Den") self.browser.find_element(By.ID, "id_create_game_btn").click() self.wait_for( lambda: self.assertIn("/gate/", self.browser.current_url) ) room_url = self.browser.current_url room = Room.objects.get(name="Dragon's Den") # Fill founder's slot via UI (slot 1) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails") ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm") ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled") ) # Fill slots 2–6 via ORM emails = ["amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io"] for i, email in enumerate(emails, start=2): gamer, _ = User.objects.get_or_create(email=email) slot = room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() room.gate_status = Room.OPEN room.save() # 2. Browser sees the PICK ROLES button (gate is now open) self.browser.refresh() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn") ).click() # 3. Gatekeeper overlay is gone self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")), 0 ) ) # 4. Table is visible and prominent table = self.wait_for( lambda: self.browser.find_element(By.ID, "id_game_table") ) self.assertTrue(table.is_displayed()) # 5. Card stack is present in the table centre self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ) # 6. Six seat portraits are visible around the table seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat") self.assertEqual(len(seats), 6) # ------------------------------------------------------------------ # # Test 2 — Card stack signals eligibility to each gamer # # ------------------------------------------------------------------ # def test_card_stack_glows_for_first_gamer_only(self): # Two browsers: founder (slot 1, eligible) and friend (slot 2, not yet) founder, _ = User.objects.get_or_create(email="founder@test.io") friend, _ = User.objects.get_or_create(email="friend@test.io") room = Room.objects.create(name="Signal Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "friend@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ]) room.table_status = Room.ROLE_SELECT room.save() room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" # Founder's browser self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) stack = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ) self.assertIn("eligible", stack.get_attribute("data-state")) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack .fa-ban")), 0 ) # Friend's browser options2 = webdriver.FirefoxOptions() if os.environ.get("HEADLESS"): options2.add_argument("--headless") self.browser2 = webdriver.Firefox(options=options2) try: self.browser2.get(self.live_server_url + "/404_no_such_url/") from django.conf import settings session_key = __import__( "functional_tests.management.commands.create_session", fromlist=["create_pre_authenticated_session"] ).create_pre_authenticated_session("friend@test.io") self.browser2.add_cookie(dict( name=settings.SESSION_COOKIE_NAME, value=session_key, path="/" )) self.browser2.get(room_url) stack2 = self.wait_for( lambda: self.browser2.find_element(By.CSS_SELECTOR, ".card-stack") ) self.assertIn("ineligible", stack2.get_attribute("data-state")) self.wait_for( lambda: self.browser2.find_element( By.CSS_SELECTOR, ".card-stack .fa-ban" ) ) finally: self.browser2.quit() # ------------------------------------------------------------------ # # Test 3 — Active gamer fans cards, inspects, selects a role # # ------------------------------------------------------------------ # def test_active_gamer_fans_cards_and_selects_role(self): founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Fan 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", ]) room.table_status = Room.ROLE_SELECT room.save() room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) # 1. Click the card stack self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ).click() # 2. Role Select modal opens with 6 cards self.wait_for( lambda: self.browser.find_element(By.ID, "id_role_select") ) cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card") self.assertEqual(len(cards), 6) # 3. Blur backdrop is present self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop") # 4. Hover over first card — it flips to reveal front from selenium.webdriver.common.action_chains import ActionChains ActionChains(self.browser).move_to_element(cards[0]).perform() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_role_select .card.flipped" ) ) # 5. Click first card to select it cards[0].click() self.confirm_guard() # 6. Modal closes self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.ID, "id_role_select")), 0 ) ) # 7. Card stack returns to table centre self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ) # ------------------------------------------------------------------ # # Test 3b — Chosen role absent from next gamer's fan # # ------------------------------------------------------------------ # def test_chosen_role_absent_from_next_gamer_fan(self): from apps.epic.models import TableSeat founder, _ = User.objects.get_or_create(email="founder@test.io") friend, _ = User.objects.get_or_create(email="friend@test.io") room = Room.objects.create(name="Pool Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "friend@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ]) room.table_status = Room.ROLE_SELECT room.save() # Simulate pick_roles: create a TableSeat per filled slot for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) # Slot 1 (founder) has already chosen PC TableSeat.objects.filter(room=room, slot_number=1).update(role="PC") # Slot 2 (friend) is now the active gamer room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("friend@test.io") self.browser.get(room_url) # Card stack is eligible for slot 2 self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".card-stack[data-state='eligible']" ) ).click() # Fan opens — only 5 cards (PC is taken) self.wait_for( lambda: self.browser.find_element(By.ID, "id_role_select") ) cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card") self.assertEqual(len(cards), 5) # Specifically, no PC card in the fan self.assertEqual( len(self.browser.find_elements( By.CSS_SELECTOR, "#id_role_select .card[data-role='PC']" )), 0, ) # ------------------------------------------------------------------ # # Test 3c — Card stack stays eligible after re-entering mid-session # # ------------------------------------------------------------------ # def test_card_stack_remains_eligible_after_re_entering_mid_selection(self): """A gamer holding multiple slots should still see an eligible card stack when they re-enter the room after having already chosen a role for their earlier slot.""" from apps.epic.models import TableSeat founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Re-entry Test", owner=founder) # Founder holds slots 1 and 2; others fill the rest _fill_room_via_orm(room, [ "founder@test.io", "founder@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) # Founder's first slot has already chosen PC TableSeat.objects.filter(room=room, slot_number=1).update(role="PC") # Founder re-enters the room (simulating a page reload / re-navigation) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) # Card stack must be eligible — slot 2 (also founder's) is the active seat stack = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ) self.assertEqual(stack.get_attribute("data-state"), "eligible") # Fan shows 5 cards — PC already taken stack.click() self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select")) cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card") self.assertEqual(len(cards), 5) # ------------------------------------------------------------------ # # Test 4 — Click-away dismisses fan without selecting # # ------------------------------------------------------------------ # def test_click_away_dismisses_card_fan_without_selecting(self): founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Dismiss 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", ]) room.table_status = Room.ROLE_SELECT room.save() room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) # Open the fan self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack") ).click() self.wait_for( lambda: self.browser.find_element(By.ID, "id_role_select") ) # Click the backdrop (outside the fan) self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop").click() # Modal closes; stack still present self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.ID, "id_role_select")), 0 ) ) self.browser.find_element(By.CSS_SELECTOR, ".card-stack") # ------------------------------------------------------------------ # # Test 4b — Stack locks out immediately after selection (no WS) # # ------------------------------------------------------------------ # def test_card_stack_ineligible_immediately_after_selection(self): """After clicking a role card the stack must flip to data-state='ineligible' straight away — before any WS turn_changed event could arrive. This test runs without a Channels server so no WS event will fire; the fix must be entirely client-side.""" founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Lockout 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", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) 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() # No WS — only the JS fix can make this transition happen self.wait_for( lambda: self.assertEqual( self.browser.find_element( By.CSS_SELECTOR, ".card-stack" ).get_attribute("data-state"), "ineligible", ) ) def test_card_stack_cannot_be_reopened_after_selection(self): """Clicking the card stack immediately after picking a role must not open a second fan — the listener must have been removed.""" founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="No-reopen 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", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) # Open fan, pick a card 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() # Wait for fan to close (selectRole closes it synchronously) self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.ID, "id_role_select")), 0 ) ) # Attempt to reopen — must not work self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click() self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.ID, "id_role_select")), 0 ) ) # ------------------------------------------------------------------ # # Test 8a — Hex seats carry role labels during role select # # ------------------------------------------------------------------ # 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="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", ]) room.table_status = Room.ROLE_SELECT room.save() room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) expected = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"} self.wait_for( 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 8b — Hex seats show .fa-ban when empty # # ------------------------------------------------------------------ # 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="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", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for( 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 8c — Hex seat gets .fa-circle-check after role selected # # ------------------------------------------------------------------ # 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="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", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) # Open fan, pick first card (SC — Shepherd), 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() # 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", ) ) # The SC seat (slot 1) now shows check, no ban self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-circle-check" ) ) self.assertEqual( len(self.browser.find_elements( By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-ban" )), 0, ) class RoleSelectTrayTest(FunctionalTest): """After confirming a role pick, the role card enters the tray grid and the tray opens to reveal it. Portrait — card lands at the topmost grid square (first child, row 1 col 1). Landscape — card lands at the leftmost grid square (first child, row 1 col 1). """ EMAILS = [ "slot1@test.io", "slot2@test.io", "slot3@test.io", "slot4@test.io", "slot5@test.io", "slot6@test.io", ] 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_room(self): """Room in ROLE_SELECT with all 6 seats created, slot 1 eligible.""" founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) room = Room.objects.create(name="Tray Card Test", owner=founder) _fill_room_via_orm(room, self.EMAILS) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number ) return room def _select_role(self): """Open the fan, pick the first card, confirm the guard dialog.""" 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() # ------------------------------------------------------------------ # # T1 — Portrait: role card marks first cell; tray opens then closes # # ------------------------------------------------------------------ # def test_portrait_role_card_enters_topmost_grid_square(self): """Portrait: after confirming a role the first .tray-cell gets .tray-role-card; the grid still has exactly 8 cells; and the tray opens briefly then closes once the arc-in animation completes.""" self.browser.set_window_size(390, 844) room = self._make_room() self.create_pre_authenticated_session("slot1@test.io") self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap")) self._select_role() # First cell receives the role card class. self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_tray_grid .tray-role-card" ) ) result = self.browser.execute_script(""" var grid = document.getElementById('id_tray_grid'); var card = grid.querySelector('.tray-role-card'); return { isFirst: card !== null && card === grid.firstElementChild, count: grid.children.length, role: card ? card.dataset.role : null }; """) self.assertTrue(result["isFirst"], "Role card should be the first cell") self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells") self.assertTrue(result["role"], "First cell should carry data-role") # Tray closes after the animation sequence. self.wait_for( lambda: self.assertFalse( self.browser.execute_script("return Tray.isOpen()"), "Tray should close after the arc-in sequence" ) ) # ------------------------------------------------------------------ # # T2 — Landscape: same contract in landscape # # ------------------------------------------------------------------ # @tag('two-browser') def test_landscape_role_card_enters_leftmost_grid_square(self): """Landscape: the first .tray-cell gets .tray-role-card; grid has 8 cells; tray opens then closes.""" self.browser.set_window_size(844, 390) room = self._make_room() self.create_pre_authenticated_session("slot1@test.io") self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap")) self._select_role() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_tray_grid .tray-role-card" ) ) result = self.browser.execute_script(""" var grid = document.getElementById('id_tray_grid'); var card = grid.querySelector('.tray-role-card'); return { isFirst: card !== null && card === grid.firstElementChild, count: grid.children.length }; """) self.assertTrue(result["isFirst"], "Role card should be the first cell") self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells") self.wait_for( lambda: self.assertFalse( self.browser.execute_script("return Tray.isOpen()"), "Tray should close after the arc-in sequence" ) ) @tag('channels') class RoleSelectChannelsTest(ChannelsFunctionalTest): 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 6 — Observer sees seat arc move via WebSocket # # ------------------------------------------------------------------ # def test_observer_sees_seat_arc_during_selection(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="Arc 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", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" # 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, ".card-stack[data-state='ineligible']" )) # 2. Founder picks a role 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, ".card-stack[data-state='eligible']" )) self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click() self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select")) self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click() self.confirm_guard(browser=self.browser2) # 3. Watcher's turn arrives via WS — card-stack becomes eligible self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".card-stack[data-state='eligible']" )) finally: self.browser2.quit() def _make_browser2(self, email): """Spin up a second Firefox, authenticate email, return the browser.""" 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 5 — Tray closes on turn advance (portrait) # # ------------------------------------------------------------------ # def _make_turn_test_room(self): founder, _ = User.objects.get_or_create(email="founder@test.io") User.objects.get_or_create(email="friend@test.io") room = Room.objects.create(name="Turn Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "friend@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ]) room.table_status = Room.ROLE_SELECT room.save() for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, ) return f"{self.live_server_url}/gameboard/room/{room.id}/gate/" def test_portrait_tray_closes_on_turn_advance(self): """Portrait: after selecting a role the tray opens and the role card lands in the topmost grid square. When turn_changed arrives via WS, the tray force-closes so the next player's card-stack is not obscured.""" self.browser.set_window_size(390, 844) room_url = self._make_turn_test_room() self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".card-stack[data-state='eligible']" )) # Select a role — card lands in topmost grid square. self.browser.find_element(By.CSS_SELECTOR, ".card-stack").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() # Wait for fetch .then() — card must be first child of grid. self.wait_for(lambda: self.assertTrue(self.browser.execute_script(""" var card = document.querySelector('#id_tray_grid .tray-role-card'); return card !== null && card === card.parentElement.firstElementChild; """))) # 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 turn_changed arrives via WS.""" self.browser.set_window_size(844, 390) room_url = self._make_turn_test_room() self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".card-stack[data-state='eligible']" )) self.browser.find_element(By.CSS_SELECTOR, ".card-stack").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() # Wait for fetch .then() — card must be first child of grid. self.wait_for(lambda: self.assertTrue(self.browser.execute_script(""" var card = document.querySelector('#id_tray_grid .tray-role-card'); return card !== null && card === card.parentElement.firstElementChild; """))) # 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" )) # ------------------------------------------------------------------ # # Test 7 — PICK SIGS appears + card stack removed on last role # # ------------------------------------------------------------------ # def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self): """When the sixth and final role is confirmed, the all_roles_filled WS event makes the PICK SIGS button visible and removes the card stack from the DOM entirely.""" emails = [ "founder@test.io", "amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ] founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Last Role Test", owner=founder) _fill_room_via_orm(room, emails) room.table_status = Room.ROLE_SELECT room.save() # Pre-assign 5 roles (slots 2–6); founder (slot 1) is the final picker. pre_assigned = {2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"} for slot in room.gate_slots.order_by("slot_number"): TableSeat.objects.create( room=room, gamer=slot.gamer, slot_number=slot.slot_number, role=pre_assigned.get(slot.slot_number), ) room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".card-stack[data-state='eligible']" )) # Founder picks the last remaining role (PC — the only card in the fan). self.browser.find_element(By.CSS_SELECTOR, ".card-stack").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() # PICK SIGS wrap must become visible via the all_roles_filled WS event. self.wait_for(lambda: self.assertFalse( self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"), )) # Card stack must be removed from the DOM entirely. self.wait_for(lambda: self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack")), 0, ))