nightmare
Version:
A high-level browser automation library.
895 lines (800 loc) • 18.9 kB
JavaScript
/**
* Module Dependencies
*/
var debug = require('debug')('nightmare:actions')
var sliced = require('sliced')
var jsesc = require('jsesc')
var fs = require('fs')
/**
* Get the version info for Nightmare, Electron and Chromium.
* @param {Function} done
*/
exports.engineVersions = function(done) {
debug('.engineVersions()')
done(null, this.engineVersions)
}
/**
* Get the title of the page.
*
* @param {Function} done
*/
exports.title = function(done) {
debug('.title() getting it')
this.evaluate_now(function() {
return document.title
}, done)
}
/**
* Get the url of the page.
*
* @param {Function} done
*/
exports.url = function(done) {
debug('.url() getting it')
this.evaluate_now(function() {
return document.location.href
}, done)
}
/**
* Get the path of the page.
*
* @param {Function} done
*/
exports.path = function(done) {
debug('.path() getting it')
this.evaluate_now(function() {
return document.location.pathname
}, done)
}
/**
* Determine if a selector is visible on a page.
*
* @param {String} selector
* @param {Function} done
*/
exports.visible = function(selector, done) {
debug('.visible() for ' + selector)
this.evaluate_now(
function(selector) {
var elem = document.querySelector(selector)
if (elem) return elem.offsetWidth > 0 && elem.offsetHeight > 0
else return false
},
done,
selector
)
}
/**
* Determine if a selector exists on a page.
*
* @param {String} selector
* @param {Function} done
*/
exports.exists = function(selector, done) {
debug('.exists() for ' + selector)
this.evaluate_now(
function(selector) {
return document.querySelector(selector) !== null
},
done,
selector
)
}
/**
* Click an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.click = function(selector, done) {
debug('.click() on ' + selector)
this.evaluate_now(
function(selector) {
document.activeElement.blur()
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('click', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Mousedown on an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mousedown = function(selector, done) {
debug('.mousedown() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mousedown', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Mouseup on an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseup = function(selector, done) {
debug('.mouseup() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mouseup', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Hover over an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseover = function(selector, done) {
debug('.mouseover() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mouseover', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Release hover from an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseout = function(selector, done) {
debug('.mouseout() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var event = document.createEvent('MouseEvent')
event.initMouseEvent('mouseout', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Helper functions for type() and insert() to focus/blur
* so that we trigger DOM events.
*/
var focusSelector = function(done, selector) {
return this.evaluate_now(
function(selector) {
document.querySelector(selector).focus()
},
done.bind(this),
selector
)
}
var blurSelector = function(done, selector) {
return this.evaluate_now(
function(selector) {
//it is possible the element has been removed from the DOM
//between the action and the call to blur the element
var element = document.querySelector(selector)
if (element) {
element.blur()
}
},
done.bind(this),
selector
)
}
/**
* Type into an element.
*
* @param {String} selector
* @param {String} text
* @param {Function} done
*/
exports.type = function() {
var selector = arguments[0],
text,
done
if (arguments.length == 2) {
done = arguments[1]
} else {
text = arguments[1]
done = arguments[2]
}
debug('.type() %s into %s', text, selector)
var self = this
focusSelector.bind(this)(function(err) {
if (err) {
debug('Unable to .type() into non-existent selector %s', selector)
return done(err)
}
var blurDone = blurSelector.bind(this, done, selector)
if ((text || '') == '') {
this.evaluate_now(
function(selector) {
document.querySelector(selector).value = ''
},
blurDone,
selector
)
} else {
self.child.call('type', text, blurDone)
}
}, selector)
}
/**
* Insert text
*
* @param {String} selector
* @param {String} text
* @param {Function} done
*/
exports.insert = function(selector, text, done) {
if (arguments.length === 2) {
done = text
text = null
}
debug('.insert() %s into %s', text, selector)
var child = this.child
focusSelector.bind(this)(function(err) {
if (err) {
debug('Unable to .insert() into non-existent selector %s', selector)
return done(err)
}
var blurDone = blurSelector.bind(this, done, selector)
if ((text || '') == '') {
this.evaluate_now(
function(selector) {
document.querySelector(selector).value = ''
},
blurDone,
selector
)
} else {
child.call('insert', text, blurDone)
}
}, selector)
}
/**
* Check a checkbox, fire change event
*
* @param {String} selector
* @param {Function} done
*/
exports.check = function(selector, done) {
debug('.check() ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.checked = true
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/*
* Uncheck a checkbox, fire change event
*
* @param {String} selector
* @param {Function} done
*/
exports.uncheck = function(selector, done) {
debug('.uncheck() ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.checked = null
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Choose an option from a select dropdown
*
*
*
* @param {String} selector
* @param {String} option value
* @param {Function} done
*/
exports.select = function(selector, option, done) {
debug('.select() ' + selector)
this.evaluate_now(
function(selector, option) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.value = option
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector,
option
)
}
/**
* Go back to previous url.
*
* @param {Function} done
*/
exports.back = function(done) {
debug('.back()')
this.evaluate_now(function() {
window.history.back()
}, done)
}
/**
* Go forward to previous url.
*
* @param {Function} done
*/
exports.forward = function(done) {
debug('.forward()')
this.evaluate_now(function() {
window.history.forward()
}, done)
}
/**
* Refresh the current page.
*
* @param {Function} done
*/
exports.refresh = function(done) {
debug('.refresh()')
this.evaluate_now(function() {
window.location.reload()
}, done)
}
/**
* Wait
*
* @param {...} args
*/
exports.wait = function() {
var args = sliced(arguments)
var done = args[args.length - 1]
if (args.length < 2) {
debug('Not enough arguments for .wait()')
return done()
}
var arg = args[0]
if (typeof arg === 'number') {
debug('.wait() for ' + arg + 'ms')
if (arg < this.options.waitTimeout) {
waitms(arg, done)
} else {
waitms(
this.options.waitTimeout,
function() {
done(
new Error(
'.wait() timed out after ' + this.options.waitTimeout + 'msec'
)
)
}.bind(this)
)
}
} else if (typeof arg === 'string') {
var timeout = null
if (typeof args[1] === 'number') {
timeout = args[1]
}
debug(
'.wait() for ' +
arg +
' element' +
(timeout ? ' or ' + timeout + 'msec' : '')
)
waitelem.apply({ timeout: timeout }, [this, arg, done])
} else if (typeof arg === 'function') {
debug('.wait() for fn')
args.unshift(this)
waitfn.apply(this, args)
} else {
done()
}
}
/**
* Wait for a specififed amount of time.
*
* @param {Number} ms
* @param {Function} done
*/
function waitms(ms, done) {
setTimeout(done, ms)
}
/**
* Wait for a specified selector to exist.
*
* @param {Nightmare} self
* @param {String} selector
* @param {Function} done
*/
function waitelem(self, selector, done) {
var elementPresent
eval(
'elementPresent = function() {' +
" var element = document.querySelector('" +
jsesc(selector) +
"');" +
' return (element ? true : false);' +
'};'
)
var newDone = function(err) {
if (err) {
return done(
new Error(
`.wait() for ${selector} timed out after ${
self.options.waitTimeout
}msec`
)
)
}
done()
}
waitfn.apply(this, [self, elementPresent, newDone])
}
/**
* Wait until evaluated function returns true.
*
* @param {Nightmare} self
* @param {Function} fn
* @param {...} args
* @param {Function} done
*/
function waitfn() {
var softTimeout = this.timeout || null
var executionTimer
var softTimeoutTimer
var self = arguments[0]
var args = sliced(arguments)
var done = args[args.length - 1]
var timeoutTimer = setTimeout(function() {
clearTimeout(executionTimer)
clearTimeout(softTimeoutTimer)
done(new Error(`.wait() timed out after ${self.options.waitTimeout}msec`))
}, self.options.waitTimeout)
return tick.apply(this, arguments)
function tick(self, fn /**, arg1, arg2..., done**/) {
if (softTimeout) {
softTimeoutTimer = setTimeout(function() {
clearTimeout(executionTimer)
clearTimeout(timeoutTimer)
done()
}, softTimeout)
}
var waitDone = function(err, result) {
if (result) {
clearTimeout(timeoutTimer)
clearTimeout(softTimeoutTimer)
return done()
} else if (err) {
clearTimeout(timeoutTimer)
clearTimeout(softTimeoutTimer)
return done(err)
} else {
executionTimer = setTimeout(function() {
tick.apply(self, args)
}, self.options.pollInterval)
}
}
var newArgs = [fn, waitDone].concat(args.slice(2, -1))
self.evaluate_now.apply(self, newArgs)
}
}
/**
* Execute a function on the page.
*
* @param {Function} fn
* @param {...} args
* @param {Function} done
*/
exports.evaluate = function(fn /**, arg1, arg2..., done**/) {
var args = sliced(arguments)
var done = args[args.length - 1]
var self = this
var newDone = function() {
clearTimeout(timeoutTimer)
done.apply(self, arguments)
}
var newArgs = [fn, newDone].concat(args.slice(1, -1))
if (typeof fn !== 'function') {
return done(new Error('.evaluate() fn should be a function'))
}
debug('.evaluate() fn on the page')
var timeoutTimer = setTimeout(function() {
done(
new Error(
`Evaluation timed out after ${
self.options.executionTimeout
}msec. Are you calling done() or resolving your promises?`
)
)
}, self.options.executionTimeout)
this.evaluate_now.apply(this, newArgs)
}
/**
* Inject a JavaScript or CSS file onto the page
*
* @param {String} type
* @param {String} file
* @param {Function} done
*/
exports.inject = function(type, file, done) {
debug('.inject()-ing a file')
if (type === 'js') {
var js = fs.readFileSync(file, { encoding: 'utf-8' })
this._inject(js, done)
} else if (type === 'css') {
var css = fs.readFileSync(file, { encoding: 'utf-8' })
this.child.call('css', css, done)
} else {
debug('unsupported file type in .inject()')
done()
}
}
/**
* Set the viewport.
*
* @param {Number} width
* @param {Number} height
* @param {Function} done
*/
exports.viewport = function(width, height, done) {
debug('.viewport()')
this.child.call('size', width, height, done)
}
/**
* Set the useragent.
*
* @param {String} useragent
* @param {Function} done
*/
exports.useragent = function(useragent, done) {
debug('.useragent() to ' + useragent)
this.child.call('useragent', useragent, done)
}
/**
* Set the scroll position.
*
* @param {Number} x
* @param {Number} y
* @param {Function} done
*/
exports.scrollTo = function(y, x, done) {
debug('.scrollTo()')
this.evaluate_now(
function(y, x) {
window.scrollTo(x, y)
},
done,
y,
x
)
}
/**
* Take a screenshot.
*
* @param {String} path
* @param {Object} clip
* @param {Function} done
*/
exports.screenshot = function(path, clip, done) {
debug('.screenshot()')
if (typeof path === 'function') {
done = path
clip = undefined
path = undefined
} else if (typeof clip === 'function') {
done = clip
clip = typeof path === 'string' ? undefined : path
path = typeof path === 'string' ? path : undefined
}
this.child.call('screenshot', path, clip, function(error, img) {
var buf = new Buffer(img.data)
debug('.screenshot() captured with length %s', buf.length)
path ? fs.writeFile(path, buf, done) : done(null, buf)
})
}
/**
* Save the current file as html to disk.
*
* @param {String} path the full path to the file to save to
* @param {String} saveType
* @param {Function} done
*/
exports.html = function(path, saveType, done) {
debug('.html()')
if (typeof path === 'function' && !saveType && !done) {
done = path
saveType = undefined
path = undefined
} else if (
typeof path === 'object' &&
typeof saveType === 'function' &&
!done
) {
done = saveType
saveType = path
path = undefined
} else if (typeof saveType === 'function' && !done) {
done = saveType
saveType = undefined
}
this.child.call('html', path, saveType, function(error) {
if (error) debug(error)
done(error)
})
}
/**
* Take a pdf.
*
* @param {String} path
* @param {Function} done
*/
exports.pdf = function(path, options, done) {
debug('.pdf()')
if (typeof path === 'function' && !options && !done) {
done = path
options = undefined
path = undefined
} else if (
typeof path === 'object' &&
typeof options === 'function' &&
!done
) {
done = options
options = path
path = undefined
} else if (typeof options === 'function' && !done) {
done = options
options = undefined
}
this.child.call('pdf', path, options, function(error, pdf) {
if (error) debug(error)
var buf = new Buffer(pdf.data)
debug('.pdf() captured with length %s', buf.length)
path ? fs.writeFile(path, buf, done) : done(null, buf)
})
}
/**
* Get and set cookies
*
* @param {String} name
* @param {Mixed} value (optional)
* @param {Function} done
*/
exports.cookies = {}
/**
* Get a cookie
*/
exports.cookies.get = function(name, done) {
debug('cookies.get()')
var query = {}
switch (arguments.length) {
case 2:
query = typeof name === 'string' ? { name: name } : name
break
case 1:
done = name
break
}
this.child.call('cookie.get', query, done)
}
/**
* Set a cookie
*/
exports.cookies.set = function(name, value, done) {
debug('cookies.set()')
var cookies = []
switch (arguments.length) {
case 3:
cookies.push({
name: name,
value: value
})
break
case 2:
cookies = [].concat(name)
done = value
break
case 1:
done = name
break
}
this.child.call('cookie.set', cookies, done)
}
/**
* Clear a cookie
*/
exports.cookies.clear = function(name, done) {
debug('cookies.clear()')
var cookies = []
switch (arguments.length) {
case 2:
cookies = [].concat(name)
break
case 1:
done = name
break
}
this.child.call('cookie.clear', cookies, done)
}
/**
* Clear all cookies
*/
exports.cookies.clearAll = function(done) {
this.child.call('cookie.clearAll', done)
}
/**
* Authentication
*/
exports.authentication = function(login, password, done) {
debug('.authentication()')
this.child.call('authentication', login, password, done)
}