@applitools/driver 1.2.4 → 1.3.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.
package/src/driver.ts DELETED
@@ -1,452 +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: userAgentInfo.platformName ?? this._driverInfo?.platformName,
126
- platformVersion: userAgentInfo.platformVersion ?? this._driverInfo?.platformVersion,
127
- browserName: userAgentInfo.browserName ?? this._driverInfo?.browserName,
128
- browserVersion: userAgentInfo.browserVersion ?? this._driverInfo?.browserVersion,
129
- userAgent,
130
- pixelRatio,
131
- }
132
- } else {
133
- if (this.isAndroid) {
134
- this._driverInfo.statusBarHeight = this._driverInfo.statusBarHeight / this.pixelRatio
135
- this._driverInfo.navigationBarHeight = this._driverInfo.navigationBarHeight / this.pixelRatio
136
- }
137
-
138
- if (!this._driverInfo.viewportSize) {
139
- const displaySize = await this.getDisplaySize()
140
- this._driverInfo.viewportSize = {
141
- width: displaySize.width,
142
- height: displaySize.height - this._driverInfo.statusBarHeight,
143
- }
144
- }
145
- }
146
-
147
- this._logger.log('Driver initialized', this._driverInfo)
148
-
149
- return this
150
- }
151
-
152
- async refreshContexts(): Promise<Context<TDriver, TContext, TElement, TSelector>> {
153
- if (this.isNative) return this.currentContext
154
-
155
- const spec = this._spec
156
- const utils = this._utils
157
-
158
- let currentContext = this.currentContext.target
159
- let contextInfo = await getContextInfo(currentContext)
160
-
161
- const path = []
162
- if (spec.parentContext) {
163
- while (!contextInfo.isRoot) {
164
- currentContext = await spec.parentContext(currentContext)
165
- const contextReference = await findContextReference(currentContext, contextInfo)
166
- if (!contextReference) throw new Error('Unable to find out the chain of frames')
167
- path.unshift(contextReference)
168
- contextInfo = await getContextInfo(currentContext)
169
- }
170
- } else {
171
- currentContext = await spec.mainContext(currentContext)
172
- path.push(...(await findContextPath(currentContext, contextInfo)))
173
- }
174
- this._currentContext = this._mainContext
175
- return this.switchToChildContext(...path)
176
-
177
- async function getContextInfo(context: TContext): Promise<any> {
178
- const [documentElement, selector, isRoot, isCORS] = await spec.executeScript(context, snippets.getContextInfo)
179
- return {documentElement, selector, isRoot, isCORS}
180
- }
181
-
182
- async function getChildContextsInfo(context: TContext): Promise<any[]> {
183
- const framesInfo = await spec.executeScript(context, snippets.getChildFramesInfo)
184
- return framesInfo.map(([contextElement, isCORS]: [TElement, boolean]) => ({contextElement, isCORS}))
185
- }
186
-
187
- async function isEqualElements(context: TContext, element1: TElement, element2: TElement): Promise<boolean> {
188
- return spec.executeScript(context, snippets.isEqualElements, [element1, element2]).catch(() => false)
189
- }
190
-
191
- async function findContextReference(context: TContext, contextInfo: any): Promise<TElement> {
192
- if (contextInfo.selector) {
193
- const contextElement = await spec.findElement(
194
- context,
195
- utils.transformSelector({type: 'xpath', selector: contextInfo.selector}),
196
- )
197
- if (contextElement) return contextElement
198
- }
199
-
200
- for (const childContextInfo of await getChildContextsInfo(context)) {
201
- if (childContextInfo.isCORS !== contextInfo.isCORS) continue
202
- const childContext = await spec.childContext(context, childContextInfo.contextElement)
203
- const contentDocument = await spec.findElement(childContext, utils.transformSelector('html'))
204
- const isWantedContext = await isEqualElements(childContext, contentDocument, contextInfo.documentElement)
205
- await spec.parentContext(childContext)
206
- if (isWantedContext) return childContextInfo.contextElement
207
- }
208
- }
209
-
210
- async function findContextPath(
211
- context: TContext,
212
- contextInfo: any,
213
- contextPath: TElement[] = [],
214
- ): Promise<TElement[]> {
215
- const contentDocument = await spec.findElement(context, utils.transformSelector('html'))
216
-
217
- if (await isEqualElements(context, contentDocument, contextInfo.documentElement)) {
218
- return contextPath
219
- }
220
-
221
- for (const childContextInfo of await getChildContextsInfo(context)) {
222
- const childContext = await spec.childContext(context, childContextInfo.contextElement)
223
- const possibleContextPath = [...contextPath, childContextInfo.contextElement]
224
- const wantedContextPath = await findContextPath(childContext, contextInfo, possibleContextPath)
225
- await spec.mainContext(context)
226
-
227
- if (wantedContextPath) return wantedContextPath
228
-
229
- for (const contextElement of contextPath) {
230
- await spec.childContext(context, contextElement)
231
- }
232
- }
233
- }
234
- }
235
-
236
- async switchTo(
237
- context: Context<TDriver, TContext, TElement, TSelector>,
238
- ): Promise<Context<TDriver, TContext, TElement, TSelector>> {
239
- if (await this.currentContext.equals(context)) {
240
- this._currentContext = context
241
- return
242
- }
243
- const currentPath = this.currentContext.path
244
- const requiredPath = context.path
245
-
246
- let diffIndex = -1
247
- for (const [index, context] of requiredPath.entries()) {
248
- if (currentPath[index] && !(await currentPath[index].equals(context))) {
249
- diffIndex = index
250
- break
251
- }
252
- }
253
-
254
- if (diffIndex === 0) {
255
- throw new Error('Cannot switch to the context, because it has different main context')
256
- } else if (diffIndex === -1) {
257
- if (currentPath.length === requiredPath.length) {
258
- // required and current paths are the same
259
- return this.currentContext
260
- } else if (requiredPath.length > currentPath.length) {
261
- // current path is a sub-path of required path
262
- return this.switchToChildContext(...requiredPath.slice(currentPath.length))
263
- } else if (currentPath.length - requiredPath.length <= requiredPath.length) {
264
- // required path is a sub-path of current path
265
- return this.switchToParentContext(currentPath.length - requiredPath.length)
266
- } else {
267
- // required path is a sub-path of current path
268
- await this.switchToMainContext()
269
- return this.switchToChildContext(...requiredPath)
270
- }
271
- } else if (currentPath.length - diffIndex <= diffIndex) {
272
- // required path is different from current or they are partially intersected
273
- // chose an optimal way to traverse from current context to target context
274
- await this.switchToParentContext(currentPath.length - diffIndex)
275
- return this.switchToChildContext(...requiredPath.slice(diffIndex))
276
- } else {
277
- await this.switchToMainContext()
278
- return this.switchToChildContext(...requiredPath)
279
- }
280
- }
281
-
282
- async switchToMainContext(): Promise<Context<TDriver, TContext, TElement, TSelector>> {
283
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
284
-
285
- this._logger.log('Switching to the main context')
286
- await this._spec.mainContext(this.currentContext.target)
287
- return (this._currentContext = this._mainContext)
288
- }
289
-
290
- async switchToParentContext(elevation = 1): Promise<Context<TDriver, TContext, TElement, TSelector>> {
291
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
292
-
293
- this._logger.log('Switching to a parent context with elevation:', elevation)
294
- if (this.currentContext.path.length <= elevation) {
295
- return this.switchToMainContext()
296
- }
297
-
298
- try {
299
- while (elevation > 0) {
300
- await this._spec.parentContext(this.currentContext.target)
301
- this._currentContext = this._currentContext.parent
302
- elevation -= 1
303
- }
304
- } catch (err) {
305
- this._logger.warn('Unable to switch to a parent context due to error', err)
306
- this._logger.log('Applying workaround to switch to the parent frame')
307
- const path = this.currentContext.path.slice(1, -elevation)
308
- await this.switchToMainContext()
309
- await this.switchToChildContext(...path)
310
- elevation = 0
311
- }
312
- return this.currentContext
313
- }
314
-
315
- async switchToChildContext(
316
- ...references: ContextReference<TDriver, TContext, TElement, TSelector>[]
317
- ): Promise<Context<TDriver, TContext, TElement, TSelector>> {
318
- if (this.isNative) throw new Error('Contexts are supported only for web drivers')
319
- this._logger.log('Switching to a child context with depth:', references.length)
320
- for (const reference of references) {
321
- if (reference === this.mainContext) continue
322
- const context = await this.currentContext.context(reference)
323
- await context.focus()
324
- }
325
- return this.currentContext
326
- }
327
-
328
- async normalizeRegion(region: types.Region): Promise<types.Region> {
329
- if (this.isWeb || !utils.types.has(this._driverInfo, ['viewportSize', 'statusBarHeight'])) return region
330
- const scaledRegion = this.isAndroid ? utils.geometry.scale(region, 1 / this.pixelRatio) : region
331
- return utils.geometry.offsetNegative(scaledRegion, {x: 0, y: this.statusBarHeight})
332
- }
333
-
334
- async getRegionInViewport(
335
- context: Context<TDriver, TContext, TElement, TSelector>,
336
- region: types.Region,
337
- ): Promise<types.Region> {
338
- await context.focus()
339
- return context.getRegionInViewport(region)
340
- }
341
-
342
- async element(selector: types.Selector<TSelector>): Promise<Element<TDriver, TContext, TElement, TSelector>> {
343
- return this.currentContext.element(selector)
344
- }
345
-
346
- async elements(selector: types.Selector<TSelector>): Promise<Element<TDriver, TContext, TElement, TSelector>[]> {
347
- return this.currentContext.elements(selector)
348
- }
349
-
350
- async execute(script: ((arg: any) => any) | string, arg?: any): Promise<any> {
351
- return this.currentContext.execute(script, arg)
352
- }
353
-
354
- async takeScreenshot(): Promise<Buffer | string> {
355
- const data = await this._spec.takeScreenshot(this.target)
356
- return utils.types.isString(data) ? data.replace(/[\r\n]+/g, '') : data
357
- }
358
-
359
- async getViewportSize(): Promise<types.Size> {
360
- let size
361
- if (this.isNative) {
362
- this._logger.log('Extracting viewport size from native driver')
363
- if (this._driverInfo?.viewportSize) {
364
- size = this._driverInfo.viewportSize
365
- } else {
366
- size = await this.getDisplaySize()
367
- if (size.height > size.width) {
368
- const orientation = await this.getOrientation()
369
- if (orientation === 'landscape') {
370
- size = {width: size.height, height: size.width}
371
- }
372
- }
373
- }
374
- } else if (this._spec.getViewportSize) {
375
- this._logger.log('Extracting viewport size from web driver using spec method')
376
- size = await this._spec.getViewportSize(this.target)
377
- } else {
378
- this._logger.log('Extracting viewport size from web driver using js snippet')
379
- size = await this.mainContext.execute(snippets.getViewportSize)
380
- }
381
-
382
- this._logger.log('Extracted viewport size', size)
383
-
384
- return size
385
- }
386
-
387
- async setViewportSize(size: types.Size): Promise<void> {
388
- if (this.isMobile) return
389
- if (this._spec.setViewportSize) {
390
- this._logger.log('Setting viewport size to', size, 'using spec method')
391
- await this._spec.setViewportSize(this.target, size)
392
- return
393
- }
394
-
395
- this._logger.log('Setting viewport size to', size, 'using workaround')
396
-
397
- const requiredViewportSize = size
398
- let currentViewportSize = await this.getViewportSize()
399
- if (utils.geometry.equals(currentViewportSize, requiredViewportSize)) return
400
-
401
- let currentWindowSize = await this._spec.getWindowSize(this.target)
402
- this._logger.log('Extracted window size', currentWindowSize)
403
-
404
- let attempt = 0
405
- while (attempt++ < 3) {
406
- const requiredWindowSize = {
407
- width: currentWindowSize.width + (requiredViewportSize.width - currentViewportSize.width),
408
- height: currentWindowSize.height + (requiredViewportSize.height - currentViewportSize.height),
409
- }
410
- this._logger.log(`Attempt #${attempt} to set viewport size by setting window size to`, requiredWindowSize)
411
- await this._spec.setWindowSize(this.target, requiredWindowSize)
412
- await utils.general.sleep(3000)
413
- currentWindowSize = requiredWindowSize
414
- currentViewportSize = await this.getViewportSize()
415
- if (utils.geometry.equals(currentViewportSize, requiredViewportSize)) return
416
- this._logger.log(`Attempt #${attempt} to set viewport size failed. Current viewport:`, currentViewportSize)
417
- }
418
-
419
- throw new Error('Failed to set viewport size!')
420
- }
421
-
422
- async getDisplaySize(): Promise<types.Size> {
423
- if (this.isWeb) return
424
- const size = await this._spec.getWindowSize(this.target)
425
- return this.isAndroid ? utils.geometry.scale(size, 1 / this.pixelRatio) : size
426
- }
427
-
428
- async getOrientation(): Promise<'portrait' | 'landscape'> {
429
- if (this.isWeb) return
430
- const orientation = this._spec.getOrientation(this.target)
431
- this._logger.log('Extracted device orientation:', orientation)
432
- return orientation
433
- }
434
-
435
- async getTitle(): Promise<string> {
436
- if (this.isNative) return null
437
- const title = await this._spec.getTitle(this.target)
438
- this._logger.log('Extracted title:', title)
439
- return title
440
- }
441
-
442
- async getUrl(): Promise<string> {
443
- if (this.isNative) return null
444
- const url = this._spec.getUrl(this.target)
445
- this._logger.log('Extracted url:', url)
446
- return url
447
- }
448
-
449
- async visit(url: string): Promise<void> {
450
- await this._spec.visit(this.target, url)
451
- }
452
- }