Files
python-tdd/src/functional_tests/test_room_role_select.py

685 lines
28 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
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 Room, GateSlot, TableSeat
from apps.lyric.models import User
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. Role card appears in inventory
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_inv_role_card .card"
)
)
# 8. 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 3d — Previously selected roles appear in inventory on re-entry#
# ------------------------------------------------------------------ #
def test_previously_selected_roles_shown_in_inventory_on_re_entry(self):
"""A multi-slot gamer who already chose some roles should see those
role cards pre-populated in the inventory when they re-enter the room."""
from apps.epic.models import TableSeat
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Inventory Re-entry Test", owner=founder)
_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 BC
TableSeat.objects.filter(room=room, slot_number=1).update(role="BC")
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)
# Inventory should contain exactly one pre-rendered card for BC
inv_cards = self.wait_for(
lambda: self.browser.find_elements(
By.CSS_SELECTOR, "#id_inv_role_card .card"
)
)
self.assertEqual(len(inv_cards), 1)
self.assertIn(
"BUILDER",
inv_cards[0].text.upper(),
)
# ------------------------------------------------------------------ #
# 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; inventory still empty
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")
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, "#id_inv_role_card .card")),
0
)
# ------------------------------------------------------------------ #
# 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 7 — All roles revealed simultaneously after all gamers select #
# ------------------------------------------------------------------ #
def test_roles_revealed_simultaneously_after_all_select(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Reveal 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)
# Assign all roles via ORM (simulating all gamers having chosen)
from apps.epic.models import TableSeat
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
for i, slot in enumerate(room.gate_slots.order_by("slot_number")):
TableSeat.objects.create(
room=room,
gamer=slot.gamer,
slot_number=slot.slot_number,
role=roles[i],
role_revealed=True,
)
room.table_status = Room.SIG_SELECT
room.save()
self.browser.refresh()
# All role cards in inventory are face-up
face_up_cards = self.wait_for(
lambda: self.browser.find_elements(
By.CSS_SELECTOR, "#id_inv_role_card .card.face-up"
)
)
self.assertGreater(len(face_up_cards), 0)
# Partner indicator is visible
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".partner-indicator")
)
@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")
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 loads the room — slot 1 is active on initial render
self.create_pre_authenticated_session("watcher@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-seat.active[data-slot='1']"
))
# 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 seat arc moves to slot 2 — no page refresh
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
))
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".table-seat.active[data-slot='1']"
)),
0,
)
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 — Turn passes to next gamer via WebSocket after selection #
# ------------------------------------------------------------------ #
def test_turn_passes_after_selection(self):
founder, _ = User.objects.get_or_create(email="founder@test.io")
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,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
# 1. Founder (slot 1) — eligible
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']"
))
# 2. Friend (slot 2) — ineligible in second browser
self.browser2 = self._make_browser2("friend@test.io")
try:
self.browser2.get(room_url)
self.wait_for(lambda: self.browser2.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
))
# 3. Founder picks a role
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()
# 4. Friend's stack becomes eligible via WebSocket — no page refresh
self.wait_for(lambda: self.browser2.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
# 5. Founder's stack is STILL ineligible — WS must not re-enable it
self.wait_for(lambda: self.assertEqual(
self.browser.find_element(
By.CSS_SELECTOR, ".card-stack"
).get_attribute("data-state"),
"ineligible",
))
# 6. Clicking founder's stack does not reopen the fan
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
))
finally:
self.browser2.quit()