From bd9155c13b03cf8c0d5566e9171f64ee26a44ee3 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 8 Jun 2026 11:19:34 -0400 Subject: [PATCH] =?UTF-8?q?my=20sea=20preview:=20namespace=20cells=20`.sea?= =?UTF-8?q?-prev-pos-*`=20to=20de-collide=20from=20the=20live=20cross;=20h?= =?UTF-8?q?arden=20spread=20dropdown=20+=20CI=20retry=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename the spread-preview's `.sea-pos-*` cells to `.sea-prev-pos-*` (template + aliased Celtic-cross geometry under `.sea-cross--preview` in _gameboard.scss + the SeaDealSpec fixture) so a bare `.sea-pos-*` matches ONLY the live `.my-sea-cross`; fixes test_picker_renders_sao_default_position_subset (was 2≠1 — the preview duplicated every cross cell) - spread dropdown: cap `.sea-select-list` w. max-height + overflow-y:auto, & JS-click the option in the `_pick` FT helper, so the last option (escape-velocity) can't land below the un-scrollable picker-modal fold in landscape (ElementNotInteractable, seen in CI build 373) - _retry_failed.sh: anchor the FAIL/ERROR label sed to the FIRST paren group so parameterized subTest failures retry the real method label, not a bogus `position='…'` module (ModuleNotFoundError) [[project-my-sea-roadmap]] [[feedback-collectstatic-before-ft]] Co-Authored-By: Claude Opus 4.8 (1M context) --- .woodpecker/_retry_failed.sh | 9 ++-- src/functional_tests/test_game_my_sea.py | 6 ++- src/static/tests/SeaDealSpec.js | 13 +++--- src/static_src/scss/_card-deck.scss | 7 +++ src/static_src/scss/_gameboard.scss | 43 +++++++++++++++++-- src/static_src/tests/SeaDealSpec.js | 13 +++--- .../_partials/_sea_spread_preview.html | 19 +++++--- 7 files changed, 83 insertions(+), 27 deletions(-) diff --git a/.woodpecker/_retry_failed.sh b/.woodpecker/_retry_failed.sh index c357ffd..92b0abe 100644 --- a/.woodpecker/_retry_failed.sh +++ b/.woodpecker/_retry_failed.sh @@ -41,10 +41,13 @@ if [ "$FIRST" -eq 0 ]; then fi # Parse failure labels. Match both FAIL: and ERROR: lines; the dotted -# path lives inside the trailing parens. `sort -u` dedupes if a single -# test produces multiple lines (rare but possible). +# path lives inside the FIRST parens. Parameterized subTest failures +# append a SECOND `(param='...')` group, so anchor to the first group — +# a greedy `^.*\(` would wrongly grab the subtest param and feed +# `manage.py test` a bogus label (ModuleNotFoundError). `sort -u` +# dedupes the repeated method label the subtests share. FAILED=$(grep -E '^(FAIL|ERROR): ' "$LOG" \ - | sed -E 's/^.*\(([^)]+)\)[^()]*$/\1/' \ + | sed -E 's/^[^(]*\(([^)]+)\).*/\1/' \ | sort -u \ | tr '\n' ' ') diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py index 6951b91..0e55710 100644 --- a/src/functional_tests/test_game_my_sea.py +++ b/src/functional_tests/test_game_my_sea.py @@ -623,7 +623,11 @@ class MySeaSpreadFormTest(FunctionalTest): By.CSS_SELECTOR, f".sea-select-list [role='option'][data-value='{value}']", ) - opt.click() + # JS-click (matches the _open_spread_modal idiom): the dropdown + # opens downward in landscape inside the centered, un-scrollable + # picker modal, so the LAST option can land below the fold where a + # native .click()'s auto-scroll fails (ElementNotInteractable). + self.browser.execute_script("arguments[0].click()", opt) for value, expected_visible in SPREAD_POSITIONS.items(): with self.subTest(spread=value): diff --git a/src/static/tests/SeaDealSpec.js b/src/static/tests/SeaDealSpec.js index 7300b03..359c3e0 100644 --- a/src/static/tests/SeaDealSpec.js +++ b/src/static/tests/SeaDealSpec.js @@ -381,9 +381,10 @@ describe("SeaDeal", () => { // ── Spread-preview scoping ──────────────────────────────────────────────── // // The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the // same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — the - // preview slots stay empty (user-spec 2026-06-07). Guards the trap where a - // bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's - // slot. (Gameroom Sea Select felt rebuild.) + // preview slots stay empty (user-spec 2026-06-07). The shipped preview now + // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs + // it; SeaDeal also scopes via `.sea-cross:not(.sea-cross--preview)`, which + // this spec locks. (Gameroom Sea Select felt rebuild.) describe("preview cross is never dealt to", () => { beforeEach(() => { makeFixture(); @@ -391,7 +392,7 @@ describe("SeaDeal", () => { const preview = document.createElement("div"); preview.className = "sea-cross sea-cross--preview"; preview.innerHTML = - '
' + + '
' + '
'; overlay.appendChild(preview); SeaDeal._testInit(); // re-capture _cross with the preview present @@ -402,7 +403,7 @@ describe("SeaDeal", () => { const realSlot = overlay.querySelector( ".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot"); const previewSlot = overlay.querySelector( - ".sea-cross--preview .sea-pos-cover .sea-card-slot"); + ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot"); expect(realSlot.classList.contains("sea-card-slot--filled")).toBe(true); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false); expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true); @@ -411,7 +412,7 @@ describe("SeaDeal", () => { it("register() (AUTO DRAW path) also skips the preview slot", () => { SeaDeal.register(CARD, ".sea-pos-cover", true); const previewSlot = overlay.querySelector( - ".sea-cross--preview .sea-pos-cover .sea-card-slot"); + ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot"); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false); }); }); diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 30e910a..f09e111 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -1972,6 +1972,13 @@ $_sea-card-glow: 0 0 0.5rem 0.5rem rgba(var(--ninUser), 0.3), 0 0 0.4rem rgba(0, border-radius: 0.3rem; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); + // Bound the dropdown height + give it its own scroll so a long list + // (or a low trigger position in landscape) can never run off the + // bottom of the un-scrollable picker modal — the last option stays + // reachable instead of landing below the fold. + max-height: 16rem; + overflow-y: auto; + li[role="option"] { padding: 0.4rem 0.5rem; border-radius: 0.2rem; diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index dda09b1..410d577 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -870,19 +870,54 @@ body.page-gameboard { .sea-card-slot--crossing { width: 1.6rem; height: 2.6rem; } .sea-sig-card { transform: scale(0.85); } + // The preview cells are namespaced `.sea-prev-pos-*` (renamed from + // `.sea-pos-*`) so a bare `.sea-pos-*` selector matches ONLY the live + // `.my-sea-cross` cells, not this preview. The live cross draws its + // Celtic-Cross geometry from bare `.sea-pos-*` rules in _card-deck.scss; + // the preview needs its own scoped copies of that geometry below. + .sea-prev-pos-crown { grid-area: crown; } + .sea-prev-pos-leave { grid-area: leave; } + .sea-prev-pos-core { grid-area: core; position: relative; } + .sea-prev-pos-loom { grid-area: loom; } + .sea-prev-pos-lay { grid-area: lay; } + + // Cover + Cross absolutely overlaid on the center Sig card (mirrors + // _card-deck.scss's `.sea-pos-cover/.sea-pos-cross` overlay rules). + .sea-prev-pos-cover, + .sea-prev-pos-cross { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + } + .sea-prev-pos-cover { z-index: 3; } + .sea-prev-pos-cross { z-index: 4; } + .sea-prev-pos-cross .sea-card-slot { transform: rotate(90deg); } + + // Empty Cover/Cross slots stay transparent + dotted so the Sig card + // reads through (matches the live cross's empty-overlay styling). + .sea-prev-pos-cover .sea-card-slot--empty, + .sea-prev-pos-cross .sea-card-slot--empty { + background-color: transparent; + border-color: rgba(var(--terUser), 0.25); + box-shadow: none; + } + // Per-spread position subsets — mirror the .my-sea-cross hide rules so the // preview shape matches the live cross for the 3-card spreads. &[data-spread="past-present-future"] { - .sea-pos-crown, .sea-pos-cross, .sea-pos-lay { display: none; } + .sea-prev-pos-crown, .sea-prev-pos-cross, .sea-prev-pos-lay { display: none; } } &[data-spread="situation-action-outcome"] { - .sea-pos-leave, .sea-pos-loom, .sea-pos-cross { display: none; } + .sea-prev-pos-leave, .sea-prev-pos-loom, .sea-prev-pos-cross { display: none; } } &[data-spread="mind-body-spirit"] { - .sea-pos-leave, .sea-pos-cover, .sea-pos-cross { display: none; } + .sea-prev-pos-leave, .sea-prev-pos-cover, .sea-prev-pos-cross { display: none; } } &[data-spread="desire-obstacle-solution"] { - .sea-pos-leave, .sea-pos-cover, .sea-pos-lay { display: none; } + .sea-prev-pos-leave, .sea-prev-pos-cover, .sea-prev-pos-lay { display: none; } } // Celtic Cross variants (waite-smith / escape-velocity) — all 6 visible. } diff --git a/src/static_src/tests/SeaDealSpec.js b/src/static_src/tests/SeaDealSpec.js index 7300b03..359c3e0 100644 --- a/src/static_src/tests/SeaDealSpec.js +++ b/src/static_src/tests/SeaDealSpec.js @@ -381,9 +381,10 @@ describe("SeaDeal", () => { // ── Spread-preview scoping ──────────────────────────────────────────────── // // The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the // same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — the - // preview slots stay empty (user-spec 2026-06-07). Guards the trap where a - // bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's - // slot. (Gameroom Sea Select felt rebuild.) + // preview slots stay empty (user-spec 2026-06-07). The shipped preview now + // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs + // it; SeaDeal also scopes via `.sea-cross:not(.sea-cross--preview)`, which + // this spec locks. (Gameroom Sea Select felt rebuild.) describe("preview cross is never dealt to", () => { beforeEach(() => { makeFixture(); @@ -391,7 +392,7 @@ describe("SeaDeal", () => { const preview = document.createElement("div"); preview.className = "sea-cross sea-cross--preview"; preview.innerHTML = - '
' + + '
' + '
'; overlay.appendChild(preview); SeaDeal._testInit(); // re-capture _cross with the preview present @@ -402,7 +403,7 @@ describe("SeaDeal", () => { const realSlot = overlay.querySelector( ".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot"); const previewSlot = overlay.querySelector( - ".sea-cross--preview .sea-pos-cover .sea-card-slot"); + ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot"); expect(realSlot.classList.contains("sea-card-slot--filled")).toBe(true); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false); expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true); @@ -411,7 +412,7 @@ describe("SeaDeal", () => { it("register() (AUTO DRAW path) also skips the preview slot", () => { SeaDeal.register(CARD, ".sea-pos-cover", true); const previewSlot = overlay.querySelector( - ".sea-cross--preview .sea-pos-cover .sea-card-slot"); + ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot"); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false); }); }); diff --git a/src/templates/apps/gameboard/_partials/_sea_spread_preview.html b/src/templates/apps/gameboard/_partials/_sea_spread_preview.html index c39f3fd..d2ff876 100644 --- a/src/templates/apps/gameboard/_partials/_sea_spread_preview.html +++ b/src/templates/apps/gameboard/_partials/_sea_spread_preview.html @@ -7,6 +7,11 @@ NEVER receive dealt cards (SeaDeal scopes its slot queries to shape still reads. Shared by the gameroom Sea Select modal AND my_sea's spread modal so the two locations are identical. +The cell classes are namespaced `.sea-prev-pos-*` (NOT `.sea-pos-*`) so a bare +`.sea-pos-*` selector matches ONLY the live `.my-sea-cross` cells, never this +preview. The preview's Celtic-Cross geometry is supplied by scoped copies under +`.sea-cross--preview` in _gameboard.scss. + Args: preview_spread — slug to seed `data-spread` (drives the per-spread show/hide). sig — the significator card for the center pip (corner_rank + @@ -14,27 +19,27 @@ Args: {% endcomment %}
-
+
-
+
-
+
{% if sig %}{{ sig.corner_rank }}{% if sig.suit_icon %}{% endif %}{% endif %}
-
+
-
+
-
+
-
+