From 03feaee9f212994771803f6b8f182e8733d05c79 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 26 May 2026 21:39:36 -0400 Subject: [PATCH] =?UTF-8?q?burger=20z-index=20drop=20318=E2=86=92314=20to?= =?UTF-8?q?=20match=20.gear-btn;=20FT=20base=20dismiss=5Fbrief=5Fif=5Fpres?= =?UTF-8?q?ent=20helper=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 3ca986f. Lands the FT fixes the burger sprint surfaced + tightens the burger's z-stack so the existing kit_btn / bud_btn / bud_panel / dialog naturally cover it on overlap (vs. the explicit opacity-fade rules the first iteration tried). ## Burger z-index drop `static_src/scss/_burger.scss`: - `#id_burger_btn` z 318 → 314 (matches the page-level `.gear-btn` z). Below kit_btn + bud_btn (318), bud_panel (317), kit_bag_dialog (316) — so every overlapping surface visually covers the burger when it appears. The earlier `html.bud-open #id_burger_btn { opacity: 0 }` + `html:has(#id_kit_bag_dialog[open]) #id_burger_btn { opacity: 0 }` rules are now redundant + deleted. - `#id_burger_fan` z 317 → 313 (stays just below burger so burger remains clickable when fan is open). `static_src/scss/_game-kit.scss`: - `#id_kit_bag_dialog` z 319 → 316 (reverts an earlier iteration that bumped it above burger). 316 keeps the dialog BELOW kit_btn + bud_btn so those stay visible + clickable when dialog opens — user re-clicks kit_btn to close. Resolves "kit btn disappears when dialog open" reported on iPad portrait. `static_src/scss/_bud.scss`: - `html.bud-open #id_kit_btn { opacity: 0 }` now wrapped in `@media (orientation: portrait)`. In landscape kit_btn lives at the TOP of the right sidebar + bud_panel sits at the BOTTOM — no visual conflict, kit stays visible. ## FT base — dismiss_brief_if_present() `functional_tests/base.py`: - New helper on both `FunctionalTest` + `ChannelsFunctionalTest`: ```python def dismiss_brief_if_present(self, banner_selector=".note-banner", browser=None): ``` - Removes any matching Brief banner from the DOM via `execute_script`. No-op if absent. Default selector matches every Brief shape; pass a specific selector to target one kind. DOM-removal rather than NVM-click bypasses the dismiss_url POST flow that some Briefs (FREE/PAID DRAW) wire up — use this when the test cares about the page state AFTER a Brief, not the dismissal mechanics. ## FT — test_trinket_coin_on_a_string.py Two methods (`test_coin_deposit_unequips_from_kit_bag_and_fills_one_slot`, `test_coin_in_use_game_kit_shows_room_attribution_and_btn_disabled`) updated: 1. `self.dismiss_brief_if_present()` right after `wait_for(id_game_kit)`. `coin@test.io` is created fresh w/o a significator, so the `_my_sea_sign_gate_brief.html` auto-spawns on `/gameboard/` + intercepts the create-game btn click. Dismissing the banner clears the runway. 2. `self.wait_for(... dialog.rect["width"] > 50 ...)` after `kit_btn.click()` + before interacting w. tokens inside the dialog. The landscape kit_bag_dialog animates `max-width 0 → 5rem` over 0.25s; the existing wait_for(find_token).click() found the COIN .token in the DOM immediately + raced the animation — Selenium's scrollIntoView fails on a 0-width container. Waiting for the rect to widen past 50px (≈ 3rem) confirms layout has rendered. ## Verification - All 4 previously-failing FTs from the burger sprint re-run green: - CarteBlanche.test_carte_blanche_equip_and_multi_slot_gatekeeper (was flaking; passed on retry — pre-existing tooltip-population race, not in scope) - CoinOnAString.test_coin_deposit_unequips_from_kit_bag_and_fills_one_slot - CoinOnAString.test_coin_in_use_game_kit_shows_room_attribution_and_btn_disabled - GatekeeperTest.test_second_gamer_drops_token_into_open_slot - IT+UT suite still 1356 green (no touches to model/view code). Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/functional_tests/base.py | 24 +++++++++++++++++++ .../test_trinket_coin_on_a_string.py | 19 +++++++++++++++ src/static_src/scss/_bud.scss | 11 ++++++--- src/static_src/scss/_burger.scss | 13 ++++++++-- src/static_src/scss/_game-kit.scss | 10 ++++---- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/functional_tests/base.py b/src/functional_tests/base.py index 7d950a4..220ed98 100644 --- a/src/functional_tests/base.py +++ b/src/functional_tests/base.py @@ -138,6 +138,20 @@ class FunctionalTest(StaticLiveServerTestCase): b.execute_script("arguments[0].click()", btn) self.wait_for(_click) + def dismiss_brief_if_present(self, banner_selector=".note-banner", browser=None): + """Remove any Brief banners matching `banner_selector` from the DOM so + subsequent clicks aren't intercepted by the slide-down overlay. No-op + if no matching banner exists. Default matches every Brief shape; pass + a specific selector (e.g. '.my-sea-sign-gate-brief') to target one. + + DOM-removal rather than NVM-click — bypasses the dismiss_url POST + flow that some Briefs (FREE/PAID DRAW) wire up; use this when the + test cares about the page state AFTER a Brief, not the dismissal.""" + b = browser or self.browser + b.execute_script( + f"document.querySelectorAll({banner_selector!r}).forEach(el => el.remove());" + ) + @wait def wait_to_be_logged_in(self, email): self.browser.find_element(By.CSS_SELECTOR, "#id_logout"), @@ -225,6 +239,16 @@ class ChannelsFunctionalTest(ChannelsLiveServerTestCase): b.execute_script("arguments[0].click()", btn) self.wait_for(_click) + def dismiss_brief_if_present(self, banner_selector=".note-banner", browser=None): + """Remove any Brief banners matching `banner_selector` from the DOM so + subsequent clicks aren't intercepted by the slide-down overlay. No-op + if no matching banner exists. See FunctionalTest.dismiss_brief_if_present + for the full docstring — mirrored here for ChannelsLiveServerTestCase.""" + b = browser or self.browser + b.execute_script( + f"document.querySelectorAll({banner_selector!r}).forEach(el => el.remove());" + ) + def create_pre_authenticated_session(self, email): if self.test_server: session_key = create_session_on_server(self.test_server, email) diff --git a/src/functional_tests/test_trinket_coin_on_a_string.py b/src/functional_tests/test_trinket_coin_on_a_string.py index 48ac9c9..4ab6d41 100644 --- a/src/functional_tests/test_trinket_coin_on_a_string.py +++ b/src/functional_tests/test_trinket_coin_on_a_string.py @@ -70,6 +70,10 @@ class CoinOnAStringTest(FunctionalTest): self.create_pre_authenticated_session("coin@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) + # The my-sea-sign-gate-brief auto-spawns on /gameboard/ for users w/o + # a significator (coin@test.io is fresh) + intercepts the create-game + # btn click. Dismiss it so subsequent clicks land. + self.dismiss_brief_if_present() # Create a new room → land on gatekeeper self.browser.find_element(By.ID, "id_new_game_name").send_keys("Coin Room") @@ -82,6 +86,13 @@ class CoinOnAStringTest(FunctionalTest): # Open kit bag, pick COIN, click rails btn — drop_token POSTs w. token_id # via the kit-bag JS that injects a hidden token_id input before submit. self.browser.find_element(By.ID, "id_kit_btn").click() + # Wait for the dialog's max-width animation (landscape: 0→5rem, + # portrait: max-height 0→5rem) to finish — clicking the COIN .token + # mid-animation fails scrollIntoView on the 0-dimension container. + self.wait_for(lambda: self.assertGreater( + self.browser.find_element(By.ID, "id_kit_bag_dialog").rect["width"], + 50, + )) self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, @@ -136,6 +147,7 @@ class CoinOnAStringTest(FunctionalTest): self.create_pre_authenticated_session("coin@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) + self.dismiss_brief_if_present() # sign-gate brief intercepts create-game-btn # Create room, deposit COIN — rails-click puts slot 1 in RESERVED; # OK btn fills it (debit_token sets coin.current_room = room). @@ -143,6 +155,13 @@ class CoinOnAStringTest(FunctionalTest): self.browser.find_element(By.ID, "id_create_game_btn").click() self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url)) self.browser.find_element(By.ID, "id_kit_btn").click() + # Wait for the dialog's max-width animation (landscape: 0→5rem, + # portrait: max-height 0→5rem) to finish — clicking the COIN .token + # mid-animation fails scrollIntoView on the 0-dimension container. + self.wait_for(lambda: self.assertGreater( + self.browser.find_element(By.ID, "id_kit_bag_dialog").rect["width"], + 50, + )) self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, diff --git a/src/static_src/scss/_bud.scss b/src/static_src/scss/_bud.scss index 6583e79..fac444e 100644 --- a/src/static_src/scss/_bud.scss +++ b/src/static_src/scss/_bud.scss @@ -103,6 +103,9 @@ } // html.bud-open: slide the panel out, fade the kit btn away. +// Kit fade is PORTRAIT-only: in landscape kit lives at the TOP of the +// right sidebar + bud_panel sits at the BOTTOM (full-width strip), so +// they don't visually compete + kit can stay visible. html.bud-open { #id_bud_panel { transform: scaleX(1); @@ -110,9 +113,11 @@ html.bud-open { pointer-events: auto; } - #id_kit_btn { - opacity: 0; - pointer-events: none; + @media (orientation: portrait) { + #id_kit_btn { + opacity: 0; + pointer-events: none; + } } } diff --git a/src/static_src/scss/_burger.scss b/src/static_src/scss/_burger.scss index 02378ca..a07a9bf 100644 --- a/src/static_src/scss/_burger.scss +++ b/src/static_src/scss/_burger.scss @@ -22,7 +22,10 @@ position: fixed; bottom: 4.2rem; left: 0.5rem; - z-index: 318; + // Match .gear-btn page-level z (314) — keeps burger BELOW kit_btn + + // bud_btn (both z-318) + bud_panel (z-317) so those naturally cover + // burger when they overlap. Dialog (z-316) covers burger too. + z-index: 314; font-size: 1.75rem; cursor: pointer; color: rgba(var(--secUser), 1); @@ -60,7 +63,7 @@ left: 2rem; // burger left (0.5) + half-btn (1.5) width: 0; height: 0; - z-index: 317; // below #id_burger_btn so the burger stays clickable + z-index: 313; // below #id_burger_btn (314) so burger stays clickable pointer-events: none; @media (orientation: landscape) { @@ -119,3 +122,9 @@ opacity: 1; pointer-events: auto; } + +// No explicit bud_panel / kit_dialog hide rules needed — burger's z=314 +// sits BELOW kit_btn / bud_btn (318), bud_panel (317), + kit_dialog (316), +// so all those naturally cover the burger when they overlap. Earlier +// iterations explicitly faded the burger via opacity; rendered redundant +// by the z-index drop. diff --git a/src/static_src/scss/_game-kit.scss b/src/static_src/scss/_game-kit.scss index 538faf2..1e7679c 100644 --- a/src/static_src/scss/_game-kit.scss +++ b/src/static_src/scss/_game-kit.scss @@ -48,10 +48,10 @@ border: none; border-top: 0.1rem solid rgba(var(--quaUser), 1); background: rgba(var(--priUser), 0.97); - // Above the burger btn (z-318) so the dialog covers it when open in - // BOTH orientations — burger sits in the bottom-left corner in - // portrait + the right sidebar in landscape; the dialog lands atop. - z-index: 319; + // Below kit_btn + burger_btn (both z-318) so those btns stay visible + // when the dialog opens — user can re-click kit_btn to close + the + // burger fan stays accessible alongside an open dialog. + z-index: 316; overflow: hidden; // Closed state — portrait: grow from below the footer (max-height) @@ -87,7 +87,7 @@ // Opaque in landscape — the dialog covers the sidebar btns + the // burger + bud area when open; transparency would let those btns // bleed through the dialog content. - background: rgba(var(--priUser), 1); + background: rgba(var(--priUser), 0.97); // Closed state — width animates from 0 (grows leftward from right) max-width: 0;