tools/ui/ — scripts + Playwright config that let Claude Code (and
humans) drive the Flutter WASM build in a real browser. The point is
to avoid screenshot round-trips: every interactive widget in clide
ships a Semantics wrapper anyway (a11y requirement), so the automation
layer just queries the `flt-semantics[aria-label]` DOM.
* build.sh — `flutter build web --wasm` from app/
* serve.sh — `python3 -m http.server 4280` in the background.
Before binding, kills any stale listener on the port (orphans
from earlier failed runs no longer accumulate).
* stop.sh — port-based kill; escalates to SIGKILL after 300ms.
The pidfile is now advisory — port ownership is the source of
truth.
* driver.ts — `ClideDriver` class with `byLabel`, `click`, `type`,
`readText`, `screenshot`, `dumpSemanticsTree`, and
`waitUntilReady` that auto-clicks the `flt-semantics-placeholder`
so the semantic tree is populated before queries.
* tests/smoke.spec.ts — first driver test; asserts welcome +
disconnected labels surface in the Semantics DOM.
Co-Authored-By: Claude <noreply@anthropic.com>
1.5 KiB
1.5 KiB
clide UI harness
A Playwright-based driver that drives the Flutter WASM build of clide through its semantic DOM tree (the same tree screen readers use). Used by Claude Code to "actually use the app" without screenshot round-trips, and by CI to catch web-regression bugs.
One-time setup
cd tools/ui
npm install
npx playwright install chromium
Dev loop
# From repo root:
make ui-dev # build web + start local server :4280
cd tools/ui && npx playwright test
# When done:
make ui-stop # kill the local server
Or the one-shot smoke:
make ui-smoke # build + serve + run smoke + stop
Driver surface
import { ClideDriver } from '../driver';
test('...', async ({ page }) => {
const clide = new ClideDriver(page);
await clide.goto('/');
await clide.click('Open project');
await clide.type('Name', 'My project');
const text = await clide.readText('disconnected');
await clide.screenshot('out/my-state.png');
const tree = await clide.dumpSemanticsTree();
});
All lookups go through Flutter's semantics tree (flt-semantics[aria-label]).
This only works because every interactive widget in clide emits a
Semantics(label:, hint:, button:) wrapper — a requirement that's baked
in for screen-reader support and gets enforced by
test/a11y/semantic_coverage_test.dart.
CI
make ui-smoke is the CI entry — builds the WASM bundle, runs the
harness, cleans up. Enabling Gitea Actions will start running this on
every push.