room scroll-of-events: applet-box card styling + scroll-driven gear menu (Frame/Redact filter) — TDD
The room scroll pane now matches scroll.html: the feed sits in a .applet-scroll %applet-box card with the rotated room-name title; dropped the special --duoUser pane bg (the dark card sits on the room-page bg). Gear menu is now view-aware. #id_room_menu carries two panes: .room-menu-default (the existing NVM/DEL/BYE) + .room-menu-scroll (a Frame/Redact #id_scroll_filter_form, rendered only when the gear include gets scroll_filter — room.html passes scroll_filter=room.table_status). room-scroll.js (NEW) runs an IntersectionObserver on .room-scroll-pane (root=#id_room_aperture): scrolled to the feed -> show the filter pane; back on the hex -> show the default. The filter mirrors scroll.html (per-room localStorage, toggles .drama-event[data-label] display). Buffer-dots animation moved from the inline partial script into room-scroll.js. Other views keep their own menus, as asked: GATE VIEW (room_gate.html) includes _room_gear.html with nvm_url only (no scroll_filter, no room-scroll.js) -> NVM(->hex)/DEL/BYE; the cross/spread phase is a modal over the hex (scrollTop 0) -> default pane. Traps: applets.js caches gear.dataset.menuTarget at bind time, so you can't swap a gear's target to a 2nd menu — both panes live in ONE #id_room_menu and JS toggles visibility. .room-menu-default is display:contents so wrapping the existing controls doesn't change their layout (JS toggles none<->contents, not ''). Tests: +3 ITs (RoomScrollOfEventsTest — .applet-scroll card + room-name title, filter pane renders in table phase, filter absent in gate phase); +2 FTs (test_game_room_scroll — gear swaps to filter when scrolled to feed, unchecking Redact+OK hides struck rows). 8 scroll ITs + 4 scroll FTs green; 554 epic ITs/UTs green; gatekeeper DEL+BYE gear FTs green (the .room-menu-default wrap is layout-neutral). [[project-room-scroll-of-events]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,13 +26,19 @@ class RoomScrollOfEventsTest(FunctionalTest):
|
||||
["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)."
|
||||
# A deposit line for the feed (data-label="frame") — renders via the
|
||||
# shared scroll partial as "deposits a Carte Blanche for slot 1 …".
|
||||
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},
|
||||
)
|
||||
# A retracted SIG_READY (struck → data-label="redact") so the Frame /
|
||||
# Redact filter has both kinds of row to toggle.
|
||||
GameEvent.objects.create(
|
||||
room=self.room, actor=self.viewer, verb=GameEvent.SIG_READY,
|
||||
data={"retracted": True, "card_name": "The Nomad"},
|
||||
)
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
@@ -93,3 +99,60 @@ class RoomScrollOfEventsTest(FunctionalTest):
|
||||
"deposits a Carte Blanche for slot 1",
|
||||
self.browser.find_element(By.ID, "id_drama_scroll").text,
|
||||
)
|
||||
|
||||
def _open_gear(self):
|
||||
gear = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".gear-btn[data-menu-target='id_room_menu']")
|
||||
self.browser.execute_script("arguments[0].click();", gear)
|
||||
|
||||
def test_gear_swaps_to_filter_when_scrolled_to_feed(self):
|
||||
"""At the hex the gear shows the room menu (NVM/DEL/BYE); scrolled to
|
||||
the feed it swaps to the Frame/Redact filter."""
|
||||
self._open()
|
||||
# Hex view: open the gear → the room menu is visible, the filter isn't.
|
||||
self._open_gear()
|
||||
self.wait_for(lambda: self.assertTrue(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_room_menu .room-menu-default a.btn-cancel"
|
||||
).is_displayed()))
|
||||
self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed())
|
||||
|
||||
# Scroll to the feed → the gear live-swaps to the filter pane.
|
||||
self.browser.execute_script(
|
||||
"document.querySelector('.room-aperture').scrollTop = 99999;")
|
||||
self.wait_for(lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))
|
||||
self.assertFalse(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_room_menu .room-menu-default a.btn-cancel"
|
||||
).is_displayed())
|
||||
|
||||
def test_redact_filter_hides_struck_rows(self):
|
||||
"""Unchecking Redact + OK in the scroll-view gear hides the struck
|
||||
(retracted) rows while the framed ones stay."""
|
||||
self._open()
|
||||
self.browser.execute_script(
|
||||
"document.querySelector('.room-aperture').scrollTop = 99999;")
|
||||
self._open_gear()
|
||||
self.wait_for(lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))
|
||||
|
||||
redact_cb = self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_scroll_filter_form input[value='redact']")
|
||||
if redact_cb.is_selected():
|
||||
self.browser.execute_script("arguments[0].click();", redact_cb)
|
||||
ok = self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_scroll_filter_form button[type='submit']")
|
||||
self.browser.execute_script("arguments[0].click();", ok)
|
||||
|
||||
self.wait_for(lambda: self.assertFalse(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"#id_drama_scroll .drama-event[data-label='redact']",
|
||||
).is_displayed()))
|
||||
self.assertTrue(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"#id_drama_scroll .drama-event[data-label='frame']",
|
||||
).is_displayed())
|
||||
|
||||
Reference in New Issue
Block a user