my-sea spectator Phase B: seat-2C occupancy + visitor token gate + one-shot seated glow + gear BYE — TDD
Phase B of the my-sea invite → spectator → voice blueprint. An ACCEPTED invitee can watch the owner's my-sea read-only, deposit a token to occupy seat 2C (opening a 24h voice window for Phase C), and BYE out. Owner's my_sea.html is left structurally intact — the spectator gets a dedicated, simpler my_sea_visit.html; the read-only draw reuses the existing `latest_draw_slots` payload (no picker surgery). - B1: my_sea_visit(owner_id) spectator view — 403 unless an ACCEPTED SeaInvite(owner, request.user); owner bounced to their own my_sea. Context forces owner-only controls off (sea_btn_active=False, read_only=True); renders the table hex (1C owner / 2C visitor) + owner draw read-only. - B2: visitor gate — my_sea_visit_gate reuses my_sea_gate.html w. a spectator branch (titles the OWNER's Sea, INSERT posts to the visitor endpoint, bud-panel suppressed, gear NVM→visit + BYE). Single-step my_sea_visit_insert_token selects+debits the visitor's token (same priority chain) and records token_deposited_at + a 24h voice_until on the SeaInvite → seat 2C present. Center btn flips GATE VIEW → VIEW DRAW. - B3: spectator gear BYE — my_sea_visit_leave sets status=LEFT, left_at, clears voice_until (frees 2C, ends voice), redirects /gameboard/. _my_sea_gear.html gains a `leave_url`-gated BYE below NVM (owner pages pass no leave_url, so unchanged). - B-seat: one-shot "seated" glow per user-spec 2026-05-27 — new shared apps/gameboard/my-sea-seats.js: on first view (localStorage-gated by a per-occupancy data-seat-token) an occupied seat flares --terUser + --ninUser glow ~1.5s then settles to full-opacity --secUser (.fa-ban already swapped to .fa-circle-check). _room.scss adds .seated / .seat-just-seated + the my-sea-seat-flare keyframes (mirrors the room's .active→.role-confirmed handoff). Wired on BOTH the spectator page (load) and the owner page (load + on the FREE DRAW seat-1 transition). MySeaSeatsSpec.js Jasmine spec covers the gating + timed class removal. - B5: MySeaSpectatorFlowTest FT — accept → visit → GATE VIEW → deposit → VIEW DRAW + seat 2C seated. URLs: my-sea/visit/<uuid:owner_id>/ (+ /gate/, /insert, /leave). 470 IT/UT green; spectator FT + full Jasmine suite green. Phase C (WebRTC mesh voice + coturn droplet) next — the 24h voice_until window set here drives it. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Look!-formatted Brief-style line w. FYI (→ /billboard/my-sign/) + NVM
|
||||
(→ /gameboard/) instead of the draw UX. The My Sea applet on /gameboard/
|
||||
mirrors the gate hint in its empty-state slot.
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
@@ -1891,6 +1892,61 @@ class MySeaInviteAcceptanceLogTest(FunctionalTest):
|
||||
self.assertEqual(bye.text.upper(), "BYE")
|
||||
|
||||
|
||||
class MySeaSpectatorFlowTest(FunctionalTest):
|
||||
"""Phase B of [[my-sea-invite-voice-blueprint]] — an ACCEPTED invitee
|
||||
visits the owner's my-sea, deposits a token at the visitor gate, and
|
||||
thereby occupies seat 2C (the center btn flips GATE VIEW → VIEW DRAW)."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.browser.set_window_size(800, 1200) # portrait — center-hex clicks
|
||||
_seed_gameboard_applets()
|
||||
from apps.gameboard.models import MySeaDraw, SeaInvite
|
||||
self.owner = User.objects.create(email="owner@test.io", username="discoman")
|
||||
# Owner has drawn → seat 1C is occupied + there's a draw to view.
|
||||
MySeaDraw.objects.create(
|
||||
user=self.owner, spread="situation-action-outcome",
|
||||
significator_id=1,
|
||||
hand=[{"position": "lay", "card_id": 1, "reversed": False,
|
||||
"polarity": "gravity"}],
|
||||
)
|
||||
self.email = "bud@test.io"
|
||||
self.bud = User.objects.create(email=self.email, username="budster")
|
||||
self.invite = SeaInvite.objects.create(
|
||||
owner=self.owner, invitee=self.bud, invitee_email=self.email,
|
||||
status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
|
||||
)
|
||||
|
||||
def test_spectator_deposits_token_to_occupy_seat_2c(self):
|
||||
from django.urls import reverse
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(
|
||||
self.live_server_url + reverse("my_sea_visit", args=[self.owner.id])
|
||||
)
|
||||
# Before deposit: GATE VIEW shown, seat 2C not yet seated.
|
||||
gate_btn = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_my_sea_gate_view_btn")
|
||||
)
|
||||
seat2 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat[data-slot='2']"
|
||||
)
|
||||
self.assertNotIn("seated", seat2.get_attribute("class"))
|
||||
# GATE VIEW → visitor gate → INSERT TOKEN (single-step deposit).
|
||||
gate_btn.click()
|
||||
insert = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-rails")
|
||||
)
|
||||
insert.click()
|
||||
# Back on the visit page: VIEW DRAW now shown, seat 2C seated.
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_my_sea_view_draw_btn")
|
||||
)
|
||||
seat2 = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat[data-slot='2']"
|
||||
)
|
||||
self.assertIn("seated", seat2.get_attribute("class"))
|
||||
|
||||
|
||||
class MySeaGearBtnTest(FunctionalTest):
|
||||
"""Sprint 6 iter 6c — `.gear-btn` on every my-sea page state
|
||||
(landing / picker / gatekeeper). Opens a NVM-only menu (DEL/BYE
|
||||
|
||||
Reference in New Issue
Block a user