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"))