My Sea sign-gate — Sprint 4b of My Sea roadmap — TDD
/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 <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -170,11 +170,16 @@ def toggle_game_kit_sections(request):
|
|||||||
def my_sea(request):
|
def my_sea(request):
|
||||||
"""Shell view for the My Sea standalone page.
|
"""Shell view for the My Sea standalone page.
|
||||||
|
|
||||||
Sprint 3 scaffolding only — the page will host the gatekeeper /
|
Sprint 3 scaffolding + Sprint 4b sign-gate. The gate fires when the
|
||||||
sig-select / sea-select phase reskin for solo-user draws in later
|
user has no saved significator — a Look!-formatted Brief-style line
|
||||||
sprints. For now it just renders the empty skeleton.
|
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", {
|
return render(request, "apps/gameboard/my_sea.html", {
|
||||||
|
"user_has_sig": user_has_sig,
|
||||||
"page_class": "page-gameboard page-my-sea",
|
"page_class": "page-gameboard page-my-sea",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
161
src/functional_tests/test_game_my_sea.py
Normal file
161
src/functional_tests/test_game_my_sea.py
Normal file
@@ -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 `<a href>` 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 `<a href>` 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,
|
||||||
|
)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,17 @@
|
|||||||
>
|
>
|
||||||
<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>
|
<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>
|
||||||
<div class="my-sea-scroll">
|
<div class="my-sea-scroll">
|
||||||
{% 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. #}
|
||||||
|
<div class="my-sea-sign-gate my-sea-sign-gate--applet">
|
||||||
|
<p class="my-sea-sign-gate__line">
|
||||||
|
Look!—pick your sign before drawing the Sea.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-info my-sea-sign-gate__fyi"
|
||||||
|
href="{% url 'billboard:my_sign' %}">FYI</a>
|
||||||
|
</div>
|
||||||
|
{% elif latest_draw_cards %}
|
||||||
{% for card in latest_draw_cards %}
|
{% for card in latest_draw_cards %}
|
||||||
<div class="my-sea-card" data-position="{{ card.position }}">
|
<div class="my-sea-card" data-position="{{ card.position }}">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
|
|||||||
@@ -6,8 +6,27 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="my-sea-page">
|
<div class="my-sea-page">
|
||||||
{# Sprint 3 shell only — gatekeeper / sig-select / sea-select phases #}
|
{% if not user_has_sig %}
|
||||||
{# will land here in later sprints of the My Sea roadmap. #}
|
{# Sprint 4b sign-gate. The draw UX is gated behind a saved #}
|
||||||
<p class="my-sea-page__empty">No draws yet—the depths remain unfathomable.</p>
|
{# 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. #}
|
||||||
|
<div class="my-sea-sign-gate">
|
||||||
|
<p class="my-sea-sign-gate__line">
|
||||||
|
Look!—pick your sign before drawing the Sea.
|
||||||
|
</p>
|
||||||
|
<div class="my-sea-sign-gate__actions">
|
||||||
|
<a class="btn btn-cancel my-sea-sign-gate__back"
|
||||||
|
href="{% url 'gameboard' %}">BACK</a>
|
||||||
|
<a class="btn btn-info my-sea-sign-gate__fyi"
|
||||||
|
href="{% url 'billboard:my_sign' %}">FYI</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{# Sprint 3 shell — gatekeeper / sig-select / sea-select phases #}
|
||||||
|
{# will land here in later sprints of the My Sea roadmap. #}
|
||||||
|
<p class="my-sea-page__empty">No draws yet—the depths remain unfathomable.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
Reference in New Issue
Block a user