Files
python-tdd/src/functional_tests/test_bill_my_sign.py
Disco DeDisco 5e78e6b832 A.3 my_sign.html image-rendering — first visible surface — TDD. Sprint A.3 of [[project-image-based-deck-face-rendering]]. When the user's equipped deck has has_card_images=True (Minchiate Fiorentine 1860-1890 today), the saved-sig stage card on /billboard/my-sign/ renders as an <img> over the irregular-shape transparent PNG with a contour-following arcana-colored stroke — not the text fan-card scaffold. First of 6 surfaces in the image-rendering rollout (my_sea + both billboard applets + room + game_kit follow in A.5+). New TarotCard.image_url property (consumes A.2's image_filename + DeckVariant.has_card_images + django.templatetags.static.static() to produce a full static-asset URL) — empty string when has_card_images=False so legacy text-only decks (Earthman, RWS) pass through transparently. my_sign.html picker grid .sig-card elements gain data-image-url + data-arcana-key attrs (the latter for stroke-color CSS selection); the .sig-stage-card scaffold gains a hidden <img class="sig-stage-card-img"> slot that JS swaps visible when image-mode is active. stage-card.js extends fromDataset to read image_url + arcana_key; new _setImageMode(stageCard, card) toggles the .sig-stage-card--image marker class + sets data-arcana-key on the stage card + populates the img src/alt; called from populateCard so all existing sig-stage flows pick up image rendering automatically (text-mode decks still pass through since image_url is empty). SCSS: new .sig-stage-card.sig-stage-card--image rule hides the .fan-card-corner + .fan-card-face text scaffold, strips the rectangular border/padding, and applies a 4-cardinal-direction filter: drop-shadow() stack to the <img> so the stroke FOLLOWS the alpha contour of the PNG instead of tracing a rectangular bounding box (per user spec 2026-05-25 PM clarification — early draft used a rectangular border which doesn't match the irregular-card aesthetic). Stroke color is driven by a CSS custom prop --img-stroke-color defaulting to rgba(var(--quiUser), 1) (cream — minor + middle arcana); [data-arcana-key="MAJOR"] override flips it to rgba(var(--terUser), 1) (gold) per Q2 lock. mobile-safe — filter on raster images works cross-browser (the [[feedback-mobile-svg-glow]] dead-end was specifically SVG glow, not raster drop-shadows). New _seed_minchiate_image_fixtures() helper in functional_tests/sig_page.py re-seeds the minimal Minchiate fixture (DeckVariant + Il Matto + Papa Uno) needed for image FTs after TransactionTestCase's flush wipes migration data — mirrors the existing _seed_earthman_sig_pile pattern per [[feedback-transactiontestcase-flush]]. New MySignImageRenderingTest.test_saved_sig_renders_as_img_for_image_deck FT seeds Minchiate + creates a superuser test gamer (superuser auto-gets super-nomad + super-schizo Notes via the User post_save signal, which _filter_major_unlocks then lets through to expose Il Matto in the picker grid — otherwise Minchiate's sig pool is empty since it has no MIDDLE arcana cards), equips Minchiate, saves Il Matto as sig, visits /billboard/my-sign/, asserts the stage card displays + contains an <img> w. src ending in the v2-convention filename minchiate-fiorentine-1860-1890-trumps-00-il-matto.png + carries .sig-stage-card--image marker class. Out of scope for this commit (deferred to A.3 follow-up polish + A.5+): the full stat-block restructure (top-left rank+suit chip Q♥ inline w. EMANATION/REVERSAL header; title in arcana-color font; keyword reposition; FYI panel re-anchor — per the locked Q3 spec) — image card-face ships now w. the existing stat-block layout to land the visible-win first. Tests: 1 new FT green; 15/15 my_sign FT class green (no regression on the 14 existing tests); 1289/1289 IT+UT total green (68s, unchanged from A.2 since no new ITs in this commit — FT covers the wiring end-to-end). Sprint A backend foundation (A.0+A.1+A.2) + first visible surface (A.3) all landed; 5 surfaces remain (A.5-A.8 + A.4's card-deck icon)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:04:18 -04:00

616 lines
28 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, _seed_minchiate_image_fixtures
from apps.applets.models import Applet
from apps.epic.models import TarotCard, 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)
class MySignImageRenderingTest(FunctionalTest):
"""Sprint A.3 — when the user's equipped deck has card images (Minchiate
Fiorentine 1860-1890 today), the saved-sig stage card renders as an <img>
pointing at the deck's image asset, not the text-only fan-card scaffold.
First visible-surface FT in the image-rendering rollout per
[[project-image-based-deck-face-rendering]]. Other 5 surfaces (my_sea,
both billboard applets, room, game_kit) follow in A.5+.
"""
def setUp(self):
super().setUp()
# Earthman is auto-equipped by the User post_save signal — seed its
# pile first so the signal succeeds, then override the equipped deck
# to Minchiate (the image-deck under test).
_seed_earthman_sig_pile()
self.minchiate = _seed_minchiate_image_fixtures()
Applet.objects.get_or_create(
slug="my-sign",
defaults={"name": "My Sign", "context": "billboard",
"default_visible": True, "grid_cols": 4, "grid_rows": 6},
)
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 = "img-sig@test.io"
# Superuser so post_save grants super-nomad + super-schizo Notes →
# `_filter_major_unlocks` lets Il Matto (Major 0) through into the
# picker grid. Without the Notes, Minchiate's sig pool is empty for
# this user (no MIDDLE arcana cards + the 2 Major-0/1 cards filtered).
self.gamer = User.objects.create(email=self.email, is_superuser=True)
self.gamer.unlocked_decks.add(self.minchiate)
self.gamer.equipped_deck = self.minchiate
self.gamer.save(update_fields=["equipped_deck"])
# Save Il Matto as the user's sig (bypass the picker UI — the FT is
# about render output, not the pick flow).
self.il_matto = TarotCard.objects.get(
deck_variant=self.minchiate, slug="il-matto",
)
_assign_sig(self.gamer, card=self.il_matto)
def test_saved_sig_renders_as_img_for_image_deck(self):
"""Visit /billboard/my-sign/ with a Minchiate sig saved → the stage
card contains an <img> child whose src points at the deck's image
asset under the v2 naming convention. The text fan-card scaffold is
hidden in image mode."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
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",
)
# <img> child renders w. the v2-convention filename for Il Matto.
img = self.wait_for(
lambda: stage_card.find_element(By.TAG_NAME, "img")
)
src = img.get_attribute("src") or ""
self.assertIn(
"minchiate-fiorentine-1860-1890-trumps-00-il-matto.png",
src,
f"Expected Minchiate Il Matto image src, got: {src}",
)
# Image mode toggle — the stage card carries a marker class so SCSS
# can hide the fan-card text scaffold + show the <img>.
self.assertIn(
"sig-stage-card--image", stage_card.get_attribute("class"),
"Stage card should carry .sig-stage-card--image class in image mode",
)