from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from .base import FunctionalTest from apps.applets.models import Applet from apps.drama.models import GameEvent, record from apps.epic.models import DeckVariant, Room from apps.lyric.models import Token, User class GameKitTest(FunctionalTest): """Game Kit : opens from footer, shows token cards, dismisses.""" def setUp(self): super().setUp() self.earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) self.create_pre_authenticated_session("gamer@kit.io") self.gamer = User.objects.get(email="gamer@kit.io") self.gamer.equipped_deck = self.earthman self.gamer.save(update_fields=["equipped_deck"]) self.gamer.unlocked_decks.add(self.earthman) self.token = self.gamer.tokens.filter(token_type=Token.COIN).first() self.room = Room.objects.create(name="Kit Room", owner=self.gamer) self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/" def test_kit_btn_in_footer_opens_dialog(self): self.browser.get(self.gate_url) kit_btn = self.wait_for( lambda: self.browser.find_element(By.ID, "id_kit_btn") ) self.assertTrue(kit_btn.is_displayed()) kit_btn.click() self.wait_for( lambda: self.assertTrue( self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed() ) ) def test_kit_dialog_shows_token_cards(self): self.browser.get(self.gate_url) self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f"#id_kit_bag_dialog [data-token-id='{self.token.id}']", ) ) def test_kit_dialog_closes_on_escape(self): self.browser.get(self.gate_url) self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.assertTrue( self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed() ) ) dialog = self.browser.find_element(By.ID, "id_kit_bag_dialog") dialog.send_keys(Keys.ESCAPE) self.wait_for( lambda: self.assertFalse( self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed() ) ) def test_kit_btn_visible_outside_room(self): self.browser.get(self.live_server_url + "/") kit_btn = self.wait_for( lambda: self.browser.find_element(By.ID, "id_kit_btn") ) self.assertTrue(kit_btn.is_displayed()) def test_kit_dialog_shows_equipped_deck(self): """New user auto-gets Earthman equipped; kit bar shows its deck card.""" self.browser.get(self.gate_url) self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f"#id_kit_bag_dialog .kit-bag-deck[data-deck-id='{self.gamer.equipped_deck.pk}']", ) ) def test_kit_dialog_always_shows_dice_placeholder(self): self.browser.get(self.gate_url) self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder", ) ) def test_kit_dialog_deck_tooltip_shows_name_count_availability_and_stock_version(self): self.browser.get(self.gate_url) self.browser.find_element(By.ID, "id_kit_btn").click() deck_el = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck" ) ) # 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 .tt" ) self.wait_for(lambda: self.assertTrue(tooltip.is_displayed())) text = tooltip.text self.assertIn("Earthman", text) self.assertIn("(Default)", text) self.assertIn("108", text) self.assertIn("Stock version", text) class PronounsAppletFlowTest(FunctionalTest): """End-to-end profile-wide pronoun flip: The same provenance prose ("X embodies as {poss} Significator …") rendered on both the per-room scroll AND the billboard's Most Recent Scroll applet should re-render when the user flips their pronouns ideology via the Pronouns applet on the Game Kit page. The test starts on a billscroll seeing "their" cognates (default pluralism), navigates to Game Kit, clicks the bawlmorese card → guard portal → OK, then verifies the Most Recent Scroll on the billboard AND the room's billscroll both render "yos" cognates. """ def setUp(self): super().setUp() # Billboard applets — page renders blank without these for slug, name, cols, rows in [ ("my-scrolls", "My Scrolls", 4, 3), ("my-buds", "My Buds", 4, 3), ("most-recent-scroll", "Most Recent Scroll", 8, 6), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"}, ) # Game Kit applets — including the new Pronouns applet for slug, name in [ ("gk-trinkets", "Trinkets"), ("gk-tokens", "Tokens"), ("gk-decks", "Card Decks"), ("gk-dice", "Dice Sets"), ("pronouns", "Pronouns"), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "grid_cols": 3, "grid_rows": 3, "context": "game-kit"}, ) self.create_pre_authenticated_session("disco@pronouns.io") self.user = User.objects.get(email="disco@pronouns.io") self.room = Room.objects.create(name="Pronoun Chamber", owner=self.user) # Seed a SIG_READY event so both scrolls have prose to render record( self.room, GameEvent.SIG_READY, actor=self.user, card_name="Maid of Brands", corner_rank="M", suit_icon="fa-wand-sparkles", ) self.scroll_url = self.live_server_url + f"/billboard/room/{self.room.id}/scroll/" self.billboard_url = self.live_server_url + "/billboard/" self.game_kit_url = self.live_server_url + "/gameboard/game-kit/" def _scroll_text(self): return self.browser.find_element(By.ID, "id_drama_scroll").text def _most_recent_text(self): return self.browser.find_element(By.ID, "id_applet_most_recent_scroll").text def test_pronoun_flip_propagates_to_billscroll_and_most_recent(self): # 1. Start on the room's billscroll — default pluralism reads "their". self.browser.get(self.scroll_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_drama_scroll")) self.assertIn("embodies as their Significator", self._scroll_text()) # 2. Navigate to the Game Kit page and find the bawlmorese card. self.browser.get(self.game_kit_url) bawlmorese_card = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".gk-pronoun-card[data-pronoun='bawlmorese']" ) ) # 3. Click the card → guard portal becomes active w. the slash-trio # preview ("yo/yo/yos") above OK|NVM. self.browser.execute_script("arguments[0].click()", bawlmorese_card) self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_guard_portal.active" ) ) portal = self.browser.find_element(By.ID, "id_guard_portal") self.assertIn("Set pronoun preference?", portal.text) self.assertIn("yo/yo/yos", portal.text) # 4. Click OK — the page reloads, .active class moves to bawlmorese. portal.find_element(By.CSS_SELECTOR, ".guard-yes").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".gk-pronoun-card.active[data-pronoun='bawlmorese']" ) ) # 5. Navigate to the Billboard — Most Recent Scroll applet must now # read "yos" cognates, not "their". self.browser.get(self.billboard_url) self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_most_recent_scroll") ) self.assertIn("embodies as yos Significator", self._most_recent_text()) self.assertNotIn("embodies as their Significator", self._most_recent_text()) # 6. And back on the per-room billscroll — same prose, same flip. self.browser.get(self.scroll_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_drama_scroll")) self.assertIn("embodies as yos Significator", self._scroll_text()) self.assertNotIn("embodies as their Significator", self._scroll_text())