FT fixes for polish-9 spec changes — CI #338 surfaced 4 stale assertions; sig-gate Brief race exposed by removing implicit wait
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

CI pipeline #338 caught 4 FT failures cascading from yesterday's polish-9 + applet realignment commits (955bdc7, 652cef0). All four are stale assertions in FT code — no production code changes needed. ITs were already updated in the original commits; missed the parallel FT updates.

**(1) `test_gear_btn_opens_menu_with_nvm_only`** (test_game_my_sea.py:1851) — NVM btn changed `<a class="btn" href="...">` → `<button onclick="location.href=...">` (per [[feedback-btn-vs-anchor-font-family]] sans-serif fix). FT was reading `href` attr (returns None on buttons → `TypeError: argument of type 'NoneType' is not iterable`). Switched to read `onclick` attr (w. `or ""` guard against None).

**(2) `test_del_btn_is_disabled_until_hand_complete`** (test_game_my_sea.py:982 → renamed) — DEL btn now un-disables on the FIRST draw, not at hand completion (per the new state-machine spec, `_setHasDrawn(true)` fires on first deposit + AUTO DRAW POST-commit). Renamed → `test_del_btn_is_disabled_until_first_draw`; inverted the mid-draw assertion (was: still-disabled after 1 draw → now: un-disables immediately after 1 draw); kept the post-completion check (DEL stays enabled).

**(3) `test_carte_blanche_equip_and_multi_slot_gatekeeper`** (test_trinket_carte_blanche.py:89) — TWO issues here, only the first was symptomatic in CI:
  - Step 2 used `#id_kit_free_token` on /gameboard/ as a "non-trinket, no mini-tooltip" demo target. Free Token moved off Game Kit applet to Wallet applet per the equippables-only spec; no non-equippable icon left on Game Kit to demo w. Dropped step 2 entirely — the test's primary thing (Carte multi-slot equip flow at steps 3+) is intact.
  - SECOND-ORDER issue uncovered when (1) above stopped masking it: the deleted step 2 used to provide a ~5+ second wait (find Free Token + hover + wait for tooltip portal). That wait was enough for the auto-firing `.my-sea-sign-gate-brief` (slides in on /gameboard/ for users w/o a sig via the My Sea applet's `{% include _my_sea_sign_gate_brief.html %}` branch) to settle. Without the wait, the Brief is mid-slide when step 8 tries to click `id_create_game_btn` → `ElementClickInterceptedException` (Brief obscures button). Added explicit `.my-sea-sign-gate-brief .btn-cancel` wait-then-click between steps 1 + 3 to dismiss the Brief before proceeding.

**(4) `test_game_kit_panel_shows_token_inventory`** (test_gameboard.py:74) — TWO issues here too:
  - Step 7's `#id_kit_free_token` Free Token tooltip assertion. Same removal as (3). Replaced w. a NEGATIVE assertion that the element does NOT exist on Game Kit (regression guard against accidentally re-adding non-equippable items).
  - SECOND-ORDER again: step 9's `#id_kit_card_deck` check was a stale assertion that predated the `apps/lyric/models.py:540` `unlocked_decks.add(earthman)` post_save signal. `id_kit_card_deck` is the `{% empty %}`-branch placeholder, only rendered when `deck_variants` is empty. `capman@test.io` (the test fixture user) gets Earthman auto-unlocked → the concrete `id_kit_earthman_deck` renders instead. This was a latent stale assertion that only surfaced now because step 7's Free Token failure used to short-circuit the test before it reached step 9. Switched check to `id_kit_earthman_deck`.

Pattern worth noting for future cross-cutting refactors: when a test step has a side-effect wait (`wait_for(... tooltip displayed ...)`), removing it can unmask sig-gate / palette / Brief banners that auto-slide in on page load. The Brief race in (3) wasn't a NEW bug introduced by polish-9; it was always there, masked by the timing of the removed step. Same for the stale `id_kit_card_deck` assertion — predates the signal change; only surfaced when the failure cascade moved past it.

Discipline note for this session: user explicitly overrode [[feedback-ft-run-discipline]] when "specifically working on FTs, new or old" — ran each fix locally by full dotted path to verify before committing. All 4 green locally (8-16s each).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-26 02:34:02 -04:00
parent 652cef09c0
commit a133a9c1c3
3 changed files with 56 additions and 33 deletions

View File

@@ -969,23 +969,26 @@ class MySeaCardDrawTest(FunctionalTest):
# ── Test 6 ─────────────────────────────────────────────────────────────── # ── Test 6 ───────────────────────────────────────────────────────────────
def test_del_btn_is_disabled_until_hand_complete(self): def test_del_btn_is_disabled_until_first_draw(self):
"""Iter-4c — DEL btn renders `.btn-disabled` server-side until """User spec 2026-05-26: DEL btn renders `.btn-disabled` only
the hand is complete (per spec: the 24h free-draw quota is UNTIL the first card lands (was: until hand complete). The 24h
committed at first-card-draw, can't be refunded by an early free-draw quota is still committed at first-card-draw + can't be
DEL). Once the hand fills, JS removes `.btn-disabled` from DEL.""" refunded by an early DEL, but the user can DEL the in-progress
hand to start over within the cycle. JS `_setHasDrawn(true)` fires
on the first deposit + at the AUTO DRAW POST-commit moment."""
picker = self._enter_picker_phase() picker = self._enter_picker_phase()
delbtn = picker.find_element(By.CSS_SELECTOR, "#id_sea_del") delbtn = picker.find_element(By.CSS_SELECTOR, "#id_sea_del")
# Pre-draw — disabled.
self.assertIn("btn-disabled", delbtn.get_attribute("class")) self.assertIn("btn-disabled", delbtn.get_attribute("class"))
self._draw_one(picker, "levity") self._draw_one(picker, "levity")
# Mid-draw — still disabled. # First card landed — DEL un-disables immediately.
self.assertIn("btn-disabled", delbtn.get_attribute("class"))
self._draw_one(picker, "levity")
self._draw_one(picker, "gravity")
# Hand complete — DEL un-disables (clicking now opens guard portal).
self.wait_for( self.wait_for(
lambda: self.assertNotIn("btn-disabled", delbtn.get_attribute("class")) lambda: self.assertNotIn("btn-disabled", delbtn.get_attribute("class"))
) )
# Subsequent draws + completion — DEL stays enabled.
self._draw_one(picker, "levity")
self._draw_one(picker, "gravity")
self.assertNotIn("btn-disabled", delbtn.get_attribute("class"))
# ── Test 7 ─────────────────────────────────────────────────────────────── # ── Test 7 ───────────────────────────────────────────────────────────────
@@ -1846,9 +1849,12 @@ class MySeaGearBtnTest(FunctionalTest):
lambda: self.browser.find_element(By.ID, "id_my_sea_menu") lambda: self.browser.find_element(By.ID, "id_my_sea_menu")
) )
self.assertEqual(menu.value_of_css_property("display"), "block") self.assertEqual(menu.value_of_css_property("display"), "block")
# NVM link present, DEL + BYE deliberately absent. # NVM btn present, DEL + BYE deliberately absent. NVM is a `<button
# onclick="location.href=...">` (NOT an `<a class="btn">`) per 2026-
# 05-26 fix — anchors inherit body's serif font, buttons stay sans-
# serif. Check the onclick attr for the nav target instead of href.
nvm = menu.find_element(By.CSS_SELECTOR, ".btn-cancel") nvm = menu.find_element(By.CSS_SELECTOR, ".btn-cancel")
self.assertIn("/gameboard/", nvm.get_attribute("href")) self.assertIn("/gameboard/", nvm.get_attribute("onclick") or "")
self.assertEqual( self.assertEqual(
len(menu.find_elements(By.CSS_SELECTOR, ".btn-danger")), 0, len(menu.find_elements(By.CSS_SELECTOR, ".btn-danger")), 0,
) )

View File

@@ -70,21 +70,26 @@ class GameboardNavigationTest(FunctionalTest):
self.assertIn("Coin-on-a-String", coin_tooltip) self.assertIn("Coin-on-a-String", coin_tooltip)
self.assertIn("Admit 1 Entry", coin_tooltip) self.assertIn("Admit 1 Entry", coin_tooltip)
self.assertIn("and another after that", coin_tooltip) self.assertIn("and another after that", coin_tooltip)
# 7. Assert 1× Free Token (complimentary) present in kit # 7. (REMOVED 2026-05-26) Free Token used to live in Game Kit applet
free_token = self.browser.find_element(By.ID, "id_kit_free_token") # — moved to /dashboard/'s My Wallet applet per spec ("only equippables
# 8. Hover over it; assert tooltip shows name, entry text & expiry date # in Game Kit"). Free Token tooltip coverage now lives on /dashboard/.
ActionChains(self.browser).move_to_element(free_token).perform() # 8. NEGATIVE assertion: Free Token element must NOT exist on Game Kit
self.wait_for( # — regression guard against accidentally re-adding non-equippable
lambda: self.assertTrue( # items here.
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() self.assertEqual(
len(self.browser.find_elements(By.ID, "id_kit_free_token")), 0,
"Free Token must NOT render in the Game Kit applet — it's not "
"equippable; lives in My Wallet applet on /dashboard/",
) )
) # 9. Assert card deck + dice set present. capman has Earthman unlocked
free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text # via the post_save signal (`apps/lyric/models.py:540`), so the
self.assertIn("Free Token", free_tooltip) # `id_kit_card_deck` empty-state placeholder doesn't render — the
self.assertIn("Admit 1 Entry", free_tooltip) # concrete `id_kit_earthman_deck` does instead. (The `id_kit_card_
self.assertIn("Expires", free_tooltip) # deck` placeholder was previously asserted here, which was a stale
# 9. Assert card deck & dice set placeholder present # check predating the auto-unlock signal — caught 2026-05-26 by the
self.browser.find_element(By.ID, "id_kit_card_deck") # CI run that surfaced it after the Free Token relocation moved this
# test's failure point past step 7.)
self.browser.find_element(By.ID, "id_kit_earthman_deck")
self.browser.find_element(By.ID, "id_kit_dice_set") self.browser.find_element(By.ID, "id_kit_dice_set")

View File

@@ -85,15 +85,27 @@ class CarteBlancheTest(FunctionalTest):
self.browser.get(self.live_server_url + "/gameboard/") self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
# 2. Hover over Free Token — no mini tooltip (not a trinket, no data-token-id) # 1a. Dismiss the My-Sea sign-gate Brief that auto-fires for new users
el = self.browser.find_element(By.ID, "id_kit_free_token") # without a sig. The Brief slides in via `Brief.showBanner` on DOM-
ActionChains(self.browser).move_to_element(el).perform() # ready + obscures the create-game-btn (step 8 below). The prior
# version of the test had a 5+ second hover/wait at step 2 (Free
# Token tooltip — now removed) that masked the race; without that
# wait, we have to explicitly dismiss the banner before proceeding.
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate-brief")
)
self.assertFalse(
self.browser.find_element(By.ID, "id_mini_tooltip_portal").is_displayed()
) )
self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-sign-gate-brief .btn-cancel"
).click()
# 2. (REMOVED 2026-05-26) Free Token used to live in the Game Kit
# applet here + served as the "non-trinket, no mini-tooltip" example.
# Per user spec, Free + Tithe tokens moved to /dashboard/'s Wallet
# applet — Game Kit now holds ONLY equippables (trinkets, decks,
# dice), all of which carry data-token-id or data-deck-id + show
# the mini-tooltip. No non-equippable icon left on Game Kit to
# demo the no-mini case w. The rest of the test (Carte Blanche
# multi-slot equip flow) is the primary thing being verified.
# Coin-on-a-String IS equippable (has data-token-id) — mini tooltip shows # Coin-on-a-String IS equippable (has data-token-id) — mini tooltip shows
coin_el = self.browser.find_element(By.ID, "id_kit_coin_on_a_string") coin_el = self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
ActionChains(self.browser).move_to_element(coin_el).perform() ActionChains(self.browser).move_to_element(coin_el).perform()