my sea preview: namespace cells .sea-prev-pos-* to de-collide from the live cross; harden spread dropdown + CI retry — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

- 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) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-08 11:19:34 -04:00
parent a0ded7f09b
commit bd9155c13b
7 changed files with 83 additions and 27 deletions

View File

@@ -41,10 +41,13 @@ if [ "$FIRST" -eq 0 ]; then
fi fi
# Parse failure labels. Match both FAIL: and ERROR: lines; the dotted # Parse failure labels. Match both FAIL: and ERROR: lines; the dotted
# path lives inside the trailing parens. `sort -u` dedupes if a single # path lives inside the FIRST parens. Parameterized subTest failures
# test produces multiple lines (rare but possible). # 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" \ FAILED=$(grep -E '^(FAIL|ERROR): ' "$LOG" \
| sed -E 's/^.*\(([^)]+)\)[^()]*$/\1/' \ | sed -E 's/^[^(]*\(([^)]+)\).*/\1/' \
| sort -u \ | sort -u \
| tr '\n' ' ') | tr '\n' ' ')

View File

@@ -623,7 +623,11 @@ class MySeaSpreadFormTest(FunctionalTest):
By.CSS_SELECTOR, By.CSS_SELECTOR,
f".sea-select-list [role='option'][data-value='{value}']", 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(): for value, expected_visible in SPREAD_POSITIONS.items():
with self.subTest(spread=value): with self.subTest(spread=value):

View File

@@ -381,9 +381,10 @@ describe("SeaDeal", () => {
// ── Spread-preview scoping ──────────────────────────────────────────────── // // ── Spread-preview scoping ──────────────────────────────────────────────── //
// The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the // The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the
// same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — 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 // preview slots stay empty (user-spec 2026-06-07). The shipped preview now
// bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs
// slot. (Gameroom Sea Select felt rebuild.) // 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", () => { describe("preview cross is never dealt to", () => {
beforeEach(() => { beforeEach(() => {
makeFixture(); makeFixture();
@@ -391,7 +392,7 @@ describe("SeaDeal", () => {
const preview = document.createElement("div"); const preview = document.createElement("div");
preview.className = "sea-cross sea-cross--preview"; preview.className = "sea-cross sea-cross--preview";
preview.innerHTML = preview.innerHTML =
'<div class="sea-crucifix-cell sea-pos-cover">' + '<div class="sea-crucifix-cell sea-prev-pos-cover">' +
'<div class="sea-card-slot sea-card-slot--empty"></div></div>'; '<div class="sea-card-slot sea-card-slot--empty"></div></div>';
overlay.appendChild(preview); overlay.appendChild(preview);
SeaDeal._testInit(); // re-capture _cross with the preview present SeaDeal._testInit(); // re-capture _cross with the preview present
@@ -402,7 +403,7 @@ describe("SeaDeal", () => {
const realSlot = overlay.querySelector( const realSlot = overlay.querySelector(
".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot"); ".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot");
const previewSlot = overlay.querySelector( 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(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--filled")).toBe(false);
expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true); 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", () => { it("register() (AUTO DRAW path) also skips the preview slot", () => {
SeaDeal.register(CARD, ".sea-pos-cover", true); SeaDeal.register(CARD, ".sea-pos-cover", true);
const previewSlot = overlay.querySelector( 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); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
}); });
}); });

View File

@@ -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; border-radius: 0.3rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); 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"] { li[role="option"] {
padding: 0.4rem 0.5rem; padding: 0.4rem 0.5rem;
border-radius: 0.2rem; border-radius: 0.2rem;

View File

@@ -870,19 +870,54 @@ body.page-gameboard {
.sea-card-slot--crossing { width: 1.6rem; height: 2.6rem; } .sea-card-slot--crossing { width: 1.6rem; height: 2.6rem; }
.sea-sig-card { transform: scale(0.85); } .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 // Per-spread position subsets — mirror the .my-sea-cross hide rules so the
// preview shape matches the live cross for the 3-card spreads. // preview shape matches the live cross for the 3-card spreads.
&[data-spread="past-present-future"] { &[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"] { &[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"] { &[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"] { &[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. // Celtic Cross variants (waite-smith / escape-velocity) — all 6 visible.
} }

View File

@@ -381,9 +381,10 @@ describe("SeaDeal", () => {
// ── Spread-preview scoping ──────────────────────────────────────────────── // // ── Spread-preview scoping ──────────────────────────────────────────────── //
// The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the // The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the
// same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — 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 // preview slots stay empty (user-spec 2026-06-07). The shipped preview now
// bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs
// slot. (Gameroom Sea Select felt rebuild.) // 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", () => { describe("preview cross is never dealt to", () => {
beforeEach(() => { beforeEach(() => {
makeFixture(); makeFixture();
@@ -391,7 +392,7 @@ describe("SeaDeal", () => {
const preview = document.createElement("div"); const preview = document.createElement("div");
preview.className = "sea-cross sea-cross--preview"; preview.className = "sea-cross sea-cross--preview";
preview.innerHTML = preview.innerHTML =
'<div class="sea-crucifix-cell sea-pos-cover">' + '<div class="sea-crucifix-cell sea-prev-pos-cover">' +
'<div class="sea-card-slot sea-card-slot--empty"></div></div>'; '<div class="sea-card-slot sea-card-slot--empty"></div></div>';
overlay.appendChild(preview); overlay.appendChild(preview);
SeaDeal._testInit(); // re-capture _cross with the preview present SeaDeal._testInit(); // re-capture _cross with the preview present
@@ -402,7 +403,7 @@ describe("SeaDeal", () => {
const realSlot = overlay.querySelector( const realSlot = overlay.querySelector(
".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot"); ".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot");
const previewSlot = overlay.querySelector( 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(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--filled")).toBe(false);
expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true); 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", () => { it("register() (AUTO DRAW path) also skips the preview slot", () => {
SeaDeal.register(CARD, ".sea-pos-cover", true); SeaDeal.register(CARD, ".sea-pos-cover", true);
const previewSlot = overlay.querySelector( 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); expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
}); });
}); });

View File

@@ -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 shape still reads. Shared by the gameroom Sea Select modal AND my_sea's spread
modal so the two locations are identical. 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: Args:
preview_spread — slug to seed `data-spread` (drives the per-spread show/hide). preview_spread — slug to seed `data-spread` (drives the per-spread show/hide).
sig — the significator card for the center pip (corner_rank + sig — the significator card for the center pip (corner_rank +
@@ -14,27 +19,27 @@ Args:
{% endcomment %} {% endcomment %}
<div class="sea-cards-col sea-cards-col--preview"> <div class="sea-cards-col sea-cards-col--preview">
<div class="sea-cross sea-cross--preview" data-spread="{{ preview_spread }}"> <div class="sea-cross sea-cross--preview" data-spread="{{ preview_spread }}">
<div class="sea-crucifix-cell sea-pos-crown"> <div class="sea-crucifix-cell sea-prev-pos-crown">
<div class="sea-card-slot sea-card-slot--empty"></div> <div class="sea-card-slot sea-card-slot--empty"></div>
</div> </div>
<div class="sea-crucifix-cell sea-pos-leave"> <div class="sea-crucifix-cell sea-prev-pos-leave">
<div class="sea-card-slot sea-card-slot--empty"></div> <div class="sea-card-slot sea-card-slot--empty"></div>
</div> </div>
<div class="sea-crucifix-cell sea-pos-core"> <div class="sea-crucifix-cell sea-prev-pos-core">
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 2rem"> <div class="sig-stage-card sea-sig-card" style="--sig-card-w: 2rem">
{% if sig %}<span class="fan-corner-rank">{{ sig.corner_rank }}</span>{% if sig.suit_icon %}<i class="fa-solid {{ sig.suit_icon }}"></i>{% endif %}{% endif %} {% if sig %}<span class="fan-corner-rank">{{ sig.corner_rank }}</span>{% if sig.suit_icon %}<i class="fa-solid {{ sig.suit_icon }}"></i>{% endif %}{% endif %}
</div> </div>
<div class="sea-pos-cover"> <div class="sea-prev-pos-cover">
<div class="sea-card-slot sea-card-slot--empty"></div> <div class="sea-card-slot sea-card-slot--empty"></div>
</div> </div>
<div class="sea-pos-cross"> <div class="sea-prev-pos-cross">
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div> <div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
</div> </div>
</div> </div>
<div class="sea-crucifix-cell sea-pos-loom"> <div class="sea-crucifix-cell sea-prev-pos-loom">
<div class="sea-card-slot sea-card-slot--empty"></div> <div class="sea-card-slot sea-card-slot--empty"></div>
</div> </div>
<div class="sea-crucifix-cell sea-pos-lay"> <div class="sea-crucifix-cell sea-prev-pos-lay">
<div class="sea-card-slot sea-card-slot--empty"></div> <div class="sea-card-slot sea-card-slot--empty"></div>
</div> </div>
</div> </div>