fluttersdk_dusk 0.0.8 copy "fluttersdk_dusk: ^0.0.8" to clipboard
fluttersdk_dusk: ^0.0.8 copied to clipboard

Flutter E2E driver for LLM agents and CI. 34 CLI commands and 33 MCP tools drive a running app over VM Service extensions; no flutter_test harness needed.

Changelog #

All notable changes to this project will be documented in this file.

This project follows Semantic Versioning 2.0.0. Entries follow the Keep a Changelog shape.


Unreleased #


0.0.8 - 2026-06-17 #

Changed #

  • dusk:install now injects import 'package:magic_devtools/dusk.dart'; and gates on the magic_devtools dependency instead of the removed package:magic/dusk_integration.dart. The MagicDuskIntegration class was extracted from the magic core into the new magic_devtools package; the injected class name (MagicDuskIntegration.install()) is unchanged. Consumers that follow magic's install.yaml (which adds magic_devtools to dev_dependencies before running dusk:install) get the integration wired automatically; magic-only consumers without magic_devtools in pubspec.yaml are unaffected. Coordinated with the magic_devtools extraction.

Fixed #

  • dusk:install no longer injects import 'package:magic_devtools/dusk.dart'; or MagicDuskIntegration.install() into a vanilla Flutter app that has magic_devtools in its pubspec but no await Magic.init( call in lib/main.dart. Previously, the magic_devtools wiring block ran whenever the pubspec listed the dependency, regardless of whether a Magic.init anchor existed. This left an unused import in the consumer's file, causing dart analyze to fail. The gate is now hasMagicInit && _hasMagicDevtoolsDep(), matching the block's own intent documented in the comment above it. The existing try/catch around injectAfterMagicInit is retained as a defensive fallback.

Documentation #

  • Docs, skill, and example synced to the magic_devtools extraction: doc/plugins/magic-integration.md updated to note that MagicDuskIntegration now ships in magic_devtools (add as a dev_dependency) and shows the required import 'package:magic_devtools/dusk.dart';. skills/fluttersdk-dusk/references/cli-commands.md updated to reflect the magic_devtools gate and magic_devtools/dusk.dart import. ARCHITECTURE.md frozen-contracts item updated from magic to magic_devtools. Version pins bumped to ^0.0.8 throughout (pubspec.yaml, example/pubspec.yaml, doc/getting-started/installation.md, skills/fluttersdk-dusk/SKILL.md).

0.0.7 - 2026-06-17 #

Added #

  • dusk:console now surfaces debugPrint output even without fluttersdk_telescope. DuskPlugin.install() now chains a debugPrint override that records every call into a bounded in-package ring buffer (cap 50, newest-first). ext.dusk.console merges this buffer with the existing telescope recentLogsReader output using the same merge+dedup pattern as ext.dusk.exceptions, so debugPrint(...) / print(...) calls appear in dusk:console results regardless of whether telescope is installed. The telescope reader indirection is preserved: when telescope is wired it augments with Logger.root.onRecord entries and any other watchers it ships. Direct dart:developer log() calls that bypass debugPrint are not captured by the in-package path; they require telescope's LogWatcher. Capture scope is documented in doc/commands/index.md under "Console and exceptions".

  • Opt-in verify flag on dusk:tap / dusk_tap / ext.dusk.tap. When verify: true, the tap handler captures a cheap TARGET-scoped signal before and after the pointer (the nearest enclosing route name plus a hash of the target element's own semantics subtree: label, value, enabled/checked flags) and adds a changed: true|false field to the response reporting whether the tap produced an observable effect on the target. The signal is deliberately target-scoped, not a global route/whole-tree hash, so a counter button whose own label increments reports changed: true while unrelated background churn elsewhere in the tree does not. Default off (verify: false) keeps the response shape byte-identical to before: no changed key. The dusk:tap CLI gains a --verify flag and the dusk_tap MCP descriptor gains a verify boolean property (a parameter addition to the existing dusk:tap / dusk_tap, not a new command or tool).

  • Optional since filter on dusk:exceptions / dusk_exceptions / ext.dusk.exceptions. Pass since: "<iso8601>" (e.g. 2024-01-01T10:00:00.000Z) to receive only exceptions whose time is strictly after that timestamp. Agents can record the current time before an action, then call dusk:exceptions --since=<time> afterwards to see only new exceptions raised by that action, eliminating false positives from cumulative history. Default behavior (no since) is unchanged: the full cumulative list is returned. Unparseable since values are silently treated as absent. The dusk:exceptions CLI gains a --since flag and the dusk_exceptions MCP descriptor gains a since string property (a parameter addition to the existing dusk:exceptions / dusk_exceptions, not a new command or tool).

  • dusk:fill / dusk_fill / ext.dusk.fill — one-call text-field fill. Resolves a text-field ref (e<N> / q<N>), then focuses, clears, types, and settles in a single round-trip, replacing the manual focus + clear + type + settle dance every agent re-discovers. Composes the existing GATED ext.dusk.focus, ext.dusk.clear, and ext.dusk.type handlers verbatim (so the 6-check actionability gate, IME focus, onChanged/validator firing, and post-action snapshot semantics are reused, never re-implemented). Retries the whole resolve + focus + clear + type sequence ONCE when the ref goes stale mid-fill (a transiently-missing q<N> re-walks the now-settled tree on the second pass); a second stale outcome surfaces a typed stale envelope so the agent re-snaps or re-finds. Returns {ref, text, filled: true} plus an optional post-fill snapshot. This is a NEW command (CLI 32 -> 34) and a NEW MCP tool (31 -> 33) backed by a NEW ext.dusk.fill extension (28 -> 30).

  • dusk:reset_overlays / dusk_reset_overlays / ext.dusk.reset_overlays — one-call overlay reset. Returns the app to a known clean screen via three escalating, idempotent layers: (1) pop every PopupRoute (reusing dismissAllModals, never touching the page stack); (2) an Escape key press for overlays driven by the dismiss shortcut that are not PopupRoutes; (3) a Cancel/Dismiss/Close/OK/Done labelled tap for modal barriers that need an explicit affordance. Each layer is a no-op when the prior already cleared the overlays, so the command is safe to call speculatively between flows. Returns {popped: N, escaped: bool, dismissTapped: bool}. This is a NEW command (CLI 32 -> 34) and a NEW MCP tool (31 -> 33) backed by a NEW ext.dusk.reset_overlays extension (28 -> 30).

  • until confirmation on dusk:tap / dusk_tap / ext.dusk.tap. When until: "<text>" is set, after the tap settles the handler polls the live element tree (reusing the dusk:wait_for poll loop) for a Text whose data equals the expected string, up to untilTimeoutMs (default 3000), and adds an untilMatched: true|false field reporting whether it appeared. Confirms a navigation / state change produced the expected text in one call, replacing a separate dusk_wait_for round-trip. Default off (no until) keeps the response shape unchanged. The dusk:tap CLI gains a --until flag and the dusk_tap MCP descriptor gains until / untilTimeoutMs properties (a param addition, not a new tool).

  • "Driving real apps: gotchas for agents" doc page (doc/getting-started/driving-real-apps-gotchas.md). Captures the hard-won lessons from a long real-app E2E session, each now partly or fully addressed by D1-D7: refs go stale on rebuild (re-snap, or prefer a q<N> from dusk:find); text fields may snapshot nested (use dusk:fill, or note the typeable: true marker on the collapsed outer node); dusk:console captures debugPrint in-package now and is enriched by telescope; dusk:exceptions is cumulative (use --since); restart preserves the CDP port; overlays may need dusk:reset_overlays. Linked from llms.txt.

Fixed #

  • dusk:dismiss_modals now dismisses modals on ALL NavigatorState instances, not just the first. The previous implementation walked the element tree with a first-match guard and popped PopupRoute entries one-at-a-time with an endOfFrame await between each pop. showDialog defaults to useRootNavigator: true (root navigator) and showModalBottomSheet defaults to useRootNavigator: false (nearest navigator); when these are different navigators, the first-match walk left one modal open. The fix collects every NavigatorState in a full DFS walk, then calls popUntil((r) => r is! PopupRoute) on each navigator innermost-first, counting every PopupRoute dismissed. The popped return value is the additive sum across all navigators. The per-pop endOfFrame await is removed, which also unblocks unit tests that previously hung the flutter_test fake-clock harness when real modal routes were open.

Changed #

  • Pointer verbs now dispatch at the element's LIVE rect, not the cached snapshot rect. Every pointer verb (tap, hover, drag start + end endpoints, dblclick, right_click, triple_click) re-resolves the target's current bounding rect via the new dispatchRectOf(entry) helper immediately after the actionability gate passes and dispatches at that live center, falling back to the cached entry.rect.center only when the live rect is null (sliver / detached / synthetic). A host that rebuilt the target into a shifted slot between snapshot and action retains the same Element / RenderObject identity, so the live rect is valid. This fixes the false-success class where dusk:tap reported success while onTap never fired because the pointer landed on the target's stale gate-time position. The helper is purely additive to the FROZEN actionability gate: it runs after the gate passes and before dispatch, touching neither the gate order nor any failure-reason substring.
  • dusk:snap collapses nested textbox nodes and marks the survivor typeable: true. A wind WInput wraps as Semantics(textField:true) > MergeSemantics > TextField; because RenderEditable unconditionally owns its own textField Semantics node (flutter#26336) and MergeSemantics cannot absorb it (flutter#160281), the tree carried TWO nested textbox nodes and minted two eN refs. Agents naturally targeted the inner leaf, where dusk:type threw -32000. The snapshot walk now suppresses any textbox node whose render object is a render-tree DESCENDANT of an enclosing textbox node's render object, emitting a single ref for the outer typeable node so existing scripts keep resolving. The surviving textbox line gains an additive typeable: true sub-line. Collapse is by render-object CONTAINMENT only, never label/value equality, so two sibling fields sharing a label stay two distinct refs. The textbox role string is unchanged; eN minting stays snapshot-only. The source-side fix lives in wind (W1); this is the defensive dusk-side collapse.
  • Bumped fluttersdk_artisan to ^0.0.8. Picks up the substrate restart fix that preserves --cdp-port across the stop/start cycle (so dusk:resize / dusk:device keep working after fsa restart) and the published-config import-path fix in the plugin installer.

0.0.6 - 2026-06-09 #

Added #

  • dusk:screenshot web CDP fallback via Page.captureScreenshot. When ~/.artisan/state.json carries a cdpPort (a web target), the CLI command sends Page.enable + Page.captureScreenshot (format, quality, fromSurface: true) over the Chrome DevTools Protocol and writes the decoded bytes directly, bypassing the in-isolate ext.dusk.screenshot extension that hangs under CanvasKit+DWDS (issue #13). Native targets (no cdpPort) keep using ext.dusk.screenshot. The command captures the full app frame. This CDP fallback is CLI-only; the dusk_screenshot MCP tool still dispatches ext.dusk.screenshot in-isolate, so web agents should use the CLI for screenshots. Region (ref/rect) capture remains deferred.
  • Non-fatal FlutterError capture surfaced by dusk:exceptions. DuskPlugin.install() now chains a FlutterError.onError handler that records every non-fatal error (including RenderFlex overflow, tagged type: "overflow") into a bounded in-package ring buffer (cap 50, dedup by message + stackHead, newest-first). ext.dusk.exceptions merges this buffer with the existing telescope reader output, so overflow and other non-fatal rendering errors appear in dusk:exceptions results even when fluttersdk_telescope is absent (issue #14).
  • Per-ref overflow: annotation in dusk:snap output. Interactive nodes inside a currently-overflowing render ancestor now carry an additive overflow: true sub-line in the snapshot YAML. The check is a live renderObject.toStringShort().contains(' OVERFLOWING') call (the Flutter debug-mode convention from RenderFlex.toStringShort); no retained state, no Expando. Non-overflowing layouts produce no annotation. The annotation silently drops if a future Flutter version renames the suffix; dusk:exceptions remains the authoritative overflow signal.

Changed #

  • fluttersdk_artisan constraint bumped from ^0.0.6 to ^0.0.7 (Dart pre-1.0 caret rule: ^0.0.7 resolves to >=0.0.7 <0.0.8). Consumers now pull in artisan 0.0.7 which hardens start --cdp-port with busy-port fast-fail and Chrome/FIFO/profile cleanup (issue #25). All dusk-consumed artisan surfaces (CommandBoot, ArtisanCommand, ArtisanContext.callExtension, McpToolDescriptor, registerExtensionIdempotent, StateFile.read/write) are signature-identical to 0.0.6; the bump is non-breaking.

0.0.5 - 2026-05-28 #

Changed #

  • fluttersdk_artisan constraint bumped from ^0.0.5 to ^0.0.6 (Dart pre-1.0 caret rule: ^0.0.6 resolves to >=0.0.6 <0.0.7). Consumers now pull in artisan 0.0.6 which ships the substrate mcp:install --invocation=<exec> flag this release depends on for the fallback behavior below.
  • mcp:install fallback when bin/fsa is absent now writes dart run fluttersdk_dusk mcp:serve. The dusk wrapper auto-injects --invocation=fluttersdk_dusk when forwarding mcp:install to the substrate, so the substrate's .mcp.json writer picks the plugin-aware payload instead of the legacy dart run :dispatcher mcp:serve fallback. No change in behavior when fastcli is present; the ./bin/fsa mcp:serve payload is unchanged.
  • Renamed every dart run fluttersdk_artisan reference inside the dusk package to dart run fluttersdk_dusk (33 docs/code occurrences). The dusk wrapper proxies the full artisan command surface; the package-local invocation is now canonical inside dusk's own docs, error messages, dartdocs, and chained subprocess calls. Substrate package:fluttersdk_artisan/ Dart imports unchanged.

Fixed #

  • bin/fluttersdk_dusk.dart now forces collectMcpTools: true when dispatching mcp:serve, so dart run fluttersdk_dusk mcp:serve surfaces all 31 dusk_* MCP tools even without the fastcli scaffold. Previously returned 0 plugin tools (only the 10 substrate tools). Verified end-to-end on a fresh flutter create consumer with path-linked dusk + artisan 0.0.6 against a running Flutter app on Chrome (real counter increments visible via dusk:tap + subsequent dusk:snap).

0.0.4 - 2026-05-27 #

Added #

  • README.md ## AI Coding Assistants section + llms.txt ## AI & Tooling section + 📡 AI-first Distribution feature-table row. Aligns dusk's surface with the cross-package fluttersdk pattern (already shipped on fluttersdk_wind): the canonical fluttersdk-dusk skill at skills/fluttersdk-dusk/ is distributed through fluttersdk/ai to 8 agents (Claude Code, Cursor, OpenCode, Gemini CLI, VS Code Copilot, Codex CLI, Cline, Roo Code) via npx skills add fluttersdk/ai --skill fluttersdk-dusk. The hosted docs MCP at mcp.fluttersdk.com exposes a search-docs tool over Streamable HTTP for direct docs-corpus queries, with an npx @fluttersdk/mcp stdio bridge for clients without HTTP MCP transport. The README copy is explicit that this is independent of dusk's own runtime MCP (./bin/fsa mcp:serve): the docs MCP teaches the agent ABOUT dusk; the runtime MCP gives the agent eyes and hands on a running Flutter app.

Changed #

  • Hero logo (.github/dusk-logo.svg) realigned to fluttersdk_magic 1:1. The previous logo had drifted toward indigo (#3730A3, #4338CA, #6366F1, #818CF8) which is not in the magic palette the sibling packages share, and its custom wavy shimmer accents diverged from the family line work. The new SVG is a verbatim copy of magic-logo.svg: same 4-layer 3D chevron geometry, same three tilted orbit rings (rotated -12°, 25°, 60° around the same center), same rx / ry / stroke-width / stop-opacity tokens, same 7-color violet palette (#4C1D95 through #DDD6FE). The only change is the gradient ID prefix (m* -> d*, plus orbit-N -> d-orbit-N) so both logos can render on the same page without DOM-level ID collisions. Verification: diff <(grep colors dusk) <(grep colors magic) is empty (set-equal); the same diff over rotate() transforms, ellipse params, chevron paths, and stroke / opacity tokens is also empty.

Fixed #

  • README + CI workflow stale develop references. README.md hero logo URL, CI badge ?branch=, and contributor-section CI sentence pointed at the retired develop branch (404 after the GitHub Flow migration in 0.0.3). All three now point at master. .github/workflows/ci.yml push + pull_request triggers reduced from [main, master, develop] to [master] (single long-lived branch per the new flow; main was never used, develop is retired). Pub.dev's frozen 0.0.3 archive still carries the broken logo URL; this 0.0.4 docs-only release ships the fix to pub.dev.

0.0.3 - 2026-05-26 #

Added #

  • skills/fluttersdk-dusk/ Section 7 + references/community.md. Opt-in star and issue-report CTAs for the LLM-agent skill, bumped to skill version: 0.0.3. Section 7 carries the trigger matrix only (star = task verified end-to-end; issue = dusk-side bug, explicitly excluding all six Core Law 3 actionability substrings since those are app-state signals). Executable detail (preflight command -v gh && gh auth status, gh api --method PUT /user/starred/fluttersdk/dusk --silent, gh issue create -R fluttersdk/dusk --body-file - heredoc, dusk:doctor + dusk_console + dusk_exceptions diagnostic gather, prefill URL fallback under 6KB, spam brakes) lives in references/community.md so the always-loaded SKILL.md body stays compact. Both flows are prose-permission only, maximum once per session, never auto-executed; on gh absence the agent prints the URL but does not invoke open / xdg-open / start.

Changed #

  • Skill bundle decontaminated from consumer-specific identifiers. dusk_evaluate examples in references/mcp-tools.md, references/cli-commands.md, and references/workflows.md now use generic placeholders (MyService.instance.state, MyService.instance.state.toString()) instead of consumer-private symbols (Magic.find<MonitorController>(), Magic.find<MagicApplication>()). Route-discovery hint switched from grep -r 'MagicRoute.page' to portable grep -rEn 'GoRoute|MaterialPage|name:' lib/.

  • Tinker REPL guidance unified on the concrete command ./bin/fsa tinker across the published skill bundle. Package-name attribution (magic_tinker, artisan_tinker) dropped from SKILL.md, references/mcp-tools.md, references/workflows.md, references/cli-commands.md since users only ever need the command they run. Code-side magic_tinker references in lib/src/dusk_artisan_provider.dart, lib/src/extensions/ext_evaluate.dart, ARCHITECTURE.md, and doc/mcp/tool-reference.md are unchanged and tracked for a separate follow-up.

  • Three Copilot review findings on closed PR #5. references/mcp-tools.md IIFE closure now returns state.toString() so the placeholder API stays consistent with the surrounding MyService.instance.state examples. references/workflows.md route-discovery grep uses portable grep -rEn extended-regex syntax instead of the BSD-incompatible basic-regex \| alternation. skills/fluttersdk-dusk/SKILL.md stale REPL attribution rewritten.

  • Two Copilot review findings on PR #6. skills/fluttersdk-dusk/SKILL.md CLI output description rewritten to match references/cli-commands.md truth: 9 read / query verbs emit JSON, the 18 side-effect verbs print a one-line success summary by default and only emit JSON when --includeSnapshot is passed. references/community.md star-flow note drops the spurious HTTP 304 reference; GitHub's PUT /user/starred/{owner}/{repo} is idempotent and returns 204 whether the star was new or already set.

Docs #

  • CLAUDE.md adopts GitHub Flow (Golden Rule 5 + Branching section). One long-lived branch (master); task branches cut from master, PR back into master; releases bump pubspec.yaml + promote [Unreleased] then tag (git tag X.Y.Z && git push origin X.Y.Z triggers publish.yml). Matches flutter/flutter, dart-lang/sdk, dart-lang/pub, and the modern OSS ecosystem (react, vscode, rust, node, kubernetes, go, angular). The repo's develop branch is retired after this release PR merges.

0.0.2 - 2026-05-24 #

Added #

  • skills/fluttersdk-dusk/ LLM-agent skill bundle. Ships an Anthropic-shape skill that teaches an LLM agent (Claude Code or any MCP client) how to drive a Flutter app where fluttersdk_dusk is installed. Mirrors the fluttersdk_telescope skill layout. Five files: SKILL.md (frontmatter + 6 core laws + 3 agent loops + tool families + install snippet), references/mcp-tools.md (per-tool input schema / return shape / when-to-use / pitfalls across all 31 dusk_* tools), references/cli-commands.md (CLI mirror via ./bin/fsa dusk:*, pipeline patterns, exit codes), references/actionability-and-refs.md (6-step gate detail + e<N> / q<N> ref recovery matrix), references/workflows.md (8 concrete agent playbooks: form fill, scroll-to-tap, modal flow, navigation verify, hot-reload-after-edit, pull-to-refresh, log tail, before/after diff). Frontmatter front-loads TRIGGER when: / DO NOT TRIGGER when: vocabulary so the model auto-loads the skill on any dusk_* MCP call, dusk:* CLI invocation, or E2E-driver task on a running Flutter app.

Docs #

  • README demo.gif placeholder removed. The <p align="center"><img src=".../screenshots/demo.gif"></p> block plus its TODO(v0.0.2-followup) recording-instructions comment are dropped until the actual asset ships. The hero logo, badges, and below-the-fold content are unchanged.
  • example showroom em-dash sweep. example/lib/main.dart section headers and inline comments are normalised to commas / colons / parentheses, aligning the example with the global no-em-dash rule applied across the rest of the repo.

0.0.1 - 2026-05-23 #

Initial public release of fluttersdk_dusk. E2E driver for Flutter apps. Snapshot, tap, type, drag, scroll, screenshot, wait, find via VM Service extensions (ext.dusk.*). Framework-agnostic (vanilla Flutter friendly); Magic / Wind integrations ship inside those packages via DuskPlugin.enrichers extension point. Plugin of fluttersdk_artisan ^0.0.5 (hosted-only; no path overrides). Wind diagnostics flow through the neutral fluttersdk_wind_diagnostics_contracts bridge (WindDebugRegistry) rather than through the enricher list, so wind alpha-10 needs no dusk-side install wiring.

Added #

  • 32 CLI commands via DuskArtisanProvider.commands() (live count from ls lib/src/commands/*_command.dart): dusk:install, dusk:snap, dusk:tap, dusk:screenshot, dusk:type, dusk:scroll, dusk:wait, dusk:wait_for_network_idle, dusk:hover, dusk:drag, dusk:modal, dusk:doctor, dusk:navigate, dusk:navigate_back, dusk:get_routes, dusk:press_key, dusk:select_option, dusk:close_app, dusk:find, dusk:focus, dusk:blur, dusk:clear, dusk:right_click, dusk:dblclick, dusk:triple_click, dusk:set_checkbox, dusk:console, dusk:exceptions, dusk:observe, dusk:resize, dusk:device, dusk:hot_reload_and_snap. dusk:install is the one-shot bootstrap; the rest wrap a matching VM Service extension or substrate-routed action.
  • 31 MCP tool descriptors via DuskArtisanProvider.mcpTools() (live count from grep "name: 'dusk_" lib/src/dusk_artisan_provider.dart | sort -u): dusk_blur, dusk_clear, dusk_close_app, dusk_console, dusk_dblclick, dusk_device_profile, dusk_dismiss_modals, dusk_drag, dusk_evaluate, dusk_exceptions, dusk_find, dusk_focus, dusk_get_routes, dusk_hot_reload_and_snap, dusk_hover, dusk_navigate, dusk_navigate_back, dusk_observe, dusk_press_key, dusk_resize_viewport, dusk_right_click, dusk_screenshot, dusk_scroll, dusk_select_option, dusk_set_checkbox, dusk_snap, dusk_tap, dusk_triple_click, dusk_type, dusk_wait_for, dusk_wait_for_network_idle. All McpToolDescriptor const instances with Claude Code canonical descriptions (imperative opener + context paragraph + Usage: bullets).
  • 28 ext.dusk. VM Service extensions + 3 artisan:dusk: substrate-routed tools** (live count from grep "extensionMethod:" lib/src/dusk_artisan_provider.dart | sort -u). Direct ext.dusk.: snap, screenshot, tap, hover, drag, type, scroll, wait_for, wait_for_network_idle, dismiss_modals, press_key, select_option, navigate, navigate_back, get_routes, evaluate, close_app, find, focus, blur, clear, right_click, dblclick, triple_click, set_checkbox, console, exceptions, observe. Substrate-routed via artisan:dusk:*: resize, device, hot_reload_and_snap (in-isolate hot-reload deadlock avoidance). All ext.dusk. extensions register through registerExtensionIdempotent for hot-restart safety.
  • DuskPlugin.install(); idempotent host-side install entry. Wraps the app widget root in a RepaintBoundary (no GlobalKey) so ext.dusk.screenshot can find it via render-tree walk. Hot-restart safe via static _installCount guard. Honors DUSK_DISABLE env var (1 / true / yes, case-insensitive) as kill switch.
  • DuskSnapshotEnricher typedef; snapshot-enricher extension point. String? Function(Element, RefRegistry). Magic ships its enrichers via MagicDuskIntegration. Wind no longer ships an enricher as of wind alpha-10: wind state is read through the neutral fluttersdk_wind_diagnostics_contracts.WindDebugRegistry.current?.resolve(element) bridge inside ext_snapshot.dart and ext_observe.dart ahead of the enricher loop, so the 6 core wind fields (breakpoint, brightness, platform, states, bgColor, textColor) survive without an enricher registration. Contract: synchronous, stateless w.r.t. call ordering, may return null to skip, multi-line fragments split + indented under the ref entry by the dispatcher.
  • fluttersdk_wind_diagnostics_contracts integration: new production dep fluttersdk_wind_diagnostics_contracts: ^1.0.0. ext.dusk.snap and ext.dusk.observe read wind state via WindDebugRegistry.current?.resolve(element) in addition to the existing enricher list dispatch; the wind: block (filtered by _kDefaultWindKeys in defaults mode) is emitted directly by dusk. Magic enricher contract UNCHANGED.
  • RefRegistry; stable e<N> (snapshot-frozen) and q<N> (re-resolvable Playwright-Locator) token systems. e<N> refs are minted at dusk_snap time and consumed by every action tool; q<N> refs are minted by dusk:find and re-execute their stored predicates against the live tree on every action call (resilient to widget rebuild + route push).
  • Actionability gate (lib/src/utils/actionability_gate.dart); tap / hover / drag / type resolve through a single gate that verifies the target's enabled flag (Tristate.isFalse fails; Tristate.none and Tristate.isTrue pass), zero-area rect, and viewport overlap BEFORE synthesising the pointer / key event. Failures surface ServiceExtensionResponse.error(extensionError, "Widget ref=$ref is not actionable: $reason") with $reason ∈ {"not enabled", "zero rect", "off-viewport (rect=..., viewport=...)"}. scroll, select_option, and press_key intentionally skip the gate (see Known gaps).
  • dusk:install one-shot bootstrap; minimal install. Edits the consumer's lib/main.dart only (no bin/artisan.dart or lib/app/ scaffolding for vanilla Flutter apps). Detects Magic-stack apps via the await Magic.init( anchor and injects DuskPlugin.install() BEFORE Magic.init (then MagicDuskIntegration.install() AFTER), falling back to the runApp( anchor for vanilla Flutter apps. Wind alpha-10 needs no install-time wiring from dusk: the consumer calls Wind.installDebugResolver() directly, and dusk reads wind state through WindDebugRegistry at snap time. Vanilla consumers access dusk via dart run fluttersdk_dusk <cmd>. Idempotent; safe to re-run.
  • Flutter-free CLI wrapper; bin/fluttersdk_dusk.dart + executables: fluttersdk_dusk pubspec entry. dart run fluttersdk_dusk <cmd> proxies the full artisan CLI surface and exposes the dusk commands without dragging dart:ui into pure-Dart contexts.
  • install.yaml plugin manifest; V1 manifest at the package root makes plugin:install fluttersdk_dusk work end-to-end via the artisan PluginInstaller.
  • lib/cli.dart codegen barrel; Flutter-free typedef alias FluttersdkDuskArtisanProvider. Consumed by consumer-side lib/app/_plugins.g.dart auto-discovery without pulling Flutter symbols into the pure-Dart artisan codegen path.
  • dusk:find Playwright-Locator pattern; mints q<N> query handles backed by text / semanticsLabel / key predicates. Unlike e<N> refs (frozen at snap time), q-handles re-execute the Semantics + Element walk on every action call, so they survive widget rebuilds and route pushes as long as the predicates still match. Stale match returns an explicit stale-handle error; the agent re-finds, never silently retries.
  • dusk:doctor; diagnostic command that checks ~/.artisan/state.json Chrome PID staleness, DUSK_DISABLE env-var value, registered enricher count, Semantics-tree-forced flag, and Magic-init wiring in one pass. Emits a categorised report (OK / WARN / ERROR per check); exit code 0 when every check passes.
  • Chrome reaper (lib/src/utils/chrome_reaper.dart); graceful Chromium subprocess teardown between dusk:* runs so leftover headless tabs no longer accumulate. Detects orphans by VM Service URI, exits cleanly via SystemNavigator.pop first, falls back to SIGTERM.
  • Example apps: example/ (vanilla Flutter, 7 scenario screens: home menu + buttons / inputs / scroll / modals / drawer / forms) for live e2e validation against the 31 MCP tools + 32 CLI commands.
  • CDP driver (lib/src/cdp/): CdpClient, DevicePresets (8 curated device presets with explicit DPR values: iphone-x, iphone-13, iphone-15-pro, pixel-5, pixel-8, ipad-pro-12.9, desktop-1440, desktop-1920), ChromeFinder. Minimal in-house Chrome DevTools Protocol client (~110 LoC, dart:io WebSocket + dart:convert; no pub.dev deps).
  • dusk:resize CLI (lib/src/commands/dusk_resize_command.dart): dart run fluttersdk_dusk dusk:resize --width=375 --height=812 [--dpr=3] [--mobile] [--touch]. Reads cdpPort from state.json, opens CdpClient, sends Emulation.setDeviceMetricsOverride (+ optional setTouchEmulationEnabled). --reset sends 3-call clear chain. Fails loudly when CDP not enabled.
  • dusk:device CLI (lib/src/commands/dusk_device_command.dart): dart run fluttersdk_dusk dusk:device --preset=iphone-x. Applies the full emulation chain (metrics + conditional touch + UA) from the curated preset database. --list prints all 8 preset entries; --reset mirrors dusk:resize --reset.
  • 2 CDP MCP tools (dusk_resize_viewport + dusk_device_profile): both dispatch via the existing artisan: substrate prefix (no mcp_server.dart changes).
  • FakeCdpServer test harness (test/src/cdp/fake_cdp_server.dart): dart:io HttpServer + WebSocketTransformer.upgrade on an ephemeral loopback port. Configurable failure modes (failOnJsonVersion, dropWebSocket, delayResponseMs). Used by cdp_client_test.dart, dusk_resize_command_test.dart, dusk_device_command_test.dart.
  • Integration smoke test (test/integration/cdp_smoke_test.dart): tagged @Skip so default flutter test skips it; run manually via flutter test test/integration --tags integration to validate dart-lang/webdev#2642 regression status.
  • dusk:install magic-detect branch: now injects import 'package:magic/dusk_integration.dart'; instead of import 'package:magic/magic.dart';. Pairs with magic 1.0.0-alpha.15 which extracts the integration class into a dedicated sub-barrel.
  • 6-step actionability gate (Wave 3): Step 0 defunct preflight + Stable + Receives-Events gates round out ensureActionable (now async). Total preconditions in evaluation order: defunct (preflight), enabled, zero-rect, off-viewport, stable (rect unchanged across 2 consecutive frames; Playwright auto-waiting), receives-events (hit-test confirms ref is the front-most pointer target). Opt-out via checkStable=false / checkReceivesEvents=false (both default true). Failure-reason substrings extended: "defunct", "not stable", "obscured by" join the existing agent branch surface.
  • Snapshot-in-action-response (Wave 3, Playwright setIncludeSnapshot pattern): 8 action handlers (tap, hover, drag, type, press_key, scroll, navigate, navigate_back) accept includeSnapshot=true and append the post-action snapshot YAML to the success response. The agent no longer needs a mandatory follow-up dusk_snap call. duskSnapBuild widened from @visibleForTesting to public (legitimate production reuse). press_key handler endOfFrame omission fixed in passing.
  • Structured error envelope + fuzzy-match suggestions (Wave 3): lib/src/utils/error_envelope.dart with DuskErrorEnvelope carrying type + widget_path + suggestions[]. 10 type values: timeout, not_found, obscured, disabled, stale, zero_rect, off_viewport, not_stable, missing_param, unexpected. 6 factories. Dual-write into errorDetail (JSON envelope alongside the free-form message) preserves backward compat for substring-matching agents. Levenshtein with prefix-bonus drives the suggestions list for not_found. RefRegistry.activeRefs() added to support candidate collection.
  • ext.dusk.wait_for_network_idle (Wave 3): polls TelescopeStore.pendingHttpCount until the count hits zero for a configurable idleMs window. Params timeoutMs (5000), idleMs (500), pollIntervalMs (200). Function-pointer indirection (pendingHttpCountReader exported from dusk.dart) keeps dusk free of a hard telescope dependency; magic-side wires the real reader at install time. New CLI command dusk:wait_for_network_idle.
  • 4 utility tools (Wave 3): dusk_console (telescope log reader, function-pointer indirection via recentLogsReader), dusk_exceptions (telescope exception reader via recentExceptionsReader), dusk_dblclick (two synthesised taps with 100ms inter-tap delay, shared 6-step actionability gate + snapshot embed), dusk_set_checkbox (idempotent Checkbox / Switch toggle via element walk; no-op when current value matches target).
  • ext.dusk.observe (Wave 4): Stagehand-style observe-once-act-many pattern. Walks every active PipelineOwner semantics tree, filters interactive nodes (buttons / textfields / links / checkboxes / dropdowns via _roleFor / _isInteractive), mints a re-resolvable q<N> ref per candidate (Playwright Locator pattern; never e<N>), and returns a structured JSON list {candidates: [...], count: N}. Each candidate carries ref, role, label, value, bounds, isEnabled, isVisible, plus enricher-projected fields. Params: intent (caller hint, echoed only), limit (default 50), roles (comma-separated filter), includeEnrichers.
  • dusk:hot_reload_and_snap (Wave 4): CLI-side orchestration via VmServiceClient.reloadSources (in-isolate handler cannot reload its own isolate; deadlock avoidance). Sequence: reload -> wait -> snap -> screenshot -> exceptions -> bundle. Success envelope {reloaded, durationMs, snapshot, screenshot, recentExceptions}; compile-error envelope skips snap/screenshot but still gathers exceptions. Screenshot failure surfaces as partial-result screenshotError rather than aborting the round-trip. MCP descriptor uses the artisan: substrate routing prefix (extensionMethod: 'artisan:dusk:hot_reload_and_snap').
  • dusk:install is now self-sufficient (Wave 5 pre-publish). Phase 1 patches lib/main.dart (unchanged contract). Phase 2 chains dart run fluttersdk_dusk install (scaffolds bin/dispatcher.dart + ./bin/fsa AOT wrapper) followed by dart run fluttersdk_dusk plugin:install fluttersdk_dusk (registers DuskArtisanProvider; artisan 0.0.5 auto-purges the AOT bundle cache). Both Phase 2 sub-process calls are file-marker-guarded (bin/dispatcher.dart, .artisan/installed/fluttersdk_dusk.json) so re-runs are fast no-ops; failures swallow with a warning so Phase 1's lib/main.dart inject remains the guaranteed contract regardless of the consumer's dart PATH / sandbox state. Net effect: a fresh consumer needs only flutter pub add fluttersdk_dusk + dart run fluttersdk_dusk dusk:install to reach a working ./bin/fsa list + MCP tools/list surface.
  • ext.dusk.find substring predicate + dusk:find --contains=<substring> CLI flag (Wave 5; pre-publish E2E pass). Existing --text=<exact> semantics unchanged; agents now have a brittle / dynamic-label fallback. DuskQuery.containsText field is the carrier; matching walks Semantics labels first, then Text.data, mirroring the text path.
  • dusk:drag --fromRef=<eN> --toRef=<eN> flag aliases parallel to the --ref shape used by dusk:tap / dusk:hover (Wave 5). Legacy --startRef / --endRef flags retained for back-compat.
  • dusk:scroll --direction=<up|down|left|right> --pixels=<N> convenience flags that translate to signed --dy / --dx (Wave 5). Explicit --dy / --dx still win when both forms supplied.
  • Surface deltas (live counts): CLI commands: 32 (lib/src/commands/*_command.dart); MCP tool descriptors: 31 (dusk_artisan_provider.dart); VM Service extensions: 28 ext.dusk.* + 3 artisan:dusk:* substrate-routed.

Fixed (pre-publish macOS + web E2E pass, Wave 5) #

  • dusk_resize_viewport MCP arg parsing (GAP I): handler cast ctx.input.option('width') as String? which failed when MCP tools/call delivers {"width":390} as a native JSON int rather than a stringified arg. Resize command now defensively reads int / double / bool from either type via _readInt / _readDouble / _readBool helpers. CLI invocations still work unchanged (ArgParser-emitted strings).

Fixed #

  • ext.dusk.focus on TextField + EditableText (GAP C): handler walked UP from the snap-captured Semantics element looking for a Focus ancestor; for TextField the FocusNode sits BELOW the captured element (inside EditableText / FocusableActionDetector). Now falls back to a descendant walk that picks the first EditableText.focusNode or Focus.focusNode it finds. Reproducer: dusk:focus --ref=<textbox-eN> previously returned no Focus ancestor; now returns focused: true.
  • ext.dusk.scroll with ref pointing at the Scrollable itself (GAP D): Scrollable.maybeOf(context) walks UP, so passing the ListView's own ref (e.g. from dusk:find --key=my-list) returned null. Handler now resolves in three stages: (1) target element IS a Scrollable, use its state; (2) Scrollable ancestor (legacy); (3) descendant Scrollable walk (when ref is a parent like a Scaffold wrapping a list).
  • dusk:press_key --key= case-sensitivity (NIT 5): agents calling --key=TAB or --key=enter hit unknown key even though the supported set covered the intent. Lookup now does a case-insensitive fallback over _kKeyMap.keys when the direct hit misses; canonical PascalCase keys (Tab, Enter, ArrowUp) remain documented.
  • dusk:screenshot success message now reports decoded byte count + KB + format, e.g. Wrote 239456 bytes (233.8 KB, jpeg) to ./shot.jpg (NIT 1). Previously the line referenced the base64 character count which misled agents parsing for byte size.
  • dusk:screenshot missing-output error now suggests the canonical invocation dusk:screenshot --output=./shot.jpg --format=jpeg (NIT 8).
  • README + installation.md document the full 3-step install flow: flutter pub add fluttersdk_dusk + dart run fluttersdk_dusk dusk:install + dart run fluttersdk_dusk install && dart run fluttersdk_dusk plugin:install fluttersdk_dusk (GAP B). Previously the plugin:install step was missing, leaving consumers with ./bin/fsa list showing 0 dusk:* commands. installation.md carries a new ## Register with artisan section explaining the fastcli scaffold + plugin registration.

Test coverage #

  • 678 tests passing (2026-05-23 pre-publish, flutter test --exclude-tags=integration --timeout=30s). Scope covers handler entry points (params + error paths + happy paths where reachable under flutter_test), 32 CLI commands (name / boot / description / configure / handle / missing-arg validation), DuskArtisanProvider.commands() / mcpTools() shape, DuskPlugin.install() idempotency + DUSK_DISABLE env-var kill switch, RefRegistry mint / lookup / disposeGroup / disposeAll / refsForGroup / registerQuery / lookupQuery, actionability gate (6-step: defunct / enabled / zero-rect / off-viewport / not-stable / obscured), encodeToJpeg PNG-to-JPEG roundtrip + quality boundaries (1, 100, error), modal-route classification, dispatcher contract, CDP client + device presets + resize/device commands, Wave 3 structured error envelopes, Wave 4 observe + hot-reload-and-snap, Wave 5 find-contains substring + descendant focus walk + Scrollable-own-ref scroll. Pre-publish E2E pass against a fresh vanilla Flutter consumer (/tmp/dusk_e2e) verified 27 of 32 CLI commands + MCP initialize + tools/list (41 tools = 31 dusk_* + 10 artisan_*) + tools/call dusk_snap (identical to CLI) + tools/call dusk_evaluate (actual evaluation via artisan 0.0.5 substrate routing).
  • Coverage: dusk ~79% line coverage via flutter test --coverage. The remaining gap covers engine-dependent paths that hang the flutter_test fake-clock harness: handler endOfFrame waits, Future.delayed poll loops in wait_for, real toImage() rasterisation in screenshot success paths, and private _defaultProcessStartTime / _parsePsLstart doctor seam defaults. End-to-end coverage for those paths is captured by the example/ playground sweep.

Known gaps #

  • dusk:doctor runs in pure-Dart CLI context and cannot import package:flutter/rendering.dart without dragging dart:ui (breaks dart run invocation). Two checks defang gracefully as a result: semanticsEnabledProbe defaults to true (the only ERROR-class check, so doctor cannot ERROR from CLI) and enrichersProbe defaults to 0 (always WARNs on Check 3). The real probes belong to a future VM-Service-attached doctor invocation that calls into the running app.
  • scroll, select_option, and press_key intentionally skip the actionability gate: scroll targets the parent scrollable not the ref, select_option dispatches through Material/Cupertino popup machinery that owns its own enabled check, and press_key targets the focused widget rather than a ref. Adding the gate to these three handlers is V1.x candidate work.
  • RefRegistry._queries (q-handle store) is monotonically growing within a debug session; only RefRegistry.disposeAll() clears it. Worst-case memory bounded by debug-session lifetime; per-handle eviction is V1.x candidate work.

Risks Accepted #

  • dart-lang/webdev#2642 live regression: "Hot restart broken when running DWDS without Chrome Debug Port". Integration smoke test (test/integration/cdp_smoke_test.dart) surfaces this if active. Mitigation lives in the user's pinned Flutter SDK; plan does not block on regression resolution.
  • Flutter SDK >= 3.30.0 required for --cdp-port (per flutter/flutter#170612). Lower versions get an actionable error from both artisan doctor (advisory) and artisan start --cdp-port (fail-fast).
  • GAP E (drag synthesis vs Flutter Draggable): dusk:drag returns success but Flutter's DragTarget.onAcceptWithDetails does not fire on synthesised events in some configurations (Pointer Down + 5x Move + Up sequence may not match Draggable's gesture recognizer expectations on certain platforms / dwell times). Verified via E2E showroom (2026-05-23). Tracked for a 0.0.2 follow-up; agents needing drag should fall back to a pair of dusk:tap + manual scroll for now.
  • GAP G (advisory): receives-events check + q-refs on widgets with deep render subtrees: when dusk:find --key=<name-field> resolves to a TextField (or any widget whose findRenderObject() returns a top-level RenderObject), the actionability gate's receives-events check sees a hit-test path topped by a deeper descendant (e.g. RenderEditable) and trips obscured by other widget. The _isDescendantOf walk does not catch this case consistently. Workarounds: (1) use the e<N> ref from a prior dusk:snap rather than a q<N> from --key; (2) pass --no-checkReceivesEvents on the action. Tracked for a 0.0.2 follow-up; deeper investigation needed in the gate's hit-test path traversal.
  • GAP H (web): dusk:screenshot + dusk:close_app timeout on Chrome (DWDS): 10s timeout. macOS desktop works fine. The web path likely needs special handling for RepaintBoundary.toImage() under DWDS pixel pipeline + the platform-close semantics of SystemNavigator.pop() (which closes the tab, so the response can't return). Workaround for close: rely on ./bin/fsa stop SIGTERM (works). Workaround for screenshot on web: use the browser DevTools snapshot. Tracked for a 0.0.2 follow-up.

Backward compat #

DuskSnapshotEnricher typedef, DuskPlugin.install / DuskPlugin.enrichers / DuskPlugin.registerNavigateAdapter, RefRegistry public methods (register, lookup, registerQuery, lookupQuery, disposeAll, resetForTesting), and every MCP tool name / ext.dusk.* extension name are part of the public 0.0.1 contract. Future releases keep these stable across the 0.x line; any change requires a coordinated bump with magic + wind.

3
likes
160
points
6.33k
downloads

Documentation

Documentation
API reference

Publisher

verified publisherfluttersdk.com

Weekly Downloads

Flutter E2E driver for LLM agents and CI. 34 CLI commands and 33 MCP tools drive a running app over VM Service extensions; no flutter_test harness needed.

Homepage
Repository (GitHub)
View/report issues

Topics

#mcp-server #e2e-testing #ai-agents #testing #flutter

License

MIT (license)

Dependencies

flutter, fluttersdk_artisan, fluttersdk_wind_diagnostics_contracts, image, meta

More

Packages that depend on fluttersdk_dusk