From ff3e4d295c342e43d6423aea4f4c50c00b5f601b Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 28 Apr 2026 22:50:39 -0400 Subject: [PATCH] =?UTF-8?q?PICK=20SEA=20Sprint=20B=20FTs:=20deck=20stacks,?= =?UTF-8?q?=20OK=20btn,=20card=20draw,=20LOCK=20HAND/DEL=20=E2=80=94=20red?= =?UTF-8?q?=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9 tests in PickSeaDealTest: DEAL btn absent; LOCK HAND present+disabled; DEL present; two deck stacks (.sea-deck-stack--levity/gravity); stack click shows .sea-stack-ok; elsewhere hides it; OK click fills .sea-pos-cover; 6 draws enables LOCK HAND; DEL clears .sea-card-slot--filled positions. All 9 fail red — no implementation yet. Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/functional_tests/test_room_sea_select.py | 208 ++++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/src/functional_tests/test_room_sea_select.py b/src/functional_tests/test_room_sea_select.py index 3903069..37b12ff 100644 --- a/src/functional_tests/test_room_sea_select.py +++ b/src/functional_tests/test_room_sea_select.py @@ -1,10 +1,12 @@ """Functional tests for the PICK SEA overlay — Celtic Cross draw.""" from django.urls import reverse +from django.utils import timezone from selenium.webdriver.common.by import By from apps.applets.models import Applet -from apps.epic.models import GateSlot, Room, TableSeat, TarotCard, DeckVariant +from apps.epic.models import Character, GateSlot, Room, TableSeat, TarotCard, DeckVariant +from apps.lyric.models import User from .base import ChannelsFunctionalTest @@ -129,3 +131,207 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): "return document.documentElement.classList.contains('sea-open');" ) self.assertTrue(has_sea_open) + + +# ── Helpers for PICK SEA deal tests ────────────────────────────────────────── + +def _seed_earthman_cards(earthman, count=20): + """Seed enough Middle Arcana cards for the deck piles.""" + suits = ["BRANDS", "GRAILS", "BLADES", "CROWNS"] + nums = [11, 12, 13, 14] + for suit in suits: + for num in nums: + TarotCard.objects.get_or_create( + deck_variant=earthman, + slug=f"m{num}-{suit.lower()}-em", + defaults={"arcana": "MIDDLE", "suit": suit, "number": num, + "name": f"Card {num} {suit}"}, + ) + + +def _make_sea_ready_room(earthman): + """Create a SKY_SELECT room with a confirmed Character ready for PICK SEA. + + Returns (room, gamer, seat, char, room_url). + """ + gamer, _ = User.objects.get_or_create(email="founder@test.io") + gamer.unlocked_decks.add(earthman) + gamer.equipped_deck = earthman + gamer.save(update_fields=["equipped_deck"]) + + room = Room.objects.create( + name="Sea Deal Room", table_status=Room.SKY_SELECT, owner=gamer + ) + slot = room.gate_slots.get(slot_number=1) + slot.gamer = gamer + slot.status = GateSlot.FILLED + slot.save() + room.gate_status = Room.OPEN + room.save() + + sig_card = TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR").first() + seat = TableSeat.objects.create( + room=room, gamer=gamer, role="PC", slot_number=1, + deck_variant=earthman, significator=sig_card, + ) + char = Character.objects.create( + seat=seat, significator=sig_card, confirmed_at=timezone.now() + ) + return room, gamer, seat, char + + +class PickSeaDealTest(ChannelsFunctionalTest): + """PICK SEA deck stacks, OK btn interaction, card draw, and LOCK HAND.""" + + def setUp(self): + super().setUp() + self.browser.set_window_size(800, 1200) + Applet.objects.get_or_create( + slug="new-game", defaults={"name": "New Game", "context": "gameboard"} + ) + Applet.objects.get_or_create( + slug="my-games", defaults={"name": "My Games", "context": "gameboard"} + ) + # Major Arcana sig card + earthman, _ = DeckVariant.objects.get_or_create( + slug="earthman", + defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, + ) + TarotCard.objects.get_or_create( + deck_variant=earthman, slug="the-schizo-em", + defaults={"arcana": "MAJOR", "number": 1, "name": "The Schizo", + "levity_qualifier": "Enlightened", "gravity_qualifier": "Engraven"}, + ) + _seed_earthman_cards(earthman) + self.room, self.gamer, self.seat, self.char = _make_sea_ready_room(earthman) + self.room_url = self.live_server_url + reverse( + "epic:room", kwargs={"room_id": self.room.id} + ) + + def _load_sea_overlay(self): + """Navigate to room page and open the sea overlay.""" + self.create_pre_authenticated_session("founder@test.io") + self.browser.get(self.room_url) + btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn")) + self.browser.execute_script("arguments[0].click()", btn) + self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_overlay")) + + # ── Button presence ───────────────────────────────────────────────── # + + def test_deal_btn_absent(self): + """DEAL btn replaced by deck stacks + LOCK HAND.""" + self._load_sea_overlay() + self.assertEqual(self.browser.find_elements(By.ID, "id_sea_deal"), []) + + def test_lock_hand_btn_present_and_disabled(self): + """LOCK HAND btn is present but disabled before any cards are drawn.""" + self._load_sea_overlay() + lock_btn = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_sea_lock_hand") + ) + self.assertFalse(lock_btn.is_enabled()) + + def test_del_btn_present(self): + """DEL btn is always present in the sea overlay.""" + self._load_sea_overlay() + self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_del")) + + # ── Deck stacks ───────────────────────────────────────────────────── # + + def test_two_deck_stacks_present(self): + """Both levity and gravity deck stacks are visible.""" + self._load_sea_overlay() + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--gravity" + )) + + def test_clicking_stack_shows_ok_btn(self): + """Clicking a deck stack reveals its OK btn.""" + self._load_sea_overlay() + stack = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.browser.execute_script("arguments[0].click()", stack) + ok_btn = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok" + )) + self.assertTrue(ok_btn.is_displayed()) + + def test_clicking_elsewhere_hides_ok_btn(self): + """Clicking outside a focused stack dismisses the OK btn.""" + self._load_sea_overlay() + stack = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.browser.execute_script("arguments[0].click()", stack) + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok" + ).is_displayed()) + # Click the sea cards column (not a stack) + col = self.browser.find_element(By.CSS_SELECTOR, ".sea-cards-col") + self.browser.execute_script("arguments[0].click()", col) + self.wait_for(lambda: not any( + el.is_displayed() + for el in self.browser.find_elements(By.CSS_SELECTOR, ".sea-stack-ok") + )) + + # ── Card draw ─────────────────────────────────────────────────────── # + + def test_ok_click_fills_cover_position(self): + """First OK click places a card in the Cover position.""" + self._load_sea_overlay() + stack = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.browser.execute_script("arguments[0].click()", stack) + ok_btn = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok" + )) + self.browser.execute_script("arguments[0].click()", ok_btn) + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-pos-cover .sea-card-slot--filled" + )) + + def test_lock_hand_enables_after_six_draws(self): + """LOCK HAND btn becomes enabled once all 6 positions are filled.""" + self._load_sea_overlay() + + for _ in range(6): + stack = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.browser.execute_script("arguments[0].click()", stack) + ok_btn = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok" + )) + self.browser.execute_script("arguments[0].click()", ok_btn) + + lock_btn = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_sea_lock_hand") + ) + self.assertTrue(lock_btn.is_enabled()) + + def test_del_clears_drawn_cards(self): + """DEL btn clears all drawn cards and resets positions to empty.""" + self._load_sea_overlay() + # Draw one card + stack = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity" + )) + self.browser.execute_script("arguments[0].click()", stack) + ok_btn = self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok" + )) + self.browser.execute_script("arguments[0].click()", ok_btn) + self.wait_for(lambda: self.browser.find_element( + By.CSS_SELECTOR, ".sea-pos-cover .sea-card-slot--filled" + )) + # DEL clears it + del_btn = self.browser.find_element(By.ID, "id_sea_del") + self.browser.execute_script("arguments[0].click()", del_btn) + self.wait_for(lambda: not self.browser.find_elements( + By.CSS_SELECTOR, ".sea-card-slot--filled" + ))