Files
python-tdd/src/functional_tests/test_bill_my_sign.py
Disco DeDisco f6093136f1 fix: shop tooltip price flex-pinned right (cross-file #id_tooltip_portal .tt-title { display: block } was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.

(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.

(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.

(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.

(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.

(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.

**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00

536 lines
24 KiB
Python

"""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 .sig_page import _assign_sig, _seed_earthman_sig_pile
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": "My 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/."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
_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_landing_renders_dry_hex_with_scan_sign_button(self):
"""GET /billboard/my-sign/ → the DRY table hex (1-chair) renders w.
a central .btn-primary reading "SCAN SIGN"; the card grid is hidden
until SCAN SIGN is clicked."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
self.wait_for(
lambda: self.assertIn(
"GAMESIGN",
"".join(
self.browser.find_element(By.CSS_SELECTOR, "h2").text.upper().split()
),
)
)
# Landing phase markers: hex + 1 chair + SCAN SIGN btn
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-page .table-hex"
)
)
chairs = self.browser.find_elements(
By.CSS_SELECTOR, ".my-sign-page .table-seat"
)
self.assertEqual(len(chairs), 1, "Landing hex should render exactly 1 chair")
scan_btn = self.browser.find_element(By.ID, "id_scan_sign_btn")
self.assertIn("SCAN", scan_btn.text.upper())
self.assertIn("SIGN", scan_btn.text.upper())
# Picker grid not yet visible — landing phase only
grid = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid")
self.assertFalse(
grid.is_displayed(),
"Picker grid should be hidden on landing phase",
)
# ── Test 2 ───────────────────────────────────────────────────────────────
def test_scan_sign_click_transitions_to_picker_phase(self):
"""Click SCAN SIGN → landing hex hides; picker grid + populated stage
frame appear. Target card present in the grid."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
scan_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_scan_sign_btn")
)
self.browser.execute_script("arguments[0].click()", scan_btn)
# Phase swap — picker now visible, hex now hidden
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-deck-grid"
).is_displayed(),
"Picker grid should be visible after SCAN SIGN click",
)
)
# Hex collapses (display:none) after transition
self.assertFalse(
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-page .table-hex"
).is_displayed(),
"Landing hex should hide once picker phase is active",
)
# Target card present in the grid
self.browser.find_element(
By.CSS_SELECTOR,
f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
)
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_click_thumbnail_shows_OK_btn_without_locking(self):
"""In picker phase: click a thumbnail → .sig-focused class + OK btn
visible on it; stat block + FLIP still hidden (no lock yet); SAVE
SIGN still disabled."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
self.browser.execute_script(
"document.getElementById('id_scan_sign_btn').click()"
)
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)
# .sig-focused class → OK btn visible
self.wait_for(
lambda: self.assertIn(
"sig-focused",
self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"]',
).get_attribute("class"),
)
)
ok_btn = self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
)
self.assertTrue(ok_btn.is_displayed(), "OK btn should be visible on focused thumb")
# Stat block still hidden (no lock yet)
stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage")
self.assertNotIn("sig-stage--frozen", stage.get_attribute("class"))
# SAVE SIGN still disabled
save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
self.assertTrue(save_btn.get_attribute("disabled"))
# ── Test 4 ───────────────────────────────────────────────────────────────
def test_OK_click_locks_thumbnail_and_enables_save_sign(self):
"""In picker phase: click thumbnail → click its OK btn → stage locks
(.sig-stage--frozen), NVM btn visible on the thumbnail, SAVE SIGN
enables. Then save persists + applet shows the card."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
self.browser.execute_script(
"document.getElementById('id_scan_sign_btn').click()"
)
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)
ok_btn = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
)
)
self.browser.execute_script("arguments[0].click()", ok_btn)
# Lock applied
self.wait_for(
lambda: self.assertIn(
"sig-stage--frozen",
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage"
).get_attribute("class"),
)
)
# NVM btn on the same thumbnail now visible
nvm_btn = self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn',
)
self.assertTrue(nvm_btn.is_displayed(), "NVM btn should be visible after OK confirm")
# SAVE SIGN enabled
save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
self.assertFalse(save_btn.get_attribute("disabled"))
# Save persists
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,
)
)
# Applet on /billboard/ shows the saved card
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 5 ───────────────────────────────────────────────────────────────
def test_NVM_click_deselects_and_disables_save_sign(self):
"""After OK-lock, click NVM on the thumbnail → lock clears (no
.sig-stage--frozen), .sig-focused removed, SAVE SIGN disables."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
self.browser.execute_script(
"document.getElementById('id_scan_sign_btn').click()"
)
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)
ok_btn = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
)
)
self.browser.execute_script("arguments[0].click()", ok_btn)
# Now NVM
nvm_btn = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn',
)
)
self.browser.execute_script("arguments[0].click()", nvm_btn)
# Lock cleared
self.wait_for(
lambda: self.assertNotIn(
"sig-stage--frozen",
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage"
).get_attribute("class"),
)
)
# SAVE SIGN disables again
save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
self.assertTrue(save_btn.get_attribute("disabled"))
# .sig-focused removed from thumb
self.assertNotIn(
"sig-focused",
self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"]',
).get_attribute("class"),
)
# ── Test 6 ───────────────────────────────────────────────────────────────
def test_landing_previews_saved_sig_on_stage(self):
"""If user already has a saved significator, the landing collapses to
a read-only stage: the saved card preview + its stat block (no SCAN
SIGN btn, no hex — user must DEL the saved sig to re-enter picker)."""
# Pre-save a sig in the DB
self.gamer.significator = self.target_card
self.gamer.save(update_fields=["significator"])
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
# Stage card is visible w. the saved card populated (corner-rank +
# name pinned by data-card-id on the stage card wrapper)
stage_card = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
)
)
self.assertTrue(
stage_card.is_displayed(),
"Stage card should preview the saved sig on landing",
)
self.assertEqual(
stage_card.get_attribute("data-card-id"),
str(self.target_card.id),
)
# Stage is frozen → stat block visible next to the card
stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage")
self.assertIn("sig-stage--frozen", stage.get_attribute("class"))
self.assertTrue(
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage .sig-stat-block"
).is_displayed(),
"Stat block should be visible alongside the saved sig",
)
# Hex + SCAN SIGN gone — user must DEL to re-enter picker
self.assertEqual(
len(self.browser.find_elements(By.ID, "id_scan_sign_btn")),
0,
"SCAN SIGN btn should not render when a sig is already saved",
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".my-sign-page .table-hex"
)),
0,
"Table hex should not render when a sig is already saved",
)
# ── 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)."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
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}",
)
class MySignClearTest(FunctionalTest):
"""Clear-sign affordance on the SCAN SIGN landing screen.
Sprint 4b-adjacent (2026-05-19): the only way to undo a saved sig was
DB surgery — which blocked manual verification of [[sprint-my-sea-sign-
gate-may19]]'s no-sig branch on dev users w. a sig already set. Design
pre-spec'd in the 4b memory: a small CLEAR btn on the SCAN SIGN
landing screen, visible only when `user.significator` is set; POST
clears the FK + reversed flag + reloads to the no-sig landing."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
_seed_my_sign_applet()
self.email = "clear@test.io"
self.gamer = User.objects.create(email=self.email)
# Pre-save a sig so the DEL affordance is visible on landing.
self.target_card = _assign_sig(self.gamer)
# ── Test 1 ───────────────────────────────────────────────────────────────
def test_clear_sign_btn_renders_on_landing_when_sig_saved(self):
"""When `user.significator` is set, the landing screen shows a
DEL btn alongside SCAN SIGN. Uses .btn-danger for destructive
action treatment (mirrors post.html gear menu DEL)."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
btn = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_clear_sign_btn"
)
)
self.assertTrue(btn.is_displayed())
self.assertIn("DEL", btn.text.upper())
self.assertIn("btn-danger", btn.get_attribute("class"))
# ── Test 2 ───────────────────────────────────────────────────────────────
def test_clear_sign_btn_absent_when_no_sig_saved(self):
"""Fresh user w. no significator → no CLEAR btn on the landing."""
# Wipe the sig set in setUp so this user lands in the no-sig state.
self.gamer.significator = None
self.gamer.save(update_fields=["significator"])
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
# Wait for the page to settle on the SCAN SIGN btn (proxy for landing).
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_scan_sign_btn")
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, "#id_clear_sign_btn"
)),
0,
)
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_clear_sign_click_wipes_sig_and_reloads_to_no_sig_landing(self):
"""Click CLEAR SIGN → POST → page reloads → landing shows the
no-sig state (no stage card preview, no CLEAR btn) + the DB has
`user.significator = None`."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
btn = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_clear_sign_btn")
)
btn.click()
# After the POST → redirect → GET, the CLEAR btn should no longer
# exist + the stage card preview should be hidden.
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, "#id_clear_sign_btn"
)),
0,
)
)
self.gamer.refresh_from_db()
self.assertIsNone(self.gamer.significator)
self.assertFalse(self.gamer.significator_reversed)