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:
@@ -512,3 +512,61 @@ class MySeaViewTest(TestCase):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertIn("page-gameboard", response.content.decode())
|
||||
self.assertIn("page-my-sea", response.content.decode())
|
||||
|
||||
|
||||
class MySeaDrawSeaLandingViewTest(TestCase):
|
||||
"""Sprint 5 iter 1 — view context for the DRAW SEA landing UX. Pins
|
||||
`no_equipped_deck` + `show_backup_intro_banner` context keys + the
|
||||
presence of the new landing template elements when user passes the
|
||||
Sprint 4b sign-gate."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.epic.models import personal_sig_cards
|
||||
self.user = User.objects.create(email="draw@test.io")
|
||||
self.client.force_login(self.user)
|
||||
# Assign a sig so the view's landing branch (not the gate) renders.
|
||||
self.user.significator = personal_sig_cards(self.user)[0]
|
||||
self.user.save(update_fields=["significator"])
|
||||
|
||||
def test_context_no_equipped_deck_false_when_user_has_deck(self):
|
||||
# post_save auto-equips Earthman; `no_equipped_deck` should be False.
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertFalse(response.context["no_equipped_deck"])
|
||||
|
||||
def test_context_no_equipped_deck_true_when_user_cleared_deck(self):
|
||||
self.user.equipped_deck = None
|
||||
self.user.save(update_fields=["equipped_deck"])
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertTrue(response.context["no_equipped_deck"])
|
||||
|
||||
def test_context_show_backup_intro_banner_when_no_deck_and_has_sig(self):
|
||||
# Brief banner fires when user has a sig AND no deck — they're on the
|
||||
# landing UX (gate passed) but headed for the backup-deck draw path.
|
||||
self.user.equipped_deck = None
|
||||
self.user.save(update_fields=["equipped_deck"])
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertTrue(response.context["show_backup_intro_banner"])
|
||||
|
||||
def test_context_show_backup_intro_banner_false_when_deck_equipped(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertFalse(response.context["show_backup_intro_banner"])
|
||||
|
||||
def test_landing_renders_draw_sea_btn_when_sig_set(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertContains(response, 'id="id_draw_sea_btn"')
|
||||
|
||||
def test_landing_renders_six_chair_seats_with_C_suffix(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
html = response.content.decode()
|
||||
for n in range(1, 7):
|
||||
with self.subTest(slot=n):
|
||||
self.assertIn(f'data-slot="{n}"', html)
|
||||
self.assertIn(f"{n}C", html)
|
||||
|
||||
def test_landing_not_rendered_when_user_has_no_sig(self):
|
||||
# Sprint 4b gate still wins precedence — DRAW SEA must not render
|
||||
# when significator is None.
|
||||
self.user.significator = None
|
||||
self.user.save(update_fields=["significator"])
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertNotContains(response, 'id="id_draw_sea_btn"')
|
||||
|
||||
@@ -170,16 +170,23 @@ def toggle_game_kit_sections(request):
|
||||
def my_sea(request):
|
||||
"""Shell view for the My Sea standalone page.
|
||||
|
||||
Sprint 3 scaffolding + Sprint 4b sign-gate. The gate fires when the
|
||||
user has no saved significator — a Look!-formatted Brief-style line
|
||||
nudges them to /billboard/my-sign/ (FYI) or back to /gameboard/
|
||||
(BACK) before the draw UX can be reached. With a sig set, the draw
|
||||
shell renders normally (gatekeeper / sig-select / sea-select land
|
||||
in Sprints 5-9).
|
||||
Branches three ways:
|
||||
|
||||
1. No sig → Look!-formatted gate w. FYI/NVM (Sprint 4b).
|
||||
2. Sig + equipped deck → DRAW SEA landing (Sprint 5 iter 1) — hex w.
|
||||
6 chair seats labeled 1C-6C + central DRAW SEA btn. Click swaps
|
||||
data-phase to picker (the picker UX itself lands in iter 2).
|
||||
3. Sig + no equipped deck → same landing PLUS a 'Default deck warning'
|
||||
Brief banner identical to the one on /billboard/my-sign/ (the user
|
||||
is headed for a draw against the Earthman [Shabby Cardstock]
|
||||
backup deck unless they equip one first).
|
||||
"""
|
||||
user_has_sig = request.user.significator_id is not None
|
||||
no_equipped_deck = request.user.equipped_deck_id is None
|
||||
return render(request, "apps/gameboard/my_sea.html", {
|
||||
"user_has_sig": user_has_sig,
|
||||
"no_equipped_deck": no_equipped_deck,
|
||||
"show_backup_intro_banner": user_has_sig and no_equipped_deck,
|
||||
"page_class": "page-gameboard page-my-sea",
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user