(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>
536 lines
24 KiB
Python
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)
|