"""FTs for the Game Sign (a.k.a. My Significator) picker + billboard applet. Sprint 4a of [[project-my-sea-roadmap]]. The picker lives at `/billboard/my-sign/` — solo lift of the room's sig-select grid (no countdown / polarity / multi-user). Selection persists on User.significator + User.significator_reversed. "Significator" remains the storage-layer term + room sig-select context; this billboard surface is branded "Sign" / "Game Sign". """ from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet from apps.epic.models import personal_sig_cards from apps.lyric.models import User def _seed_my_sign_applet(): Applet.objects.get_or_create( slug="my-sign", defaults={"name": "Game Sign", "context": "billboard", "default_visible": True, "grid_cols": 4, "grid_rows": 6}, ) class MySignPickerTest(FunctionalTest): """Happy-path picker: a user with the Earthman deck equipped lands at /billboard/my-sign/, picks a card, clicks SAVE SIGN, and sees the sig propagate to the Game Sign applet on /billboard/.""" # StaticLiveServerTestCase → TransactionTestCase flushes DB between tests, # wiping migration-seeded DeckVariant + TarotCard rows. Without this flag, # personal_sig_cards(user) returns [] because the signal that auto-equips # Earthman can't find the deck. See [[feedback_transactiontestcase_flush]]. serialized_rollback = True def setUp(self): super().setUp() _seed_my_sign_applet() # Seed the rest of the billboard applets so /billboard/ renders # without missing-applet errors. for slug, name in [ ("my-scrolls", "My Scrolls"), ("my-buds", "My Buds"), ("most-recent-scroll", "Most Recent Scroll"), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "context": "billboard"}, ) self.email = "sig@test.io" self.gamer = User.objects.create(email=self.email) # post_save signal auto-equips Earthman. Picker uses personal_sig_cards # (= 16 middle arcana + Major 0 & 1, filtered by Note unlocks) so the # target must come from that subset, not the full deck. sig_pile = personal_sig_cards(self.gamer) self.target_card = sig_pile[0] if sig_pile else None self.assertIsNotNone( self.target_card, "personal_sig_cards(user) returned no cards — check Earthman seed" " + DeckVariant fixture availability in the FT DB.", ) # ── Test 1 ─────────────────────────────────────────────────────────────── def test_picker_renders_card_grid_from_equipped_deck(self): """GET /billboard/my-sign/ → page renders w. a card grid + the page wordmark reads "Game Sign", populated by the user's equipped_deck.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") # Wordmark. The h2 letter-splitter (base.html) wraps each character # in its own , so Selenium's `.text` joins them w. newlines — # strip whitespace before the substring check. self.wait_for( lambda: self.assertIn( "GAMESIGN", "".join( self.browser.find_element(By.CSS_SELECTOR, "h2").text.upper().split() ), ) ) # Target card present in the grid. The data-card-id selector itself # is the assertion — find_element raises if absent. Avoid asserting # against `.fan-corner-rank .text` (CSS hides it via font-size or # similar, so Selenium returns ""). self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', ) ) # ── Test 2 ─────────────────────────────────────────────────────────────── def test_pick_card_then_save_persists_choice_and_shows_in_applet(self): """Click card → SAVE SIGN btn enables → click → DB updated → applet on /billboard/ shows the chosen card.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") # Click target card card_el = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', ) ) self.browser.execute_script("arguments[0].click()", card_el) # SAVE SIGN should be enabled now save_btn = self.wait_for( lambda: self.browser.find_element(By.ID, "id_save_sign_btn") ) self.wait_for( lambda: self.assertFalse( save_btn.get_attribute("disabled"), "SAVE SIGN should be enabled after card click", ) ) # Hidden card_id input should match the clicked card self.assertEqual( str(self.target_card.id), self.browser.find_element(By.ID, "id_save_sign_card_id").get_attribute("value"), ) # Save → DB updated self.browser.execute_script("arguments[0].click()", save_btn) self.wait_for( lambda: self.gamer.refresh_from_db() or self.assertEqual( self.gamer.significator_id, self.target_card.id, ) ) # Navigate to /billboard/ → applet shows the saved card. Pin by # data-card-id (same reasoning as test 1 re: CSS-hidden corner rank). self.browser.get(self.live_server_url + "/billboard/") self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, f'#id_applet_my_sign .my-sign-applet-card[data-card-id="{self.target_card.id}"]', ) ) # ── Test 3 ─────────────────────────────────────────────────────────────── def test_applet_renders_blank_state_when_no_sig_chosen(self): """Fresh user with no significator → applet shows the empty-state copy, no card.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/") empty = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, "#id_applet_my_sign .my-sign-applet-empty" ) ) self.assertIn("No sign chosen", empty.text) self.assertEqual( len(self.browser.find_elements( By.CSS_SELECTOR, "#id_applet_my_sign .my-sign-applet-card" )), 0, ) class MySignBackupDeckTest(FunctionalTest): """User with no equipped_deck + no saved sig should still be able to pick. Sprint 4a-follow: no-equipped-deck path. Page renders a Brief banner nudging "Look!—no deck is equipped. Navigate to the game kit to equip one (FYI) or (NVM) proceed with Earthman [Shabby Paperboard] deck." — NVM dismisses + picker stays usable w. backup deck cards; FYI links to the gameboard (Game Kit applet).""" serialized_rollback = True def setUp(self): super().setUp() for slug, name in [ ("my-sign", "Game Sign"), ("my-scrolls", "My Scrolls"), ("my-buds", "My Buds"), ("most-recent-scroll", "Most Recent Scroll"), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "context": "billboard"}, ) self.email = "dekless@test.io" self.gamer = User.objects.create(email=self.email) # Simulate "deck-in-use elsewhere" — clear equipped_deck (the # symmetry of the room flow that hands the deck off to a TableSeat). self.gamer.equipped_deck = None self.gamer.save(update_fields=["equipped_deck"]) # ── Test 1 ─────────────────────────────────────────────────────────────── def test_brief_banner_renders_when_no_deck_equipped_and_no_sig(self): """Banner should appear via Brief.showBanner() w. title "Default deck warning" + the Shabby Cardstock copy + FYI + NVM buttons.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") banner = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".my-sign-intro-banner" ) ) self.assertIn("Default deck warning", banner.text) self.assertIn("no deck is equipped", banner.text) self.assertIn("Shabby Cardstock", banner.text) # Brief banner action buttons: .note-banner__nvm + .note-banner__fyi self.assertTrue( banner.find_element(By.CSS_SELECTOR, ".note-banner__fyi").is_displayed() ) self.assertTrue( banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").is_displayed() ) # ── Test 2 ─────────────────────────────────────────────────────────────── def test_picker_renders_18_card_pile_using_backup_deck(self): """No equipped deck → personal_sig_cards falls back to Earthman, so the picker grid still has the full sig pile (16 cards w. no Note unlocks).""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") # Grid present + populated (find_elements returns N cards) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid") ) cards = self.browser.find_elements( By.CSS_SELECTOR, ".my-sign-deck-grid .sig-card" ) self.assertEqual(len(cards), 16) # ── Test 3 ─────────────────────────────────────────────────────────────── def test_nvm_dismisses_banner_and_keeps_picker_usable(self): """NVM click hides the banner; the grid remains interactive (can click a sig-card + SAVE SIGN persists against the backup deck).""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") banner = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-intro-banner") ) banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").click() self.wait_for( lambda: self.assertEqual( len(self.browser.find_elements( By.CSS_SELECTOR, ".my-sign-intro-banner" )), 0, ) ) # ── Test 4 ─────────────────────────────────────────────────────────────── def test_fyi_links_to_gameboard(self): """FYI button is `.note-banner__fyi` rendered as by Brief.showBanner pointing at the Brief's `post_url` — here /gameboard/ so the user can equip a deck via Game Kit.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") fyi = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, ".my-sign-intro-banner .note-banner__fyi" ) ) href = fyi.get_attribute("href") or "" self.assertTrue( href.endswith("/gameboard/"), f"FYI should link to /gameboard/, got {href!r}", )