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:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user