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

434 lines
21 KiB
Python
Raw Normal View History

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
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
from apps.lyric.models import ShopItem, Token, User
class WalletDisplayTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
for slug, name, cols, rows, order in [
("wallet-shop", "Shop", 12, 3, 10),
("wallet-balances", "Wallet Balances", 3, 3, 100),
("wallet-tokens", "Wallet Tokens", 3, 3, 100),
("wallet-payment", "Payment Methods", 6, 3, 100),
]:
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
Applet.objects.update_or_create(
slug=slug,
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
defaults={
"name": name, "grid_cols": cols, "grid_rows": rows,
"context": "wallet", "display_order": order,
},
)
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
# Seed the 3 starting ShopItems — migration `lyric/0009_seed_shop_items`
# populates these in fresh DBs, but FTs run on `TransactionTestCase`
# which wipes all data between tests (post-migrate signal pollution
# doesn't restore RunPython-seeded rows). Mirror the migration's
# locked row shapes here.
ShopItem.objects.update_or_create(
slug="tithe-1",
defaults={
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
"name": "Tithe Token", "description": "1 Tithe Token + 12 Writs",
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
"icon": "fa-piggy-bank", "badge_text": "",
"price_cents": 100, "granted_token_type": Token.TITHE,
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
"granted_count": 1, "granted_writs": 12,
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
"max_owned": None, "display_order": 10, "active": True,
},
)
ShopItem.objects.update_or_create(
slug="tithe-5",
defaults={
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
"name": "Tithe Bundle", "description": "5 Tithe Tokens + 60 Writs",
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
"icon": "fa-piggy-bank", "badge_text": "×5",
"price_cents": 400, "granted_token_type": Token.TITHE,
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
"granted_count": 5, "granted_writs": 60,
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
"max_owned": None, "display_order": 20, "active": True,
},
)
ShopItem.objects.update_or_create(
slug="band-1",
defaults={
"name": "Wristband", "description": "Admit All Entry",
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
"shoptalk": "Unlimited free entry (BYOB)",
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
"icon": "fa-ring", "badge_text": "",
"price_cents": 2000, "granted_token_type": Token.BAND,
"granted_count": 1, "granted_writs": 0,
"max_owned": 1, "display_order": 30, "active": True,
},
)
def test_new_user_wallet_shows_starting_balances(self):
# 1. Log in as new user
self.create_pre_authenticated_session("capman@test.io")
# 2. Navigate to dashboard
self.browser.get(self.live_server_url)
# 3. Find wallet applet summary card
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_wallet")
)
# 4. Click thru to full wallet page via manage link
self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_wallet a.wallet-manage-link"
).click()
# 5. Assert user landed on wallet page
self.wait_for(
lambda: self.assertRegex(self.browser.current_url, r"/dashboard/wallet/$")
)
# 6. Assert writs balance shows 144 (complimentary bundle on signup)
self.wait_for(
lambda: self.assertEqual(
"144",
self.browser.find_element(By.ID, "id_writs_balance").text,
)
)
# 7. Assert esteem balance shows 0
self.assertEqual(
"0",
self.browser.find_element(By.ID, "id_esteem_balance").text,
)
# 8. Assert Coin-on-a-String token element present
coin = self.browser.find_element(By.ID, "id_coin_on_a_string")
# 9. Hover over it; assert tooltip appears w. name, entry text, 'no expiry'
ActionChains(self.browser).move_to_element(coin).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
self.assertIn("Coin-on-a-String", coin_tooltip)
self.assertIn("Admit 1 Entry", coin_tooltip)
self.assertIn("no expiry", coin_tooltip)
# 10. Assert ×1 Free Token present (complimentary on signup)
free_token = self.browser.find_element(By.ID, "id_free_token")
# 11. Hover over it; assert tooltip shows name, entry text, expiry date
ActionChains(self.browser).move_to_element(free_token).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
self.assertIn("Free Token", free_tooltip)
self.assertIn("Admit 1 Entry", free_tooltip)
self.assertIn("Expires", free_tooltip)
def test_wallet_payment_section_renders(self):
# 1. Log in, navigate directly to wallet page
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
# 2. Assert saved payment methods section present
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_payment_methods")
)
# 3. Assert the add-payment-method button is visible
self.browser.find_element(By.ID, "id_add_payment_method")
# 4. Assert Stripe Payment Element mount point exists
self.browser.find_element(By.ID, "id_stripe_payment_element")
def test_user_can_save_a_payment_method(self):
# 1. Log in, navigate to wallet page
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
# 2. Click add-payment-method btn
self.browser.find_element(By.ID, "id_add_payment_method").click()
# 3. Wait for Stripe Payment Element iframe to appear inside mount point
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_stripe_payment_element iframe"
)
)
# 4. Switch into Stripe iframe to interact w. card fields
stripe_frame = self.browser.find_element(
By.CSS_SELECTOR, "#id_stripe_payment_element iframe"
)
self.browser.switch_to.frame(stripe_frame)
# 4a. Wait for card inputs to render inside iframe
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
)
)
# 5. Fill in Stripe test card details
self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
).send_keys("4242424242424242")
self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="MM / YY"]'
).send_keys("12 / 26")
self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="CVC"]'
).send_keys("424")
self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="12345"]'
).send_keys("42424")
# 6. Return to main doc & submit form
self.browser.switch_to.default_content()
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
)
)
self.browser.find_element(By.ID, "id_save_payment_method").click()
# 7. Wait for saved card to appear in payment methods list
# Assert last 4 digits shown (Stripe confirmSetup + server round-trip can be slow)
self.wait_for_slow(
lambda: self.assertIn(
"4242",
self.browser.find_element(By.ID, "id_payment_methods").text,
)
)
def test_user_can_cancel_adding_payment_method(self):
# 1. Log in, navigate to wallet page
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
# 2. Click Add Payment Method
self.wait_for_slow(
lambda: self.browser.find_element(By.ID, "id_add_payment_method")
).click()
# 3. Wait for Cancel button to appear (visible after setup-intent fetch returns)
self.wait_for_slow(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_cancel_payment_method").get_attribute("hidden")
)
)
# 3a. Assert applet expanded to 15 rows
rows = self.browser.execute_script(
"return document.getElementById('id_payment_methods')"
".style.getPropertyValue('--applet-rows').trim()"
)
self.assertEqual(rows, '15')
# 4. Click Cancel
self.browser.find_element(By.ID, "id_cancel_payment_method").click()
# 5. Assert Cancel + Save buttons are hidden again
self.wait_for_slow(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_cancel_payment_method").get_attribute("hidden")
)
)
self.assertTrue(
self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
)
# 6. Assert applet collapses back to 3 grid rows
rows = self.browser.execute_script(
"return document.getElementById('id_payment_methods')"
".style.getPropertyValue('--applet-rows').trim()"
)
self.assertEqual(rows, '3')
feat: wallet Tokens applet shows CARTE + BAND + COIN + PASS independently — Chunk 1 of the Shop applet rollout per [[project-wallet-shop-expansion]]. Pre-Chunk-1 the `_applet-wallet-tokens.html` template used a `{% if pass_token %} ... {% elif band %} ... {% elif coin %}` chain that suppressed 2-of-3 trinkets from the wallet whenever the user held a higher-priority one — bad UX since the equip slot is now the user's opt-in for trinket-as-token use per [[feedback-equip-slot-gates-trinket-use]], so ALL owned trinkets need visibility. Fix: dropped the elif chain → independent `{% if %}` blocks for PASS / BAND / COIN; added a new CARTE block w. `fa-money-check` icon mirroring the Game Kit's render. View context (`apps.dashboard.views.wallet` + `:toggle_wallet_applets`) now passes `carte = user.tokens.filter(token_type=Token.CARTE).first()` alongside the existing pass/band/coin keys (no `is_staff` filter — CARTE has no admin gate). TDD — new `WalletTokensAppletAllTrinketsVisibleTest` (9 ITs): 6 pin individual `#id_<token>` visibility for a staff user holding all 5 types, 2 pin view-context shape (`carte` + `band` keys), 1 pins CARTE-on-non-staff. New FT `test_wallet_tokens_applet_shows_all_owned_trinket_types` reads BAND/CARTE `.tt` `innerHTML` directly (no hover ceremony — already covered by the COIN/FREE hover paths in `test_new_user_wallet_shows_starting_balances`) to pin the new template blocks server-render full tooltip prose. **Trap caught mid-build**: initial multi-line `{# ... #}` Django comment leaked as plain text into the rendered DOM (Django's hash-comment is single-line only), pushing the COIN tile off-screen + breaking the existing hover FT. Switched to `{% comment %}...{% endcomment %}`. Captured in [[feedback-django-comments-single-line-only]] — symptom signature: previously-passing Selenium hover times out + screendump shows literal `{# ...` text near the broken element. 1169 IT/UT + 6 wallet FTs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:07:42 -04:00
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)
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
def test_shop_applet_renders_seeded_items_with_icons_and_badges(self):
"""Chunk 4 of [[project-wallet-shop-expansion]] — the new Shop
applet renders the 3 seeded items (`tithe-1`, `tithe-5`, `band-1`)
as tooltipped tiles. The bundle (`tithe-5`) carries a `×5` badge;
the single-tithe + band tiles render without a badge.
Pinned visual contract: tile presence (id), icon class, badge text,
and price text inside the tooltip prose."""
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
# 1. Shop applet is present
self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
# 2. Each of the 3 seeded items renders w. its own tile
tithe1 = self.browser.find_element(By.ID, "id_shop_tithe-1")
tithe5 = self.browser.find_element(By.ID, "id_shop_tithe-5")
band1 = self.browser.find_element(By.ID, "id_shop_band-1")
# 3. Icons are correct
self.assertIn("fa-piggy-bank", tithe1.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
self.assertIn("fa-piggy-bank", tithe5.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
self.assertIn("fa-ring", band1.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
# 4. Bundle carries the ×5 badge; singles don't
self.assertIn("×5", tithe5.find_element(By.CSS_SELECTOR, ".shop-badge").text)
self.assertEqual(tithe1.find_elements(By.CSS_SELECTOR, ".shop-badge"), [])
self.assertEqual(band1.find_elements(By.CSS_SELECTOR, ".shop-badge"), [])
# 5. Tooltip prose includes name + price (read .tt innerHTML directly
# — hover→portal cloning is already exercised by the existing
# COIN/FREE hover tests).
tithe1_tt = tithe1.find_element(By.CSS_SELECTOR, ".tt").get_attribute("innerHTML")
self.assertIn("Tithe Token", tithe1_tt)
self.assertIn("$1", tithe1_tt)
band1_tt = band1.find_element(By.CSS_SELECTOR, ".tt").get_attribute("innerHTML")
self.assertIn("Wristband", band1_tt)
self.assertIn("$20", band1_tt)
def test_shop_buy_click_opens_guard_portal_with_purchase_prompt(self):
"""BUY ITEM click opens `#id_guard_portal` w. an 'Are you sure?'-style
prompt naming the item + price. Click NVM dismisses; click OK
triggers the Stripe.js dance (mocked out / deferred to a follow-up
FT)."""
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
# 1. Find BUY ITEM btn inside the tithe-1 tile's microtooltip
buy_btn = self.browser.find_element(
By.CSS_SELECTOR, "#id_shop_tithe-1 .tt-buy-btn"
)
# 2. Click it via JS (microtooltip lives in the portal layer + may
# not be hover-visible during Selenium's strict-target check)
self.browser.execute_script("arguments[0].click();", buy_btn)
# 3. Guard portal opens w. a prompt mentioning Tithe + $1
portal = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_guard_portal")
)
self.wait_for(
lambda: self.assertIn("active", portal.get_attribute("class"))
)
msg = portal.find_element(By.CSS_SELECTOR, ".guard-message").text
self.assertIn("Tithe", msg)
self.assertIn("$1", msg)
# 4. NVM dismisses
portal.find_element(By.CSS_SELECTOR, ".guard-no").click()
self.wait_for(
lambda: self.assertNotIn("active", portal.get_attribute("class"))
)
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
def test_shop_buy_guard_portal_pins_item_tooltip(self):
"""While the BUY-ITEM guard portal is open, the item's main +
mini tooltip stay pinned they don't dismiss when the cursor
leaves the BUY btn area to reach the guard's OK / NVM. Fixes
the orphan-prompt UX where "Buy {item} for ${price}?" floated
on its own w. no visual referent. Pinning is released on
either confirm OR dismiss; both schedule the normal hide."""
import time
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
# 1. Programmatically trigger the tile-enter → both portals active.
self.browser.execute_script(
"document.getElementById('id_shop_tithe-5').dispatchEvent("
"new MouseEvent('mouseenter', {bubbles: true}));"
)
self.wait_for(
lambda: self.assertIn(
"active",
self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
)
)
self.assertIn(
"active",
self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
)
# 2. Click the cloned BUY btn inside the mini portal — opens guard.
self.browser.execute_script(
"document.querySelector('#id_mini_tooltip_portal .tt-buy-btn').click();"
)
guard = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_guard_portal")
)
self.wait_for(
lambda: self.assertIn("active", guard.get_attribute("class"))
)
# 3. Simulate cursor leaving both portals to reach the guard btns.
self.browser.execute_script("""
document.getElementById('id_mini_tooltip_portal').dispatchEvent(
new MouseEvent('mouseleave', {bubbles: true}));
document.getElementById('id_tooltip_portal').dispatchEvent(
new MouseEvent('mouseleave', {bubbles: true}));
""")
# 4. Wait longer than the wallet.js hide delay (200ms) + buffer.
time.sleep(0.4)
# 5. Both portals are STILL active — pinned by the open guard.
self.assertIn(
"active",
self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
)
self.assertIn(
"active",
self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
)
# 6. Click NVM — guard dismisses, pin releases.
guard.find_element(By.CSS_SELECTOR, ".guard-no").click()
self.wait_for(
lambda: self.assertNotIn("active", guard.get_attribute("class"))
)
# 7. After unpin + hide delay, portals fade out.
time.sleep(0.4)
self.assertNotIn(
"active",
self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
)
self.assertNotIn(
"active",
self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
)
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
def test_shop_band_already_owned_shows_disabled_buy_btn(self):
"""User who already owns 1 BAND (`max_owned=1`) sees the band-1
tile w. its BUY btn disabled + an 'Already owned' microtooltip
visible-but-unbuyable per the decision in [[project-wallet-
shop-expansion]]."""
owner = User.objects.create(email="bander@test.io")
Token.objects.create(user=owner, token_type=Token.BAND)
self.create_pre_authenticated_session("bander@test.io")
self.browser.get(self.live_server_url + "/dashboard/wallet/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
band_tile = self.browser.find_element(By.ID, "id_shop_band-1")
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: `.tt-microbutton-portal` (Chunk 4's wrap-inside-`.tt`) replaced w. a sibling `.tt-micro` div on each `.shop-tile`. `wallet.js`'s `initWalletTooltips` clones BOTH into separate portals on hover — `.tt` → `#id_tooltip_portal` (main card), `.tt-micro` → `#id_mini_tooltip_portal` (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: `WalletTooltips.pin()` / `.unpin()` exposed on window; `wallet-shop.js`'s BUY click calls `pin()` before `showGuard()` + both `onConfirm` / `onDismiss` callbacks call `unpin()` → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new `Applet.display_order` field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets `wallet-shop.display_order=10` so Shop renders atop Balances/Tokens/Payment. `applet_context()` updated to `.order_by("display_order", "pk")`. New `WalletAppletOrderTest` (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot `.tt-title` / `.tt-description` / `.tt-shoptalk` / `.tt-expiry` classes as the Tokens row. New `ShopItem.shoptalk` field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New `ShopItem.tooltip_expiry()` method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: `.tt-micro` hidden in source DOM (portal-only); `#id_mini_tooltip_portal` mostly mirrors gameboard's mini at `_gameboard.scss:140` but allows BUY-btn label to wrap onto multiple lines (`white-space: normal` on `.tt-buy-btn`); `.tt-already-owned` styled w. `--secUser` italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: `lyric/0010_repricing_tithe_writs` (writs + description), `lyric/0011_shopitem_shoptalk` (schema), `lyric/0012_seed_shop_shoptalk` (band split), `applets/0012_applet_display_order` (schema), `applets/0013_wallet_shop_display_order` (Shop atop). All idempotent. **TDD** — 5 new ITs across `test_shop_models.py` (`shoptalk` default + per-item assertions, `tooltip_expiry` method, updated tithe writs values, `WalletAppletOrderTest`), 1 new FT (`test_shop_buy_guard_portal_pins_item_tooltip` — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to `.tt-micro` (no `.tt-buy-btn` present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line `{# #}` comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) `spyOn(window, 'fetch')` Jasmine double-spy collision (cf trapped previously); (c) async pollution where `afterEach` restores `window.Stripe=undefined` before `_doBuy`'s continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:21:10 -04:00
# Capped item — NO buy btn at all (parity w. Game Kit's status-
# only "Equipped" / "In-Use: X" pills, which never pair status
# text w. a disabled action btn).
self.assertEqual(band_tile.find_elements(By.CSS_SELECTOR, ".tt-buy-btn"), [])
# 'Already owned' microtext lives in the `.tt-micro` sibling-of-.tt
# (cloned into `#id_mini_tooltip_portal` on hover by wallet.js).
micro_html = band_tile.find_element(By.CSS_SELECTOR, ".tt-micro").get_attribute("innerHTML")
self.assertIn("Already owned", micro_html)
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug `wallet-shop`, seeded in Chunk 2) now renders the catalog as a horizontal grid of `.shop-tile` icons: `tithe-1` ($1, fa-piggy-bank), `tithe-5` ($4, fa-piggy-bank w. `×5` badge), `band-1` ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a `.tt-microbutton-portal` w. a `.btn-primary` BUY ITEM button — clicking opens `#id_guard_portal` w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit `max_owned` (eg. BAND, owned=1, cap=1) render w. `.btn-disabled` + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — `wallet` view + `toggle_wallet_applets` view both pass `shop_items` (decorated w. per-user `.available` via the new `_shop_items_for(user)` helper) + `default_payment_method_id` + `stripe_publishable_key`. SCSS — `.wallet-shop` (flex column wrapping `.shop-grid` flex row), `.shop-tile` (inline-flex tooltip target), `.shop-badge` (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), `.tt-microbutton-portal` (column-flex, BUY btn + 'Already owned' caption styling). JS in `wallet-shop.js` exposes a singleton `WalletShop` module (matching the project's `Brief` / `SeaDeal` / `StageCard` module pattern) w. a tested `initWalletShop()` method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed `data-shop-wired` flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into `wallet.html` after `wallet.js`. **TDD** — 5 Jasmine specs in `WalletShopSpec.js`: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs `shop_item_slug` to `/shop/buy`; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) `spyOn(window, 'fetch')` collides if another spec already spied on fetch — switched to save+restore via per-test `_origFetch` capture; (b) T3 async pollution — sync assertion passed, `afterEach` restored `window.Stripe=undefined`, then `_doBuy`'s async continuation hit `Stripe(pubKey)` and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in `test_dash_wallet.py`: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via `textContent` since `.tt` is `display: none`). FT trap caught: `TransactionTestCase` wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors `test_shop_views.py`'s `_seed_starting_items` pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:15:05 -04:00
# Legacy `test_user_can_purchase_tithe_token_bundle` FT (asserting
# `#id_tithe_token_shop` inside Balances) was removed in Chunk 5 of
# [[project-wallet-shop-expansion]] — the tithe purchase surface
# moved to the dedicated Shop applet. Coverage now lives in:
# - `test_shop_applet_renders_seeded_items_with_icons_and_badges`
# (tile + icon + badge + price)
# - `test_shop_buy_click_opens_guard_portal_with_purchase_prompt`
# (BUY → guard portal → NVM dismisses)
# - `test_shop_band_already_owned_shows_disabled_buy_btn`
# (max_owned cap renders BUY as `.btn-disabled` w. microtext)