room scroll-of-events: table-hex aperture binary scroll-snaps to the room provenance feed — TDD
From Role Select onwards, scrolling DOWN in the table-hex aperture swaps the entire hex view for the room's GameEvent feed (mirrors my_sky's wheel<->form; scroll-snap-stop:always = no partial scroll). Reuses the Billscroll query + core/_partials/_scroll.html so the feed renders identically.
room.html: #id_room_aperture wraps .room-hex-pane (existing .room-shell + the _table_positions strip moved INSIDE so the circles scroll away with the hex) + .room-scroll-pane (includes new _room_scroll.html); 'is-scrollable' added iff table_status set.
_room_scroll.html (new) = the DRY seam for my_sea; includes the shared scroll partial + a tiny dots-animation script (no scroll-position persistence). room_view adds events/viewer/scroll_position (same query as billboard.views.scroll).
_room.scss: .room-aperture + .room-pane (height:100%, not min-height); .is-scrollable engages scroll-snap-type:y mandatory + per-pane scroll-snap-align:start & scroll-snap-stop:always; .room-scroll-pane styles #id_drama_scroll + .scroll-buffer { margin-top:auto } (pure-CSS bottom-pin).
Trap: the aperture & panes set NO z-index/transform/opacity/filter -> NO stacking context, so the position strip's z-130 still resolves in the root context, above the gate/sig overlays (z-100/120). Verified by gatekeeper FTs (token drop + circle/modal layering).
Deferred INDEFINITELY (user): rising-game-cost + max room membership + max simultaneous CARTE slots + in-slot token combinations — until CARTE is anything but a secret type of Trinket.
Tests: RoomScrollOfEventsTest (5 ITs — aperture wraps hex+scroll panes, is-scrollable from Role Select, feed renders + scoped to room, no scroll pane in gate phase); functional_tests/test_game_room_scroll.py (2 FTs — computed scroll-snap props + scroll-down reveals the feed). 551 epic ITs/UTs green; 2 new FTs green; gatekeeper (token-drop + circle layering) + role-select (card fan) FTs green.
[[project-room-scroll-of-events]] [[project-position-circle-tooltips]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
src/functional_tests/test_game_room_scroll.py
Normal file
95
src/functional_tests/test_game_room_scroll.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Room scroll-of-events FT — the table-hex aperture's binary scroll-snap
|
||||
toggle (mirrors my_sky's wheel<->form swap). From Role Select onwards the
|
||||
viewer scrolls DOWN past the hex to reveal the room's provenance feed (the
|
||||
same GameEvent stream the Billscroll page shows); scrolling back up snaps to
|
||||
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 selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||
from apps.drama.models import GameEvent
|
||||
from apps.epic.models import Room
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
class RoomScrollOfEventsTest(FunctionalTest):
|
||||
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="Whataburgher", owner=self.viewer)
|
||||
_fill_room_via_orm(
|
||||
self.room,
|
||||
["disco@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io"],
|
||||
)
|
||||
# A deposit line for the feed — renders via the shared scroll partial as
|
||||
# "deposits a Carte Blanche for slot 1 (expires in 7 days)."
|
||||
GameEvent.objects.create(
|
||||
room=self.room, actor=self.viewer, verb=GameEvent.SLOT_FILLED,
|
||||
data={"token_type": "carte", "token_display": "Carte Blanche",
|
||||
"slot_number": 1, "renewal_days": 7},
|
||||
)
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
self.url = self.live_server_url + f"/gameboard/room/{self.room.id}/"
|
||||
|
||||
def _open(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.set_window_size(820, 600)
|
||||
self.browser.get(self.url)
|
||||
return self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".room-aperture")
|
||||
)
|
||||
|
||||
def _in_viewport(self, css):
|
||||
return self.browser.execute_script(
|
||||
"var e=document.querySelector(arguments[0]); if(!e) return false;"
|
||||
"var r=e.getBoundingClientRect();"
|
||||
"return r.top < window.innerHeight && r.bottom > 0;", css)
|
||||
|
||||
def test_aperture_binary_scroll_snap_with_two_panes(self):
|
||||
"""The aperture is scroll-snap-y-mandatory and both panes snap to the
|
||||
start with `always` — without these it's a free scroll, not the binary
|
||||
hex<->scroll toggle."""
|
||||
self._open()
|
||||
snap = self.browser.execute_script(
|
||||
"return getComputedStyle(document.querySelector('.room-aperture'))"
|
||||
".scrollSnapType;"
|
||||
)
|
||||
self.assertIn("y", snap)
|
||||
self.assertIn("mandatory", snap)
|
||||
for pane in (".room-hex-pane", ".room-scroll-pane"):
|
||||
align = self.browser.execute_script(
|
||||
"return getComputedStyle(document.querySelector(arguments[0]))"
|
||||
".scrollSnapAlign;", pane,
|
||||
)
|
||||
self.assertIn("start", align)
|
||||
|
||||
def test_scroll_down_reveals_event_feed(self):
|
||||
"""The feed sits below the hex (off-screen); scrolling the aperture
|
||||
down brings it into view — the hex content is replaced entirely."""
|
||||
self._open()
|
||||
# The event feed starts off-screen, below the hex pane.
|
||||
self.assertFalse(self._in_viewport("#id_drama_scroll"))
|
||||
|
||||
self.browser.execute_script(
|
||||
"document.querySelector('.room-aperture').scrollTop = 99999;"
|
||||
)
|
||||
# The aperture actually scrolled to the 2nd snap section…
|
||||
self.wait_for(lambda: self.assertGreater(
|
||||
self.browser.execute_script(
|
||||
"return document.querySelector('.room-aperture').scrollTop;"
|
||||
),
|
||||
10,
|
||||
))
|
||||
# …and the provenance feed (with the deposit line) is now in view.
|
||||
self.wait_for(lambda: self.assertTrue(self._in_viewport("#id_drama_scroll")))
|
||||
self.assertIn(
|
||||
"deposits a Carte Blanche for slot 1",
|
||||
self.browser.find_element(By.ID, "id_drama_scroll").text,
|
||||
)
|
||||
Reference in New Issue
Block a user