a44b4de717
`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>
90 lines
3.5 KiB
Python
90 lines
3.5 KiB
Python
#!/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)
|