my_sea_visit: give the visitor a top-left deck stack (owner's deck) with a hover-revealed disabled FLIP — TDD

Mirrors the owner's deck stack onto the spectator hex, DRY:
- new shared _my_sea_deck_stack.html partial (mono DECK / dubbo DECKS by
  is_polarized) rendered by BOTH the owner picker (my_sea.html, flip_disabled=
  hand_complete) AND the visitor cross (flip_disabled=True). Owner markup is
  byte-identical, so its assertions hold.
- the visitor's stack uses the OWNER's deck (everyone at @owner's sea plays the
  owner's deck — the visitor's own equipped deck is irrelevant), pinned
  top-left (--visit) across the table from the owner who deals bottom-right.
- dubbodeck: the Gravity/Levity name flips above the face + upside-down to
  signal someone across the table is dealing.
- the read-only FLIP (disabled ×) is hidden until its stack is hovered/focused,
  then eases in (same opacity 0->1 over 0.3s as the shared flip-btn-base
  reveal; inlined since _gameboard precedes _card-deck in the import order) so a
  permanent × doesn't clutter the stack.

ITs: stack keys on the owner's deck (not the viewer's), dubbo renders 2 named
stacks, FLIP is the disabled state, no stack when the owner has no deck. Owner
deck-stack IT + FT stay green (identical markup).

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 23:01:21 -04:00
parent c3594d27ed
commit 260c1c1325
5 changed files with 126 additions and 29 deletions

View File

@@ -147,6 +147,51 @@ class MySeaVisitContextTest(TestCase):
self.assertIn('data-card-id="1"', content) # owner's drawn card slot
self.assertNotIn("my-sea-scroll", content)
def test_present_renders_owner_deck_stack_top_left_flip_disabled(self):
"""The deck stack mirrors the OWNER's deck (everyone plays the owner's
deck), pinned top-left (--visit) with FLIP always disabled (read-only
spectator can't deal). A polarized owner deck → a dubbodeck."""
from apps.epic.models import DeckVariant
deck = DeckVariant.objects.filter(is_polarized=True).first()
self.assertIsNotNone(deck, "expected a polarized (dubbo) deck seeded")
self.owner.equipped_deck = deck # the OWNER's deck, not the viewer's
self.owner.save(update_fields=["equipped_deck"])
self.invite.token_deposited_at = timezone.now()
self.invite.voice_until = timezone.now() + timedelta(hours=24)
self.invite.save()
html = self.client.get(self.url).content.decode()
self.assertIn("my-sea-stacks-wrap--visit", html)
# Dubbodeck → two named stacks.
self.assertIn("sea-deck-stack--gravity", html)
self.assertIn("sea-deck-stack--levity", html)
# FLIP is the disabled (×) state; never an enabled FLIP for a visitor.
self.assertIn("sea-stack-ok btn-disabled", html)
self.assertNotIn(
'class="btn btn-reveal sea-stack-ok" type="button">FLIP', html)
def test_stack_keys_on_owner_deck_not_viewer_deck(self):
# Viewer has no deck, owner has one → the stack still renders (it's the
# owner's deck in play).
from apps.epic.models import DeckVariant
self.bud.equipped_deck = None
self.bud.save(update_fields=["equipped_deck"])
self.owner.equipped_deck = DeckVariant.objects.first()
self.owner.save(update_fields=["equipped_deck"])
self.invite.token_deposited_at = timezone.now()
self.invite.voice_until = timezone.now() + timedelta(hours=24)
self.invite.save()
html = self.client.get(self.url).content.decode()
self.assertIn("my-sea-stacks-wrap--visit", html)
def test_no_stack_when_owner_has_no_deck(self):
self.owner.equipped_deck = None
self.owner.save(update_fields=["equipped_deck"])
self.invite.token_deposited_at = timezone.now()
self.invite.voice_until = timezone.now() + timedelta(hours=24)
self.invite.save()
html = self.client.get(self.url).content.decode()
self.assertNotIn("my-sea-stacks-wrap--visit", html)
def test_not_present_does_not_render_cross_or_deck_json(self):
# Before deposit (gate view) there's no draw surface to seed.
content = self.client.get(self.url).content.decode()