Permalink
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Aug 10, 2017
Aug 10, 2017
Aug 10, 2017
Aug 24, 2017
Aug 24, 2017
Aug 10, 2017
Aug 10, 2017
Aug 24, 2017
Aug 10, 2017
May 19, 2017
Jul 16, 2017
Jul 31, 2017
May 31, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
May 20, 2017
May 19, 2017
Jul 16, 2017
Jul 31, 2017
Aug 1, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 30, 2017
Jun 1, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
Jul 31, 2017
Jul 31, 2017
May 19, 2017
May 19, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
Jul 31, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
Jul 31, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 31, 2017
May 20, 2017
Jul 16, 2017
Jul 16, 2017
May 30, 2017
Jul 16, 2017
Jun 22, 2017
Jul 16, 2017
Jul 16, 2017
Jul 31, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
May 19, 2017
Jul 16, 2017
Jul 16, 2017
May 30, 2017
Jul 16, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
Jul 16, 2017
Jun 14, 2017
Aug 28, 2017
Aug 28, 2017
Aug 28, 2017
May 19, 2017
May 30, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
May 19, 2017
May 30, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
May 21, 2017
Aug 6, 2017
May 21, 2017
Aug 6, 2017
Aug 6, 2017
Jul 16, 2017
May 21, 2017
Aug 6, 2017
May 21, 2017
Aug 6, 2017
Aug 6, 2017
Aug 6, 2017
Aug 28, 2017
Aug 28, 2017
Aug 28, 2017
Aug 6, 2017
May 30, 2017
Jul 16, 2017
Jul 16, 2017
Jul 16, 2017
Aug 6, 2017
Aug 6, 2017
May 31, 2017
Jul 16, 2017
May 31, 2017
Jul 16, 2017
Jul 16, 2017
Newer
100644
1213 lines (1068 sloc)
43.5 KB
3
6
const Promise = require('bluebird')
7
const fs = require('mz/fs')
8
const _ = require('lodash')
9
10
const {
11
browserIsInitialized,
12
sleep,
13
fixSelector,
14
interleaveArrayToObject,
15
promiseTimeout,
16
objectToEncodedUri
17
} = require('./util')
18
20
* Injects JavaScript in the page
21
*
22
* Modules available: jQuery, jquery, jQuery.slim and jquery.slim
23
* @example inject('jquery')
24
*
25
* You can use jsdelivr to inject any npm or github package in the page
26
* @example inject('https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js')
27
* @example inject('https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js')
28
*
29
* You can inject a local Javascript file
30
* @example inject('./custom-file.js')
31
* @example inject(__dirname + '/path/to/file.js')
32
*
33
* Note: the path will be resolved with `require.resolve()` so you can include
34
* files that are in `node_modules` simply by installing them with NPM
35
* @example inject('jquery/dist/jquery.min')
36
* @example inject('lodash/dist/lodash.min')
37
*
38
* @param {string} moduleOrScript - Javascript code, file, url or name of the
39
* module to inject.
41
exports.inject = async function inject (moduleOrScript) {
42
debug(`:: inject => Inject "${moduleOrScript}" on the root document...`)
44
45
if (moduleOrScript.slice(0, 8) === 'https://' || moduleOrScript.slice(0, 7) === 'http://') {
46
return this.injectRemoteScript(moduleOrScript)
47
} else {
48
switch (moduleOrScript) {
49
// TODO: Inject modules from local file
50
case 'jquery':
51
case 'jQuery':
52
return this.evaluateAsync(function () {
53
return new Promise((resolve, reject) => {
54
if (typeof window.jQuery === 'undefined') {
55
const script = document.createElement('script')
56
script.src = 'https://code.jquery.com/jquery-3.2.1.min.js'
57
script.onload = resolve
58
script.onerror = reject
59
document.head.appendChild(script)
60
} else {
61
resolve()
62
}
63
})
68
return this.evaluateAsync(function () {
69
return new Promise((resolve, reject) => {
70
if (typeof window.jQuery === 'undefined') {
71
const script = document.createElement('script')
72
script.src = 'https://code.jquery.com/jquery-3.2.1.slim.min.js'
73
script.onload = resolve
74
script.onerror = reject
75
document.head.appendChild(script)
76
} else {
77
resolve()
78
}
79
})
80
})
81
82
default:
83
const file = require.resolve(moduleOrScript)
84
const buff = await fs.readFile(file)
85
return this.injectScript(buff.toString())
86
}
90
/**
91
* Injects a remote script in the page
92
* @example injectRemoteScript(https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js)
93
* @param {string} src - Url to remote JavaScript file
94
*/
95
exports.injectRemoteScript = async function injectRemoteScript (src) {
96
debug(`:: injectRemoteScript => Inject "${src}" on the root document...`)
97
browserIsInitialized.call(this)
98
const result = await this.evaluateAsync(function (src) {
99
return new Promise((resolve, reject) => {
100
const script = document.createElement('script')
101
script.src = src
102
script.onload = resolve
103
script.onerror = reject
104
document.head.appendChild(script)
105
})
106
}, src)
107
debug(`:: injectRemoteScript => script "${src}" was injected successfully!`)
108
109
return result
110
}
111
112
/**
113
* Injects code in the DOM as script tag
114
* @param {string} script - Code to be injected and evaluated in the DOM
115
*/
116
exports.injectScript = async function injectScript (script) {
117
debug(`:: injectScript => Injecting "${script.slice(0, 10)}..." on the root document...`)
118
browserIsInitialized.call(this)
119
const result = await this.evaluate(function (text) {
120
const script = document.createElement('script')
121
script.text = text
122
document.head.appendChild(script)
123
}, script)
124
debug(`:: injectScript => script "${script.slice(0, 10)}..." was injected successfully!`)
125
126
return result
127
}
128
129
/**
130
* Evaluates a fn in the context of the browser
131
* @param fn {function} - The function to evaluate in the browser
132
* @param args {*} - The arguments to pass to the function
133
*/
134
exports.evaluate = async function evaluate (fn, ...args) {
135
debug(`:: evaluate => ${fn}`, ...args)
136
browserIsInitialized.call(this)
137
const exp = args && args.length > 0 ? `(${String(fn)}).apply(null, ${JSON.stringify(args)})` : `(${String(fn)}).apply(null)`
142
debug(`:: evaluate => Function ${fn} evaluated successfully!`, result)
143
144
return result
146
147
/**
148
* Evaluates an async fn in the context of the browser
149
* @param fn {function} - The function to evaluate in the browser
150
* @param args {*} - The arguments to pass to the function
151
*/
152
exports.evaluateAsync = async function evaluate (fn, ...args) {
153
debug(`:: evaluate => ${fn}`, ...args)
154
browserIsInitialized.call(this)
155
const exp = args && args.length > 0 ? `(${String(fn)}).apply(null, ${JSON.stringify(args)})` : `(${String(fn)}).apply(null)`
156
return this.client.Runtime.evaluate({
157
expression: exp,
158
returnByValue: true,
159
awaitPromise: true
160
})
161
}
162
163
/**
164
* Evaluates a fn in the context of a passed node
165
* @param {NodeObject} node - The Node Object used to get the context
166
* @param fn {function} - The function to evaluate in the browser
167
* @param args {*} - The arguments to pass to the function
168
*/
169
exports.evaluateOnNode = async function evaluateOnNode (node, fn, ...args) {
170
debug(`:: evaluateOnNode => Evaluate on node "${node.nodeId}", fn: ${fn}`, ...args)
171
browserIsInitialized.call(this)
172
const exp = args && args.length > 0 ? `function() {(${String(fn)}).apply(null, ${JSON.stringify(args)})}` : `function() {(${String(fn)}).apply(null)}`
176
return this.client.Runtime.callFunctionOn({
177
objectId: resolvedNode.object.objectId,
178
functionDeclaration: exp
179
})
180
}
182
/**
183
* Navigates to a URL
184
* @param {string} url - The URL to navigate to
185
* @param {object} options - The options object.
186
* options:
187
* @property {number} timeout - Time in ms that this method has to wait until the
188
* "pageLoaded" event is triggered. If the value is 0 or false, it means that it doesn't
189
* have to wait after calling the "Page.navigate" method
190
*/
191
exports.goTo = async function (url, opt = {}) {
192
debug(`:: goTo => URL "${url}" | Options: ${JSON.stringify(opt, null, 2)}`)
193
browserIsInitialized.call(this)
195
const options = Object.assign({
196
timeout: this.options.browser.loadPageTimeout
197
}, opt)
199
if (options.bypassCertificate) {
200
this.client.Security.certificateError(({eventId}) => {
201
this.client.Security.handleCertificateError({
202
eventId,
203
action: 'continue'
210
await this.client.Page.navigate({ url })
211
212
if (options.timeout && typeof options.timeout) {
213
await this.waitForPageToLoad(options.timeout)
214
}
217
218
/**
219
* Get the value of an Node.
220
* @param {NodeObject} node - The Node Object
221
* @return {object} - Object containing type and value of the element
222
*/
223
exports.getNodeValue = async function (node) {
224
debug(`:: getNodeValue => Get value from Node "${node.nodeId}"`)
225
browserIsInitialized.call(this)
227
const nodeAttrs = interleaveArrayToObject((await this.client.DOM.getAttributes(node)).attributes)
228
const value = nodeAttrs.value
229
debug(`:: getNodeValue => Value from Node "${node.nodeId}":`, value)
230
return value
231
}
233
/**
234
* Get the value of an element.
235
* @param {string} selector - The target selector
237
* @return {object} - Object containing type and value of the element
238
*/
239
exports.getValue = async function (selector, frameId) {
240
debug(`:: getValue => Get value from element matching "${selector}"`)
241
browserIsInitialized.call(this)
242
selector = fixSelector(selector)
243
244
const node = await this.querySelector(selector, frameId)
245
const { className: htmlObjectType } = (await this.client.DOM.resolveNode(node)).object
246
247
let value
248
// If the node object type is HTMLTextAreaElement, then get the value from the console.
249
/**
250
* TODO: Take the value from the DOM Node. For some reason, there're some pages where is not possible
251
* to get the textarea value, as its nodeId refreshes all the time
252
*/
253
if (htmlObjectType === 'HTMLTextAreaElement') {
254
// Escape selector for onConsole use
255
selector = selector.replace(/\\/g, '\\')
256
const textareaEvaluate = await this.evaluate(function (selector) {
257
return document.querySelector(selector).value
258
}, selector)
259
value = textareaEvaluate.result.value
260
} else {
261
value = await this.getNodeValue(node)
262
}
263
debug(`:: getValue => Value from element matching "${selector}":`, value)
264
return value
265
}
266
267
/**
268
* Set the value of an element.
269
* @param {NodeObject} node - The Node Object
270
* @param {string} value - The value to set the node to (it may be an array of values when the node is a multiple "HTMLSelectElement")
272
exports.setNodeValue = async function (node, value) {
273
debug(`:: setNodeValue => Set node value "${value}" to node "${node.nodeId}"`)
274
browserIsInitialized.call(this)
275
276
const { className: htmlObjectType } = (await this.client.DOM.resolveNode(node)).object
277
278
if (htmlObjectType === 'HTMLSelectElement') {
279
const selectOptions = await this.client.DOM.querySelectorAll({
280
nodeId: node.nodeId,
281
selector: 'option'
282
})
283
284
// Ensure the selectedValuesArray is an array
285
let selectedValuesArray = value
286
if (!Array.isArray(value)) {
287
selectedValuesArray = [value]
288
}
290
// Iterate over all the options of the select
291
for (let option of selectOptions.nodeIds) {
292
const optionValue = await this.getNodeValue({nodeId: option})
293
if (selectedValuesArray.indexOf(optionValue) > -1) {
294
// If the option value is in the list of selectedValues, then set the "selected" property
295
await this.client.DOM.setAttributeValue({
296
nodeId: option,
297
name: 'selected',
298
value: 'true'
299
})
300
} else {
301
// Otherwise, remove the "selected" property
302
await this.client.DOM.removeAttribute({
303
nodeId: option,
304
name: 'selected'
305
})
306
}
307
}
308
} else {
309
await this.client.DOM.setAttributeValue({
310
nodeId: node.nodeId,
311
name: 'value',
312
value: value
313
})
314
}
315
}
316
317
/**
318
* Set the value of an element.
319
* @param {string} selector - The selector to set the value of.
320
* @param {string} [value] - The value to set the selector to
322
*/
323
exports.setValue = async function (selector, value, frameId) {
324
debug(`:: setValue => Set value "${value}" to element matching selector "${selector}" in frame "${frameId || 'root'}"`)
325
browserIsInitialized.call(this)
326
selector = fixSelector(selector)
327
328
const node = await this.querySelector(selector, frameId)
329
const { className: htmlObjectType } = (await this.client.DOM.resolveNode(node)).object
330
331
// If the node object type is HTMLTextAreaElement, then get the value from the console.
332
/**
333
* TODO: Take the value from the DOM Node. For some reason, there're some pages where is not possible
334
* to get the textarea value, as its nodeId refreshes all the time
335
*/
336
if (htmlObjectType === 'HTMLTextAreaElement') {
337
// Escape selector for onConsole use
338
selector = selector.replace(/\\/g, '\\')
339
await this.evaluate(function (selector, value) {
340
document.querySelector(selector).value = value
341
}, selector, value)
342
} else {
343
await this.setNodeValue(node, value)
344
}
345
}
346
347
/**
348
* Fills a selector of an input or textarea element with the passed value
349
* @param {string} selector - The selector
350
* @param {string} value - The value to fill the element matched in the selector
351
* @param {string} frameId - The FrameID where the selector should be searched
352
*/
353
exports.fill = async function (selector, value, frameId) {
354
debug(`:: fill => Fill selector "${selector}" with value "${value}" in frame "${frameId || 'root'}"`)
355
browserIsInitialized.call(this)
356
selector = fixSelector(selector)
357
358
const node = await this.querySelector(selector, frameId)
359
360
const nodeAttrs = interleaveArrayToObject((await this.client.DOM.getAttributes(node)).attributes)
361
const { className: htmlObjectType } = (await this.client.DOM.resolveNode(node)).object
362
363
const type = nodeAttrs.type
364
const supportedTypes = ['color', 'date', 'datetime', 'datetime-local', 'email', 'hidden', 'month', 'number', 'password', 'range', 'search', 'tel', 'text', 'time', 'url', 'week']
365
// Check https://developer.mozilla.org/en-US/docs/Web/API for more info about HTML Objects
366
const isValidInput = htmlObjectType === 'HTMLInputElement' && (typeof type === 'undefined' || supportedTypes.indexOf(type) !== -1)
367
const isTextArea = htmlObjectType === 'HTMLTextAreaElement'
368
369
if (isTextArea || isValidInput) {
370
await this.setNodeValue(node, value)
371
} else {
372
throw new Error(`DOM element ${selector} can not be filled`)
373
}
374
}
375
376
/**
377
* Clear an input field.
378
* @param {string} selector - The selector to clear.
380
*/
381
exports.clear = async function (selector, frameId) {
382
debug(`:: clear => Clear the field of the selector "${selector}"`)
383
browserIsInitialized.call(this)
384
selector = fixSelector(selector)
385
const node = await this.querySelector(selector, frameId)
386
await this.setNodeValue(node, '')
387
}
388
389
/**
390
* Returns the node associated to the passed selector
391
* @param {string} selector - The selector to find
393
* @return {NodeId Object} - NodeId Object (+info: https://chromedevtools.github.io/devtools-protocol/tot/DOM/#type-NodeId)
394
*/
395
exports.querySelector = async function (selector, frameId) {
396
debug(`:: querySelector => Get element from selector "${selector}" in frame "${frameId || 'root'}"`)
397
browserIsInitialized.call(this)
398
selector = fixSelector(selector)
399
400
const DOM = this.client.DOM
401
402
try {
403
const document = (frameId) ? await DOM.getFlattenedDocument() : await DOM.getDocument()
404
let node = (frameId) ? _.find(document.nodes, { frameId: frameId }) : document.root
405
406
// If the node is an iFrame, the #document node is in the contentDocument attribute of the node
407
if (node.nodeName === 'IFRAME' && node.contentDocument) {
408
node = node.contentDocument
409
}
410
411
return await DOM.querySelector({
412
nodeId: node.nodeId,
413
selector
414
})
415
} catch (err) {
416
err.name = `Can not query selector ${selector} in frame "${frameId || 'root'}": ${err.name}`
417
throw err
418
}
419
}
420
421
/**
422
* Focus on an element matching the selector
423
* @param {string} selector - The selector to find the element
425
*/
426
exports.focus = async function (selector, frameId) {
427
debug(`:: focus => Focusing on selector "${selector}" in frame "${frameId || 'root'}"`)
428
browserIsInitialized.call(this)
429
selector = fixSelector(selector)
430
431
try {
432
const node = await this.querySelector(selector, frameId)
433
await this.client.DOM.focus({ nodeId: node.nodeId })
434
} catch (err) {
435
throw new Error(`Can not focus to ${selector}. Error: ${err}`)
436
}
437
}
438
439
/**
440
* Simulate a keypress on a selector
442
* @param {string} text - The text to type.
444
* @param {object} options - Lets you send keys like control & shift
445
*/
446
exports.type = async function (selector, text, frameId, opts) {
447
const options = Object.assign({
448
reset: false, // Clear the field first
449
eventType: 'char' // Enum: [ 'keyDown', 'keyUp', 'rawKeyDown', 'char' ]
450
}, opts)
451
452
debug(`:: type => Type text "${text}" in selector "${selector}" | Options: ${JSON.stringify(options, null, 2)}`)
453
selector = fixSelector(selector)
454
function computeModifier (modifierString) {
455
const modifiers = {
456
'ctrl': 0x04000000,
457
'shift': 0x02000000,
458
'alt': 0x08000000,
459
'meta': 0x10000000,
460
'keypad': 0x20000000
461
}
462
let modifier = 0
463
const checkKey = function (key) {
464
if (key in modifiers) {
465
return
466
}
467
debug(key + 'is not a supported key modifier')
468
}
469
if (!modifierString) {
470
return modifier
471
}
472
473
const keys = modifierString.split('+')
474
keys.forEach(checkKey)
475
return keys.reduce(function (acc, key) {
476
return acc | modifiers[key]
477
}, modifier)
478
}
479
480
const modifiers = computeModifier(options && options.modifiers)
481
482
// Clear the field in the selector, if needed
483
if (options.reset) {
484
await this.clear(selector, frameId)
485
}
486
487
// Focus on the selector
488
await this.focus(selector, frameId)
489
490
function isSpecialChar (key) {
491
if (key === '\r') {
492
return true
493
}
494
}
495
498
if (isSpecialChar(key)) {
499
await this.keyboardEvent('rawKeyDown', '', modifiers, 13)
500
await this.keyboardEvent(options.eventType, '\r', modifiers)
501
await this.keyboardEvent('keyUp', '', modifiers, 13)
502
} else {
503
await this.keyboardEvent(options.eventType, key, modifiers)
504
}
505
}
506
}
507
/**
508
* Types text (doesn't matter where it is)
509
* @param {string} text - The text to type.
510
* @param {object} options - Lets you send keys like control & shift
511
*/
512
exports.typeText = async function (text, opts) {
513
const options = Object.assign({
514
reset: false, // Clear the field first
515
eventType: 'char' // Enum: [ 'keyDown', 'keyUp', 'rawKeyDown', 'char' ]
516
}, opts)
517
518
debug(`:: typeText => Type text "${text}" | Options: ${JSON.stringify(options, null, 2)}`)
519
function computeModifier (modifierString) {
520
const modifiers = {
521
'ctrl': 0x04000000,
522
'shift': 0x02000000,
523
'alt': 0x08000000,
524
'meta': 0x10000000,
525
'keypad': 0x20000000
526
}
527
let modifier = 0
528
const checkKey = function (key) {
529
if (key in modifiers) {
530
return
531
}
532
debug(key + 'is not a supported key modifier')
533
}
534
if (!modifierString) {
535
return modifier
536
}
537
538
const keys = modifierString.split('+')
539
keys.forEach(checkKey)
540
return keys.reduce(function (acc, key) {
541
return acc | modifiers[key]
542
}, modifier)
543
}
544
545
const modifiers = computeModifier(options && options.modifiers)
546
547
function isSpecialChar (key) {
548
if (key === '\r') {
549
return true
550
}
551
}
552
553
// Type on the selector
554
for (let key of text.split('')) {
555
if (isSpecialChar(key)) {
556
await this.keyboardEvent('rawKeyDown', '', modifiers, 13)
557
await this.keyboardEvent(options.eventType, '\r', modifiers)
558
await this.keyboardEvent('keyUp', '', modifiers, 13)
559
} else {
560
await this.keyboardEvent(options.eventType, key, modifiers)
561
}
564
565
/**
566
* Select a value in an html select element.
567
* @param {string} selector - The identifier for the select element.
568
* @param {string} value - The value to select.
570
*/
571
exports.select = async function (selector, value, frameId) {
572
debug(`:: select => ${(typeof value !== 'undefined') ? 'Set select value to "' + value + '"' : 'Get select value'} in the select matching selector "${selector}" for frame "${frameId || 'root'}"`)
573
browserIsInitialized.call(this)
574
selector = fixSelector(selector)
575
576
const node = await this.querySelector(selector, frameId)
578
if (typeof value !== 'undefined') {
579
return this.setNodeValue(node, value)
580
} else {
581
return this.getNodeValue(node)
582
}
583
}
584
585
/**
586
* Fire a key event.
587
* @param {string} [type=keypress] - The type of key event.
588
* @param {string} [key=null] - The key to use for the event.
589
* @param {number} [modifier=0] - The keyboard modifier to use.
590
* @see {@link https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent}
591
*/
592
exports.keyboardEvent = async function (type, key, modifier, windowsVirtualKeyCode = 0) {
593
debug(`:: keyboardEvent => Event type "${type}" | Key "${key}" | Modifier: "${modifier}"`)
594
browserIsInitialized.call(this)
595
596
type = (typeof type === 'undefined') ? 'char' : type
597
key = (typeof key === 'undefined') ? null : key
598
modifier = modifier || 0
599
604
}
605
if (windowsVirtualKeyCode) {
606
parameters.windowsVirtualKeyCode = windowsVirtualKeyCode
607
parameters.nativeVirtualKeyCode = windowsVirtualKeyCode
608
}
609
await this.client.Input.dispatchKeyEvent(parameters)
611
612
/**
613
* Waits certain amount of ms
614
* @param {number} time - Ammount of ms to wait
615
*/
616
exports.wait = async function (time) {
617
debug(`:: wait => Waiting ${time} ms...`)
618
browserIsInitialized.call(this)
619
620
return sleep(time)
621
}
622
623
/**
624
* Binding callback to handle console messages
625
*
626
* @param listener is a callback for handling console message
627
*
628
*/
629
exports.onConsole = async function (listener) {
630
debug(`:: onConsole => Binding new listener to console`)
631
browserIsInitialized.call(this)
636
/**
637
* Waits for a page to finish loading. Throws error after timeout
638
* @param {number} timeout - The timeout in ms. (Default: "loadPageTimeout" property in the browser instance options)
639
*/
640
exports.waitForPageToLoad = async function (timeout) {
641
debug(`:: waitForPageToLoad => Waiting page load...`)
642
browserIsInitialized.call(this)
644
return promiseTimeout(new Promise((resolve, reject) => {
645
const listener = () => {
646
debug(`:: waitForPageToLoad => Page loaded!`)
647
this.removeListener('pageLoaded', listener)
648
resolve(true)
649
}
650
this.on('pageLoaded', listener)
654
/**
655
* Waits for all the frames in the page to finish loading. Returns the list of frames after that
656
* @param {regexp|string} url - The URL that must be waited for load
657
* @return {object} - List of frames, with childFrames
658
*/
659
exports.waitForFrameToLoad = async function (url, timeout) {
660
if (!url) {
661
throw new Error(`"url" parameter must be passed `)
662
}
663
debug(`:: waitForFramesToLoad => Waiting page frame ${url} load...`)
664
browserIsInitialized.call(this)
665
666
const frames = await this.getFrames()
667
let frame = _.find(frames, function (f) {
668
if ((typeof url === 'string' && f.url === url) || (typeof url !== 'string' && f.url.match(url))) {
669
return f
670
}
671
})
672
673
if (!frame) {
674
frame = await promiseTimeout(new Promise((resolve, reject) => {
675
const listener = (data) => {
676
debug(`:: waitForPageToLoad => Page loaded!`)
677
this.removeListener('frameNavigated', listener)
678
if ((typeof url === 'string' && data.frame.url === url) || (typeof url !== 'string' && data.frame.url.match(url))) {
679
resolve(data.frame)
689
/**
690
* Waits for a selector to finish loading. Throws error after timeout
691
* @param {string} selector - The identifier for the select element.
692
* @param {number} interval - The interval in ms. (Default: "loadPageTimeout" property in the browser instance options)
693
* @param {number} timeout - The timeout in ms. (Default: "loadPageTimeout" property in the browser instance options)
694
*/
695
exports.waitForSelectorToLoad = async function (selector, interval, timeout) {
696
debug(`:: waitForSelectorToLoad => Waiting selector "${selector}" load...`)
697
browserIsInitialized.call(this)
698
699
const startDate = (new Date()).getTime()
700
while (true) {
701
const exists = await this.exist(selector)
703
if (exists === true) {
704
debug(`:: waitForSelectorToLoad => Selector "${selector}" loaded!`)
705
return
706
}
708
debug(`:: waitForSelectorToLoad => Selector "${selector}" hasn't loaded yet. Waiting ${interval || this.options.browser.loadSelectorInterval}ms...`)
709
await sleep(interval || this.options.browser.loadSelectorInterval)
713
debug(`:: waitForSelectorToLoad => Timeout while trying to wait for selector "${selector}".`)
714
throw new Error(`Timeout while trying to wait for selector "${selector}"`)
715
}
716
}
717
}
719
/**
720
* Fire a mouse event.
721
* @param {string} [type=mousePressed] - Type of the mouse event. Allowed values: mousePressed, mouseReleased, mouseMoved.
722
* @param {number} [x=0] - X coordinate of the event relative to the main frame's viewport.
723
* @param {number} [y=0] - Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport.
724
* @param {number} [modifier=0] - Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
725
* @param {string} [button=left] - Mouse button (default: "none"). Allowed values: none, left, middle, right.
726
* @see {@link https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent}
727
*/
728
exports.mouseEvent = async function ({ type = 'mousePressed', x = 0, y = 0, modifiers = 0, button = 'left', clickCount = 1 }) {
729
debug(`:: mouseEvent => Type "${type}" | (${x}; ${y}) | Modifiers: "${modifiers}" | Button: ${button}`)
730
browserIsInitialized.call(this)
731
732
await this.client.Input.dispatchMouseEvent({ type, x, y, button, modifiers, clickCount })
733
}
734
735
/**
736
* Click on a selector by firing a 'click event' directly in the element of the selector
737
* @param {string} selector - Selector of the element to click
738
* @param {string} frameId - The FrameID where the selector should be searched
739
*/
740
exports.click = async function (selector, frameId) {
741
debug(`:: click => Click event on selector "${selector}"`)
742
browserIsInitialized.call(this)
743
selector = fixSelector(selector)
745
const node = await this.querySelector(selector, frameId)
746
await this.evaluateOnNode(node, function clickElement (selector) {
747
document.querySelector(selector).click()
748
}, selector)
749
}
751
/**
752
* Clicks left button hover the centroid of the element matching the passed selector
753
* @param {string} [selector]
754
* @param {string} frameId - The FrameID where the selector should be searched
755
*/
756
exports.clickOnSelector = async function (selector, frameId) {
757
debug(`:: clickOnSelector => Mouse click in centroid of element with selector "${selector}" for frame "${frameId || 'root'}"`)
758
browserIsInitialized.call(this)
759
selector = fixSelector(selector)
760
761
const node = await this.querySelector(selector, frameId)
762
const nodeCentroid = await this.getNodeCentroid(node.nodeId)
763
await this.mouseEvent({
764
type: 'mousePressed',
765
x: nodeCentroid.x,
766
y: nodeCentroid.y
767
})
768
await this.mouseEvent({
769
type: 'mouseReleased',
770
x: nodeCentroid.x,
771
y: nodeCentroid.y
772
})
773
}
774
775
/**
776
* Calculates the centroid of a node by using the boxModel data of the element
777
* @param {string} nodeId - The Node Id
778
* @return {object} - { x, y } object with the coordinates
779
*/
780
exports.getNodeCentroid = async function (nodeId) {
781
debug(`:: getNodeCentroid => Get centroid coordinates for node "${nodeId}"`)
782
browserIsInitialized.call(this)
783
const boxModel = await this.client.DOM.getBoxModel({ nodeId: nodeId })
785
return {
786
x: parseInt((boxModel.model.content[0] + boxModel.model.content[2] + boxModel.model.content[4] + boxModel.model.content[6]) / 4),
787
y: parseInt((boxModel.model.content[1] + boxModel.model.content[3] + boxModel.model.content[5] + boxModel.model.content[7]) / 4)
788
}
789
}
791
/**
792
* Get the browser cookies
793
* @return {object} - Object with all the cookies
794
*/
795
exports.getCookies = async function () {
796
debug(`:: getCookies => Return the browser cookies`)
797
browserIsInitialized.call(this)
798
799
return this.client.Network.getCookies()
800
}
804
* @param {string} name - The name of the cookie.
805
* @param {string} value - The value of the cookie.
806
* @param {string} url - The request-URI to associate with the setting of the cookie.
807
* @param {{
808
* @property {object} options - Options object
809
* @property {string} [domain] - If omitted, the cookie becomes a host-only cookie
810
* @property {string} [path] - Defaults to the path portion of the url parameter
811
* @property {boolean} [secure] - Defaults to false.
812
* @property {boolean} [httpOnly] - Defaults to false.
813
* @property {string} [sameSite] - Represents the cookie's 'SameSite' status: https://tools.ietf.org/html/draft-west-first-party-cookies
814
* @property {number} [expirationDate] - If omitted, cookie becomes a session cookie
815
* }} options - additional options for setting the cookie (more info here: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setCookie)
818
exports.setCookie = async function (name, value, options = {}) {
819
debug(`:: setCookie => Setting browser cookie "${name}" with value "${value}". Additional options: ${options}`)
822
const setCookieOptions = Object.assign({ name, value }, options)
823
824
return this.client.Network.setCookie(setCookieOptions)
825
}
826
827
/**
828
* Clear the browser cookies
829
*/
830
exports.clearBrowserCookies = async function () {
831
debug(`:: clearBrowserCookies => Clearing browser cookies`)
832
browserIsInitialized.call(this)
833
834
return this.client.Network.clearBrowserCookies()
837
/**
838
* Checks if an element matches the selector
839
* @param {string} selector - The selector string
840
* @param {string} frameId - The FrameID where the selector should be searched
841
* @return {boolean} - Boolean indicating if element of selector exists or not
843
// TODO: make use of the "frameId" parameter in this fn, and avoid using this.evaluate()
844
exports.exist = async function (selector, frameId) {
845
debug(`:: exist => Exist element with selector "${selector}" for frame "${frameId || 'root'}"?`)
846
browserIsInitialized.call(this)
847
selector = fixSelector(selector)
849
const exist = await this.evaluate(selector => !!document.querySelector(selector), selector)
855
856
/**
857
* Checks if an element matching a selector is visible
858
* @param {string} selector - The selector string
859
* @param {string} frameId - The FrameID where the selector should be searched
860
* @return {boolean} - Boolean indicating if element of selector is visible or not
862
// TODO: make use of the "frameId" parameter in this fn, and avoid using this.evaluate()
863
exports.visible = async function (selector, frameId) {
864
debug(`:: visible => Visible element with selector "${selector}" for frame "${frameId || 'root'}"?`)
865
browserIsInitialized.call(this)
866
868
const el = document.querySelector(selector)
869
let style
870
try {
871
style = window.getComputedStyle(el, null)
872
} catch (e) {
873
return false
874
}
875
if (style.visibility === 'hidden' || style.display === 'none') {
876
return false
877
}
878
if (style.display === 'inline' || style.display === 'inline-block') {
879
return true
880
}
881
return el.clientHeight > 0 && el.clientWidth > 0
882
}, selector)
883
884
debug(`:: visible => Visible: "${visible.result.value}"`)
888
889
/**
890
* Takes a screenshot of the page and returns it as a string
891
* @param {object} captureOptions - Options object
892
* Options properties:
893
* @property {string} [format] - Image compression format (defaults to png). Allowed values: jpeg, png.
894
* @property {integer} [quality] - Compression quality from range [0..100] (jpeg only).
895
* @property {ViewPort} [clip] - Capture the screenshot of a given viewport/region only (https://chromedevtools.github.io/devtools-protocol/tot/Page/#type-Viewport)
896
* @property {boolean} [fromSurface] - Capture the screenshot from the surface, rather than the view. Defaults to false. EXPERIMENTAL
897
* @property {string} [selector] - The selector to be captured. If empty, will capture the page
898
* @property {boolean} [fullPage] - If true, captures the full page height
899
* @param {boolean} returnBinary - If true, returns as binary. Otherwise, returns a base64 string
900
* @return {string} - Binary or Base64 string with the image data
901
*/
902
exports.getScreenshot = async function ({
903
format = 'png',
904
quality,
905
clip = {
906
x: 0,
907
y: 0,
908
width: this.options.deviceMetrics.width,
909
height: this.options.deviceMetrics.height,
910
scale: this.options.deviceMetrics.deviceScaleFactor
911
},
912
fromSurface,
913
selector,
914
fullPage
915
}, returnBinary = false) {
916
debug(`:: getScreenshot => Taking a screenshot of the ${selector ? 'selector "' + selector + '"' : 'page'}. Returning as ${returnBinary ? 'binary' : 'base64'} string`)
917
browserIsInitialized.call(this)
918
919
format = format.toLowerCase()
920
if (format === 'jpg') {
921
format = 'jpeg'
922
}
924
// Validate screenshot format
925
if (format !== 'jpeg' && format !== 'png') {
926
throw new Error(`Invalid format "${format}" for the screenshot. Allowed values: "jpeg" and "png".`)
927
}
928
929
const screenshotSettings = {
930
format,
931
quality,
932
fromSurface: true
933
}
934
935
// If fullPage parameter is on, then resize the screen to full size
936
if (fullPage === true) {
937
screenshotSettings.clip = await this.resizeFullScreen()
938
}
939
940
// If there's no clip, setup a default clip
941
if (selector) {
942
screenshotSettings.clip = await this.getSelectorViewport(selector)
943
}
944
945
let screenshot = (await this.client.Page.captureScreenshot(screenshotSettings)).data
946
947
if (returnBinary) {
948
screenshot = Buffer.from(screenshot, 'base64')
949
}
950
debug(`:: getScreenshot => Screenshot took!`)
951
952
return screenshot
953
}
958
* @param {object} captureOptions - Options object
959
* Options properties:
960
* @property {string} format - Image compression format (defaults to png). Allowed values: jpeg, png.
961
* @property {integer} quality - Compression quality from range [0..100] (jpeg only).
962
* @property {ViewPort} clip - Capture the screenshot of a given region only (https://chromedevtools.github.io/devtools-protocol/tot/Page/#type-Viewport)
963
* @property {boolean} fromSurface - Capture the screenshot from the surface, rather than the view. Defaults to false. EXPERIMENTAL
964
* @property {string} [selector] - The selector to be captured. If empty, will capture the page
965
* @property {boolean} [fullPage] - If true, captures the full page height
968
exports.saveScreenshot = async function (fileName = `screenshot-${Date.now()}`, { format = 'png', quality, clip, fromSurface, selector, fullPage } = {}) {
969
debug(`:: saveScreenshot => Saving a screenshot of the ${selector ? 'selector "' + selector + '"' : 'page'}...`)
972
format = format.toLowerCase()
973
if (format === 'jpg') {
974
format = 'jpeg'
975
}
976
977
// Validate screenshot format
978
if (format !== 'jpeg' && format !== 'png') {
979
throw new Error(`Invalid format "${format}" for the screenshot. Allowed values: "jpeg" and "png".`)
980
}
996
/**
997
* Prints the page to PDF
998
* @param {{
999
* @property {boolean} landscape - Paper orientation. Defaults to false.
1000
* @property {boolean} displayHeaderFooter - Display header and footer. Defaults to false.
1001
* @property {boolean} printBackground - Print background graphics. Defaults to false.
1002
* @property {number} scale - Scale of the webpage rendering. Defaults to 1.
1003
* @property {number} paperWidth - Paper width in inches. Defaults to 8.5 inches.
1004
* @property {number} paperHeight - Paper height in inches. Defaults to 11 inches.
1005
* @property {number} marginTop - Top margin in inches. Defaults to 1cm (~0.4 inches).
1006
* @property {number} marginBottom - Bottom margin in inches. Defaults to 1cm (~0.4 inches).
1007
* @property {number} marginLeft - Left margin in inches. Defaults to 1cm (~0.4 inches).
1008
* @property {number} marginRight - Right margin in inches. Defaults to 1cm (~0.4 inches).
1009
* @property {string} pageRanges - Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.
1010
* }} options - Options object
1011
* @param {boolean} returnBinary - If true, returns as binary. Otherwise, returns a base64 string
1012
* @return {string} - Binary or Base64 string with the PDF data
1013
*/
1014
exports.printToPDF = async function (options = {}, returnBinary = false) {
1015
debug(`:: printToPDF => Printing page to PDF with options: ${JSON.stringify(options, null, 2)}. Returning as ${returnBinary ? 'binary' : 'base64'} string`)
1016
browserIsInitialized.call(this)
1017
let pdf = (await this.client.Page.printToPDF(options)).data
1018
1019
if (returnBinary) {
1020
pdf = Buffer.from(pdf, 'base64')
1021
}
1022
1023
return pdf
1024
}
1025
1026
/**
1027
* Saves a PDF file of the page
1028
* @param {string} fileName - Path and Name of the file
1029
* @param {{
1030
* @property {boolean} landscape - Paper orientation. Defaults to false.
1031
* @property {boolean} displayHeaderFooter - Display header and footer. Defaults to false.
1032
* @property {boolean} printBackground - Print background graphics. Defaults to false.
1033
* @property {number} scale - Scale of the webpage rendering. Defaults to 1.
1034
* @property {number} paperWidth - Paper width in inches. Defaults to 8.5 inches.
1035
* @property {number} paperHeight - Paper height in inches. Defaults to 11 inches.
1036
* @property {number} marginTop - Top margin in inches. Defaults to 1cm (~0.4 inches).
1037
* @property {number} marginBottom - Bottom margin in inches. Defaults to 1cm (~0.4 inches).
1038
* @property {number} marginLeft - Left margin in inches. Defaults to 1cm (~0.4 inches).
1039
* @property {number} marginRight - Right margin in inches. Defaults to 1cm (~0.4 inches).
1040
* @property {string} pageRanges - Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.
1041
* }} options - PDF options
1042
*/
1043
exports.savePdf = async function (fileName = `pdf-${Date.now()}`, options = {}) {
1044
debug(`:: savePdf => Saving a PDF of the page...`)
1045
browserIsInitialized.call(this)
1046
1047
await fs.writeFile(`${fileName}.pdf`, await this.printToPDF(options, true))
1048
debug(`:: savePdf => PDF saved!`)
1049
return `${fileName}.pdf`
1050
}
1051
1052
/**
1053
* Get the Viewport of the element matching a selector
1054
* @param {string} selector - The selector string
1055
* @param {string} frameId - The FrameID where the selector should be searched
1056
* @return {Viewport} - Object with the viewport properties (https://chromedevtools.github.io/devtools-protocol/tot/Page/#type-Viewport)
1057
*/
1058
exports.getSelectorViewport = async function (selector, frameId) {
1059
debug(`:: getSelectorViewport => Getting viewport for element with selector "${selector}" for frame "${frameId || 'root'}"?`)
1060
browserIsInitialized.call(this)
1061
1062
selector = fixSelector(selector)
1063
1064
const node = await this.querySelector(selector, frameId)
1065
1066
const boxModel = await this.client.DOM.getBoxModel({ nodeId: node.nodeId })
1067
1068
const viewport = {
1069
x: boxModel.model.margin[0],
1070
y: boxModel.model.margin[1],
1071
width: boxModel.model.width,
1072
height: boxModel.model.height,
1073
scale: this.options.deviceMetrics.deviceScaleFactor
1074
}
1075
1080
1081
/**
1082
* Get the list of frames in the loaded page
1083
* @return {object} - List of frames, with childFrames
1084
*/
1085
exports.getFrames = async function () {
1086
debug(`:: getFrames => Getting frames list`)
1087
browserIsInitialized.call(this)
1088
const frames = []
1089
const resourceTree = await this.client.Page.getResourceTree()
1090
frames.push(resourceTree.frameTree.frame)
1091
_.each(resourceTree.frameTree.childFrames, (frameObj) => {
1092
frames.push(frameObj.frame)
1093
})
1094
return frames
1095
}
1100
exports.resizeFullScreen = async function () {
1101
debug(`:: resizeFullScreen => Resizing viewport to full screen size`)
1102
const {DOM, Emulation} = this.client
1104
const {root: {nodeId: documentNodeId}} = await DOM.getDocument()
1105
const {nodeId: bodyNodeId} = await DOM.querySelector({
1106
selector: 'body',
1109
const deviceMetrics = await this.options.deviceMetrics
1110
const { model: { height } } = await DOM.getBoxModel({ nodeId: bodyNodeId })
1111
1112
await Emulation.setDeviceMetricsOverride({
1113
width: deviceMetrics.width,
1114
height: height,
1115
deviceScaleFactor: deviceMetrics.deviceScaleFactor,
1118
})
1119
await this.client.Emulation.setVisibleSize({
1120
width: deviceMetrics.width,
1121
height: height
1122
})
1123
1124
await Emulation.setPageScaleFactor({ pageScaleFactor: deviceMetrics.deviceScaleFactor })
1125
1126
const fullPageViewport = {
1127
x: 0,
1128
y: 0,
1129
width: deviceMetrics.width,
1130
height: height,
1131
scale: deviceMetrics.deviceScaleFactor
1132
}
1133
1134
return fullPageViewport
1137
/**
1138
* Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload)
1139
* @param {boolean} [accept=true] - Whether to accept or dismiss the dialog
1140
* @param {string} [promptText] - The text to enter into the dialog prompt before accepting. Used only if this is a prompt dialog.
1141
*/
1142
exports.handleDialog = async function (accept = true, promptText = '') {
1143
debug(`:: handleDialog => Handling dialog on the page...`)
1144
browserIsInitialized.call(this)
1145
await this.client.Page.handleJavaScriptDialog({
1146
accept,
1147
promptText
1148
})
1149
debug(`:: handleDialog => Dialog handled!`)
1150
}
1151
1152
/**
1153
* Post data from the browser context
1154
* @param {string} url - The URL or path to POST to
1155
* @param {object} [data] - The data object to be posted
1156
* @param {object} [options] - Options of the request
1157
* @return {object} - Request status and data
1158
*/
1159
exports.post = async function (url, data = {}, options = {}) {
1160
debug(`:: post => Posting on URL "${url}" data: ${JSON.stringify(data, null, 2)} with options ${options}...`)
1161
browserIsInitialized.call(this)
1162
1163
if (!url) {
1164
throw new Error(`An URL must be supplied in order to make the POST request`)
1165
}
1166
1167
let requestData = data
1168
if (typeof data === 'object') {
1169
requestData = objectToEncodedUri(data)
1170
}
1171
1172
const requestOptions = Object.assign({
1173
contentType: 'application/x-www-form-urlencoded',
1175
}, options)
1176
1177
return this.evaluateAsync(function (url, data, options) {
1178
return new Promise((resolve, reject) => {
1179
const xhr = new XMLHttpRequest()
1180
xhr.open('POST', url)
1181
xhr.setRequestHeader('Content-Type', options.contentType)
1182
xhr.onload = function (e) {
1183
resolve({
1184
responseText: xhr.responseText,
1185
status: xhr.status,
1186
response: xhr.response,
1187
responseType: xhr.responseType,
1188
responseURL: xhr.responseURL,
1189
statusText: xhr.statusText,
1190
responseXML: xhr.responseXML,
1191
readyState: xhr.readyState
1192
})
1193
}
1194
xhr.onerror = function (e) {
1195
const error = new Error(`Error while making request POST to URL "${url}"`)
1196
error.requestData = data
1197
error.options = options
1198
error.response = {
1199
responseText: xhr.responseText,
1200
status: xhr.status,
1201
response: xhr.response,
1202
responseType: xhr.responseType,
1203
responseURL: xhr.responseURL,
1204
statusText: xhr.statusText,
1205
responseXML: xhr.responseXML,
1206
readyState: xhr.readyState