skip to content

In Appium, why does a pointerDown then pointerUp action chain tap instead of long-pressing?

level: seniorimportance: must knowfreq 58%

answer

  1. a tick is one instant
  2. nothing schedules time while pressed
  3. move duration is travel, not hold
  4. pause between down and up

basics

~20 s

Nothing in the chain holds the finger down. A pointer sequence advances one tick at a time, so a press is followed immediately by the lift. Insert a pause tick carrying the dwell between them.

solid answer

~40 s

A pointer sequence posted to `POST /session/:sessionId/actions` is a list of **ticks**, and each tick is one instant of time. `pointerDown` presses and the very next tick lifts, so contact lasts about one tick and both Android's UiAutomator2 driver and Apple's XCUITest driver report it as a tap. A long press is written by giving the finger something to do while it is down: a `pause` tick with a `duration` in milliseconds, placed between `pointerDown` and `pointerUp`. The `duration` on a `pointerMove` is travel time to the coordinate, not hold time, so approaching slowly changes nothing. If you would rather not tune the dwell yourself, each driver ships a named long-press command of its own — `mobile: longClickGesture` on Android's UiAutomator2 driver, `mobile: touchAndHold` on Apple's XCUITest driver.

code

json · 15 lines
json
{
  "actions": [
    {
      "type": "pointer",
      "id": "finger1",
      "parameters": { "pointerType": "touch" },
      "actions": [
        { "type": "pointerMove", "duration": 0, "origin": "viewport", "x": 540, "y": 1180 },
        { "type": "pointerDown", "button": 0 },
        { "type": "pause", "duration": 900 },
        { "type": "pointerUp", "button": 0 }
      ]
    }
  ]
}

go deeper

for a junior

Recall that a pointer chain is a list of ticks and that a press with a lift right behind it registers as a tap. Know that pause is the tick that spends time.

for a middle

Explain the difference between a pointerMove duration and a hold, and show exactly where the pause tick goes so the contact actually lasts.

for a senior

Diagnose this from symptoms alone: intermittent context menus, a case that only passes on a slow build. Argue for headroom over the app's own threshold rather than a number copied from a blog post.

for a principal

Decide whether gesture timing lives in one shared helper with a measured dwell or is tuned per case, and how the suite proves that changing a threshold on either platform broke nothing.

## The tick model, and why a bare press-and-release is a tap A W3C action chain is not a list of gestures; it is a list of **input sources**, and each source carries a list of **ticks**. A tick is one instant on a shared clock. The driver walks every source forward one tick at a time and dispatches whatever that source has scheduled for that instant. In Appium the whole chain arrives as a single `POST /session/:sessionId/actions` request, and this is the one gesture vocabulary Android's UiAutomator2 driver and Apple's XCUITest driver both understand, because both take a pointer source whose `pointerType` is `touch` and inject it as touch input on the device. That model is the entire answer. In the scaffolding-inspection app, long-pressing a row in the scaffold register is supposed to open the flag-defect menu. If the chain reads `pointerMove`, then `pointerDown`, then `pointerUp`, the finger arrives, presses and lifts on three consecutive instants. Nothing in the request asks the clock to stand still while the finger is down, so contact lasts only as long as the driver needs to dispatch the next tick. The app sees a tap, the row opens the scaffold detail instead, and the menu never appears. ## Where the duration actually goes The usual wrong fix is to lengthen the move, because `pointerMove` is the tick that visibly takes a `duration`: - `pointerMove` carries `x`, `y`, an `origin` and a `duration`, and that duration is **travel time** — how long the pointer takes to interpolate from where it is to the target. It is spent **before** contact begins. - `pointerDown` and `pointerUp` carry a `button` and no duration at all. They are instantaneous state changes. - `pause` carries a `duration` and nothing else. It is the only tick that spends time while the source holds its current state, pressed or released. So a `pointerMove` with `duration` of 1000 followed immediately by `pointerDown` and `pointerUp` buys you a slow approach and a fast tap. The coordinates are right and the timing is not. ## Writing the dwell 1. `pointerMove` to the target, with `origin` set to the element or the viewport and any travel `duration` you like — zero is fine for a press in place. 2. `pointerDown` with `button` 0. 3. `pause` with `duration` set to the hold the app needs, in milliseconds. 4. `pointerUp` with `button` 0. The whole chain still goes in one request. A sleep in your test code between two separate `POST /session/:sessionId/actions` calls does **not** hold the finger down: each request is a complete sequence, and the pointer has already been lifted by the time the second one is sent. The dwell has to live inside the sequence, as a tick. ## The four ticks at a glance | tick | carries | effect | |---|---|---| | `pointerMove` | `x`, `y`, `origin`, `duration` | interpolates the pointer to a point; the duration is travel | | `pointerDown` | `button` | begins contact, instantaneously | | `pause` | `duration` | spends time in the state the source is already in | | `pointerUp` | `button` | ends contact, instantaneously | ## What each platform contributes The chain is portable; the **threshold** is not. How long contact must last before a press counts as a long press belongs to the platform and to the app under test, not to the driver, so a dwell measured against the scaffolding-inspection app on an Android device is worth re-measuring on an Apple one rather than assumed. Give yourself headroom over the app's threshold instead of sitting on it, because a loaded device can shave real time off a nominal pause. If you would rather not own that number at all, both drivers offer a named long-press command posted to `POST /session/:sessionId/execute/sync` — `mobile: longClickGesture` on Android's UiAutomator2 driver, `mobile: touchAndHold` on Apple's XCUITest driver — at the cost of maintaining two branches instead of one portable chain. ## Symptoms this explains - A long-press case that passes on a slow debug build and fails on a fast release build: the missing dwell was being supplied accidentally by device lag. - A context menu that opens roughly one run in ten, on one device model. - A drag that starts from the wrong place, because the app never registered the press that should have begun it. - A chain that "works when I step through it": the debugger supplies the pause that the payload does not. - A case that only passes on a heavily loaded CI device, which is the same accident in a different disguise. The repair is always the same shape. Model the gesture as time, not as a list of positions, and put every millisecond you need into the sequence itself.

  • Where do the pause ticks belong in a press, drag, then release gesture?
    One after `pointerDown`, so the app registers the press before anything moves; then the `pointerMove` ticks that carry the drag; then usually a second `pause` before `pointerUp` so the app settles at the drop point. All of them sit in the same source's tick list, in one request.
  • Why does a sleep in test code between two actions requests not produce a long press?
    Each request is a complete sequence. The driver dispatches its ticks and the pointer is already up when the call returns, so the sleep sits between two taps rather than inside one contact. A dwell only counts when it is a `pause` tick within the sequence that pressed.

saying these in an interview costs you the question

  • Says the pointerMove duration sets how long the press lasts
  • Adds a client-side sleep between two separate actions requests
  • Assumes the driver inserts a default hold between ticks
  • Increases the move distance to make the press longer
  • Blames device flake instead of the missing pause tick