FTs: unified Sig-stage felt/stat-block (single-browser) + live SCROLL refresh (channels)
End-to-end coverage for this session's two shipped features. SigStageUnifiedTest (FunctionalTest, no WS — both pass locally): - the sig stage renders INSIDE .room-hex-pane on green --duoUser felt (.has-sig-stage), the overlay is a descendant of the hex pane, the dark .sig-backdrop is gone, and the overlay bg is not a translucent-black wash; - OK'ing a card freezes the stage and reveals the DRY _stat_face.html — .stat-face-title + .stat-chip-rank populate (the old reduced block had neither; proves the populateStatExtras wiring). RoomScrollLiveRefreshTest (ChannelsFunctionalTest, @tag channels): - with the room open, a server-side record() of a new GameEvent grows the feed (#id_drama_scroll .drama-event 1 → 2) WITHOUT a reload, via the record() on_commit broadcast → RoomConsumer.scroll_update relay → room-scroll.js re-fetch+swap. Validated in the CI channels stage (needs a cross-process channel layer); the plumbing is already green via the consumer-relay + record-hook + scroll_status ITs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,12 @@ the hex. No partial scroll — `scroll-snap-stop: always` enforces the binary.
|
||||
|
||||
FT bucket: game_room. Built to DRY-template onto my_sea next.
|
||||
"""
|
||||
from django.test import tag
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .base import FunctionalTest, ChannelsFunctionalTest
|
||||
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||
from apps.drama.models import GameEvent
|
||||
from apps.drama.models import GameEvent, record
|
||||
from apps.epic.models import Room
|
||||
from apps.lyric.models import User
|
||||
|
||||
@@ -189,3 +190,54 @@ class RoomScrollOfEventsTest(FunctionalTest):
|
||||
By.CSS_SELECTOR,
|
||||
"#id_drama_scroll .drama-event[data-label='frame']",
|
||||
).is_displayed())
|
||||
|
||||
|
||||
@tag("channels")
|
||||
class RoomScrollLiveRefreshTest(ChannelsFunctionalTest):
|
||||
"""The SCROLL applet refreshes live over WebSocket — no page reload. A
|
||||
GameEvent recorded by any actor nudges every open room socket (record() →
|
||||
on_commit broadcast → RoomConsumer.scroll_update relay → room-scroll.js
|
||||
re-fetches `core/_partials/_scroll.html` + swaps #id_drama_scroll), so an
|
||||
open feed grows on its own. Needs daphne + a real channel layer (channels
|
||||
stage)."""
|
||||
|
||||
EMAILS = [
|
||||
"disco@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.viewer = User.objects.create(email="disco@test.io", username="disco")
|
||||
_equip_earthman_deck(self.viewer)
|
||||
self.room = Room.objects.create(name="Willawonky", owner=self.viewer)
|
||||
_fill_room_via_orm(self.room, self.EMAILS)
|
||||
# One seed line so the feed isn't empty at open.
|
||||
record(self.room, GameEvent.SLOT_FILLED, actor=self.viewer,
|
||||
slot_number=1, token_type="carte", token_display="Carte Blanche",
|
||||
renewal_days=7)
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
|
||||
def _rows(self):
|
||||
return len(self.browser.find_elements(
|
||||
By.CSS_SELECTOR, "#id_drama_scroll .drama-event"))
|
||||
|
||||
def test_feed_grows_live_when_an_event_is_recorded(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.set_window_size(820, 600)
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{self.room.id}/")
|
||||
# Wait for the room WebSocket to actually open (room.js sets _roomSocket).
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.execute_script(
|
||||
"return window._roomSocket ? window._roomSocket.readyState : -1;"),
|
||||
1))
|
||||
self.wait_for(lambda: self.assertEqual(self._rows(), 1))
|
||||
|
||||
# Another action is recorded server-side → broadcasts scroll_update on
|
||||
# commit; the open feed must grow WITHOUT a reload.
|
||||
record(self.room, GameEvent.SLOT_FILLED, actor=self.viewer,
|
||||
slot_number=2, token_type="carte", token_display="Carte Blanche",
|
||||
renewal_days=7)
|
||||
self.wait_for(lambda: self.assertEqual(self._rows(), 2))
|
||||
|
||||
@@ -325,6 +325,72 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
self.assertEqual(corr.text, "")
|
||||
|
||||
|
||||
class SigStageUnifiedTest(FunctionalTest):
|
||||
"""The Sig Select stage unified with the my_sign card-stage apparatus
|
||||
(2026-06-03): the green --duoUser felt fills the hex pane (no dark Gaussian
|
||||
backdrop), and the stat-block is the shared DRY `_stat_face.html` (rank-chip
|
||||
+ title + arcana), not the old label-only copy. No WebSocket — local stage
|
||||
updates; plain FunctionalTest."""
|
||||
|
||||
EMAILS = [
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.browser.set_window_size(800, 1200)
|
||||
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"})
|
||||
|
||||
def _open_sig(self):
|
||||
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
|
||||
room = Room.objects.create(name="Felt Test", owner=founder)
|
||||
_fill_room_via_orm(room, self.EMAILS)
|
||||
_assign_all_roles(room)
|
||||
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
return room
|
||||
|
||||
def test_stage_fills_hex_pane_on_felt_no_dark_backdrop(self):
|
||||
"""The sig stage renders INSIDE the hex pane on edge-to-edge green felt
|
||||
(my_sea-style), replacing the old fixed dark-Gaussian modal."""
|
||||
self._open_sig()
|
||||
self.assertTrue(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".room-hex-pane.has-sig-stage"))
|
||||
# The overlay is a descendant of the hex pane (not a root-level modal).
|
||||
inside = self.browser.execute_script(
|
||||
"var o=document.querySelector('.sig-overlay');"
|
||||
"return !!(o && o.closest('.room-hex-pane'));")
|
||||
self.assertTrue(inside)
|
||||
# The dark blur backdrop element is gone…
|
||||
self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".sig-backdrop"))
|
||||
# …and the overlay's own background is the green --duoUser felt, not a
|
||||
# translucent-black wash.
|
||||
bg = self.browser.execute_script(
|
||||
"return getComputedStyle(document.querySelector('.sig-overlay'))"
|
||||
".backgroundColor;")
|
||||
self.assertNotIn("rgba(0, 0, 0", bg)
|
||||
|
||||
def test_stat_block_is_dry_stat_face_with_rank_and_title(self):
|
||||
"""OK'ing a card freezes the stage and reveals the rich DRY stat-face —
|
||||
rank-chip + title + arcana (the old reduced block had none of these)."""
|
||||
self._open_sig()
|
||||
card = self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-card"))
|
||||
card.click()
|
||||
ok = self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-ok-btn"))
|
||||
ok.click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-stage--frozen"))
|
||||
# populateStatExtras filled the title + rank chip (was the missing call).
|
||||
title = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".sig-stat-block .stat-face-title")
|
||||
rank = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".sig-stat-block .stat-chip-rank")
|
||||
self.wait_for(lambda: self.assertTrue(title.text.strip()))
|
||||
self.assertTrue(rank.text.strip())
|
||||
|
||||
|
||||
# ── SAVE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
|
||||
#
|
||||
# SAVE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
|
||||
|
||||
Reference in New Issue
Block a user