The semantics hold
DuskPlugin.install() takes a semantics handle for the whole process, because every snapshot, q handle and actionability check reads the semantics tree. The cost is that every frame of a dusk-driven app also builds that tree, which an app without dusk (and without a screen reader) does not. ext.dusk.semantics_hold lets dusk:perf_run --semantics-pass measure the app without it, for one timed window.
ext.dusk.semantics_hold
| Param | Meaning |
|---|---|
action=release |
Drops dusk's handle. Refused unless a perf session is open, so the tree can only go away inside a timed window. |
action=acquire |
Takes the handle again, then awaits one frame (bounded, 200 ms) so the tree exists again when it answers. |
{"action": "release", "released": true, "semanticsEnabled": false, "heldByPlatform": false}
{"action": "acquire", "acquired": true, "semanticsEnabled": true, "treeReady": true}
semanticsEnabled stays true after a release when something else holds a handle: the tree only goes away when the last handle does. heldByPlatform is platformDispatcher.semanticsEnabled, true when the platform holds a handle of its own. On Flutter web that is every real app once it has sent a semantics tree: the engine turns semantics on at the first update and nothing turns it off, so a release there answers semanticsEnabled: true, heldByPlatform: true. On a native device it is true while an accessibility service runs. heldByPlatform: false with semanticsEnabled: true is another SemanticsHandle in the app (or the test harness). DuskPlugin.semanticsReleased reads the state in-app.
The caller owns the pairing: acquire after perf_end, so the acquire's full tree rebuild is not a frame of the session, and acquire even when a step or perf_end failed. perf_end also re-takes a handle left released, as a safety net for a caller that died in between. dusk:perf_run does all of it.
Driving while the tree is released
Anything that resolves a target reads the tree, so while it is released nothing may: ext.dusk.find, a q action and a snapshot would all see no nodes, and a transient handle taken to look would rebuild the tree inside the window being measured. Resolve first, with the tree on, then dispatch by coordinates:
| Extension | Records the point (tree on) | Dispatches by coordinates |
|---|---|---|
ext.dusk.tap |
reportPoint: true adds point: {x, y} |
{x, y} with no ref |
ext.dusk.drag |
reportPoint: true adds from and to; startRef plus dx/dy drags by an offset |
{x, y, toX, toY} with no ref |
ext.dusk.hover |
reportPoint: true adds point |
(a wheel then goes through CDP) |
A coordinate dispatch has no element, so none of the actionability gate's checks can run. Its response always carries:
"checks": {"gate": "skipped", "semantics": "released", "why": "..."}
semantics is held or released; the enabled check is the one that reads the tree. Nothing about a coordinate dispatch proves what it landed on: it is a replay of a point that was valid when the tree was on, for a screen that is expected to be in the same state.
See also
- dusk:perf_run, which drives the pass.
- The actionability gate.