616 lines
28 KiB
Python
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",
|
|
)
|