forked from webdriverio/webdriverio
/
utils.js
343 lines (308 loc) · 10.3 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import logger from '@wdio/logger'
import command from './command'
import merge from 'lodash.merge'
import WebDriverProtocol from '../protocol/webdriver.json'
import MJsonWProtocol from '../protocol/mjsonwp.json'
import JsonWProtocol from '../protocol/jsonwp.json'
import AppiumProtocol from '../protocol/appium.json'
import ChromiumProtocol from '../protocol/chromium.json'
import SauceLabsProtocol from '../protocol/saucelabs.json'
const log = logger('webdriver')
/**
* check if WebDriver requests was successful
* @param {Object} body body payload of response
* @return {Boolean} true if request was successful
*/
export function isSuccessfulResponse (statusCode, body) {
/**
* response contains a body
*/
if (!body || typeof body.value === 'undefined') {
log.debug('request failed due to missing body')
return false
}
/**
* ignore failing element request to enable lazy loading capability
*/
if (
body.status === 7 && body.value && body.value.message &&
(
body.value.message.toLowerCase().startsWith('no such element') ||
// Appium
body.value.message === 'An element could not be located on the page using the given search parameters.' ||
// Internet Explorter
body.value.message.toLowerCase().startsWith('unable to find element')
)
) {
return true
}
/**
* if it has a status property, it should be 0
* (just here to stay backwards compatible to the jsonwire protocol)
*/
if (body.status && body.status !== 0) {
log.debug(`request failed due to status ${body.status}`)
return false
}
const hasErrorResponse = body.value && (body.value.error || body.value.stackTrace || body.value.stacktrace)
/**
* check status code
*/
if (statusCode === 200 && !hasErrorResponse) {
return true
}
/**
* if an element was not found we don't flag it as failed request because
* we lazy load it
*/
if (statusCode === 404 && body.value && body.value.error === 'no such element') {
return true
}
/**
* that has no error property (Appium only)
*/
if (hasErrorResponse) {
log.debug('request failed due to response error:', body.value.error)
return false
}
return true
}
/**
* checks if command argument is valid according to specificiation
*
* @param {*} arg command argument
* @param {Object} expectedType parameter type (e.g. `number`, `string[]` or `(number|string)`)
* @return {Boolean} true if argument is valid
*/
export function isValidParameter (arg, expectedType) {
let shouldBeArray = false
if (expectedType.slice(-2) === '[]') {
expectedType = expectedType.slice(0, -2)
shouldBeArray = true
}
/**
* check type of each individual array element
*/
if (shouldBeArray) {
if (!Array.isArray(arg)) {
return false
}
} else {
/**
* transform to array to have a unified check
*/
arg = [arg]
}
for (const argEntity of arg) {
const argEntityType = getArgumentType(argEntity)
if (!argEntityType.match(expectedType)) {
return false
}
}
return true
}
/**
* get type of command argument
*/
export function getArgumentType (arg) {
return arg === null ? 'null' : typeof arg
}
/**
* creates the base prototype for the webdriver monad
*/
export function getPrototype ({ isW3C, isChrome, isMobile, isSauce }) {
const prototype = {}
const ProtocolCommands = merge(
/**
* if mobile apply JSONWire and WebDriver protocol because
* some legacy JSONWire commands are still used in Appium
* (e.g. set/get geolocation)
*/
isMobile
? merge(JsonWProtocol, WebDriverProtocol)
: isW3C ? WebDriverProtocol : JsonWProtocol,
/**
* only apply mobile protocol if session is actually for mobile
*/
isMobile ? merge(MJsonWProtocol, AppiumProtocol) : {},
/**
* only apply special Chrome commands if session is using Chrome
*/
isChrome ? ChromiumProtocol : {},
/**
* only Sauce Labs specific vendor commands
*/
isSauce ? SauceLabsProtocol : {}
)
for (const [endpoint, methods] of Object.entries(ProtocolCommands)) {
for (const [method, commandData] of Object.entries(methods)) {
prototype[commandData.command] = { value: command(method, endpoint, commandData) }
}
}
return prototype
}
/**
* get command call structure
* (for logging purposes)
*/
export function commandCallStructure (commandName, args) {
const callArgs = args.map((arg) => {
if (typeof arg === 'string') {
arg = `"${arg}"`
} else if (typeof arg === 'function') {
arg = '<fn>'
} else if (arg === null) {
arg = 'null'
} else if (typeof arg === 'object') {
arg = '<object>'
} else if (typeof arg === 'undefined') {
arg = typeof arg
}
return arg
}).join(', ')
return `${commandName}(${callArgs})`
}
/**
* check if session is based on W3C protocol based on the /session response
* @param {Object} capabilities caps of session response
* @return {Boolean} true if W3C (browser)
*/
export function isW3C (capabilities) {
/**
* JSONWire protocol doesn't return a property `capabilities`.
* Also check for Appium response as it is using JSONWire protocol for most of the part.
*/
if (!capabilities) {
return false
}
/**
* assume session to be a WebDriver session when
* - capabilities are returned
* (https://w3c.github.io/webdriver/#dfn-new-sessions)
* - it is an Appium session (since Appium is full W3C compliant)
*/
const isAppium = capabilities.automationName || capabilities.deviceName
const hasW3CCaps = (
capabilities.platformName &&
capabilities.browserVersion &&
/**
* local safari doesn't provide platformVersion therefor
* check also for setWindowRect
*/
(capabilities.platformVersion || capabilities.setWindowRect)
)
return Boolean(hasW3CCaps || isAppium)
}
/**
* check if session is run by Chromedriver
* @param {Object} capabilities caps of session response
* @return {Boolean} true if run by Chromedriver
*/
export function isChrome (caps) {
return (
Boolean(caps.chrome) ||
Boolean(caps['goog:chromeOptions'])
)
}
/**
* check if current platform is mobile device
*
* @param {Object} caps capabilities
* @return {Boolean} true if platform is mobile device
*/
export function isMobile (caps) {
return Boolean(
(typeof caps['appium-version'] !== 'undefined') ||
(typeof caps['device-type'] !== 'undefined') || (typeof caps['deviceType'] !== 'undefined') ||
(typeof caps['device-orientation'] !== 'undefined') || (typeof caps['deviceOrientation'] !== 'undefined') ||
(typeof caps.deviceName !== 'undefined') ||
// Check browserName for specific values
(caps.browserName === '' ||
(caps.browserName !== undefined && (caps.browserName.toLowerCase() === 'ipad' || caps.browserName.toLowerCase() === 'iphone' || caps.browserName.toLowerCase() === 'android')))
)
}
/**
* check if session is run on iOS device
* @param {Object} capabilities caps of session response
* @return {Boolean} true if run on iOS device
*/
export function isIOS (caps) {
return Boolean(
(caps.platformName && caps.platformName.match(/iOS/i)) ||
(caps.deviceName && caps.deviceName.match(/(iPad|iPhone)/i))
)
}
/**
* check if session is run on Android device
* @param {Object} capabilities caps of session response
* @return {Boolean} true if run on Android device
*/
export function isAndroid (caps) {
return Boolean(
(caps.platformName && caps.platformName.match(/Android/i)) ||
(caps.browserName && caps.browserName.match(/Android/i))
)
}
/**
* detects if session is run on Sauce with extended debugging enabled
* @param {string} hostname hostname of session request
* @param {object} capabilities session capabilities
* @return {Boolean} true if session is running on Sauce with extended debugging enabled
*/
export function isSauce (hostname, caps) {
return Boolean(
hostname &&
hostname.includes('saucelabs') &&
(
caps.extendedDebugging ||
(
caps['sauce:options'] &&
caps['sauce:options'].extendedDebugging
)
)
)
}
/**
* returns information about the environment
* @param {Object} hostname name of the host to run the session against
* @param {Object} capabilities caps of session response
* @return {Object} object with environment flags
*/
export function environmentDetector ({ hostname, capabilities, requestedCapabilities }) {
return {
isW3C: isW3C(capabilities),
isChrome: isChrome(capabilities),
isMobile: isMobile(capabilities),
isIOS: isIOS(capabilities),
isAndroid: isAndroid(capabilities),
isSauce: isSauce(hostname, requestedCapabilities.w3cCaps.alwaysMatch)
}
}
/**
* helper method to determine the error from webdriver response
* @param {Object} body body object
* @return {Object} error
*/
export function getErrorFromResponseBody (body) {
if (!body) {
return new Error('Response has empty body')
}
if (typeof body === 'string' && body.length) {
return new Error(body)
}
if (typeof body !== 'object' || !body.value) {
return new Error('unknown error')
}
return new CustomRequestError(body)
}
//Exporting for testability
export class CustomRequestError extends Error {
constructor(body) {
super(body.value.message || body.value.class || 'unknown error')
if (body.value.error) {
this.name = body.value.error
} else if (body.value.message && body.value.message.includes('stale element reference')) {
this.name = 'stale element reference'
}
}
}