from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet from apps.lyric.models import Token, User class CarteBlancheTest(FunctionalTest): def setUp(self): super().setUp() for slug, name, cols, rows in [ ("new-game", "New Game", 6, 3), ("my-games", "My Games", 6, 3), ("game-kit", "Game Kit", 6, 3), ]: Applet.objects.get_or_create( slug=slug, defaults={ "name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard", }, ) # is_staff triggers COIN + FREE + PASS via post_save signal; PASS auto-equipped self.gamer = User.objects.create(email="blanche@test.io", is_staff=True) Token.objects.create(user=self.gamer, token_type=Token.TITHE) self.carte = Token.objects.create(user=self.gamer, token_type=Token.CARTE) # ── Test 1 ─────────────────────────────────────────────────────────────── def test_equipped_pass_shows_mini_tooltip_in_game_kit(self): # 1. Log in, land on dashboard self.create_pre_authenticated_session("blanche@test.io") self.browser.get(self.live_server_url) # 2. Open kit bag — Backstage Pass visible in Trinkets section self.wait_for(lambda: 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-type="{Token.PASS}"]', ) ) # 3. Navigate to gameboard (use get() — kit bag dialog still open and # would intercept a click on the footer nav link in headless Firefox) self.browser.get(self.live_server_url + "/gameboard/") self.wait_for( lambda: self.assertRegex(self.browser.current_url, r"/gameboard/$") ) # 4. Find Backstage Pass in the Game Kit applet self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) pass_el = self.browser.find_element(By.ID, "id_kit_pass") self.browser.execute_script( "arguments[0].scrollIntoView({block: 'center'})", pass_el ) # 5. Hover over Pass — main tooltip appears via portal ActionChains(self.browser).move_to_element(pass_el).perform() self.wait_for( lambda: self.assertTrue( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) ) portal = self.browser.find_element(By.ID, "id_tooltip_portal") self.assertIn("Backstage Pass", portal.text) self.assertIn("Admit All Entry", portal.text) # 6. A mini tooltip appears below the main tooltip, flush with its right edge. # Since Pass is the equipped trinket, it says "Equipped." mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) self.assertIn("Equipped", mini.text) portal_rect = self.browser.execute_script( "return arguments[0].getBoundingClientRect()", portal ) mini_rect = self.browser.execute_script( "return arguments[0].getBoundingClientRect()", mini ) self.assertGreater(mini_rect["top"], portal_rect["bottom"] - 5) # below main self.assertAlmostEqual(mini_rect["right"], portal_rect["right"], delta=10) # flush right # ── Test 2 ─────────────────────────────────────────────────────────────── def test_carte_blanche_equip_and_multi_slot_gatekeeper(self): # 1. Log in, navigate directly to gameboard self.create_pre_authenticated_session("blanche@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) # 2. Hover over Free Token — no mini tooltip (not a trinket, no data-token-id) el = self.browser.find_element(By.ID, "id_kit_free_token") ActionChains(self.browser).move_to_element(el).perform() self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) self.assertFalse( self.browser.find_element(By.ID, "id_mini_tooltip_portal").is_displayed() ) # Coin-on-a-String IS equippable (has data-token-id) — mini tooltip shows coin_el = self.browser.find_element(By.ID, "id_kit_coin_on_a_string") ActionChains(self.browser).move_to_element(coin_el).perform() self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) self.wait_for( lambda: self.assertTrue( self.browser.find_element(By.ID, "id_mini_tooltip_portal").is_displayed() ) ) # 3. Hover Carte Blanche — main tooltip present; mini shows "Not Equipped"; DON active carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") self.browser.execute_script( "arguments[0].scrollIntoView({block: 'center'})", carte_el ) ActionChains(self.browser).move_to_element(carte_el).perform() self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) portal = self.browser.find_element(By.ID, "id_tooltip_portal") self.assertIn("Carte Blanche", portal.text) self.assertIn("Admit up to +6", portal.text) self.assertIn("taking over from here", portal.text) self.assertIn("no expiry", portal.text) mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) self.assertIn("Not Equipped", mini.text) don = portal.find_element(By.CSS_SELECTOR, ".btn-equip") self.assertNotIn("btn-disabled", don.get_attribute("class")) # 4. Click DON — DON becomes disabled; data-equipped-id set optimistically don.click() self.wait_for( lambda: self.assertIn( "btn-disabled", portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), ) ) # 5. JS-side optimistic update: data-equipped-id reflects Carte immediately game_kit = self.browser.find_element(By.ID, "id_game_kit") self.wait_for( lambda: self.assertEqual( game_kit.get_attribute("data-equipped-id"), str(self.carte.pk), ) ) # 6. Hover Backstage Pass — mini shows "Not Equipped" (Pass no longer equipped) pass_el = self.browser.find_element(By.ID, "id_kit_pass") ActionChains(self.browser).move_to_element(pass_el).perform() self.wait_for( lambda: self.assertIn( "Backstage Pass", self.browser.find_element(By.ID, "id_tooltip_portal").text, ) ) mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) self.assertIn("Not Equipped", mini.text) # ── GATEKEEPER PHASE ───────────────────────────────────────────────── # 8. Create a new game room via the New Game applet self.browser.find_element(By.ID, "id_new_game_name").send_keys("The Long Room") self.browser.find_element(By.ID, "id_create_game_btn").click() self.wait_for( lambda: self.assertIn("/gameboard/room/", self.browser.current_url) ) self.wait_for( lambda: self.assertIn("/gate/", self.browser.current_url) ) gate_url = self.browser.current_url self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay") ) def open_kit_and_select_carte(): 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-type="{Token.CARTE}"]', ) ) self.browser.find_element( By.CSS_SELECTOR, f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', ).click() def deposit_carte(): self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") ) # 9. Open kit bag, select Carte Blanche, deposit via rails btn open_kit_and_select_carte() deposit_carte() # Helper: always fetches a fresh gate-slot element (avoid stale refs after redirects) def get_circle(i): return self.browser.find_element( By.CSS_SELECTOR, f".gate-slot[data-slot='{i + 1}']" ) # 10. Return panel glows; circle 1 gains OK btn → click it → fills return_panel = self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") self.assertTrue(return_panel.is_displayed()) self.wait_for( lambda: get_circle(0).find_element(By.CSS_SELECTOR, ".drop-token-btn") ).click() self.wait_for( lambda: self.assertIn("filled", get_circle(0).get_attribute("class")) ) # 11. Return panel still glows; circle 2 now has OK btn self.assertTrue( self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed").is_displayed() ) self.wait_for(lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".drop-token-btn")) # 12. Kit bag trinket section is now empty — Carte Blanche unequipped on deposit 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" ) ) self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.assertFalse( self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed() ) ) # 13. Cold feet: click return panel → Carte returned, return panel gone self.browser.find_element(By.CSS_SELECTOR, ".token-return-btn").click() self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".token-slot.claimed")), 0 ) ) # Carte reappears in Game Kit with DON available — not re-equipped automatically. # Game Kit is only in the /gameboard/ context, not the gatekeeper page. self.browser.get(self.live_server_url + "/gameboard/") carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") self.browser.execute_script( "arguments[0].scrollIntoView({block:'center'})", carte_el ) ActionChains(self.browser).move_to_element(carte_el).perform() portal = self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal") ) self.wait_for(lambda: self.assertTrue(portal.is_displayed())) don = self.wait_for( lambda: portal.find_element(By.CSS_SELECTOR, ".btn-equip") ) self.assertNotIn("btn-disabled", don.get_attribute("class")) # ── COLD FEET RESOLVED: full six-slot run ──────────────────────────── # 14. Re-equip Carte via portal DON, navigate back to gate don.click() self.wait_for( lambda: self.assertIn( "btn-disabled", portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), ) ) self.browser.get(gate_url) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")) open_kit_and_select_carte() deposit_carte() # 15. Click OK on circle 1 → fills; circle 1 loses its OK btn; circle 2 gains one self.wait_for( lambda: get_circle(0).find_element(By.CSS_SELECTOR, ".drop-token-btn") ).click() self.wait_for( lambda: self.assertIn("filled", get_circle(0).get_attribute("class")) ) self.wait_for( lambda: self.assertEqual( len(get_circle(0).find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0 ) ) self.wait_for(lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".drop-token-btn")) # 16. Click OK on circle 2 → turns to NVM; circle 3 gains OK self.wait_for( lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".drop-token-btn") ).click() self.wait_for( lambda: self.assertEqual( get_circle(1).find_element(By.CSS_SELECTOR, ".slot-release-btn").text.upper(), "NVM", ) ) self.wait_for(lambda: get_circle(2).find_element(By.CSS_SELECTOR, ".drop-token-btn")) # 17. Click NVM on circle 2 → circle 2 regains OK; circle 3 loses it (strict n+1) # Circle 1 still filled; return panel still glowing; lease name still present self.wait_for( lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".slot-release-btn") ).click() self.wait_for(lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".drop-token-btn")) self.assertIn("filled", get_circle(0).get_attribute("class")) self.assertTrue( self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed").is_displayed() ) self.assertEqual( len(get_circle(2).find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0 ) # 18. Fill circles 2 → 6 sequentially; verify each subsequent OK appears for i in range(1, 6): self.wait_for( lambda i=i: get_circle(i).find_element(By.CSS_SELECTOR, ".drop-token-btn") ).click() self.wait_for( lambda i=i: self.assertIn("filled", get_circle(i).get_attribute("class")) ) if i < 5: self.wait_for( lambda i=i: get_circle(i + 1).find_element( By.CSS_SELECTOR, ".drop-token-btn" ) ) # 19. All six circles filled — launch btn appears self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn") ) def test_carte_in_use_game_kit_shows_room_attribution(self): """While Carte Blanche is deposited in a room, its Game Kit token carries the room name on data-current-room-name so the mini-portal can render 'In-Use: ' on hover.""" self.create_pre_authenticated_session("blanche@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) # DON the Carte Blanche via tooltip portal carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") self.browser.execute_script( "arguments[0].scrollIntoView({block:'center'})", carte_el ) ActionChains(self.browser).move_to_element(carte_el).perform() portal = self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal") ) self.wait_for(lambda: self.assertTrue(portal.is_displayed())) don = self.wait_for(lambda: portal.find_element(By.CSS_SELECTOR, ".btn-equip")) don.click() self.wait_for( lambda: self.assertIn( "btn-disabled", portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), ) ) # Create a room and deposit the Carte Blanche self.browser.find_element(By.ID, "id_new_game_name").send_keys("Commitment 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.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', ) ).click() self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") ) # Game Kit panel is on /gameboard/, not the gate page — navigate back to check token self.browser.get(self.live_server_url + "/gameboard/") carte_el = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_carte_blanche") ) self.assertEqual( "Commitment Room", carte_el.get_attribute("data-current-room-name"), )