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:
@@ -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"))
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user