functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale test_bud_btn.py references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push smoke-import: 31/31 FT modules green after the rename pass Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
895
src/functional_tests/test_game_room_select_role.py
Normal file
895
src/functional_tests/test_game_room_select_role.py
Normal file
@@ -0,0 +1,895 @@
|
||||
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 .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||
from apps.applets.models import Applet
|
||||
from apps.epic.models import Room, GateSlot, TableSeat
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
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 2–6 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")
|
||||
_equip_earthman_deck(founder)
|
||||
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")
|
||||
_equip_earthman_deck(friend) # friend is the active gamer (slot 2)
|
||||
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")
|
||||
_equip_earthman_deck(founder) # active slot is slot 2 (also founder)
|
||||
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")
|
||||
_equip_earthman_deck(founder)
|
||||
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")
|
||||
_equip_earthman_deck(founder)
|
||||
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")
|
||||
_equip_earthman_deck(founder)
|
||||
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")
|
||||
_equip_earthman_deck(founder)
|
||||
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 fade-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])
|
||||
_equip_earthman_deck(founder)
|
||||
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 fade-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 fade-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 fade-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 2–6); 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,
|
||||
))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user