diff --git a/src/apps/dashboard/tests/integrated/test_wallet_views.py b/src/apps/dashboard/tests/integrated/test_wallet_views.py index abe6827..81610a5 100644 --- a/src/apps/dashboard/tests/integrated/test_wallet_views.py +++ b/src/apps/dashboard/tests/integrated/test_wallet_views.py @@ -48,6 +48,65 @@ class WalletViewTest(TestCase): self.assertGreater(len(bundles), 0) +class WalletTokensAppletAllTrinketsVisibleTest(TestCase): + """Chunk 1 of the Shop applet rollout (2026-05-22) — the Tokens applet + in `wallet.html` must show every owned trinket-as-token type at once. + Pre-Chunk-1 the template's `{% if pass_token %} ... {% elif band %} + ... {% elif coin %}` chain hid two of the three from any user holding + a higher-priority trinket — bad UX since all three are usable at the + gate (per [[feedback-equip-slot-gates-trinket-use]], the user picks + WHICH one fires via the equip slot).""" + + def setUp(self): + self.user = User.objects.create(email="multitoken@test.io", is_staff=True) + # Auto-COIN (equipped) + FREE created by post_save signal; PASS auto- + # granted by the is_staff branch of the same signal. Add the rest. + Token.objects.create(user=self.user, token_type=Token.BAND) + Token.objects.create(user=self.user, token_type=Token.CARTE) + Token.objects.create(user=self.user, token_type=Token.TITHE) + self.client.force_login(self.user) + response = self.client.get("/dashboard/wallet/") + self.parsed = lxml.html.fromstring(response.content) + + def test_wallet_shows_pass_token(self): + [_] = self.parsed.cssselect("#id_pass_token") + + def test_wallet_shows_band_token(self): + [_] = self.parsed.cssselect("#id_band_token") + + def test_wallet_shows_coin_on_a_string(self): + [_] = self.parsed.cssselect("#id_coin_on_a_string") + + def test_wallet_shows_carte_token(self): + [_] = self.parsed.cssselect("#id_carte_token") + + def test_wallet_shows_free_token(self): + [_] = self.parsed.cssselect("#id_free_token") + + def test_wallet_shows_tithe_token(self): + [_] = self.parsed.cssselect("#id_tithe_token") + + def test_view_context_passes_carte(self): + """Defense-in-depth: not just the template but the view context too — + a renamed/refactored template should still receive `carte` in ctx.""" + response = self.client.get("/dashboard/wallet/") + self.assertEqual(response.context["carte"].token_type, Token.CARTE) + + def test_view_context_passes_band(self): + response = self.client.get("/dashboard/wallet/") + self.assertEqual(response.context["band"].token_type, Token.BAND) + + def test_non_staff_user_with_carte_still_sees_carte(self): + """CARTE has no `is_staff` gating (unlike PASS) — a regular gamer + holding a CARTE must see it in the Tokens applet.""" + non_staff = User.objects.create(email="grunt@test.io") + Token.objects.create(user=non_staff, token_type=Token.CARTE) + self.client.force_login(non_staff) + response = self.client.get("/dashboard/wallet/") + parsed = lxml.html.fromstring(response.content) + [_] = parsed.cssselect("#id_carte_token") + + class WalletPassTokenVisibilityTest(TestCase): """PASS is admin-only — the model guard blocks bogus rows from existing for non-staff users, but defend the wallet surface too so a future diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index 1a26fa0..080c029 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -167,6 +167,7 @@ def wallet(request): "pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None, "band": request.user.tokens.filter(token_type=Token.BAND).first(), "coin": request.user.tokens.filter(token_type=Token.COIN).first(), + "carte": request.user.tokens.filter(token_type=Token.CARTE).first(), "free_tokens": free_tokens, "tithe_tokens": tithe_tokens, "free_count": len(free_tokens), @@ -204,6 +205,7 @@ def toggle_wallet_applets(request): "pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None, "band": request.user.tokens.filter(token_type=Token.BAND).first(), "coin": request.user.tokens.filter(token_type=Token.COIN).first(), + "carte": request.user.tokens.filter(token_type=Token.CARTE).first(), "free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)), "tithe_tokens": list(request.user.tokens.filter(token_type=Token.TITHE)), }) diff --git a/src/functional_tests/test_dash_wallet.py b/src/functional_tests/test_dash_wallet.py index ef01a41..c4c44c7 100644 --- a/src/functional_tests/test_dash_wallet.py +++ b/src/functional_tests/test_dash_wallet.py @@ -3,6 +3,7 @@ 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 WalletDisplayTest(FunctionalTest): @@ -179,6 +180,50 @@ class WalletDisplayTest(FunctionalTest): ) self.assertEqual(rows, '3') + def test_wallet_tokens_applet_shows_all_owned_trinket_types(self): + """Wallet Tokens applet renders every owned trinket-as-token type + independently — PASS (staff), BAND, COIN, CARTE — alongside FREE + + TITHE. Pre-2026-05-22 the template had an if/elif chain that + only showed ONE of {PASS, BAND, COIN}; that hid BAND + COIN + + CARTE entirely from the wallet for any staff user holding a PASS, + even though all three are usable at the gate. Chunk 1 of the + Shop applet rollout drops that exclusivity so each trinket gets + its own tooltipped icon in the row.""" + # 1. Build a staff user holding every trinket-as-token type + staff = User.objects.create(email="ledger@test.io", is_staff=True) + # post_save signal already created COIN (auto-equipped) + FREE + PASS; + # mint the rest manually so we exercise the full inventory render. + Token.objects.create(user=staff, token_type=Token.BAND) + Token.objects.create(user=staff, token_type=Token.CARTE) + Token.objects.create(user=staff, token_type=Token.TITHE) + # 2. Log in + land on wallet page + self.create_pre_authenticated_session("ledger@test.io") + self.browser.get(self.live_server_url + "/dashboard/wallet/") + # 3. Every trinket-as-token icon is present (no if/elif suppression) + self.wait_for(lambda: self.browser.find_element(By.ID, "id_pass_token")) + self.browser.find_element(By.ID, "id_band_token") + self.browser.find_element(By.ID, "id_coin_on_a_string") + self.browser.find_element(By.ID, "id_carte_token") + # 4. Consumable tokens still present + self.browser.find_element(By.ID, "id_free_token") + self.browser.find_element(By.ID, "id_tithe_token") + # 5. BAND tile carries its tooltip content in the DOM (the wallet's + # `initWalletTooltips` clones `.tt` innerHTML into the portal on + # hover — already exercised by the COIN/FREE hover paths in + # `test_new_user_wallet_shows_starting_balances`; here we just pin + # that the new BAND/CARTE template blocks server-render their full + # tooltip prose, which is the Chunk 1 contract). + band_tt = self.browser.find_element(By.CSS_SELECTOR, "#id_band_token .tt").get_attribute("innerHTML") + self.assertIn("Wristband", band_tt) + self.assertIn("Admit All Entry", band_tt) + self.assertIn("no expiry", band_tt) + # 6. CARTE tile carries its tooltip content too + carte_tt = self.browser.find_element(By.CSS_SELECTOR, "#id_carte_token .tt").get_attribute("innerHTML") + self.assertIn("Carte Blanche", carte_tt) + self.assertIn("Admit up to +6", carte_tt) + self.assertIn("no expiry", carte_tt) + + def test_user_can_purchase_tithe_token_bundle(self): # 1. Log in, navigate to wallet page self.create_pre_authenticated_session("capman@test.io") diff --git a/src/templates/apps/wallet/_partials/_applet-wallet-tokens.html b/src/templates/apps/wallet/_partials/_applet-wallet-tokens.html index 031ddc8..4acc0fa 100644 --- a/src/templates/apps/wallet/_partials/_applet-wallet-tokens.html +++ b/src/templates/apps/wallet/_partials/_applet-wallet-tokens.html @@ -4,6 +4,12 @@ >
{{ pass_token.tooltip_expiry }}
{{ band.tooltip_expiry }}
{{ carte.tooltip_description }}
+ {% if carte.tooltip_shoptalk %} +{{ carte.tooltip_shoptalk }}
+ {% endif %} +{{ carte.tooltip_expiry }}
+