My Sea form col + SPREAD dropdown w. 3-card/6-card section dividers — Sprint 5 iter 3 of My Sea roadmap — TDD
Picker phase form col: SPREAD combobox w. 6 spread options under 2 horizontal section dividers ("3-card spreads" / "6-card spreads"), reversal-% caption, GRAVITY + LEVITY deck swatches, LOCK HAND + DEL btns. Default = Situation, Action, Outcome (a 3-card spread). Selecting a 6-card spread (Celtic Cross Waite-Smith or Escape Velocity) swaps `.my-sea-cross[data-spread-shape]` from `three-card` to `six-card` — revealing the crown / lay / cross cells that the default 3-card variants hide.
Naming correction (user-locked): the spread itself is a "three-card spread" not a "three-card cross" — "cross" stays scoped to the Celtic Cross variants (6-card spreads). CSS class `.my-sea-cross` carries grid-container semantics regardless of which spread shape is active; the spread-vs-cross distinction lives at the spread-name layer only.
- **View** (gameboard/views.py): `my_sea` adds `default_spread = "situation-action-outcome"` + `reversals_pct = 25` context keys.
- **Template** (my_sea.html): renders all 7 cross cells (crown/leave/core+cover+cross/loom/lay) unconditionally + adds `data-spread-shape="three-card"` to `.my-sea-cross`. Form col DRY-reuses gameroom `_sea_overlay.html`'s `.sea-form-col` shape — `.sea-form-main` w. `.sea-field` (SPREAD label + reversal hint + custom combobox) + `.sea-stacks` (GRAVITY + LEVITY swatches) + `.sea-form-actions` (LOCK HAND + DEL). 6 options + 2 dividers in the combobox `<ul>`; dividers are `role="presentation"` so `combobox.js` skips them naturally. Inline IIFE listens for the hidden `<input id="id_sea_spread">`'s `change` event + sets `.my-sea-cross`'s `data-spread-shape` based on whether the value is in `['waite-smith', 'escape-velocity']`. No new combobox.js wiring — the existing module's `change`-bubbling contract feeds straight in.
- **SCSS** (_gameboard.scss):
- `.my-sea-cross[data-spread-shape="three-card"]` — single-row `"leave core loom"` grid + `display: none` on crown/lay/cross.
- `.my-sea-cross[data-spread-shape="six-card"]` — inherits the gameroom `.sea-cross`'s 3×3 grid + reveals all cells.
- `.sea-select-divider` — section header style mirrors `.kit-bag-label`'s small-uppercase-underlined-letter-spaced --quaUser/0.75 treatment but HORIZONTAL (kit-bag uses `writing-mode: vertical-rl`; dropdown menus are flat). `pointer-events: none` belt-and-braces against accidental click/hover.
- `.my-sea-form-col` — width-constrains the form col so the picker's cross + form sit side-by-side.
**Iter-2 contract updated** (cells in DOM, hidden via CSS for 3-card default):
- FT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_hides_six_card_only_positions_by_default` — asserts the 3 cells are in the DOM but `is_displayed() == False` so iter-3's spread switch can reveal them via CSS without re-rendering.
- IT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_renders_six_card_only_positions_for_spread_switch` — assertContains the classes (server now renders them unconditionally).
**Tests**:
- 4 FTs in new `MySeaSpreadFormTest`: combobox renders 6 options + 2 dividers w. correct labels, default is Situation/Action/Outcome (hidden input value + visible current-label span + cross's data-spread-shape), picking Celtic Cross flips data-spread-shape to six-card + reveals crown/lay/cross, form col carries DECKS swatches + LOCK HAND + DEL + reversal-% caption. Combobox `<li>` options are inside `aria-expanded='false'` listbox → use `get_attribute("textContent")` not `.text` (which returns "" for Selenium-hidden elements).
- 7 ITs in new `MySeaSpreadFormTemplateTest`: default_spread + reversals_pct context keys, all 6 options + both labels render, 2 dividers render w. expected text, default option carries aria-selected="true", cross's initial data-spread-shape="three-card", form col DECKS + buttons + reversal hint render.
Tests: 32/32 FT green across test_bill_my_sign + test_game_my_sea; 1048/1048 IT/UT green in 52s.
Card-draw mechanics (clicking a deck swatch deposits a card into the next empty slot; LOCK HAND commits the draw) defer to iter 4 — this iter ships the spread-selection + layout-shape switch UI; the buttons are stubs (LOCK HAND starts disabled, DEL is a placeholder).
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:
@@ -618,13 +618,17 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
||||
self.assertContains(response, "sea-pos-leave")
|
||||
self.assertContains(response, "sea-pos-loom")
|
||||
|
||||
def test_picker_does_not_render_forsaken_positions(self):
|
||||
# Crown / lay / cross are gameroom-only — user-locked spec drops
|
||||
# them from the solo three-card spread.
|
||||
def test_picker_renders_six_card_only_positions_for_spread_switch(self):
|
||||
# Crown / lay / cross sit in the DOM unconditionally so iter 3's
|
||||
# SPREAD dropdown can reveal them via CSS attribute swap (data-
|
||||
# spread-shape="six-card") without re-rendering. Default 3-card
|
||||
# spread hides them via `.my-sea-cross[data-spread-shape=
|
||||
# "three-card"]` rules in _gameboard.scss — FT pins the hidden
|
||||
# state visually.
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertNotContains(response, "sea-pos-crown")
|
||||
self.assertNotContains(response, "sea-pos-lay")
|
||||
self.assertNotContains(response, "sea-pos-cross")
|
||||
self.assertContains(response, "sea-pos-crown")
|
||||
self.assertContains(response, "sea-pos-lay")
|
||||
self.assertContains(response, "sea-pos-cross")
|
||||
|
||||
def test_picker_not_rendered_when_user_has_no_sig(self):
|
||||
# 4b gate wins; picker has no business rendering without a sig.
|
||||
@@ -632,3 +636,75 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
||||
self.user.save(update_fields=["significator"])
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertNotContains(response, "my-sea-picker")
|
||||
|
||||
|
||||
class MySeaSpreadFormTemplateTest(TestCase):
|
||||
"""Sprint 5 iter 3 — form col + SPREAD dropdown structure + default-
|
||||
spread context + cross's `data-spread-shape` attribute. Iter 3 spec
|
||||
locks `Situation, Action, Outcome` as the default spread (a 3-card
|
||||
variant); the 6 spreads sit under 2 section dividers (3-card / 6-
|
||||
card)."""
|
||||
|
||||
SPREAD_OPTIONS = [
|
||||
("past-present-future", "Past, Present, Future"),
|
||||
("situation-action-outcome", "Situation, Action, Outcome"),
|
||||
("mind-body-spirit", "Mind, Body, Spirit"),
|
||||
("desire-obstacle-solution", "Desire, Obstacle, Solution"),
|
||||
("waite-smith", "Celtic Cross, Waite-Smith"),
|
||||
("escape-velocity", "Celtic Cross, Escape Velocity"),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
from apps.epic.models import personal_sig_cards
|
||||
self.user = User.objects.create(email="spread@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.target = personal_sig_cards(self.user)[0]
|
||||
self.user.significator = self.target
|
||||
self.user.save(update_fields=["significator"])
|
||||
|
||||
def test_context_default_spread_is_situation_action_outcome(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertEqual(
|
||||
response.context["default_spread"], "situation-action-outcome",
|
||||
)
|
||||
|
||||
def test_context_reversals_pct_defaults_to_25(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertEqual(response.context["reversals_pct"], 25)
|
||||
|
||||
def test_template_renders_all_six_spread_options(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
html = response.content.decode()
|
||||
for value, label in self.SPREAD_OPTIONS:
|
||||
with self.subTest(spread=value):
|
||||
self.assertIn(f'data-value="{value}"', html)
|
||||
self.assertIn(label, html)
|
||||
|
||||
def test_template_renders_three_card_and_six_card_section_dividers(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
html = response.content.decode()
|
||||
self.assertEqual(html.count("sea-select-divider"), 2)
|
||||
self.assertIn("3-card spreads", html)
|
||||
self.assertIn("6-card spreads", html)
|
||||
|
||||
def test_template_marks_situation_action_outcome_aria_selected(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
html = response.content.decode()
|
||||
# The default option carries aria-selected="true"; the others false.
|
||||
self.assertIn(
|
||||
'data-value="situation-action-outcome" aria-selected="true"', html,
|
||||
)
|
||||
|
||||
def test_cross_carries_initial_three_card_spread_shape(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertContains(response, 'data-spread-shape="three-card"')
|
||||
|
||||
def test_form_col_renders_decks_lock_hand_del_and_reversal_hint(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
html = response.content.decode()
|
||||
self.assertIn("sea-deck-stack--gravity", html)
|
||||
self.assertIn("sea-deck-stack--levity", html)
|
||||
self.assertIn('id="id_sea_lock_hand"', html)
|
||||
self.assertIn('id="id_sea_del"', html)
|
||||
self.assertIn("sea-reversal-hint", html)
|
||||
self.assertIn("25% reversals", html)
|
||||
|
||||
Reference in New Issue
Block a user