My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD
User-driven bug-squash + UX-polish cycle on top of iter 4a (ca2a62f). All 14 fixes ship behind the same iter-4a banner since they close the substage's UX gaps without expanding scope to iter 4b's persistence layer.
SeaDeal modal port — extracted apps/gameboard/_partials/_sea_stage.html shared by gameroom + my-sea; aliased .my-sea-picker w. id=id_sea_overlay so SeaDeal.init() finds it; FLIP click → SeaDeal.openStage delegation instead of bare _fillSlot. Fixes the user-reported 'thumbnail disappears' bug — slot was landing at opacity 0 (.--filled w.o .--visible) because SeaDeal's _hideStage (which adds --visible on modal dismiss) was never running. 3 new FTs cover the modal flow.
Spread lock + DEL reshuffle — _lockSpread/_unlockSpread toggle .sea-select--locked class on the combobox; first deposit locks, _resetHand unlocks. _reshuffleDeck Fisher-Yates over combined piles + re-rolls 25% reversal axis on DEL so successive DELs don't re-deal the same hand. Verified Claudezilla: 3 DEL cycles produced distinct lay cards (150 → 114 → 155).
Cover/cross empty slots — subtle dotted outline (transparent bg + 0.25 alpha border) w. --duoUser mask reveal on hover/touch. Per the user spec; rule lives in _card-deck.scss (shared between gameroom + my-sea). Plus matching label-opacity (0.25 idle → 0.6 hover) via CSS :hover ancestor propagation.
DOS spec — Solution moved from cover → crown per user correction. DRAW_ORDER ['loom', 'cross', 'crown']; POSITION_LABELS {loom: Desire, cross: Obstacle, crown: Solution}; SCSS hide list flipped from [leave, crown, lay] → [leave, cover, lay]; FT/IT assertions updated.
SAO → DOS soft-reload bug — Firefox autofill on hidden input restored the previous-session DOS value, tripping combobox.js's change-event guard. Fix: autocomplete=off + force-sync hidden.value from server-rendered aria-selected option in init. Captured as feedback_firefox_autofill_hidden_inputs (generalizable trap).
.sea-pos-label outside .sea-card-slot — moved label to be a sibling of the slot in the cell, so SeaDeal innerHTML clobber on draw doesn't erase it. Per-position absolute positioning touching slot borders: crown/cover above (translate -50%, 0.1rem, scaleY 1.2); lay/cross below (translate -50%, -0.1rem, scaleY 1.2); leave left, CCW (writing-mode vertical-rl + rotate 180deg + scaleX 1.2); loom right, CW (writing-mode vertical-rl + scaleX 1.2). scaleX for rotated labels (not scaleY) — perpendicular to text-flow is the visible-width direction after rotation. .my-sea-cross gap bumped to 1.75rem for label clearance.
Escape Velocity label swaps — POSITION_LABELS for escape-velocity: {crown: Crown, leave: Lay, cover: Cover, cross: Cross, loom: Loom, lay: Leave}. Replaces the Waite-Smith Behind/Beneath/Before per user spec.
SPREAD dropdown portal — .my-sea-form-col .sea-form-main { overflow: visible } + .sea-select-list { z-index: 1000 } so the dropdown extends past the form-main scroll area + sits above the picker stacking ints. Gameroom .sea-form-main still scrolls (only my-sea opts out).
Major Arcana polarity-split rendering — added 9 missing _card_dict keys to my-sea's _my_sea_deck_data to match gameroom epic.views.sea_deck's contract: levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word, keywords_upright, keywords_reversed, energies, operations. Without these StageCard.populateCard falls through to plain name_title for trumps 19-21 + cards 48-49. Iter 4b cleanup candidate: extract apps.epic.utils.card_dict() to DRY the now-identical helpers.
Tests deferred — user explicitly belayed FT runs during the bug-fix substage. Iter 4b will re-establish a green sweep before its commit lands.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -533,7 +533,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
||||
"past-present-future": {"leave", "cover", "loom"},
|
||||
"situation-action-outcome": {"lay", "cover", "crown"},
|
||||
"mind-body-spirit": {"crown", "lay", "loom"},
|
||||
"desire-obstacle-solution": {"loom", "cross", "cover"},
|
||||
"desire-obstacle-solution": {"loom", "cross", "crown"},
|
||||
"waite-smith": ALL_POSITIONS,
|
||||
"escape-velocity": ALL_POSITIONS,
|
||||
}
|
||||
@@ -583,7 +583,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
||||
"situation-action-outcome": {"lay": "Situation", "cover": "Action", "crown": "Outcome"},
|
||||
"past-present-future": {"leave": "Past", "cover": "Present", "loom": "Future"},
|
||||
"mind-body-spirit": {"crown": "Mind", "lay": "Body", "loom": "Spirit"},
|
||||
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","cover":"Solution"},
|
||||
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","crown":"Solution"},
|
||||
"waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover",
|
||||
"cross": "Cross", "loom": "Before", "lay": "Behind"},
|
||||
}
|
||||
@@ -670,9 +670,10 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
)
|
||||
)
|
||||
|
||||
def _draw_one(self, picker, polarity):
|
||||
"""Click a polarity swatch + the FLIP btn that appears →
|
||||
deposits a card. `polarity` is `'levity'` or `'gravity'`."""
|
||||
def _draw_open_modal(self, picker, polarity):
|
||||
"""Click a polarity swatch + the FLIP btn that appears → opens
|
||||
the SeaDeal stage modal. Returns the stage element so callers
|
||||
can assert on it before dismissing."""
|
||||
stack = picker.find_element(
|
||||
By.CSS_SELECTOR, f".sea-deck-stack--{polarity}"
|
||||
)
|
||||
@@ -680,9 +681,45 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
flip = self.wait_for(
|
||||
lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-ok")
|
||||
)
|
||||
# FLIP btn becomes visible after the stack click; wait for it.
|
||||
self.wait_for(lambda: self.assertTrue(flip.is_displayed()))
|
||||
flip.click()
|
||||
# SeaDeal.openStage shows #id_sea_stage. Wait for the modal.
|
||||
return self.wait_for(
|
||||
lambda: self._stage_visible()
|
||||
)
|
||||
|
||||
def _stage_visible(self):
|
||||
stage = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage")
|
||||
if not stage.is_displayed():
|
||||
raise AssertionError("sea-stage not visible after FLIP click")
|
||||
return stage
|
||||
|
||||
def _dismiss_modal(self):
|
||||
"""Click the stage backdrop → SeaDeal._hideStage → modal hides +
|
||||
slot gains `.--visible` (thumbnail fades in).
|
||||
|
||||
Uses `execute_script` to dispatch the click rather than a native
|
||||
Selenium `.click()` — `.sea-stage-content` overlays the backdrop
|
||||
visually (centered card + stat block), so Selenium reports
|
||||
ElementClickInterceptedException for a direct click. This is
|
||||
the documented Selenium-limitation exception per the TDD skill;
|
||||
the actual backdrop-click → close behaviour is Jasmine-tested
|
||||
in [[SeaDealSpec.js]] / "Backdrop click closes the stage"."""
|
||||
self.browser.execute_script(
|
||||
"document.querySelector('#id_sea_stage .sea-stage-backdrop').click();"
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
def _draw_one(self, picker, polarity):
|
||||
"""Full single-draw cycle: open modal + dismiss it. Used by FTs
|
||||
that need to deposit multiple cards in sequence (the stage
|
||||
backdrop blocks subsequent deck-stack clicks)."""
|
||||
self._draw_open_modal(picker, polarity)
|
||||
self._dismiss_modal()
|
||||
|
||||
# ── Test 1 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -890,3 +927,71 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
hint = picker.find_element(By.CSS_SELECTOR, ".sea-reversal-hint")
|
||||
self.assertIn("25", hint.text)
|
||||
self.assertIn("reversal", hint.text.lower())
|
||||
|
||||
# ── Test (modal bug fix) ────────────────────────────────────────────────
|
||||
|
||||
def test_flip_click_opens_portaled_stage_modal(self):
|
||||
"""Bug fix (2026-05-19): the user-reported missing modal. After
|
||||
clicking the deck stack + the FLIP btn that appears, SeaDeal.
|
||||
openStage should fire — showing `#id_sea_stage` (position-fixed
|
||||
full-viewport portal) above everything else. Before the fix the
|
||||
slot got filled directly at opacity 0 → 'thumbnail summarily
|
||||
disappears'. Now: modal opens; slot stays at `--filled` but
|
||||
`--visible` is NOT added yet (waits for backdrop dismiss)."""
|
||||
picker = self._enter_picker_phase()
|
||||
stage = self._draw_open_modal(picker, "levity")
|
||||
# Stage card carries the drawn card's data — non-empty corner rank.
|
||||
rank = stage.find_element(
|
||||
By.CSS_SELECTOR, ".sea-stage-card .fan-card-corner--tl .fan-corner-rank"
|
||||
)
|
||||
self.assertTrue(rank.text.strip(), "stage card should display the drawn card's corner rank")
|
||||
# Slot in the cross is in `.--filled` state but the thumbnail is
|
||||
# invisible until the modal dismisses (the bug we're guarding).
|
||||
slot = picker.find_element(
|
||||
By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled"
|
||||
)
|
||||
self.assertNotIn(
|
||||
"sea-card-slot--visible", slot.get_attribute("class"),
|
||||
"slot should still be in pre-reveal opacity-0 state while modal is open",
|
||||
)
|
||||
|
||||
# ── Test (modal bug fix, dismiss reveal) ───────────────────────────────
|
||||
|
||||
def test_backdrop_click_dismisses_modal_and_reveals_thumbnail(self):
|
||||
"""Bug fix part 2: clicking the `.sea-stage-backdrop` closes the
|
||||
modal AND adds `.sea-card-slot--visible` to the deposited slot,
|
||||
making the thumbnail fade in. Confirms the user-reported 'card
|
||||
appears where the slot was' behavior post-dismiss."""
|
||||
picker = self._enter_picker_phase()
|
||||
self._draw_open_modal(picker, "levity")
|
||||
self._dismiss_modal()
|
||||
slot = picker.find_element(
|
||||
By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled"
|
||||
)
|
||||
self.assertIn(
|
||||
"sea-card-slot--visible", slot.get_attribute("class"),
|
||||
"post-dismiss, the slot should fade in via `.--visible`",
|
||||
)
|
||||
|
||||
# ── Test (modal bug fix, stat block populates) ─────────────────────────
|
||||
|
||||
def test_modal_stage_renders_stat_block_dom_contract(self):
|
||||
"""SeaDeal._populate populates the stat-block keyword `<ul>`s
|
||||
via `#id_sea_stat_upright` / `#id_sea_stat_reversed`. The DOM
|
||||
contract — these IDs exist inside the stage — is what this FT
|
||||
pins; the actual stat content (keyword text, qualifier render)
|
||||
is exercised by [[SeaDealSpec.js]]. Earthman seed cards in the
|
||||
iter-4a FT pile carry empty keyword arrays so we can't assert
|
||||
text content here without enriching the seed."""
|
||||
picker = self._enter_picker_phase()
|
||||
self._draw_open_modal(picker, "levity")
|
||||
# Stat-block UL elements exist inside the visible stage.
|
||||
upright = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_upright")
|
||||
reversed_ul = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_reversed")
|
||||
self.assertIsNotNone(upright)
|
||||
self.assertIsNotNone(reversed_ul)
|
||||
# The sea stat block is inside the visible stage modal.
|
||||
stat_block = self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_sea_stage .sea-stat-block"
|
||||
)
|
||||
self.assertIsNotNone(stat_block)
|
||||
|
||||
Reference in New Issue
Block a user