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 %}