fix(ui): advance turn glow/interactivity to the player to move
`active` was `$derived(gameStore.activeBoards)`, and that getter read the non-`$state` `#game`. The $derived captured no reactive dependency, so the glowing/interactive boards froze on North's NW+NE after move 1: the next player couldn't act, clicks on their boards did nothing, and selections fired on unexpected boards — the reported "turns", "what piece is selected", and "mirrored movements" symptoms were all this one stale-derived bug. Fix: derive `active` from the reactive `view.currentPlayer`; remove the footgun `activeBoards` getter so nothing re-reads `#game` reactively. Add scripts/smoke.py: Playwright end-to-end test driving the real app through a full N->S->E->W round (engine vitest can't see Svelte reactivity wiring). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end smoke test driving the real app in a browser.
|
||||
|
||||
Engine vitest covers move logic; this covers the Svelte reactivity wiring that
|
||||
vitest can't see — specifically the turn glow / interactivity advancing player
|
||||
to player (the bug where `active` froze on North after move 1).
|
||||
|
||||
Boots `vite` itself, drives chromium via Playwright, asserts a full N->S->E->W
|
||||
round plays and each player's two boards light up in turn. Self-contained:
|
||||
|
||||
python3 scripts/smoke.py
|
||||
|
||||
Needs system chromium + the `playwright` python package. Exits non-zero on failure.
|
||||
"""
|
||||
import subprocess, sys, time, urllib.request, os, signal
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
URL = "http://localhost:5177/duplicate/"
|
||||
BID = ["NW", "NE", "SW", "SE"]
|
||||
# (player, board to act on, from-square, to-square) — one legal opening per player.
|
||||
ROUND = [("N", "NW", "e2", "e4"), ("S", "SW", "d2", "d4"),
|
||||
("E", "NE", "e7", "e5"), ("W", "SW", "d7", "d5")]
|
||||
EXPECT_ACTIVE = {"N": {"NW", "NE"}, "S": {"SW", "SE"},
|
||||
"E": {"NE", "SE"}, "W": {"NW", "SW"}}
|
||||
|
||||
|
||||
def wait_server(timeout=30):
|
||||
for _ in range(timeout * 2):
|
||||
try:
|
||||
urllib.request.urlopen(URL, timeout=1); return True
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
def active_boards(pg):
|
||||
return {BID[i] for i, bd in enumerate(pg.query_selector_all(".board"))
|
||||
if "active" in (bd.get_attribute("class") or "")}
|
||||
|
||||
|
||||
def piece_at(pg, board, sq):
|
||||
bi = BID.index(board)
|
||||
span = pg.locator(".board").nth(bi).locator(f'button[aria-label="{sq}"] span.pc')
|
||||
return span.inner_text() if span.count() else None
|
||||
|
||||
|
||||
def click(pg, board, sq):
|
||||
pg.locator(".board").nth(BID.index(board)).locator(f'button[aria-label="{sq}"]').click()
|
||||
pg.wait_for_timeout(80)
|
||||
|
||||
|
||||
def main():
|
||||
vite = subprocess.Popen(["pnpm", "dev", "--port", "5177"], cwd=ROOT,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
preexec_fn=os.setsid)
|
||||
try:
|
||||
assert wait_server(), "vite did not come up"
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(executable_path="/usr/bin/chromium", args=["--no-sandbox"])
|
||||
pg = b.new_page(viewport={"width": 1400, "height": 1000})
|
||||
pg.goto(URL, wait_until="networkidle"); pg.wait_for_timeout(300)
|
||||
|
||||
for player, board, frm, to in ROUND:
|
||||
got = active_boards(pg)
|
||||
assert got == EXPECT_ACTIVE[player], \
|
||||
f"{player}'s turn: expected active {EXPECT_ACTIVE[player]}, got {got}"
|
||||
other = next(x for x in EXPECT_ACTIVE[player] if x != board)
|
||||
click(pg, board, frm)
|
||||
click(pg, board, to)
|
||||
# move must apply IDENTICALLY on both of the player's boards
|
||||
for bd in (board, other):
|
||||
assert piece_at(pg, bd, frm) is None, f"{player} {bd} {frm} not vacated"
|
||||
assert piece_at(pg, bd, to) is not None, f"{player} {bd} {to} empty after move"
|
||||
print(f"OK {player}: {frm}{to} applied on {board}+{other}, turn advanced")
|
||||
|
||||
assert active_boards(pg) == EXPECT_ACTIVE["N"], "did not cycle back to North"
|
||||
print("OK full N->S->E->W round cycled back to North")
|
||||
b.close()
|
||||
print("\nSMOKE PASS")
|
||||
finally:
|
||||
os.killpg(os.getpgid(vite.pid), signal.SIGTERM)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except AssertionError as e:
|
||||
print(f"\nSMOKE FAIL: {e}"); sys.exit(1)
|
||||
Reference in New Issue
Block a user