import os from django.conf import settings as django_settings from django.test import tag from selenium import webdriver from selenium.webdriver.common.by import By from .base import FunctionalTest, ChannelsFunctionalTest from .management.commands.create_session import create_pre_authenticated_session from apps.applets.models import Applet from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard from apps.lyric.models import User from .test_room_role_select import _fill_room_via_orm # ── Significator Selection ──────────────────────────────────────────────────── # # After all 6 roles are revealed the room enters SIG_SELECT. Two parallel # 18-card overlays appear (levity: PC/NC/SC; gravity: BC/EC/AC). Each polarity # group picks simultaneously — no sequential turn order. # # ───────────────────────────────────────────────────────────────────────────── SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"] def _assign_all_roles(room, role_order=None): """Assign roles to all slots, reveal them, and advance to SIG_SELECT. Also ensures all gamers have an equipped_deck (required for sig_deck_cards).""" if role_order is None: role_order = SIG_SEAT_ORDER[:] earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) # Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs) _NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"} for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"): for number in (11, 12, 13, 14): TarotCard.objects.get_or_create( deck_variant=earthman, slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em", defaults={"arcana": "MINOR", "suit": suit, "number": number, "name": f"{_NAME[number]} of {suit.capitalize()}"}, ) for number, name, slug in [ (0, "The Schiz", "the-schiz-em"), (1, "Pope 1: Chancellor", "pope-1-chancellor-em"), ]: TarotCard.objects.get_or_create( deck_variant=earthman, slug=slug, defaults={"arcana": "MAJOR", "number": number, "name": name}, ) for slot in room.gate_slots.order_by("slot_number"): if slot.gamer and not slot.gamer.equipped_deck: slot.gamer.equipped_deck = earthman slot.gamer.save(update_fields=["equipped_deck"]) TableSeat.objects.update_or_create( room=room, slot_number=slot.slot_number, defaults={ "gamer": slot.gamer, "role": role_order[slot.slot_number - 1], "role_revealed": True, }, ) room.table_status = Room.SIG_SELECT room.save() class SigSelectTest(FunctionalTest): """Significator Selection — non-WebSocket tests.""" def setUp(self): super().setUp() 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"} ) # ------------------------------------------------------------------ # # Test S1 — Seats reorder to canonical role sequence at SIG_SELECT # # ------------------------------------------------------------------ # def test_seats_display_in_pc_nc_ec_sc_ac_bc_order_after_reveal(self): """Slots were filled in arbitrary token-drop order; after roles are revealed the seat portraits must appear in PC→NC→EC→SC→AC→BC order.""" founder, _ = User.objects.get_or_create(email="founder@test.io") room = Room.objects.create(name="Seat Order Test", owner=founder) # Assign roles in reverse of canonical order so the reordering is visible _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ]) _assign_all_roles(room, role_order=["BC", "AC", "SC", "EC", "NC", "PC"]) self.create_pre_authenticated_session("founder@test.io") room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_sig_deck")) seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat[data-role]") self.assertEqual(len(seats), 6) roles_in_order = [s.get_attribute("data-role") for s in seats] self.assertEqual(roles_in_order, SIG_SEAT_ORDER) @tag("channels") class SigSelectChannelsTest(ChannelsFunctionalTest): """Significator Selection — WebSocket tests.""" def setUp(self): super().setUp() 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"} ) def _make_browser2(self, email): session_key = create_pre_authenticated_session(email) options = webdriver.FirefoxOptions() if os.environ.get("HEADLESS"): options.add_argument("--headless") b = webdriver.Firefox(options=options) b.get(self.live_server_url + "/404_no_such_url/") b.add_cookie(dict( name=django_settings.SESSION_COOKIE_NAME, value=session_key, path="/", )) return b def _setup_sig_select_room(self): """Create a full SIG_SELECT room; return (room, [user_pc, user_nc, ...]).""" emails = [ "founder@test.io", "amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ] founder, _ = User.objects.get_or_create(email=emails[0]) room = Room.objects.create(name="Cursor Colour Test", owner=founder) gamers = _fill_room_via_orm(room, emails) _assign_all_roles(room) return room, gamers # ── SC1: NC hover → PC sees mid cursor active, coloured --priYl ────────── # @tag('channels') def test_nc_hover_activates_mid_cursor_in_pc_browser(self): """ When NC (levity mid) hovers a card, PC (levity left) must see the --mid cursor become active, coloured --priYl (rgb 255 207 52). Verifies: WS broadcast pipeline + JS applyHover + CSS role colouring. """ room, gamers = self._setup_sig_select_room() room_url = self.live_server_url + f"/gameboard/room/{room.pk}/" # ── Browser 1: PC (founder) ─────────────────────────────────────────── self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) # ── Browser 2: NC (amigo) ───────────────────────────────────────────── browser2 = self._make_browser2("amigo@test.io") try: browser2.get(room_url) self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay")) # Grab the first card ID visible in browser2's deck first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card") card_id = first_card.get_attribute("data-card-id") # Hover over it — triggers sendHover() → WS broadcast from selenium.webdriver.common.action_chains import ActionChains ActionChains(browser2).move_to_element(first_card).perform() # ── Browser 1 should see --mid cursor go active (anchor carries class) ─ mid_cursor_sel = f'.sig-card[data-card-id="{card_id}"] .sig-cursor--mid' self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, mid_cursor_sel + ".active" ) ) # CSS colour check: portal float has data-role="NC" → --priYl = 255, 207, 52 portal_sel = '.sig-cursor-float[data-role="NC"]' portal_cursor = self.browser.find_element(By.CSS_SELECTOR, portal_sel) color = self.browser.execute_script( "return window.getComputedStyle(arguments[0]).color", portal_cursor, ) self.assertEqual(color, "rgb(255, 207, 52)", f"Expected --priYl colour for NC cursor, got {color}") # ── Mouse-off: anchor class removed, portal float gone ──────────── ActionChains(browser2).move_to_element( browser2.find_element(By.CSS_SELECTOR, ".sig-stage") ).perform() self.wait_for( lambda: not self.browser.find_elements( By.CSS_SELECTOR, mid_cursor_sel + ".active" ) ) finally: browser2.quit() # ── SC2: NC reserves → PC sees card border coloured --priYl ──────────── # @tag('channels') def test_nc_reservation_glows_priYl_in_pc_browser(self): """ When NC (levity mid) clicks OK on a card, PC must see that card's border coloured --priYl (rgb 255 207 52) via the data-reserved-by CSS selector. Verifies: sig_reserve view → WS broadcast → applyReservation → CSS glow. """ room, gamers = self._setup_sig_select_room() room_url = self.live_server_url + f"/gameboard/room/{room.pk}/" # ── Browser 1: PC (founder) ─────────────────────────────────────────── self.create_pre_authenticated_session("founder@test.io") self.browser.get(room_url) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) # ── Browser 2: NC (amigo) ───────────────────────────────────────────── browser2 = self._make_browser2("amigo@test.io") try: browser2.get(room_url) self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay")) # Get first card in B2's deck first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card") card_id = first_card.get_attribute("data-card-id") # Click card body → .sig-focused → OK button appears from selenium.webdriver.common.action_chains import ActionChains ActionChains(browser2).move_to_element(first_card).perform() first_card.click() ok_btn = self.wait_for( lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn") ) ok_btn.click() # ── B1 should see the card's border turn --priYl ────────────────── reserved_card_sel = f'.sig-card[data-card-id="{card_id}"]' self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, reserved_card_sel + '[data-reserved-by="NC"]' ) ) reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel) border_color = self.browser.execute_script( "return window.getComputedStyle(arguments[0]).borderTopColor", reserved_card, ) self.assertEqual( border_color, "rgb(255, 207, 52)", f"Expected --priYl border for NC reservation, got {border_color}", ) finally: browser2.quit() # ── Polarity theming: qualifier text + no correspondence ───────────────────── class SigSelectThemeTest(FunctionalTest): """Polarity-qualifier display (Graven/Leavened) and correspondence suppression. No WebSocket needed — stage updates are local; uses plain FunctionalTest.""" EMAILS = [ "founder@test.io", "amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io", ] 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"}) def _setup_sig_room(self): founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) room = Room.objects.create(name="Theme Test", owner=founder) _fill_room_via_orm(room, self.EMAILS) _assign_all_roles(room) return room def _hover_card(self, css): from selenium.webdriver.common.action_chains import ActionChains card = self.browser.find_element(By.CSS_SELECTOR, css) ActionChains(self.browser).move_to_element(card).perform() return card # ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── # def test_levity_non_major_card_shows_leavened_above(self): """Hovering a non-major card in the levity overlay shows 'Leavened' in qualifier-above and nothing in qualifier-below.""" room = self._setup_sig_room() self.create_pre_authenticated_session("founder@test.io") # PC = levity self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self._hover_card('.sig-card[data-arcana="Minor Arcana"]') above = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") ) self.assertEqual(above.text, "Leavened") below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") self.assertEqual(below.text, "") def test_levity_major_card_shows_leavened_below(self): """Hovering a major arcana card in the levity overlay shows 'Leavened' in qualifier-below and nothing in qualifier-above.""" room = self._setup_sig_room() self.create_pre_authenticated_session("founder@test.io") # PC = levity self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self._hover_card('.sig-card[data-arcana="Major Arcana"]') below = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") ) self.assertEqual(below.text, "Leavened") above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") self.assertEqual(above.text, "") # ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── # def test_gravity_non_major_card_shows_graven_above(self): """EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'.""" room = self._setup_sig_room() self.create_pre_authenticated_session("bud@test.io") # EC = gravity self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self._hover_card('.sig-card[data-arcana="Minor Arcana"]') above = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") ) self.assertEqual(above.text, "Graven") # ── ST3: Correspondence not shown ─────────────────────────────────────── # def test_correspondence_not_shown_in_sig_select(self): """The Minchiate-equivalence field must always be blank on the stage card.""" room = self._setup_sig_room() self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) # Hover any card — correspondence should remain empty regardless self._hover_card(".sig-card") self.wait_for(lambda: self.browser.find_element( By.CSS_SELECTOR, ".sig-stage-card" )) corr = self.browser.find_element(By.CSS_SELECTOR, ".fan-card-correspondence") self.assertEqual(corr.text, "")