Files
python-tdd/src/functional_tests/test_game_kit.py

223 lines
9.4 KiB
Python
Raw Normal View History

from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest
billboard Most Recent Scroll: fix SQLite NULL drop on SIG_READY exclude; pronouns flow FT; Blades middle reversal Nervous → Fickle — TDD - billboard/views.py _billboard_context: `.exclude(verb=SIG_READY, data__retracted=True)` was silently dropping every SIG_READY event whose data had no `retracted` key — `WHERE NOT (NULL AND verb='sig_ready')` evaluates to NULL via JSON_EXTRACT, which the SQL engine treats as "row not satisfying WHERE", so the row was excluded. Fix: pull a 100-row buffer w. only the SIG_UNREADY exclude at the SQL level, then post-filter retracted SIG_READY in Python before slicing to 36; PostgreSQL handles the lookup correctly so this is a SQLite-only manifestation that explained intermittent "No events yet" in Most Recent Scroll - CLAUDE.md gotchas: new entry warning that `.exclude(data__key=value)` / `.filter(data__key=value)` on SQLite JSONField bites on missing keys; if the predicate must require key existence, post-filter in Python - functional_tests/test_game_kit.py PronounsAppletFlowTest: end-to-end profile-wide pronoun flip — start on per-room billscroll seeing "their" cognates, navigate to Game Kit, click bawlmorese card, assert guard portal active w. "yo/yo/yos" preview, click OK, navigate to billboard + see Most Recent Scroll re-rendered w. "yos", navigate back to billscroll + see same flip; covers the whole render-time-pronoun-resolution path on real DOM - epic/0008_blades_reversal_fickle.py: rename Middle Arcana Blades reversal_qualifier "Nervous" → "Fickle" (RunPython forward+reverse on arcana=MIDDLE, suit=BLADES, number ∈ {11,12,13,14}); SigSelectSpec.js hardcoded "Nervous" updated to "Fickle" + collected static Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:27:17 -04:00
from apps.applets.models import Applet
from apps.drama.models import GameEvent, record
from apps.epic.models import DeckVariant, Room
from apps.lyric.models import Token, User
class GameKitTest(FunctionalTest):
"""Game Kit <dialog>: opens from footer, shows token cards, dismisses."""
def setUp(self):
super().setUp()
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
self.create_pre_authenticated_session("gamer@kit.io")
self.gamer = User.objects.get(email="gamer@kit.io")
self.gamer.equipped_deck = self.earthman
self.gamer.save(update_fields=["equipped_deck"])
self.gamer.unlocked_decks.add(self.earthman)
self.token = self.gamer.tokens.filter(token_type=Token.COIN).first()
self.room = Room.objects.create(name="Kit Room", owner=self.gamer)
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
def test_kit_btn_in_footer_opens_dialog(self):
self.browser.get(self.gate_url)
kit_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_btn")
)
self.assertTrue(kit_btn.is_displayed())
kit_btn.click()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed()
)
)
def test_kit_dialog_shows_token_cards(self):
self.browser.get(self.gate_url)
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f"#id_kit_bag_dialog [data-token-id='{self.token.id}']",
)
)
def test_kit_dialog_closes_on_escape(self):
self.browser.get(self.gate_url)
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed()
)
)
dialog = self.browser.find_element(By.ID, "id_kit_bag_dialog")
dialog.send_keys(Keys.ESCAPE)
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_kit_bag_dialog").is_displayed()
)
)
def test_kit_btn_visible_outside_room(self):
self.browser.get(self.live_server_url + "/")
kit_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_btn")
)
self.assertTrue(kit_btn.is_displayed())
def test_kit_dialog_shows_equipped_deck(self):
"""New user auto-gets Earthman equipped; kit bar shows its deck card."""
self.browser.get(self.gate_url)
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f"#id_kit_bag_dialog .kit-bag-deck[data-deck-id='{self.gamer.equipped_deck.pk}']",
)
)
def test_kit_dialog_always_shows_dice_placeholder(self):
self.browser.get(self.gate_url)
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
"#id_kit_bag_dialog .kit-bag-placeholder",
)
)
def test_kit_dialog_deck_tooltip_shows_name_count_availability_and_stock_version(self):
self.browser.get(self.gate_url)
self.browser.find_element(By.ID, "id_kit_btn").click()
deck_el = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck"
)
)
# Dispatch mouseenter via JS — more reliable than ActionChains in headless CI
self.browser.execute_script(
"arguments[0].dispatchEvent(new Event('mouseenter'))", deck_el
)
tooltip = self.browser.find_element(
By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck .tt"
)
self.wait_for(lambda: self.assertTrue(tooltip.is_displayed()))
text = tooltip.text
self.assertIn("Earthman", text)
self.assertIn("(Default)", text)
self.assertIn("108", text)
self.assertIn("Stock version", text)
billboard Most Recent Scroll: fix SQLite NULL drop on SIG_READY exclude; pronouns flow FT; Blades middle reversal Nervous → Fickle — TDD - billboard/views.py _billboard_context: `.exclude(verb=SIG_READY, data__retracted=True)` was silently dropping every SIG_READY event whose data had no `retracted` key — `WHERE NOT (NULL AND verb='sig_ready')` evaluates to NULL via JSON_EXTRACT, which the SQL engine treats as "row not satisfying WHERE", so the row was excluded. Fix: pull a 100-row buffer w. only the SIG_UNREADY exclude at the SQL level, then post-filter retracted SIG_READY in Python before slicing to 36; PostgreSQL handles the lookup correctly so this is a SQLite-only manifestation that explained intermittent "No events yet" in Most Recent Scroll - CLAUDE.md gotchas: new entry warning that `.exclude(data__key=value)` / `.filter(data__key=value)` on SQLite JSONField bites on missing keys; if the predicate must require key existence, post-filter in Python - functional_tests/test_game_kit.py PronounsAppletFlowTest: end-to-end profile-wide pronoun flip — start on per-room billscroll seeing "their" cognates, navigate to Game Kit, click bawlmorese card, assert guard portal active w. "yo/yo/yos" preview, click OK, navigate to billboard + see Most Recent Scroll re-rendered w. "yos", navigate back to billscroll + see same flip; covers the whole render-time-pronoun-resolution path on real DOM - epic/0008_blades_reversal_fickle.py: rename Middle Arcana Blades reversal_qualifier "Nervous" → "Fickle" (RunPython forward+reverse on arcana=MIDDLE, suit=BLADES, number ∈ {11,12,13,14}); SigSelectSpec.js hardcoded "Nervous" updated to "Fickle" + collected static Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:27:17 -04:00
class PronounsAppletFlowTest(FunctionalTest):
"""End-to-end profile-wide pronoun flip:
The same provenance prose ("X embodies as {poss} Significator …") rendered
on both the per-room scroll AND the billboard's Most Recent Scroll applet
should re-render when the user flips their pronouns ideology via the
Pronouns applet on the Game Kit page. The test starts on a billscroll seeing
"their" cognates (default pluralism), navigates to Game Kit, clicks the
bawlmorese card guard portal OK, then verifies the Most Recent Scroll
on the billboard AND the room's billscroll both render "yos" cognates.
"""
def setUp(self):
super().setUp()
# Billboard applets — page renders blank without these
for slug, name, cols, rows in [
("my-scrolls", "My Scrolls", 4, 3),
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial) - lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design. - applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts). - billboard.views.my_buddies + add_buddy: • my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all(). • add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup). - templates: • _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/). • my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet." • _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add. - SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement). - tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op. - 841 ITs (+14) + 4 my_buddies FTs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
("my-buddies", "My Buddies", 4, 3),
billboard Most Recent Scroll: fix SQLite NULL drop on SIG_READY exclude; pronouns flow FT; Blades middle reversal Nervous → Fickle — TDD - billboard/views.py _billboard_context: `.exclude(verb=SIG_READY, data__retracted=True)` was silently dropping every SIG_READY event whose data had no `retracted` key — `WHERE NOT (NULL AND verb='sig_ready')` evaluates to NULL via JSON_EXTRACT, which the SQL engine treats as "row not satisfying WHERE", so the row was excluded. Fix: pull a 100-row buffer w. only the SIG_UNREADY exclude at the SQL level, then post-filter retracted SIG_READY in Python before slicing to 36; PostgreSQL handles the lookup correctly so this is a SQLite-only manifestation that explained intermittent "No events yet" in Most Recent Scroll - CLAUDE.md gotchas: new entry warning that `.exclude(data__key=value)` / `.filter(data__key=value)` on SQLite JSONField bites on missing keys; if the predicate must require key existence, post-filter in Python - functional_tests/test_game_kit.py PronounsAppletFlowTest: end-to-end profile-wide pronoun flip — start on per-room billscroll seeing "their" cognates, navigate to Game Kit, click bawlmorese card, assert guard portal active w. "yo/yo/yos" preview, click OK, navigate to billboard + see Most Recent Scroll re-rendered w. "yos", navigate back to billscroll + see same flip; covers the whole render-time-pronoun-resolution path on real DOM - epic/0008_blades_reversal_fickle.py: rename Middle Arcana Blades reversal_qualifier "Nervous" → "Fickle" (RunPython forward+reverse on arcana=MIDDLE, suit=BLADES, number ∈ {11,12,13,14}); SigSelectSpec.js hardcoded "Nervous" updated to "Fickle" + collected static Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:27:17 -04:00
("most-recent-scroll", "Most Recent Scroll", 8, 6),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
)
# Game Kit applets — including the new Pronouns applet
for slug, name in [
("gk-trinkets", "Trinkets"),
("gk-tokens", "Tokens"),
("gk-decks", "Card Decks"),
("gk-dice", "Dice Sets"),
("pronouns", "Pronouns"),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": 3, "grid_rows": 3, "context": "game-kit"},
)
self.create_pre_authenticated_session("disco@pronouns.io")
self.user = User.objects.get(email="disco@pronouns.io")
self.room = Room.objects.create(name="Pronoun Chamber", owner=self.user)
# Seed a SIG_READY event so both scrolls have prose to render
record(
self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands", corner_rank="M",
suit_icon="fa-wand-sparkles",
)
self.scroll_url = self.live_server_url + f"/billboard/room/{self.room.id}/scroll/"
self.billboard_url = self.live_server_url + "/billboard/"
self.game_kit_url = self.live_server_url + "/gameboard/game-kit/"
def _scroll_text(self):
return self.browser.find_element(By.ID, "id_drama_scroll").text
def _most_recent_text(self):
return self.browser.find_element(By.ID, "id_applet_most_recent_scroll").text
def test_pronoun_flip_propagates_to_billscroll_and_most_recent(self):
# 1. Start on the room's billscroll — default pluralism reads "their".
self.browser.get(self.scroll_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_drama_scroll"))
self.assertIn("embodies as their Significator", self._scroll_text())
# 2. Navigate to the Game Kit page and find the bawlmorese card.
self.browser.get(self.game_kit_url)
bawlmorese_card = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gk-pronoun-card[data-pronoun='bawlmorese']"
)
)
# 3. Click the card → guard portal becomes active w. the slash-trio
# preview ("yo/yo/yos") above OK|NVM.
self.browser.execute_script("arguments[0].click()", bawlmorese_card)
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_guard_portal.active"
)
)
portal = self.browser.find_element(By.ID, "id_guard_portal")
self.assertIn("Set pronoun preference?", portal.text)
self.assertIn("yo/yo/yos", portal.text)
# 4. Click OK — the page reloads, .active class moves to bawlmorese.
portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gk-pronoun-card.active[data-pronoun='bawlmorese']"
)
)
# 5. Navigate to the Billboard — Most Recent Scroll applet must now
# read "yos" cognates, not "their".
self.browser.get(self.billboard_url)
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_most_recent_scroll")
)
self.assertIn("embodies as yos Significator", self._most_recent_text())
self.assertNotIn("embodies as their Significator", self._most_recent_text())
# 6. And back on the per-room billscroll — same prose, same flip.
self.browser.get(self.scroll_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_drama_scroll"))
self.assertIn("embodies as yos Significator", self._scroll_text())
self.assertNotIn("embodies as their Significator", self._scroll_text())