My Sea FREE DRAW + seat-1C seated transition — Sprint 5 iter 1 follow-up — TDD
Iter-1 follow-up after the user re-spec'd the FREE DRAW click behavior:
- **Btn label** DRAW SEA → FREE DRAW (the 1/24h free-quota draw). Element ID `id_draw_sea_btn` retained — describes intent, not label, so a future sprint can conditionally swap the label back to DRAW SEA once the daily free has been used (at which point the btn calls the room gatekeeper partial for token-deposit per [[project-my-sea-roadmap]] Sprint 6).
- **Chair seat markup** — each `.table-seat` now renders w. `.fa-chair` + `.seat-position-label` (1C-6C) + `.position-status-icon.fa-solid.fa-ban` mirroring the room's hex grammar from `_table_positions.html`. **`.seat-position-label`** (not `.seat-role-label`) because my-sea is the solo flow — no roles — and the existing room class carries role-grammar semantics that don't apply here. New class gets its own grid placement in `_gameboard.scss` (col 2 / row 1 default, col 1 for left-side seats 3/4/5 per the room's flip rule).
- **FREE DRAW click flow** — (1) seat 1C immediately gains `.seated` class & its `.fa-ban` icon swaps to `.fa-circle-check`; (2) after 800ms (so the user sees the seat animation against `_room.scss`'s 0.6s `color`/`filter` transition on `.fa-chair`), `data-phase` swaps to `picker` & landing hides. Seat 1C-only because my-sea is single-user-per-page until friend-invite lands — the user always occupies the lowest-numeral seat.
- **`.table-seat.seated` SCSS** in `_gameboard.scss` — `--terUser` chair color + `drop-shadow(--ninUser)` glow. Mirrors `_room.scss:626` `.table-seat.active .fa-chair` styling but uses a stable `.seated` class (semantically distinct: `.active` = current turn in a multi-user room, `.seated` = draw-locked occupant in the solo flow). Status icon green via the existing `_room.scss:616` `.position-status-icon.fa-circle-check` rule — no new color rule needed.
- **FT/IT updates** — renamed `test_landing_renders_hex_with_draw_sea_btn` → `…_free_draw_btn` w. "FREE DRAW" label assertions; T2 extended to assert `.position-status-icon.fa-ban` on each seat at render time; T3 (formerly `…_transitions_to_picker_phase`) rewritten as `test_free_draw_click_seats_user_in_1C_then_swaps_phase` to pin the full click contract: 1C goes `.seated` + `.fa-circle-check`, seats 2-6 unchanged, picker phase swap after the delay. New IT `test_landing_renders_position_status_ban_icon_on_each_seat` asserts initial ban + `.seat-position-label` counts.
**Substring trap caught** (worth a sticky note): bare class-name substrings (`fa-ban`, `position-status-icon`) appear ALSO in the inline JS handler's `classList.remove(…)` / `querySelector(…)` arg strings → `html.count("fa-ban") = 7`, not 6. Tightened IT assertions to match the full class attribute (`class="position-status-icon fa-solid fa-ban"`) — never count bare class names in `assertContains` / `html.count` when the same class is JS-manipulated client-side.
Tests: 25/25 FT green across test_bill_my_sign + test_game_my_sea (165s); 1037/1037 IT/UT green (49s).
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:
@@ -164,20 +164,30 @@ class MySeaSignGateTest(FunctionalTest):
|
||||
|
||||
|
||||
class MySeaDrawSeaLandingTest(FunctionalTest):
|
||||
"""Sprint 5 iter 1 — DRAW SEA landing on /gameboard/my-sea/ for a
|
||||
"""Sprint 5 iter 1 — FREE DRAW landing on /gameboard/my-sea/ for a
|
||||
user w. a saved sig (past the [[sprint-my-sea-sign-gate-may19]] gate).
|
||||
|
||||
Landing renders a DRY table hex (parameterized from the room) w. 6
|
||||
chair seats labeled 1C-6C (placeholders for the eventual friend-
|
||||
invite feature per [[project-my-sea-roadmap]] architectural anchor
|
||||
"Six chairs retained even in solo") + a central DRAW SEA `.btn-
|
||||
primary` mirroring SCAN SIGN on /billboard/my-sign/. The same Brief
|
||||
"Default deck warning" copy from my-sign fires when the user has no
|
||||
equipped deck.
|
||||
"Six chairs retained even in solo") + a central FREE DRAW `.btn-
|
||||
primary` mirroring SCAN SIGN on /billboard/my-sign/. Each chair seat
|
||||
renders w. a red `.fa-ban` status icon (empty slot).
|
||||
|
||||
Iter 1 scope: landing render + DRAW SEA click swaps `data-phase` to
|
||||
`picker` (picker UX itself lands in iter 2). Form-col (spread
|
||||
dropdown / decks / LOCK HAND / DEL) lands in iter 3."""
|
||||
Click flow: FREE DRAW → seat 1C transitions to `.seated` state
|
||||
(chair `--terUser` + drop-shadow glow + `.fa-ban` swap to `.fa-
|
||||
circle-check` green) → after a brief delay so the user sees the
|
||||
animation, `data-phase` swaps to `picker` (picker content lands in
|
||||
iter 2). The 'C' = "Chair" (user-locked vocabulary); no role
|
||||
semantics in this solo flow.
|
||||
|
||||
"FREE DRAW" is the label for the 1/24h free quota draw — a future
|
||||
sprint will conditionally swap the label to "DRAW SEA" once the
|
||||
free has been used, w. the DRAW SEA btn calling the room
|
||||
gatekeeper partial for token-deposit.
|
||||
|
||||
The same Brief "Default deck warning" copy from my-sign fires when
|
||||
the user has no equipped deck."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -191,9 +201,12 @@ class MySeaDrawSeaLandingTest(FunctionalTest):
|
||||
|
||||
# ── Test 1 ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_landing_renders_hex_with_draw_sea_btn(self):
|
||||
def test_landing_renders_hex_with_free_draw_btn(self):
|
||||
"""User w. sig → /gameboard/my-sea/ shows the DRY table hex (re-
|
||||
used from my-sign / the room shell) w. a central DRAW SEA btn."""
|
||||
used from my-sign / the room shell) w. a central FREE DRAW btn.
|
||||
Element ID `id_draw_sea_btn` describes intent (the draw entry
|
||||
point) — a future sprint will conditionally swap the label to
|
||||
DRAW SEA once the daily free has been used."""
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||
# data-phase=landing on the page wrapper
|
||||
@@ -202,11 +215,11 @@ class MySeaDrawSeaLandingTest(FunctionalTest):
|
||||
)
|
||||
# Hex shell present
|
||||
page.find_element(By.CSS_SELECTOR, ".room-shell .table-hex")
|
||||
# DRAW SEA btn in hex center
|
||||
# FREE DRAW btn in hex center
|
||||
btn = page.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
|
||||
self.assertTrue(btn.is_displayed())
|
||||
self.assertIn("FREE", btn.text.upper())
|
||||
self.assertIn("DRAW", btn.text.upper())
|
||||
self.assertIn("SEA", btn.text.upper())
|
||||
self.assertIn("btn-primary", btn.get_attribute("class"))
|
||||
|
||||
# ── Test 2 ───────────────────────────────────────────────────────────────
|
||||
@@ -214,19 +227,22 @@ class MySeaDrawSeaLandingTest(FunctionalTest):
|
||||
def test_landing_renders_six_chair_seats_labeled_1C_to_6C(self):
|
||||
"""All 6 chair positions render w. labels 1C-6C (placeholder for
|
||||
friend-invite). CSS class `.table-seat` is preserved so the SCSS
|
||||
positioning rules (data-slot=N) carry over from the room shell."""
|
||||
positioning rules (data-slot=N) carry over from the room shell.
|
||||
Each seat starts w. a red `.fa-ban` status icon (empty)."""
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||
seats = self.wait_for(
|
||||
lambda: self._six_seats()
|
||||
)
|
||||
self.assertEqual(len(seats), 6)
|
||||
labels = [
|
||||
"".join(s.text.upper().split()) for s in seats
|
||||
]
|
||||
for n in range(1, 7):
|
||||
for n, seat in enumerate(seats, start=1):
|
||||
with self.subTest(slot=n):
|
||||
self.assertIn(f"{n}C", labels[n - 1])
|
||||
label = "".join(seat.text.upper().split())
|
||||
self.assertIn(f"{n}C", label)
|
||||
# Each seat carries the red ban status icon initially.
|
||||
seat.find_element(
|
||||
By.CSS_SELECTOR, ".position-status-icon.fa-ban"
|
||||
)
|
||||
|
||||
def _six_seats(self):
|
||||
seats = self.browser.find_elements(
|
||||
@@ -238,22 +254,40 @@ class MySeaDrawSeaLandingTest(FunctionalTest):
|
||||
|
||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_draw_sea_click_transitions_to_picker_phase(self):
|
||||
"""Click DRAW SEA → page wrapper's data-phase swaps to 'picker';
|
||||
landing hides. Picker content itself lands in iter 2 — this test
|
||||
pins only the phase-swap contract iter 1 must establish."""
|
||||
def test_free_draw_click_seats_user_in_1C_then_swaps_phase(self):
|
||||
"""Click FREE DRAW → seat 1C transitions to `.seated` w. fa-ban
|
||||
swapped for fa-circle-check (visible to the user during the
|
||||
~800ms animation delay); other seats remain empty; then the
|
||||
page's data-phase swaps to 'picker' so iter 2's content can
|
||||
take over. Single-user instance for now → user always gets the
|
||||
lowest-numeral seat (1C)."""
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||
btn = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
|
||||
)
|
||||
btn.click()
|
||||
# Seat 1C goes seated + icon swaps. Other seats unchanged.
|
||||
seat1 = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".table-seat[data-slot='1'].seated"
|
||||
)
|
||||
)
|
||||
seat1.find_element(By.CSS_SELECTOR, ".position-status-icon.fa-circle-check")
|
||||
# Seats 2-6 retain the .fa-ban icon (still empty).
|
||||
for n in range(2, 7):
|
||||
with self.subTest(slot=n):
|
||||
other = self.browser.find_element(
|
||||
By.CSS_SELECTOR, f".table-seat[data-slot='{n}']"
|
||||
)
|
||||
self.assertNotIn("seated", other.get_attribute("class"))
|
||||
other.find_element(By.CSS_SELECTOR, ".position-status-icon.fa-ban")
|
||||
# After the seat animation, data-phase swaps to picker + landing hides.
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".my-sea-page[data-phase='picker']"
|
||||
)
|
||||
)
|
||||
# Landing block hidden after swap.
|
||||
landing = self.browser.find_element(By.CSS_SELECTOR, ".my-sea-landing")
|
||||
self.assertFalse(landing.is_displayed())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user