forked from webdriverio/webdriverio
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcommand.ts
More file actions
268 lines (237 loc) · 11.1 KB
/
command.ts
File metadata and controls
268 lines (237 loc) · 11.1 KB
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
import logger from '@testplane/wdio-logger'
import { commandCallStructure, isValidParameter, getArgumentType } from '@testplane/wdio-utils'
import { WebDriverBidiProtocol, type CommandEndpoint } from '@testplane/wdio-protocols'
import { environment } from './environment.js'
import type { BidiHandler } from './bidi/handler.js'
import type { WebDriverResponse } from './request/types.js'
import type { BaseClient, BidiCommands, BidiResponses, WebDriverResultEvent } from './types.js'
const log = logger('webdriver')
const BIDI_COMMANDS: BidiCommands[] = Object.values(WebDriverBidiProtocol).map((def) => def.socket.command)
const sessionAbortListeners = new Map<string, Set<AbortController> | null>()
export default function (
method: string,
endpointUri: string,
commandInfo: CommandEndpoint,
doubleEncodeVariables = false
) {
const { command, deprecated, ref, parameters, variables = [], isHubCommand = false } = commandInfo
return async function protocolCommand (this: BaseClient, ...args: unknown[]): Promise<WebDriverResponse | BidiResponses | void> {
const isBidiCommand = BIDI_COMMANDS.includes(command as BidiCommands)
let endpoint = endpointUri // clone endpointUri in case we change it
const commandParams = [...variables.map((v) => Object.assign(v, {
/**
* url variables are:
*/
required: true, // always required as they are part of the endpoint
type: 'string' // have to be always type of string
})), ...parameters]
const commandUsage = `${command}(${commandParams.map((p) => p.name).join(', ')})`
const moreInfo = `\n\nFor more info see ${ref}\n`
const body: Record<string, unknown> = {}
/**
* log deprecation warning if command is deprecated
*
* Note: there are situations where we have to use deprecated commands, e.g. `switchToFrame`
* internally where we don't want to have the message shown to the user. In these cases we
* use the `DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS` env variable to suppress the message.
*/
if (typeof deprecated === 'string' && !process.env.DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS) {
const warning = deprecated.replace('This command', `The "${command}" command`)
log.warn(warning)
// console.warn(`⚠️ [WEBDRIVERIO DEPRECATION NOTICE] ${warning}`)
}
/**
* Throw this error message for all WebDriver Bidi commands.
* In case a successful connection to the browser bidi interface was established,
* we attach a custom Bidi prototype to the browser instance.
*/
if (isBidiCommand) {
throw new Error(
`Failed to execute WebDriver Bidi command "${command}" as no Bidi session ` +
'was established. Make sure you enable it by setting "webSocketUrl: true" ' +
'in your capabilities and verify that your environment and browser supports it.'
)
}
/**
* parameter check
*/
const minAllowedParams = commandParams.filter((param) => param.required).length
if (args.length < minAllowedParams || args.length > commandParams.length) {
const parameterDescription = commandParams.length
? `\n\nProperty Description:\n${commandParams.map((p) => ` "${p.name}" (${p.type}): ${p.description}`).join('\n')}`
: ''
throw new Error(
`Wrong parameters applied for ${command}\n` +
`Usage: ${commandUsage}` +
parameterDescription +
moreInfo
)
}
/**
* parameter type check
*/
for (const [it, arg] of Object.entries(args)) {
if (isBidiCommand) {
break
}
const i = parseInt(it, 10)
const commandParam = commandParams[i]
if (!isValidParameter(arg, commandParam.type)) {
/**
* ignore if argument is not required
*/
if (typeof arg === 'undefined' && !commandParam.required) {
continue
}
const actual = commandParam.type.endsWith('[]')
? `(${(Array.isArray(arg) ? arg : [arg]).map((a) => getArgumentType(a))})[]`
: getArgumentType(arg)
throw new Error(
`Malformed type for "${commandParam.name}" parameter of command ${command}\n` +
`Expected: ${commandParam.type}\n` +
`Actual: ${actual}` +
moreInfo
)
}
/**
* inject url variables
*/
if (i < variables.length) {
const encodedArg = doubleEncodeVariables ? encodeURIComponent(encodeURIComponent(arg as string)) : encodeURIComponent(arg as string)
endpoint = endpoint.replace(`:${commandParams[i].name}`, encodedArg)
continue
}
/**
* rest of args are part of body payload
*/
body[commandParams[i].name] = arg
}
/**
* Make sure we pass along an abort signal to the request class so we
* can abort the request as well as any retries in case the session is
* deleted.
*
* Abort the attempt to make the WebDriver call, except for:
* - `deleteSession` calls which should go through in case we retry the command.
* - requests that don't require a session.
*/
const { isAborted, cleanup } = manageSessionAbortions.call(this)
const requiresSession = endpointUri.includes('/:sessionId/')
if (isAborted && command !== 'deleteSession' && requiresSession) {
throw new Error(`Trying to run command "${commandCallStructure(command, args)}" after session has been deleted, aborting request without executing it`)
}
const request = new environment.value.Request(method, endpoint, body, isHubCommand)
request.on('performance', (...args) => this.emit('request.performance', ...args))
this.emit('command', { command, method, endpoint, body })
log.info('COMMAND', commandCallStructure(command, args))
/**
* use then here so we can better unit test what happens before and after the request
*/
return request.makeRequest(this.options, this.sessionId).then((result) => {
if (typeof result.value !== 'undefined') {
let resultLog = result.value
if (/screenshot|recording/i.test(command) && typeof result.value === 'string' && result.value.length > 64) {
resultLog = `${result.value.slice(0, 61)}...`
} else if (command === 'executeScript' && typeof body.script === 'string' && body.script.includes('(() => window.__wdioEvents__)')) {
resultLog = `[${(result.value as unknown[]).length} framework events captured]`
}
log.info('RESULT', resultLog)
}
this.emit('result', { command, method, endpoint, body, result })
if (command === 'deleteSession') {
/**
* close WebDriver Bidi socket
*/
const browser = this as { _bidiHandler?: BidiHandler }
browser._bidiHandler?.close()
const shutdownDriver = (body.deleteSessionOpts as { shutdownDriver?: boolean })?.shutdownDriver !== false
/**
* kill driver process if there is one
*/
if (shutdownDriver && 'wdio:driverPID' in this.capabilities && this.capabilities['wdio:driverPID']) {
log.info(`Kill driver process with PID ${this.capabilities['wdio:driverPID']}`)
try {
const killedSuccessfully = process.kill(this.capabilities['wdio:driverPID'], 'SIGKILL')
if (!killedSuccessfully) {
log.warn('Failed to kill driver process, manually clean-up might be required')
}
} catch (err) {
log.warn('Failed to kill driver process', err)
}
setTimeout(() => {
/**
* clear up potential leaked TLS Socket handles
* see https://github.com/puppeteer/puppeteer/pull/10667
*/
for (const handle of process._getActiveHandles()) {
if (handle.servername && handle.servername.includes('edgedl.me')) {
handle.destroy()
}
}
}, 10)
}
/**
* clear logger stream if session has been terminated
*/
if (!process.env.WDIO_WORKER_ID) {
logger.clearLogger()
}
}
return result.value as WebDriverResponse | BidiResponses
}).catch((error) => {
this.emit('result', { command, method, endpoint, body, result: { error } })
throw error
}).finally(() => {
cleanup()
})
}
}
/**
* Manage session abortions, e.g. abort requests after session has been deleted.
* @param this - WebDriver client instance
* @returns Object with `isAborted`, `abortSignal`, and `cleanup`
*/
function manageSessionAbortions (this: BaseClient): {
isAborted: boolean
abortSignal?: AbortSignal
cleanup: () => void
} {
const abort = new AbortController()
const abortOnSessionEnd = (result: WebDriverResultEvent) => {
if (result.command !== 'deleteSession') {
return
}
const abortListeners = sessionAbortListeners.get(this.sessionId)
if (abortListeners) {
for (const abortListener of abortListeners) {
abortListener.abort()
}
abortListeners.clear()
sessionAbortListeners.set(this.sessionId, null)
}
}
let abortListenerForCurrentSession = sessionAbortListeners.get(this.sessionId)
if (typeof abortListenerForCurrentSession === 'undefined') {
abortListenerForCurrentSession = new Set()
sessionAbortListeners.set(this.sessionId, abortListenerForCurrentSession)
this.on('result', abortOnSessionEnd)
}
/**
* If the session has been deleted, we don't want to run any further commands
*/
if (abortListenerForCurrentSession === null) {
return { isAborted: true, abortSignal: undefined, cleanup: () => {} }
}
/**
* listen for session deletion and abort all requests
*/
abortListenerForCurrentSession.add(abort)
return {
isAborted: false,
abortSignal: abort.signal,
cleanup: () => {
this.off('result', abortOnSessionEnd)
abortListenerForCurrentSession?.delete(abort)
}
}
}