appium-webdriveragent 16.12.1 → 16.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [16.12.2](https://github.com/appium/WebDriverAgent/compare/v16.12.1...v16.12.2) (2026-09-03)
2
+
3
+ ### Bug Fixes
4
+
5
+ * allow pause action items to appear before any pointer movement ([#1246](https://github.com/appium/WebDriverAgent/issues/1246)) ([282478a](https://github.com/appium/WebDriverAgent/commit/282478a2e631501fd904b0675717123c779d9b84))
6
+
1
7
  ## [16.12.1](https://github.com/appium/WebDriverAgent/compare/v16.12.0...v16.12.1) (2026-09-01)
2
8
 
3
9
  ### Miscellaneous Chores
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.12.1</string>
18
+ <string>16.12.2</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.12.1</string>
22
+ <string>16.12.2</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -133,7 +133,8 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
133
133
  }
134
134
  self.duration = durationObj.doubleValue;
135
135
  XCUICoordinate *position = [self positionWithError:error];
136
- if (nil == position) {
136
+ // A pause may legally have no position yet (nil position, no error set)
137
+ if (nil == position && error && nil != *error) {
137
138
  return nil;
138
139
  }
139
140
  self.atPosition = position;
@@ -143,7 +144,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
143
144
 
144
145
  - (nullable XCUICoordinate *)positionWithError:(NSError **)error
145
146
  {
146
- if (nil == self.previousItem) {
147
+ if (nil == self.previousItem || nil == self.previousItem.atPosition) {
147
148
  NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE];
148
149
  if (error) {
149
150
  *error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
@@ -205,9 +206,19 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
205
206
  currentItemIndex:(NSUInteger)currentItemIndex
206
207
  error:(NSError **)error
207
208
  {
208
- if (nil != eventPath && currentItemIndex == 1) {
209
+ if (nil != eventPath && currentItemIndex >= 1) {
209
210
  FBW3CGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1];
210
- if ([preceedingItem isKindOfClass:FBPointerMoveItem.class]) {
211
+ // Only skip creating a new touch if the preceding pointerMove is the one that
212
+ // implicitly opened this touch, i.e. nothing but (possibly zero-duration) pauses
213
+ // came before it. Pauses never create an event path themselves.
214
+ BOOL isPreceedingMoveTheFirstRealItem = YES;
215
+ for (NSInteger index = (NSInteger)currentItemIndex - 2; index >= 0; index--) {
216
+ if (![[allItems objectAtIndex:index] isKindOfClass:FBPointerPauseItem.class]) {
217
+ isPreceedingMoveTheFirstRealItem = NO;
218
+ break;
219
+ }
220
+ }
221
+ if ([preceedingItem isKindOfClass:FBPointerMoveItem.class] && isPreceedingMoveTheFirstRealItem) {
211
222
  return @[];
212
223
  }
213
224
  }
@@ -280,7 +291,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
280
291
  }
281
292
 
282
293
  // origin == FB_ORIGIN_TYPE_POINTER
283
- if (nil == self.previousItem) {
294
+ if (nil == self.previousItem || nil == self.previousItem.atPosition) {
284
295
  NSString *errorDescription = [NSString stringWithFormat:@"There is no previous item for '%@' action item, however %@ is set to '%@'", self.actionItem, FB_ACTION_ITEM_KEY_ORIGIN, FB_ORIGIN_TYPE_POINTER];
285
296
  if (error) {
286
297
  *error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
@@ -320,6 +331,13 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
320
331
  return FB_ACTION_ITEM_TYPE_PAUSE;
321
332
  }
322
333
 
334
+ - (nullable XCUICoordinate *)positionWithError:(NSError **)error
335
+ {
336
+ // A pause has no position of its own; proxy whatever real move preceded
337
+ // it, or nil (not a fabricated point) if none has run yet
338
+ return self.previousItem.atPosition;
339
+ }
340
+
323
341
  - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
324
342
  allItems:(NSArray *)allItems
325
343
  currentItemIndex:(NSUInteger)currentItemIndex
@@ -120,5 +120,40 @@
120
120
  [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
121
121
  }
122
122
 
123
+ - (void)testTwoFingersTapWithLeadingZeroDurationPause
124
+ {
125
+ // Selenium clients pad shorter action sequences with a zero-duration pause
126
+ // so that all pointers/devices end up with the same number of ticks
127
+ XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName];
128
+ NSArray<NSDictionary<NSString *, id> *> *gesture =
129
+ @[
130
+ @{
131
+ @"type": @"pointer",
132
+ @"id": @"finger1",
133
+ @"parameters": @{@"pointerType": @"touch"},
134
+ @"actions": @[
135
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
136
+ @{@"type": @"pointerDown"},
137
+ @{@"type": @"pause", @"duration": @100},
138
+ @{@"type": @"pointerUp"},
139
+ ],
140
+ },
141
+ @{
142
+ @"type": @"pointer",
143
+ @"id": @"finger2",
144
+ @"parameters": @{@"pointerType": @"touch"},
145
+ @"actions": @[
146
+ @{@"type": @"pause", @"duration": @0},
147
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
148
+ @{@"type": @"pointerDown"},
149
+ @{@"type": @"pause", @"duration": @100},
150
+ @{@"type": @"pointerUp"},
151
+ ],
152
+ },
153
+ ];
154
+
155
+ [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
156
+ }
157
+
123
158
  @end
124
159
 
@@ -17,6 +17,10 @@
17
17
  #import "XCUIDevice+FBRotation.h"
18
18
  #import "FBRunLoopSpinner.h"
19
19
  #import "FBXCodeCompatibility.h"
20
+ #import "FBW3CActionsSynthesizer.h"
21
+ #import "XCSynthesizedEventRecord.h"
22
+ #import "XCPointerEventPath.h"
23
+ #import "XCPointerEvent.h"
20
24
 
21
25
  @interface FBW3CTouchActionsIntegrationTestsPart1 : FBIntegrationTestCase
22
26
  @end
@@ -185,14 +189,13 @@
185
189
  },
186
190
  ],
187
191
 
188
- // Chain element where action items start with an incorrect item
192
+ // Chain element where pointerMove action item does not contain coordinates
189
193
  @[@{
190
194
  @"type": @"pointer",
191
195
  @"id": @"finger1",
192
196
  @"parameters": @{@"pointerType": @"touch"},
193
197
  @"actions": @[
194
- @{@"type": @"pause", @"duration": @100},
195
- @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
198
+ @{@"type": @"pointerMove", @"duration": @0},
196
199
  @{@"type": @"pointerDown"},
197
200
  @{@"type": @"pause", @"duration": @100},
198
201
  @{@"type": @"pointerUp"},
@@ -200,13 +203,13 @@
200
203
  },
201
204
  ],
202
205
 
203
- // Chain element where pointerMove action item does not contain coordinates
206
+ // Chain element where pointerMove action item cannot use coordinates of the previous item
204
207
  @[@{
205
208
  @"type": @"pointer",
206
209
  @"id": @"finger1",
207
210
  @"parameters": @{@"pointerType": @"touch"},
208
211
  @"actions": @[
209
- @{@"type": @"pointerMove", @"duration": @0},
212
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
210
213
  @{@"type": @"pointerDown"},
211
214
  @{@"type": @"pause", @"duration": @100},
212
215
  @{@"type": @"pointerUp"},
@@ -214,34 +217,51 @@
214
217
  },
215
218
  ],
216
219
 
217
- // Chain element where pointerMove action item cannot use coordinates of the previous item
220
+ // Chain element where action items contains negative duration
218
221
  @[@{
219
222
  @"type": @"pointer",
220
223
  @"id": @"finger1",
221
224
  @"parameters": @{@"pointerType": @"touch"},
222
225
  @"actions": @[
223
- @{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
226
+ @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
224
227
  @{@"type": @"pointerDown"},
225
- @{@"type": @"pause", @"duration": @100},
228
+ @{@"type": @"pause", @"duration": @-100},
226
229
  @{@"type": @"pointerUp"},
227
230
  ],
228
231
  },
229
232
  ],
230
233
 
231
- // Chain element where action items contains negative duration
234
+ // Chain element where a leading pause is followed directly by pointerDown,
235
+ // with no real pointerMove ever establishing a position
232
236
  @[@{
233
237
  @"type": @"pointer",
234
238
  @"id": @"finger1",
235
239
  @"parameters": @{@"pointerType": @"touch"},
236
240
  @"actions": @[
237
- @{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
241
+ @{@"type": @"pause", @"duration": @0},
238
242
  @{@"type": @"pointerDown"},
239
- @{@"type": @"pause", @"duration": @-100},
243
+ @{@"type": @"pause", @"duration": @100},
240
244
  @{@"type": @"pointerUp"},
241
245
  ],
242
246
  },
243
247
  ],
244
-
248
+
249
+ // Chain element where a leading pause is followed directly by a relative
250
+ // pointerMove, with no real preceding position to be relative to
251
+ @[@{
252
+ @"type": @"pointer",
253
+ @"id": @"finger1",
254
+ @"parameters": @{@"pointerType": @"touch"},
255
+ @"actions": @[
256
+ @{@"type": @"pause", @"duration": @0},
257
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
258
+ @{@"type": @"pointerDown"},
259
+ @{@"type": @"pause", @"duration": @100},
260
+ @{@"type": @"pointerUp"},
261
+ ],
262
+ },
263
+ ],
264
+
245
265
  // Chain element where action items start with an incorrect one, because the correct one is canceled
246
266
  @[@{
247
267
  @"type": @"pointer",
@@ -299,6 +319,69 @@
299
319
  [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
300
320
  }
301
321
 
322
+ - (void)testLeadingZeroDurationPauseDoesNotAddExtraTouch
323
+ {
324
+ // A leading pause must not defeat the down-after-move dedup logic in
325
+ // FBPointerDownItem and make WDA synthesize a second, separate touch-down
326
+ // for the same finger. Inspect the actual synthesized XCTest event stream
327
+ // (without dispatching it) rather than only checking the gesture's visible
328
+ // side effect, since a duplicate touch at the same point may still produce
329
+ // the same visible outcome.
330
+ XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName];
331
+ NSDictionary<NSString *, id> *(^sequenceWithLeadingPause)(BOOL) = ^NSDictionary<NSString *, id> *(BOOL withLeadingPause) {
332
+ NSMutableArray<NSDictionary<NSString *, id> *> *actions = [NSMutableArray array];
333
+ if (withLeadingPause) {
334
+ [actions addObject:@{@"type": @"pause", @"duration": @0}];
335
+ }
336
+ [actions addObjectsFromArray:@[
337
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
338
+ @{@"type": @"pointerDown"},
339
+ @{@"type": @"pause", @"duration": @100},
340
+ @{@"type": @"pointerUp"},
341
+ ]];
342
+ return @{
343
+ @"type": @"pointer",
344
+ @"id": @"finger1",
345
+ @"parameters": @{@"pointerType": @"touch"},
346
+ @"actions": actions.copy,
347
+ };
348
+ };
349
+
350
+ NSError *error;
351
+ FBW3CActionsSynthesizer *baselineSynthesizer =
352
+ [[FBW3CActionsSynthesizer alloc] initWithActions:@[sequenceWithLeadingPause(NO)]
353
+ forApplication:self.testedApplication
354
+ elementCache:nil
355
+ error:&error];
356
+ XCTAssertNotNil(baselineSynthesizer);
357
+ XCSynthesizedEventRecord *baselineRecord = [baselineSynthesizer synthesizeWithError:&error];
358
+ XCTAssertNotNil(baselineRecord, @"%@", error);
359
+
360
+ FBW3CActionsSynthesizer *pausedSynthesizer =
361
+ [[FBW3CActionsSynthesizer alloc] initWithActions:@[sequenceWithLeadingPause(YES)]
362
+ forApplication:self.testedApplication
363
+ elementCache:nil
364
+ error:&error];
365
+ XCTAssertNotNil(pausedSynthesizer);
366
+ XCSynthesizedEventRecord *pausedRecord = [pausedSynthesizer synthesizeWithError:&error];
367
+ XCTAssertNotNil(pausedRecord, @"%@", error);
368
+
369
+ XCTAssertEqual(baselineRecord.eventPaths.count, (NSUInteger)1);
370
+ XCTAssertEqual(pausedRecord.eventPaths.count, baselineRecord.eventPaths.count);
371
+
372
+ XCPointerEventPath *baselinePath = baselineRecord.eventPaths.firstObject;
373
+ XCPointerEventPath *pausedPath = pausedRecord.eventPaths.firstObject;
374
+ XCTAssertEqual(pausedPath.pointerEvents.count, baselinePath.pointerEvents.count);
375
+ for (NSUInteger i = 0; i < baselinePath.pointerEvents.count; i++) {
376
+ XCPointerEvent *baselineEvent = baselinePath.pointerEvents[i];
377
+ XCPointerEvent *pausedEvent = pausedPath.pointerEvents[i];
378
+ XCTAssertEqual(pausedEvent.eventType, baselineEvent.eventType);
379
+ XCTAssertEqualWithAccuracy(pausedEvent.offset, baselineEvent.offset, 0.001);
380
+ XCTAssertEqualWithAccuracy(pausedEvent.coordinate.x, baselineEvent.coordinate.x, 0.001);
381
+ XCTAssertEqualWithAccuracy(pausedEvent.coordinate.y, baselineEvent.coordinate.y, 0.001);
382
+ }
383
+ }
384
+
302
385
  - (void)testDoubleTap
303
386
  {
304
387
  NSArray<NSDictionary<NSString *, id> *> *gesture =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.12.1",
3
+ "version": "16.12.2",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",