btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default .sig-stage .sig-stage-card .fan-card-face .sig-qualifier-* rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each .fan-card-reversal-* class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-<p> skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two <p>s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-18 00:25:10 -04:00
parent ace8612099
commit 3242873625
23 changed files with 179 additions and 128 deletions

View File

@@ -1,8 +1,8 @@
"""
Management command for manual single-gamer PICK SEA testing.
Management command for manual single-gamer DRAW SEA testing.
Creates a room at SKY_SELECT with one seated gamer whose sky is already
confirmed, so the PICK SEA overlay is immediately visible on page load.
confirmed, so the DRAW SEA overlay is immediately visible on page load.
Usage:
python src/manage.py setup_sea_session
@@ -32,7 +32,7 @@ def _make_session(user):
class Command(BaseCommand):
help = "Set up a SKY_SELECT room with sky confirmed and print a PICK SEA URL"
help = "Set up a SKY_SELECT room with sky confirmed and print a DRAW SEA URL"
def add_arguments(self, parser):
parser.add_argument("--base-url", default="http://localhost:8000")

View File

@@ -453,7 +453,7 @@ 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.
Redact = struck-through (retracted) entries, e.g. a WAIT NVM after SAVE SIG.
Scenario (one gamer, Role + Sig events):
1. Both labels checked by default — all events visible.

View File

@@ -843,12 +843,12 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
))
# ------------------------------------------------------------------ #
# Test 7 — PICK SIGS appears + card stack removed on last role #
# Test 7 — SCAN SIGS appears + card stack removed on last role #
# ------------------------------------------------------------------ #
def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
"""When the sixth and final role is confirmed, the all_roles_filled
WS event makes the PICK SIGS button visible and removes the card
WS event makes the SCAN SIGS button visible and removes the card
stack from the DOM entirely."""
emails = [
"founder@test.io", "amigo@test.io", "bud@test.io",
@@ -883,7 +883,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# PICK SIGS wrap must become visible via the all_roles_filled WS event.
# SCAN SIGS wrap must become visible via the all_roles_filled WS event.
self.wait_for(lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
))

View File

@@ -1,4 +1,4 @@
"""Functional tests for the PICK SEA overlay — Celtic Cross draw."""
"""Functional tests for the DRAW SEA overlay — Celtic Cross draw."""
from django.test import tag
from django.urls import reverse
@@ -39,7 +39,7 @@ def _make_sky_confirmed_room(live_server_url, user, earthman):
@tag("channels")
class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
"""After sky confirm, the sky overlay closes and the room reloads to the
table hex w. the PICK SEA btn visible — the gamer must opt into the sea
table hex w. the DRAW SEA btn visible — the gamer must opt into the sea
overlay rather than be auto-launched into it."""
def setUp(self):
@@ -92,22 +92,22 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
""")
def test_pick_sea_btn_visible_after_sky_confirm(self):
"""Confirming sky reloads the room to the hex w. PICK SEA replacing
PICK SKY; the sea overlay is NOT auto-opened."""
"""Confirming sky reloads the room to the hex w. DRAW SEA replacing
CAST SKY; the sea overlay is NOT auto-opened."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
# Sky not yet confirmed — PICK SKY btn present.
# Sky not yet confirmed — CAST SKY btn present.
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self._confirm_sky()
# Page reloads → hex shows PICK SEA in place of PICK SKY.
# Page reloads → hex shows DRAW SEA in place of CAST SKY.
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), [])
# Sea overlay is NOT auto-opened — it only appears once the gamer
# clicks PICK SEA.
# clicks DRAW SEA.
has_sea_open = self.browser.execute_script(
"return document.documentElement.classList.contains('sea-open');"
)
@@ -126,7 +126,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
self.assertTrue(not sky or not sky[0].is_displayed())
def test_clicking_pick_sea_btn_opens_sea_overlay(self):
"""The gamer's explicit click on PICK SEA is what opens the sea overlay."""
"""The gamer's explicit click on DRAW SEA is what opens the sea overlay."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
@@ -134,7 +134,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
self._confirm_sky()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
# On slow CI, the PICK SEA btn parses into the DOM before the inline
# On slow CI, the DRAW SEA btn parses into the DOM before the inline
# `<script>` at the bottom of _sea_overlay.html has bound `openSea` to
# it; a one-shot click can land before the handler exists. Retry click
# + assert together via wait_for so the race resolves naturally.
@@ -150,7 +150,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
self.assertTrue(sea_overlay.is_displayed())
# ── Helpers for PICK SEA deal tests ──────────────────────────────────────────
# ── Helpers for DRAW SEA deal tests ──────────────────────────────────────────
def _seed_earthman_cards(earthman, count=20):
"""Seed enough Middle Arcana cards for the deck piles."""
@@ -167,7 +167,7 @@ def _seed_earthman_cards(earthman, count=20):
def _make_sea_ready_room(earthman):
"""Create a SKY_SELECT room with a confirmed Character ready for PICK SEA.
"""Create a SKY_SELECT room with a confirmed Character ready for DRAW SEA.
Returns (room, gamer, seat, char, room_url).
"""
@@ -199,7 +199,7 @@ def _make_sea_ready_room(earthman):
@tag("channels")
class PickSeaDealTest(ChannelsFunctionalTest):
"""PICK SEA deck stacks, OK btn interaction, card draw, and LOCK HAND."""
"""DRAW SEA deck stacks, OK btn interaction, card draw, and LOCK HAND."""
def setUp(self):
super().setUp()

View File

@@ -325,25 +325,25 @@ class SigSelectThemeTest(FunctionalTest):
self.assertEqual(corr.text, "")
# ── TAKE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
# ── SAVE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
#
# TAKE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
# SAVE 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 NVM.
# WAIT NVM cancels the ready status and reverts back to TAKE SIG.
# WAIT NVM cancels the ready status and reverts back to SAVE SIG.
#
# When all three gamers in a polarity WS room are ready, a 12-second countdown
# 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
# When both polarity groups have confirmed, CAST SKY btn appears in the hex
# center for all six gamers.
#
# ─────────────────────────────────────────────────────────────────────────────
class SigReadyGateTest(FunctionalTest):
"""Single-browser tests for TAKE SIG / WAIT NVM btn."""
"""Single-browser tests for SAVE SIG / WAIT NVM btn."""
def setUp(self):
super().setUp()
@@ -377,10 +377,10 @@ class SigReadyGateTest(FunctionalTest):
)
ok_btn.click()
# ── SRG1: TAKE SIG btn not visible before OK ──────────────────────── #
# ── SRG1: SAVE SIG btn not visible before OK ──────────────────────── #
def test_take_sig_btn_not_visible_before_ok_click(self):
"""TAKE SIG must be absent until the gamer has OK'd a card."""
"""SAVE SIG must be absent until the gamer has OK'd a card."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
@@ -389,7 +389,7 @@ class SigReadyGateTest(FunctionalTest):
take_sig_btns = self.browser.find_elements(By.ID, "id_take_sig_btn")
self.assertEqual(len(take_sig_btns), 0)
# ── SRG2: TAKE SIG btn appears after OK ──────────────────────────── #
# ── SRG2: SAVE SIG btn appears after OK ──────────────────────────── #
def test_take_sig_btn_appears_after_ok_click(self):
room = self._setup_sig_room()
@@ -401,9 +401,9 @@ class SigReadyGateTest(FunctionalTest):
take_sig_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
)
self.assertIn("TAKE SIG", take_sig_btn.text.upper())
self.assertIn("SAVE SIG", take_sig_btn.text.upper())
# ── SRG3: TAKE SIG → WAIT NVM ─────────────────────────────────────── #
# ── SRG3: SAVE SIG → WAIT NVM ─────────────────────────────────────── #
def test_take_sig_btn_becomes_wait_no_after_click(self):
room = self._setup_sig_room()
@@ -429,7 +429,7 @@ class SigReadyGateTest(FunctionalTest):
).get_attribute("class")
)
# ── SRG4: WAIT NVM → TAKE SIG ─────────────────────────────────────── #
# ── SRG4: WAIT NVM → SAVE SIG ─────────────────────────────────────── #
def test_wait_no_reverts_to_take_sig(self):
room = self._setup_sig_room()
@@ -446,11 +446,11 @@ class SigReadyGateTest(FunctionalTest):
By.ID, "id_take_sig_btn").text.upper()
)
btn = self.browser.find_element(By.ID, "id_take_sig_btn")
btn.click() # → TAKE SIG again
btn.click() # → SAVE SIG again
self.wait_for(
lambda: self.assertIn(
"TAKE SIG",
"SAVE SIG",
self.browser.find_element(By.ID, "id_take_sig_btn").text.upper(),
)
)
@@ -458,7 +458,7 @@ class SigReadyGateTest(FunctionalTest):
@tag("channels")
class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
"""Multi-browser WebSocket tests for the polarity-room countdown and PICK SKY."""
"""Multi-browser WebSocket tests for the polarity-room countdown and CAST SKY."""
def setUp(self):
super().setUp()
@@ -546,7 +546,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
@tag("channels")
def test_countdown_element_appears_when_all_three_levity_gamers_ready(self):
"""When PC, NC, and SC each click TAKE SIG the countdown becomes visible."""
"""When PC, NC, and SC each click SAVE SIG the countdown becomes visible."""
room, emails = self._setup_sig_select_room()
levity_emails = [emails[0], emails[1], emails[3]] # PC, NC, SC
browsers = []
@@ -556,7 +556,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
browsers.append(b)
# Each levity gamer OK's a card then clicks TAKE SIG
# Each levity gamer OK's a card then clicks SAVE SIG
for b in browsers:
self._ok_card_in_browser(b)
self.wait_for(
@@ -616,12 +616,12 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
for b in browsers:
b.quit()
# ── SRG7: PICK SKY btn appears after both polarity groups confirm ─── #
# ── SRG7: CAST SKY btn appears after both polarity groups confirm ─── #
@tag("channels")
def test_pick_sky_btn_appears_in_hex_after_both_groups_confirm(self):
"""Once both levity and gravity countdowns complete, all six browsers
see the PICK SKY btn in the table hex center."""
see the CAST SKY btn in the table hex center."""
# This test drives the full flow end-to-end but uses ORM shortcuts
# to set all-ready state for one polarity, letting the other complete
# via the UI, to keep execution time manageable.
@@ -647,7 +647,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
browsers.append(b)
# All levity gamers OK and TAKE SIG
# All levity gamers OK and SAVE SIG
for b in browsers:
self._ok_card_in_browser(b)
self.wait_for(
@@ -655,7 +655,7 @@ 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
# Wait for countdown to expire or be confirmed; CAST 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_slow(
@@ -711,7 +711,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
# class PickSkyTrayFlowTest(FunctionalTest):
#
# def test_pick_sky_btn_opens_tray_with_sig_card_in_slot_2(self):
# """Clicking PICK SKY opens #id_tray; tray cell 2 shows the gamer's
# """Clicking CAST SKY opens #id_tray; tray cell 2 shows the gamer's
# sig card icon (Blank.svg placeholder until card-specific icons land)."""
# ...
#

View File

@@ -1,4 +1,4 @@
"""Functional tests for the PICK SKY overlay — natal chart entry."""
"""Functional tests for the CAST SKY overlay — natal chart entry."""
import json as _json
@@ -42,7 +42,7 @@ def _make_sky_select_room():
class PickSkyLocalStorageTest(FunctionalTest):
"""PICK SKY form fields persist to localStorage."""
"""CAST SKY form fields persist to localStorage."""
def setUp(self):
super().setUp()
@@ -145,7 +145,7 @@ class PickSkyLocalStorageTest(FunctionalTest):
class PickSkyDelTest(FunctionalTest):
"""PICK SKY overlay gets a DEL btn at the wheel center: clicking opens the
"""CAST SKY overlay gets a DEL btn at the wheel center: clicking opens the
global guard portal; OK clears the wheel SVG, resets the form fields, &
purges the localStorage entry that would otherwise rehydrate the form on
the next overlay open / page refresh. No server hit (the wheel here is
@@ -173,7 +173,7 @@ class PickSkyDelTest(FunctionalTest):
self.create_pre_authenticated_session(self.founder_email)
self.browser.get(self.room_url)
# Open PICK SKY modal
# Open CAST SKY modal
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self.browser.execute_script("arguments[0].click()", btn)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_overlay"))