diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index e6e4f34..fa80d52 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -1116,26 +1116,45 @@ class MySeaViewWithSavedDrawTest(TestCase): self.assertNotIn("my-sea-sign-gate", html) self.assertIn('data-phase="picker"', html) - def test_view_renders_brief_banner_when_active_draw_exists(self): + def test_view_triggers_brief_banner_when_active_draw_exists(self): + # Brief is rendered client-side via Brief.showBanner (standard + # `.note-banner` w. Gaussian-glass bg, portaled atop the h2 — + # same UX as my-notes / my-sign default-deck-warning Briefs). + # Server emits a `window._showFreeDrawLockedBrief("")` call + # gated on active_draw; ISO timestamp (`|date:'c'`) is re-used + # as both `created_at` AND the source for the human-formatted + # display string note.js renders in the `.note-banner__timestamp` + # slot — single source of truth, no "Invalid Date" on bad input. response = self.client.get(reverse("my_sea")) - self.assertContains(response, "my-sea-brief") - self.assertContains(response, "my-sea-brief__timestamp") - self.assertContains(response, "my-sea-brief__nvm") + # Match the call form w. opening quote — the bare token + # `_showFreeDrawLockedBrief(` also appears in the function + # definition emitted unconditionally inside the picker IIFE. + self.assertContains(response, 'window._showFreeDrawLockedBrief("') + # The ISO format produced by Django's `|date:'c'` starts with the + # full year + ISO-style T separator — pin a representative token. + from django.utils import timezone + from datetime import timedelta + expected_year = (timezone.now() + timedelta(hours=24)).strftime("%Y") + self.assertContains(response, '_showFreeDrawLockedBrief("' + expected_year) - def test_brief_banner_hidden_without_active_draw(self): - # Markup is rendered unconditionally so JS can un-hide it on LOCK - # HAND POST success without a page reload. When no active_draw, - # the wrapping div carries `[hidden]` so the banner is invisible. + def test_view_does_not_trigger_brief_banner_without_active_draw(self): + # Definition of `_showFreeDrawLockedBrief` is always emitted; + # only the CALL is gated on active_draw. Pin the call form. from apps.gameboard.models import MySeaDraw MySeaDraw.objects.all().delete() response = self.client.get(reverse("my_sea")) - self.assertContains(response, ' - {# Iter 4b — Look!-formatted Brief banner above the picker. #} - {# Always rendered when the picker is rendered, but hidden #} - {# unless a saved draw occupies the user's free-quota slot #} - {# (server) OR LOCK HAND just fired (client un-hides on the #} - {# fetch response w. the next-free-draw timestamp). Avoiding #} - {# a full page reload on LOCK lets the iter-4a FTs keep their #} - {# picker element refs valid post-lock. #} -
-

- Look!—your free draw is locked in for the next 24 hours. Next free draw available at - . -

- -
- - {# Iter 4b — DEL guard portal. Uniform 'Are you sure?' copy #} - {# regardless of quota state (the Brief banner above carries #} - {# the quota-specific info). Always rendered (hidden by #} - {# default); DEL click un-hides when picker is `_locked`. #} - {# CONFIRM POSTs to /gameboard/my-sea/delete; NVM dismisses. #} - + {# Iter 4b — DEL guard reuses the shared `#id_guard_portal` #} + {# from base.html (the same one the room's gear-menu DEL btn #} + {# uses). Gaussian-glass tooltip positioned above the DEL btn,#} + {# no backdrop. The picker IIFE below invokes it via #} + {# `window.showGuard(delBtn, "Are you sure?", confirmFn, null,#} + {# {yesLabel: "DEL"})` when DEL is clicked post-lock. #} {# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #} {# sig excluded) embedded as JSON; JS reads on init and #} {# pops from the relevant pile on each deposit. #} @@ -492,19 +465,39 @@ // ── DEL semantics differ by lock state ────────────────── // Pre-lock: DEL resets the in-progress hand client-side // (iter 4a behaviour — no server round-trip). - // Post-lock: DEL opens `#id_my_sea_del_portal` guard portal - // (iter 4b). The portal CONFIRM POSTs to - // /gameboard/my-sea/delete; NVM closes the - // portal. + // Post-lock: DEL invokes the shared `#id_guard_portal` + // from base.html via `window.showGuard`, w. a + // "DEL" YES-label override (the room's gear- + // menu DEL flow uses the same portal). On YES + // we POST to /gameboard/my-sea/delete then + // navigate back to the page (server returns + // 204; we redirect manually to land on the + // FREE DRAW landing). if (delBtn) { delBtn.addEventListener('click', function (e) { e.stopPropagation(); - var portal = document.getElementById('id_my_sea_del_portal'); - if (_locked && portal) { - portal.hidden = false; + if (!_locked) { + _resetHand(); return; } - _resetHand(); + if (!window.showGuard) return; + // Trigger btn (DEL, `.btn-danger`) opens the shared + // guard portal; the portal's confirm button is the + // standard `.btn-confirm` "OK" + `.btn-cancel` "NVM" + // pair — matches the room gear-menu DEL flow exactly. + window.showGuard( + delBtn, + 'Are you sure?', + function () { + fetch('{% url "my_sea_delete" %}', { + method: 'POST', + credentials: 'same-origin', + headers: {'X-CSRFToken': _csrf()}, + }).then(function (r) { + if (r.ok) window.location.reload(); + }); + } + ); }); } if (lockBtn) { @@ -538,11 +531,8 @@ return m ? decodeURIComponent(m[1]) : ''; } function _formatTimestamp(iso) { - // Mirror the server-side `D, M j @ g:i A` format used in - // the template's pre-rendered next-free-draw timestamp - // (e.g., "Thu, May 21 @ 2:41 AM"). Keeps the post-LOCK - // visual consistent with a fresh-page-load saved-draw - // render. + // Mirror the server-side `D, M j @ g:i A` format + // (e.g., "Thu, May 21 @ 2:41 AM"). var d = new Date(iso); if (isNaN(d)) return ''; var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; @@ -556,16 +546,40 @@ return DAYS[d.getDay()] + ', ' + MONTHS[d.getMonth()] + ' ' + d.getDate() + ' @ ' + h + ':' + mm + ' ' + ampm; } - function _revealBrief(nextFreeDrawIso) { - var brief = document.querySelector('.my-sea-brief'); - if (!brief) return; - var ts = brief.querySelector('.my-sea-brief__timestamp'); - if (ts && nextFreeDrawIso) { - ts.setAttribute('datetime', nextFreeDrawIso); - ts.textContent = _formatTimestamp(nextFreeDrawIso); + window._showFreeDrawLockedBrief = function (iso) { + // Standard Brief banner — portaled atop the h2 w. + // Gaussian-glass bg (see [[note.js]] showBanner). The + // next-free-draw moment is passed as an ISO string + + // re-used as `created_at` so note.js's `