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>
This commit is contained in:
Disco DeDisco
2026-05-18 22:49:49 -04:00
parent 400762c0e5
commit 66b2947e8c
6 changed files with 169 additions and 23 deletions

View File

@@ -160,3 +160,109 @@ class MySignPickerTest(FunctionalTest):
)),
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 <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}",
)