@applitools/driver 1.2.3 → 1.2.7

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/src/element.ts DELETED
@@ -1,502 +0,0 @@
1
- import type * as types from '@applitools/types'
2
- import type {Context} from './context'
3
- import type {SpecUtils} from './utils'
4
- import * as utils from '@applitools/utils'
5
- import {makeSpecUtils} from './utils'
6
-
7
- const snippets = require('@applitools/snippets')
8
-
9
- export type ElementState = {
10
- contentSize?: types.Size
11
- scrollOffset?: types.Location
12
- transforms?: any
13
- }
14
-
15
- export class Element<TDriver, TContext, TElement, TSelector> {
16
- private _target: TElement
17
-
18
- private _context: Context<TDriver, TContext, TElement, TSelector>
19
- private _selector: types.Selector<TSelector>
20
- private _index: number
21
- private _state: ElementState = {}
22
- private _originalOverflow: any
23
- private _touchPadding: number
24
- private _logger: any
25
- private _utils: SpecUtils<TDriver, TContext, TElement, TSelector>
26
-
27
- protected readonly _spec: types.SpecDriver<TDriver, TContext, TElement, TSelector>
28
-
29
- constructor(options: {
30
- spec: types.SpecDriver<TDriver, TContext, TElement, TSelector>
31
- element?: TElement | Element<TDriver, TContext, TElement, TSelector>
32
- context?: Context<TDriver, TContext, TElement, TSelector>
33
- selector?: types.Selector<TSelector>
34
- index?: number
35
- logger?: any
36
- }) {
37
- if (options.element instanceof Element) return options.element
38
-
39
- this._spec = options.spec
40
- this._utils = makeSpecUtils(options.spec)
41
-
42
- if (options.context) this._context = options.context
43
- if (options.logger) this._logger = options.logger
44
-
45
- if (this._spec.isElement(options.element)) {
46
- this._target = this._spec.transformElement?.(options.element) ?? options.element
47
- // Some frameworks contains information about the selector inside an element
48
- this._selector = options.selector ?? this._spec.extractSelector?.(options.element)
49
- this._index = options.index
50
- } else if (this._utils.isSelector(options.selector)) {
51
- this._selector = options.selector
52
- } else {
53
- throw new TypeError('Element constructor called with argument of unknown type!')
54
- }
55
- }
56
-
57
- get target() {
58
- return this._target
59
- }
60
-
61
- get selector() {
62
- return this._selector
63
- }
64
-
65
- get context() {
66
- return this._context
67
- }
68
-
69
- get driver() {
70
- return this.context.driver
71
- }
72
-
73
- get isRef() {
74
- return this.context.isRef || !this.target
75
- }
76
-
77
- async equals(element: Element<TDriver, TContext, TElement, TSelector> | TElement): Promise<boolean> {
78
- if (this.isRef) return false
79
-
80
- element = element instanceof Element ? element.target : element
81
- if (this._spec.isEqualElements) {
82
- return this._spec.isEqualElements(this.context.target, this.target, element)
83
- } else {
84
- return this._spec
85
- .executeScript(this.context.target, snippets.isEqualElements, [this.target, element])
86
- .catch(() => false)
87
- }
88
- }
89
-
90
- async init(context: Context<TDriver, TContext, TElement, TSelector>): Promise<this> {
91
- this._context = context
92
- this._logger = (context as any)._logger
93
- if (this._target) return this
94
-
95
- if (this._selector) {
96
- const element = await this._context.element(this._selector)
97
- if (!element) throw new Error(`Cannot find element with selector ${JSON.stringify(this._selector)}`)
98
- this._target = element.target
99
- return this
100
- }
101
- }
102
-
103
- async getRegion(): Promise<types.Region> {
104
- const region = await this.withRefresh(async () => {
105
- if (this.driver.isWeb) {
106
- this._logger.log('Extracting region of web element with selector', this.selector)
107
- return this.context.execute(snippets.getElementRect, [this, false])
108
- } else {
109
- this._logger.log('Extracting region of native element with selector', this.selector)
110
- const region = await this._spec.getElementRegion(this.driver.target, this.target)
111
- this._logger.log('Extracted native region', region)
112
- return this.driver.normalizeRegion(region)
113
- }
114
- })
115
- this._logger.log('Extracted region', region)
116
- return region
117
- }
118
-
119
- async getClientRegion(): Promise<types.Region> {
120
- const region = await this.withRefresh(async () => {
121
- if (this.driver.isWeb) {
122
- this._logger.log('Extracting region of web element with selector', this.selector)
123
- return this.context.execute(snippets.getElementRect, [this, true])
124
- } else {
125
- this._logger.log('Extracting region of native element with selector', this.selector)
126
- const region = await this._spec.getElementRegion(this.driver.target, this.target)
127
- this._logger.log('Extracted native region', region)
128
- return this.driver.normalizeRegion(region)
129
- }
130
- })
131
- this._logger.log('Extracted client region', region)
132
- return region
133
- }
134
-
135
- async getContentSize(): Promise<types.Size> {
136
- if (this._state.contentSize) return this._state.contentSize
137
-
138
- const size = await this.withRefresh(async () => {
139
- if (this.driver.isWeb) {
140
- this._logger.log('Extracting content size of web element with selector', this.selector)
141
- return this.context.execute(snippets.getElementContentSize, [this])
142
- } else {
143
- this._logger.log('Extracting content size of native element with selector', this.selector)
144
- try {
145
- if (this.driver.isAndroid) {
146
- const className = await this.getAttribute('className')
147
- if (
148
- [
149
- 'android.widget.ListView',
150
- 'android.widget.GridView',
151
- 'android.support.v7.widget.RecyclerView',
152
- // 'androidx.recyclerview.widget.RecyclerView',
153
- 'androidx.viewpager2.widget.ViewPager2',
154
- ].includes(className)
155
- ) {
156
- this._logger.log('Trying to extract content size using android helper library')
157
- const helperElement = await this.driver.element({
158
- type: '-android uiautomator',
159
- selector: 'new UiSelector().description("EyesAppiumHelper")',
160
- })
161
- if (helperElement) {
162
- const elementRegion = await this._spec.getElementRegion(this.driver.target, this.target)
163
- await helperElement.click()
164
- const info = await this._spec.getElementText(this.driver.target, helperElement.target)
165
- this._state.contentSize = utils.geometry.scale(
166
- {width: elementRegion.width, height: Number(info)},
167
- 1 / this.driver.pixelRatio,
168
- )
169
- } else {
170
- this._logger.log('Helper library for android was not detected')
171
- }
172
- }
173
- } else if (this.driver.isIOS) {
174
- const type = await this.getAttribute('type')
175
- if (type === 'XCUIElementTypeScrollView') {
176
- const elementRegion = await this._spec.getElementRegion(this.driver.target, this.target)
177
- const [childElement] = await this.driver.elements({
178
- type: 'xpath',
179
- selector: '//XCUIElementTypeScrollView[1]/*', // We cannot be sure that our element is the first one
180
- })
181
- const childElementRegion = await this._spec.getElementRegion(this.driver.target, childElement.target)
182
- this._state.contentSize = {
183
- width: elementRegion.width,
184
- height: childElementRegion.y + childElementRegion.height - elementRegion.y,
185
- }
186
- } else if (type === 'XCUIElementTypeCollectionView') {
187
- this._logger.log('Trying to extract content size using ios helper library')
188
- const helperElement = await this.driver.element({
189
- type: 'name',
190
- selector: 'applitools_grab_scrollable_data_button',
191
- })
192
- if (helperElement) {
193
- const helperElementRegion = await this._spec.getElementRegion(this.driver.target, helperElement.target)
194
- await this._spec.performAction(this.driver.target, [
195
- {action: 'tap', x: helperElementRegion.x, y: helperElementRegion.y},
196
- {action: 'wait', ms: 1000},
197
- {action: 'release'},
198
- ])
199
- const infoElement = await this.driver.element({type: 'name', selector: 'applitools_content_size_label'})
200
- const info = await this._spec.getElementText(this.driver.target, infoElement.target)
201
- if (info) {
202
- const [_, width, height] = info.match(/\{(\d+),\s?(\d+)\}/)
203
- this._state.contentSize = {width: Number(width), height: Number(height)}
204
- }
205
- } else {
206
- this._logger.log('Helper library for ios was not detected')
207
- }
208
- }
209
- }
210
-
211
- if (!this._state.contentSize) {
212
- const data = JSON.parse(await this.getAttribute('contentSize'))
213
- this._logger.log('Extracted native content size attribute', data)
214
- this._state.contentSize = this.driver.isIOS
215
- ? {width: data.width, height: data.scrollableOffset}
216
- : utils.geometry.scale(
217
- {width: data.width, height: data.height + data.scrollableOffset},
218
- 1 / this.driver.pixelRatio,
219
- )
220
- this._touchPadding = data.touchPadding ?? this._touchPadding
221
- }
222
-
223
- if (this.driver.isAndroid) {
224
- this._logger.log('Stabilizing android scroll offset')
225
-
226
- // android has a bug when after extracting 'contentSize' attribute the element is being scrolled by undetermined number of pixels
227
- const originalScrollOffset = await this.getScrollOffset()
228
- this._state.scrollOffset = {x: -1, y: -1}
229
- await this.scrollTo({x: 0, y: 0})
230
- await this.scrollTo(originalScrollOffset)
231
- }
232
-
233
- return this._state.contentSize
234
- } catch (err) {
235
- this._logger.warn('Failed to extract content size, extracting client size instead')
236
- this._logger.error(err)
237
- return utils.geometry.size(await this.getClientRegion())
238
- }
239
- }
240
- })
241
-
242
- this._logger.log('Extracted content size', size)
243
- return size
244
- }
245
-
246
- async isScrollable(): Promise<boolean> {
247
- this._logger.log('Check is element with selector', this.selector, 'is scrollable')
248
- const isScrollable = await this.withRefresh(async () => {
249
- if (this.driver.isWeb) {
250
- return this.context.execute(snippets.isElementScrollable, [this])
251
- } else if (this.driver.isAndroid) {
252
- const data = JSON.parse(await this.getAttribute('scrollable'))
253
- return Boolean(data) || false
254
- } else if (this.driver.isIOS) {
255
- const type = await this.getAttribute('type')
256
- return ['XCUIElementTypeScrollView', 'XCUIElementTypeTable', 'XCUIElementTypeCollectionView'].includes(type)
257
- }
258
- })
259
- this._logger.log('Element is scrollable', isScrollable)
260
- return isScrollable
261
- }
262
-
263
- async isRoot(): Promise<boolean> {
264
- // TODO replace with snippet
265
- return this.withRefresh(async () => {
266
- if (this.driver.isWeb) {
267
- const rootElement = await this.context.element({type: 'css', selector: 'html'})
268
- return this.equals(rootElement)
269
- } else {
270
- return false
271
- }
272
- })
273
- }
274
-
275
- async getTouchPadding(): Promise<number> {
276
- if (this._touchPadding == null) {
277
- if (this.driver.isWeb) this._touchPadding = 0
278
- else if (this.driver.isIOS) this._touchPadding = 14
279
- else if (this.driver.isAndroid) {
280
- const {touchPadding} = JSON.parse(await this.getAttribute('contentSize'))
281
- this._touchPadding = touchPadding ?? 0
282
- }
283
- }
284
- return this._touchPadding
285
- }
286
-
287
- async getAttribute(name: string): Promise<string> {
288
- if (this.driver.isWeb) {
289
- const properties = await this.context.execute(snippets.getElementProperties, [this, [name]])
290
- return properties[name]
291
- } else {
292
- return this._spec.getElementAttribute(this.driver.target, this.target, name)
293
- }
294
- }
295
-
296
- async setAttribute(name: string, value: string): Promise<void> {
297
- if (this.driver.isWeb) {
298
- await this.context.execute(snippets.setElementAttributes, [this, {[name]: value}])
299
- }
300
- }
301
-
302
- async scrollTo(offset: types.Location): Promise<types.Location> {
303
- return this.withRefresh(async () => {
304
- offset = {x: Math.round(offset.x), y: Math.round(offset.y)}
305
- if (this.driver.isWeb) {
306
- let actualOffset = await this.context.execute(snippets.scrollTo, [this, offset])
307
- // iOS has an issue when scroll offset is read immediately after it is been set it will always return the exact value that was set
308
- if (this.driver.isIOS) actualOffset = await this.getScrollOffset()
309
- return actualOffset
310
- } else {
311
- const currentScrollOffset = await this.getScrollOffset()
312
- if (utils.geometry.equals(offset, currentScrollOffset)) return currentScrollOffset
313
-
314
- const contentSize = await this.getContentSize()
315
- const scrollableRegion = await this._spec.getElementRegion(this.driver.target, this.target)
316
- const scaledScrollableRegion = this.driver.isAndroid
317
- ? utils.geometry.scale(scrollableRegion, 1 / this.driver.pixelRatio)
318
- : scrollableRegion
319
- const maxOffset = {
320
- x: Math.round(scaledScrollableRegion.width * (contentSize.width / scaledScrollableRegion.width - 1)),
321
- y: Math.round(scaledScrollableRegion.height * (contentSize.height / scaledScrollableRegion.height - 1)),
322
- }
323
- let requiredOffset
324
- let remainingOffset
325
- if (offset.x === 0 && offset.y === 0) {
326
- requiredOffset = offset
327
- remainingOffset = {x: -maxOffset.x, y: -maxOffset.y}
328
- } else {
329
- requiredOffset = {x: Math.min(offset.x, maxOffset.x), y: Math.min(offset.y, maxOffset.y)}
330
- remainingOffset = utils.geometry.offsetNegative(requiredOffset, currentScrollOffset)
331
- }
332
-
333
- if (this.driver.isAndroid) {
334
- remainingOffset = utils.geometry.scale(remainingOffset, this.driver.pixelRatio)
335
- }
336
-
337
- const actions = []
338
-
339
- const xPadding = Math.floor(scrollableRegion.width * 0.1)
340
- const yCenter = Math.floor(scrollableRegion.y + scrollableRegion.height / 2)
341
- const xLeft = scrollableRegion.y + xPadding
342
- const xDirection = remainingOffset.y > 0 ? 'right' : 'left'
343
- let xRemaining = Math.abs(remainingOffset.x)
344
- while (xRemaining > 0) {
345
- const xRight = scrollableRegion.x + Math.min(xRemaining + xPadding, scrollableRegion.width - xPadding)
346
- const [xStart, xEnd] = xDirection === 'right' ? [xRight, xLeft] : [xLeft, xRight]
347
- actions.push(
348
- {action: 'press', x: xStart, y: yCenter},
349
- {action: 'wait', ms: 1500},
350
- {action: 'moveTo', x: xEnd, y: yCenter},
351
- {action: 'release'},
352
- )
353
- xRemaining -= xRight - xLeft
354
- }
355
-
356
- const yPadding = Math.floor(scrollableRegion.height * 0.1)
357
- const xCenter = Math.floor(scrollableRegion.x + scrollableRegion.width / 2) // 0
358
- const yTop = scrollableRegion.y + yPadding
359
- const yDirection = remainingOffset.y > 0 ? 'down' : 'up'
360
- let yRemaining = Math.abs(remainingOffset.y) + (await this.getTouchPadding()) * 2
361
- while (yRemaining > 0) {
362
- const yBottom = scrollableRegion.y + Math.min(yRemaining + yPadding, scrollableRegion.height - yPadding)
363
- const [yStart, yEnd] = yDirection === 'down' ? [yBottom, yTop] : [yTop, yBottom]
364
- actions.push(
365
- {action: 'press', x: xCenter, y: yStart},
366
- {action: 'wait', ms: 1500},
367
- {action: 'moveTo', x: xCenter, y: yEnd},
368
- {action: 'wait', ms: 1500},
369
- {action: 'release'},
370
- )
371
- yRemaining -= yBottom - yTop
372
- }
373
-
374
- if (actions.length > 0) {
375
- await this._spec.performAction(this.driver.target, actions)
376
- }
377
-
378
- this._state.scrollOffset = requiredOffset
379
- return this._state.scrollOffset
380
- }
381
- })
382
- }
383
-
384
- async translateTo(offset: types.Location): Promise<types.Location> {
385
- offset = {x: Math.round(offset.x), y: Math.round(offset.y)}
386
- if (this.driver.isWeb) {
387
- return this.withRefresh(async () => this.context.execute(snippets.translateTo, [this, offset]))
388
- } else {
389
- throw new Error('Cannot apply css translate scrolling on non-web element')
390
- }
391
- }
392
-
393
- async getScrollOffset(): Promise<types.Location> {
394
- if (this.driver.isWeb) {
395
- return this.withRefresh(() => this.context.execute(snippets.getElementScrollOffset, [this]))
396
- } else {
397
- return this._state.scrollOffset ?? {x: 0, y: 0}
398
- }
399
- }
400
-
401
- async getTranslateOffset(): Promise<types.Location> {
402
- if (this.driver.isWeb) {
403
- return this.withRefresh(() => this.context.execute(snippets.getElementTranslateOffset, [this]))
404
- } else {
405
- throw new Error('Cannot apply css translate scrolling on non-web element')
406
- }
407
- }
408
-
409
- async getInnerOffset(): Promise<types.Location> {
410
- if (this.driver.isWeb) {
411
- return this.withRefresh(() => this.context.execute(snippets.getElementInnerOffset, [this]))
412
- } else {
413
- return this.getScrollOffset()
414
- }
415
- }
416
-
417
- async click(): Promise<void> {
418
- await this._spec.click(this.context.target, this.target)
419
- }
420
-
421
- async preserveState(): Promise<ElementState> {
422
- if (this.driver.isNative) return
423
- // TODO create single js snippet
424
- const scrollOffset = await this.getScrollOffset()
425
- const transforms = await this.context.execute(snippets.getElementStyleProperties, [
426
- this,
427
- ['transform', '-webkit-transform'],
428
- ])
429
- if (!utils.types.has(this._state, ['scrollOffset', 'transforms'])) {
430
- this._state.scrollOffset = scrollOffset
431
- this._state.transforms = transforms
432
- }
433
- return {scrollOffset, transforms}
434
- }
435
-
436
- async restoreState(state: ElementState = this._state): Promise<void> {
437
- if (this.driver.isNative) return
438
- if (state.scrollOffset) await this.scrollTo(state.scrollOffset)
439
- if (state.transforms) await this.context.execute(snippets.setElementStyleProperties, [this, state.transforms])
440
- if (state === this._state) {
441
- this._state.scrollOffset = null
442
- this._state.transforms = null
443
- }
444
- }
445
-
446
- async hideScrollbars(): Promise<void> {
447
- if (this.driver.isNative) return
448
- if (this._originalOverflow) return
449
- return this.withRefresh(async () => {
450
- const {overflow} = await this.context.execute(snippets.setElementStyleProperties, [this, {overflow: 'hidden'}])
451
- this._originalOverflow = overflow
452
- })
453
- }
454
-
455
- async restoreScrollbars(): Promise<void> {
456
- if (this.driver.isNative) return
457
- if (!this._originalOverflow) return
458
- return this.withRefresh(async () => {
459
- await this.context.execute(snippets.setElementStyleProperties, [this, {overflow: this._originalOverflow}])
460
- this._originalOverflow = null
461
- })
462
- }
463
-
464
- async refresh(freshElement?: TElement): Promise<boolean> {
465
- if (this._spec.isElement(freshElement)) {
466
- this._target = freshElement
467
- return true
468
- }
469
- if (!this._selector) return false
470
- const element =
471
- this._index > 0
472
- ? await this.context.elements(this._selector).then(elements => elements[this._index])
473
- : await this.context.element(this._selector)
474
- if (element) {
475
- this._target = element.target
476
- }
477
- return Boolean(element)
478
- }
479
-
480
- async withRefresh<TResult>(operation: (...args: any[]) => TResult): Promise<TResult> {
481
- if (!this._spec.isStaleElementError) return operation()
482
- try {
483
- const result = await operation()
484
- // Some frameworks could handle stale element reference error by itself or doesn't throw an error
485
- if (this._spec.isStaleElementError(result, this.selector as TSelector)) {
486
- await this.refresh()
487
- return operation()
488
- }
489
- return result
490
- } catch (err) {
491
- if (this._spec.isStaleElementError(err)) {
492
- const refreshed = await this.refresh()
493
- if (refreshed) return operation()
494
- }
495
- throw err
496
- }
497
- }
498
-
499
- toJSON(): TElement {
500
- return this.target
501
- }
502
- }
@@ -1,3 +0,0 @@
1
- export type MockDriver = any
2
-
3
- export const MockDriver: MockDriver