wired PICK SKY server-side polarity countdown via threading.Timer (tasks.py); fixed polarity_done overlay gating on refresh; cleared sig-select floats on overlay dismiss; filtered Redact events from Most Recent applet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,7 +101,7 @@ class BillboardScrollTest(FunctionalTest):
|
||||
self.assertIn("deposits a Free Token for slot 2 (expires in 7 days).", scroll.text)
|
||||
|
||||
# Role selection event is rendered as prose
|
||||
self.assertIn("elects to start as the Player", scroll.text)
|
||||
self.assertIn("assumes 1st Chair", scroll.text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 3 — current user's events are right-aligned; others' are left #
|
||||
@@ -354,3 +354,149 @@ class BillscrollEntryLayoutTest(FunctionalTest):
|
||||
# events[0] is the backdated record (oldest first, ascending order)
|
||||
old_ts = events[0].find_element(By.CSS_SELECTOR, ".drama-event-time")
|
||||
self.assertRegex(old_ts.text, r"\d{2}\s+[A-Z][a-z]{2}\s+\d{4}")
|
||||
|
||||
|
||||
class BillscrollGearMenuTest(FunctionalTest):
|
||||
"""
|
||||
FT: the billscroll page has a gear menu that filters events by label.
|
||||
|
||||
Frame = all regular (non-struck) drama entries.
|
||||
Redact = struck-through (retracted) entries, e.g. a WAIT NVM after TAKE SIG.
|
||||
|
||||
Scenario (one gamer, Role + Sig events):
|
||||
1. Both labels checked by default — all events visible.
|
||||
2. Uncheck Redact → OK: struck entries disappear.
|
||||
3. Recheck Redact + uncheck Frame → OK: regular entries gone; struck
|
||||
entries visible (but still render struck-through — they remain "redacted"
|
||||
in the narrative sense).
|
||||
4. Recheck Frame → OK: all entries return.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.founder = User.objects.create(email="founder@geartest.io")
|
||||
self.room = Room.objects.create(name="Gear Filter Room", owner=self.founder)
|
||||
# Two Frame events — ROLE_SELECTED, non-struck
|
||||
record(self.room, GameEvent.ROLE_SELECTED, actor=self.founder,
|
||||
role="PC", slot_number=1, role_display="Player")
|
||||
record(self.room, GameEvent.ROLE_SELECTED, actor=self.founder,
|
||||
role="NC", slot_number=2, role_display="Narrator")
|
||||
# Two Redact events — SIG_READY with retracted=True → event.struck is True
|
||||
sig1 = record(self.room, GameEvent.SIG_READY, actor=self.founder,
|
||||
card_name="The Wanderer", corner_rank="0", suit_icon="")
|
||||
sig1.data["retracted"] = True
|
||||
sig1.save(update_fields=["data"])
|
||||
sig2 = record(self.room, GameEvent.SIG_READY, actor=self.founder,
|
||||
card_name="Maid of Brands", corner_rank="M",
|
||||
suit_icon="fa-wand-sparkles")
|
||||
sig2.data["retracted"] = True
|
||||
sig2.save(update_fields=["data"])
|
||||
|
||||
def _go_to_scroll(self):
|
||||
self.create_pre_authenticated_session("founder@geartest.io")
|
||||
self.browser.get(
|
||||
self.live_server_url + f"/billboard/room/{self.room.id}/scroll/"
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_drama_scroll")
|
||||
)
|
||||
|
||||
def _open_gear(self):
|
||||
"""Click the gear btn and return the now-visible menu element."""
|
||||
gear = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".gear-btn[data-menu-target='id_billscroll_menu']"
|
||||
)
|
||||
)
|
||||
self.browser.execute_script("arguments[0].click()", gear)
|
||||
return self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_billscroll_menu")
|
||||
)
|
||||
|
||||
def _visible_events(self, label):
|
||||
"""Count displayed .drama-event elements with the given data-label."""
|
||||
els = self.browser.find_elements(
|
||||
By.CSS_SELECTOR, f".drama-event[data-label='{label}']"
|
||||
)
|
||||
return sum(1 for e in els if e.is_displayed())
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Step 1 — gear menu opens; both labels present and pre-checked #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_gear_menu_shows_frame_and_redact_checkboxes(self):
|
||||
self._go_to_scroll()
|
||||
menu = self._open_gear()
|
||||
self.assertTrue(menu.is_displayed())
|
||||
frame_cb = menu.find_element(By.CSS_SELECTOR, "input[value='frame']")
|
||||
redact_cb = menu.find_element(By.CSS_SELECTOR, "input[value='redact']")
|
||||
self.assertTrue(frame_cb.is_selected())
|
||||
self.assertTrue(redact_cb.is_selected())
|
||||
self.assertIn("Frame", menu.text)
|
||||
self.assertIn("Redact", menu.text)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Steps 2 – 4 — filter flow #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_gear_menu_filter_flow(self):
|
||||
self._go_to_scroll()
|
||||
|
||||
# Step 1: all 4 events visible (2 frame + 2 redact)
|
||||
self.assertEqual(self._visible_events("frame"), 2)
|
||||
self.assertEqual(self._visible_events("redact"), 2)
|
||||
|
||||
# Step 2: uncheck Redact → OK → struck entries disappear
|
||||
menu = self._open_gear()
|
||||
menu.find_element(By.CSS_SELECTOR, "input[value='redact']").click()
|
||||
menu.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
|
||||
self.wait_for(lambda: self.assertEqual(self._visible_events("redact"), 0))
|
||||
self.assertEqual(self._visible_events("frame"), 2)
|
||||
|
||||
# Step 3: recheck Redact + uncheck Frame → OK
|
||||
# Redact events re-appear (still struck-through); Frame events gone.
|
||||
menu = self._open_gear()
|
||||
menu.find_element(By.CSS_SELECTOR, "input[value='redact']").click() # recheck
|
||||
menu.find_element(By.CSS_SELECTOR, "input[value='frame']").click() # uncheck
|
||||
menu.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
|
||||
self.wait_for(lambda: self.assertEqual(self._visible_events("frame"), 0))
|
||||
self.assertEqual(self._visible_events("redact"), 2)
|
||||
# Struck-through entries still carry the .struck class (visually "gone" in narrative)
|
||||
redact_bodies = self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".drama-event[data-label='redact'] .drama-event-body"
|
||||
)
|
||||
self.assertTrue(all("struck" in b.get_attribute("class") for b in redact_bodies))
|
||||
|
||||
# Step 4: recheck Frame → OK → all events return
|
||||
menu = self._open_gear()
|
||||
menu.find_element(By.CSS_SELECTOR, "input[value='frame']").click() # recheck
|
||||
menu.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
|
||||
self.wait_for(lambda: self.assertEqual(self._visible_events("frame"), 2))
|
||||
self.assertEqual(self._visible_events("redact"), 2)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Persistence — filter survives a full page reload #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_filter_selection_persists_across_refresh(self):
|
||||
self._go_to_scroll()
|
||||
|
||||
# Uncheck Redact → OK: struck entries disappear
|
||||
menu = self._open_gear()
|
||||
menu.find_element(By.CSS_SELECTOR, "input[value='redact']").click()
|
||||
menu.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
|
||||
self.wait_for(lambda: self.assertEqual(self._visible_events("redact"), 0))
|
||||
|
||||
# Hard reload — same URL, same session cookie
|
||||
self.browser.refresh()
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_drama_scroll"))
|
||||
|
||||
# Struck entries still absent after reload
|
||||
self.wait_for(lambda: self.assertEqual(self._visible_events("redact"), 0))
|
||||
self.assertEqual(self._visible_events("frame"), 2)
|
||||
|
||||
# Gear menu still shows Redact unchecked
|
||||
menu = self._open_gear()
|
||||
redact_cb = menu.find_element(By.CSS_SELECTOR, "input[value='redact']")
|
||||
self.assertFalse(redact_cb.is_selected())
|
||||
|
||||
@@ -372,15 +372,15 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
self.assertEqual(corr.text, "")
|
||||
|
||||
|
||||
# ── TAKE SIG / WAIT NO — ready gate ──────────────────────────────────────────
|
||||
# ── TAKE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
|
||||
#
|
||||
# TAKE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
|
||||
# stage preview once a gamer has clicked OK on a card (SigReservation exists).
|
||||
# Clicking it sets the gamer's status to ready and changes the btn to WAIT NO.
|
||||
# WAIT NO cancels the ready status and reverts back to TAKE SIG.
|
||||
# Clicking it sets the gamer's status to ready and changes the btn to WAIT NVM.
|
||||
# WAIT NVM cancels the ready status and reverts back to TAKE SIG.
|
||||
#
|
||||
# When all three gamers in a polarity WS room are ready, a 12-second countdown
|
||||
# starts. Any WAIT NO during the countdown cancels it; the saved remaining time
|
||||
# starts. Any WAIT NVM during the countdown cancels it; the saved remaining time
|
||||
# is resumed when all three are ready again. When the countdown completes
|
||||
# (client POSTs sig_confirm) the polarity group returns to the table hex.
|
||||
# When both polarity groups have confirmed, PICK SKY btn appears in the hex
|
||||
@@ -390,7 +390,7 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
|
||||
|
||||
class SigReadyGateTest(FunctionalTest):
|
||||
"""Single-browser tests for TAKE SIG / WAIT NO btn."""
|
||||
"""Single-browser tests for TAKE SIG / WAIT NVM btn."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -450,7 +450,7 @@ class SigReadyGateTest(FunctionalTest):
|
||||
)
|
||||
self.assertIn("TAKE SIG", take_sig_btn.text.upper())
|
||||
|
||||
# ── SRG3: TAKE SIG → WAIT NO ─────────────────────────────────────── #
|
||||
# ── SRG3: TAKE SIG → WAIT NVM ─────────────────────────────────────── #
|
||||
|
||||
def test_take_sig_btn_becomes_wait_no_after_click(self):
|
||||
room = self._setup_sig_room()
|
||||
@@ -467,9 +467,16 @@ class SigReadyGateTest(FunctionalTest):
|
||||
wait_no_btn = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
|
||||
)
|
||||
self.assertIn("WAIT NO", wait_no_btn.text.upper())
|
||||
self.assertIn("WAIT NVM", wait_no_btn.text.upper())
|
||||
|
||||
# ── SRG4: WAIT NO → TAKE SIG ─────────────────────────────────────── #
|
||||
# WAIT NVM pulses a --terOr glow: btn-cancel class appears within one tick
|
||||
self.wait_for(
|
||||
lambda: "btn-cancel" in self.browser.find_element(
|
||||
By.ID, "id_take_sig_btn"
|
||||
).get_attribute("class")
|
||||
)
|
||||
|
||||
# ── SRG4: WAIT NVM → TAKE SIG ─────────────────────────────────────── #
|
||||
|
||||
def test_wait_no_reverts_to_take_sig(self):
|
||||
room = self._setup_sig_room()
|
||||
@@ -481,8 +488,8 @@ class SigReadyGateTest(FunctionalTest):
|
||||
btn = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
|
||||
)
|
||||
btn.click() # → WAIT NO
|
||||
self.wait_for(lambda: "WAIT NO" in self.browser.find_element(
|
||||
btn.click() # → WAIT NVM
|
||||
self.wait_for(lambda: "WAIT NVM" in self.browser.find_element(
|
||||
By.ID, "id_take_sig_btn").text.upper()
|
||||
)
|
||||
btn = self.browser.find_element(By.ID, "id_take_sig_btn")
|
||||
@@ -562,20 +569,21 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
||||
)
|
||||
b.find_element(By.ID, "id_take_sig_btn").click()
|
||||
|
||||
# All three browsers should now see the countdown
|
||||
# All three browsers should now see the countdown button (numeral text)
|
||||
for b in browsers:
|
||||
self.wait_for(
|
||||
lambda: b.find_element(By.ID, "id_sig_countdown"), browser=b
|
||||
lambda: b.find_element(By.ID, "id_take_sig_btn").text.isdigit(),
|
||||
browser=b,
|
||||
)
|
||||
finally:
|
||||
for b in browsers:
|
||||
b.quit()
|
||||
|
||||
# ── SRG6: countdown disappears when WAIT NO clicked ──────────────── #
|
||||
# ── SRG6: countdown disappears when WAIT NVM clicked ──────────────── #
|
||||
|
||||
@tag("channels")
|
||||
def test_countdown_disappears_when_any_levity_gamer_clicks_wait_no(self):
|
||||
"""Any WAIT NO during the countdown cancels it for all three browsers."""
|
||||
"""Any WAIT NVM during the countdown cancels it for all three browsers."""
|
||||
room, emails = self._setup_sig_select_room()
|
||||
levity_emails = [emails[0], emails[1], emails[3]]
|
||||
browsers = []
|
||||
@@ -600,19 +608,20 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
||||
)
|
||||
b.find_element(By.ID, "id_take_sig_btn").click()
|
||||
|
||||
# Confirm countdown started for all
|
||||
# Confirm countdown started for all (button text is a numeral)
|
||||
for b in browsers:
|
||||
self.wait_for(
|
||||
lambda: b.find_element(By.ID, "id_sig_countdown"), browser=b
|
||||
lambda: b.find_element(By.ID, "id_take_sig_btn").text.isdigit(),
|
||||
browser=b,
|
||||
)
|
||||
|
||||
# PC clicks WAIT NO
|
||||
# PC clicks the countdown button to cancel
|
||||
browsers[0].find_element(By.ID, "id_take_sig_btn").click()
|
||||
|
||||
# Countdown element should disappear for all three
|
||||
# Countdown should cancel for all three (button back to WAIT NVM)
|
||||
for b in browsers:
|
||||
self.wait_for(
|
||||
lambda: len(b.find_elements(By.ID, "id_sig_countdown")) == 0,
|
||||
lambda: b.find_element(By.ID, "id_take_sig_btn").text == "WAIT NVM",
|
||||
browser=b,
|
||||
)
|
||||
finally:
|
||||
@@ -666,8 +675,9 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
||||
b.find_element(By.ID, "id_take_sig_btn").click()
|
||||
|
||||
# Wait for countdown to expire or be confirmed; PICK SKY appears in hex
|
||||
# countdown is 12 s so use wait_for_slow (MAX_WAIT=10 is not enough)
|
||||
for b in browsers:
|
||||
self.wait_for(
|
||||
self.wait_for_slow(
|
||||
lambda: b.find_element(By.ID, "id_pick_sky_btn"), browser=b
|
||||
)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user