Skip to content

0.0.10

Choose a tag to compare

@github-actions github-actions released this 20 Aug 17:51
· 7 commits to master since this release
35e3519

Added

  • The actionability gate now reports what it could NOT prove, instead of passing silently. Step 5 hit-tests the target's centre and throws when something else is on top, but it can also fail to ANSWER: on Flutter Web's debug build that is routine, because DWDS pipes hit-tests through a snapshot view that does not mirror the live element subtree. The gate proceeds in that case (breaking every valid tap on the artifact is the worse failure) and used to do so silently, so a clean pass was indistinguishable from a confirmed one. That is how dusk:fill printed a green tick four times onto a row covered by a pinned footer, with nothing in any response saying the check had not run.

    ensureActionable now returns an ActionabilityReport (confirmed / indeterminate / skipped), and the response carries a checks block whenever step 5 did not confirm, with a why and, on the indeterminate path, overlapCandidates: a rect scan naming render objects that overlap the target and paint after it, capped at five. Advisory rather than a verdict, since an overlap is not proof of occlusion. The block is absent on the healthy path. The six-step order and every failure-reason substring are unchanged. Covered by test/src/utils/actionability_report_test.dart and a payload case in test/src/extensions/ext_pointer_test.dart.

  • dusk:exceptions --clear empties the capture buffer after returning the current entries. The buffer is cumulative by design (it is the app's error history, which is what the command is for), so one real fault at boot rides along on every later read and a per-route sweep reports it against every route. A 12-of-12 "overflow on every screen" finding once turned out to be a single 4.8px transient, and an instrument with a permanent false positive stops being consulted. Clearing after the read rather than before gives a caller everything so far plus a clean slate, which is the primitive a before/after sweep needs. Only dusk's in-package buffer is affected; a wired telescope owns its own store. New: clearCapturedExceptions() in lib/src/dusk_error_capture.dart (the existing reset was test-only and also uninstalled the hook). Covered by three cases in test/src/extensions/ext_exceptions_test.dart.

  • --json on every dusk:* verb prints the raw envelope. The CLI used to split by verb: read commands printed JSON, the side-effect verbs printed a one-line summary, and a caller driving from a shell had to know which shape each verb produced. Worse, the summarising verbs dropped fields that mattered, and one of them (dusk:wait) dropped the only field it had. The flag makes output shape a caller's choice; the default is unchanged, so a human at a terminal still gets the summary. Where a summary can hide a verdict it now names it: ✓ Tapped e7 (no observable change). New: lib/src/commands/json_output.dart, applied to 20 commands.

  • dusk:snap gained --within, --interactiveOnly and --grep; dusk:find gained --within. A full tree is the wrong default answer to most questions. It costs context on any real screen, and on a shell whose sidebar repeats the labels of the pages it opens it is also the misleading one: an exact-label lookup resolves the nav item, so the caller measures the sidebar and concludes two pages differ. The workaround in the field was an x-coordinate threshold for "the content region", which is wrong at every other width and meaningless on a phone where there is no sidebar.

    --within takes an e<N> ref and walks that subtree; an unknown or node-less ref is an error rather than a silent widening. --interactiveOnly drops the plain - text lines. --grep keeps matching nodes plus the ancestors leading to them, because the ancestors carry the refs an agent acts on and a matching text line has none of its own. The three compose, and an unfiltered call is byte-identical to before.

    On find, the scope becomes part of the minted q<N> handle (DuskQuery.withinRef) rather than a one-off resolution argument: a handle re-executes on every action, and a scoped locator that forgot its scope on the next re-resolve would look correct right up until the shell rebuilt. Playwright's scoped locators behave the same way, including the part where a handle stops resolving once its scope is gone; dusk reports that as matched: false with a diagnostic naming the ref. Touches lib/src/extensions/ext_snapshot.dart, lib/src/extensions/ext_find.dart, lib/src/ref_registry.dart, both commands and both MCP descriptors; covered by test/src/extensions/ext_snapshot_filter_test.dart and test/src/extensions/ext_find_within_test.dart.

  • dusk:doctor gained two checks for the failures that present as "dusk is broken" and are not. Session ownership compares state.json's projectRoot against the working directory: ~/.artisan/state.json is a single global slot, so a sibling project's artisan start silently takes it and every dusk:* call from here drives that app instead, succeeding each time. The measured case had a worktree in another repository rewrite it mid-session, and two commands produced a screenshot of an entirely different product before anyone noticed. CDP session health probes the recorded cdpPort for three failures that share one symptom, a capture that never changes: the port refuses (a killed run left its dev server holding the web port while its Chrome is gone), it serves no page on this run's webPort (an orphan browser still up with the old build), or the matching page is hidden (frame production off). Both are WARN and both skip cleanly when the relevant state is absent. Touches lib/src/commands/dusk_doctor_command.dart; covered by eight cases in test/src/commands/dusk_doctor_command_test.dart.

  • CdpClient.connect accepts matchUrlSubstring to pick the page tab that belongs to this run. It selected the first type: "page" tab unconditionally, which is not reliably the app under test: an orphan Chrome from a killed run answers on its own debug port with the old build still loaded. The parameter defaults to null, so existing callers are unchanged; dusk:doctor passes this run's web port. Touches lib/src/cdp/cdp_client.dart.

  • Five verbs now return an effect block reporting what the widget HOLDS, not what it was asked to do. A dusk action confirms that it DISPATCHED; nothing in the response confirmed the widget received, and that gap has produced defect-shaped stories more than once. ext.dusk.fill printed a green tick four times onto a field covered by a pinned footer. A fill against an InputType.number field reported the text it had been handed while the widget kept nothing, and the resulting "the sheet holds a stale copy" theory survived two rewrites of a widget that had been correct the whole time. The block is always present on those five, because the agents who most need it are the ones who do not know to ask. The verbs left out have nothing cheap to read back; select_option is the exception worth a follow-up, since it still echoes its own value parameter:

    Verb kind Fields
    tap treeChanged changed (target-scoped route + semantics-subtree signal)
    type, clear, fill text verified, value read back off the live TextEditingController
    scroll scrollOffset changed, before, after
    set_checkbox checked verified, before, after re-read from the widget

    Two handlers were reporting the request rather than the result and now read back: ext.dusk.type echoed its own text parameter, and ext.dusk.set_checkbox returned value: <requested> for a control that may have ignored the tap. typeIntoElement returns the post-write value for this. New: lib/src/utils/effect_report.dart. Covered by test/src/extensions/ext_text_input_effect_test.dart (including a digits-only field that rejects the write, the reproducible stand-in for the number-field case) plus cases in the scroll, checkbox, fill and pointer suites.

  • dusk:screenshot and dusk_screenshot now expose ref and rect, so an agent can capture one component instead of the whole screen. ext.dusk.screenshot has supported all three modes (viewport, ref, ref + sub-rect) since it shipped, and the skill documented them, but neither surface an agent actually reaches declared the parameters: the CLI's configure had only --output / --format / --quality, the MCP inputSchema had only format / quality, and its description sent the reader to dusk_snap for "region screenshots", which mints a ref that nothing would accept. The capability was reachable only by calling the VM Service extension by hand. Agents worked around it by capturing the full frame and cropping in Python, or by growing the viewport to a size no device has and putting it back afterwards. rect still requires ref and is a hard error alone, rather than a silent full-frame capture.

    On web the CLI captures through CDP because the in-isolate rasterise hangs under CanvasKit + DWDS, and CDP has no notion of a Flutter ref. Rather than duplicate the geometry, ext.dusk.screenshot gained a geometry: 'true' mode that resolves the same ref + rect and returns {rect: {x, y, width, height}, devicePixelRatio} without rasterising; the CLI turns that into a Page.captureScreenshot clip (Flutter logical pixels and CDP CSS pixels are the same unit, so it crosses over unscaled). A ref that no longer resolves exits 1 rather than falling back to a full-frame capture, because an image that looks right and answers a different question is the failure this flag exists to remove. Touches lib/src/extensions/ext_screenshot.dart, lib/src/commands/dusk_screenshot_command.dart, lib/src/dusk_artisan_provider.dart; covered by test/src/extensions/ext_screenshot_test.dart and test/src/commands/dusk_screenshot_command_test.dart. New page: doc/reference/frame-production.md sibling doc/commands/dusk-screenshot.md examples 5 and 6.

  • Every ext.dusk.* success payload now carries a warnings block while the app has stopped producing frames, and the CLI prints a matching stderr banner. With frames off, semantics labels are never rebuilt and dispatched gestures cannot take effect, so two different readings go wrong at once and neither looks like a harness problem: dusk:snap returns a screen with its buttons and none of its - text nodes (a rendering dashboard reads as "permanently stuck on loading skeletons", which nearly shipped as a defect), and an action reports a clean dispatch that could not possibly have landed. The block carries framesEnabled: false, the lifecycleState behind it, and a hint naming Page.bringToFront as the fix. It is omitted entirely on a healthy engine, so its presence is the signal and a clean run carries no extra bytes. The banner exists because the commands that summarise rather than print the envelope (dusk:snap prints only the tree, dusk:tap prints ✓ Tapped e7) would otherwise drop the one field that says the result is untrustworthy. New: lib/src/utils/dusk_response.dart (duskResult, the single seam all 34 handler success paths now return through), frameProductionWarning() in lib/src/utils/frame_sync.dart, lib/src/commands/frame_warning_output.dart, doc/reference/frame-production.md. Covered by test/src/utils/dusk_response_test.dart plus banner cases in the snap and tap command tests.

Removed

  • ext.dusk.tap's opt-in verify flag and its top-level changed field. The signal it produced is now the always-on effect block above, so the flag was a second way to ask for something the response already carries. dusk:tap --verify and the verify MCP property are gone; read effect.changed instead of changed. Migration is a one-line rename for anything that branched on it. dusk:tap's one-line output also gained a (no observable change) suffix, because the default path prints ✓ Tapped e7 and would otherwise drop the one field worth reading.

Fixed

  • The gate's checks block reached tap and none of the other seven verbs that run the gate. ensureActionable returns an ActionabilityReport, and only the tap handler stamped it; hover, drag, dblclick, right_click, triple_click and type discarded the return value, which means fill did too. ARCHITECTURE.md and doc/reference/actionability-gate.md both present it as a gate-level guarantee, and the anecdote that motivated it is a fill onto a row covered by a pinned footer, so the one verb it was written for was the one that could not report it. All eight now route through a shared stampChecks.

  • dusk:wait_for_network_idle reported success and exited 0 when the network never went idle, the same defect dusk:wait had, in the sibling command, fixed in the same release. See the dusk:wait entry below.

  • dusk:scroll's effect block measured a different scrollable than the one it drove. The before-offset came from Scrollable.maybeOf(target), which is the ANCESTOR, while the delta branch resolves through a three-stage ladder that also accepts the target BEING a scrollable or containing one. Passing a ListView's own ref, which is what dusk:find --key=my-list returns, therefore reported before: null beside a real after and called it changed: true: exactly the "ref is not a scrollable" case the block was added to catch. The offset is now read from the scrollable each branch actually resolved.

  • dusk_screenshot advertised a q<N> handle it could not resolve. Both the CLI help and the MCP inputSchema say ref takes "an e<N> token from dusk_snap or a q<N> handle from dusk_find", but the resolver called RefRegistry.lookup, which never sees the q space. A query handle failed with "not found in RefRegistry. Call ext.dusk.snapshot first", pointing the agent at the wrong recovery. It now routes through resolveRefForAction like every other verb.

  • dusk:doctor warned that a session belonged to another project when the caller stood in a subdirectory of it, and crashed outright when the recorded CDP port had been taken over by a non-CDP service. The ownership row compared paths exactly while artisan's own sessionOwnershipError compares is-within, so the two tools disagreed about the same state file; and the CDP probe decoded JSON outside the guard that catches the port being dead, turning one of the three cases the check exists to name into a crash of the whole run. The warning text also still described ~/.artisan/state.json as a single global slot, which stops being true with per-project sessions.

  • dusk:doctor carried a third copy of artisan's path-ownership rule, and the copy disagreed with the original. The doctor compared state.json's projectRoot to the working directory for EQUALITY while sessionOwnershipError compares is-within, so standing in a package subdirectory made the doctor report the session as another project's while every artisan command drove it without complaint. Two tools disagreeing about one state file is worse than either answer alone.

    It now calls sessionOwnershipError as the predicate and keeps its own dusk-specific wording for the warning. That is what the dependency bump to fluttersdk_artisan ^0.0.10 is for: the function does not exist in 0.0.9, so the older constraint would let a consumer resolve a version this package no longer compiles against. Touches lib/src/commands/dusk_doctor_command.dart, pubspec.yaml.

  • dusk:find --within scoped only one of its five predicate legs and silently searched the whole tree for the other four. _findElementByKey, _findElementByTextData, _findElementByTextContains and _findSemanticsNodeByLabelContains each took the scope as a from parameter and then walked from the root anyway, so --key / --contains (and --text whenever it fell through to the element leg) resolved against the entire screen while reporting a scoped answer. Only --semanticsLabel honoured it, which is why a live drive of the feature looked correct. An unused named parameter is not an analyzer diagnostic, so nothing caught it. All four walks now start at the scope.

    A second hole sat behind it: a scope entry carrying no SemanticsNode left the semantics walks unbounded rather than refusing. ext.dusk.find_by_text mints exactly that shape (RefRegistry.register without a node), so a ref taken from a dusk:wait result reached the widening path. A label lookup, which has no element-tree fallback, now returns matched: false with a diagnostic naming the recovery; --text and --contains fall through to the element leg, which the scope does bound. Touches lib/src/extensions/ext_find.dart; covered by four cases in test/src/extensions/ext_find_within_test.dart.

  • The gate reported obscured by other widget (top=_ReusableRenderView), naming the render view as the thing covering the widget it hosts. The graceful-degradation branch tested path.length == 1 && isRootRenderView(path.first), but path runs deepest-first, so the root view being topmost already means nothing in the widget layer claimed the point. A path of view plus gesture-handler therefore missed the branch and threw. The condition is now isRootRenderView(path.first), which is what it was a proxy for. Touches lib/src/utils/actionability_gate.dart.

  • dusk:wait reported success and exited 0 when the condition never matched. ext.dusk.wait_for returns a SUCCESS envelope carrying matched: false on timeout rather than an error, and the command printed ✓ Condition matched without reading it. So the one command whose entire job is asserting a post-condition passed on exactly the case it exists to catch, and any shell chain gated on its exit code proved nothing. It now prints what happened and exits 1 when matched is false. dusk:wait_for_network_idle had the identical defect and the identical fix: its handler also answers a timeout with {matched: false}, and the command printed Network idle and returned 0 regardless. It is the one a CI script is most likely to chain on. Touches lib/src/commands/dusk_wait_command.dart and lib/src/commands/dusk_wait_for_network_idle_command.dart; covered by both command test files.

  • Every action extension hung forever when the app stopped producing frames, which is what a backgrounded browser tab does. Twenty-seven await WidgetsBinding.instance.endOfFrame calls across ext_pointer, ext_text_input, ext_navigation, ext_fill, ext_focus, ext_scroll and ext_checkbox settled a gesture or an edit by awaiting the binding directly. endOfFrame only schedules a frame while SchedulerBinding.framesEnabled is true, and Flutter Web turns frame production off once Chrome reports document.visibilityState: "hidden", so the future never completed: dusk:tap sat for 45s+ with no output and no error until the caller's shell timeout killed it, which reads as a wedged app rather than a backgrounded window. All twenty-seven now route through awaitFrameOrTimeout / awaitFramesOrTimeout (lib/src/utils/frame_sync.dart), which falls through after kFrameSyncTimeout (200ms per frame). A healthy engine is unaffected: a real frame lands in ~16ms and still wins. The actionability gate's own private copy of this helper was removed in favour of the shared one, leaving one bound for the whole package. Covered by test/src/utils/frame_sync_test.dart and a frame-starvation case in test/src/extensions/ext_pointer_test.dart.

  • CdpClient.defaultHttpGet and ChromeFinder.defaultHttpGet closed the HttpClient while the response body was still streaming. Both returned response.transform(utf8.decoder).join() without awaiting it inside a try/finally whose finally calls client.close(), so the close raced the body drain and a truncated or failed read was possible on a slow /json response. The same shape was in the integration smoke helper. Touches lib/src/cdp/cdp_client.dart, lib/src/cdp/chrome_finder.dart, test/integration/cdp_smoke_test.dart.

Docs

  • The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger fluttersdk/ai climbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published dusk releases instead of counting commits. workflow_dispatch stays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml)
  • dusk_fill and dusk_reset_overlays were invisible to the skill. Both shipped in 0.0.7, and neither appeared anywhere in skills/fluttersdk-dusk/: not in references/mcp-tools.md (whose header still promised "31 tools" against a real 33), not in references/cli-commands.md, not in the SKILL.md family table, not in the CLI output-shape law. An agent loading the skill therefore re-discovered the manual focus + clear + type + wait sequence that dusk_fill exists to replace, and had no answer at all for an overlay that is not a PopupRoute, since dusk_dismiss_modals only pops those. Both now carry a full entry: input schema, return shape, when to reach for them over the older tool, and the CLI form. (skills/fluttersdk-dusk/SKILL.md, skills/fluttersdk-dusk/references/mcp-tools.md, skills/fluttersdk-dusk/references/cli-commands.md)
  • Three counts corrected with them: the MCP tool total (31 to 33), the in-isolate ext.dusk.* split (28 to 30 extension tools, 3 substrate), and the CLI side-effect verb list (18 to 19, dusk:fill). dusk:reset_overlays went into the JSON-returning list instead, because unlike the other side-effect verbs it always emits JSON. (skills/fluttersdk-dusk/SKILL.md, skills/fluttersdk-dusk/references/mcp-tools.md)
  • Documented one asymmetry a Bash caller trips on: includeSnapshot defaults to true on the dusk_fill MCP tool and to false on dusk:fill. (skills/fluttersdk-dusk/references/cli-commands.md, skills/fluttersdk-dusk/references/mcp-tools.md)