hex position indicators: chair icons at hex edge midpoints replace gate-slot circles

- Split .gate-overlay into .gate-backdrop (z-100, blur) + .gate-overlay modal (z-120) so .table-position elements (z-110) render above backdrop but below modal
- New _table_positions.html partial: 6 .table-position divs with .fa-chair, role label, and .fa-ban/.fa-circle-check status icons; included unconditionally in room.html
- New epic:room view at /gameboard/room/<uuid>/; gatekeeper redirects there when table_status set; pick_roles redirects there
- role-select.js: adds .active glow to position on selectRole(); swaps .fa-ban→.fa-circle-check in placeCard onComplete; handleTurnChanged clears stale .active from all positions
- FTs: PositionIndicatorsTest (5 tests) + RoleSelectTest 8a/8b (glow + check state)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-03-30 18:31:05 -04:00
parent 8b006be138
commit a8592aeaec
11 changed files with 370 additions and 35 deletions

View File

@@ -8,6 +8,7 @@ from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, select_token
from apps.lyric.models import Token, User
from .test_room_role_select import _fill_room_via_orm
class GatekeeperTest(FunctionalTest):
@@ -585,3 +586,120 @@ class GameKitInsertTest(FunctionalTest):
)
self.assertTrue(Token.objects.filter(id=pass_token.id).exists())
self.assertEqual(self.browser.current_url, self.gate_url)
class PositionIndicatorsTest(FunctionalTest):
"""Hex-side position indicators — always rendered outside the gatekeeper modal."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
self.create_pre_authenticated_session("founder@test.io")
self.founder, _ = User.objects.get_or_create(email="founder@test.io")
self.room = Room.objects.create(name="Position Test Room", owner=self.founder)
self.gate_url = (
f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
)
# ------------------------------------------------------------------ #
# Test P1 — 6 position indicators present while gatekeeper is open #
# ------------------------------------------------------------------ #
def test_position_indicators_visible_alongside_gatekeeper(self):
self.browser.get(self.gate_url)
# Gatekeeper modal is open
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
# Six .table-position elements are rendered outside the modal
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
self.assertEqual(len(positions), 6)
for pos in positions:
self.assertTrue(pos.is_displayed())
# ------------------------------------------------------------------ #
# Test P2 — URL drops /gate/ after pick_roles #
# ------------------------------------------------------------------ #
def test_url_drops_gate_after_pick_roles(self):
_fill_room_via_orm(self.room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
# Simulate pick_roles having fired: room advances to ROLE_SELECT
self.room.table_status = Room.ROLE_SELECT
self.room.save()
# Navigating to the /gate/ URL should redirect to the plain room URL
self.browser.get(self.gate_url)
expected_url = (
f"{self.live_server_url}/gameboard/room/{self.room.id}/"
)
self.wait_for(
lambda: self.assertEqual(self.browser.current_url, expected_url)
)
# ------------------------------------------------------------------ #
# Test P3 — Each position has a chair icon and correct role label #
# ------------------------------------------------------------------ #
def test_position_shows_chair_icon_and_role_label(self):
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
for slot_number, role_label in SLOT_ROLE_LABELS.items():
pos = self.browser.find_element(
By.CSS_SELECTOR, f".table-position[data-slot='{slot_number}']"
)
# Chair icon present
self.assertTrue(pos.find_elements(By.CSS_SELECTOR, ".fa-chair"))
# Role label attribute and visible text
self.assertEqual(pos.get_attribute("data-role-label"), role_label)
label_el = pos.find_element(By.CSS_SELECTOR, ".position-role-label")
self.assertEqual(label_el.text.strip(), role_label)
# ------------------------------------------------------------------ #
# Test P4 — Unoccupied position shows ban icon #
# ------------------------------------------------------------------ #
def test_unoccupied_position_shows_ban_icon(self):
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
# All slots are empty — every position should have a ban icon
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
for pos in positions:
self.assertTrue(
pos.find_elements(By.CSS_SELECTOR, ".fa-ban"),
f"Expected .fa-ban on slot {pos.get_attribute('data-slot')}",
)
# ------------------------------------------------------------------ #
# Test P5 — Occupied position shows check icon after token confirmed #
# ------------------------------------------------------------------ #
def test_occupied_position_shows_check_icon_after_token_confirmed(self):
# Slot 1 is filled via ORM
from apps.epic.models import GateSlot
slot = self.room.gate_slots.get(slot_number=1)
slot.gamer = self.founder
slot.status = GateSlot.FILLED
slot.save()
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
pos1 = self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-slot='1']"
)
self.assertTrue(pos1.find_elements(By.CSS_SELECTOR, ".fa-circle-check"))
self.assertFalse(pos1.find_elements(By.CSS_SELECTOR, ".fa-ban"))

View File

@@ -484,6 +484,104 @@ class RoleSelectTest(FunctionalTest):
)
# ------------------------------------------------------------------ #
# Test 8a — Position glows while role card is being placed #
# ------------------------------------------------------------------ #
def test_position_glows_when_role_card_confirmed(self):
"""Immediately after confirming a role pick, the matching
.table-position should receive .active (the glow state) before
the tray animation completes."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Position Glow 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, click first card (PC), confirm guard
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# PC position gains .active immediately after confirmation
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'].active"
)
)
# ------------------------------------------------------------------ #
# Test 8b — Position shows check icon after tray sequence ends #
# ------------------------------------------------------------------ #
def test_position_gets_check_when_tray_sequence_ends(self):
"""After the tray arc-in animation completes and the tray closes,
the PC .table-position should show .fa-circle-check and no .fa-ban."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Position Check Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
# Open fan, pick PC card, confirm guard
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# Wait for tray animation to complete (tray closes)
self.wait_for(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should close after arc-in sequence"
)
)
# PC position now shows check icon, ban icon gone
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-circle-check"
)
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-ban"
)),
0,
)
class RoleSelectTrayTest(FunctionalTest):
"""After confirming a role pick, the role card enters the tray grid and
the tray opens to reveal it.