headspin-control · bug-finding path

Finding real defects in the app under test

The plugin drives a live Appium WebDriver session against a real device, reads the app's accessibility tree, and runs package-scoped defect predicates over it. Proven live on com.android.settings (Pixel 6) — a genuine undersized touch target, surfaced with source + screenshot evidence.

Appiumwd/hub interface
4defect predicates
1real bug proven
34live devices
the data flow

Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full size

Loading…
The fix. The crawler already captured page_source + screenshots — but _detect only checked for crashes, error keywords, and foreground. It never analyzed the app. Adding a11y_defects.py and wiring it into _detect closed that drift: the plugin now surfaces real app-UI defects, not just crashes.
what changed
Before — page_source collected, never judged
After — page_source analyzed for app defects
_detect() → error-keyword match, foreground check, stuck-screen hash. _inventory() was a stub returning []. The app's accessibility tree was captured to disk but nothing inspected it.
_detect() now calls _a11y_defects(page_source), running package-scoped, subtree-aware predicates over the live a11y tree. A flagged defect flows straight into the evidence bundle with its exact node + bounds.
defect predicates
A1

Crash / ANR dialog

A system "isn't responding" / "has stopped" dialog is on top of the target app.

critical
A3

Unlabeled clickable

A real control whose entire subtree has no text and no content-desc — TalkBack can't announce it.

medium
A4

Unlabeled image control

A clickable ImageView/ImageButton leaf with empty content-desc and no labelled child.

high
A5

Undersized touch target

An on-screen control below 48 dp (Material / WCAG 2.5.8). The proven defect: a 24.5 dp Dismiss button.

low
false-positive guards

Package scope

Predicates skip any node whose package ≠ the target app — OS chrome (status bar, nav bar) is never flagged.

Subtree-aware

A clickable row whose child carries the label is not flagged — only leaf-or-fully-empty subtrees count as unlabeled.

is_control gate

Anonymous layout slivers (no resource-id, no label, generic *Layout) are structural, not controls — suppressed on A3 and A5.

Fully on-screen

A5 only counts nodes fully inside the viewport with non-zero area — edge-clipping list rows don't register.