my-sea gear NVM: gatekeeper + picker nav-back to table hex instead of ejecting to gameboard — TDD
User-spec'd 2026-05-26: the gear menu's NVM was always nav-backing to /gameboard/ from every my-sea state, which ejects the user one step further than they want when bailing on the gatekeeper or the spread/crucifix picker. Both should nav-back to the my-sea TABLE HEX (the landing) — one step back, not all the way out.
## templates/apps/gameboard/_partials/_my_sea_gear.html
Now takes an optional `nvm_url` context variable; falls back to `{% url 'gameboard' %}` when unset. The onclick handler reads `{{ nvm_url }}` so callers can override per-page.
## templates/apps/gameboard/my_sea.html
```
{% if show_picker %}{% url 'my_sea' as nvm_url %}{% endif %}
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
```
Picker phase → nvm_url = my_sea landing (table hex). Sign-gate + landing phases fall through to the default (gameboard) — landing's NVM = "back out of my-sea entirely", which is the existing behaviour.
## templates/apps/gameboard/my_sea_gate.html
```
{% url 'my_sea' as nvm_url %}
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
```
Gatekeeper unconditionally nav-backs to the my-sea landing.
## Tests
`apps/gameboard/tests/integrated/test_views.py`:
- `MySeaViewTest.test_gear_nvm_navs_to_gameboard_on_landing_phase` — landing NVM still goes to /gameboard/ (regression guard).
- `MySeaViewTest.test_gear_nvm_navs_to_my_sea_landing_on_picker_phase` — picker NVM goes to /gameboard/my-sea/ (seeds a non-empty hand to trigger show_picker per views.py:277's `hand_non_empty or (phase_param and show_paid_draw)` logic).
- `MySeaGateViewTest.test_gear_nvm_navs_to_my_sea_landing_not_gameboard` — gatekeeper NVM goes to /gameboard/my-sea/ (and explicitly NOT /gameboard/).
## Verification
All 1364 IT+UT green. No FTs assert the gear-menu NVM target URL (per pre-change grep) so no test fixes required there.
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:
@@ -1037,6 +1037,31 @@ class MySeaViewTest(TestCase):
|
|||||||
self.assertIn('id="id_bud_btn"', body)
|
self.assertIn('id="id_bud_btn"', body)
|
||||||
self.assertIn('id="id_burger_btn"', body)
|
self.assertIn('id="id_burger_btn"', body)
|
||||||
|
|
||||||
|
def test_gear_nvm_navs_to_gameboard_on_landing_phase(self):
|
||||||
|
"""Landing-phase NVM = "back out of my-sea entirely" → /gameboard/."""
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertIn("location.href='/gameboard/'", response.content.decode())
|
||||||
|
|
||||||
|
def test_gear_nvm_navs_to_my_sea_landing_on_picker_phase(self):
|
||||||
|
"""Picker-phase NVM = "back out of the spread to the table hex" →
|
||||||
|
/gameboard/my-sea/ (the landing). User-spec'd 2026-05-26 so the
|
||||||
|
spread/crucifix exit doesn't eject all the way to /gameboard/.
|
||||||
|
Picker phase triggers on a non-empty hand (see views.py:277)."""
|
||||||
|
from apps.epic.models import personal_sig_cards, TarotCard
|
||||||
|
from apps.gameboard.models import MySeaDraw
|
||||||
|
sig = personal_sig_cards(self.user)[0]
|
||||||
|
self.user.significator = sig
|
||||||
|
self.user.save(update_fields=["significator"])
|
||||||
|
card = TarotCard.objects.exclude(pk=sig.pk).first()
|
||||||
|
MySeaDraw.objects.create(
|
||||||
|
user=self.user, spread="situation-action-outcome",
|
||||||
|
significator_id=sig.id,
|
||||||
|
hand=[{"position": "lay", "card_id": card.id,
|
||||||
|
"reversed": False, "polarity": "gravity"}],
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertIn("location.href='/gameboard/my-sea/'", response.content.decode())
|
||||||
|
|
||||||
def test_sea_stage_stat_block_renders_rank_suit_chip_per_face(self):
|
def test_sea_stage_stat_block_renders_rank_suit_chip_per_face(self):
|
||||||
"""Sprint A.7.5 — `_sea_stage.html` modal scaffold (included from
|
"""Sprint A.7.5 — `_sea_stage.html` modal scaffold (included from
|
||||||
my_sea-picker-phase + the gameroom sea overlay) carries the new
|
my_sea-picker-phase + the gameroom sea overlay) carries the new
|
||||||
@@ -2189,6 +2214,15 @@ class MySeaGateViewTest(TestCase):
|
|||||||
self.assertContains(response, "id_my_sea_paid_draw_btn")
|
self.assertContains(response, "id_my_sea_paid_draw_btn")
|
||||||
self.assertContains(response, reverse("my_sea_refund_token"))
|
self.assertContains(response, reverse("my_sea_refund_token"))
|
||||||
|
|
||||||
|
def test_gear_nvm_navs_to_my_sea_landing_not_gameboard(self):
|
||||||
|
"""Gatekeeper NVM = "back out of the gate to the table hex" →
|
||||||
|
/gameboard/my-sea/ (the landing), NOT /gameboard/. User-spec'd
|
||||||
|
2026-05-26 so the gatekeeper exit lands one step back."""
|
||||||
|
response = self.client.get(reverse("my_sea_gate"))
|
||||||
|
body = response.content.decode()
|
||||||
|
self.assertIn("location.href='/gameboard/my-sea/'", body)
|
||||||
|
self.assertNotIn("location.href='/gameboard/'", body)
|
||||||
|
|
||||||
def test_gate_view_renders_burger_btn_and_fan(self):
|
def test_gate_view_renders_burger_btn_and_fan(self):
|
||||||
response = self.client.get(reverse("my_sea_gate"))
|
response = self.client.get(reverse("my_sea_gate"))
|
||||||
self.assertContains(response, 'id="id_burger_btn"')
|
self.assertContains(response, 'id="id_burger_btn"')
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{# Sprint 6 iter 6c — gear-btn on /gameboard/my-sea/ + the gatekeeper. #}
|
{# Sprint 6 iter 6c — gear-btn on /gameboard/my-sea/ + the gatekeeper. #}
|
||||||
{# NVM-only menu (no DEL, no BYE) — gear is the "back out without #}
|
{# NVM-only menu (no DEL, no BYE) — gear is the "back out without #}
|
||||||
{# committing" affordance; NVM nav-backs to /gameboard/ mirroring the #}
|
{# committing" affordance. Caller passes `nvm_url` to control where #}
|
||||||
{# room's gear-menu convention. Rendered unconditionally (no active- #}
|
{# NVM nav-backs to: my_sea landing (table hex) from gatekeeper + #}
|
||||||
{# draw guard) so fresh users + post-DEL states still see it. #}
|
{# picker; falls back to /gameboard/ otherwise. Render unconditionally #}
|
||||||
{# `<button>` not `<a class="btn">` — `.btn` doesn't reset font-family, so #}
|
{# (no active-draw guard) so fresh users + post-DEL states still see it.#}
|
||||||
{# the anchor inherits body's serif font (the Brief's NVM uses `<button>` #}
|
{# `<button>` not `<a class='btn'>` — `.btn` doesn't reset font-family, #}
|
||||||
{# + reads correctly as sans-serif). Inline `onclick` preserves the nav #}
|
{# so the anchor inherits body's serif font (the Brief's NVM uses #}
|
||||||
{# semantics the href had. See [[feedback-btn-vs-anchor-font-family]]. #}
|
{# `<button>` + reads correctly as sans-serif). #}
|
||||||
|
{# See [[feedback-btn-vs-anchor-font-family]]. #}
|
||||||
|
{% if not nvm_url %}{% url 'gameboard' as nvm_url %}{% endif %}
|
||||||
<div id="id_my_sea_menu" style="display:none">
|
<div id="id_my_sea_menu" style="display:none">
|
||||||
<button type="button" class="btn btn-cancel" onclick="location.href='{% url 'gameboard' %}'">NVM</button>
|
<button type="button" class="btn btn-cancel" onclick="location.href='{{ nvm_url }}'">NVM</button>
|
||||||
</div>
|
</div>
|
||||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}
|
||||||
|
|||||||
@@ -1062,8 +1062,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Sprint 6 iter 6c — gear-btn lives on every my-sea page state #}
|
{# Sprint 6 iter 6c — gear-btn lives on every my-sea page state #}
|
||||||
{# (sign-gate / landing / picker). NVM-only menu mirrors the #}
|
{# (sign-gate / landing / picker). NVM-only menu. Picker NVM #}
|
||||||
{# gatekeeper's gear; "back out to /gameboard/" affordance. #}
|
{# nav-backs to the my-sea TABLE HEX (landing), letting the user #}
|
||||||
|
{# leave the spread without committing. Sign-gate + landing NVM #}
|
||||||
|
{# nav-backs to /gameboard/ (the partial's default fallback). #}
|
||||||
|
{% if show_picker %}{% url 'my_sea' as nvm_url %}{% endif %}
|
||||||
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
||||||
{# Bud + burger persist across every stage too — same affordance #}
|
{# Bud + burger persist across every stage too — same affordance #}
|
||||||
{# as on the my-sea gatekeeper, so the user can invite + reach #}
|
{# as on the my-sea gatekeeper, so the user can invite + reach #}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@
|
|||||||
{# bud-btn lives at viewport fixed position (per `_bud.scss`) and #}
|
{# bud-btn lives at viewport fixed position (per `_bud.scss`) and #}
|
||||||
{# the gear-btn sits atop `#id_kit_btn` in the bottom-right corner. #}
|
{# the gear-btn sits atop `#id_kit_btn` in the bottom-right corner. #}
|
||||||
{% include "apps/gameboard/_partials/_my_sea_bud_panel.html" %}
|
{% include "apps/gameboard/_partials/_my_sea_bud_panel.html" %}
|
||||||
|
{# Gatekeeper NVM nav-backs to the my-sea TABLE HEX (landing) so the #}
|
||||||
|
{# user lands one step back from the gatekeeper rather than ejecting #}
|
||||||
|
{# all the way to /gameboard/. #}
|
||||||
|
{% url 'my_sea' as nvm_url %}
|
||||||
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
||||||
{% include "apps/gameboard/_partials/_burger.html" %}
|
{% include "apps/gameboard/_partials/_burger.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user