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,
|
|
|
|
|
)
|
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}",
|
|
|
|
|
)
|