Gesture TDDs for mobile UI (single/multi-finger tap · drag · parallel multi-finger drag · pinch/rotate · swipe/fling · system-edge gestures)

mandatory

The Koder Stack test generators MUST emit touch-gesture tests for every MOBILE UI variant (Android/iOS, and any touch surface) of every component: single-finger tap/long-press/double-tap, single-finger touch-and-drag, MULTI-finger simultaneous tap, and MULTI-finger PARALLEL touch-and-drag (two-finger scroll, pinch/zoom, rotate), plus directional swipe/fling and system-edge gestures. Each gesture asserts the surface RESPONDS correctly (navigation/state/scroll-offset/zoom-level), gestures are reachable, and parallel multi-finger input is honored. Driven per surface: Flutter `integration_test`/`WidgetTester` multi-pointer gestures; Android Compose `performTouchInput`; iOS XCUITest; and native shell / Flutter-Linux / mobile-on-Kompose end-to-end via `koder-vdev` touch (synthetic-input-and-virtual-devices.kmd modality #4). Companion to visual-regression-tdds.kmd (Category E scroll uses the drag primitive).

Scope

Every touch-capable Koder UI variant MUST ship gesture TDDs under <module>/tests/regression/gesture/. Primary targets:

  • Flutter mobile (app/mobile/ — Android + iOS).
  • Android native (Compose / Views).
  • iOS native (when shipped).
  • Native shell / mobile-on-Kompose and Flutter-Linux touch

    end-to-end via koder-vdev touch (modality #4).

Out of scope: pointer-only desktop surfaces (covered by the pointer driver + visual-regression), CLI/TUI, server-only services. A desktop surface that ALSO accepts touch ships the gesture categories its touch handlers exercise.

R1 — Gesture categories

A surface ships the categories its handlers exercise. Each category asserts the surface's response, not merely that the event was sent.

Category G1 — Single-finger taps

Tap, double-tap, long-press at the target's center. Assert the bound action fires exactly once (tap → activate; double-tap → its distinct action; long-press → context menu / selection). A tap that lands but triggers nothing (dead control) fails; a tap that triggers twice (double-fire) fails.

Category G2 — Single-finger drag / swipe / fling

Touch-and-drag from A to B. Covers: scroll (the driver for visual-regression Category E reachability), directional swipe (dismiss, reveal, page), and fling (momentum scroll). Assert the resulting scroll offset / navigation / dismissal matches the gesture direction + distance, and that a short drag below the slop threshold is treated as a tap, not a drag (G7).

Category G3 — Multi-finger simultaneous tap

Two- and three-finger taps (each finger its own contact, down together). Assert the multi-finger binding fires (e.g. two-finger tap → secondary action / a11y) and that it is NOT misread as N separate single taps.

Category G4 — Multi-finger PARALLEL drag

Two (or more) fingers dragging at the same time, one interpolated frame containing every contact (koder-vdev touch multidrag / WidgetTester multi-pointer). Covers two-finger pan/scroll. Assert all contacts move in parallel and the surface honors the multi-finger pan (not just the primary finger).

Category G5 — Pinch / zoom / rotate

Two-finger parallel drag whose paths converge (pinch-in / zoom-out), diverge (pinch-out / zoom-in), or arc (rotate). Assert the zoom level / rotation of the target (map, image, canvas — e.g. KVG/Kad) changes monotonically with the gesture and clamps at its min/max.

Category G6 — System / edge gestures

Edge-swipe back, app-switch, top/bottom-edge reveals — where the surface participates. Assert the surface reacts correctly AND does not swallow a system gesture it shouldn't (respecting app-layout/safe-area.kmd gesture insets).

Category G7 — Disambiguation / conflict

Tap-vs-drag slop threshold, scroll-vs-swipe, single-vs-double-tap timeout, drag-vs-long-press. Assert the surface resolves ambiguous input to the intended gesture (a 3 px wobble during a tap is still a tap; a slow press that then moves is a drag, not a long-press).

R2 — Driver per surface

Surface Gesture driver
Flutter mobile (Android/iOS) integration_test + WidgetTester TestGesture (multi-pointer: startGesture per finger → parallel moveBy)
Android native (Compose) performTouchInput { down/moveTo/up }, multi-pointer via pointer IDs; UiAutomator for system gestures
iOS native XCUITest gesture APIs (tap, press(forDuration:thenDragTo:), multi-touch)
Native shell / Flutter-Linux / mobile-on-Kompose koder-vdev touch (modality #4): touch tap/multitap/drag/multidrag over a real /dev/uinput multitouch device on the s.khost1 seat

The native end-to-end path (koder-vdev) is the one that proves the REAL touch stack (compositor → toolkit → app), the same way the pointer driver supersedes the animation debug hook. FlutterComposeXCUITest drive the widget layer where a device seat isn't available.

R3 — Determinism & isolation

  • Absolute, fixed contact coordinates (mapped from the target's center) —

    reproducible hits (synthetic-input-and-virtual-devices.kmd § R3).

  • Parallel multi-finger frames are emitted atomically (all contacts in one

    SYN_REPORT) so timing is deterministic, not finger-by-finger races.

  • The koder-vdev path runs on s.khost1, never the owner's laptop

    (a real multitouch device would hijack the seat — spec § R6).

  • Each gesture emits a trigger marker (AV timeline) so the gesture's

    visualaudio response can be cross-checked (animationaudio parity).

R4 — Generation contract

/k-test MUST emit the gesture suite when a module has a touch surface:

  • koder.toml [ui] with surface/surfaces including mobile (Flutter

    Android/iOS) or touch = true.

  • A pubspec.yaml with flutter + an app/mobile/ target.
  • An Android AndroidManifest.xml with a touchscreen <activity>.
  • A native shell declaring touch input.

Generated tests live under <module>/tests/regression/gesture/, named <interaction>-<category>.{dart,kt,swift,sh}. The generator scaffolds the per-surface driver (R2) and, for the native path, the koder-vdev calls + the assertion on the surface's observable response. At minimum every mobile surface gets G1 (tap) and, for any scrollable surface, G2 (drag) — the latter shared with visual-regression Category E.

R5 — Run cadence

Trigger Categories Where On failure
Pre-commit (/k-commit) G1 + G2 --fast (widget-level) laptop/CI Block commit
Nightly CI G1–G7 (widget) + native G1–G5 via koder-vdev s.khost1 / emulator Open ticket auto
Pre-release (/k-ship) G1–G7 all surfaces s.khost1 / emulator Block release

R6 — Accessibility

  • Every gesture-only action MUST have a non-gesture alternative where a11y

    requires it (a pinch-zoom surface also offers buttons; a swipe-to-delete also offers a menu). The gesture suite asserts the alternative exists for flagged actions.

  • Long-press / multi-finger timings respect the platform a11y timing

    settings.

R7 — Forbidden / anti-patterns

  • ❌ Asserting a gesture by sending events and checking nothing about the

    response — assert the surface's observable reaction (R1).

  • ❌ Emitting multi-finger contacts finger-by-finger across frames when the

    gesture is parallel — one frame per step, all contacts (R3).

  • ❌ Running the koder-vdev native path on the owner's laptop (R3/§R6).
  • ❌ Shipping a gesture-handler fix without a gesture TDD that would have

    caught it (policies/regression-tests.kmd).

Tests of the test contract

ID Test
T1 A Flutter app/mobile/ module gets tests/regression/gesture/ populated by /k-test --gen-only (≥ G1 tap + G2 drag).
T2 G1: a tap on a button fires its action exactly once; a dead control fails.
T3 G2: a drag scrolls a long list to the bottom; the last item becomes reachable (ties to visual-regression Category E).
T4 G3: a two-finger tap fires the two-finger binding and is NOT counted as two single taps.
T5 G4: a two-finger PARALLEL drag pans the surface; both contacts move together (verified via the koder-vdev multidrag read-back on s.khost1).
T6 G5: a two-finger pinch-out raises the zoom level monotonically and clamps at max.
T7 G7: a 3 px wobble during a tap is resolved as a tap, not a drag.
T8 Native path: koder-vdev touch multidrag injects parallel contacts proven by BTNTOUCH + per-slot ABSMTTRACKINGID/POSITION read-back (s.khost1).

Relationship to existing specs

  • Driven by synthetic-input-and-virtual-devices.kmd modality #4 (the

    koder-vdev touch driver — single/multi-finger tap + drag + parallel multidrag, implemented KSTACK-183).

  • Feeds visual-regression-tdds.kmd Category E — the G2 drag is the

    scroll-reachability driver on touch surfaces.

  • Cross-checks animation-parity-tdds.kmd / audio-parity-tdds.kmd

    via the shared trigger marker (a gesture's visual/audio response).

  • Respects app-layout/safe-area.kmd gesture insets (G6).
  • Bound by policies/regression-tests.kmd + heavy-work-isolation.kmd.

R8 — Existing gaps (snapshot at ratification)

At ratification (2026-06-07) no Koder mobile surface ships a gesture suite. The native driver (koder-vdev touch, modality #4) exists and is validated on s.khost1 (KSTACK-183 — tapmultitapdrag/multidrag + selftest read-back); the per-surface FlutterComposeXCUITest emitters and the /k-test wiring are the remaining build-out. Track closure in registries/synthetic-input-coverage.md (#4) + the per-module gesture coverage.

Open follow-ups

  • /k-test gesture emitters per surface (FlutterComposeXCUITest/native).
  • Wire G2 as the touch-surface driver for visual-regression Category E.
  • Android-emulator lane on s.khost1 for the device-level G1–G7.