"""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 Character, GateSlot, Room, TableSeat, TarotCard, DeckVariant from apps.lyric.models import User from .base import ChannelsFunctionalTest def _make_sky_confirmed_room(live_server_url, user, earthman): """Create a SKY_SELECT room with one gamer seated and sig assigned. Returns (room, seat). The Character is NOT yet confirmed — call _confirm_sky() in the browser to trigger the async transition. """ room = Room.objects.create( name="Sea Test Room", table_status=Room.SKY_SELECT, owner=user ) slot = room.gate_slots.get(slot_number=1) slot.gamer = user 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=user, role="PC", slot_number=1, deck_variant=earthman, significator=sig_card, ) return room, seat class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): """After sky confirm, PICK SEA overlay appears without a page refresh.""" 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"} ) from apps.lyric.models import User gamer, _ = User.objects.get_or_create(email="founder@test.io") earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) gamer.unlocked_decks.add(earthman) gamer.equipped_deck = earthman gamer.save(update_fields=["equipped_deck"]) self.gamer = gamer self.room, self.seat = _make_sky_confirmed_room(self.live_server_url, gamer, earthman) self.room_url = self.live_server_url + reverse( "epic:room", kwargs={"room_id": self.room.id} ) self.natus_save_url = self.live_server_url + reverse( "epic:natus_save", kwargs={"room_id": self.room.id} ) def _confirm_sky(self): """POST to natus_save with action=confirm from browser JS (bypasses chart form).""" # Wait for the room WS connection to be ready before triggering confirm self.wait_for(lambda: self.browser.execute_script( "return !!(window._roomSocket && window._roomSocket.readyState === 1);" )) self.browser.execute_script(f""" const csrf = (document.cookie.match(/csrftoken=([^;]+)/) || ['',''])[1]; fetch('{self.natus_save_url}', {{ method: 'POST', credentials: 'same-origin', headers: {{'Content-Type': 'application/json', 'X-CSRFToken': csrf}}, body: JSON.stringify({{ birth_dt: '1990-06-15T09:00:00Z', birth_lat: 51.5, birth_lon: -0.1, birth_place: 'London', house_system: 'O', chart_data: {{}}, action: 'confirm', }}), }}); """) def test_sea_overlay_appears_without_page_refresh(self): """Confirming sky replaces the natus overlay with the sea overlay in-place.""" self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.room_url) # Sky not yet confirmed — PICK SKY btn present, no sea overlay self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn")) self.assertEqual(self.browser.find_elements(By.ID, "id_sea_overlay"), []) self._confirm_sky() # Sea overlay appears without page refresh sea_overlay = self.wait_for( lambda: self.browser.find_element(By.ID, "id_sea_overlay") ) self.assertTrue(sea_overlay.is_displayed()) def test_natus_overlay_not_visible_after_sky_confirm(self): """Natus overlay is removed from the DOM after sky confirm.""" self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.room_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn")) self._confirm_sky() # Sea overlay must appear first (confirms transition happened) self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_overlay")) natus = self.browser.find_elements(By.ID, "id_natus_overlay") self.assertTrue(not natus or not natus[0].is_displayed()) def test_sea_open_class_on_html_after_confirm(self): """html.sea-open is set after sky confirm, giving the sea overlay its backdrop.""" self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.room_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn")) self._confirm_sky() self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_overlay")) has_sea_open = self.browser.execute_script( "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" ))