room-stage FTs: realign 4 fails to new my-sea NVM + spread-modal + landscape kit-bag UX — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

CI #346 test-FTs-room had 4 consistent fails (failed on both the first run
AND the retry, so real, not flakes). All 4 are test-side — the shipped
features are correct; the FTs lagged behind deliberate UX changes + a
race they never needed to depend on.

test_game_my_sea.py
- test_nvm_navigates_back_to_gameboard → renamed test_nvm_navigates_back_to_
  my_sea_hex; asserts /gameboard/my-sea/$ now. NVM on the gatekeeper navigates
  to the table hex, not out to /gameboard/ (changed 5cade51: gatekeeper +
  picker NVM → hex; only landing + sign-gate eject to /gameboard/). Sibling
  test_gear_btn_opens_menu_with_nvm_only still passes (only checks the onclick
  contains /gameboard/).
- test_default_spread_is_situation_action_outcome → add _open_spread_modal(self).
  The spread combobox moved into #id_sea_spread_modal (burger Sea sub-btn
  sprint); .sea-select-current .text returned '' while the modal was hidden.
  Mirrors the already-updated sibling test_picking_spread_swaps_*.

test_trinket_carte_blanche.py (the recurring #344/#345/#346 carte fail)
- Sign-gate Brief: replace the hard wait_for_slow(find .my-sea-sign-gate-brief)
  + NVM-click with dismiss_brief_if_present(). The Brief fires via
  Brief.showBanner on DOM-ready; its appearance is a DOM-ready-vs-note.js-load
  race, so under CI contention it sometimes never lands in-window and ANY hard
  wait throws NoSuchElement. a39053d misdiagnosed this as a timeout (→
  wait_for_slow); it is not. The test never asserts the Brief — it only clears
  it to unblock a later click. dismiss_brief_if_present removes it if present +
  no-ops if absent: robust to the race.
- Kit-bag token select: JS-click the #id_kit_bag_dialog CARTE token. In
  landscape (CI default viewport) the kit-bag dialog is a vertical bar that
  slides in via a max-width transition (burger landscape refactor), so a
  Selenium .click races the animation + can't scroll the token into the
  overflow container ("could not be scrolled into view"). execute_script fires
  the bound handler directly. Applied in both carte tests
  (open_kit_and_select_carte + the in-use attribution flow); token-rails stays
  a normal click (it lives on the gate page, not in the dialog).

Verification: all 4 methods green locally (landscape viewport) —
test_carte_in_use_game_kit_shows_room_attribution 10.8s; the multi-slot carte
+ both my-sea methods 35.5s.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-27 02:00:35 -04:00
parent c30b63cd5d
commit 1c799d35ca
2 changed files with 35 additions and 24 deletions

View File

@@ -565,6 +565,7 @@ class MySeaSpreadFormTest(FunctionalTest):
the hidden `<input id="id_sea_spread">` initial value + on the hidden `<input id="id_sea_spread">` initial value + on
`.my-sea-cross[data-spread]`.""" `.my-sea-cross[data-spread]`."""
picker = self._enter_picker_phase() picker = self._enter_picker_phase()
_open_spread_modal(self) # .sea-select-current lives in the spread modal
hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread") hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread")
self.assertEqual( self.assertEqual(
hidden.get_attribute("value"), "situation-action-outcome", hidden.get_attribute("value"), "situation-action-outcome",
@@ -1929,7 +1930,13 @@ class MySeaGearBtnTest(FunctionalTest):
len(menu.find_elements(By.CSS_SELECTOR, ".btn-abandon")), 0, len(menu.find_elements(By.CSS_SELECTOR, ".btn-abandon")), 0,
) )
def test_nvm_navigates_back_to_gameboard(self): def test_nvm_navigates_back_to_my_sea_hex(self):
# NVM on the my-sea gatekeeper navigates back to the my-sea table hex
# (/gameboard/my-sea/), NOT out to /gameboard/ — changed 5cade51
# (gatekeeper + picker NVM → table hex; only the landing + sign-gate
# phases eject to /gameboard/). Sibling test_gear_btn_opens_menu_with_
# nvm_only still passes since it only checks the onclick *contains*
# /gameboard/.
self.create_pre_authenticated_session(self.email) self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/") self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
gear = self.wait_for( gear = self.wait_for(
@@ -1946,6 +1953,6 @@ class MySeaGearBtnTest(FunctionalTest):
).click() ).click()
self.wait_for( self.wait_for(
lambda: self.assertRegex( lambda: self.assertRegex(
self.browser.current_url, r"/gameboard/$" self.browser.current_url, r"/gameboard/my-sea/$"
) )
) )

View File

@@ -85,21 +85,16 @@ 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"))
# 1a. Dismiss the My-Sea sign-gate Brief that auto-fires for new users # 1a. Clear the My-Sea sign-gate Brief that auto-fires for new users
# without a sig. The Brief slides in via `Brief.showBanner` on DOM- # without a sig — it slides in via `Brief.showBanner` on DOM-ready and
# ready + obscures the create-game-btn (step 8 below). The prior # can obscure the create-game-btn (step 8 below). Earlier revisions
# version of the test had a 5+ second hover/wait at step 2 (Free # hard-WAITED for the banner (wait_for, then wait_for_slow), but its
# Token tooltip — now removed) that masked the race; without that # appearance is a DOM-ready-vs-note.js-load race: under CI contention
# wait, we have to explicitly dismiss the banner before proceeding. # it sometimes never lands in-window, so the wait threw NoSuchElement
# `wait_for_slow` (60s ceiling) — under CI contention the Brief's # (the recurring CI #344/#345/#346 carte fail). We don't assert the
# DOM-ready handler can land past wait_for's 10s default; CI #345 # Brief here — we just need it gone. dismiss_brief_if_present DOM-
# hit this exact NoSuchElement timeout. # removes it if present and no-ops if absent: robust to the race.
self.wait_for_slow( self.dismiss_brief_if_present()
lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sea-sign-gate-brief")
)
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 # 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. # applet here + served as the "non-trinket, no mini-tooltip" example.
@@ -190,16 +185,18 @@ class CarteBlancheTest(FunctionalTest):
def open_kit_and_select_carte(): def open_kit_and_select_carte():
self.browser.find_element(By.ID, "id_kit_btn").click() self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for( token = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR,
f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]',
) )
) )
self.browser.find_element( # JS-click: in landscape (CI default viewport) the kit-bag dialog
By.CSS_SELECTOR, # is a vertical bar that slides in via a max-width transition, so a
f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', # Selenium .click races the animation and can't scroll the token
).click() # into view inside the overflow container ("could not be scrolled
# into view"). execute_script fires the bound handler directly.
self.browser.execute_script("arguments[0].click()", token)
def deposit_carte(): def deposit_carte():
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
@@ -353,6 +350,9 @@ class CarteBlancheTest(FunctionalTest):
self.create_pre_authenticated_session("blanche@test.io") self.create_pre_authenticated_session("blanche@test.io")
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"))
# Clear the sign-gate Brief if it fired (see the multi-slot test) so it
# can't obscure the New Game applet's create-game-btn below.
self.dismiss_brief_if_present()
# DON the Carte Blanche via tooltip portal # DON the Carte Blanche via tooltip portal
carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche")
@@ -379,12 +379,16 @@ class CarteBlancheTest(FunctionalTest):
self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url)) self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))
self.browser.find_element(By.ID, "id_kit_btn").click() self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for( token = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR,
f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]',
) )
).click() )
# JS-click — landscape kit-bag dialog slide-in animation (see the
# multi-slot test's open_kit_and_select_carte); bypasses the "could
# not be scrolled into view".
self.browser.execute_script("arguments[0].click()", token)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed")