2026-03-14 00:34:07 -04:00
|
|
|
|
import time
|
|
|
|
|
|
|
2026-03-14 22:00:16 -04:00
|
|
|
|
from datetime import timedelta
|
2026-03-14 02:03:44 -04:00
|
|
|
|
from django.utils import timezone
|
2026-03-13 00:31:17 -04:00
|
|
|
|
from selenium.webdriver.common.by import By
|
|
|
|
|
|
|
|
|
|
|
|
from .base import FunctionalTest
|
|
|
|
|
|
from apps.applets.models import Applet
|
2026-03-14 22:00:16 -04:00
|
|
|
|
from apps.epic.models import Room, GateSlot, select_token
|
|
|
|
|
|
from apps.lyric.models import Token, User
|
2026-03-30 18:31:05 -04:00
|
|
|
|
from .test_room_role_select import _fill_room_via_orm
|
2026-03-13 00:31:17 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GatekeeperTest(FunctionalTest):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
Applet.objects.get_or_create(
|
|
|
|
|
|
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
|
|
|
|
|
)
|
2026-03-13 17:31:52 -04:00
|
|
|
|
Applet.objects.get_or_create(
|
|
|
|
|
|
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
|
|
|
|
|
|
)
|
2026-03-13 00:31:17 -04:00
|
|
|
|
|
|
|
|
|
|
def test_founder_creates_room_and_sees_gatekeeper(self):
|
|
|
|
|
|
# 1. Log in, navigate to gameboard
|
2026-03-14 02:03:44 -04:00
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
2026-03-13 00:31:17 -04:00
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
# 2. New Game applet has room name input, create button
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_applet_new_game")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
|
|
|
|
|
# 3. User is redirected to Gatekeeper page for new room
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn("/gameboard/room/", self.browser.current_url)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn("/gate/", self.browser.current_url)
|
|
|
|
|
|
)
|
|
|
|
|
|
# 4. Page shows room name, GATHERING status
|
|
|
|
|
|
body = self.browser.find_element(By.TAG_NAME, "body")
|
2026-03-14 22:00:16 -04:00
|
|
|
|
self.assertIn("TEST ROOM", body.text)
|
2026-03-14 01:14:05 -04:00
|
|
|
|
self.assertIn("GATHERING GAMERS", body.text)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 5. Six token slot circles are visible, all empty
|
2026-03-13 00:31:17 -04:00
|
|
|
|
slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
|
|
|
|
|
|
self.assertEqual(len(slots), 6)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
for slot in slots:
|
2026-03-13 00:31:17 -04:00
|
|
|
|
self.assertIn("empty", slot.get_attribute("class"))
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 6. Shared coin slot is present; no individual drop buttons
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, ".token-slot")
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0
|
|
|
|
|
|
)
|
2026-03-13 17:31:52 -04:00
|
|
|
|
|
|
|
|
|
|
def test_founder_drops_token_and_slot_fills(self):
|
|
|
|
|
|
# 1. Set up: log in, create room, arrive at gatekeeper
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 2. Founder clicks Insert Token via the shared coin slot
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
# 3. Slot 1 (lowest) now shows OK button; slot is reserved
|
|
|
|
|
|
ok_btn = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
|
|
|
|
|
|
)
|
2026-03-13 17:31:52 -04:00
|
|
|
|
)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 4. Founder clicks OK → slot fills
|
|
|
|
|
|
ok_btn.click()
|
2026-03-13 17:31:52 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
|
|
|
|
|
|
)
|
|
|
|
|
|
slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
|
|
|
|
|
|
self.assertIn("filled", slots[0].get_attribute("class"))
|
|
|
|
|
|
self.assertEqual(
|
2026-03-14 22:00:16 -04:00
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
|
2026-03-13 17:31:52 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_room_appears_in_my_games_after_creation(self):
|
|
|
|
|
|
# 1. Set up founder, game room, name
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
)
|
|
|
|
|
|
# 2. Navigate back to gameboard
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
my_games = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertIn("Dragon's Den", my_games.text)
|
2026-03-13 18:37:19 -04:00
|
|
|
|
|
|
|
|
|
|
def test_second_gamer_drops_token_into_open_slot(self):
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 1. Founder creates room, confirms slot 1
|
2026-03-13 18:37:19 -04:00
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
2026-03-13 18:37:19 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
|
|
|
|
|
)
|
|
|
|
|
|
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
|
2026-03-14 02:03:44 -04:00
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
|
|
|
|
|
).click()
|
2026-03-13 18:37:19 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
|
|
|
|
|
|
)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 2. Founder invites friend
|
2026-03-13 18:37:19 -04:00
|
|
|
|
invite_input = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_invite_email")
|
|
|
|
|
|
)
|
|
|
|
|
|
invite_input.send_keys("friend@test.io")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_invite_btn").click()
|
|
|
|
|
|
# 3. Friend logs in, sees invitation in My Games
|
|
|
|
|
|
self.create_pre_authenticated_session("friend@test.io")
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
my_games = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertIn("Dragon's Den", my_games.text)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 4. Friend follows link to gatekeeper
|
2026-03-13 18:37:19 -04:00
|
|
|
|
self.browser.find_element(By.LINK_TEXT, "Dragon's Den").click()
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 5. Friend drops token via coin slot and confirms
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
# 6. Now two slots filled
|
2026-03-13 18:37:19 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertEqual(
|
|
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot.filled")), 2
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-03-13 22:51:42 -04:00
|
|
|
|
|
|
|
|
|
|
def test_gate_opens_when_all_slots_filled(self):
|
|
|
|
|
|
# 1. Founder creates room
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 2. Founder confirms slot 1 via coin slot
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
|
|
|
|
|
).click()
|
2026-03-13 22:51:42 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
|
|
|
|
|
|
)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
# 3. Fill slots 2–6 directly via ORM
|
2026-03-13 22:51:42 -04:00
|
|
|
|
room = Room.objects.get(name="Dragon's Den")
|
|
|
|
|
|
for i, email in enumerate([
|
|
|
|
|
|
"g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io", "g6@test.io"
|
|
|
|
|
|
], start=2):
|
|
|
|
|
|
gamer = User.objects.create(email=email)
|
|
|
|
|
|
slot = room.gate_slots.get(slot_number=i)
|
|
|
|
|
|
slot.gamer = gamer
|
|
|
|
|
|
slot.status = GateSlot.FILLED
|
|
|
|
|
|
slot.save()
|
|
|
|
|
|
room.refresh_from_db()
|
|
|
|
|
|
room.gate_status = Room.OPEN
|
|
|
|
|
|
room.save()
|
Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
2026-03-17 00:24:23 -04:00
|
|
|
|
# 4. Gate shows launch button when all slots filled
|
|
|
|
|
|
# update this for ASGI after channels sprint!
|
|
|
|
|
|
self.browser.refresh()
|
2026-03-13 22:51:42 -04:00
|
|
|
|
self.wait_for(
|
2026-03-16 00:07:52 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn")
|
2026-03-13 22:51:42 -04:00
|
|
|
|
)
|
2026-03-14 00:10:40 -04:00
|
|
|
|
|
|
|
|
|
|
def test_owner_can_delete_room_via_gear_menu(self):
|
|
|
|
|
|
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"))
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
|
|
|
|
|
self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))
|
|
|
|
|
|
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
|
|
|
|
|
|
).click()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard()
|
2026-03-14 00:10:40 -04:00
|
|
|
|
|
|
|
|
|
|
self.wait_for(lambda: self.assertEqual(
|
|
|
|
|
|
self.browser.current_url, self.live_server_url + "/gameboard/"
|
|
|
|
|
|
))
|
|
|
|
|
|
self.assertFalse(Room.objects.filter(name="Doomed Room").exists())
|
|
|
|
|
|
|
2026-03-14 00:34:07 -04:00
|
|
|
|
def test_gatekeeper_overlay_persists_after_htmx_poll(self):
|
|
|
|
|
|
# 1. Create room directly (GATHERING) and navigate to its gate URL
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
founder = User.objects.get(email="founder@test.io")
|
|
|
|
|
|
room = Room.objects.create(name="Persistent Room", owner=founder)
|
|
|
|
|
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
|
|
|
|
|
|
# 2. Assert overlay visible on initial page load
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
)
|
|
|
|
|
|
# 3. Wait for HTMX poll cycle to fire (poll interval is 3s)
|
|
|
|
|
|
time.sleep(4)
|
|
|
|
|
|
# 4. Assert overlay still present and visible after poll
|
|
|
|
|
|
overlays = self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
self.assertEqual(len(overlays), 1)
|
|
|
|
|
|
self.assertTrue(overlays[0].is_displayed())
|
|
|
|
|
|
|
2026-03-14 00:10:40 -04:00
|
|
|
|
def test_gamer_can_abandon_room_via_gear_menu(self):
|
|
|
|
|
|
founder = User.objects.create(email="founder@test.io")
|
|
|
|
|
|
room = Room.objects.create(name="Dragon's Den", owner=founder)
|
|
|
|
|
|
slot = room.gate_slots.get(slot_number=2)
|
|
|
|
|
|
self.create_pre_authenticated_session("gamer@test.io")
|
|
|
|
|
|
gamer, _ = User.objects.get_or_create(email="gamer@test.io")
|
|
|
|
|
|
slot.gamer = gamer
|
|
|
|
|
|
slot.status = "FILLED"
|
|
|
|
|
|
slot.save()
|
|
|
|
|
|
|
|
|
|
|
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
|
|
|
|
|
|
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
|
|
|
|
|
).click()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard()
|
2026-03-14 00:10:40 -04:00
|
|
|
|
|
|
|
|
|
|
self.wait_for(lambda: self.assertEqual(
|
|
|
|
|
|
self.browser.current_url, self.live_server_url + "/gameboard/"
|
|
|
|
|
|
))
|
|
|
|
|
|
slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(slot.status, "EMPTY")
|
|
|
|
|
|
self.assertIsNone(slot.gamer)
|
2026-03-14 02:03:44 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoinSlotTest(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"}
|
|
|
|
|
|
)
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.founder = User.objects.get(email="founder@test.io")
|
|
|
|
|
|
self.room = Room.objects.create(name="Coin Room", owner=self.founder)
|
|
|
|
|
|
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_slot_active_for_eligible_gamer(self):
|
|
|
|
|
|
# Gamer with no slot arrives at gatekeeper — coin slot is active
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
|
|
|
|
|
|
)
|
2026-03-14 02:25:51 -04:00
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
|
|
|
|
|
|
def test_drop_token_reserves_lowest_empty_slot(self):
|
|
|
|
|
|
# Gamer drops token; slot 1 (lowest) becomes reserved with OK button
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(slot.status, GateSlot.RESERVED)
|
|
|
|
|
|
self.assertEqual(slot.gamer, self.founder)
|
|
|
|
|
|
|
|
|
|
|
|
def test_confirm_fills_slot_and_removes_ok_button(self):
|
|
|
|
|
|
# Drop then confirm → slot 1 FILLED, OK button gone
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).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")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
2026-03-14 22:00:16 -04:00
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
|
2026-03-14 02:03:44 -04:00
|
|
|
|
)
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(slot.status, GateSlot.FILLED)
|
|
|
|
|
|
|
2026-03-15 16:08:34 -04:00
|
|
|
|
def test_gamer_can_return_pending_token(self):
|
|
|
|
|
|
# Drop then return via Push to Return → slot remains empty
|
2026-03-14 02:03:44 -04:00
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
2026-03-15 16:08:34 -04:00
|
|
|
|
# Push to Return appears in coin slot
|
2026-03-14 02:03:44 -04:00
|
|
|
|
self.wait_for(
|
2026-03-15 16:08:34 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-return-btn")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
# Slot 1 still empty; coin slot active again
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
|
|
|
|
|
|
)
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(slot.status, GateSlot.EMPTY)
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_slot_locked_while_another_token_is_pending(self):
|
|
|
|
|
|
# Pre-set slot 1 as RESERVED by a different user
|
|
|
|
|
|
other = User.objects.create(email="other@test.io")
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
slot.gamer = other
|
|
|
|
|
|
slot.status = GateSlot.RESERVED
|
|
|
|
|
|
slot.reserved_at = timezone.now()
|
|
|
|
|
|
slot.save()
|
|
|
|
|
|
# Current user (founder) sees coin slot locked
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.locked")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
|
2026-03-14 02:03:44 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_last_gamer_sees_pick_roles_button(self):
|
|
|
|
|
|
# Fill slots 1–5 via ORM; slot 6 empty
|
|
|
|
|
|
for i, email in enumerate([
|
|
|
|
|
|
"g1@test.io", "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io"
|
|
|
|
|
|
], start=1):
|
|
|
|
|
|
gamer = User.objects.create(email=email)
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=i)
|
|
|
|
|
|
slot.gamer = gamer
|
|
|
|
|
|
slot.status = GateSlot.FILLED
|
|
|
|
|
|
slot.save()
|
|
|
|
|
|
# Founder (no slot yet) drops token → gets slot 6
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
2026-03-14 02:25:51 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
).click()
|
|
|
|
|
|
# Slot 6 shows PICK ROLES instead of OK
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn(
|
|
|
|
|
|
"PICK ROLES",
|
|
|
|
|
|
self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".gate-slot[data-slot='6']"
|
|
|
|
|
|
).text,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
2026-03-14 22:00:16 -04:00
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TokenPriorityTest(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"}
|
|
|
|
|
|
)
|
|
|
|
|
|
self.create_pre_authenticated_session("gamer@test.io")
|
|
|
|
|
|
self.gamer = User.objects.get(email="gamer@test.io")
|
|
|
|
|
|
self.room = Room.objects.create(name="Token Room", owner=self.gamer)
|
|
|
|
|
|
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
|
|
|
|
|
|
self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN)
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_is_used_by_default(self):
|
|
|
|
|
|
# 1. COIN token created at signup, not yet leased to a room
|
|
|
|
|
|
self.assertEqual(self.coin.token_type, Token.COIN)
|
|
|
|
|
|
self.assertIsNone(self.coin.current_room)
|
|
|
|
|
|
# 2. Gamer drops token and confirms
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
# 3. Coin is now leased to this room, page not refreshed
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
|
|
|
|
|
self.coin.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.coin.current_room, self.room)
|
|
|
|
|
|
|
|
|
|
|
|
def test_free_token_used_when_coin_in_use(self):
|
|
|
|
|
|
# 1. Coin already leased to another room
|
|
|
|
|
|
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
|
|
|
|
|
|
self.coin.current_room = other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
# 2. Gamer has one unexpired free token (signup gives one; delete it and add fresh)
|
|
|
|
|
|
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
|
|
|
|
|
|
Token.objects.create(
|
|
|
|
|
|
user=self.gamer,
|
|
|
|
|
|
token_type=Token.FREE,
|
|
|
|
|
|
expires_at=timezone.now() + timedelta(days=7),
|
|
|
|
|
|
)
|
|
|
|
|
|
# 3. Gamer drops token → Free Token consumed
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0
|
|
|
|
|
|
)
|
|
|
|
|
|
# 4. Coin untouched, still leased to other room
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
|
|
|
|
|
self.coin.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.coin.current_room, other_room)
|
|
|
|
|
|
|
|
|
|
|
|
def test_tithe_token_used_when_free_tokens_exhausted(self):
|
|
|
|
|
|
# 1. Coin in use, no Free Tokens, one Tithe Token
|
|
|
|
|
|
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
|
|
|
|
|
|
self.coin.current_room = other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
|
|
|
|
|
|
tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
|
|
|
|
|
|
# 2. Gamer drops token → tithe consumed
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
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")
|
|
|
|
|
|
)
|
|
|
|
|
|
# Tithe row deleted, page hasn't refreshed
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
|
|
|
|
|
self.assertFalse(Token.objects.filter(pk=tithe.pk).exists())
|
|
|
|
|
|
|
|
|
|
|
|
def test_slot_blocked_when_no_tokens_available(self):
|
|
|
|
|
|
# Coin in use, no Free Tokens, no Tithe Tokens → depleted state
|
|
|
|
|
|
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
|
|
|
|
|
|
self.coin.current_room = other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.depleted")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_staff_backstage_pass_bypasses_token_cost(self):
|
|
|
|
|
|
# 1. Staff user has a PASS token
|
|
|
|
|
|
self.gamer.is_staff = True
|
|
|
|
|
|
self.gamer.save()
|
|
|
|
|
|
pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
|
|
|
|
|
|
# 2. Drops token, confirms as normal
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
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")
|
2026-03-14 02:03:44 -04:00
|
|
|
|
)
|
2026-03-14 22:00:16 -04:00
|
|
|
|
# 3. Pass not consumed, coin not leased; no reload
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
|
|
|
|
|
self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists())
|
|
|
|
|
|
self.coin.refresh_from_db()
|
|
|
|
|
|
self.assertIsNone(self.coin.current_room)
|
2026-03-15 01:17:09 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GameKitInsertTest(FunctionalTest):
|
|
|
|
|
|
"""Token selected from Game Kit, inserted via token-slot click."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
self.create_pre_authenticated_session("gamer@insert.io")
|
|
|
|
|
|
self.gamer = User.objects.get(email="gamer@insert.io")
|
|
|
|
|
|
self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first()
|
|
|
|
|
|
self.room = Room.objects.create(name="Insert Room", owner=self.gamer)
|
|
|
|
|
|
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
|
|
|
|
|
|
|
|
|
|
|
|
def _select_token_from_kit(self, token):
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_kit_btn").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, f"[data-token-id='{token.id}']"
|
|
|
|
|
|
).click()
|
|
|
|
|
|
)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.ready")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_insert_via_kit_reserves_slot(self):
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self._select_token_from_kit(self.coin)
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
|
|
|
|
|
|
|
|
|
|
|
def test_free_token_insert_via_kit_consumed_on_confirm(self):
|
2026-03-15 16:08:34 -04:00
|
|
|
|
self.gamer.tokens.filter(token_type=Token.FREE).delete()
|
2026-03-15 01:17:09 -04:00
|
|
|
|
token = Token.objects.create(
|
|
|
|
|
|
user=self.gamer,
|
|
|
|
|
|
token_type=Token.FREE,
|
|
|
|
|
|
expires_at=timezone.now() + timedelta(days=7),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self._select_token_from_kit(token)
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
|
|
|
|
|
|
)
|
|
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertFalse(Token.objects.filter(id=token.id).exists())
|
|
|
|
|
|
|
|
|
|
|
|
def test_tithe_token_insert_via_kit_consumed_on_confirm(self):
|
|
|
|
|
|
token = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self._select_token_from_kit(token)
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
|
|
|
|
|
|
)
|
|
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertFalse(Token.objects.filter(id=token.id).exists())
|
|
|
|
|
|
|
|
|
|
|
|
def test_pass_token_insert_via_kit_not_consumed(self):
|
|
|
|
|
|
self.gamer.is_staff = True
|
|
|
|
|
|
self.gamer.save()
|
|
|
|
|
|
pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
|
2026-03-16 00:07:52 -04:00
|
|
|
|
self.gamer.equipped_trinket = pass_token
|
|
|
|
|
|
self.gamer.save(update_fields=["equipped_trinket"])
|
2026-03-15 01:17:09 -04:00
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self._select_token_from_kit(pass_token)
|
|
|
|
|
|
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertTrue(Token.objects.filter(id=pass_token.id).exists())
|
|
|
|
|
|
self.assertEqual(self.browser.current_url, self.gate_url)
|
2026-03-30 18:31:05 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PositionIndicatorsTest(FunctionalTest):
|
|
|
|
|
|
"""Hex-side position indicators — always rendered outside the gatekeeper modal."""
|
|
|
|
|
|
|
|
|
|
|
|
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"}
|
|
|
|
|
|
)
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.founder, _ = User.objects.get_or_create(email="founder@test.io")
|
|
|
|
|
|
self.room = Room.objects.create(name="Position Test Room", owner=self.founder)
|
|
|
|
|
|
self.gate_url = (
|
|
|
|
|
|
f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test P1 — 6 position indicators present while gatekeeper is open #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_position_indicators_visible_alongside_gatekeeper(self):
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
# Gatekeeper modal is open
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
)
|
|
|
|
|
|
# Six .table-position elements are rendered outside the modal
|
|
|
|
|
|
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
|
|
|
|
|
|
self.assertEqual(len(positions), 6)
|
|
|
|
|
|
for pos in positions:
|
|
|
|
|
|
self.assertTrue(pos.is_displayed())
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test P2 — URL drops /gate/ after pick_roles #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_url_drops_gate_after_pick_roles(self):
|
|
|
|
|
|
_fill_room_via_orm(self.room, [
|
|
|
|
|
|
"founder@test.io", "amigo@test.io", "bud@test.io",
|
|
|
|
|
|
"pal@test.io", "dude@test.io", "bro@test.io",
|
|
|
|
|
|
])
|
|
|
|
|
|
# Simulate pick_roles having fired: room advances to ROLE_SELECT
|
|
|
|
|
|
self.room.table_status = Room.ROLE_SELECT
|
|
|
|
|
|
self.room.save()
|
|
|
|
|
|
|
|
|
|
|
|
# Navigating to the /gate/ URL should redirect to the plain room URL
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
expected_url = (
|
|
|
|
|
|
f"{self.live_server_url}/gameboard/room/{self.room.id}/"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertEqual(self.browser.current_url, expected_url)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test P3 — Each position has a chair icon and correct role label #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_position_shows_chair_icon_and_role_label(self):
|
|
|
|
|
|
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
)
|
|
|
|
|
|
for slot_number, role_label in SLOT_ROLE_LABELS.items():
|
|
|
|
|
|
pos = self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, f".table-position[data-slot='{slot_number}']"
|
|
|
|
|
|
)
|
|
|
|
|
|
# Chair icon present
|
|
|
|
|
|
self.assertTrue(pos.find_elements(By.CSS_SELECTOR, ".fa-chair"))
|
|
|
|
|
|
# Role label attribute and visible text
|
|
|
|
|
|
self.assertEqual(pos.get_attribute("data-role-label"), role_label)
|
|
|
|
|
|
label_el = pos.find_element(By.CSS_SELECTOR, ".position-role-label")
|
|
|
|
|
|
self.assertEqual(label_el.text.strip(), role_label)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test P4 — Unoccupied position shows ban icon #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_unoccupied_position_shows_ban_icon(self):
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
)
|
|
|
|
|
|
# All slots are empty — every position should have a ban icon
|
|
|
|
|
|
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
|
|
|
|
|
|
for pos in positions:
|
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
|
pos.find_elements(By.CSS_SELECTOR, ".fa-ban"),
|
|
|
|
|
|
f"Expected .fa-ban on slot {pos.get_attribute('data-slot')}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test P5 — Occupied position shows check icon after token confirmed #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_occupied_position_shows_check_icon_after_token_confirmed(self):
|
|
|
|
|
|
# Slot 1 is filled via ORM
|
|
|
|
|
|
from apps.epic.models import GateSlot
|
|
|
|
|
|
slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
slot.gamer = self.founder
|
|
|
|
|
|
slot.status = GateSlot.FILLED
|
|
|
|
|
|
slot.save()
|
|
|
|
|
|
|
|
|
|
|
|
self.browser.get(self.gate_url)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
|
|
|
|
|
|
)
|
|
|
|
|
|
pos1 = self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, ".table-position[data-slot='1']"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertTrue(pos1.find_elements(By.CSS_SELECTOR, ".fa-circle-check"))
|
|
|
|
|
|
self.assertFalse(pos1.find_elements(By.CSS_SELECTOR, ".fa-ban"))
|