my-sea spectator: render owner's draw as the identical interactive cross stage; owner seated in 1C when paid OR drawn — TDD

Phase 1 + 2 of the my-sea spectator/voice batch (user-spec 2026-05-29).

── Phase 1: spectator VIEW DRAW parity ──
The visitor's VIEW DRAW rendered _my_sea_readonly_draw.html — a flat
`.my-sea-scroll` strip that, out of its applet context, blew a single card up
to fill the viewport. It now renders the SAME `.my-sea-cross` picker +
`_sea_stage` modal the owner sees, populated from the owner's draw, read-only
but fully interactive (click card → magnified stage, hover, SPIN, FYI). No
FLIP / DEL / AUTO DRAW / deck-stacks / spread combobox — the visitor watches.

- `_saved_by_position(saved_hand)` extracted as a shared helper (owner picker
  + spectator render build the IDENTICAL cross); my_sea refactored onto it.
- my_sea_visit context gains `saved_by_position`, `label_by_position`,
  `default_spread`, and the OWNER's `sea_deck_data` (so sea.js resolves each
  clicked slot's full card face for the stage).
- new `_my_sea_visit_cross.html` mirrors the owner cross + includes `_sea_stage`
  under `#id_sea_overlay`; my_sea_visit.html embeds the owner deck JSON + loads
  stage-card.js + sea.js + a trimmed seed IIFE (reconstructs SeaDeal's
  `_seaHand` from the filled slots so each card is clickable into the stage).
- deletes the obsolete `_my_sea_readonly_draw.html`.

── Phase 2: owner 1C seating ──
The owner is "seated" in 1C whenever committed to a draw cycle — paid for one
(deposit reserved / paid-through credit) OR partially/completely drawn — not
only once a card lands. Previously a paid-but-undrawn owner (the PAID DRAW
landing) and the visitor's view of her showed the semi-opaque `.fa-ban`
default. Seat 1C now carries persistent `.seated` + `.fa-circle-check`
(sync on refresh; the one-shot flare just settles into it).

- my_sea: new `seat1_seated = hand_non_empty or show_paid_draw`; my_sea.html
  seat 1C keys on it (class + data-seat-token + status icon).
- my_sea_visit: `seat1_present = owner drawn OR owner paid` so the visitor
  sees the owner seated on the spectator hex under the same conditions.
- seat flare bumped 1.5s → 2s (my-sea-seats.js GLOW_MS + _room.scss keyframe).

Tests: +2 spectator-cross ITs, +1 spectator-cross FT (Phase 1); +4 owner-seat
ITs, +2 visitor both-seated/owner-seated ITs, +1 owner-seating FT (Phase 2).
286 gameboard ITs/UTs green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-29 20:48:31 -04:00
parent 1ac380dfc5
commit 7bd8e3256a
11 changed files with 402 additions and 80 deletions

View File

@@ -2053,3 +2053,93 @@ class MySeaGearBtnTest(FunctionalTest):
self.browser.current_url, r"/gameboard/my-sea/$"
)
)
class MySeaOwnerSeatingTest(FunctionalTest):
"""Phase 2 (2026-05-29) — the owner's 1C chair stays persistently seated
(`.seated` + green `.fa-circle-check`, NOT the semi-opaque `.fa-ban`
default) whenever she's committed to a draw cycle, including a PAID DRAW
landing before any card lands. Previously only a non-empty hand seated 1C,
so a paid-but-undrawn owner showed the empty-seat default."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
_seed_gameboard_applets()
self.email = "seated@test.io"
self.gamer = User.objects.create(email=self.email)
_assign_sig(self.gamer)
def _paid_empty_draw(self):
from apps.gameboard.models import MySeaDraw
return MySeaDraw.objects.create(
user=self.gamer, spread="situation-action-outcome",
significator_id=self.gamer.significator_id, hand=[],
deposit_token_id=99, deposit_reserved_at=timezone.now(),
)
def test_paid_draw_landing_seats_owner_in_1c_persistently(self):
self._paid_empty_draw()
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
seat1 = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-seat[data-slot='1']"
))
# Seated (not the empty default) + the green check — and STAYS so
# after the 2s flare settles (the steady state is server-rendered).
self.assertIn("seated", (seat1.get_attribute("class") or "").split())
self.assertTrue(seat1.find_elements(
By.CSS_SELECTOR, ".position-status-icon.fa-circle-check"
))
self.assertFalse(seat1.find_elements(
By.CSS_SELECTOR, ".position-status-icon.fa-ban"
))
class MySeaVisitSpectatorCrossTest(FunctionalTest):
"""Phase 1 (2026-05-29) — the visitor's VIEW DRAW renders the owner's draw
as the SAME interactive cross stage the owner sees (`.my-sea-cross` +
`_sea_stage`), NOT the old flat scroll that blew a single card up to fill
the viewport."""
def setUp(self):
super().setUp()
_seed_earthman_sig_pile()
_seed_gameboard_applets()
self.owner = User.objects.create(email="owner@test.io", username="owner")
_assign_sig(self.owner)
self.visitor_email = "viz@test.io"
self.visitor = User.objects.create(
email=self.visitor_email, username="viz")
from apps.epic.models import TarotCard
from apps.gameboard.models import MySeaDraw, SeaInvite
card = TarotCard.objects.exclude(id=self.owner.significator_id).first()
MySeaDraw.objects.create(
user=self.owner, spread="situation-action-outcome",
significator_id=self.owner.significator_id,
hand=[{"position": "lay", "card_id": card.id, "reversed": False,
"polarity": "gravity"}],
)
SeaInvite.objects.create(
owner=self.owner, invitee=self.visitor,
invitee_email=self.visitor_email,
status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
token_deposited_at=timezone.now(),
voice_until=timezone.now() + timedelta(hours=24),
)
def test_view_draw_reveals_interactive_cross_with_filled_slot(self):
self.create_pre_authenticated_session(self.visitor_email)
self.browser.get(
self.live_server_url + f"/gameboard/my-sea/visit/{self.owner.id}/")
view_btn = self.wait_for(lambda: self.browser.find_element(
By.ID, "id_my_sea_view_draw_btn"))
self.browser.execute_script("arguments[0].click()", view_btn)
cross = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-cross"))
self.assertTrue(cross.is_displayed())
self.assertTrue(self.browser.find_elements(
By.CSS_SELECTOR, ".sea-card-slot--filled"))
self.assertTrue(self.browser.find_elements(By.ID, "id_sea_stage"))
self.assertFalse(self.browser.find_elements(
By.CSS_SELECTOR, ".my-sea-scroll"))