My Sea iter 6b: navbar GATE VIEW swap on page-my-sea + landing PAID DRAW state + seat-1 server-render + auto-token IT trap in gatekeeper FT — Sprint 5 iter 6b of My Sea roadmap — TDD
Second of three Sprint 6 commits per [[sprint-my-sea-iter-6-plan]]. Wires the always-reachable navbar gate-entry, completes the landing center-btn 3-way state machine (FREE DRAW / GATE VIEW / PAID DRAW), and lifts seat-1's `.seated` state from JS-only to server-rendered (reload-stable).
## Navbar GATE VIEW swap
`templates/core/_partials/_navbar.html` — when `'page-my-sea' in page_class`, CONT GAME swaps for `#id_navbar_gate_view_btn` (`.btn-primary`, plain `<button>` w. inline onclick navigation). Reaches the gatekeeper at any quota state — no confirm guard (non-destructive nav).
**Typeface trap caught (user 2026-05-20 visual report)**: first cut used `<a>` for GATE VIEW, which UA-renders serif while `<button>` stays sans-serif (`.btn` doesn't reset `font-family`). Same fix pattern as iter-4c's in-hex GATE VIEW: always use `<button>`. Second cut used a form-wrapped `<button>` w. `display:contents`; the form was correctly invisible in layout but broke the landscape `> #id_cont_game { order: -1 }` direct-child SCSS pin (form became the direct child, not the button). Final cut: plain `<button>` w. `onclick="window.location.href=..."`, no form, no anchor — direct flex child of `.container-fluid` so the SCSS pin matches.
`_base.scss` — paired `> #id_navbar_gate_view_btn` alongside `> #id_cont_game` in both portrait (line 93) + landscape (line 309) rules so GATE VIEW occupies the same top-center navbar slot CONT GAME does (above brand, `order: -1`).
## Landing center-btn 3-way state machine
`my_sea` view gains `deposit_reserved` (active_draw has deposit_token_id) + `hand_non_empty` context vars.
`my_sea.html` landing branches:
- `deposit_reserved` → **PAID DRAW** form (POSTs to `my_sea_paid_draw`); fastest path back to picker w. one click — no gatekeeper round-trip.
- `quota_spent and not deposit_reserved` → **GATE VIEW** (existing iter-4c btn, navigates to gatekeeper).
- else → **FREE DRAW** (existing iter-1 btn).
Three branches are mutually exclusive — FT asserts only one of `#id_my_sea_paid_draw_btn` / `#id_my_sea_gate_view_btn` / `#id_draw_sea_btn` renders at a time.
## Seat-1 server-render
`my_sea.html` table-seat 1 now picks up `.seated` + `.fa-circle-check` (instead of `.fa-ban`) when `hand_non_empty`. Other 5 seats stay banned (placeholders for the future friend-invite feature; only owner ever occupies seat 1 in solo my-sea). Reloads no longer lose the chair-styling state — existing JS animation (FREE DRAW click → flip seat to seated) still fires on first draw.
In practice today the landing only renders when hand IS empty (show_picker hides landing once hand has cards), so the `.seated` branch isn't actually visible in iter 6b. Defensive code for future surfaces (any hex render w. hand non-empty) per [[sprint-my-sea-iter-6-plan]] §Seat-1 persistence.
## FT delta
**Replaced** `MySeaGatekeeperPageTest.test_gatekeeper_renders_six_chair_seats_with_seat1_seated` w. `test_gatekeeper_renders_no_hex_modal_only`. The iter-6a FT skeleton was written before the user's "no hex on gatekeeper" spec (2026-05-20) — seats now live ONLY on the my-sea picker page; the gatekeeper is a transient `.gate-modal` overlay w. no hex / chair-seats.
**Trap caught**: `MySeaGatekeeperPageTest.test_paid_draw_commits_token_and_redirects_to_picker` was passing in iter 6a only because it didn't actually exist in CI then; running it locally exposed the IT-trap pattern: User post_save signal auto-creates COIN + FREE tokens (`apps.lyric.models:309`), so `_select_my_sea_token` picks the auto-COIN (PASS > **COIN** > FREE > TITHE) instead of the manually-seeded FREE. Test asserted FREE count drops by 1 → fails because COIN was actually debited (sets cooldown, doesn't delete the token). Same trap as the iter-6a IT memo; fix is identical: `self.gamer.tokens.all().delete()` after User.create + then seed only the token the test cares about.
## Tests
- 4 MySeaGatekeeperPageTest (iter 6a, now passing) + 1 MySeaLandingPaidDrawTest + 1 MySeaNavbarGateViewTest + 2 MySeaSeatOnePersistenceTest = 8 FTs green in 84s.
- All 7 `test_core_navbar` FTs (NavbarByeTest + NavbarContGameTest) still green — landscape order rule extension is additive; CONT GAME path unchanged.
- 153/153 gameboard ITs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1200,6 +1200,12 @@ class MySeaGatekeeperPageTest(FunctionalTest):
|
||||
self.email = "gate@test.io"
|
||||
self.gamer = User.objects.create(email=self.email)
|
||||
_assign_sig(self.gamer)
|
||||
# The User post_save signal auto-creates COIN + FREE tokens
|
||||
# (apps.lyric.models). Clear them so the deposit-priority test
|
||||
# below picks only the FREE we seed explicitly — otherwise
|
||||
# COIN wins (PASS > COIN > FREE > TITHE) and the FREE-count
|
||||
# assertion fails (per IT-trap memo, 2026-05-19).
|
||||
self.gamer.tokens.all().delete()
|
||||
# Seed a FREE token so the gatekeeper has something to deposit.
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone as dj_tz
|
||||
@@ -1245,27 +1251,27 @@ class MySeaGatekeeperPageTest(FunctionalTest):
|
||||
0,
|
||||
)
|
||||
|
||||
def test_gatekeeper_renders_six_chair_seats_with_seat1_seated(self):
|
||||
"""Hex w. 6 chair seats; seat 1 is the owner's (always `.seated`
|
||||
when quota is committed); seats 2-6 carry `.fa-ban` (placeholders
|
||||
for the future friend-invite feature)."""
|
||||
def test_gatekeeper_renders_no_hex_modal_only(self):
|
||||
"""Per user spec 2026-05-20, the gatekeeper is a transient in-flight
|
||||
UI — modal-over-`--duoUser` bg, NO hex / chair-seats. Seats live on
|
||||
the my-sea picker page itself; the gatekeeper just offers the
|
||||
coin-slot + (post-deposit) PAID DRAW affordance."""
|
||||
self._save_empty_hand_draw()
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
|
||||
self.wait_for(
|
||||
lambda: self._assert_seats(6)
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".my-sea-gate-modal"
|
||||
)
|
||||
)
|
||||
seat1 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat[data-slot='1']"
|
||||
self.assertEqual(
|
||||
len(self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(self.browser.find_elements(By.CSS_SELECTOR, ".table-hex")),
|
||||
0,
|
||||
)
|
||||
self.assertIn("seated", seat1.get_attribute("class"))
|
||||
seat1.find_element(By.CSS_SELECTOR, ".fa-circle-check")
|
||||
|
||||
def _assert_seats(self, count):
|
||||
seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
|
||||
if len(seats) != count:
|
||||
raise AssertionError(f"expected {count} seats, got {len(seats)}")
|
||||
return seats
|
||||
|
||||
def test_insert_token_reserves_deposit_and_reveals_paid_draw_btn(self):
|
||||
"""Click INSERT TOKEN → server reserves the user's next-priority
|
||||
|
||||
Reference in New Issue
Block a user