burger Sea sub-btn: first-draw --priYl glow handoff (phase 3/3) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

Final slice of the Sea sub-btn rollout (phase 1 = .active wiring 3ae85b9; phase 2 = modal extraction + CONT DRAW 6fbeed7). Adds a --priYl + --ninUser glow that rides the affordance chain to teach the user where to click pre-first-draw.

## The handoff chain

  burger  →  click  →  sea_btn  →  click  →  .sea-select  →  click  →  end

- Modal close (Esc / backdrop / DEL guard-OK) restarts the cycle on the burger.
- Burger fan close w/o sea_btn click ALSO restarts on the burger.
- AUTO DRAW guard-OK ends the cycle permanently (user found the path).
- `#id_sea_action_btn` data-state → 'gate-view' (last card landed via ANY path — AUTO DRAW or manual FLIP) ALSO ends permanently.

## SCSS

`static_src/scss/_burger.scss` — `.glow-handoff` on burger / sea_btn = --priYl color + border + --ninUser glow.
`static_src/scss/_gameboard.scss` — `.glow-handoff` on .sea-select = --terUser border + --ninUser glow (no font-color change per spec).

## Server side

`apps/gameboard/views.py` — new `sea_first_draw_pending = show_picker and not hand_non_empty`. True when picker is active w. an empty hand (paid-draw entry, or page reload of a freshly-entered picker). The FREE-DRAW → picker transition fires client-side w. show_picker=False on the rendered template, so the FREE DRAW JS handler seeds the burger glow itself in that path.

`templates/apps/gameboard/_partials/_burger.html` — `#id_burger_btn` conditionally renders `class="glow-handoff"` when `sea_first_draw_pending`.

`templates/apps/gameboard/my_sea.html` — FREE DRAW transition handler adds `.glow-handoff` to burger at the same SEAT_ANIM_MS moment data-phase swaps to 'picker' (covers the client-side path).

## JS state machine

`templates/apps/gameboard/my_sea.html` — new inline IIFE owns the .glow-handoff transitions:
- `burger.click` → if .glow-handoff on burger, transfer to sea_btn.
- `sea_btn.click` → if .glow-handoff on sea_btn, transfer to .sea-select.
- `.sea-select.click` → end this cycle (just clear the glow; cycle restarts on next modal open).
- AUTO DRAW guard-OK (via doc-level click listener) → sets `autoDrawConfirmed`.
- Modal `hidden`-attr observer: AUTO DRAW path → endPermanently; any other close (Esc / backdrop / DEL) → startOnBurger (skip if glow already permanently ended).
- Burger `class`-attr observer: fan closes (`.active` removed) while glow on sea_btn → restart on burger.
- `#id_sea_action_btn` `data-state`-attr observer: flips to 'gate-view' (last card landed via ANY path — AUTO DRAW finishing OR manual FLIP filling the final slot) → endPermanently.

The data-state observer makes the "stop glowing when all slots filled" guarantee async + decoupled from how the cards arrived.

## CONT DRAW polish (drag-in from prior commit's spec gap)

`apps/gameboard/views.py` — `show_cont_draw` now additionally requires `bool(active_draw.hand)` (at least one card drawn). Pre-draw NVM-to-landing falls through to the existing 3-way state machine (PAID DRAW / GATE VIEW / FREE DRAW) instead of misleading w. CONT DRAW that lands back on an empty picker.

## Tests (4 new ITs)

`apps/gameboard/tests/integrated/test_views.py::MySeaViewTest`:
- `test_burger_renders_glow_handoff_class_when_sea_first_draw_pending` — paid-draw entry to picker w. empty hand → burger has .glow-handoff.
- `test_burger_omits_glow_handoff_when_hand_non_empty` — mid-draw → no .glow-handoff.
- `test_burger_omits_glow_handoff_on_landing` — landing → no .glow-handoff (FREE DRAW handler seeds client-side instead).
- `test_force_landing_hides_cont_draw_when_hand_empty` — pre-first-draw NVM → no CONT DRAW.

(JS state-machine behaviour is verified visually; not Jasmine-tested since the IIFE lives inline on my_sea.html, not as a separate module.)

## Verification

All 1374 IT+UT green (+4 from Phase 3). Visual verification of glow handoff + hand-complete auto-end confirmed.

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:
Disco DeDisco
2026-05-27 00:39:46 -04:00
parent a39053d3f6
commit c30b63cd5d
6 changed files with 240 additions and 3 deletions

View File

@@ -1157,6 +1157,84 @@ class MySeaViewTest(TestCase):
self.assertNotContains(response, 'id="id_my_sea_cont_draw_btn"')
self.assertContains(response, 'id="id_draw_sea_btn"')
def test_burger_renders_glow_handoff_class_when_sea_first_draw_pending(self):
"""When picker phase is active + hand is still empty (paid-draw
entry, or page reload of an empty picker), the burger btn renders
w. .glow-handoff so the first-draw nudge cycle starts on it.
Picker w. an empty hand requires the paid-draw context — the
FREE-DRAW client-side transition seeds the class via JS instead
(see my_sea.html DRAW SEA handler)."""
from apps.epic.models import personal_sig_cards
from apps.gameboard.models import MySeaDraw
from apps.lyric.models import Token
from datetime import timedelta
from django.utils import timezone
sig = personal_sig_cards(self.user)[0]
self.user.significator = sig
self.user.save(update_fields=["significator"])
free_tok = Token.objects.create(
user=self.user, token_type=Token.FREE,
expires_at=timezone.now() + timedelta(days=30),
)
MySeaDraw.objects.create(
user=self.user, spread="situation-action-outcome",
significator_id=sig.id, hand=[],
deposit_token_id=free_tok.pk,
deposit_reserved_at=timezone.now(),
)
response = self.client.get(reverse("my_sea") + "?phase=picker")
# Burger btn rendered w. .glow-handoff (server-side seed)
self.assertContains(
response,
'<button id="id_burger_btn" type="button" aria-label="Open burger menu" aria-expanded="false" class="glow-handoff">',
)
def test_burger_omits_glow_handoff_when_hand_non_empty(self):
"""Mid-draw picker (hand has at least 1 card) — first-draw nudge
is over, burger renders w/o .glow-handoff."""
from apps.epic.models import personal_sig_cards, TarotCard
from apps.gameboard.models import MySeaDraw
sig = personal_sig_cards(self.user)[0]
self.user.significator = sig
self.user.save(update_fields=["significator"])
card = TarotCard.objects.exclude(pk=sig.pk).first()
MySeaDraw.objects.create(
user=self.user, spread="situation-action-outcome",
significator_id=sig.id,
hand=[{"position": "lay", "card_id": card.id,
"reversed": False, "polarity": "gravity"}],
)
response = self.client.get(reverse("my_sea"))
self.assertNotContains(response, 'class="glow-handoff"')
def test_burger_omits_glow_handoff_on_landing(self):
"""Landing phase (no sig, or sig w/o draw) — burger renders w/o
glow. FREE DRAW transition adds the class client-side; server-
rendered state stays clean to avoid a flash before user interaction."""
from apps.epic.models import personal_sig_cards
sig = personal_sig_cards(self.user)[0]
self.user.significator = sig
self.user.save(update_fields=["significator"])
response = self.client.get(reverse("my_sea"))
self.assertNotContains(response, 'class="glow-handoff"')
def test_force_landing_hides_cont_draw_when_hand_empty(self):
"""`?phase=landing` w. an active draw row BUT no cards yet → CONT
DRAW absent (nothing to continue — picker hasn't started). The
landing's existing 3-way state machine takes over (PAID DRAW when
deposit reserved, GATE VIEW for in-cooldown no-deposit, etc.)."""
from apps.epic.models import personal_sig_cards
from apps.gameboard.models import MySeaDraw
sig = personal_sig_cards(self.user)[0]
self.user.significator = sig
self.user.save(update_fields=["significator"])
MySeaDraw.objects.create(
user=self.user, spread="situation-action-outcome",
significator_id=sig.id, hand=[],
)
response = self.client.get(reverse("my_sea") + "?phase=landing")
self.assertNotContains(response, 'id="id_my_sea_cont_draw_btn"')
def test_force_landing_hides_cont_draw_when_hand_complete(self):
"""`?phase=landing` w. a complete hand → CONT DRAW absent (nothing
to continue). GATE VIEW takes over via the existing landing state