"""FT for the gatekeeper invite via #id_bud_btn slide-out. Replaces the legacy inline `
` panel inside the gatekeeper modal. The bud-btn lives at the upper-right corner of the right sidebar (footer in landscape); slide-out hosts the email/ username field + OK btn. Submit fires async POST to epic:invite_gamer w. Accept: application/json — server returns {brief, recipient_display}, JS shows the slide-down Brief banner. """ from selenium.webdriver.common.by import By from apps.billboard.models import Brief from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat from apps.lyric.models import User from .base import FunctionalTest class GatekeeperBudBtnPresenceTest(FunctionalTest): """The bud-btn renders for the room owner during gate phase, and is absent for non-owners (friend invites are owner-only).""" def setUp(self): super().setUp() self.owner = User.objects.create(email="owner@test.io", username="owner") self.gamer = User.objects.create(email="gamer@test.io", username="gamer") self.room = Room.objects.create(name="Bingobango", owner=self.owner) self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/" def test_bud_btn_renders_for_owner(self): self.create_pre_authenticated_session("owner@test.io") self.browser.get(self.room_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn")) def test_bud_btn_absent_for_non_owner(self): # A registered non-owner viewer doesn't see the invite affordance. self.create_pre_authenticated_session("gamer@test.io") self.browser.get(self.room_url) # Gatekeeper-specific element confirms page rendered self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal")) self.assertFalse(self.browser.find_elements(By.ID, "id_bud_btn")) def test_legacy_invite_email_input_is_gone(self): """Sanity: the old inline form has been removed.""" self.create_pre_authenticated_session("owner@test.io") self.browser.get(self.room_url) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal")) self.assertFalse(self.browser.find_elements(By.ID, "id_invite_email")) class GatekeeperBudBtnAsyncInviteTest(FunctionalTest): """OK on the bud-btn slide-out fires the async invite — RoomInvite persisted, Brief w/ kind=GAME_INVITE created, slide-down banner shown.""" def setUp(self): super().setUp() self.owner = User.objects.create(email="owner@test.io", username="owner") self.alice = User.objects.create(email="alice@test.io", username="alice") self.room = Room.objects.create(name="Bingobango", owner=self.owner) self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/" self.create_pre_authenticated_session("owner@test.io") self.browser.get(self.room_url) def _open_panel_and_invite(self, recipient): bud_btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn")) bud_btn.click() recipient_input = self.wait_for( lambda: self.browser.find_element(By.ID, "id_recipient") ) recipient_input.send_keys(recipient) self.browser.find_element( By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm" ).click() return bud_btn def test_invite_creates_room_invite(self): self._open_panel_and_invite("alice@test.io") self.wait_for(lambda: self.assertEqual( RoomInvite.objects.filter( room=self.room, invitee_email="alice@test.io" ).count(), 1, )) def test_invite_spawns_game_invite_brief(self): self._open_panel_and_invite("alice@test.io") self.wait_for(lambda: self.assertEqual( Brief.objects.filter( owner=self.owner, kind=Brief.KIND_GAME_INVITE, ).count(), 1, )) def test_invite_renders_slide_down_banner(self): self._open_panel_and_invite("alice@test.io") self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")) def test_invite_closes_panel_after_success(self): bud_btn = self._open_panel_and_invite("alice@test.io") self.wait_for(lambda: self.assertNotIn("active", bud_btn.get_attribute("class"))) def test_invite_username_resolves_to_user_email(self): """Username-typed invite stores the resolved User's email.""" self._open_panel_and_invite("alice") self.wait_for(lambda: self.assertEqual( RoomInvite.objects.filter( room=self.room, invitee_email="alice@test.io" ).count(), 1, )) def test_invite_auto_adds_recipient_to_owner_buds(self): self._open_panel_and_invite("alice@test.io") self.wait_for(lambda: self.assertIn( self.alice, list(self.owner.buds.all()) )) class GatekeeperBudBtnDuplicateInviteErrorTest(FunctionalTest): """Re-inviting a recipient already seated in the room triggers the error Brief titled `@ is already present`. FYI on the Brief dismisses + adds .bud-duplicate-flash to the existing .gate-slot.filled[data-user-id=…] element. Pending-but-unseated duplicates also surface the Brief but FYI has no slot to highlight.""" def setUp(self): super().setUp() self.owner = User.objects.create(email="owner@test.io", username="owner") self.alice = User.objects.create(email="alice@test.io", username="alice") self.room = Room.objects.create(name="Dup Room", owner=self.owner) # Seat alice via a GateSlot — _gate_positions renders .gate-slot.filled # cells from GateSlot records (TableSeat spins up later at SIG SELECT), # so the duplicate-highlight target lives there during gatekeeper phase. GateSlot.objects.create( room=self.room, gamer=self.alice, slot_number=1, status=GateSlot.FILLED, ) self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/" self.create_pre_authenticated_session("owner@test.io") self.browser.get(self.room_url) def test_duplicate_invite_shows_error_brief_and_fyi_flashes_slot(self): btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn")) btn.click() recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient")) recipient.send_keys("alice@test.io") self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm").click() title = self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".note-banner--duplicate .note-banner__title" )) self.assertEqual(title.text, "@alice is already present") # No new RoomInvite or Brief persisted server-side on duplicate self.assertFalse(RoomInvite.objects.filter( room=self.room, invitee_email="alice@test.io", ).exists()) self.assertEqual( Brief.objects.filter(owner=self.owner, kind=Brief.KIND_GAME_INVITE).count(), 0, ) slot = self.browser.find_element( By.CSS_SELECTOR, f".gate-slot.filled[data-user-id='{self.alice.id}']" ) self.assertNotIn("bud-duplicate-flash", slot.get_attribute("class") or "") self.browser.find_element( By.CSS_SELECTOR, ".note-banner--duplicate .note-banner__fyi" ).click() self.wait_for(lambda: self.assertEqual( self.browser.find_elements(By.CSS_SELECTOR, ".note-banner--duplicate"), [], )) self.wait_for(lambda: self.assertIn( "bud-duplicate-flash", slot.get_attribute("class") or "" ))