Files
python-tdd/src/functional_tests/test_room_role_select.py
Disco DeDisco 4b8e02b698
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
role select channels FTs: equip Earthman deck in setUp — fixes no-deck guard block
The role-select.js no-deck guard (added with e512e94) shows a warning instead of
opening #id_role_select when data-equipped-deck is empty. Three RoleSelectChannelsTest
setups didn't equip a deck for the founder, so the card-stack click never opened the
modal and all three failed with NoSuchElementException on #id_role_select.
Only the founder needs equipping — other gamers' roles are ORM-assigned, no browser click.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 02:33:15 -04:00

910 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import unittest
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, GateSlot, TableSeat
from apps.lyric.models import User
def _equip_earthman_deck(user):
"""Equip the Earthman DeckVariant so the role-select no-deck guard passes."""
deck = DeckVariant.objects.filter(name__icontains="Earthman").first()
if deck:
user.equipped_deck = deck
user.save(update_fields=["equipped_deck"])
def _fill_room_via_orm(room, emails):
"""Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers."""
gamers = []
for i, email in enumerate(emails, start=1):
gamer, _ = User.objects.get_or_create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
gamers.append(gamer)
room.gate_status = Room.OPEN
room.save()
return gamers
class RoleSelectTest(FunctionalTest):
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 1 — PICK ROLES dismisses gatekeeper and reveals the table #
# ------------------------------------------------------------------ #
def test_pick_roles_dismisses_gatekeeper_and_reveals_table(self):
# 1. Founder logs in, creates room via UI, fills remaining slots via ORM
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_new_game_name")
).send_keys("Dragon's Den")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
room_url = self.browser.current_url
room = Room.objects.get(name="Dragon's Den")
# Fill founder's slot via UI (slot 1)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# Fill slots 26 via ORM
emails = ["amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io"]
for i, email in enumerate(emails, start=2):
gamer, _ = User.objects.get_or_create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
# 2. Browser sees the PICK ROLES button (gate is now open)
self.browser.refresh()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn")
).click()
# 3. Gatekeeper overlay is gone
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")), 0
)
)
# 4. Table is visible and prominent
table = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_game_table")
)
self.assertTrue(table.is_displayed())
# 5. Card stack is present in the table centre
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
)
# 6. Six seat portraits are visible around the table
seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
self.assertEqual(len(seats), 6)
# ------------------------------------------------------------------ #
# Test 2 — Card stack signals eligibility to each gamer #
# ------------------------------------------------------------------ #
def test_card_stack_glows_for_first_gamer_only(self):
# Two browsers: founder (slot 1, eligible) and friend (slot 2, not yet)
founder, _ = User.objects.get_or_create(email="founder@test.io")
friend, _ = User.objects.get_or_create(email="friend@test.io")
room = Room.objects.create(name="Signal Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "friend@test.io",
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
# Founder's browser
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
stack = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
)
self.assertIn("eligible", stack.get_attribute("data-state"))
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack .fa-ban")), 0
)
# Friend's browser
options2 = webdriver.FirefoxOptions()
if os.environ.get("HEADLESS"):
options2.add_argument("--headless")
self.browser2 = webdriver.Firefox(options=options2)
try:
self.browser2.get(self.live_server_url + "/404_no_such_url/")
from django.conf import settings
session_key = __import__(
"functional_tests.management.commands.create_session",
fromlist=["create_pre_authenticated_session"]
).create_pre_authenticated_session("friend@test.io")
self.browser2.add_cookie(dict(
name=settings.SESSION_COOKIE_NAME, value=session_key, path="/"
))
self.browser2.get(room_url)
stack2 = self.wait_for(
lambda: self.browser2.find_element(By.CSS_SELECTOR, ".card-stack")
)
self.assertIn("ineligible", stack2.get_attribute("data-state"))
self.wait_for(
lambda: self.browser2.find_element(
By.CSS_SELECTOR, ".card-stack .fa-ban"
)
)
finally:
self.browser2.quit()
# ------------------------------------------------------------------ #
# Test 3 — Active gamer fans cards, inspects, selects a role #
# ------------------------------------------------------------------ #
def test_active_gamer_fans_cards_and_selects_role(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Fan Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# 1. Click the card stack
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
).click()
# 2. Role Select modal opens with 6 cards
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_role_select")
)
cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
self.assertEqual(len(cards), 6)
# 3. Blur backdrop is present
self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop")
# 4. Hover over first card — it flips to reveal front
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(self.browser).move_to_element(cards[0]).perform()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_role_select .card.flipped"
)
)
# 5. Click first card to select it
cards[0].click()
self.confirm_guard()
# 6. Modal closes
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.ID, "id_role_select")), 0
)
)
# 7. Card stack returns to table centre
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
)
# ------------------------------------------------------------------ #
# Test 3b — Chosen role absent from next gamer's fan #
# ------------------------------------------------------------------ #
def test_chosen_role_absent_from_next_gamer_fan(self):
from apps.epic.models import TableSeat
founder, _ = User.objects.get_or_create(email="founder@test.io")
friend, _ = User.objects.get_or_create(email="friend@test.io")
room = Room.objects.create(name="Pool Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "friend@test.io",
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
# Simulate pick_roles: create a TableSeat per filled slot
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
# Slot 1 (founder) has already chosen PC
TableSeat.objects.filter(room=room, slot_number=1).update(role="PC")
# Slot 2 (friend) is now the active gamer
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("friend@test.io")
self.browser.get(room_url)
# Card stack is eligible for slot 2
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
# Fan opens — only 5 cards (PC is taken)
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_role_select")
)
cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
self.assertEqual(len(cards), 5)
# Specifically, no PC card in the fan
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, "#id_role_select .card[data-role='PC']"
)),
0,
)
# ------------------------------------------------------------------ #
# Test 3c — Card stack stays eligible after re-entering mid-session #
# ------------------------------------------------------------------ #
def test_card_stack_remains_eligible_after_re_entering_mid_selection(self):
"""A gamer holding multiple slots should still see an eligible card
stack when they re-enter the room after having already chosen a role
for their earlier slot."""
from apps.epic.models import TableSeat
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Re-entry Test", owner=founder)
# Founder holds slots 1 and 2; others fill the rest
_fill_room_via_orm(room, [
"founder@test.io", "founder@test.io",
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
# Founder's first slot has already chosen PC
TableSeat.objects.filter(room=room, slot_number=1).update(role="PC")
# Founder re-enters the room (simulating a page reload / re-navigation)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# Card stack must be eligible — slot 2 (also founder's) is the active seat
stack = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
)
self.assertEqual(stack.get_attribute("data-state"), "eligible")
# Fan shows 5 cards — PC already taken
stack.click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
self.assertEqual(len(cards), 5)
# ------------------------------------------------------------------ #
# Test 4 — Click-away dismisses fan without selecting #
# ------------------------------------------------------------------ #
def test_click_away_dismisses_card_fan_without_selecting(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Dismiss Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# Open the fan
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
).click()
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_role_select")
)
# Click the backdrop (outside the fan)
self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop").click()
# Modal closes; stack still present
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.ID, "id_role_select")), 0
)
)
self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
# ------------------------------------------------------------------ #
# Test 4b — Stack locks out immediately after selection (no WS) #
# ------------------------------------------------------------------ #
def test_card_stack_ineligible_immediately_after_selection(self):
"""After clicking a role card the stack must flip to
data-state='ineligible' straight away — before any WS turn_changed
event could arrive. This test runs without a Channels server so
no WS event will fire; the fix must be entirely client-side."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Lockout Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# No WS — only the JS fix can make this transition happen
self.wait_for(
lambda: self.assertEqual(
self.browser.find_element(
By.CSS_SELECTOR, ".card-stack"
).get_attribute("data-state"),
"ineligible",
)
)
def test_card_stack_cannot_be_reopened_after_selection(self):
"""Clicking the card stack immediately after picking a role must
not open a second fan — the listener must have been removed."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="No-reopen Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# Open fan, pick a card
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# Wait for fan to close (selectRole closes it synchronously)
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.ID, "id_role_select")), 0
)
)
# Attempt to reopen — must not work
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.ID, "id_role_select")), 0
)
)
# ------------------------------------------------------------------ #
# Test 8a — Hex seats carry role labels during role select #
# ------------------------------------------------------------------ #
def test_seats_around_hex_have_role_labels(self):
"""During role select the 6 .table-seat elements carry data-role
attributes matching the fixed slot→role mapping (PC at slot 1, etc.)."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Seat Label Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
expected = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
)
for slot_number, role_label in expected.items():
seat = self.browser.find_element(
By.CSS_SELECTOR, f".table-seat[data-slot='{slot_number}']"
)
self.assertEqual(seat.get_attribute("data-role"), role_label)
# ------------------------------------------------------------------ #
# Test 8b — Hex seats show .fa-ban when empty #
# ------------------------------------------------------------------ #
def test_seats_show_ban_icon_when_empty(self):
"""All 6 seats carry .fa-ban before any role has been chosen."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Seat Ban Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
)
seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
self.assertEqual(len(seats), 6)
for seat in seats:
self.assertTrue(
seat.find_elements(By.CSS_SELECTOR, ".fa-ban"),
f"Expected .fa-ban on seat slot {seat.get_attribute('data-slot')}",
)
# ------------------------------------------------------------------ #
# Test 8c — Hex seat gets .fa-circle-check after role selected #
# ------------------------------------------------------------------ #
def test_seat_gets_check_after_role_selected(self):
"""After confirming a role pick the corresponding hex seat should
show .fa-circle-check and lose .fa-ban."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Seat Check Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# Open fan, pick first card (SC — Shepherd), confirm guard
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# Wait for tray animation to complete
self.wait_for(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should close after arc-in sequence",
)
)
# The SC seat (slot 1) now shows check, no ban
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-circle-check"
)
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-ban"
)),
0,
)
class RoleSelectTrayTest(FunctionalTest):
"""After confirming a role pick, the role card enters the tray grid and
the tray opens to reveal it.
Portrait — card lands at the topmost grid square (first child, row 1 col 1).
Landscape — card lands at the leftmost grid square (first child, row 1 col 1).
"""
EMAILS = [
"slot1@test.io", "slot2@test.io", "slot3@test.io",
"slot4@test.io", "slot5@test.io", "slot6@test.io",
]
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_room(self):
"""Room in ROLE_SELECT with all 6 seats created, slot 1 eligible."""
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
room = Room.objects.create(name="Tray Card Test", owner=founder)
_fill_room_via_orm(room, self.EMAILS)
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number
)
return room
def _select_role(self):
"""Open the fan, pick the first card, confirm the guard dialog."""
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# ------------------------------------------------------------------ #
# T1 — Portrait: role card marks first cell; tray opens then closes #
# ------------------------------------------------------------------ #
def test_portrait_role_card_enters_topmost_grid_square(self):
"""Portrait: after confirming a role the first .tray-cell gets
.tray-role-card; the grid still has exactly 8 cells; and the tray
opens briefly then closes once the arc-in animation completes."""
self.browser.set_window_size(390, 844)
room = self._make_room()
self.create_pre_authenticated_session("slot1@test.io")
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
self._select_role()
# First cell receives the role card class.
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
)
)
result = self.browser.execute_script("""
var grid = document.getElementById('id_tray_grid');
var card = grid.querySelector('.tray-role-card');
return {
isFirst: card !== null && card === grid.firstElementChild,
count: grid.children.length,
role: card ? card.dataset.role : null
};
""")
self.assertTrue(result["isFirst"], "Role card should be the first cell")
self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
self.assertTrue(result["role"], "First cell should carry data-role")
# Tray closes after the animation sequence.
self.wait_for(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should close after the arc-in sequence"
)
)
# ------------------------------------------------------------------ #
# T2 — Landscape: same contract in landscape #
# ------------------------------------------------------------------ #
@tag('two-browser')
def test_landscape_role_card_enters_leftmost_grid_square(self):
"""Landscape: the first .tray-cell gets .tray-role-card; grid has
8 cells; tray opens then closes."""
self.browser.set_window_size(844, 390)
room = self._make_room()
self.create_pre_authenticated_session("slot1@test.io")
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
self._select_role()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
)
)
result = self.browser.execute_script("""
var grid = document.getElementById('id_tray_grid');
var card = grid.querySelector('.tray-role-card');
return {
isFirst: card !== null && card === grid.firstElementChild,
count: grid.children.length
};
""")
self.assertTrue(result["isFirst"], "Role card should be the first cell")
self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
self.wait_for(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should close after the arc-in sequence"
)
)
@tag('channels')
class RoleSelectChannelsTest(ChannelsFunctionalTest):
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 6 — Observer sees seat arc move via WebSocket #
# ------------------------------------------------------------------ #
def test_observer_sees_seat_arc_during_selection(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
User.objects.get_or_create(email="watcher@test.io")
room = Room.objects.create(name="Arc Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "watcher@test.io",
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
# 1. Watcher (slot 2) loads the room
self.create_pre_authenticated_session("watcher@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
))
# 2. Founder picks a role in second browser
self.browser2 = self._make_browser2("founder@test.io")
try:
self.browser2.get(room_url)
self.wait_for(lambda: self.browser2.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select"))
self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard(browser=self.browser2)
# 3. Watcher's turn arrives via WS — card-stack becomes eligible
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
finally:
self.browser2.quit()
def _make_browser2(self, email):
"""Spin up a second Firefox, authenticate email, return the browser."""
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
# ------------------------------------------------------------------ #
# Test 5 — Tray closes on turn advance (portrait) #
# ------------------------------------------------------------------ #
def _make_turn_test_room(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
User.objects.get_or_create(email="friend@test.io")
room = Room.objects.create(name="Turn Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "friend@test.io",
"bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
def test_portrait_tray_closes_on_turn_advance(self):
"""Portrait: after selecting a role the tray opens and the role card lands
in the topmost grid square. When turn_changed arrives via WS, the tray
force-closes so the next player's card-stack is not obscured."""
self.browser.set_window_size(390, 844)
room_url = self._make_turn_test_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
# Select a role — card lands in topmost grid square.
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# Wait for fetch .then() — card must be first child of grid.
self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
var card = document.querySelector('#id_tray_grid .tray-role-card');
return card !== null && card === card.parentElement.firstElementChild;
""")))
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
self.wait_for(lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should be closed after turn advances"
))
def test_landscape_tray_closes_on_turn_advance(self):
"""Landscape: role card at leftmost grid square; tray closes when
turn_changed arrives via WS."""
self.browser.set_window_size(844, 390)
room_url = self._make_turn_test_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# Wait for fetch .then() — card must be first child of grid.
self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
var card = document.querySelector('#id_tray_grid .tray-role-card');
return card !== null && card === card.parentElement.firstElementChild;
""")))
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
self.wait_for(lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should be closed after turn advances"
))
# ------------------------------------------------------------------ #
# Test 7 — PICK SIGS appears + card stack removed on last role #
# ------------------------------------------------------------------ #
def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
"""When the sixth and final role is confirmed, the all_roles_filled
WS event makes the PICK SIGS button visible and removes the card
stack from the DOM entirely."""
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="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="Last Role Test", owner=founder)
_fill_room_via_orm(room, emails)
room.table_status = Room.ROLE_SELECT
room.save()
# Pre-assign 5 roles (slots 26); founder (slot 1) is the final picker.
pre_assigned = {2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room,
gamer=slot.gamer,
slot_number=slot.slot_number,
role=pre_assigned.get(slot.slot_number),
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
# Founder picks the last remaining role (PC — the only card in the fan).
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# PICK SIGS wrap must become visible via the all_roles_filled WS event.
self.wait_for(lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
))
# Card stack must be removed from the DOM entirely.
self.wait_for(lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack")), 0,
))