A.6 + A.7 billboard My Sign applet + gameboard My Sea applet image-rendering + applet-level FLIP-to-back — TDD. Sprints A.6 + A.7 of [[project-image-based-deck-face-rendering]]: rolls image-mode out to the two card-rendering applets (My Sign on /billboard/, My Sea on /gameboard/). Both reuse the shared .sig-stage-card.sig-stage-card--image SCSS contract via a comma-list selector extension covering the parallel container classes (.my-sign-applet-card.my-sign-applet-card--image + .my-sea-slot.my-sea-slot--image) — single source of truth for the contour-stroke drop-shadow chain + tray-card silhouette black depth shadow + .is-flipped-to-back visibility toggle + the --img-stroke-color arcana-keyed CSS prop. Templates branch server-side on card.deck_variant.has_card_images: image-mode renders <img class="sig-stage-card-img" src="{{ card.image_url }}"> w. the marker class + data-arcana-key attr; text mode keeps the existing fan-card-corner + fan-card-face scaffold unchanged. SCSS import-order quirk: _card-deck.scss imports BEFORE both _billboard.scss (which nests .my-sign-applet-card inside .my-sign-applet-body for container queries) and _gameboard.scss (which nests .my-sea-slot--filled.--gravity/--levity inside #id_applet_my_sea w. specificity 1,2,0). The shared top-level image-mode rule at 0,2,0 loses on bg/border/padding to those nested base rules, so each app's stylesheet gets a parallel &.--image { background: transparent; border: 0; padding: 0 } override inside its own nest. The filter-chain rules on .sig-stage-card-img (descendant selector inside the shared rule) DO win since the apps don't restyle that class — only the outer container needs the parallel override. Sprint A.6 bonus: applet-level FLIP btn for non-polarized image-equipped decks (Minchiate today). Mirrors the my_sign.html main page A.5-polish-2 FLIP-to-back contract — .my-sign-applet-flip-btn nested inside the .--image card so absolute positioning anchors to the card bounds; inline <script> IIFE (gated inside the sig-present {% with card %} scope to keep card in lexical reach + prevent the JS selector string leaking into the no-sig DOM where assertNotContains "my-sign-applet-card" ITs catch it) attaches a click handler that runs the same rotateY 0→90→0 animation, toggles .is-flipped-to-back at the halfway point, and clears data-flipping at end; SCSS .my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn { opacity: 0; pointer-events: none } hides the btn mid-spin. Critical scope bug caught + fixed during browser verify: initial draft had the script BLOCK + its {% if card.deck_variant.has_card_images %} gate placed AFTER the {% endwith %} closing tag — card was out of scope at the {% if %} evaluation, Django treats undefined vars as empty string, the gate evaluated falsy, and the script NEVER rendered (the FLIP btn rendered fine since it was inside the with block, but no JS handler → click did nothing but the CSS depress animation). Fix: move {% endwith %} to AFTER the script gate so card is still in scope. 7 new ITs total: 2 in BillboardAppletMySignTest (image-equipped Minchiate renders --image class + img + correct asset URL + lacks text scaffold; Earthman keeps the text scaffold + lacks --image); 3 in BillboardMySignViewTest (data-deck-polarized attr present; back-img element renders for non-polarized image deck; polarized deck omits it); 1 in GameboardViewTest (image-equipped Minchiate slot renders --image + img + lacks text scaffold); plus regression coverage on the no-sig empty-state assertion that originally caught the script-scope bug (assertNotContains validates the script doesn't leak in the no-sig case). Tests: 6 new ITs green; 1306/1306 IT+UT total green (72s; +6 from bdf6a25's 1303 — minus 3 dups since some ITs were counted across both A.6 + A.5-polish-2 runs). Visual verify by user 2026-05-25 PM: stage card image renders cleanly; FLIP cycles to back image + back via animation; FLIP btn hides during 500ms spin; placeholder dim styling correctly distinguishes no-deck state

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 01:58:36 -04:00
parent bdf6a251f4
commit dd99364b78
7 changed files with 284 additions and 63 deletions

View File

@@ -1034,3 +1034,62 @@ class BillboardAppletMySignTest(TestCase):
self.assertContains(response, "my-sign-applet-card--gravity")
if target.gravity_qualifier:
self.assertContains(response, target.gravity_qualifier)
def test_my_sign_applet_renders_image_when_deck_has_card_images(self):
"""Sprint A.6 — applet card carries `.my-sign-applet-card--image` +
an <img.sig-stage-card-img> child when the user's equipped deck is
image-equipped (Minchiate today). Shares the contour-stroke + depth
shadow SCSS w. my_sign.html's stage-card-image via comma-list selector.
Text scaffold (fan-card-corner / fan-card-face) is NOT rendered in
image mode — server-side template `{% if/else %}` branch."""
from apps.epic.models import DeckVariant, TarotCard
import lxml.html
minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890")
self.user.is_superuser = True
self.user.save()
from apps.drama.models import Note
Note.grant_if_new(self.user, "super-nomad")
Note.grant_if_new(self.user, "super-schizo")
self.user.unlocked_decks.add(minchiate)
self.user.equipped_deck = minchiate
il_matto = TarotCard.objects.get(deck_variant=minchiate, slug="il-matto")
self.user.significator = il_matto
self.user.save(update_fields=["equipped_deck", "significator"])
response = self.client.get("/billboard/")
parsed = lxml.html.fromstring(response.content)
[card_el] = parsed.cssselect(".my-sign-applet-card")
self.assertIn("my-sign-applet-card--image", card_el.get("class", ""))
self.assertEqual(card_el.get("data-arcana-key"), "MAJOR")
[img] = card_el.cssselect("img.sig-stage-card-img")
self.assertIn(
"minchiate-fiorentine-1860-1890-trumps-00-il-matto.png",
img.get("src", ""),
)
# Text scaffold absent in image mode (the server-side {% if %} branch
# skips the fan-card-corner + fan-card-face children entirely).
self.assertEqual(
len(card_el.cssselect(".fan-card-corner")), 0,
"Text scaffold must not render in image mode",
)
def test_my_sign_applet_keeps_text_render_for_non_image_deck(self):
"""Earthman (has_card_images=False) keeps the existing fan-card-corner
text scaffold + lacks the --image modifier class."""
from apps.epic.models import personal_sig_cards
target = personal_sig_cards(self.user)[0]
self.user.significator = target
self.user.save(update_fields=["significator"])
import lxml.html
response = self.client.get("/billboard/")
parsed = lxml.html.fromstring(response.content)
[card_el] = parsed.cssselect(".my-sign-applet-card")
self.assertNotIn("my-sign-applet-card--image", card_el.get("class", ""))
self.assertEqual(
len(card_el.cssselect("img.sig-stage-card-img")), 0,
"Non-image deck must not render the <img>",
)
self.assertGreater(
len(card_el.cssselect(".fan-card-corner")), 0,
"Non-image deck keeps the text scaffold",
)