nightmare
Version:
A high-level browser automation library.
438 lines (357 loc) • 11 kB
JavaScript
/**
* DEBUG=nightmare*
*/
var log = require('debug')('nightmare:log');
var debug = require('debug')('nightmare');
/**
* Module dependencies
*/
var default_electron_path = require('electron-prebuilt');
var source = require('function-source');
var proc = require('child_process');
var actions = require('./actions');
var join = require('path').join;
var sliced = require('sliced');
var child = require('./ipc');
var once = require('once');
var noop = function() {};
var keys = Object.keys;
/**
* Export `Nightmare`
*/
module.exports = Nightmare;
/**
* runner script
*/
var runner = join(__dirname, 'runner.js');
/**
* Template
*/
var template = require('./javascript');
/**
* Initialize `Nightmare`
*
* @param {Object} options
*/
function Nightmare(options) {
if (!(this instanceof Nightmare)) return new Nightmare(options);
options = options || {};
var electronArgs = {};
var self = this;
self.optionWaitTimeout = options.waitTimeout || 30000;
var electron_path = options.electronPath || default_electron_path
if (options.paths) {
electronArgs.paths = options.paths;
}
if (options.switches) {
electronArgs.switches = options.switches;
}
electronArgs.dock = options.dock || false;
this.proc = proc.spawn(electron_path, [runner].concat(JSON.stringify(electronArgs)), {
stdio: [null, null, null, 'ipc']
});
this.proc.on('close', function(code) {
var help = {
127: 'command not found - you may not have electron installed correctly',
126: 'permission problem or command is not an executable - you may not have all the necessary dependencies for electron',
1: 'general error - you may need xvfb',
0: 'success!'
};
debug('electron child process exited with code ' + code + ': ' + help[code]);
});
process.setMaxListeners(Infinity);
process.on('uncaughtException', function(err) {
console.error(err.stack);
endInstance(self);
});
// if the process nightmare is running in dies, make sure to kill electron
var endSelf = endInstance.bind(null, self);
process.on('exit', endSelf);
process.on('SIGINT', endSelf);
process.on('SIGTERM', endSelf);
process.on('SIGQUIT', endSelf);
process.on('SIGHUP', endSelf);
process.on('SIGBREAK', endSelf);
// initial state
this.state = 'initial';
this.running = false;
this.ending = false;
this.ended = false;
this._queue = [];
this._headers = {};
this.options = options;
// initialize namespaces
Nightmare.namespaces.forEach(function (name) {
if ('function' === typeof this[name]) {
this[name] = this[name]()
}
}, this)
//prepend adding child actions to the queue
Object.keys(Nightmare.childActions).forEach(function(key){
debug('queueing child action addition for "%s"', key);
this.queue(function(done){
this.child.once('action', done);
this.child.emit('action', key, String(Nightmare.childActions[key]));
});
}, this);
this.child = child(this.proc);
this.child.once('ready', function() {
self.child.once('browser-initialize', function() {
self.state = 'ready';
});
self.child.emit('browser-initialize', options);
});
// propagate console.log(...) through
this.child.on('log', function() {
log.apply(log, arguments);
});
this.child.on('uncaughtException', function(stack) {
console.error('Nightmare runner error:\n\n%s\n', '\t' + stack.replace(/\n/g, '\n\t'));
endInstance(self);
process.exit(1);
});
this.child.on('page', function(type) {
log.apply(null, ['page-' + type].concat(sliced(arguments, 1)));
});
// proporate events through to debugging
this.child.on('did-finish-load', function () { log('did-finish-load', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-fail-load', function () { log('did-fail-load', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-frame-finish-load', function () { log('did-frame-finish-load', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-start-loading', function () { log('did-start-loading', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-stop-loading', function () { log('did-stop-loading', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-get-response-details', function () { log('did-get-response-details', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('did-get-redirect-request', function () { log('did-get-redirect-request', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('dom-ready', function () { log('dom-ready', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('page-favicon-updated', function () { log('page-favicon-updated', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('new-window', function () { log('new-window', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('will-navigate', function () { log('will-navigate', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('crashed', function () { log('crashed', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('plugin-crashed', function () { log('plugin-crashed', JSON.stringify(Array.prototype.slice.call(arguments))); });
this.child.on('destroyed', function () { log('destroyed', JSON.stringify(Array.prototype.slice.call(arguments))); });
}
function endInstance(instance) {
if (instance.proc.connected) {
instance.proc.disconnect();
instance.proc.kill();
instance.ended = true;
}
}
/**
* Namespaces to initialize
*/
Nightmare.namespaces = [];
/**
* Child actions to create
*/
Nightmare.childActions = {};
/**
* ready
*/
Nightmare.prototype.ready = function(fn) {
if (this.state == 'ready') return fn();
this.child.once('ready', fn);
return this;
};
/**
* Override headers for all HTTP requests
*/
Nightmare.prototype.header = function(header, value) {
if (header && typeof value !== 'undefined') {
this._headers[header] = value;
} else {
this._headers = header || {};
}
return this;
};
/**
* Go to a `url`
*/
Nightmare.prototype.goto = function(url, headers) {
debug('queueing action "goto" for %s', url);
var child = this.child;
headers = headers || {};
for (var key in this._headers) {
headers[key] = headers[key] || this._headers[key];
}
this.queue(function(fn) {
child.once('goto', fn);
child.emit('goto', url, headers);
});
return this;
};
/**
* run
*/
Nightmare.prototype.run = function(fn) {
debug('running')
var ready = [this.ready.bind(this)];
var steps = [ready].concat(this.queue());
this.running = true;
this._queue = [];
var self = this;
// kick us off
next();
// next function
function next (err, res) {
var item = steps.shift();
// Immediately halt execution if an error has been thrown, or we have no more queued up steps.
if (err || !item) return done.apply(self, arguments);
var args = item[1] || [];
var method = item[0];
args.push(once(after));
method.apply(self, args);
}
function after (err, res) {
var args = sliced(arguments);
self.child.once('continue', function() {
next.apply(self, args);
});
self.child.emit('continue');
}
function done () {
self.running = false;
if (self.ending) {
endInstance(self);
}
return fn.apply(self, arguments);
}
return this;
};
/**
* run the code now (do not queue it)
*
* you should not use this, unless you know what you're doing
* it should be used for plugins and custom actions, not for
* normal API usage
*/
Nightmare.prototype.evaluate_now = function(js_fn, done) {
var child = this.child;
child.once('javascript', function(err, result) {
if (err) return done(err);
done(null, result);
});
var args = Array.prototype.slice.call(arguments).slice(2);
var argsList = JSON.stringify(args).slice(1,-1);
child.emit('javascript', template.execute({ src: String(js_fn), args: argsList }));
return this;
};
/**
* inject javascript
*/
Nightmare.prototype._inject = function(js, done) {
var child = this.child;
child.once('javascript', function(err, result) {
if (err) return done(err);
done(null, result);
});
child.emit('javascript', template.inject({ src: js }));
return this;
};
/**
* end
*/
Nightmare.prototype.end = function(done) {
this.ending = true;
if (done && !this.running && !this.ended) {
this.run(done);
}
return this;
};
/**
* on
*/
Nightmare.prototype.on = function(event, handler) {
this.child.on(event, handler);
return this;
};
/**
* Queue
*/
Nightmare.prototype.queue = function(done) {
if (!arguments.length) return this._queue;
var args = sliced(arguments);
var fn = args.pop();
this._queue.push([fn, args]);
};
/**
* then
*/
Nightmare.prototype.then = function(fulfill, reject) {
var self = this;
return new Promise(function (success, failure) {
self.run(function(err, result) {
if (err) failure(err);
else success(result);
})
})
.then(fulfill)
.catch(reject)
};
/**
* use
*/
Nightmare.prototype.use = function(fn) {
fn(this)
return this
};
// wrap all the functions in the queueing function
function queued (name, fn) {
return function action () {
debug('queueing action "' + name + '"');
var args = [].slice.call(arguments);
this._queue.push([fn, args]);
return this;
}
}
/**
* Static: Support attaching custom actions
*
* @param {String} name - method name
* @param {Function|Object} [childfn] - Electron implementation
* @param {Function|Object} parentfn - Nightmare implementation
* @return {Nightmare}
*/
Nightmare.action = function() {
var name = arguments[0], childfn, parentfn;
if(arguments.length === 2) {
parentfn = arguments[1];
} else {
parentfn = arguments[2];
childfn = arguments[1];
}
// support functions and objects
// if it's an object, wrap it's
// properties in the queue function
if(parentfn) {
if (typeof parentfn === 'function') {
Nightmare.prototype[name] = queued(name, parentfn);
} else {
if (!~Nightmare.namespaces.indexOf(name)) {
Nightmare.namespaces.push(name);
}
Nightmare.prototype[name] = function() {
var self = this;
return keys(parentfn).reduce(function (obj, key) {
obj[key] = queued(name, parentfn[key]).bind(self)
return obj;
}, {});
}
}
}
if(childfn) {
if (typeof childfn === 'function') {
Nightmare.childActions[name] = childfn;
} else {
for(var key in childfn){
Nightmare.childActions[name+'.'+key] = childfn;
}
}
}
}
/**
* Attach all the actions.
*/
Object.keys(actions).forEach(function (name) {
var fn = actions[name];
Nightmare.action(name, fn);
});