import time from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet from apps.drama.models import GameEvent, record from apps.epic.models import Room, GateSlot from apps.lyric.models import User class BillboardScrollTest(FunctionalTest): """ FT: game actions in room.html are logged to the drama stream, and the founder can navigate from any page to /billboard/ via the footer fa-scroll icon, select a room, and read the provenance scroll. Events are seeded via ORM — the IT suite covers the recording side; here we test the user-visible navigation and prose display. """ def setUp(self): super().setUp() for slug, name, cols, rows in [ ("billboard-my-scrolls", "My Scrolls", 4, 3), ("billboard-my-contacts", "Contacts", 4, 3), ("billboard-most-recent", "Most Recent", 8, 6), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"}, ) self.founder = User.objects.create(email="founder@test.io") self.other = User.objects.create(email="other@test.io") self.room = Room.objects.create(name="Blissful Ignorance", owner=self.founder) # Simulate two gate fills — one by founder, one by the other gamer record( self.room, GameEvent.SLOT_FILLED, actor=self.founder, slot_number=1, token_type="coin", token_display="Coin-on-a-String", renewal_days=7, ) record( self.room, GameEvent.SLOT_FILLED, actor=self.other, slot_number=2, token_type="Free", token_display="Free Token", renewal_days=7, ) # Simulate founder selecting a role record( self.room, GameEvent.ROLE_SELECTED, actor=self.founder, role="PC", slot_number=1, role_display="Player", ) # ------------------------------------------------------------------ # # Test 1 — footer icon navigates to billboard, rooms listed # # ------------------------------------------------------------------ # def test_footer_scroll_icon_leads_to_billboard_with_rooms(self): # Founder logs in and lands on the dashboard self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/") # Footer contains a scroll icon link pointing to /billboard/ scroll_link = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_footer_nav a[href='/billboard/']") ) scroll_link.click() # Billboard page lists the founder's room self.wait_for( lambda: self.assertIn("/billboard/", self.browser.current_url) ) body = self.browser.find_element(By.TAG_NAME, "body") self.assertIn("Blissful Ignorance", body.text) # ------------------------------------------------------------------ # # Test 2 — scroll page renders human-readable prose # # ------------------------------------------------------------------ # def test_scroll_shows_human_readable_event_log(self): self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/billboard/") # Click the room link to reach the scroll self.wait_for( lambda: self.browser.find_element(By.LINK_TEXT, "Blissful Ignorance") ).click() self.wait_for( lambda: self.assertIn("/scroll/", self.browser.current_url) ) scroll = self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) # Gate fill events are rendered as prose self.assertIn("deposits a Coin-on-a-String for slot 1 (7 days)", scroll.text) self.assertIn("deposits a Free Token for slot 2 (7 days)", scroll.text) # Role selection event is rendered as prose self.assertIn("elects to start as Player", scroll.text) # ------------------------------------------------------------------ # # Test 3 — current user's events are right-aligned; others' are left # # ------------------------------------------------------------------ # def test_scroll_aligns_own_events_right_and_others_left(self): self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/billboard/") self.wait_for( lambda: self.browser.find_element(By.LINK_TEXT, "Blissful Ignorance") ).click() self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) mine_events = self.browser.find_elements(By.CSS_SELECTOR, ".drama-event.mine") theirs_events = self.browser.find_elements(By.CSS_SELECTOR, ".drama-event.theirs") # Founder has 2 events (slot fill + role select); other gamer has 1 self.assertEqual(len(mine_events), 2) self.assertEqual(len(theirs_events), 1) # The other gamer's event mentions their display name self.assertIn("other", theirs_events[0].text) class BillscrollPositionTest(FunctionalTest): """ FT: the user's scroll position in a billscroll is saved to the server and restored when they return to the same scroll from any device/session. """ def setUp(self): super().setUp() self.founder = User.objects.create(email="founder@scrollpos.io") self.room = Room.objects.create(name="Persistent Chamber", owner=self.founder) # Enough events to make #id_drama_scroll scrollable for i in range(20): record( self.room, GameEvent.SLOT_FILLED, actor=self.founder, slot_number=(i % 6) + 1, token_type="coin", token_display=f"Coin-{i}", renewal_days=7, ) def test_scroll_position_persists_across_sessions(self): # 1. Log in and navigate to the room's billscroll self.create_pre_authenticated_session("founder@scrollpos.io") self.browser.get( self.live_server_url + f"/billboard/room/{self.room.id}/scroll/" ) scroll_el = self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) # 2. Force the element scrollable (CSS not served by StaticLiveServerTestCase), # set position, dispatch scroll event, and capture the position value the # JS listener will save — all in one synchronous script so the layout # snapshot is identical to what the scroll handler sees. scroll_top = 100 saved_pos = self.browser.execute_script(""" var el = arguments[0]; var remPx = parseFloat(getComputedStyle(document.documentElement).fontSize); el.style.overflow = 'auto'; el.style.height = '150px'; el.scrollTop = arguments[1]; var pos = Math.round(el.scrollTop + el.clientHeight + remPx * 2.5); el.dispatchEvent(new Event('scroll')); return pos; """, scroll_el, scroll_top) # 3. Wait for debounce (800ms) + fetch to complete time.sleep(3) # 4. Navigate away and back in a fresh session self.browser.get(self.live_server_url + "/billboard/") self.create_pre_authenticated_session("founder@scrollpos.io") self.browser.get( self.live_server_url + f"/billboard/room/{self.room.id}/scroll/" ) # 5. The saved position is reflected in the page's data attribute scroll_el = self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) restored = int(scroll_el.get_attribute("data-scroll-position")) self.assertEqual(restored, saved_pos) class BillboardAppletsTest(FunctionalTest): """ FT: billboard page renders three applets in the grid — My Scrolls, My Contacts, and Most Recent — with a functioning gear menu. """ def setUp(self): super().setUp() self.founder = User.objects.create(email="founder@test.io") self.room = Room.objects.create(name="Arcane Assembly", owner=self.founder) for slug, name, cols, rows in [ ("billboard-my-scrolls", "My Scrolls", 4, 3), ("billboard-my-contacts", "Contacts", 4, 3), ("billboard-most-recent", "Most Recent", 8, 6), ]: Applet.objects.get_or_create( slug=slug, defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"}, ) def test_billboard_shows_three_applets(self): # 1. Log in, navigate to billboard self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/billboard/") # 2. Assert all three applet sections present self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_billboard_my_scrolls") ) self.browser.find_element(By.ID, "id_applet_billboard_my_contacts") self.browser.find_element(By.ID, "id_applet_billboard_most_recent") def test_billboard_my_scrolls_lists_rooms(self): # 1. Log in, navigate to billboard self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/billboard/") # 2. My Scrolls applet contains a link to the room's scroll self.wait_for( lambda: self.assertIn( "Arcane Assembly", self.browser.find_element(By.ID, "id_applet_billboard_my_scrolls").text, ) ) def test_billboard_gear_btn_opens_applet_menu(self): # 1. Log in, navigate to billboard self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.live_server_url + "/billboard/") # 2. Gear button is visible gear_btn = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".billboard-page .gear-btn") ) # 3. Menu is hidden before click menu = self.browser.find_element(By.ID, "id_billboard_applet_menu") self.assertFalse(menu.is_displayed()) # 4. Clicking gear opens the menu (JS click bypasses kit-bag overlap in headless) self.browser.execute_script("arguments[0].click()", gear_btn) self.wait_for_slow(lambda: self.assertTrue(menu.is_displayed())) class BillscrollAppletsTest(FunctionalTest): """ FT: billscroll page renders as a single full-width applet that fills the viewport aperture and contains the room's drama events. """ def setUp(self): super().setUp() self.founder = User.objects.create(email="founder@billtest.io") self.room = Room.objects.create(name="Spectral Council", owner=self.founder) record( self.room, GameEvent.SLOT_FILLED, actor=self.founder, slot_number=1, token_type="coin", token_display="Coin-on-a-String", renewal_days=7, ) def test_billscroll_shows_full_width_applet(self): # 1. Log in, navigate to the room's scroll self.create_pre_authenticated_session("founder@billtest.io") self.browser.get( self.live_server_url + f"/billboard/room/{self.room.id}/scroll/" ) # 2. The full-width applet section is present self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_billboard_scroll") ) def test_billscroll_applet_contains_drama_events(self): # 1. Log in, navigate to the room's scroll self.create_pre_authenticated_session("founder@billtest.io") self.browser.get( self.live_server_url + f"/billboard/room/{self.room.id}/scroll/" ) # 2. Drama scroll is inside the applet and shows the event scroll = self.wait_for( lambda: self.browser.find_element(By.ID, "id_drama_scroll") ) self.assertIn("Coin-on-a-String", scroll.text)