From cd0add1e3cd0e076f651634cc9c9244b01dc1ee6 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 19 May 2026 01:38:55 -0400 Subject: [PATCH] =?UTF-8?q?My=20Sea=20sign-gate=20=E2=80=94=20Sprint=204b?= =?UTF-8?q?=20of=20My=20Sea=20roadmap=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /gameboard/my-sea/ standalone page + /gameboard/ My Sea applet gated behind User.significator. When no sig is saved, render a Look!-formatted Brief-style line — "Look!—pick your sign before drawing the Sea." — w. BACK (.btn-cancel → /gameboard/) + FYI (.btn-info → /billboard/my-sign/) action buttons in `--terUser` ink ; gate is inline content (not portaled like .note-banner) — it IS the page content until a sig is picked, not a transient nudge ; applet partial mirrors the gate via `{% if not request.user.significator_id %}` w. a `.my-sea-sign-gate--applet` denser variant (just FYI, no BACK — user's already on the gameboard); .my-sea-sign-gate__line shrinks from 1.1rem → 0.85rem + padding from 1.5rem → 0.5rem ; my_sea view passes `user_has_sig = request.user.significator_id is not None` so the standalone template branches at server side (avoids a request.user template-context-processor dependency in the standalone page) ; .woodpecker/main.yaml routes test_game_my_sea.py to the test-FTs-non-room stage by default (FT doesn't yet touch the table hex — Sprint 5+ will bring the hex into my_sea via the same DRY .room-shell stack as my_sign, at which point the file gets moved to test-FTs-room) ; TDD trail — 6 FTs in test_game_my_sea.py covering standalone gate copy + FYI/BACK href targets (T1-T3), with-sig skips gate + renders draw shell (T4), applet mirrors gate w. FYI (T5), applet w. sig falls back to .my-sea-empty (T6); all written red against the un-implemented gate before view/template/SCSS landed ; KNOWN: visual verification on existing admin user (@disco) blocked by lack of a clear-sign affordance (he has a sig saved from Sprint 4a testing); adjacent feature spec'd in [[sprint_my_sea_sign_gate_may19]] memory — likely lands as a CLEAR btn in the picker's saved-sig stage state, deferred to next session Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/gameboard/views.py | 11 +- src/functional_tests/test_game_my_sea.py | 161 ++++++++++++++++++ src/static_src/scss/_gameboard.scss | 44 +++++ .../gameboard/_partials/_applet-my-sea.html | 12 +- src/templates/apps/gameboard/my_sea.html | 25 ++- 5 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 src/functional_tests/test_game_my_sea.py diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 671e303..9f16e16 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -170,11 +170,16 @@ def toggle_game_kit_sections(request): def my_sea(request): """Shell view for the My Sea standalone page. - Sprint 3 scaffolding only — the page will host the gatekeeper / - sig-select / sea-select phase reskin for solo-user draws in later - sprints. For now it just renders the empty skeleton. + 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). """ + user_has_sig = request.user.significator_id is not None return render(request, "apps/gameboard/my_sea.html", { + "user_has_sig": user_has_sig, "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 new file mode 100644 index 0000000..065846b --- /dev/null +++ b/src/functional_tests/test_game_my_sea.py @@ -0,0 +1,161 @@ +"""FTs for the My Sea standalone page sign-gate. + +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 +(→ /gameboard/) instead of the draw UX. The My Sea applet on /gameboard/ +mirrors the gate hint in its empty-state slot. +""" +from selenium.webdriver.common.by import By + +from .base import FunctionalTest +from apps.applets.models import Applet +from apps.epic.models import personal_sig_cards +from apps.lyric.models import User + + +def _seed_gameboard_applets(): + """My Sea + the rest of the gameboard applets so /gameboard/ renders + without missing-applet errors during the applet-side assertions.""" + for slug, name, cols, rows, ctx in [ + ("my-sea", "My Sea", 12, 4, "gameboard"), + ("game-kit", "Game Kit", 4, 6, "gameboard"), + ("my-palette", "My Palette", 4, 4, "gameboard"), + ("my-games", "My Games", 4, 4, "gameboard"), + ]: + Applet.objects.get_or_create( + slug=slug, + defaults={"name": name, "context": ctx, + "default_visible": True, "grid_cols": cols, "grid_rows": rows}, + ) + + +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.""" + + serialized_rollback = True + + def setUp(self): + super().setUp() + _seed_gameboard_applets() + self.email = "sea@test.io" + self.gamer = User.objects.create(email=self.email) + sig_pile = personal_sig_cards(self.gamer) + self.target_card = sig_pile[0] if sig_pile else None + self.assertIsNotNone( + self.target_card, + "personal_sig_cards(user) returned no cards — check Earthman seed", + ) + + # ── Test 1 ─────────────────────────────────────────────────────────────── + + 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.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/gameboard/my-sea/") + gate = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sea-sign-gate" + ) + ) + text = gate.text + self.assertIn("Look!", text) + self.assertIn("pick your sign", text.lower()) + self.assertIn("drawing the Sea", text) + # FYI + BACK action buttons + 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()) + + # ── Test 2 ─────────────────────────────────────────────────────────────── + + def test_gate_fyi_links_to_my_sign_picker(self): + """FYI button is an `` pointing at /billboard/my-sign/.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/gameboard/my-sea/") + fyi = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sea-sign-gate__fyi" + ) + ) + href = fyi.get_attribute("href") or "" + self.assertTrue( + href.endswith("/billboard/my-sign/"), + f"FYI should link to /billboard/my-sign/, got {href!r}", + ) + + # ── Test 3 ─────────────────────────────────────────────────────────────── + + def test_gate_back_links_to_gameboard(self): + """BACK button is an `` pointing at /gameboard/.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/gameboard/my-sea/") + back = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sea-sign-gate__back" + ) + ) + href = back.get_attribute("href") or "" + self.assertTrue( + href.endswith("/gameboard/"), + f"BACK should link to /gameboard/, got {href!r}", + ) + + # ── Test 4 ─────────────────────────────────────────────────────────────── + + def test_with_sig_skips_gate_and_renders_draw_shell(self): + """User w. saved significator → no .my-sea-sign-gate on the page; + draw shell renders normally (Sprint 3 placeholder).""" + self.gamer.significator = self.target_card + self.gamer.save(update_fields=["significator"]) + 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, ".my-sea-page") + ) + self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, ".my-sea-sign-gate")), + 0, + "Gate should not render when user has a saved significator", + ) + + # ── Test 5 ─────────────────────────────────────────────────────────────── + + def test_no_sig_applet_mirrors_gate_with_fyi_link(self): + """On /gameboard/, the My Sea applet's empty state shows the same + Look!-formatted gate w. FYI link to /billboard/my-sign/ when the + user has no significator. Provides a consistent UX across surfaces.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/gameboard/") + applet_gate = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, "#id_applet_my_sea .my-sea-sign-gate" + ) + ) + self.assertIn("Look!", applet_gate.text) + fyi = applet_gate.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate__fyi") + href = fyi.get_attribute("href") or "" + self.assertTrue(href.endswith("/billboard/my-sign/")) + + # ── Test 6 ─────────────────────────────────────────────────────────────── + + def test_with_sig_applet_renders_default_empty_state(self): + """Applet w. saved sig → no gate, empty-state placeholder (until + Sprint 7 wires up the latest-draw rendering).""" + self.gamer.significator = self.target_card + self.gamer.save(update_fields=["significator"]) + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/gameboard/") + self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_my_sea") + ) + self.assertEqual( + len(self.browser.find_elements( + By.CSS_SELECTOR, "#id_applet_my_sea .my-sea-sign-gate" + )), + 0, + ) diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index d9b4e4e..e14cfe4 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -161,3 +161,47 @@ body.page-gameboard { } } } + +// ─── My Sea sign-gate ──────────────────────────────────────────────────────── +// Sprint 4b of [[project-my-sea-roadmap]]. Renders when User.significator +// is None, on both the standalone /gameboard/my-sea/ page AND the +// /gameboard/ My Sea applet. Look!-formatted Brief-style line w. FYI +// (→ /billboard/my-sign/) + BACK (→ /gameboard/) action buttons. Inline +// content (not portaled like .note-banner) — it IS the page content +// until a sig is picked, not a transient nudge. +.my-sea-sign-gate { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 1.5rem; + color: rgba(var(--terUser), 1); + + .my-sea-sign-gate__line { + text-align: center; + font-size: 1.1rem; + line-height: 1.4; + margin: 0; + // --terUser ink mirrors the gate's accent + signals "do this + // first" visually distinct from the body's standard --secUser. + color: rgba(var(--terUser), 1); + } + + .my-sea-sign-gate__actions { + display: flex; + gap: 1rem; + align-items: center; + } + + // Applet variant — denser layout, omits BACK (the user is already on + // the gameboard). Smaller line + just the FYI action surviving. + &.my-sea-sign-gate--applet { + padding: 0.5rem; + gap: 0.5rem; + + .my-sea-sign-gate__line { + font-size: 0.85rem; + } + } +} diff --git a/src/templates/apps/gameboard/_partials/_applet-my-sea.html b/src/templates/apps/gameboard/_partials/_applet-my-sea.html index ea119f7..7dee233 100644 --- a/src/templates/apps/gameboard/_partials/_applet-my-sea.html +++ b/src/templates/apps/gameboard/_partials/_applet-my-sea.html @@ -4,7 +4,17 @@ >

My Sea

- {% if latest_draw_cards %} + {% if not request.user.significator_id %} + {# Sprint 4b applet-gate mirror — same Look!-formatted nudge as #} + {# the standalone page so the UX is consistent across surfaces. #} +
+

+ Look!—pick your sign before drawing the Sea. +

+ FYI +
+ {% elif latest_draw_cards %} {% for card in latest_draw_cards %}
{{ card.corner_rank }} diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html index 736492c..9994a28 100644 --- a/src/templates/apps/gameboard/my_sea.html +++ b/src/templates/apps/gameboard/my_sea.html @@ -6,8 +6,27 @@ {% block content %}
- {# Sprint 3 shell only — gatekeeper / sig-select / sea-select phases #} - {# will land here in later sprints of the My Sea roadmap. #} -

No draws yet—the depths remain unfathomable.

+ {% 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 #} + {# user picks a sign. Inline (not portaled like .note-banner) #} + {# because the gate IS the page content, not a transient nudge. #} +
+

+ Look!—pick your sign before drawing the Sea. +

+
+ BACK + 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.

+ {% endif %}
{% endblock content %}