@applitools/driver 1.8.16 → 1.9.0

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,78 @@ 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();
176
- const orientation = await this.getOrientation();
177
- // calculate status and navigation bars sizes
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);
178
178
  if (this.isAndroid) {
179
179
  // 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));
180
+ const barsSize = await ((_t = (_s = this._spec).getBarsSize) === null || _t === void 0 ? void 0 : _t.call(_s, this.target).catch(() => undefined));
181
181
  if (barsSize) {
182
182
  this._logger.log('Driver bars size', barsSize);
183
183
  // navigation bar height is replaced with the width in landscape orientation on android (due to the bug in appium)
184
- if (orientation === 'landscape')
184
+ if ((await this.getOrientation()) === 'landscape')
185
185
  barsSize.navigationBarHeight = barsSize.navigationBarWidth;
186
186
  // 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);
187
+ if (barsSize.statusBarHeight < this._driverInfo.displaySize.height) {
188
+ this._driverInfo.statusBarHeight = Math.max((_u = this._driverInfo.statusBarHeight) !== null && _u !== void 0 ? _u : 0, barsSize.statusBarHeight);
189
189
  }
190
190
  // 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;
191
+ if (barsSize.navigationBarHeight < this._driverInfo.displaySize.height) {
192
+ this._driverInfo.navigationBarHeight = Math.max((_v = this._driverInfo.navigationBarHeight) !== null && _v !== void 0 ? _v : 0, barsSize.navigationBarHeight);
197
193
  }
198
194
  // 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);
195
+ (_5 = this._driverInfo).statusBarHeight && (_5.statusBarHeight = this._driverInfo.statusBarHeight / this.pixelRatio);
196
+ (_6 = this._driverInfo).navigationBarHeight && (_6.navigationBarHeight = this._driverInfo.navigationBarHeight / this.pixelRatio);
201
197
  }
198
+ windowSize = utils.geometry.scale(this._driverInfo.displaySize, 1 / this.pixelRatio);
199
+ (_7 = this._driverInfo).displaySize && (_7.displaySize = utils.geometry.scale(this._driverInfo.displaySize, 1 / this.pixelRatio));
202
200
  }
203
201
  // calculate viewport size
204
202
  if (!this._driverInfo.viewportSize) {
205
- this._driverInfo.viewportSize = {
206
- width: displaySize.width,
207
- height: displaySize.height - this.statusBarHeight,
208
- };
203
+ if (this.navigationBarHeight > 1) {
204
+ this._driverInfo.viewportSize = {
205
+ width: this._driverInfo.displaySize.width,
206
+ height: this._driverInfo.displaySize.height - this.statusBarHeight - this.navigationBarHeight,
207
+ };
208
+ }
209
+ else {
210
+ this._driverInfo.viewportSize = {
211
+ width: windowSize.width,
212
+ height: windowSize.height - this.statusBarHeight,
213
+ };
214
+ }
209
215
  }
210
216
  // calculate safe area
211
217
  if (this.isIOS && !this._driverInfo.safeArea) {
212
- this._driverInfo.safeArea = { x: 0, y: 0, ...displaySize };
218
+ this._driverInfo.safeArea = { x: 0, y: 0, ...this._driverInfo.displaySize };
213
219
  const topElement = await this.element({ type: '-ios class chain', selector: '**/XCUIElementTypeNavigationBar' });
214
220
  if (topElement) {
215
221
  const topRegion = await this._spec.getElementRegion(this.target, topElement.target);
@@ -230,7 +236,7 @@ class Driver {
230
236
  : await helper_android_1.HelperAndroid.make({ spec: this._spec, driver: this, logger: this._logger });
231
237
  }
232
238
  if (this.isMobile) {
233
- (_v = (_5 = this._driverInfo).orientation) !== null && _v !== void 0 ? _v : (_5.orientation = await this.getOrientation().catch(() => undefined));
239
+ (_w = (_8 = this._driverInfo).orientation) !== null && _w !== void 0 ? _w : (_8.orientation = await this.getOrientation().catch(() => undefined));
234
240
  }
235
241
  this._logger.log('Combined driver info', this._driverInfo);
236
242
  return this;
@@ -400,19 +406,22 @@ class Driver {
400
406
  async normalizeRegion(region) {
401
407
  if (this.isWeb || !utils.types.has(this._driverInfo, ['viewportSize', 'statusBarHeight']))
402
408
  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, {
409
+ let normalizedRegion = region;
410
+ if (this.isAndroid) {
411
+ normalizedRegion = utils.geometry.scale(normalizedRegion, 1 / this.pixelRatio);
412
+ }
413
+ if (this.isIOS && utils.geometry.isIntersected(normalizedRegion, this._driverInfo.safeArea)) {
414
+ normalizedRegion = utils.geometry.intersect(normalizedRegion, this._driverInfo.safeArea);
415
+ }
416
+ normalizedRegion = utils.geometry.offsetNegative(normalizedRegion, {
408
417
  x: this.isAndroid && this.orientation === 'landscape' && this.platformVersion > 7 ? this.navigationBarHeight : 0,
409
418
  y: this.statusBarHeight,
410
419
  });
411
- if (offsetRegion.y < 0) {
412
- offsetRegion.height += offsetRegion.y;
413
- offsetRegion.y = 0;
420
+ if (normalizedRegion.y < 0) {
421
+ normalizedRegion.height += normalizedRegion.y;
422
+ normalizedRegion.y = 0;
414
423
  }
415
- return offsetRegion;
424
+ return normalizedRegion;
416
425
  }
417
426
  async getRegionInViewport(context, region) {
418
427
  await context.focus();
@@ -508,8 +517,13 @@ class Driver {
508
517
  throw new Error('Failed to set viewport size!');
509
518
  }
510
519
  async getDisplaySize() {
520
+ var _a;
511
521
  if (this.isWeb && !this.isMobile)
512
522
  return;
523
+ if ((_a = this._driverInfo) === null || _a === void 0 ? void 0 : _a.viewportSize) {
524
+ this._logger.log('Extracting display size from native driver using cached value');
525
+ return this._driverInfo.displaySize;
526
+ }
513
527
  const size = await this._spec.getWindowSize(this.target);
514
528
  const normalizedSize = this.isAndroid ? utils.geometry.scale(size, 1 / this.pixelRatio) : size;
515
529
  this._logger.log('Extracted and normalized display size:', normalizedSize);
package/dist/element.js CHANGED
@@ -88,20 +88,44 @@ 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.isAndroid ? contentSize.height : 0) + contentSize.scrollableOffset,
114
+ };
115
+ })
116
+ .catch(() => this._spec.getElementRegion(this.driver.target, this.target));
117
+ const contentSize = await ((_b = this.driver.helper) === null || _b === void 0 ? void 0 : _b.getContentSize(this));
118
+ const region = {
119
+ x: contentRegion.x,
120
+ y: contentRegion.y,
121
+ width: Math.max((_c = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width) !== null && _c !== void 0 ? _c : 0, contentRegion.width),
122
+ height: Math.max((_d = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height) !== null && _d !== void 0 ? _d : 0, contentRegion.height),
123
+ };
103
124
  const innerRegion = await this._spec.getElementRegion(this.driver.target, innerElement);
104
- return utils.geometry.contains(region, innerRegion);
125
+ const contains = utils.geometry.contains(region, innerRegion);
126
+ (_e = (_f = this._state).containedElements) !== null && _e !== void 0 ? _e : (_f.containedElements = new Map());
127
+ this._state.containedElements.set(innerElement, contains);
128
+ return contains;
105
129
  }
106
130
  });
107
131
  this._logger.log('Element with selector', this.selector, contains ? 'contains' : `doesn't contain`, innerElement);
@@ -166,11 +190,10 @@ class Element {
166
190
  else {
167
191
  this._logger.log('Extracting content size of native element with selector', this.selector);
168
192
  try {
169
- const { touchPadding, ...contentRegion } = await this.getAttribute('contentSize')
193
+ const contentRegion = await this.getAttribute('contentSize')
170
194
  .then(data => {
171
195
  const contentSize = JSON.parse(data);
172
196
  return {
173
- touchPadding: contentSize.touchPadding,
174
197
  x: contentSize.left,
175
198
  y: contentSize.top,
176
199
  width: contentSize.width,
@@ -178,7 +201,7 @@ class Element {
178
201
  };
179
202
  })
180
203
  .catch(err => {
181
- this._logger.log(`Unable to get the attribute 'contentSize' when looking up touchPadding due to the following error:`, `'${err.message}'`);
204
+ this._logger.warn(`Unable to get the attribute 'contentSize' due to the following error: '${err.message}'`);
182
205
  return this._spec.getElementRegion(this.driver.target, this.target);
183
206
  });
184
207
  this._logger.log('Extracted native content size attribute', contentRegion);
@@ -188,22 +211,12 @@ class Element {
188
211
  width: Math.max((_b = contentSize === null || contentSize === void 0 ? void 0 : contentSize.width) !== null && _b !== void 0 ? _b : 0, contentRegion.width),
189
212
  height: Math.max((_c = contentSize === null || contentSize === void 0 ? void 0 : contentSize.height) !== null && _c !== void 0 ? _c : 0, contentRegion.height),
190
213
  };
191
- this._touchPadding = touchPadding !== null && touchPadding !== void 0 ? touchPadding : this._touchPadding;
192
- this._logger.log('touchPadding', this._touchPadding);
193
214
  if (this.driver.isAndroid) {
194
215
  this._state.contentSize = utils.geometry.scale(this._state.contentSize, 1 / this.driver.pixelRatio);
195
216
  }
196
217
  if (contentRegion.y < this.driver.statusBarHeight) {
197
218
  this._state.contentSize.height -= this.driver.statusBarHeight - contentRegion.y;
198
219
  }
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
220
  return this._state.contentSize;
208
221
  }
209
222
  catch (err) {
@@ -216,8 +229,22 @@ class Element {
216
229
  this._logger.log('Extracted content size', size);
217
230
  return size;
218
231
  }
232
+ async isPager() {
233
+ this._logger.log('Check if element with selector', this.selector, 'is scrollable by pages');
234
+ const isPager = await this.withRefresh(async () => {
235
+ if (this.driver.isAndroid) {
236
+ const className = await this.getAttribute('className');
237
+ return ['androidx.viewpager.widget.ViewPager'].includes(className);
238
+ }
239
+ else {
240
+ return false;
241
+ }
242
+ });
243
+ this._logger.log('Element scrollable by pages', isPager);
244
+ return isPager;
245
+ }
219
246
  async isScrollable() {
220
- this._logger.log('Check is element with selector', this.selector, 'is scrollable');
247
+ this._logger.log('Check if element with selector', this.selector, 'is scrollable');
221
248
  const isScrollable = await this.withRefresh(async () => {
222
249
  if (this.driver.isWeb) {
223
250
  return this.context.execute(snippets.isElementScrollable, [this]);
@@ -247,23 +274,22 @@ class Element {
247
274
  });
248
275
  }
249
276
  async getTouchPadding() {
250
- var _a;
251
- if (this._touchPadding == null) {
277
+ if (this._state.touchPadding == null) {
252
278
  if (this.driver.isWeb)
253
- this._touchPadding = 0;
279
+ this._state.touchPadding = 0;
254
280
  else if (this.driver.isIOS)
255
- this._touchPadding = 10;
281
+ this._state.touchPadding = 10;
256
282
  else if (this.driver.isAndroid) {
257
- const data = await this.getAttribute('contentSize')
258
- .then(JSON.parse)
283
+ const touchPadding = await this.getAttribute('contentSize')
284
+ .then(value => JSON.parse(value).touchPadding)
259
285
  .catch(err => {
260
- this._logger.log(`Unable to get the attribute 'contentSize' when looking up touchPadding due to the following error:`, `'${err.message}'`);
286
+ this._logger.warn(`Unable to get the attribute 'contentSize' when looking up 'touchPadding' due to the following error: '${err.message}'`);
261
287
  });
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);
288
+ this._state.touchPadding = touchPadding !== null && touchPadding !== void 0 ? touchPadding : 20;
289
+ this._logger.log('Touch padding set:', this._state.touchPadding);
264
290
  }
265
291
  }
266
- return this._touchPadding;
292
+ return this._state.touchPadding;
267
293
  }
268
294
  async getText() {
269
295
  const text = await this.withRefresh(async () => {
@@ -279,20 +305,41 @@ class Element {
279
305
  return text;
280
306
  }
281
307
  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
- }
308
+ var _a;
309
+ // we assumes that attributes are not changed during the session
310
+ if ((_a = this._state.attributes) === null || _a === void 0 ? void 0 : _a[name])
311
+ return this._state.attributes[name];
312
+ const value = await this.withRefresh(async () => {
313
+ var _a;
314
+ var _b;
315
+ if (this.driver.isWeb) {
316
+ const properties = await this.context.execute(snippets.getElementProperties, [this, [name]]);
317
+ return properties[name];
318
+ }
319
+ else {
320
+ this._logger.log(`Extracting "${name}" attribute of native element with selector`, this.selector);
321
+ const value = await this._spec.getElementAttribute(this.driver.target, this.target, name);
322
+ (_a = (_b = this._state).attributes) !== null && _a !== void 0 ? _a : (_b.attributes = {});
323
+ this._state.attributes[name] = value;
324
+ if (this.driver.isAndroid && name === 'contentSize') {
325
+ // android has a bug when after extracting 'contentSize' attribute the element is being scrolled by undetermined number of pixels
326
+ this._logger.log('Stabilizing android scroll offset');
327
+ const originalScrollOffset = await this.getScrollOffset();
328
+ await this.scrollTo({ x: 0, y: 0 }, { force: true });
329
+ await this.scrollTo(originalScrollOffset);
330
+ }
331
+ return value;
332
+ }
333
+ });
334
+ this._logger.log(`Extracted element "${name}" attribute:`, value);
335
+ return value;
289
336
  }
290
337
  async setAttribute(name, value) {
291
338
  if (this.driver.isWeb) {
292
339
  await this.context.execute(snippets.setElementAttributes, [this, { [name]: value }]);
293
340
  }
294
341
  }
295
- async scrollTo(offset) {
342
+ async scrollTo(offset, options) {
296
343
  return this.withRefresh(async () => {
297
344
  offset = utils.geometry.round({ x: Math.max(offset.x, 0), y: Math.max(offset.y, 0) });
298
345
  if (this.driver.isWeb) {
@@ -304,75 +351,112 @@ class Element {
304
351
  }
305
352
  else {
306
353
  const currentScrollOffset = await this.getScrollOffset();
307
- if (utils.geometry.equals(offset, currentScrollOffset))
354
+ if (!(options === null || options === void 0 ? void 0 : options.force) && utils.geometry.equals(offset, currentScrollOffset))
308
355
  return currentScrollOffset;
309
356
  const contentSize = await this.getContentSize();
310
357
  const scrollableRegion = await this.getClientRegion();
311
- const effectiveRegion = this.driver.isAndroid
312
- ? utils.geometry.scale(scrollableRegion, this.driver.pixelRatio)
313
- : scrollableRegion;
314
358
  const maxOffset = {
315
359
  x: Math.round(scrollableRegion.width * (contentSize.width / scrollableRegion.width - 1)),
316
360
  y: Math.round(scrollableRegion.height * (contentSize.height / scrollableRegion.height - 1)),
317
361
  };
318
362
  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
363
+ let effectiveRegion = scrollableRegion;
364
+ let remainingOffset = utils.geometry.equals(requiredOffset, { x: 0, y: 0 })
320
365
  ? { x: -maxOffset.x, y: -maxOffset.y } // if it has to be scrolled to the very beginning, then scroll maximum amount of pixels
321
366
  : utils.geometry.offsetNegative(requiredOffset, currentScrollOffset);
322
367
  if (this.driver.isAndroid) {
323
368
  remainingOffset = utils.geometry.scale(remainingOffset, this.driver.pixelRatio);
369
+ effectiveRegion = utils.geometry.scale(scrollableRegion, this.driver.pixelRatio);
324
370
  }
325
371
  const actions = [];
326
372
  const touchPadding = await this.getTouchPadding();
327
- const xPadding = Math.max(Math.floor(effectiveRegion.width * 0.1), touchPadding);
373
+ const isPager = await this.isPager();
374
+ const xPadding = Math.max(Math.floor(effectiveRegion.width * 0.07), touchPadding);
328
375
  const yTrack = Math.floor(effectiveRegion.y + effectiveRegion.height / 2); // center
329
376
  const xLeft = effectiveRegion.y + xPadding;
330
- const xDirection = remainingOffset.y > 0 ? 'right' : 'left';
377
+ const xDirection = remainingOffset.x > 0 ? 'right' : 'left';
331
378
  const xGap = xDirection === 'right' ? -touchPadding : touchPadding;
332
379
  let xRemaining = Math.abs(remainingOffset.x);
380
+ if (isPager) {
381
+ const xPages = Math.floor(xRemaining / effectiveRegion.width);
382
+ xRemaining = (effectiveRegion.width - xPadding * 2) * xPages;
383
+ }
333
384
  while (xRemaining > 0) {
334
385
  const xRight = effectiveRegion.x + Math.min(xRemaining + xPadding, effectiveRegion.width - xPadding);
335
386
  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
- ]);
387
+ if (isPager) {
388
+ actions.push([
389
+ { action: 'press', y: yTrack, x: xStart },
390
+ // scroll through the page
391
+ { action: 'wait', ms: 170 },
392
+ { action: 'moveTo', y: yTrack, x: xEnd },
393
+ { action: 'release' },
394
+ ]);
395
+ }
396
+ else {
397
+ actions.push([
398
+ { action: 'press', y: yTrack, x: xStart },
399
+ // move through scrolling gap (actual scrolling will be triggered only after that)
400
+ { action: 'wait', ms: 100 },
401
+ { action: 'moveTo', y: yTrack, x: xStart + xGap },
402
+ // perform actual scrolling
403
+ { action: 'wait', ms: 100 },
404
+ { action: 'moveTo', y: yTrack, x: xEnd + xGap },
405
+ // prevent inertial scrolling after release
406
+ { action: 'wait', ms: 100 },
407
+ { action: 'moveTo', y: yTrack + 1, x: xEnd + xGap },
408
+ { action: 'release' },
409
+ ]);
410
+ }
346
411
  xRemaining -= xRight - xLeft;
347
412
  }
348
- const yPadding = Math.max(Math.floor(effectiveRegion.height * 0.1), touchPadding);
413
+ const yPadding = Math.max(Math.floor(effectiveRegion.height * 0.07), touchPadding);
349
414
  const xTrack = Math.floor(effectiveRegion.x + 5); // a little bit off left border
350
415
  const yBottom = effectiveRegion.y + effectiveRegion.height - yPadding;
351
416
  const yDirection = remainingOffset.y > 0 ? 'down' : 'up';
352
417
  const yGap = yDirection === 'down' ? -touchPadding : touchPadding;
353
418
  let yRemaining = Math.abs(remainingOffset.y);
419
+ if (isPager) {
420
+ const yPages = Math.floor(yRemaining / effectiveRegion.height);
421
+ yRemaining = (effectiveRegion.height - yPadding * 2) * yPages;
422
+ }
354
423
  while (yRemaining > 0) {
355
424
  const yTop = Math.max(yBottom - yRemaining, effectiveRegion.y + yPadding);
356
425
  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
- ]);
426
+ if (isPager) {
427
+ actions.push([
428
+ { action: 'press', x: xTrack, y: yStart },
429
+ // scroll through the page
430
+ { action: 'wait', ms: 170 },
431
+ { action: 'moveTo', x: xTrack, y: yEnd },
432
+ { action: 'release' },
433
+ ]);
434
+ }
435
+ else {
436
+ actions.push([
437
+ { action: 'press', x: xTrack, y: yStart },
438
+ // move through scrolling gap (actual scrolling will be triggered only after that)
439
+ { action: 'wait', ms: 100 },
440
+ { action: 'moveTo', x: xTrack, y: yStart + yGap },
441
+ // perform actual scrolling
442
+ { action: 'wait', ms: 100 },
443
+ { action: 'moveTo', x: xTrack, y: yEnd + yGap },
444
+ // prevent inertial scrolling after release
445
+ { action: 'wait', ms: 100 },
446
+ { action: 'moveTo', x: xTrack + 1, y: yEnd + yGap },
447
+ { action: 'release' },
448
+ ]);
449
+ }
367
450
  yRemaining -= yBottom - yTop;
368
451
  }
369
452
  // ios actions should be executed one-by-one sequentially, otherwise the result isn't stable
370
- if (this.driver.isIOS) {
453
+ // pages should be scrolled one-by-one as well
454
+ if (isPager || this.driver.isIOS) {
371
455
  for (const action of actions) {
372
456
  await this._spec.performAction(this.driver.target, action);
373
457
  }
374
458
  }
375
- else {
459
+ else if (actions.length > 0) {
376
460
  await this._spec.performAction(this.driver.target, [].concat(...actions));
377
461
  }
378
462
  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.0",
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>;