import time from datetime import timedelta from django.utils import timezone from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet from apps.epic.models import Room, GateSlot, select_token from apps.lyric.models import Token, User class GatekeeperTest(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"} ) def test_founder_creates_room_and_sees_gatekeeper(self): # 1. Log in, navigate to gameboard self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/gameboard/") # 2. New Game applet has room name input, create button self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_new_game") ) self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room") self.browser.find_element(By.ID, "id_create_game_btn").click() # 3. User is redirected to Gatekeeper page for new room self.wait_for( lambda: self.assertIn("/gameboard/room/", self.browser.current_url) ) self.wait_for( lambda: self.assertIn("/gate/", self.browser.current_url) ) # 4. Page shows room name, GATHERING status body = self.browser.find_element(By.TAG_NAME, "body") self.assertIn("TEST ROOM", body.text) self.assertIn("GATHERING GAMERS", body.text) # 5. Six token slot circles are visible, all empty slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot") self.assertEqual(len(slots), 6) for slot in slots: self.assertIn("empty", slot.get_attribute("class")) # 6. Shared coin slot is present; no individual drop buttons self.browser.find_element(By.CSS_SELECTOR, ".token-slot") self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0 ) def test_founder_drops_token_and_slot_fills(self): # 1. Set up: log in, create room, arrive at gatekeeper 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") ) 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) ) # 2. Founder clicks Insert Token via the shared coin slot self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails") ).click() # 3. Slot 1 (lowest) now shows OK button; slot is reserved ok_btn = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm" ) ) # 4. Founder clicks OK → slot fills ok_btn.click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled") ) slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot") self.assertIn("filled", slots[0].get_attribute("class")) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0 ) def test_room_appears_in_my_games_after_creation(self): # 1. Set up founder, game room, name 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") ) 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) ) # 2. Navigate back to gameboard self.browser.get(self.live_server_url + "/gameboard/") my_games = self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_my_games") ) self.assertIn("Dragon's Den", my_games.text) def test_second_gamer_drops_token_into_open_slot(self): # 1. Founder creates room, confirms slot 1 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") ) 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 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") ) # 2. Founder invites friend invite_input = self.wait_for( lambda: self.browser.find_element(By.ID, "id_invite_email") ) invite_input.send_keys("friend@test.io") self.browser.find_element(By.ID, "id_invite_btn").click() # 3. Friend logs in, sees invitation in My Games self.create_pre_authenticated_session("friend@test.io") self.browser.get(self.live_server_url + "/gameboard/") my_games = self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_my_games") ) self.assertIn("Dragon's Den", my_games.text) # 4. Friend follows link to gatekeeper self.browser.find_element(By.LINK_TEXT, "Dragon's Den").click() # 5. Friend drops token via coin slot and confirms 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() # 6. Now two slots filled self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot.filled")), 2 ) ) def test_gate_opens_when_all_slots_filled(self): # 1. Founder creates room 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") ) 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) ) # 2. Founder confirms slot 1 via coin slot 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") ) # 3. Fill slots 2–6 directly via ORM room = Room.objects.get(name="Dragon's Den") for i, email in enumerate([ "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io", "g6@test.io" ], start=2): gamer = User.objects.create(email=email) slot = room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() room.refresh_from_db() room.gate_status = Room.OPEN room.save() # 4. Gatekeeper disappears via htmx self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-modal")), 0 ) ) def test_owner_can_delete_room_via_gear_menu(self): 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")) self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room") self.browser.find_element(By.ID, "id_create_game_btn").click() self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url)) self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger") ).click() self.wait_for(lambda: self.assertEqual( self.browser.current_url, self.live_server_url + "/gameboard/" )) self.assertFalse(Room.objects.filter(name="Doomed Room").exists()) def test_gatekeeper_overlay_persists_after_htmx_poll(self): # 1. Create room directly (GATHERING) and navigate to its gate URL self.create_pre_authenticated_session("founder@test.io") founder = User.objects.get(email="founder@test.io") room = Room.objects.create(name="Persistent Room", owner=founder) self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/") # 2. Assert overlay visible on initial page load self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay") ) # 3. Wait for HTMX poll cycle to fire (poll interval is 3s) time.sleep(4) # 4. Assert overlay still present and visible after poll overlays = self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay") self.assertEqual(len(overlays), 1) self.assertTrue(overlays[0].is_displayed()) def test_gamer_can_abandon_room_via_gear_menu(self): founder = User.objects.create(email="founder@test.io") room = Room.objects.create(name="Dragon's Den", owner=founder) slot = room.gate_slots.get(slot_number=2) self.create_pre_authenticated_session("gamer@test.io") gamer, _ = User.objects.get_or_create(email="gamer@test.io") slot.gamer = gamer slot.status = "FILLED" slot.save() self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/") self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon") ).click() self.wait_for(lambda: self.assertEqual( self.browser.current_url, self.live_server_url + "/gameboard/" )) slot.refresh_from_db() self.assertEqual(slot.status, "EMPTY") self.assertIsNone(slot.gamer) class CoinSlotTest(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"} ) self.create_pre_authenticated_session("founder@test.io") self.founder = User.objects.get(email="founder@test.io") self.room = Room.objects.create(name="Coin Room", owner=self.founder) self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/" def test_coin_slot_active_for_eligible_gamer(self): # Gamer with no slot arrives at gatekeeper — coin slot is active self.browser.get(self.gate_url) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active") ) self.browser.find_element(By.CSS_SELECTOR, "button.token-rails") def test_drop_token_reserves_lowest_empty_slot(self): # Gamer drops token; slot 1 (lowest) becomes reserved with OK button self.browser.get(self.gate_url) 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, ".gate-slot[data-slot='1'] .btn-confirm" ) ) slot = self.room.gate_slots.get(slot_number=1) slot.refresh_from_db() self.assertEqual(slot.status, GateSlot.RESERVED) self.assertEqual(slot.gamer, self.founder) def test_confirm_fills_slot_and_removes_ok_button(self): # Drop then confirm → slot 1 FILLED, OK button gone self.browser.get(self.gate_url) 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") ) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0 ) slot = self.room.gate_slots.get(slot_number=1) slot.refresh_from_db() self.assertEqual(slot.status, GateSlot.FILLED) def test_gamer_can_return_pending_token(self): # Drop then return via Push to Return → slot remains empty self.browser.get(self.gate_url) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails") ).click() # Push to Return appears in coin slot self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-return-btn") ).click() # Slot 1 still empty; coin slot active again self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active") ) slot = self.room.gate_slots.get(slot_number=1) slot.refresh_from_db() self.assertEqual(slot.status, GateSlot.EMPTY) def test_coin_slot_locked_while_another_token_is_pending(self): # Pre-set slot 1 as RESERVED by a different user other = User.objects.create(email="other@test.io") slot = self.room.gate_slots.get(slot_number=1) slot.gamer = other slot.status = GateSlot.RESERVED slot.reserved_at = timezone.now() slot.save() # Current user (founder) sees coin slot locked self.browser.get(self.gate_url) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.locked") ) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0 ) def test_last_gamer_sees_pick_roles_button(self): # Fill slots 1–5 via ORM; slot 6 empty for i, email in enumerate([ "g1@test.io", "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io" ], start=1): gamer = User.objects.create(email=email) slot = self.room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() # Founder (no slot yet) drops token → gets slot 6 self.browser.get(self.gate_url) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails") ).click() # Slot 6 shows PICK ROLES instead of OK self.wait_for( lambda: self.assertIn( "PICK ROLES", self.browser.find_element( By.CSS_SELECTOR, ".gate-slot[data-slot='6']" ).text, ) ) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0 ) class TokenPriorityTest(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"} ) self.create_pre_authenticated_session("gamer@test.io") self.gamer = User.objects.get(email="gamer@test.io") self.room = Room.objects.create(name="Token Room", owner=self.gamer) self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/" self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN) def test_coin_is_used_by_default(self): # 1. COIN token created at signup, not yet leased to a room self.assertEqual(self.coin.token_type, Token.COIN) self.assertIsNone(self.coin.current_room) # 2. Gamer drops token and confirms self.browser.get(self.gate_url) 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") ) # 3. Coin is now leased to this room, page not refreshed self.assertEqual(self.browser.current_url, self.gate_url) self.coin.refresh_from_db() self.assertEqual(self.coin.current_room, self.room) def test_free_token_used_when_coin_in_use(self): # 1. Coin already leased to another room other_room = Room.objects.create(name="Other Room", owner=self.gamer) self.coin.current_room = other_room self.coin.save() # 2. Gamer has one unexpired free token (signup gives one; delete it and add fresh) Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() Token.objects.create( user=self.gamer, token_type=Token.FREE, expires_at=timezone.now() + timedelta(days=7), ) # 3. Gamer drops token → Free Token consumed self.browser.get(self.gate_url) 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") ) self.assertEqual( Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0 ) # 4. Coin untouched, still leased to other room self.assertEqual(self.browser.current_url, self.gate_url) self.coin.refresh_from_db() self.assertEqual(self.coin.current_room, other_room) def test_tithe_token_used_when_free_tokens_exhausted(self): # 1. Coin in use, no Free Tokens, one Tithe Token other_room = Room.objects.create(name="Other Room", owner=self.gamer) self.coin.current_room = other_room self.coin.save() Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE) # 2. Gamer drops token → tithe consumed self.browser.get(self.gate_url) 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") ) # Tithe row deleted, page hasn't refreshed self.assertEqual(self.browser.current_url, self.gate_url) self.assertFalse(Token.objects.filter(pk=tithe.pk).exists()) def test_slot_blocked_when_no_tokens_available(self): # Coin in use, no Free Tokens, no Tithe Tokens → depleted state other_room = Room.objects.create(name="Other Room", owner=self.gamer) self.coin.current_room = other_room self.coin.save() Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() self.browser.get(self.gate_url) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.depleted") ) self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0 ) def test_staff_backstage_pass_bypasses_token_cost(self): # 1. Staff user has a PASS token self.gamer.is_staff = True self.gamer.save() pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS) # 2. Drops token, confirms as normal self.browser.get(self.gate_url) 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") ) # 3. Pass not consumed, coin not leased; no reload self.assertEqual(self.browser.current_url, self.gate_url) self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists()) self.coin.refresh_from_db() self.assertIsNone(self.coin.current_room) class GameKitInsertTest(FunctionalTest): """Token selected from Game Kit, inserted via token-slot click.""" def setUp(self): super().setUp() self.create_pre_authenticated_session("gamer@insert.io") self.gamer = User.objects.get(email="gamer@insert.io") self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first() self.room = Room.objects.create(name="Insert Room", owner=self.gamer) self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/" def _select_token_from_kit(self, token): self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f"[data-token-id='{token.id}']" ).click() ) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.ready") ) def test_coin_insert_via_kit_reserves_slot(self): self.browser.get(self.gate_url) self._select_token_from_kit(self.coin) self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved") ) self.assertEqual(self.browser.current_url, self.gate_url) def test_free_token_insert_via_kit_consumed_on_confirm(self): self.gamer.tokens.filter(token_type=Token.FREE).delete() token = Token.objects.create( user=self.gamer, token_type=Token.FREE, expires_at=timezone.now() + timedelta(days=7), ) self.browser.get(self.gate_url) self._select_token_from_kit(token) self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm" ) ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled") ) self.assertFalse(Token.objects.filter(id=token.id).exists()) def test_tithe_token_insert_via_kit_consumed_on_confirm(self): token = Token.objects.create(user=self.gamer, token_type=Token.TITHE) self.browser.get(self.gate_url) self._select_token_from_kit(token) self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm" ) ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled") ) self.assertFalse(Token.objects.filter(id=token.id).exists()) def test_pass_token_insert_via_kit_not_consumed(self): self.gamer.is_staff = True self.gamer.save() pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS) self.browser.get(self.gate_url) self._select_token_from_kit(pass_token) self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved") ) self.assertTrue(Token.objects.filter(id=pass_token.id).exists()) self.assertEqual(self.browser.current_url, self.gate_url)