A drag request with four ordinary numbers in it: fromX: 10, fromY: 700, toX: 10, toY: 300. It works in the first app. Switch to a second app, send the same request, and the phone goes sluggish, the call hangs, and eventually WebDriverAgent returns:
unknown error: Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY
None of those numbers is infinite.
That is appium/WebDriverAgent#1056, opened in September 2025 against WDA 9.15.3 on an iPhone 12 mini running iOS 18.5. The maintainers traced the error into Apple's own drag API and suggested reporting it to Apple or switching to W3C actions. The reporter filed it with Apple. Then it sat for a year.
I fixed it in PR #1270. It merged on 29 September 2026 and shipped in appium-webdriveragent 16.13.0 the next day. This is how, starting with the first fix I wrote, which was wrong.
fix: avoid infinite-point assertion and hang in coordinate drag gestures
#1270
Fixes #1056: /wda/dragfromtoforduration (and /wda/pressAndDragWithVelocity) can hang for a long time and then fail with unknown error: Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY when the target app is busy, e.g. right after switching applications.
Both endpoints called -[XCUICoordinate pressForDuration:thenDragToCoordinate:(withVelocity:thenHoldForDuration:)]. XCTest resolves the coordinates' screen points lazily, inside its event dispatch (_dispatchEvent:eventBuilder:): it re-snapshots the application, waits for it to become idle, and only then calls screenPoint on both coordinates and passes them to its event builder (XCUIPressHoldDragWithVelocityAndHoldEvent), which asserts that neither point is infinite. When the app is busy, that snapshot is slow (the hang) and comes back unusable, so the resolved point is INFINITY (the assertion). WDA had no chance to validate anything, because the points only existed inside XCTest's own call.
A new -[XCUIApplication fb_pressAtCoordinate:forDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:error:] (in XCUIApplication+FBTouchAction), used by both endpoints:
- Resolves both screen points once, up front, and returns a clear error if either is not finite ("Cannot resolve the on-screen position of the drag gesture … make sure it is idle and retry"). The points that are validated are exactly the ones that get sent.
- Builds the same touch event path as XCTest's own builder: touch down, hold still for the press duration, move to the end point at the drag velocity, hold, lift. The velocity mapping comes from XCTest's own
XCUIPixelsPerSecondForDragGestureVelocity(looked up at runtime), falling back to the values it currently returns (default 500, slow 250, fast 750). - Synthesizes the event directly via
FBXCTestDaemonsProxy, the same way W3C actions do, so XCTest's second app snapshot and idle wait are skipped. After the gesture it waits up toanimationCoolOffTimeout, like W3C actions. - Rejects zero/negative velocity and negative/non-finite durations with
invalid argument(400). XCTest used to raise these as a 500unknown error.
- There is no idle wait before the drag any more (that wait is where the hang came from); this matches W3C actions.
- Invalid velocity or duration values now return 400 instead of 500.
- New integration tests in
FBElementSwipingTests.m(FBPressAndDragTests):- a drag between application coordinates scrolls the list;
- a coordinate whose
screenPointisINFINITYfails cleanly with the new error and sends no gesture; - a velocity of 0 is rejected as invalid argument.
- As a check, the old
pressForDuration:thenDragToCoordinate:call with the same infinite coordinate raises exactly the error from the issue. -
WebDriverAgentLibbuilds for iOS, tvOS and watchOS simulators; the new tests pass on an iPhone 18 Pro simulator (iOS 27.0). - Manual curl run on a simulator: drag in Settings, go to the home screen, open Photos, drag again; both succeed, and invalid velocity/duration return 400.
Not reproduced: the exact busy third-party app on a real iOS 18.5 device from the issue. It would be great if the reporter could try this branch.
The first fix guarded the wrong place
WDA builds gesture coordinates in FBCoordinateWithAnchorOffset. It normalizes the client's offset against the element's wdFrame and returns an XCUICoordinate. An infinite value in a coordinate points straight at that function, so I hardened it with isfinite checks on the frame and on the computed offset.
It went nowhere, for three reasons:
- The inputs in the issue are finite. The offset check never fires.
-
The frame was already guarded.
wdFrameswaps an infinite frame for a sentinel rect(-1, -1, 0, 0), which the existingCGRectIsEmptycheck rejects. My new check could only ever catch NaN. - The infinite point is created later, inside XCTest. A check that runs while the coordinate is being built cannot see a value that does not exist yet.
I dropped it. The third reason turned out to be the whole bug.
XCUICoordinate is a recipe, not a point
An XCUICoordinate stores "this element, plus this normalized offset". It has no screen position until something reads screenPoint, and reading it means resolving the element through an accessibility snapshot.
WDA handed XCTest two of these recipes and called:
[startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
Everything after that line is Apple's code, and the docs say nothing about what it does internally. So I disassembled XCUIAutomation.framework from Xcode 27.0 with otool -tV and followed the call chain. Inside _dispatchEvent:eventBuilder:, XCTest snapshots the app, waits for it to go idle, and only then resolves both points and hands them to its private event builder, XCUIPressHoldDragWithVelocityAndHoldEvent:
After an app switch, the new app's main thread is saturated. The snapshot request crawls, which is the hang. The snapshot comes back without a usable frame, screenPoint resolves to {INFINITY, INFINITY}, the event builder asserts, and WDA's exception handler turns the NSInternalInconsistencyException into a 500.
WDA never saw a point. Both points were created and rejected inside a single XCTest call, after XCTest's own snapshot. There was no seam to validate them, time out sooner, or return a better error.
Three ways out, and why I took the expensive one
| Option | Fixes the hang | Fixes the crash | Cost |
|---|---|---|---|
| Catch XCTest's exception, return a friendly error | No | Cosmetically | Users still sit through the hang |
Pre-check screenPoint, then call XCTest |
No | Partly | XCTest re-resolves later, so it is a race, and every drag pays an extra snapshot |
| Resolve once, validate, synthesize the event directly | Yes | Yes, as a clean 400 | Must match XCTest's gesture timing exactly |
The first two are cheap and both keep the hang, which is the part users actually feel. The third has one real risk: timing drift from XCTest. Disassembly removed that risk, because I could copy Apple's builder line for line instead of approximating it.
The fix
A new method on XCUIApplication, fb_pressAtCoordinate:forDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:error:. Both /wda/dragfromtoforduration and /wda/pressAndDragWithVelocity now go through it.
The core, trimmed from the merged code:
// Resolved once, here, so the points validated are exactly the points sent.
CGPoint startPoint = startCoordinate.screenPoint;
CGPoint endPoint = endCoordinate.screenPoint;
if (!FBIsFinitePoint(startPoint) || !FBIsFinitePoint(endPoint)) {
return [[[FBErrorBuilder builder]
withDescriptionFormat:@"Cannot resolve the on-screen position of the drag gesture ..."]
buildError:error];
}
And the event builder, mirroring XCTest's private one:
XCPointerEventPath *path = [[XCPointerEventPath alloc] initForTouchAtPoint:start offset:0];
if (pressDuration > 0) {
[path moveToPoint:start atOffset:pressDuration]; // hold still
}
CGFloat distance = (CGFloat)hypot(end.x - start.x, end.y - start.y);
NSTimeInterval dragEnd = pressDuration + distance / pointsPerSecond;
[path moveToPoint:end atOffset:dragEnd]; // drag
[path liftUpAtOffset:dragEnd + holdDuration]; // hold, lift
The event goes out through FBXCTestDaemonsProxy, the same path W3C actions use. No second snapshot, no idle wait before the gesture. After the gesture it waits up to animationCoolOffTimeout, again matching W3C actions.
Two XCTest details found along the way shaped the code:
-
The velocity constants are not speeds.
XCUIGestureVelocityDefault,SlowandFastare sentinels:-10,-20,-30. XCTest maps them through an exported but undeclared function,XCUIPixelsPerSecondForDragGestureVelocity, to 500, 250 and 750. The fix looks that function up at runtime withdlsym, so a future Xcode change is picked up automatically, falls back to the measured values if it disappears, and logs once when it does. - "Pixels per second" is points per second. XCTest divides a distance measured in points by that value, with no scale factor.
What changed for clients:
| Situation | Before | After |
|---|---|---|
| Busy app after a switch | Long hang, then 500 unknown error
|
Fast 400 invalid element state telling you the app was not idle, or the drag succeeds if the single resolution works |
velocity: 0 |
500 unknown error, raised inside XCTest |
400 invalid argument
|
| Negative duration | Passed straight through to XCTest | 400 invalid argument
|
| Normal drag | Works | Works |
Making the test prove something
The simulator cannot reproduce a busy third-party app, so I built a stand-in for the broken state:
@interface FBUnresolvableCoordinate : XCUICoordinate
@end
@implementation FBUnresolvableCoordinate
- (CGPoint)screenPoint
{
return CGPointMake(INFINITY, INFINITY);
}
@end
A fake is only worth something if it breaks the old code the same way production did. So before writing the real test, I ran a throwaway one that fed this coordinate to the old XCTest API. It threw NSInternalInconsistencyException with the exact message from #1056. Only then did the fake become the regression test: the same coordinate through the new method returns NO, the expected error, and the list on screen has not moved, so no gesture was sent.
Four tests ended up in FBPressAndDragTests:
- A real drag between app coordinates scrolls the list.
- An unresolvable coordinate fails cleanly and sends nothing.
- A velocity of 0 throws
invalid argument. - The event builder keeps a non-default display ID and gets the timing right. This one came out of review.
On top of that: builds for iOS, tvOS and watchOS, and a manual curl replay of the issue on the simulator. Drag in Settings, go to the home screen, launch Photos, drag again. Both drags succeeded, and invalid velocity and duration returned 400.
Review: two maintainers, five comments
Round one, mykola-mokhnach, four comments. All code hygiene, in a codebase that builds with warnings as errors:
- No float equality comparisons. A
<= 0check on aCGFloathad to go. - Throw a proper argument error where the value enters, instead of returning a 0 sentinel and relying on XCTest to complain.
- Compute the preset comparisons once instead of twice.
- Log when the
dlsymlookup fails, so a renamed symbol does not silently change drag speeds.
Fixed in one commit, replied per comment, approved the same day.
Round two, KazuCocoa, one comment, and the one that mattered. XCTest's builder passes the start element's display ID into the event record. My version used the W3C path's initializer, which means display 0. On a secondary display, the gesture would land on the wrong screen. He had confirmed the difference by inspecting XCTest himself. The same release added multi-display screenshot support (#1267), so this would have been a real regression, not a theoretical one.
The fix kept the "resolve once" property intact. Disassembly showed -[XCUIElement displayID] just reads lastSnapshot.displayID without taking a new snapshot, and screenPoint had refreshed that snapshot a moment earlier. To test it without a second display, I pulled the event-record construction into its own class method and asserted on its output:
Approved and merged on 29 September.
Six red CI jobs, none of them mine, each one proven
The final CI run: 51 jobs passed, 6 failed. From a first-time contributor, "unrelated failures" is the least credible sentence in open source, so I read every log before saying it:
-
Functional tests, Xcode 16.4. The simulator needed four boot attempts, the runner never answered
/status, and the job was killed at the timeout. The same job passed on my first commit. - tvOS build and iPhone unit tests on the minimum Xcode. No device matched the destination specifier, exit 70, before any code ran.
- One integration job on the maximum Xcode. The runner never bootstrapped. Zero tests ran.
-
Two
int_test_3jobs. Eight failures each, all inFBVoiceOverTests, which fail intermittently on unrelated branches too.
Every new test passed wherever the runner started, including Xcode 16.4 on iOS 18.5, the reporter's OS version.
What I did not test
I said this in the PR and it belongs here too:
- The reporter's exact case. A busy third-party app on a real iOS 18.5 device. The fake coordinate covers it synthetically, not on hardware.
- A real secondary display. Covered by the builder test only.
-
originalOffset. XCTest also sets it on the event record. The fix does not. No observed difference; still an open question.
And one known gap: /wda/element/:uuid/pressAndDragWithVelocity, the element-to-element variant, still calls XCTest directly and very likely has the same weakness. That is the next PR.
What carries over
None of this is specific to iOS.
- Find where the bad value is created before you guard against it. Validation at the edge of your code is useless when the value is born inside a dependency.
- Validate the thing you actually send. Resolve once, check it, send exactly that. Anything else is a race with a nicer error message.
- A fake proves nothing until it fails the old code the way production did. Reproduce the exact error first, then trust the test.
- When the docs stop, read the binary. A maintainer and I both settled design questions by disassembling XCTest instead of arguing about what it probably does.
Most of my work is mainframe-to-cloud migration testing, where rule 2 is the entire job: compare what the new system actually emits, not what you assume it emits. Same discipline, different stack.
- PR: appium/WebDriverAgent#1270
- Issue: appium/WebDriverAgent#1056
- Release: v16.13.0



Top comments (0)