My Sign picker iteration 3 — SCAN SIGN landing hex + OK/NVM thumbnail two-step + duoUser picker bg — Sprint 4a-cont — TDD
Two-phase picker. Landing phase renders the DRY 1-chair table hex w. a central SCAN SIGN .btn-primary; clicking it swaps the page to picker phase (hex hides, sig-card grid + always-present stage frame + SAVE SIGN visible). Stage frame previews the saved sig on landing if User.significator is set ; sig-card selection lifts the room's two-step OK/NVM-on-thumbnail pattern via `.sig-card-actions` w. `.sig-ok-btn`/`.sig-nvm-btn`: click thumb → `.sig-focused` (CSS reveals OK badge, stage previews card, no lock); click OK → `.sig-reserved--own` (CSS swaps OK→NVM badge, `.sig-stage--frozen` reveals stat block + FLIP, SAVE SIGN enables); click NVM → unlock + clear focus + disable SAVE SIGN ; SAVE SIGN form pinned `position:absolute; bottom:0.75rem; right:1rem` to .my-sign-stage so it stops shifting across the stage row when the stat block reveals on lock (was getting shoved left as a flex item alongside the stat-block reveal) ; .my-sign-page mirrors .room-page's `flex:1; min-height:0; display:flex; flex-direction:column` so the DRY hex container chain propagates real height down into #id_game_table for room.js's scaleTable() to compute against (was reading 0 + leaving the hex unscaled at 200×231 in a 360×320 scene) ; stage min-height gated to picker phase (`.my-sign-page[data-phase="picker"] .my-sign-stage`) — landing-phase stage is natural-sized so the hex centers in the bigger available area instead of being bottom-anchored by a 376px stage reservation ; picker-phase bg uses `rgba(var(--duoUser), 1)` so the transition from "hex face" → "card pile on felt" reads as a continuous surface rather than a context swap ; room sig-select media queries re-scoped to `.sig-overlay .sig-deck-grid` so they don't bleed into my-sign — my-sign gets its own breakpoint cascade: 6×3rem (portrait) → 9×3rem (≥900px landscape) → 18×3rem (≥1600px) → 18×5rem (≥2200px); thresholds bumped from sig-select's 1400/1800px so 18×col + sidebar/footer margins clear the viewport at fluid-rem ceiling (rem=22 → 18×3rem=1188px + 220px margins=1408, safe with 1600px floor) ; default `repeat(6, 1fr)` collapsed to 0-width when paired w. `align-self:center` (no parent width for `fr` to resolve against, hence the dotted-line miniscule cards in portrait); fixed `repeat(6, 3rem)` at portrait default fixes it ; SCAN SIGN font-size 0.75rem (vs .btn-primary's default 0.875rem) so the 2-line "SCAN/SIGN" label fits inside the 4rem circle without crowding the border — treated as a smaller variant via `#id_scan_sign_btn` rule scoped under .my-sign-landing ; room.js's scaleTable() runs on DOMContentLoaded before flex layout flushes (#id_game_table.clientWidth/Height read 0 at that moment) — added `requestAnimationFrame → dispatchEvent('resize')` tick at the end of the inline IIFE so scaleTable re-fires once layout settles ; tests — 6 FTs in test_bill_my_sign.py rewritten for the new flow: test_landing_renders_dry_hex_with_scan_sign_button pins the 1-chair hex + central SCAN SIGN + hidden picker grid; test_scan_sign_click_transitions_to_picker_phase pins the phase swap (hex hides, grid shows); test_click_thumbnail_shows_OK_btn_without_locking pins step 1 (focus + OK appears, no lock yet); test_OK_click_locks_thumbnail_and_enables_save_sign pins step 2 (lock + NVM appears + SAVE SIGN enables + persists to /billboard/ applet); test_NVM_click_deselects_and_disables_save_sign pins NVM unlock cycle; test_landing_previews_saved_sig_on_stage pins the on-load saved-sig preview behavior — all green visually verified across portrait + landscape
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:
@@ -62,14 +62,12 @@ class MySignPickerTest(FunctionalTest):
|
||||
|
||||
# ── 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."""
|
||||
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/")
|
||||
# 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",
|
||||
@@ -78,26 +76,70 @@ class MySignPickerTest(FunctionalTest):
|
||||
),
|
||||
)
|
||||
)
|
||||
# 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 "").
|
||||
# Landing phase markers: hex + 1 chair + SCAN SIGN btn
|
||||
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}"]',
|
||||
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_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."""
|
||||
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}"]',
|
||||
)
|
||||
|
||||
# Click target card
|
||||
# ── 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,
|
||||
@@ -105,34 +147,79 @@ class MySignPickerTest(FunctionalTest):
|
||||
)
|
||||
)
|
||||
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")
|
||||
)
|
||||
# .sig-focused class → OK btn visible
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
save_btn.get_attribute("disabled"),
|
||||
"SAVE SIGN should be enabled after card click",
|
||||
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"),
|
||||
)
|
||||
)
|
||||
|
||||
# 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"),
|
||||
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"))
|
||||
|
||||
# Save → DB updated
|
||||
# ── 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,
|
||||
)
|
||||
)
|
||||
|
||||
# Navigate to /billboard/ → applet shows the saved card. Pin by
|
||||
# data-card-id (same reasoning as test 1 re: CSS-hidden corner rank).
|
||||
# Applet on /billboard/ shows the saved card
|
||||
self.browser.get(self.live_server_url + "/billboard/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
@@ -141,6 +228,87 @@ class MySignPickerTest(FunctionalTest):
|
||||
)
|
||||
)
|
||||
|
||||
# ── 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-phase stage
|
||||
frame should preview the saved card (above/behind the hex)."""
|
||||
# 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/")
|
||||
# Landing phase still shows hex w. SCAN SIGN
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_scan_sign_btn")
|
||||
)
|
||||
# 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.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),
|
||||
)
|
||||
|
||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_applet_renders_blank_state_when_no_sig_chosen(self):
|
||||
|
||||
Reference in New Issue
Block a user