2026-03-17 01:00:15 -04:00
|
|
|
|
import os
|
2026-03-29 18:35:20 -04:00
|
|
|
|
import unittest
|
2026-03-17 01:00:15 -04:00
|
|
|
|
|
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
|
|
|
|
from django.conf import settings as django_settings
|
2026-03-18 21:11:07 -04:00
|
|
|
|
from django.test import tag
|
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
|
|
|
|
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
|
2026-05-12 19:23:08 -04:00
|
|
|
|
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
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
|
|
|
|
from apps.applets.models import Applet
|
2026-05-12 19:23:08 -04:00
|
|
|
|
from apps.epic.models import Room, GateSlot, TableSeat
|
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
|
|
|
|
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
|
2026-03-17 01:00:15 -04:00
|
|
|
|
options2 = webdriver.FirefoxOptions()
|
|
|
|
|
|
if os.environ.get("HEADLESS"):
|
|
|
|
|
|
options2.add_argument("--headless")
|
|
|
|
|
|
self.browser2 = webdriver.Firefox(options=options2)
|
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
|
|
|
|
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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
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
|
|
|
|
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()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard()
|
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
|
|
|
|
|
|
|
|
|
|
# 6. Modal closes
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertEqual(
|
|
|
|
|
|
len(self.browser.find_elements(By.ID, "id_role_select")), 0
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# 7. Card stack returns to table centre
|
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
|
|
|
|
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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(friend) # friend is the active gamer (slot 2)
|
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
|
|
|
|
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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder) # active slot is slot 2 (also founder)
|
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
|
|
|
|
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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
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
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# Modal closes; stack still present
|
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
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 14:33:06 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# 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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
2026-03-21 14:33:06 -04:00
|
|
|
|
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()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard()
|
2026-03-21 14:33:06 -04:00
|
|
|
|
|
|
|
|
|
|
# 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")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
2026-03-21 14:33:06 -04:00
|
|
|
|
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()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard()
|
2026-03-21 14:33:06 -04:00
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Test 8a — Hex seats carry role labels during role select #
|
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
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
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.)."""
|
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
|
|
|
|
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
2026-03-31 00:01:04 -04:00
|
|
|
|
room = Room.objects.create(name="Seat Label Test", owner=founder)
|
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
|
|
|
|
_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)
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
expected = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
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
|
|
|
|
self.wait_for(
|
2026-03-31 00:01:04 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
|
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
|
|
|
|
)
|
2026-03-31 00:01:04 -04:00
|
|
|
|
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)
|
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
|
|
|
|
|
2026-03-30 18:31:05 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Test 8b — Hex seats show .fa-ban when empty #
|
2026-03-30 18:31:05 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
def test_seats_show_ban_icon_when_empty(self):
|
|
|
|
|
|
"""All 6 seats carry .fa-ban before any role has been chosen."""
|
2026-03-30 18:31:05 -04:00
|
|
|
|
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
2026-03-31 00:01:04 -04:00
|
|
|
|
room = Room.objects.create(name="Seat Ban Test", owner=founder)
|
2026-03-30 18:31:05 -04:00
|
|
|
|
_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(
|
2026-03-31 00:01:04 -04:00
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
|
2026-03-30 18:31:05 -04:00
|
|
|
|
)
|
2026-03-31 00:01:04 -04:00
|
|
|
|
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')}",
|
|
|
|
|
|
)
|
2026-03-30 18:31:05 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Test 8c — Hex seat gets .fa-circle-check after role selected #
|
2026-03-30 18:31:05 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
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."""
|
2026-03-30 18:31:05 -04:00
|
|
|
|
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
2026-03-31 00:01:04 -04:00
|
|
|
|
room = Room.objects.create(name="Seat Check Test", owner=founder)
|
2026-03-30 18:31:05 -04:00
|
|
|
|
_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)
|
|
|
|
|
|
|
2026-04-05 01:52:30 -04:00
|
|
|
|
# Open fan, pick first card (SC — Shepherd), confirm guard
|
2026-03-30 18:31:05 -04:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Wait for tray animation to complete
|
2026-03-30 18:31:05 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertFalse(
|
|
|
|
|
|
self.browser.execute_script("return Tray.isOpen()"),
|
tray: Tray.placeSig analogue of placeCard for SIG SELECT exit; rename arc-in → fade-in — TDD
After all 3 gamers in a polarity room confirm TAKE SIG and the 12s countdown
expires, sig-select.js's room:polarity_room_done handler now plays the same
tray-open / fade-in / tray-close sequence the role-select uses, then
dismisses the sig overlay & shows the waiting msg ("Gravity settling…" /
"Levity appraising…") on Tray.placeSig's completion callback. Visual order:
sig stage → tray slides in → sig fades into the second tray cell → tray
slides out → table hex w. waiting msg. Cross-polarity events (other room
finishing while we're still in our overlay) are no-op as before.
- tray.js: new Tray.placeSig(sourceEl, onComplete). Mutates the SECOND
.tray-cell in place (sig slot), copies aria-label / data-energies /
data-operations / corner-rank + suit-icon markup from the source
.sig-stage-card, then runs the shared open → fade-in → close sequence.
Extracted _runFadeInSequence helper so placeCard + placeSig share the
same animation glue. reset() now also clears .tray-sig-card from cells.
- _tray.scss: .tray-sig-card.fade-in > .sig-stage-card animates via the
existing tray-role-fade-in keyframes.
- sig-select.js polarity_room_done handler: Tray.placeSig(stageCard,
_settle); _settle runs the existing _dismissSigOverlay + _showWaitingMsg.
Falls back to immediate dismiss when Tray is undefined (test environments
without the tray).
- arc-in → fade-in rename across tray.js, role-select.js, _tray.scss
(incl. @keyframes tray-role-arc-in → tray-role-fade-in), TraySpec.js
spec descriptions + assertions, & test_room_role_select.py docstrings.
The original "arc-in" name suggested a curved-path animation; the actual
behaviour is a 1s opacity fade, so fade-in is the accurate label.
- TraySpec: 10 new placeSig specs mirroring placeCard (second-cell mutation,
data + markup copy, tabIndex, fade-in class, animationend-triggered close,
onComplete callback, landscape parity, reset cleanup).
- SigSelectSpec: 3 new specs (Tray.placeSig called w. stageCard on own
polarity; not called on other polarity; overlay dismiss deferred to the
Tray.placeSig completion callback).
344 specs / 4 pending green; RoleSelectTrayTest FT still green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:58:44 -04:00
|
|
|
|
"Tray should close after fade-in sequence",
|
2026-03-30 18:31:05 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-05 01:52:30 -04:00
|
|
|
|
# The SC seat (slot 1) now shows check, no ban
|
2026-03-30 18:31:05 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
2026-04-05 01:52:30 -04:00
|
|
|
|
By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-circle-check"
|
2026-03-30 18:31:05 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
len(self.browser.find_elements(
|
2026-04-05 01:52:30 -04:00
|
|
|
|
By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-ban"
|
2026-03-30 18:31:05 -04:00
|
|
|
|
)),
|
|
|
|
|
|
0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-29 18:35:20 -04:00
|
|
|
|
class RoleSelectTrayTest(FunctionalTest):
|
|
|
|
|
|
"""After confirming a role pick, the role card enters the tray grid and
|
|
|
|
|
|
the tray opens to reveal it.
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
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).
|
2026-03-29 18:35:20 -04:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
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"}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
def _make_room(self):
|
|
|
|
|
|
"""Room in ROLE_SELECT with all 6 seats created, slot 1 eligible."""
|
2026-03-29 18:35:20 -04:00
|
|
|
|
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
|
2026-04-28 16:29:51 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
2026-03-29 18:35:20 -04:00
|
|
|
|
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"):
|
2026-03-29 23:39:03 -04:00
|
|
|
|
TableSeat.objects.create(
|
2026-03-29 18:35:20 -04:00
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# T1 — Portrait: role card marks first cell; tray opens then closes #
|
2026-03-29 18:35:20 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
def test_portrait_role_card_enters_topmost_grid_square(self):
|
2026-03-30 16:42:23 -04:00
|
|
|
|
"""Portrait: after confirming a role the first .tray-cell gets
|
|
|
|
|
|
.tray-role-card; the grid still has exactly 8 cells; and the tray
|
tray: Tray.placeSig analogue of placeCard for SIG SELECT exit; rename arc-in → fade-in — TDD
After all 3 gamers in a polarity room confirm TAKE SIG and the 12s countdown
expires, sig-select.js's room:polarity_room_done handler now plays the same
tray-open / fade-in / tray-close sequence the role-select uses, then
dismisses the sig overlay & shows the waiting msg ("Gravity settling…" /
"Levity appraising…") on Tray.placeSig's completion callback. Visual order:
sig stage → tray slides in → sig fades into the second tray cell → tray
slides out → table hex w. waiting msg. Cross-polarity events (other room
finishing while we're still in our overlay) are no-op as before.
- tray.js: new Tray.placeSig(sourceEl, onComplete). Mutates the SECOND
.tray-cell in place (sig slot), copies aria-label / data-energies /
data-operations / corner-rank + suit-icon markup from the source
.sig-stage-card, then runs the shared open → fade-in → close sequence.
Extracted _runFadeInSequence helper so placeCard + placeSig share the
same animation glue. reset() now also clears .tray-sig-card from cells.
- _tray.scss: .tray-sig-card.fade-in > .sig-stage-card animates via the
existing tray-role-fade-in keyframes.
- sig-select.js polarity_room_done handler: Tray.placeSig(stageCard,
_settle); _settle runs the existing _dismissSigOverlay + _showWaitingMsg.
Falls back to immediate dismiss when Tray is undefined (test environments
without the tray).
- arc-in → fade-in rename across tray.js, role-select.js, _tray.scss
(incl. @keyframes tray-role-arc-in → tray-role-fade-in), TraySpec.js
spec descriptions + assertions, & test_room_role_select.py docstrings.
The original "arc-in" name suggested a curved-path animation; the actual
behaviour is a 1s opacity fade, so fade-in is the accurate label.
- TraySpec: 10 new placeSig specs mirroring placeCard (second-cell mutation,
data + markup copy, tabIndex, fade-in class, animationend-triggered close,
onComplete callback, landscape parity, reset cleanup).
- SigSelectSpec: 3 new specs (Tray.placeSig called w. stageCard on own
polarity; not called on other polarity; overlay dismiss deferred to the
Tray.placeSig completion callback).
344 specs / 4 pending green; RoleSelectTrayTest FT still green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:58:44 -04:00
|
|
|
|
opens briefly then closes once the fade-in animation completes."""
|
2026-03-29 18:35:20 -04:00
|
|
|
|
self.browser.set_window_size(390, 844)
|
2026-03-29 23:39:03 -04:00
|
|
|
|
room = self._make_room()
|
2026-03-29 18:35:20 -04:00
|
|
|
|
self.create_pre_authenticated_session("slot1@test.io")
|
|
|
|
|
|
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
|
2026-03-29 18:35:20 -04:00
|
|
|
|
self._select_role()
|
|
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# First cell receives the role card class.
|
2026-03-29 18:35:20 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
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
|
|
|
|
|
|
};
|
2026-03-29 18:35:20 -04:00
|
|
|
|
""")
|
2026-03-30 16:42:23 -04:00
|
|
|
|
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")
|
2026-03-29 18:35:20 -04:00
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# Tray closes after the animation sequence.
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertFalse(
|
|
|
|
|
|
self.browser.execute_script("return Tray.isOpen()"),
|
tray: Tray.placeSig analogue of placeCard for SIG SELECT exit; rename arc-in → fade-in — TDD
After all 3 gamers in a polarity room confirm TAKE SIG and the 12s countdown
expires, sig-select.js's room:polarity_room_done handler now plays the same
tray-open / fade-in / tray-close sequence the role-select uses, then
dismisses the sig overlay & shows the waiting msg ("Gravity settling…" /
"Levity appraising…") on Tray.placeSig's completion callback. Visual order:
sig stage → tray slides in → sig fades into the second tray cell → tray
slides out → table hex w. waiting msg. Cross-polarity events (other room
finishing while we're still in our overlay) are no-op as before.
- tray.js: new Tray.placeSig(sourceEl, onComplete). Mutates the SECOND
.tray-cell in place (sig slot), copies aria-label / data-energies /
data-operations / corner-rank + suit-icon markup from the source
.sig-stage-card, then runs the shared open → fade-in → close sequence.
Extracted _runFadeInSequence helper so placeCard + placeSig share the
same animation glue. reset() now also clears .tray-sig-card from cells.
- _tray.scss: .tray-sig-card.fade-in > .sig-stage-card animates via the
existing tray-role-fade-in keyframes.
- sig-select.js polarity_room_done handler: Tray.placeSig(stageCard,
_settle); _settle runs the existing _dismissSigOverlay + _showWaitingMsg.
Falls back to immediate dismiss when Tray is undefined (test environments
without the tray).
- arc-in → fade-in rename across tray.js, role-select.js, _tray.scss
(incl. @keyframes tray-role-arc-in → tray-role-fade-in), TraySpec.js
spec descriptions + assertions, & test_room_role_select.py docstrings.
The original "arc-in" name suggested a curved-path animation; the actual
behaviour is a 1s opacity fade, so fade-in is the accurate label.
- TraySpec: 10 new placeSig specs mirroring placeCard (second-cell mutation,
data + markup copy, tabIndex, fade-in class, animationend-triggered close,
onComplete callback, landscape parity, reset cleanup).
- SigSelectSpec: 3 new specs (Tray.placeSig called w. stageCard on own
polarity; not called on other polarity; overlay dismiss deferred to the
Tray.placeSig completion callback).
344 specs / 4 pending green; RoleSelectTrayTest FT still green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:58:44 -04:00
|
|
|
|
"Tray should close after the fade-in sequence"
|
2026-03-30 16:42:23 -04:00
|
|
|
|
)
|
2026-03-29 18:35:20 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-30 16:42:23 -04:00
|
|
|
|
# T2 — Landscape: same contract in landscape #
|
2026-03-29 18:35:20 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_landscape_role_card_enters_leftmost_grid_square(self):
|
2026-03-30 16:42:23 -04:00
|
|
|
|
"""Landscape: the first .tray-cell gets .tray-role-card; grid has
|
|
|
|
|
|
8 cells; tray opens then closes."""
|
2026-03-29 18:35:20 -04:00
|
|
|
|
self.browser.set_window_size(844, 390)
|
2026-03-29 23:39:03 -04:00
|
|
|
|
room = self._make_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("slot1@test.io")
|
2026-03-29 18:35:20 -04:00
|
|
|
|
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(
|
2026-03-29 23:39:03 -04:00
|
|
|
|
lambda: self.browser.find_element(
|
|
|
|
|
|
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
|
2026-03-29 18:35:20 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
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
|
|
|
|
|
|
};
|
2026-03-29 21:11:24 -04:00
|
|
|
|
""")
|
2026-03-30 16:42:23 -04:00
|
|
|
|
self.assertTrue(result["isFirst"], "Role card should be the first cell")
|
|
|
|
|
|
self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
|
2026-03-29 18:35:20 -04:00
|
|
|
|
|
2026-03-30 16:42:23 -04:00
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertFalse(
|
|
|
|
|
|
self.browser.execute_script("return Tray.isOpen()"),
|
tray: Tray.placeSig analogue of placeCard for SIG SELECT exit; rename arc-in → fade-in — TDD
After all 3 gamers in a polarity room confirm TAKE SIG and the 12s countdown
expires, sig-select.js's room:polarity_room_done handler now plays the same
tray-open / fade-in / tray-close sequence the role-select uses, then
dismisses the sig overlay & shows the waiting msg ("Gravity settling…" /
"Levity appraising…") on Tray.placeSig's completion callback. Visual order:
sig stage → tray slides in → sig fades into the second tray cell → tray
slides out → table hex w. waiting msg. Cross-polarity events (other room
finishing while we're still in our overlay) are no-op as before.
- tray.js: new Tray.placeSig(sourceEl, onComplete). Mutates the SECOND
.tray-cell in place (sig slot), copies aria-label / data-energies /
data-operations / corner-rank + suit-icon markup from the source
.sig-stage-card, then runs the shared open → fade-in → close sequence.
Extracted _runFadeInSequence helper so placeCard + placeSig share the
same animation glue. reset() now also clears .tray-sig-card from cells.
- _tray.scss: .tray-sig-card.fade-in > .sig-stage-card animates via the
existing tray-role-fade-in keyframes.
- sig-select.js polarity_room_done handler: Tray.placeSig(stageCard,
_settle); _settle runs the existing _dismissSigOverlay + _showWaitingMsg.
Falls back to immediate dismiss when Tray is undefined (test environments
without the tray).
- arc-in → fade-in rename across tray.js, role-select.js, _tray.scss
(incl. @keyframes tray-role-arc-in → tray-role-fade-in), TraySpec.js
spec descriptions + assertions, & test_room_role_select.py docstrings.
The original "arc-in" name suggested a curved-path animation; the actual
behaviour is a 1s opacity fade, so fade-in is the accurate label.
- TraySpec: 10 new placeSig specs mirroring placeCard (second-cell mutation,
data + markup copy, tabIndex, fade-in class, animationend-triggered close,
onComplete callback, landscape parity, reset cleanup).
- SigSelectSpec: 3 new specs (Tray.placeSig called w. stageCard on own
polarity; not called on other polarity; overlay dismiss deferred to the
Tray.placeSig completion callback).
344 specs / 4 pending green; RoleSelectTrayTest FT still green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:58:44 -04:00
|
|
|
|
"Tray should close after the fade-in sequence"
|
2026-03-30 16:42:23 -04:00
|
|
|
|
)
|
2026-03-29 18:35:20 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 21:11:07 -04:00
|
|
|
|
@tag('channels')
|
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
|
|
|
|
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")
|
2026-04-28 02:33:15 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
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
|
|
|
|
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/"
|
|
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# 1. Watcher (slot 2) loads the room
|
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
|
|
|
|
self.create_pre_authenticated_session("watcher@test.io")
|
|
|
|
|
|
self.browser.get(room_url)
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(
|
2026-03-31 00:01:04 -04:00
|
|
|
|
By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
|
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
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
# 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()
|
2026-03-23 19:31:57 -04:00
|
|
|
|
self.confirm_guard(browser=self.browser2)
|
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
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# 3. Watcher's turn arrives via WS — card-stack becomes eligible
|
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
|
|
|
|
self.wait_for(lambda: self.browser.find_element(
|
2026-03-31 00:01:04 -04:00
|
|
|
|
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
|
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
|
|
|
|
))
|
|
|
|
|
|
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)
|
2026-03-17 01:00:15 -04:00
|
|
|
|
options = webdriver.FirefoxOptions()
|
|
|
|
|
|
if os.environ.get("HEADLESS"):
|
|
|
|
|
|
options.add_argument("--headless")
|
|
|
|
|
|
b = webdriver.Firefox(options=options)
|
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
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-29 23:39:03 -04:00
|
|
|
|
# Test 5 — Tray closes on turn advance (portrait) #
|
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
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
def _make_turn_test_room(self):
|
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
|
|
|
|
founder, _ = User.objects.get_or_create(email="founder@test.io")
|
2026-04-28 02:33:15 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
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
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-03-29 23:39:03 -04:00
|
|
|
|
return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
|
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
|
|
|
|
|
2026-03-29 23:39:03 -04:00
|
|
|
|
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()
|
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
|
|
|
|
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']"
|
|
|
|
|
|
))
|
|
|
|
|
|
|
2026-03-29 23:46:23 -04:00
|
|
|
|
# Select a role — card lands in topmost grid square.
|
2026-03-29 23:39:03 -04:00
|
|
|
|
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()
|
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
|
|
|
|
|
2026-03-29 23:46:23 -04:00
|
|
|
|
# Wait for fetch .then() — card must be first child of grid.
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
|
2026-03-29 23:39:03 -04:00
|
|
|
|
var card = document.querySelector('#id_tray_grid .tray-role-card');
|
|
|
|
|
|
return card !== null && card === card.parentElement.firstElementChild;
|
2026-03-29 23:46:23 -04:00
|
|
|
|
""")))
|
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
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
|
|
|
|
|
|
self.wait_for(lambda: self.assertFalse(
|
2026-03-29 23:39:03 -04:00
|
|
|
|
self.browser.execute_script("return Tray.isOpen()"),
|
|
|
|
|
|
"Tray should be closed after turn advances"
|
2026-03-31 00:01:04 -04:00
|
|
|
|
))
|
2026-03-29 23:39:03 -04:00
|
|
|
|
|
|
|
|
|
|
def test_landscape_tray_closes_on_turn_advance(self):
|
2026-03-29 23:46:23 -04:00
|
|
|
|
"""Landscape: role card at leftmost grid square; tray closes when
|
|
|
|
|
|
turn_changed arrives via WS."""
|
2026-03-29 23:39:03 -04:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-29 23:46:23 -04:00
|
|
|
|
# Wait for fetch .then() — card must be first child of grid.
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
|
2026-03-29 23:39:03 -04:00
|
|
|
|
var card = document.querySelector('#id_tray_grid .tray-role-card');
|
|
|
|
|
|
return card !== null && card === card.parentElement.firstElementChild;
|
2026-03-29 23:46:23 -04:00
|
|
|
|
""")))
|
2026-03-29 23:39:03 -04:00
|
|
|
|
|
2026-03-31 00:01:04 -04:00
|
|
|
|
# Turn advances via WS — tray must close (forceClose in handleTurnChanged).
|
|
|
|
|
|
self.wait_for(lambda: self.assertFalse(
|
2026-03-29 23:39:03 -04:00
|
|
|
|
self.browser.execute_script("return Tray.isOpen()"),
|
|
|
|
|
|
"Tray should be closed after turn advances"
|
2026-03-31 00:01:04 -04:00
|
|
|
|
))
|
2026-03-21 22:22:06 -04:00
|
|
|
|
|
2026-04-05 01:14:31 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each `.fan-card-reversal-*` class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-`<p>` skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two `<p>`s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:25:10 -04:00
|
|
|
|
# Test 7 — SCAN SIGS appears + card stack removed on last role #
|
2026-04-05 01:14:31 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
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
|
btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each `.fan-card-reversal-*` class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-`<p>` skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two `<p>`s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:25:10 -04:00
|
|
|
|
WS event makes the SCAN SIGS button visible and removes the card
|
2026-04-05 01:14:31 -04:00
|
|
|
|
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")
|
2026-04-28 02:33:15 -04:00
|
|
|
|
_equip_earthman_deck(founder)
|
2026-04-05 01:14:31 -04:00
|
|
|
|
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()
|
|
|
|
|
|
|
btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each `.fan-card-reversal-*` class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-`<p>` skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two `<p>`s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:25:10 -04:00
|
|
|
|
# SCAN SIGS wrap must become visible via the all_roles_filled WS event.
|
2026-04-05 01:14:31 -04:00
|
|
|
|
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,
|
|
|
|
|
|
))
|
|
|
|
|
|
|
2026-03-25 01:30:18 -04:00
|
|
|
|
|