my sea preview: namespace cells .sea-prev-pos-* to de-collide from the live cross; harden spread dropdown + CI retry — TDD
- 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:
@@ -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' ' ')
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 =
|
||||
'<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>';
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
'<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>';
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 %}
|
||||
<div class="sea-cards-col sea-cards-col--preview">
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
{% 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 class="sea-pos-cover">
|
||||
<div class="sea-prev-pos-cover">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user