My Sea DRAW SEA landing — Sprint 5 iter 1 of My Sea roadmap — TDD

DRAW SEA landing UX on /gameboard/my-sea/ for users past the [[sprint-my-sea-sign-gate-may19]] gate. DRY table hex (reused from the room shell + my-sign Sprint 4a iter 3) w. 6 chair seats labeled 1C-6C (placeholder for friend-invite per the My Sea roadmap "Six chairs retained even in solo" anchor) + central DRAW SEA `.btn-primary` mirroring SCAN SIGN on /billboard/my-sign/. Click swaps `.my-sea-page[data-phase]` from `landing` to `picker`; the picker UX itself (three-card cross w. cover/leave/loom + form col / spread dropdown / decks / LOCK HAND / DEL) lands in iters 2 + 3.

The 'C' suffix on the chair labels = "Chair" (user-locked); no role semantics (this is a solo draw, not a 6-player role assignment). `.table-seat` CSS class + `data-slot` attribute preserved so the room's existing `[data-slot="N"]` positioning rules (`_room.scss` L583-588) carry over for free — no SCSS fork; just a new `.seat-label` span inside each seat.

The 'Default deck warning' Brief banner from /billboard/my-sign/ fires verbatim when `user.equipped_deck` is None (the user is headed for a draw against the Earthman [Shabby Cardstock] backup unless they equip one first). Tagged `.my-sea-intro-banner` so FTs disambiguate from other Briefs. Same FYI (→ /gameboard/) / NVM (dismiss) action grammar.

Bundled: BACK→NVM label swap (user-edited mid-sprint) in the existing sign-gate. CSS class `.my-sea-sign-gate__back` retained — the swap was label-only — so existing FTs targeting the class still pass; docstrings + comments updated for accuracy.

Files:
- `apps/gameboard/views.py` — `my_sea` view adds 2 context keys: `no_equipped_deck` (bool) + `show_backup_intro_banner` (= user_has_sig AND no_equipped_deck). The sig-gate path still wins precedence.
- `templates/apps/gameboard/my_sea.html` — `.my-sea-page[data-phase="landing"]`; new `.my-sea-landing` block w. room-shell hex + `#id_draw_sea_btn` + 6 `.table-seat[data-slot="N"]` w. `<span class="seat-label">NC</span>`; new `.my-sea-picker` placeholder (`display:none` til DRAW SEA click); inline `<script>` for the click→data-phase swap + scaleTable re-fire on next tick (mirrors my-sign's iter-3 RAF dispatch); copies the my-sign Brief banner script block verbatim w. `.my-sea-intro-banner` post-render tag.
- `static_src/scss/_gameboard.scss` — new `.my-sea-page` + `.my-sea-landing` + `.my-sea-picker` rule blocks mirroring `.my-sign-page` / `.my-sign-landing` from `_card-deck.scss`. `.seat-label` styled in `--terUser` to match the chair iconography.
- `apps/gameboard/tests/integrated/test_views.py` — `+7 ITs` in new `MySeaDrawSeaLandingViewTest` pinning the context keys + presence/absence of `#id_draw_sea_btn` + 6 `data-slot=N` w. `NC` labels + Sprint 4b gate's precedence over the new landing.
- `functional_tests/test_game_my_sea.py` — `+5 FTs` in new `MySeaDrawSeaLandingTest` for the visible UX (hex + DRAW SEA, 6 seats labeled 1C-6C, click→picker phase swap, Brief banner on no-deck, no banner when deck equipped). Uses new `_assign_sig` helper from [[sprint-sig-page-helper-may19c]] for the user-w-sig precondition.

Tests: 25/25 FTs green across test_bill_my_sign + test_game_my_sea in 219s; 1036/1036 IT/UT green in 50s (+7 from baseline).

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-19 15:15:37 -04:00
parent 4d1c74a2af
commit de48ae226d
5 changed files with 352 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
Sprint 4b of [[project-my-sea-roadmap]]. The /gameboard/my-sea/ page is
gated behind sig selection — when `user.significator` is None, render a
Look!-formatted Brief-style line w. FYI (→ /billboard/my-sign/) + BACK
Look!-formatted Brief-style line w. FYI (→ /billboard/my-sign/) + NVM
(→ /gameboard/) instead of the draw UX. The My Sea applet on /gameboard/
mirrors the gate hint in its empty-state slot.
"""
@@ -36,7 +36,7 @@ def _seed_gameboard_applets():
class MySeaSignGateTest(FunctionalTest):
"""Sign-gate UX on the standalone /gameboard/my-sea/ page + the
/gameboard/ My Sea applet. User without a saved sig sees a Look!-
formatted nudge w. FYI to the picker + BACK to the gameboard."""
formatted nudge w. FYI to the picker + NVM to the gameboard."""
def setUp(self):
super().setUp()
@@ -55,7 +55,7 @@ class MySeaSignGateTest(FunctionalTest):
def test_no_sig_renders_lookline_gate_on_standalone_page(self):
"""User without significator → /gameboard/my-sea/ shows the Look!-
formatted Brief-style line w. the gate copy + FYI + BACK buttons."""
formatted Brief-style line w. the gate copy + FYI + NVM buttons."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
gate = self.wait_for(
@@ -67,11 +67,12 @@ class MySeaSignGateTest(FunctionalTest):
self.assertIn("Look!", text)
self.assertIn("pick your sign", text.lower())
self.assertIn("drawing the Sea", text)
# FYI + BACK action buttons
# FYI + NVM action buttons (class .my-sea-sign-gate__back retained
# post-relabel; the BACK→NVM swap was label-only).
fyi = gate.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate__fyi")
self.assertTrue(fyi.is_displayed())
back = gate.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate__back")
self.assertTrue(back.is_displayed())
nvm = gate.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate__back")
self.assertTrue(nvm.is_displayed())
# ── Test 2 ───────────────────────────────────────────────────────────────
@@ -93,18 +94,19 @@ class MySeaSignGateTest(FunctionalTest):
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_gate_back_links_to_gameboard(self):
"""BACK button is an `<a href>` pointing at /gameboard/."""
"""NVM button is an `<a href>` pointing at /gameboard/. CSS class
`.my-sea-sign-gate__back` retained post BACK→NVM label swap."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
back = self.wait_for(
nvm = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-sign-gate__back"
)
)
href = back.get_attribute("href") or ""
href = nvm.get_attribute("href") or ""
self.assertTrue(
href.endswith("/gameboard/"),
f"BACK should link to /gameboard/, got {href!r}",
f"NVM should link to /gameboard/, got {href!r}",
)
# ── Test 4 ───────────────────────────────────────────────────────────────
@@ -159,3 +161,134 @@ class MySeaSignGateTest(FunctionalTest):
)),
0,
)
class MySeaDrawSeaLandingTest(FunctionalTest):
"""Sprint 5 iter 1 — DRAW SEA landing on /gameboard/my-sea/ for a
user w. a saved sig (past the [[sprint-my-sea-sign-gate-may19]] gate).
Landing renders a DRY table hex (parameterized from the room) w. 6
chair seats labeled 1C-6C (placeholders for the eventual friend-
invite feature per [[project-my-sea-roadmap]] architectural anchor
"Six chairs retained even in solo") + a central DRAW SEA `.btn-
primary` mirroring SCAN SIGN on /billboard/my-sign/. The same Brief
"Default deck warning" copy from my-sign fires when the user has no
equipped deck.
Iter 1 scope: landing render + DRAW SEA click swaps `data-phase` to
`picker` (picker UX itself lands in iter 2). Form-col (spread
dropdown / decks / LOCK HAND / DEL) lands in iter 3."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
_seed_gameboard_applets()
self.email = "draw@test.io"
self.gamer = User.objects.create(email=self.email)
# Assign a sig so the page passes the Sprint 4b gate + lands on
# the new DRAW SEA UX rather than the Look!-line gate.
self.target_card = _assign_sig(self.gamer)
# ── Test 1 ───────────────────────────────────────────────────────────────
def test_landing_renders_hex_with_draw_sea_btn(self):
"""User w. sig → /gameboard/my-sea/ shows the DRY table hex (re-
used from my-sign / the room shell) w. a central DRAW SEA btn."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
# data-phase=landing on the page wrapper
page = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sea-page[data-phase='landing']")
)
# Hex shell present
page.find_element(By.CSS_SELECTOR, ".room-shell .table-hex")
# DRAW SEA btn in hex center
btn = page.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
self.assertTrue(btn.is_displayed())
self.assertIn("DRAW", btn.text.upper())
self.assertIn("SEA", btn.text.upper())
self.assertIn("btn-primary", btn.get_attribute("class"))
# ── Test 2 ───────────────────────────────────────────────────────────────
def test_landing_renders_six_chair_seats_labeled_1C_to_6C(self):
"""All 6 chair positions render w. labels 1C-6C (placeholder for
friend-invite). CSS class `.table-seat` is preserved so the SCSS
positioning rules (data-slot=N) carry over from the room shell."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
seats = self.wait_for(
lambda: self._six_seats()
)
self.assertEqual(len(seats), 6)
labels = [
"".join(s.text.upper().split()) for s in seats
]
for n in range(1, 7):
with self.subTest(slot=n):
self.assertIn(f"{n}C", labels[n - 1])
def _six_seats(self):
seats = self.browser.find_elements(
By.CSS_SELECTOR, ".my-sea-page[data-phase='landing'] .table-seat"
)
if len(seats) != 6:
raise AssertionError(f"expected 6 seats, got {len(seats)}")
return seats
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_draw_sea_click_transitions_to_picker_phase(self):
"""Click DRAW SEA → page wrapper's data-phase swaps to 'picker';
landing hides. Picker content itself lands in iter 2 — this test
pins only the phase-swap contract iter 1 must establish."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
btn = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
)
btn.click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-page[data-phase='picker']"
)
)
# Landing block hidden after swap.
landing = self.browser.find_element(By.CSS_SELECTOR, ".my-sea-landing")
self.assertFalse(landing.is_displayed())
# ── Test 4 ───────────────────────────────────────────────────────────────
def test_brief_banner_renders_when_no_deck_equipped(self):
"""No equipped deck → the same 'Default deck warning' Brief
banner from my-sign fires (lifted verbatim). Tagged w. a my-sea-
specific class so FTs can disambiguate from any other Briefs."""
self.gamer.equipped_deck = None
self.gamer.save(update_fields=["equipped_deck"])
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
banner = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-intro-banner"
)
)
self.assertIn("Default deck warning", banner.text)
self.assertIn("no deck is equipped", banner.text)
self.assertIn("Shabby Cardstock", banner.text)
# ── Test 5 ───────────────────────────────────────────────────────────────
def test_no_brief_banner_when_deck_equipped(self):
"""User w. an equipped deck → no Default-deck-warning Brief on
landing. Auto-equip via the User post_save signal handles this
for fresh users; assertion guards against accidental render of
the banner when the condition shouldn't fire."""
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
)
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".my-sea-intro-banner")),
0,
)