@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/CHANGELOG.md +17 -0
- package/dist/context.js +1 -1
- package/dist/driver.js +11 -4
- package/dist/element.js +4 -4
- package/package.json +10 -5
- package/types/context.d.ts +70 -0
- package/types/driver.d.ts +60 -0
- package/types/element.d.ts +55 -0
- package/types/fake/mock-driver.d.ts +71 -0
- package/types/fake/spec-driver.d.ts +40 -0
- package/types/index.d.ts +5 -0
- package/types/user-agent.d.ts +12 -0
- package/types/utils.d.ts +11 -0
- package/.bongo/dry-run/package-lock.json +0 -30
- package/.bongo/dry-run.tgz +0 -0
- package/.eslintrc +0 -36
- package/src/context.ts +0 -519
- package/src/driver.ts +0 -451
- package/src/element.ts +0 -502
- package/src/fake/mock-driver.d.ts +0 -3
- package/src/fake/mock-driver.js +0 -355
- package/src/fake/spec-driver.d.ts +0 -11
- package/src/fake/spec-driver.js +0 -66
- package/src/index.ts +0 -6
- package/src/user-agent.ts +0 -99
- package/src/utils.ts +0 -77
- package/test/unit/context.spec.ts +0 -243
- package/test/unit/driver.spec.ts +0 -482
- package/test/unit/user-agent.spec.d.ts +0 -1
- package/tsconfig.json +0 -24
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
|
-
}
|