Files
python-tdd/src/functional_tests/test_trinket_wristband.py
Disco DeDisco 99ffdb3943
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
feat: Token.BAND (Wristband) — non-admin variant of PASS, admin-awarded via Django admin to any user (NOT auto-granted on signal, NO is_staff coupling, NO model-layer guard). Mirrors PASS at runtime — fills 1 gate slot, never consumed, stays equipped, no current_room tie, no expiry, no In-Use microtooltip — but separates the policy concerns so PASS stays a deliberate staff-only trinket while BAND becomes the regular-user version (promotional / play-reward / staging give-away). Tooltip prose: name "Wristband", desc "Admit All Entry" (shared w. PASS — phrasing reflects the never-depleted lifetime, not multi-slot semantics), shoptalk "Unlimited free entry (BYOB)", expiry "no expiry". fa-ring icon across all 4 surfaces (Game Kit applet #id_kit_wristband between PASS + CARTE, gk-trinkets section, kit-bag dialog Trinket slot, wallet PASS→BAND→COIN elif chain). Priority chain — PASS → BAND → COIN → FREE → TITHE — wired identically into both apps.epic.models.select_token (room gatekeeper) + apps.gameboard.models._select_my_sea_token (my-sea gatekeeper); BAND wins over consumables for any holder while PASS still wins for staff who happen to hold both. debit_token + debit_my_sea_token treat BAND same as PASS: slot marked FILLED w. debited_token_type=BAND, token row preserved, current_room untouched, equipped_trinket unchanged. View contexts (gameboard, toggle_game_applets, _game_kit_context, wallet, toggle_wallet_applets) pass a band key — universal lookup, NO is_staff filter. Migration lyric/0007_alter_token_token_type — choices-only AlterField. TDD — 5 FTs in test_trinket_wristband.py (test_band_not_auto_equipped_after_award, test_band_tooltip_renders_full_prose, test_band_uses_fa_ring_icon, test_equipped_band_shows_equipped_mini_tooltip, test_equipped_band_shows_doff_active_don_disabled); 4 tooltip UTs (BandTokenTooltipTest); 5 model ITs (BandTokenAdminAwardTest — no-auto-grant for non-staff + staff, admin-can-award to either branch, not-auto-equipped); 2 priority-chain ITs (test_returns_band_when_held_and_no_pass, test_pass_still_wins_over_band_for_staff); 1 debit IT (test_debit_band_does_not_consume_or_unequip). 1145 IT/UT + 5 FT green. A boost-pass / promo-band w. richer semantics (multi-slot admit, time-window, etc.) lands as YET-ANOTHER token_type later — keep BAND the minimal "PASS minus admin gate" trinket so the policy axis stays clean. Captured in [[sprint-band-trinket-may21]] alongside the standing auto-commit rule [[feedback-auto-commit-after-build]]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:33:09 -04:00

136 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Default-structure FTs for the Wristband trinket.
BAND is the non-admin variant of PASS — same magical 'Admit All Entry' tooltip
and same never-deposited, stays-equipped behavior, but admin-assigned (not
auto-granted on user creation) and not gated behind `is_staff`. Like PASS, it
doesn't go through the `.token-rails` deposit flow that CARTE / COIN use, so
there's no `data-current-room-name` / In-Use parity work to do.
"""
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from apps.applets.models import Applet
from apps.lyric.models import Token, User
class WristbandTest(FunctionalTest):
"""BAND is admin-awarded to a non-staff user — coverage pins the
awarded-but-not-auto-equipped state + tooltip prose + DON/DOFF parity."""
def setUp(self):
super().setUp()
for slug, name, cols, rows in [
("new-game", "New Game", 6, 3),
("my-games", "My Games", 6, 3),
("game-kit", "Game Kit", 6, 3),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={
"name": name, "grid_cols": cols,
"grid_rows": rows, "context": "gameboard",
},
)
# Non-staff user — BAND is admin-awarded after creation, NOT auto-granted
# by the post_save signal (the way PASS is for staff users).
self.gamer = User.objects.create(email="bro@test.io", is_staff=False)
self.band = Token.objects.create(user=self.gamer, token_type=Token.BAND)
# ── Test 1 ───────────────────────────────────────────────────────────────
def test_band_not_auto_equipped_after_award(self):
"""Award diverges from PASS — BAND lands in the wallet but the user
must DON it manually. Confirms `create_wallet_and_tokens` doesn't
auto-equip a freshly-minted BAND (the way it auto-equips PASS for
staff at user-creation)."""
self.gamer.refresh_from_db()
self.assertNotEqual(self.gamer.equipped_trinket_id, self.band.pk)
# ── Test 2 ───────────────────────────────────────────────────────────────
def test_band_tooltip_renders_full_prose(self):
"""Hover the Wristband token in the Game Kit applet → main tooltip
shows title, description, shoptalk, and expiry."""
self.create_pre_authenticated_session("bro@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
band_el = self.browser.find_element(By.ID, "id_kit_wristband")
self.browser.execute_script(
"arguments[0].scrollIntoView({block: 'center'})", band_el
)
ActionChains(self.browser).move_to_element(band_el).perform()
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
self.assertIn("Wristband", portal.text)
self.assertIn("Admit All Entry", portal.text)
self.assertIn("Unlimited free entry", portal.text) # shoptalk
self.assertIn("BYOB", portal.text)
self.assertIn("no expiry", portal.text)
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_band_uses_fa_ring_icon(self):
"""Wristband renders w. the fa-ring icon — distinct from PASS's
fa-clipboard, CARTE's fa-money-check, COIN's fa-medal."""
self.create_pre_authenticated_session("bro@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
band_el = self.browser.find_element(By.ID, "id_kit_wristband")
icon = band_el.find_element(By.CSS_SELECTOR, "i")
self.assertIn("fa-ring", icon.get_attribute("class"))
# ── Test 4 ───────────────────────────────────────────────────────────────
def test_equipped_band_shows_equipped_mini_tooltip(self):
"""After DON-ing BAND, mini portal under the main tooltip says
'Equipped' (same shape as PASS's equipped-state mini portal)."""
self.gamer.equipped_trinket = self.band
self.gamer.save()
self.create_pre_authenticated_session("bro@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
band_el = self.browser.find_element(By.ID, "id_kit_wristband")
ActionChains(self.browser).move_to_element(band_el).perform()
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
self.assertIn("Equipped", mini.text)
# ── Test 5 ───────────────────────────────────────────────────────────────
def test_equipped_band_shows_doff_active_don_disabled(self):
"""Equipped BAND apparatus: DOFF active (clickable), DON is × disabled.
Symmetric to PASS's equipped state."""
self.gamer.equipped_trinket = self.band
self.gamer.save()
self.create_pre_authenticated_session("bro@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
band_el = self.browser.find_element(By.ID, "id_kit_wristband")
ActionChains(self.browser).move_to_element(band_el).perform()
portal = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_tooltip_portal")
)
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip")
self.assertIn("btn-disabled", don.get_attribute("class"))
self.assertEqual(don.text, "×")
self.assertNotIn("btn-disabled", doff.get_attribute("class"))
self.assertEqual(doff.text, "DOFF")
# TODO — BAND deposit/admit flow:
# Like PASS, BAND auto-admits any gate w.o going through the rails like
# CARTE / COIN. When the gatekeeper UX for never-deposited trinkets is
# formalized (PASS still has the same TODO open), add tests for the
# "BAND admits 1 slot, stays equipped" flow on both room.html + my_sea.html.