Files
python-tdd/src/functional_tests/test_bill_my_sign.py

269 lines
12 KiB
Python
Raw Normal View History

Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD User scope (per design conv this session): split the room's sig-select responsibility off into a standalone billboard-context "My Significator" applet — branded "Game Sign" on the surface. Same 18-card pile as room sig-select (16 middle arcana + Major 0 & 1 filtered by Note unlocks); polarity collapses to a single FLIP choice (the FLIP btn in the picker carousel toggles User.significator_reversed). Selection persists globally on the User model + propagates to the billboard's Game Sign applet ; **naming convention locked**: "significator" stays at storage (User.significator FK + User.significator_reversed) + room sig-select context (DRY w. existing template/JS); "Sign" / "Game Sign" is the billboard-surface branding (file my_sign.html, URL /billboard/my-sign/, URL names my_sign + save_sign, applet name "Game Sign", page wordmark "Game Sign", btn label SAVE SIGN). Action URLs don't carry a trailing slash per project convention (/billboard/my-sign/save vs the page's /billboard/my-sign/) ; **schema**: User gains 2 fields — `significator: FK → epic.TarotCard (nullable, on_delete=SET_NULL)` + `significator_reversed: BooleanField(default=False)`. Migration lyric/0006_user_significator_user_significator_reversed.py auto-generated; reversible. Applet seed in applets/0009_seed_my_sig_applet.py adds the row (slug='my-sign', name='Game Sign', context='billboard', default_visible=True, grid_cols=4, grid_rows=6), idempotent update_or_create, reversible unseed() ; **picker page** (my_sign.html): solo lift of `_sig_select_overlay.html` — sig-stage-card scaffold + sig-stat-block + 18-card grid + SAVE SIGN form. Stripped: countdown / WebSocket / polarity / multi-user / reservations. Empty-state branch covers no-equipped-deck (link back to Game Kit; full Brief-redirect + Earthman-Backup fallback deferred to a follow-up sub-sprint). Minimal inline JS: click .sig-card → mark .sig-focused + set hidden card_id + enable SAVE SIGN; FLIP btn toggles .is-reversed + the hidden reversed input. Stage-card preview (name/qualifier population + keyword swap on FLIP) deferred — Sprint 4a follow-up will lift stage-card.js's populator into a non-room context ; **applet partial** (_applet-my-sign.html): renders user.significator's corner-rank + suit-icon + name_title if set; `.my-sign-applet-empty` "No sign chosen yet." otherwise. Header `<h2><a href="{% url 'billboard:my_sign' %}">Game Sign</a></h2>` links to the picker ; **helper refactor** (epic/models.py): extracted `_sig_unique_cards_for_deck(deck_variant)` from `_sig_unique_cards(room)`. New public `personal_sig_cards(user)` parallels `levity_sig_cards / gravity_sig_cards` but pulls from `user.equipped_deck` instead of `room.deck_variant`. Same Note-unlock filtering. No behavior change to existing room callers (3-line wrapper preserves the room signature) ; **TDD trail** — user called out mid-sprint that I'd skipped FTs; pivoted to FT-first. test_bill_my_sign.py (new, 3 FTs): T1 picker renders w. wordmark + target card present in grid; T2 click card → SAVE SIGN enables → POST persists → applet shows the card; T3 fresh user → applet renders empty-state. Initial reds — (a) setUp's `personal_sig_cards(user)` returned [] because StaticLiveServerTestCase → TransactionTestCase flushes migration-seeded DeckVariant + TarotCard between tests; fixed w. `serialized_rollback = True` on the test class (per [[feedback_transactiontestcase_flush]]); (b) h2 wordmark assertion against `MYSIGNIFICATOR` failed against the renamed "Game Sign" + the letter-splitter spreading chars across <span> children — switched to whitespace-stripped substring check `GAMESIGN`; (c) `.fan-corner-rank` text is CSS-hidden so Selenium returns "" — replaced corner-rank assertions w. data-card-id selectors (already-proven reliable from the parent .sig-card lookup) ; ITs (+12, in apps.billboard.tests.integrated.test_views): MySignViewTest (6 — login redirect, 200 + template, 16-card pile, save persists, invalid card_id → 403, GET save redirects); BillboardAppletMySignTest (3 — applet rendered, empty-state w/o sig, card+reversed class w. sig). PersonalSigCardsTest in apps.epic.tests.integrated.test_models (3 — happy path 16 cards, no-equipped-deck → [], schizo Note unlocks Major 1) ; pre-existing change picked up by the commit: my_sea.html branding "Game Sea" (user-modified mid-session; was "My Sea" in Sprint 3 — divergence captured in MEMORY.md follow-up) ; 1020 IT/UT green (+12) in 46s; 3 FTs green in 24s. Sprint 4a unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/) + Sprint 4c (FT helper for mocking the sig choice across other FTs) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:23:24 -04:00
"""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",
fix CARTE multi-seat Role-Select bug on navigate-away + back; My Sign applet rename **CARTE bug** (user-reported on iPhone): a CARTE gamer who contributed their deck to multiple gate slots could fill ≥1 role for ≥1 seat, navigate away (BYE → dashboard, CONT GAME → return, etc.), come back to the room — and the JS guard on .card-stack would wrongly fire "Equip card deck before Role select" + block further role picks, even though the deck was demonstrably in play on existing seats. Symmetric for the "stay in room during Role Select" variant the user thought we'd squashed before (the prior fix was 759ce8d for the multi-slot SELECT path, but the room VIEW context never got the same treatment) ; **root cause**: `select_role()` at epic/views.py:619-621 clears `user.equipped_deck` after the first role pick ("deck committed to room"). The room view's role-select context at epic/views.py:286 then passes `equipped_deck_id = user.equipped_deck_id` to the template — which is now None — and the template renders `data-equipped-deck=""` → JS guard at role-select.js:165 sees the empty string and fires the "no deck" warning. The deck IS in play; the context just isn't recognizing seat-level deck assignment as a deck source ; **fix** (epic/views.py:286ish): when `user.equipped_deck_id` is None, fall back to the deck_variant of any of the user's seats in this room (order_by slot_number for determinism). The guard now sees a non-empty id and the fan opens. Storage-side unchanged — seat.deck_variant remains the canonical "this deck is in play on this seat" signal, and the user's deck-third contribution per role (PC=levity brands+crowns / NC=levity trumps / SC=levity grails+blades / AC=gravity grails+blades / EC=gravity trumps / BC=gravity brands+crowns) flows from existing `select_role` logic that inherits deck_variant from the first seat ; **TDD trail** — 2 new ITs in `SelectRoleMultiSeatTest` (apps.epic.tests.integrated.test_views): T1 pins the context (`response.context["equipped_deck_id"]` equals the existing seat's deck_variant_id after `user.equipped_deck` clears); T2 pins the template (rendered `data-equipped-deck="<id>"` not `""`). Initial reds — `None != 2` + `data-equipped-deck=""` substring assertion. Fix lands both green ; **bundled: My Sign applet rename** — user clarified naming convention 2026-05-18: **applets** use the "My X" prefix (My Sign, My Sea, My Posts), **standalone pages** use the "Game/Dash/Bill X" prefix (Game Sign page, Game Sea page, Game Kit page). Sprint 4a's initial migration set the applet name to "Game Sign" — corrected after the user saw the gear-menu toggle list reading the wrong word. Applet template header link "Game Sign" → "My Sign" (user-edited); migration 0010 added to update the Applet row's `name` in already-migrated DBs (dev + staging); applets/0009 frontmatter + defaults updated to "My Sign" in case of a fresh migrate-from-zero; test seed helpers in billboard test_views.py + functional_tests/test_bill_my_sign.py updated to "My Sign". Slug stays `my-sign` (URL + selectors stable) ; **bundled: rootvars.scss** — user-modified mid-session (pre-staged) ; 1022 IT/UT green in 46s — no regressions; 4 ITs in SelectRoleMultiSeatTest green (2 pre-existing CARTE multi-seat ITs + 2 new return-trip context ITs) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:18:32 -04:00
defaults={"name": "My Sign", "context": "billboard",
Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD User scope (per design conv this session): split the room's sig-select responsibility off into a standalone billboard-context "My Significator" applet — branded "Game Sign" on the surface. Same 18-card pile as room sig-select (16 middle arcana + Major 0 & 1 filtered by Note unlocks); polarity collapses to a single FLIP choice (the FLIP btn in the picker carousel toggles User.significator_reversed). Selection persists globally on the User model + propagates to the billboard's Game Sign applet ; **naming convention locked**: "significator" stays at storage (User.significator FK + User.significator_reversed) + room sig-select context (DRY w. existing template/JS); "Sign" / "Game Sign" is the billboard-surface branding (file my_sign.html, URL /billboard/my-sign/, URL names my_sign + save_sign, applet name "Game Sign", page wordmark "Game Sign", btn label SAVE SIGN). Action URLs don't carry a trailing slash per project convention (/billboard/my-sign/save vs the page's /billboard/my-sign/) ; **schema**: User gains 2 fields — `significator: FK → epic.TarotCard (nullable, on_delete=SET_NULL)` + `significator_reversed: BooleanField(default=False)`. Migration lyric/0006_user_significator_user_significator_reversed.py auto-generated; reversible. Applet seed in applets/0009_seed_my_sig_applet.py adds the row (slug='my-sign', name='Game Sign', context='billboard', default_visible=True, grid_cols=4, grid_rows=6), idempotent update_or_create, reversible unseed() ; **picker page** (my_sign.html): solo lift of `_sig_select_overlay.html` — sig-stage-card scaffold + sig-stat-block + 18-card grid + SAVE SIGN form. Stripped: countdown / WebSocket / polarity / multi-user / reservations. Empty-state branch covers no-equipped-deck (link back to Game Kit; full Brief-redirect + Earthman-Backup fallback deferred to a follow-up sub-sprint). Minimal inline JS: click .sig-card → mark .sig-focused + set hidden card_id + enable SAVE SIGN; FLIP btn toggles .is-reversed + the hidden reversed input. Stage-card preview (name/qualifier population + keyword swap on FLIP) deferred — Sprint 4a follow-up will lift stage-card.js's populator into a non-room context ; **applet partial** (_applet-my-sign.html): renders user.significator's corner-rank + suit-icon + name_title if set; `.my-sign-applet-empty` "No sign chosen yet." otherwise. Header `<h2><a href="{% url 'billboard:my_sign' %}">Game Sign</a></h2>` links to the picker ; **helper refactor** (epic/models.py): extracted `_sig_unique_cards_for_deck(deck_variant)` from `_sig_unique_cards(room)`. New public `personal_sig_cards(user)` parallels `levity_sig_cards / gravity_sig_cards` but pulls from `user.equipped_deck` instead of `room.deck_variant`. Same Note-unlock filtering. No behavior change to existing room callers (3-line wrapper preserves the room signature) ; **TDD trail** — user called out mid-sprint that I'd skipped FTs; pivoted to FT-first. test_bill_my_sign.py (new, 3 FTs): T1 picker renders w. wordmark + target card present in grid; T2 click card → SAVE SIGN enables → POST persists → applet shows the card; T3 fresh user → applet renders empty-state. Initial reds — (a) setUp's `personal_sig_cards(user)` returned [] because StaticLiveServerTestCase → TransactionTestCase flushes migration-seeded DeckVariant + TarotCard between tests; fixed w. `serialized_rollback = True` on the test class (per [[feedback_transactiontestcase_flush]]); (b) h2 wordmark assertion against `MYSIGNIFICATOR` failed against the renamed "Game Sign" + the letter-splitter spreading chars across <span> children — switched to whitespace-stripped substring check `GAMESIGN`; (c) `.fan-corner-rank` text is CSS-hidden so Selenium returns "" — replaced corner-rank assertions w. data-card-id selectors (already-proven reliable from the parent .sig-card lookup) ; ITs (+12, in apps.billboard.tests.integrated.test_views): MySignViewTest (6 — login redirect, 200 + template, 16-card pile, save persists, invalid card_id → 403, GET save redirects); BillboardAppletMySignTest (3 — applet rendered, empty-state w/o sig, card+reversed class w. sig). PersonalSigCardsTest in apps.epic.tests.integrated.test_models (3 — happy path 16 cards, no-equipped-deck → [], schizo Note unlocks Major 1) ; pre-existing change picked up by the commit: my_sea.html branding "Game Sea" (user-modified mid-session; was "My Sea" in Sprint 3 — divergence captured in MEMORY.md follow-up) ; 1020 IT/UT green (+12) in 46s; 3 FTs green in 24s. Sprint 4a unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/) + Sprint 4c (FT helper for mocking the sig choice across other FTs) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:23:24 -04:00
"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 <span>, 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,
)
My Sign: Brief banner + Earthman [Shabby Cardstock] backup deck when no equipped — TDD User-reported gap on /billboard/my-sign/ — admin user's only deck was in-use as `TableSeat.deck_variant` in another room (Wonderbeard) → `equipped_deck` cleared → previous my-sign template showed "Equip a card deck first…" w. no actionable next step. User scoped fix: don't force equip, just nudge via a Brief banner "Look!—no deck is equipped. Navigate to the Game Kit to equip one (FYI) or (NVM) proceed with the Earthman [Shabby Cardstock] deck.", title "Default deck warning". NVM dismisses + picker proceeds against an Earthman card pile labeled in-copy as the temporary backup; FYI links to /gameboard/ (Game Kit equip). User-modified line text in template: "Paperboard" → "Cardstock" mid-session ; **helper fallback** (epic/models.py): `personal_sig_cards(user)` now falls back to `DeckVariant.objects.filter(slug='earthman').first()` when `user.equipped_deck` is None — same 16-or-18 card pile, just sourced from the canonical Earthman deck rather than the empty FK. No new DeckVariant row needed; "Shabby Cardstock" is purely UX framing (cards are the same TarotCard records the room sig-select uses). Preserves the existing helper signature so no callers had to change ; **view + template** (billboard/views.py + my_sign.html): view passes `no_equipped_deck` + `show_backup_intro_banner` flags. Template removes the old `{% if not equipped_deck %}` forced-equip branch — picker now renders unconditionally w. cards from the backup helper when no deck is equipped. Brief banner fires via `Brief.showBanner({...})` on DOMContentLoaded when `show_backup_intro_banner` is true — gets h2-overlay positioning + NVM behavior + portal styling for free (per [[sprint-baltimorean-note-unlock-may18]] portrait h2 measurement in note.js's `_alignToH2`). Added `<script src="note.js">` to my_sign.html since the page didn't load it before. Post-render JS tags the Brief w. a `.my-sign-intro-banner` class so FTs (and any future my-sign-specific styling) can distinguish this nudge from other Briefs on the page ; **TDD trail** — 4 new FTs in `MySignBackupDeckTest` (test_bill_my_sign.py): T1 banner renders w. "Default deck warning" title + "no deck is equipped" + "Shabby Cardstock" copy + both action btns visible; T2 picker still populates 16 cards from backup; T3 NVM click removes the banner from the DOM; T4 FYI href ends w. /gameboard/. Initial reds (`NoSuchElementException` on all 4) confirmed before implementation. Plus 1 new IT in `PersonalSigCardsTest` pinning the helper fallback (16 cards w. all `c.deck_variant.slug == "earthman"`) ; pre-existing change picked up: `static_src/scss/rootvars.scss` (user-modified mid-session) ; 1020 IT/UT green; 7 FTs green (3 picker happy-path + 4 backup deck) in 56s. Sprint 4a-follow complete — primary deferral from Sprint 4a (deck-source fallback UX) now landed. Unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/ when no sig set) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:49:49 -04:00
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 <a href> 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}",
)