diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index b4f795e..a02182b 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -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"') diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 9f16e16..13b6a28 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -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", }) diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py index 802845a..2f3df7c 100644 --- a/src/functional_tests/test_game_my_sea.py +++ b/src/functional_tests/test_game_my_sea.py @@ -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 `` pointing at /gameboard/.""" + """NVM button is an `` 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, + ) diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index e14cfe4..a5a6192 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -194,7 +194,7 @@ body.page-gameboard { align-items: center; } - // Applet variant — denser layout, omits BACK (the user is already on + // Applet variant — denser layout, omits NVM (the user is already on // the gameboard). Smaller line + just the FYI action surviving. &.my-sea-sign-gate--applet { padding: 0.5rem; @@ -205,3 +205,54 @@ body.page-gameboard { } } } + +// ─── My Sea DRAW SEA landing ───────────────────────────────────────────────── +// Sprint 5 iter 1 of [[project-my-sea-roadmap]]. When a user has a saved +// significator (gate passed), /gameboard/my-sea/ renders this landing +// screen: DRY table hex w. 6 chair seats labeled 1C-6C + central DRAW +// SEA btn. Mirrors my-sign's `.my-sign-page` + `.my-sign-landing` +// structure — same room-shell chain so room.js's scaleTable() can size +// the hex; same flex setup so the container chain propagates real +// height down for the scale calc. +.my-sea-page { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + position: relative; +} + +.my-sea-landing { + flex: 1; + min-height: 0; + display: flex; + + // DRAW SEA btn — centered in the hex, mirrors SCAN SIGN's 2-line + // font sizing so "DRAW/SEA" sits cleanly inside the 4rem circle. + #id_draw_sea_btn { + font-size: 0.75rem; + line-height: 1.1; + white-space: normal; + } + + // Chair-seat labels (1C-6C) sit beside the chair icon. The room's + // .table-seat positioning by data-slot (1-6) already places them + // around the hex — this just sizes the label readably + colors it + // to match the gate's --terUser accent. + .table-seat .seat-label { + font-size: 0.8rem; + font-weight: 600; + color: rgba(var(--terUser), 1); + margin-left: 0.25rem; + } +} + +.my-sea-picker { + // Iter-1 placeholder — iter 2 will populate w. the three-card cross + // (sig in center + cover/leave/loom) on a --duoUser background. + flex: 1; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html index 9994a28..0f86739 100644 --- a/src/templates/apps/gameboard/my_sea.html +++ b/src/templates/apps/gameboard/my_sea.html @@ -5,11 +5,11 @@ {% block header_text %}GameSea{% endblock header_text %} {% block content %} -
+
{% if not user_has_sig %} {# Sprint 4b sign-gate. The draw UX is gated behind a saved #} {# significator — render a Look!-formatted Brief-style line w. #} - {# FYI (→ /billboard/my-sign/) + BACK (→ /gameboard/) until the #} + {# FYI (→ /billboard/my-sign/) + NVM (→ /gameboard/) until the #} {# user picks a sign. Inline (not portaled like .note-banner) #} {# because the gate IS the page content, not a transient nudge. #}
@@ -18,15 +18,95 @@

BACK + href="{% url 'gameboard' %}">NVM FYI
{% else %} - {# Sprint 3 shell — gatekeeper / sig-select / sea-select phases #} - {# will land here in later sprints of the My Sea roadmap. #} -

No draws yet—the depths remain unfathomable.

+ {# Sprint 5 iter 1 — DRAW SEA landing UX. DRY table hex from #} + {# the room shell (.room-shell > .room-table > … > .table-hex) #} + {# w. 6 chair seats labeled 1C-6C as placeholders for the #} + {# friend-invite feature per the My Sea roadmap architectural #} + {# anchor "Six chairs retained even in solo". DRAW SEA btn #} + {# mirrors SCAN SIGN on /billboard/my-sign/. #} +
+
+
+
+
+
+
+ +
+
+
+ {% for n in "123456" %} +
+ + {{ n }}C +
+ {% endfor %} +
+
+
+
+ + {# Picker phase placeholder — iter 2 wires up the three-card #} + {# cross layout (sig in center + cover/leave/loom) w. the #} + {# --duoUser bg + form col (spread dropdown / decks / LOCK #} + {# HAND / DEL). For iter 1 it's just a phase-swap target. #} + + + + + + {# Brief 'Default deck warning' banner — lifted verbatim from #} + {# /billboard/my-sign/'s no-equipped-deck path. Same copy, #} + {# same FYI (→ /gameboard/) + NVM (dismiss + proceed) actions.#} + {# Tagged w. .my-sea-intro-banner so FTs disambiguate from #} + {# any other Briefs on the page. #} + + {% if show_backup_intro_banner %} + + {% endif %} {% endif %}
{% endblock content %}