@applitools/driver 1.8.16 → 1.9.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.
@@ -35,6 +35,7 @@ function parseCapabilities(capabilities, customConfig) {
35
35
  if (info.isNative) {
36
36
  info.pixelRatio = capabilities.pixelRatio;
37
37
  info.statusBarHeight = capabilities.statBarHeight;
38
+ info.displaySize = extractDisplaySize(capabilities);
38
39
  }
39
40
  return info;
40
41
  }
@@ -66,3 +67,11 @@ function isIOS(capabilities) {
66
67
  function isAndroid(capabilities) {
67
68
  return /Android/i.test(capabilities.platformName) || /Android/i.test(capabilities.browserName);
68
69
  }
70
+ function extractDisplaySize(capabilities) {
71
+ if (!capabilities.deviceScreenSize)
72
+ return undefined;
73
+ const [width, height] = capabilities.deviceScreenSize.split('x');
74
+ if (Number.isNaN(Number(width)) || Number.isNaN(Number(height)))
75
+ return undefined;
76
+ return { width: Number(width), height: Number(height) };
77
+ }
package/dist/driver.js CHANGED
@@ -144,72 +144,87 @@ class Driver {
144
144
  this._currentContext = context;
145
145
  }
146
146
  async init() {
147
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
148
- var _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
147
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
148
+ var _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8;
149
149
  const capabilities = await ((_b = (_a = this._spec).getCapabilities) === null || _b === void 0 ? void 0 : _b.call(_a, this.target));
150
150
  this._logger.log('Driver capabilities', capabilities);
151
151
  const capabilitiesInfo = capabilities ? (0, capabilities_1.parseCapabilities)(capabilities, this._customConfig) : undefined;
152
152
  const driverInfo = await ((_d = (_c = this._spec).getDriverInfo) === null || _d === void 0 ? void 0 : _d.call(_c, this.target));
153
153
  this._driverInfo = { ...capabilitiesInfo, ...driverInfo };
154
154
  if (this.isWeb) {
155
- (_e = (_w = this._driverInfo).pixelRatio) !== null && _e !== void 0 ? _e : (_w.pixelRatio = await this.execute(snippets.getPixelRatio));
156
- (_f = (_x = this._driverInfo).viewportScale) !== null && _f !== void 0 ? _f : (_x.viewportScale = await this.execute(snippets.getViewportScale));
157
- (_g = (_y = this._driverInfo).userAgent) !== null && _g !== void 0 ? _g : (_y.userAgent = await this.execute(snippets.getUserAgent));
155
+ (_e = (_x = this._driverInfo).pixelRatio) !== null && _e !== void 0 ? _e : (_x.pixelRatio = await this.execute(snippets.getPixelRatio));
156
+ (_f = (_y = this._driverInfo).viewportScale) !== null && _f !== void 0 ? _f : (_y.viewportScale = await this.execute(snippets.getViewportScale));
157
+ (_g = (_z = this._driverInfo).userAgent) !== null && _g !== void 0 ? _g : (_z.userAgent = await this.execute(snippets.getUserAgent));
158
158
  if (this._driverInfo.userAgent) {
159
159
  const userAgentInfo = (0, user_agent_1.parseUserAgent)(this._driverInfo.userAgent);
160
160
  this._driverInfo.browserName = (_h = userAgentInfo.browserName) !== null && _h !== void 0 ? _h : this._driverInfo.browserName;
161
161
  this._driverInfo.browserVersion = (_j = userAgentInfo.browserVersion) !== null && _j !== void 0 ? _j : this._driverInfo.browserVersion;
162
162
  if (this._driverInfo.isMobile) {
163
- (_k = (_z = this._driverInfo).platformName) !== null && _k !== void 0 ? _k : (_z.platformName = userAgentInfo.platformName);
164
- (_l = (_0 = this._driverInfo).platformVersion) !== null && _l !== void 0 ? _l : (_0.platformVersion = userAgentInfo.platformVersion);
163
+ (_k = (_0 = this._driverInfo).platformName) !== null && _k !== void 0 ? _k : (_0.platformName = userAgentInfo.platformName);
164
+ (_l = (_1 = this._driverInfo).platformVersion) !== null && _l !== void 0 ? _l : (_1.platformVersion = userAgentInfo.platformVersion);
165
165
  }
166
166
  else {
167
167
  this._driverInfo.platformName = (_m = userAgentInfo.platformName) !== null && _m !== void 0 ? _m : this._driverInfo.platformName;
168
168
  this._driverInfo.platformVersion = (_o = userAgentInfo.platformVersion) !== null && _o !== void 0 ? _o : this._driverInfo.platformVersion;
169
169
  }
170
170
  }
171
- (_p = (_1 = this._driverInfo).features) !== null && _p !== void 0 ? _p : (_1.features = {});
172
- (_q = (_2 = this._driverInfo.features).allCookies) !== null && _q !== void 0 ? _q : (_2.allCookies = /chrome/i.test(this._driverInfo.browserName) && !this._driverInfo.isMobile);
171
+ (_p = (_2 = this._driverInfo).features) !== null && _p !== void 0 ? _p : (_2.features = {});
172
+ (_q = (_3 = this._driverInfo.features).allCookies) !== null && _q !== void 0 ? _q : (_3.allCookies = /chrome/i.test(this._driverInfo.browserName) && !this._driverInfo.isMobile);
173
173
  }
174
174
  else {
175
- const displaySize = await this.getDisplaySize();
175
+ // this value always excludes the height of the navigation bar, and sometimes it also excludes the height of the status bar
176
+ let windowSize = await this._spec.getWindowSize(this.target);
177
+ (_r = (_4 = this._driverInfo).displaySize) !== null && _r !== void 0 ? _r : (_4.displaySize = windowSize);
176
178
  const orientation = await this.getOrientation();
177
- // calculate status and navigation bars sizes
178
179
  if (this.isAndroid) {
179
180
  // bar sizes could be extracted only on android
180
- const barsSize = await ((_s = (_r = this._spec).getBarsSize) === null || _s === void 0 ? void 0 : _s.call(_r, this.target).catch(() => undefined));
181
+ const barsSize = await ((_t = (_s = this._spec).getBarsSize) === null || _t === void 0 ? void 0 : _t.call(_s, this.target).catch(() => undefined));
181
182
  if (barsSize) {
182
183
  this._logger.log('Driver bars size', barsSize);
183
184
  // navigation bar height is replaced with the width in landscape orientation on android (due to the bug in appium)
184
185
  if (orientation === 'landscape')
185
186
  barsSize.navigationBarHeight = barsSize.navigationBarWidth;
186
187
  // when status bar is overlapping content on android it returns status bar height equal to viewport height
187
- if (barsSize.statusBarHeight / this.pixelRatio < displaySize.height) {
188
- this._driverInfo.statusBarHeight = Math.max((_t = this._driverInfo.statusBarHeight) !== null && _t !== void 0 ? _t : 0, barsSize.statusBarHeight);
188
+ if (barsSize.statusBarHeight < this._driverInfo.displaySize.height) {
189
+ this._driverInfo.statusBarHeight = Math.max((_u = this._driverInfo.statusBarHeight) !== null && _u !== void 0 ? _u : 0, barsSize.statusBarHeight);
189
190
  }
190
191
  // when navigation bar is invisible on android it returns navigation bar height equal to viewport height
191
- if (barsSize.navigationBarHeight / this.pixelRatio < displaySize.height) {
192
- this._driverInfo.navigationBarHeight = Math.max((_u = this._driverInfo.navigationBarHeight) !== null && _u !== void 0 ? _u : 0, barsSize.navigationBarHeight);
193
- }
194
- else {
195
- // if navigation bar height is bigger then display height he can use it to reset display height
196
- displaySize.height = barsSize.navigationBarHeight / this.pixelRatio;
192
+ if (barsSize.navigationBarHeight < this._driverInfo.displaySize.height) {
193
+ this._driverInfo.navigationBarHeight = Math.max((_v = this._driverInfo.navigationBarHeight) !== null && _v !== void 0 ? _v : 0, barsSize.navigationBarHeight);
197
194
  }
198
195
  // bar heights have to be scaled on android
199
- (_3 = this._driverInfo).statusBarHeight && (_3.statusBarHeight = this._driverInfo.statusBarHeight / this.pixelRatio);
200
- (_4 = this._driverInfo).navigationBarHeight && (_4.navigationBarHeight = this._driverInfo.navigationBarHeight / this.pixelRatio);
196
+ (_5 = this._driverInfo).statusBarHeight && (_5.statusBarHeight = this._driverInfo.statusBarHeight / this.pixelRatio);
197
+ (_6 = this._driverInfo).navigationBarHeight && (_6.navigationBarHeight = this._driverInfo.navigationBarHeight / this.pixelRatio);
201
198
  }
199
+ windowSize = utils.geometry.scale(this._driverInfo.displaySize, 1 / this.pixelRatio);
200
+ (_7 = this._driverInfo).displaySize && (_7.displaySize = utils.geometry.scale(this._driverInfo.displaySize, 1 / this.pixelRatio));
202
201
  }
203
202
  // calculate viewport size
204
203
  if (!this._driverInfo.viewportSize) {
205
- this._driverInfo.viewportSize = {
206
- width: displaySize.width,
207
- height: displaySize.height - this.statusBarHeight,
208
- };
204
+ if (this.navigationBarHeight > 1) {
205
+ if (orientation === 'landscape') {
206
+ this._driverInfo.viewportSize = {
207
+ width: this._driverInfo.displaySize.height - this.navigationBarHeight,
208
+ height: this._driverInfo.displaySize.width - this.statusBarHeight,
209
+ };
210
+ }
211
+ else {
212
+ this._driverInfo.viewportSize = {
213
+ width: this._driverInfo.displaySize.width,
214
+ height: this._driverInfo.displaySize.height - this.statusBarHeight - this.navigationBarHeight,
215
+ };
216
+ }
217
+ }
218
+ else {
219
+ this._driverInfo.viewportSize = {
220
+ width: windowSize.width,
221
+ height: windowSize.height - this.statusBarHeight,
222
+ };
223
+ }
209
224
  }
210
225
  // calculate safe area
211
226
  if (this.isIOS && !this._driverInfo.safeArea) {
212
- this._driverInfo.safeArea = { x: 0, y: 0, ...displaySize };
227
+ this._driverInfo.safeArea = { x: 0, y: 0, ...this._driverInfo.displaySize };
213
228
  const topElement = await this.element({ type: '-ios class chain', selector: '**/XCUIElementTypeNavigationBar' });
214
229
  if (topElement) {
215
230
  const topRegion = await this._spec.getElementRegion(this.target, topElement.target);
@@ -230,7 +245,7 @@ class Driver {
230
245
  : await helper_android_1.HelperAndroid.make({ spec: this._spec, driver: this, logger: this._logger });
231
246
  }
232
247
  if (this.isMobile) {
233
- (_v = (_5 = this._driverInfo).orientation) !== null && _v !== void 0 ? _v : (_5.orientation = await this.getOrientation().catch(() => undefined));
248
+ (_w = (_8 = this._driverInfo).orientation) !== null && _w !== void 0 ? _w : (_8.orientation = await this.getOrientation().catch(() => undefined));
234
249
  }
235
250
  this._logger.log('Combined driver info', this._driverInfo);
236
251
  return this;
@@ -400,19 +415,22 @@ class Driver {
400
415
  async normalizeRegion(region) {
401
416
  if (this.isWeb || !utils.types.has(this._driverInfo, ['viewportSize', 'statusBarHeight']))
402
417
  return region;
403
- const scaledRegion = this.isAndroid ? utils.geometry.scale(region, 1 / this.pixelRatio) : region;
404
- const safeRegion = this.isIOS && utils.geometry.isIntersected(scaledRegion, this._driverInfo.safeArea)
405
- ? utils.geometry.intersect(scaledRegion, this._driverInfo.safeArea)
406
- : scaledRegion;
407
- const offsetRegion = utils.geometry.offsetNegative(safeRegion, {
418
+ let normalizedRegion = region;
419
+ if (this.isAndroid) {
420
+ normalizedRegion = utils.geometry.scale(normalizedRegion, 1 / this.pixelRatio);
421
+ }
422
+ if (this.isIOS && utils.geometry.isIntersected(normalizedRegion, this._driverInfo.safeArea)) {
423
+ normalizedRegion = utils.geometry.intersect(normalizedRegion, this._driverInfo.safeArea);
424
+ }
425
+ normalizedRegion = utils.geometry.offsetNegative(normalizedRegion, {
408
426
  x: this.isAndroid && this.orientation === 'landscape' && this.platformVersion > 7 ? this.navigationBarHeight : 0,
409
427
  y: this.statusBarHeight,
410
428
  });
411
- if (offsetRegion.y < 0) {
412
- offsetRegion.height += offsetRegion.y;
413
- offsetRegion.y = 0;
429
+ if (normalizedRegion.y < 0) {
430
+ normalizedRegion.height += normalizedRegion.y;
431
+ normalizedRegion.y = 0;
414
432
  }
415
- return offsetRegion;
433
+ return normalizedRegion;
416
434
  }
417
435
  async getRegionInViewport(context, region) {
418
436
  await context.focus();
@@ -445,12 +463,6 @@ class Driver {
445
463
  else {
446
464
  this._logger.log('Extracting viewport size from native driver');
447
465
  size = await this.getDisplaySize();
448
- if (size.height > size.width) {
449
- const orientation = await this.getOrientation();
450
- if (orientation === 'landscape') {
451
- size = { width: size.height, height: size.width };
452
- }
453
- }
454
466
  size.height -= this.statusBarHeight;
455
467
  }
456
468
  this._logger.log(`Rounding viewport size using`, this._customConfig.useCeilForViewportSize ? 'ceil' : 'round');
@@ -508,9 +520,17 @@ class Driver {
508
520
  throw new Error('Failed to set viewport size!');
509
521
  }
510
522
  async getDisplaySize() {
523
+ var _a;
511
524
  if (this.isWeb && !this.isMobile)
512
525
  return;
513
- const size = await this._spec.getWindowSize(this.target);
526
+ if ((_a = this._driverInfo) === null || _a === void 0 ? void 0 : _a.displaySize) {
527
+ this._logger.log('Extracting display size from native driver using cached value');
528
+ return this._driverInfo.displaySize;
529
+ }
530
+ let size = await this._spec.getWindowSize(this.target);
531
+ if ((await this.getOrientation()) === 'landscape' && size.height > size.width) {
532
+ size = { width: size.height, height: size.width };
533
+ }
514
534
  const normalizedSize = this.isAndroid ? utils.geometry.scale(size, 1 / this.pixelRatio) : size;
515
535
  this._logger.log('Extracted and normalized display size:', normalizedSize);
516
536
  return normalizedSize;
package/dist/element.js CHANGED
@@ -88,20 +88,46 @@ class Element {
88
88
  }
89
89
  async contains(innerElement) {
90
90
  const contains = await this.withRefresh(async () => {
91
+ var _a, _b, _c, _d, _e;
92
+ var _f;
91
93
  innerElement = innerElement instanceof Element ? innerElement.target : innerElement;
92
94
  if (this.driver.isWeb) {
93
95
  this._logger.log('Checking if web element with selector', this.selector, 'contains element', innerElement);
94
96
  return false; // TODO implement a snipped for web
95
97
  }
96
98
  else {
99
+ if ((_a = this._state.containedElements) === null || _a === void 0 ? void 0 : _a.has(innerElement))
100
+ return this._state.containedElements.get(innerElement);
97
101
  this._logger.log('Checking if native element with selector', this.selector, 'contains element', innerElement);
98
102
  // appium doesn't have a way to check if an element is contained in another element, so juristic applied
99
103
  if (await this.equals(innerElement))
100
104
  return false;
101
- // if inner element region is located contained in the this element region, then it is contained
102
- const region = await this._spec.getElementRegion(this.driver.target, this.target);
105
+ // if the inner element region is contained in this element region, then it then could be assumed that the inner element is contained in this element
106
+ const contentRegion = await this.getAttribute('contentSize')
107
+ .then(data => {
108
+ const contentSize = JSON.parse(data);
109
+ return {
110
+ x: contentSize.left,
111
+ y: contentSize.top,
112
+ width: contentSize.width,
113
+ height: this.driver.isIOS
114
+ ? Math.max(contentSize.height, contentSize.scrollableOffset)
115
+ : contentSize.height + contentSize.scrollableOffset,
116
+ };
117
+ })
118
+ .catch(() => this._spec.getElementRegion(this.driver.target, this.target));
119
+ const contentSize = await ((_b = this.driver.helper) === null || _b === void 0 ? void 0 : _b.getContentSize(this));
120
+ const region = {
121
+ x: contentRegion.x,
122
+ y: contentRegion.y,
123
+ width: Math.max((_c = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width) !== null && _c !== void 0 ? _c : 0, contentRegion.width),
124
+ height: Math.max((_d = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height) !== null && _d !== void 0 ? _d : 0, contentRegion.height),
125
+ };
103
126
  const innerRegion = await this._spec.getElementRegion(this.driver.target, innerElement);
104
- return utils.geometry.contains(region, innerRegion);
127
+ const contains = utils.geometry.contains(region, innerRegion);
128
+ (_e = (_f = this._state).containedElements) !== null && _e !== void 0 ? _e : (_f.containedElements = new Map());
129
+ this._state.containedElements.set(innerElement, contains);
130
+ return contains;
105
131
  }
106
132
  });
107
133
  this._logger.log('Element with selector', this.selector, contains ? 'contains' : `doesn't contain`, innerElement);
@@ -166,19 +192,20 @@ class Element {
166
192
  else {
167
193
  this._logger.log('Extracting content size of native element with selector', this.selector);
168
194
  try {
169
- const { touchPadding, ...contentRegion } = await this.getAttribute('contentSize')
195
+ const contentRegion = await this.getAttribute('contentSize')
170
196
  .then(data => {
171
197
  const contentSize = JSON.parse(data);
172
198
  return {
173
- touchPadding: contentSize.touchPadding,
174
199
  x: contentSize.left,
175
200
  y: contentSize.top,
176
201
  width: contentSize.width,
177
- height: (this.driver.isAndroid ? contentSize.height : 0) + contentSize.scrollableOffset,
202
+ height: this.driver.isIOS
203
+ ? Math.max(contentSize.height, contentSize.scrollableOffset)
204
+ : contentSize.height + contentSize.scrollableOffset,
178
205
  };
179
206
  })
180
207
  .catch(err => {
181
- this._logger.log(`Unable to get the attribute 'contentSize' when looking up touchPadding due to the following error:`, `'${err.message}'`);
208
+ this._logger.warn(`Unable to get the attribute 'contentSize' due to the following error: '${err.message}'`);
182
209
  return this._spec.getElementRegion(this.driver.target, this.target);
183
210
  });
184
211
  this._logger.log('Extracted native content size attribute', contentRegion);
@@ -188,22 +215,12 @@ class Element {
188
215
  width: Math.max((_b = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width) !== null && _b !== void 0 ? _b : 0, contentRegion.width),
189
216
  height: Math.max((_c = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height) !== null && _c !== void 0 ? _c : 0, contentRegion.height),
190
217
  };
191
- this._touchPadding = touchPadding !== null && touchPadding !== void 0 ? touchPadding : this._touchPadding;
192
- this._logger.log('touchPadding', this._touchPadding);
193
218
  if (this.driver.isAndroid) {
194
219
  this._state.contentSize = utils.geometry.scale(this._state.contentSize, 1 / this.driver.pixelRatio);
195
220
  }
196
221
  if (contentRegion.y < this.driver.statusBarHeight) {
197
222
  this._state.contentSize.height -= this.driver.statusBarHeight - contentRegion.y;
198
223
  }
199
- // android has a bug when after extracting 'contentSize' attribute the element is being scrolled by undetermined number of pixels
200
- if (this.driver.isAndroid) {
201
- this._logger.log('Stabilizing android scroll offset');
202
- const originalScrollOffset = await this.getScrollOffset();
203
- this._state.scrollOffset = { x: -1, y: -1 };
204
- await this.scrollTo({ x: 0, y: 0 });
205
- await this.scrollTo(originalScrollOffset);
206
- }
207
224
  return this._state.contentSize;
208
225
  }
209
226
  catch (err) {
@@ -216,8 +233,22 @@ class Element {
216
233
  this._logger.log('Extracted content size', size);
217
234
  return size;
218
235
  }
236
+ async isPager() {
237
+ this._logger.log('Check if element with selector', this.selector, 'is scrollable by pages');
238
+ const isPager = await this.withRefresh(async () => {
239
+ if (this.driver.isAndroid) {
240
+ const className = await this.getAttribute('className');
241
+ return ['androidx.viewpager.widget.ViewPager'].includes(className);
242
+ }
243
+ else {
244
+ return false;
245
+ }
246
+ });
247
+ this._logger.log('Element scrollable by pages', isPager);
248
+ return isPager;
249
+ }
219
250
  async isScrollable() {
220
- this._logger.log('Check is element with selector', this.selector, 'is scrollable');
251
+ this._logger.log('Check if element with selector', this.selector, 'is scrollable');
221
252
  const isScrollable = await this.withRefresh(async () => {
222
253
  if (this.driver.isWeb) {
223
254
  return this.context.execute(snippets.isElementScrollable, [this]);
@@ -247,23 +278,22 @@ class Element {
247
278
  });
248
279
  }
249
280
  async getTouchPadding() {
250
- var _a;
251
- if (this._touchPadding == null) {
281
+ if (this._state.touchPadding == null) {
252
282
  if (this.driver.isWeb)
253
- this._touchPadding = 0;
283
+ this._state.touchPadding = 0;
254
284
  else if (this.driver.isIOS)
255
- this._touchPadding = 10;
285
+ this._state.touchPadding = 10;
256
286
  else if (this.driver.isAndroid) {
257
- const data = await this.getAttribute('contentSize')
258
- .then(JSON.parse)
287
+ const touchPadding = await this.getAttribute('contentSize')
288
+ .then(data => JSON.parse(data).touchPadding)
259
289
  .catch(err => {
260
- this._logger.log(`Unable to get the attribute 'contentSize' when looking up touchPadding due to the following error:`, `'${err.message}'`);
290
+ this._logger.warn(`Unable to get the attribute 'contentSize' when looking up 'touchPadding' due to the following error: '${err.message}'`);
261
291
  });
262
- this._touchPadding = (_a = data === null || data === void 0 ? void 0 : data.touchPadding) !== null && _a !== void 0 ? _a : 20;
263
- this._logger.log('touchPadding', this._touchPadding);
292
+ this._state.touchPadding = touchPadding !== null && touchPadding !== void 0 ? touchPadding : 20;
293
+ this._logger.log('Touch padding set:', this._state.touchPadding);
264
294
  }
265
295
  }
266
- return this._touchPadding;
296
+ return this._state.touchPadding;
267
297
  }
268
298
  async getText() {
269
299
  const text = await this.withRefresh(async () => {
@@ -279,20 +309,41 @@ class Element {
279
309
  return text;
280
310
  }
281
311
  async getAttribute(name) {
282
- if (this.driver.isWeb) {
283
- const properties = await this.context.execute(snippets.getElementProperties, [this, [name]]);
284
- return properties[name];
285
- }
286
- else {
287
- return this._spec.getElementAttribute(this.driver.target, this.target, name);
288
- }
312
+ var _a;
313
+ // we assumes that attributes are not changed during the session
314
+ if ((_a = this._state.attributes) === null || _a === void 0 ? void 0 : _a[name])
315
+ return this._state.attributes[name];
316
+ const value = await this.withRefresh(async () => {
317
+ var _a;
318
+ var _b;
319
+ if (this.driver.isWeb) {
320
+ const properties = await this.context.execute(snippets.getElementProperties, [this, [name]]);
321
+ return properties[name];
322
+ }
323
+ else {
324
+ this._logger.log(`Extracting "${name}" attribute of native element with selector`, this.selector);
325
+ const value = await this._spec.getElementAttribute(this.driver.target, this.target, name);
326
+ (_a = (_b = this._state).attributes) !== null && _a !== void 0 ? _a : (_b.attributes = {});
327
+ this._state.attributes[name] = value;
328
+ if (this.driver.isAndroid && name === 'contentSize') {
329
+ // android has a bug when after extracting 'contentSize' attribute the element is being scrolled by undetermined number of pixels
330
+ this._logger.log('Stabilizing android scroll offset');
331
+ const originalScrollOffset = await this.getScrollOffset();
332
+ await this.scrollTo({ x: 0, y: 0 }, { force: true });
333
+ await this.scrollTo(originalScrollOffset);
334
+ }
335
+ return value;
336
+ }
337
+ });
338
+ this._logger.log(`Extracted element "${name}" attribute:`, value);
339
+ return value;
289
340
  }
290
341
  async setAttribute(name, value) {
291
342
  if (this.driver.isWeb) {
292
343
  await this.context.execute(snippets.setElementAttributes, [this, { [name]: value }]);
293
344
  }
294
345
  }
295
- async scrollTo(offset) {
346
+ async scrollTo(offset, options) {
296
347
  return this.withRefresh(async () => {
297
348
  offset = utils.geometry.round({ x: Math.max(offset.x, 0), y: Math.max(offset.y, 0) });
298
349
  if (this.driver.isWeb) {
@@ -304,75 +355,112 @@ class Element {
304
355
  }
305
356
  else {
306
357
  const currentScrollOffset = await this.getScrollOffset();
307
- if (utils.geometry.equals(offset, currentScrollOffset))
358
+ if (!(options === null || options === void 0 ? void 0 : options.force) && utils.geometry.equals(offset, currentScrollOffset))
308
359
  return currentScrollOffset;
309
360
  const contentSize = await this.getContentSize();
310
361
  const scrollableRegion = await this.getClientRegion();
311
- const effectiveRegion = this.driver.isAndroid
312
- ? utils.geometry.scale(scrollableRegion, this.driver.pixelRatio)
313
- : scrollableRegion;
314
362
  const maxOffset = {
315
363
  x: Math.round(scrollableRegion.width * (contentSize.width / scrollableRegion.width - 1)),
316
364
  y: Math.round(scrollableRegion.height * (contentSize.height / scrollableRegion.height - 1)),
317
365
  };
318
366
  const requiredOffset = { x: Math.min(offset.x, maxOffset.x), y: Math.min(offset.y, maxOffset.y) };
319
- let remainingOffset = offset.x === 0 && offset.y === 0
367
+ let effectiveRegion = scrollableRegion;
368
+ let remainingOffset = utils.geometry.equals(requiredOffset, { x: 0, y: 0 })
320
369
  ? { x: -maxOffset.x, y: -maxOffset.y } // if it has to be scrolled to the very beginning, then scroll maximum amount of pixels
321
370
  : utils.geometry.offsetNegative(requiredOffset, currentScrollOffset);
322
371
  if (this.driver.isAndroid) {
323
372
  remainingOffset = utils.geometry.scale(remainingOffset, this.driver.pixelRatio);
373
+ effectiveRegion = utils.geometry.scale(scrollableRegion, this.driver.pixelRatio);
324
374
  }
325
375
  const actions = [];
326
376
  const touchPadding = await this.getTouchPadding();
327
- const xPadding = Math.max(Math.floor(effectiveRegion.width * 0.1), touchPadding);
377
+ const isPager = await this.isPager();
378
+ const xPadding = Math.max(Math.floor(effectiveRegion.width * 0.07), touchPadding);
328
379
  const yTrack = Math.floor(effectiveRegion.y + effectiveRegion.height / 2); // center
329
380
  const xLeft = effectiveRegion.y + xPadding;
330
- const xDirection = remainingOffset.y > 0 ? 'right' : 'left';
381
+ const xDirection = remainingOffset.x > 0 ? 'right' : 'left';
331
382
  const xGap = xDirection === 'right' ? -touchPadding : touchPadding;
332
383
  let xRemaining = Math.abs(remainingOffset.x);
384
+ if (isPager) {
385
+ const xPages = Math.floor(xRemaining / effectiveRegion.width);
386
+ xRemaining = (effectiveRegion.width - xPadding * 2) * xPages;
387
+ }
333
388
  while (xRemaining > 0) {
334
389
  const xRight = effectiveRegion.x + Math.min(xRemaining + xPadding, effectiveRegion.width - xPadding);
335
390
  const [xStart, xEnd] = xDirection === 'right' ? [xRight, xLeft] : [xLeft, xRight];
336
- actions.push([
337
- { action: 'press', y: yTrack, x: xStart },
338
- { action: 'wait', ms: 100 },
339
- { action: 'moveTo', y: yTrack, x: xStart + xGap },
340
- { action: 'wait', ms: 100 },
341
- { action: 'moveTo', y: yTrack, x: xEnd + xGap },
342
- { action: 'wait', ms: 100 },
343
- { action: 'moveTo', y: yTrack + 1, x: xEnd + xGap },
344
- { action: 'release' },
345
- ]);
391
+ if (isPager) {
392
+ actions.push([
393
+ { action: 'press', y: yTrack, x: xStart },
394
+ // scroll through the page
395
+ { action: 'wait', ms: 170 },
396
+ { action: 'moveTo', y: yTrack, x: xEnd },
397
+ { action: 'release' },
398
+ ]);
399
+ }
400
+ else {
401
+ actions.push([
402
+ { action: 'press', y: yTrack, x: xStart },
403
+ // move through scrolling gap (actual scrolling will be triggered only after that)
404
+ { action: 'wait', ms: 100 },
405
+ { action: 'moveTo', y: yTrack, x: xStart + xGap },
406
+ // perform actual scrolling
407
+ { action: 'wait', ms: 100 },
408
+ { action: 'moveTo', y: yTrack, x: xEnd + xGap },
409
+ // prevent inertial scrolling after release
410
+ { action: 'wait', ms: 100 },
411
+ { action: 'moveTo', y: yTrack + 1, x: xEnd + xGap },
412
+ { action: 'release' },
413
+ ]);
414
+ }
346
415
  xRemaining -= xRight - xLeft;
347
416
  }
348
- const yPadding = Math.max(Math.floor(effectiveRegion.height * 0.1), touchPadding);
417
+ const yPadding = Math.max(Math.floor(effectiveRegion.height * 0.07), touchPadding);
349
418
  const xTrack = Math.floor(effectiveRegion.x + 5); // a little bit off left border
350
419
  const yBottom = effectiveRegion.y + effectiveRegion.height - yPadding;
351
420
  const yDirection = remainingOffset.y > 0 ? 'down' : 'up';
352
421
  const yGap = yDirection === 'down' ? -touchPadding : touchPadding;
353
422
  let yRemaining = Math.abs(remainingOffset.y);
423
+ if (isPager) {
424
+ const yPages = Math.floor(yRemaining / effectiveRegion.height);
425
+ yRemaining = (effectiveRegion.height - yPadding * 2) * yPages;
426
+ }
354
427
  while (yRemaining > 0) {
355
428
  const yTop = Math.max(yBottom - yRemaining, effectiveRegion.y + yPadding);
356
429
  const [yStart, yEnd] = yDirection === 'down' ? [yBottom, yTop] : [yTop, yBottom];
357
- actions.push([
358
- { action: 'press', x: xTrack, y: yStart },
359
- { action: 'wait', ms: 100 },
360
- { action: 'moveTo', x: xTrack, y: yStart + yGap },
361
- { action: 'wait', ms: 100 },
362
- { action: 'moveTo', x: xTrack, y: yEnd + yGap },
363
- { action: 'wait', ms: 100 },
364
- { action: 'moveTo', x: xTrack + 1, y: yEnd + yGap },
365
- { action: 'release' },
366
- ]);
430
+ if (isPager) {
431
+ actions.push([
432
+ { action: 'press', x: xTrack, y: yStart },
433
+ // scroll through the page
434
+ { action: 'wait', ms: 170 },
435
+ { action: 'moveTo', x: xTrack, y: yEnd },
436
+ { action: 'release' },
437
+ ]);
438
+ }
439
+ else {
440
+ actions.push([
441
+ { action: 'press', x: xTrack, y: yStart },
442
+ // move through scrolling gap (actual scrolling will be triggered only after that)
443
+ { action: 'wait', ms: 100 },
444
+ { action: 'moveTo', x: xTrack, y: yStart + yGap },
445
+ // perform actual scrolling
446
+ { action: 'wait', ms: 100 },
447
+ { action: 'moveTo', x: xTrack, y: yEnd + yGap },
448
+ // prevent inertial scrolling after release
449
+ { action: 'wait', ms: 100 },
450
+ { action: 'moveTo', x: xTrack + 1, y: yEnd + yGap },
451
+ { action: 'release' },
452
+ ]);
453
+ }
367
454
  yRemaining -= yBottom - yTop;
368
455
  }
369
456
  // ios actions should be executed one-by-one sequentially, otherwise the result isn't stable
370
- if (this.driver.isIOS) {
457
+ // pages should be scrolled one-by-one as well
458
+ if (isPager || this.driver.isIOS) {
371
459
  for (const action of actions) {
372
460
  await this._spec.performAction(this.driver.target, action);
373
461
  }
374
462
  }
375
- else {
463
+ else if (actions.length > 0) {
376
464
  await this._spec.performAction(this.driver.target, [].concat(...actions));
377
465
  }
378
466
  const actualScrollableRegion = await this.getClientRegion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/driver",
3
- "version": "1.8.16",
3
+ "version": "1.9.2",
4
4
  "description": "Applitools universal framework wrapper",
5
5
  "keywords": [
6
6
  "applitools",
@@ -73,13 +73,13 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@applitools/logger": "1.1.8",
76
+ "@applitools/logger": "1.1.10",
77
77
  "@applitools/snippets": "2.2.3",
78
- "@applitools/types": "1.4.5",
79
- "@applitools/utils": "1.3.3"
78
+ "@applitools/types": "1.4.7",
79
+ "@applitools/utils": "1.3.6"
80
80
  },
81
81
  "devDependencies": {
82
- "@applitools/bongo": "^2.1.1",
82
+ "@applitools/bongo": "^2.1.4",
83
83
  "@types/mocha": "^9.1.1",
84
84
  "@types/node": "^17.0.31",
85
85
  "@typescript-eslint/eslint-plugin": "^5.22.0",
@@ -1,10 +1,13 @@
1
1
  import type * as types from '@applitools/types';
2
2
  import { type Logger } from '@applitools/logger';
3
3
  import { type Context } from './context';
4
- export declare type ElementState = {
4
+ export declare type ElementState<TElement> = {
5
5
  contentSize?: types.Size;
6
6
  scrollOffset?: types.Location;
7
7
  transforms?: any;
8
+ attributes?: Record<string, string>;
9
+ touchPadding?: number;
10
+ containedElements?: Map<TElement, boolean>;
8
11
  };
9
12
  export declare class Element<TDriver, TContext, TElement, TSelector> {
10
13
  private _target;
@@ -13,7 +16,6 @@ export declare class Element<TDriver, TContext, TElement, TSelector> {
13
16
  private _index;
14
17
  private _state;
15
18
  private _originalOverflow;
16
- private _touchPadding;
17
19
  private _logger;
18
20
  protected readonly _spec: types.SpecDriver<TDriver, TContext, TElement, TSelector>;
19
21
  constructor(options: {
@@ -36,21 +38,24 @@ export declare class Element<TDriver, TContext, TElement, TSelector> {
36
38
  getRegion(): Promise<types.Region>;
37
39
  getClientRegion(): Promise<types.Region>;
38
40
  getContentSize(): Promise<types.Size>;
41
+ isPager(): Promise<boolean>;
39
42
  isScrollable(): Promise<boolean>;
40
43
  isRoot(): Promise<boolean>;
41
44
  getTouchPadding(): Promise<number>;
42
45
  getText(): Promise<string>;
43
46
  getAttribute(name: string): Promise<string>;
44
47
  setAttribute(name: string, value: string): Promise<void>;
45
- scrollTo(offset: types.Location): Promise<types.Location>;
48
+ scrollTo(offset: types.Location, options?: {
49
+ force: boolean;
50
+ }): Promise<types.Location>;
46
51
  translateTo(offset: types.Location): Promise<types.Location>;
47
52
  getScrollOffset(): Promise<types.Location>;
48
53
  getTranslateOffset(): Promise<types.Location>;
49
54
  getInnerOffset(): Promise<types.Location>;
50
55
  click(): Promise<void>;
51
56
  type(value: string): Promise<void>;
52
- preserveState(): Promise<ElementState>;
53
- restoreState(state?: ElementState): Promise<void>;
57
+ preserveState(): Promise<ElementState<TElement>>;
58
+ restoreState(state?: ElementState<TElement>): Promise<void>;
54
59
  hideScrollbars(): Promise<void>;
55
60
  restoreScrollbars(): Promise<void>;
56
61
  refresh(freshElement?: TElement): Promise<boolean>;