@applitools/driver 1.2.5 → 1.3.1

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/driver.ts DELETED
@@ -1,456 +0,0 @@
1
- import type * as types from '@applitools/types'
2
- import type {SpecUtils} from './utils'
3
- import * as utils from '@applitools/utils'
4
- import {Context, ContextReference} from './context'
5
- import {Element} from './element'
6
- import {makeSpecUtils} from './utils'
7
- import {parseUserAgent} from './user-agent'
8
-
9
- const snippets = require('@applitools/snippets')
10
-
11
- // eslint-disable-next-line
12
- export class Driver<TDriver, TContext, TElement, TSelector> {
13
- private _target: TDriver
14
-
15
- private _mainContext: Context<TDriver, TContext, TElement, TSelector>
16
- private _currentContext: Context<TDriver, TContext, TElement, TSelector>
17
- private _driverInfo: types.DriverInfo
18
- private _logger: any
19
- private _utils: SpecUtils<TDriver, TContext, TElement, TSelector>
20
-
21
- protected readonly _spec: types.SpecDriver<TDriver, TContext, TElement, TSelector>
22
-
23
- constructor(options: {
24
- spec: types.SpecDriver<TDriver, TContext, TElement, TSelector>
25
- driver: Driver<TDriver, TContext, TElement, TSelector> | TDriver
26
- logger?: any
27
- }) {
28
- if (options.driver instanceof Driver) return options.driver
29
-
30
- this._spec = options.spec
31
- this._utils = makeSpecUtils(options.spec)
32
-
33
- if (options.logger) this._logger = options.logger
34
-
35
- if (this._spec.isDriver(options.driver)) {
36
- this._target = this._spec.transformDriver?.(options.driver) ?? options.driver
37
- } else {
38
- throw new TypeError('Driver constructor called with argument of unknown type!')
39
- }
40
-
41
- this._mainContext = new Context({
42
- spec: this._spec,
43
- context: this._spec.extractContext?.(this._target) ?? ((<unknown>this._target) as TContext),
44
- driver: this,
45
- logger: this._logger,
46
- })
47
- this._currentContext = this._mainContext
48
- }
49
-
50
- get target(): TDriver {
51
- return this._target
52
- }
53
- get currentContext(): Context<TDriver, TContext, TElement, TSelector> {
54
- return this._currentContext
55
- }
56
- get mainContext(): Context<TDriver, TContext, TElement, TSelector> {
57
- return this._mainContext
58
- }
59
- get features() {
60
- return this._driverInfo?.features
61
- }
62
- get deviceName(): string {
63
- return this._driverInfo?.deviceName
64
- }
65
- get platformName(): string {
66
- return this._driverInfo?.platformName
67
- }
68
- get platformVersion(): string | number {
69
- return this._driverInfo?.platformVersion
70
- }
71
- get browserName(): string {
72
- return this._driverInfo?.browserName
73
- }
74
- get browserVersion(): string | number {
75
- return this._driverInfo?.browserVersion
76
- }
77
- get userAgent(): string {
78
- return this._driverInfo?.userAgent
79
- }
80
- get pixelRatio(): number {
81
- return this._driverInfo.pixelRatio ?? 1
82
- }
83
- get statusBarHeight(): number {
84
- return this._driverInfo.statusBarHeight ?? (this.isNative ? 0 : undefined)
85
- }
86
- get navigationBarHeight(): number {
87
- return this._driverInfo.navigationBarHeight ?? (this.isNative ? 0 : undefined)
88
- }
89
- get isNative(): boolean {
90
- return this._driverInfo?.isNative ?? false
91
- }
92
- get isWeb(): boolean {
93
- return !this.isNative
94
- }
95
- get isMobile(): boolean {
96
- return this._driverInfo?.isMobile ?? false
97
- }
98
- get isIOS(): boolean {
99
- return this.platformName === 'iOS'
100
- }
101
- get isAndroid(): boolean {
102
- return this.platformName === 'Android'
103
- }
104
- get isIE(): boolean {
105
- return /(internet explorer|ie)/i.test(this.browserName)
106
- }
107
- get isEdgeLegacy(): boolean {
108
- return /edge/i.test(this.browserName) && Number(this.browserVersion) <= 44
109
- }
110
-
111
- updateCurrentContext(context: Context<TDriver, TContext, TElement, TSelector>): void {
112
- this._currentContext = context
113
- }
114
-
115
- async init(): Promise<this> {
116
- this._driverInfo = await this._spec.getDriverInfo?.(this.target)
117
-
118
- if (this.isWeb) {
119
- const userAgent = this._driverInfo?.userAgent ?? (await this.execute(snippets.getUserAgent))
120
- const pixelRatio = this._driverInfo?.pixelRatio ?? (await this.execute(snippets.getPixelRatio))
121
- const userAgentInfo = userAgent ? parseUserAgent(userAgent) : ({} as any)
122
- this._driverInfo = {
123
- ...this._driverInfo,
124
- isMobile: this._driverInfo?.isMobile ?? ['iOS', 'Android'].includes(userAgentInfo.platformName),
125
- platformName: this._driverInfo?.isMobile
126
- ? this._driverInfo?.platformName ?? userAgentInfo.platformName
127
- : userAgentInfo.platformName ?? this._driverInfo?.platformName,
128
- platformVersion: this._driverInfo?.isMobile
129
- ? this._driverInfo?.platformVersion ?? userAgentInfo.platformVersion
130
- : userAgentInfo.platformVersion ?? this._driverInfo?.platformVersion,
131
- browserName: userAgentInfo.browserName ?? this._driverInfo?.browserName,
132
- browserVersion: userAgentInfo.browserVersion ?? this._driverInfo?.browserVersion,
133
- userAgent,
134
- pixelRatio,
135
- }
136
- } else {
137
- if (this.isAndroid) {
138
- this._driverInfo.statusBarHeight = this._driverInfo.statusBarHeight / this.pixelRatio
139
- this._driverInfo.navigationBarHeight = this._driverInfo.navigationBarHeight / this.pixelRatio
140
- }
141
-
142
- if (!this._driverInfo.viewportSize) {
143
- const displaySize = await this.getDisplaySize()
144
- this._driverInfo.viewportSize = {
145
- width: displaySize.width,
146
- height: displaySize.height - this._driverInfo.statusBarHeight,
147
- }
148
- }
149
- }
150
-
151
- this._logger.log('Driver initialized', this._driverInfo)
152
-
153
- return this
154
- }
155
-
156
- async refreshContexts(): Promise<Context<TDriver, TContext, TElement, TSelector>> {
157
- if (this.isNative) return this.currentContext
158
-
159
- const spec = this._spec
160
- const utils = this._utils
161
-
162
- let currentContext = this.currentContext.target
163
- let contextInfo = await getContextInfo(currentContext)
164
-
165
- const path = []
166
- if (spec.parentContext) {
167
- while (!contextInfo.isRoot) {
168
- currentContext = await spec.parentContext(currentContext)
169
- const contextReference = await findContextReference(currentContext, contextInfo)
170
- if (!contextReference) throw new Error('Unable to find out the chain of frames')
171
- path.unshift(contextReference)
172
- contextInfo = await getContextInfo(currentContext)
173
- }
174
- } else {
175
- currentContext = await spec.mainContext(currentContext)
176
- path.push(...(await findContextPath(currentContext, contextInfo)))
177
- }
178
- this._currentContext = this._mainContext
179
- return this.switchToChildContext(...path)
180
-
181
- async function getContextInfo(context: TContext): Promise<any> {
182
- const [documentElement, selector, isRoot, isCORS] = await spec.executeScript(context, snippets.getContextInfo)
183
- return {documentElement, selector, isRoot, isCORS}
184
- }
185
-
186
- async function getChildContextsInfo(context: TContext): Promise<any[]> {
187
- const framesInfo = await spec.executeScript(context, snippets.getChildFramesInfo)
188
- return framesInfo.map(([contextElement, isCORS]: [TElement, boolean]) => ({contextElement, isCORS}))
189
- }
190
-
191
- async function isEqualElements(context: TContext, element1: TElement, element2: TElement): Promise<boolean> {
192
- return spec.executeScript(context, snippets.isEqualElements, [element1, element2]).catch(() => false)
193
- }
194
-
195
- async function findContextReference(context: TContext, contextInfo: any): Promise<TElement> {
196
- if (contextInfo.selector) {
197
- const contextElement = await spec.findElement(
198
- context,
199
- utils.transformSelector({type: 'xpath', selector: contextInfo.selector}),
200
- )
201
- if (contextElement) return contextElement
202
- }
203
-
204
- for (const childContextInfo of await getChildContextsInfo(context)) {
205
- if (childContextInfo.isCORS !== contextInfo.isCORS) continue
206
- const childContext = await spec.childContext(context, childContextInfo.contextElement)
207
- const contentDocument = await spec.findElement(childContext, utils.transformSelector('html'))
208
- const isWantedContext = await isEqualElements(childContext, contentDocument, contextInfo.documentElement)
209
- await spec.parentContext(childContext)
210
- if (isWantedContext) return childContextInfo.contextElement
211
- }
212
- }
213
-
214
- async function findContextPath(
215
- context: TContext,
216
- contextInfo: any,
217
- contextPath: TElement[] = [],
218
- ): Promise<TElement[]> {
219
- const contentDocument = await spec.findElement(context, utils.transformSelector('html'))
220
-
221
- if (await isEqualElements(context, contentDocument, contextInfo.documentElement)) {
222
- return contextPath
223
- }
224
-
225
- for (const childContextInfo of await getChildContextsInfo(context)) {
226
- const childContext = await spec.childContext(context, childContextInfo.contextElement)
227
- const possibleContextPath = [...contextPath, childContextInfo.contextElement]
228
- const wantedContextPath = await findContextPath(childContext, contextInfo, possibleContextPath)
229
- await spec.mainContext(context)
230
-
231
- if (wantedContextPath) return wantedContextPath
232
-
233
- for (const contextElement of contextPath) {
234
- await spec.childContext(context, contextElement)
235
- }
236
- }
237
- }
238
- }
239
-
240
- async switchTo(
241
- context: Context<TDriver, TContext, TElement, TSelector>,
242
- ): Promise<Context<TDriver, TContext, TElement, TSelector>> {
243
- if (await this.currentContext.equals(context)) {
244
- this._currentContext = context
245
- return
246
- }
247
- const currentPath = this.currentContext.path
248
- const requiredPath = context.path
249
-
250
- let diffIndex = -1
251
- for (const [index, context] of requiredPath.entries()) {
252
- if (currentPath[index] && !(await currentPath[index].equals(context))) {
253
- diffIndex = index
254
- break
255
- }
256
- }
257
-
258
- if (diffIndex === 0) {
259
- throw new Error('Cannot switch to the context, because it has different main context')
260
- } else if (diffIndex === -1) {
261
- if (currentPath.length === requiredPath.length) {
262
- // required and current paths are the same
263
- return this.currentContext
264
- } else if (requiredPath.length > currentPath.length) {
265
- // current path is a sub-path of required path
266
- return this.switchToChildContext(...requiredPath.slice(currentPath.length))
267
- } else if (currentPath.length - requiredPath.length <= requiredPath.length) {
268
- // required path is a sub-path of current path
269
- return this.switchToParentContext(currentPath.length - requiredPath.length)
270
- } else {
271
- // required path is a sub-path of current path
272
- await this.switchToMainContext()
273
- return this.switchToChildContext(...requiredPath)
274
- }
275
- } else if (currentPath.length - diffIndex <= diffIndex) {
276
- // required path is different from current or they are partially intersected
277
- // chose an optimal way to traverse from current context to target context
278
- await this.switchToParentContext(currentPath.length - diffIndex)
279
- return this.switchToChildContext(...requiredPath.slice(diffIndex))
280
- } else {
281
- await this.switchToMainContext()
282
- return this.switchToChildContext(...requiredPath)
283
- }
284
- }
285
-
286
- async switchToMainContext(): Promise<Context<TDriver, TContext, TElement, TSelector>> {
287
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
288
-
289
- this._logger.log('Switching to the main context')
290
- await this._spec.mainContext(this.currentContext.target)
291
- return (this._currentContext = this._mainContext)
292
- }
293
-
294
- async switchToParentContext(elevation = 1): Promise<Context<TDriver, TContext, TElement, TSelector>> {
295
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
296
-
297
- this._logger.log('Switching to a parent context with elevation:', elevation)
298
- if (this.currentContext.path.length <= elevation) {
299
- return this.switchToMainContext()
300
- }
301
-
302
- try {
303
- while (elevation > 0) {
304
- await this._spec.parentContext(this.currentContext.target)
305
- this._currentContext = this._currentContext.parent
306
- elevation -= 1
307
- }
308
- } catch (err) {
309
- this._logger.warn('Unable to switch to a parent context due to error', err)
310
- this._logger.log('Applying workaround to switch to the parent frame')
311
- const path = this.currentContext.path.slice(1, -elevation)
312
- await this.switchToMainContext()
313
- await this.switchToChildContext(...path)
314
- elevation = 0
315
- }
316
- return this.currentContext
317
- }
318
-
319
- async switchToChildContext(
320
- ...references: ContextReference<TDriver, TContext, TElement, TSelector>[]
321
- ): Promise<Context<TDriver, TContext, TElement, TSelector>> {
322
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
323
- this._logger.log('Switching to a child context with depth:', references.length)
324
- for (const reference of references) {
325
- if (reference === this.mainContext) continue
326
- const context = await this.currentContext.context(reference)
327
- await context.focus()
328
- }
329
- return this.currentContext
330
- }
331
-
332
- async normalizeRegion(region: types.Region): Promise<types.Region> {
333
- if (this.isWeb || !utils.types.has(this._driverInfo, ['viewportSize', 'statusBarHeight'])) return region
334
- const scaledRegion = this.isAndroid ? utils.geometry.scale(region, 1 / this.pixelRatio) : region
335
- return utils.geometry.offsetNegative(scaledRegion, {x: 0, y: this.statusBarHeight})
336
- }
337
-
338
- async getRegionInViewport(
339
- context: Context<TDriver, TContext, TElement, TSelector>,
340
- region: types.Region,
341
- ): Promise<types.Region> {
342
- await context.focus()
343
- return context.getRegionInViewport(region)
344
- }
345
-
346
- async element(selector: types.Selector<TSelector>): Promise<Element<TDriver, TContext, TElement, TSelector>> {
347
- return this.currentContext.element(selector)
348
- }
349
-
350
- async elements(selector: types.Selector<TSelector>): Promise<Element<TDriver, TContext, TElement, TSelector>[]> {
351
- return this.currentContext.elements(selector)
352
- }
353
-
354
- async execute(script: ((arg: any) => any) | string, arg?: any): Promise<any> {
355
- return this.currentContext.execute(script, arg)
356
- }
357
-
358
- async takeScreenshot(): Promise<Buffer | string> {
359
- const data = await this._spec.takeScreenshot(this.target)
360
- return utils.types.isString(data) ? data.replace(/[\r\n]+/g, '') : data
361
- }
362
-
363
- async getViewportSize(): Promise<types.Size> {
364
- let size
365
- if (this.isNative) {
366
- this._logger.log('Extracting viewport size from native driver')
367
- if (this._driverInfo?.viewportSize) {
368
- size = this._driverInfo.viewportSize
369
- } else {
370
- size = await this.getDisplaySize()
371
- if (size.height > size.width) {
372
- const orientation = await this.getOrientation()
373
- if (orientation === 'landscape') {
374
- size = {width: size.height, height: size.width}
375
- }
376
- }
377
- }
378
- } else if (this._spec.getViewportSize) {
379
- this._logger.log('Extracting viewport size from web driver using spec method')
380
- size = await this._spec.getViewportSize(this.target)
381
- } else {
382
- this._logger.log('Extracting viewport size from web driver using js snippet')
383
- size = await this.mainContext.execute(snippets.getViewportSize)
384
- }
385
-
386
- this._logger.log('Extracted viewport size', size)
387
-
388
- return size
389
- }
390
-
391
- async setViewportSize(size: types.Size): Promise<void> {
392
- if (this.isMobile) return
393
- if (this._spec.setViewportSize) {
394
- this._logger.log('Setting viewport size to', size, 'using spec method')
395
- await this._spec.setViewportSize(this.target, size)
396
- return
397
- }
398
-
399
- this._logger.log('Setting viewport size to', size, 'using workaround')
400
-
401
- const requiredViewportSize = size
402
- let currentViewportSize = await this.getViewportSize()
403
- if (utils.geometry.equals(currentViewportSize, requiredViewportSize)) return
404
-
405
- let currentWindowSize = await this._spec.getWindowSize(this.target)
406
- this._logger.log('Extracted window size', currentWindowSize)
407
-
408
- let attempt = 0
409
- while (attempt++ < 3) {
410
- const requiredWindowSize = {
411
- width: currentWindowSize.width + (requiredViewportSize.width - currentViewportSize.width),
412
- height: currentWindowSize.height + (requiredViewportSize.height - currentViewportSize.height),
413
- }
414
- this._logger.log(`Attempt #${attempt} to set viewport size by setting window size to`, requiredWindowSize)
415
- await this._spec.setWindowSize(this.target, requiredWindowSize)
416
- await utils.general.sleep(3000)
417
- currentWindowSize = requiredWindowSize
418
- currentViewportSize = await this.getViewportSize()
419
- if (utils.geometry.equals(currentViewportSize, requiredViewportSize)) return
420
- this._logger.log(`Attempt #${attempt} to set viewport size failed. Current viewport:`, currentViewportSize)
421
- }
422
-
423
- throw new Error('Failed to set viewport size!')
424
- }
425
-
426
- async getDisplaySize(): Promise<types.Size> {
427
- if (this.isWeb) return
428
- const size = await this._spec.getWindowSize(this.target)
429
- return this.isAndroid ? utils.geometry.scale(size, 1 / this.pixelRatio) : size
430
- }
431
-
432
- async getOrientation(): Promise<'portrait' | 'landscape'> {
433
- if (this.isWeb) return
434
- const orientation = this._spec.getOrientation(this.target)
435
- this._logger.log('Extracted device orientation:', orientation)
436
- return orientation
437
- }
438
-
439
- async getTitle(): Promise<string> {
440
- if (this.isNative) return null
441
- const title = await this._spec.getTitle(this.target)
442
- this._logger.log('Extracted title:', title)
443
- return title
444
- }
445
-
446
- async getUrl(): Promise<string> {
447
- if (this.isNative) return null
448
- const url = this._spec.getUrl(this.target)
449
- this._logger.log('Extracted url:', url)
450
- return url
451
- }
452
-
453
- async visit(url: string): Promise<void> {
454
- await this._spec.visit(this.target, url)
455
- }
456
- }