Files
python-tdd/src/functional_tests/test_game_kit.py
Disco DeDisco 246e45e55d buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
  - applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
  - billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
  - global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
  - new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
  - my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
  - my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
  - SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
  - 841 ITs + 5 my_buds/my_posts 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 23:08:33 -04:00

223 lines
9.4 KiB
Python

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
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)
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),
("my-buds", "My Buds", 4, 3),
("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())