Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add New spooky.exec("code") method. #43

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Casper's API, have a look at [PhantomJS

### Prerequisites

* [Node.js](http://nodejs.org)
* [PhantomJS](http://phantomjs.org/)
* [CasperJS](http://casperjs.org/)
* [Node.js](http://nodejs.org) >= 0.8
* [PhantomJS](http://phantomjs.org/) >= 1.9
* [CasperJS](http://casperjs.org/) >= 1.0

SpookyJS is available from npm.

Expand Down Expand Up @@ -119,6 +119,17 @@ The following make parameters are supported (defaults are in parentheses):
* `TEST_DEBUG` Print debug logging to the console (false)
* `TEST_TRANSPORT` the Spooky transport to use when running the tests (stdio)

## Release Notes

### 0.2.2

- Node 0.10 support
- use Phantom 1.9's `system.stdin` for stdio transport
- add `phantom.onError` handler. Spooky now emits an error event and exits
non-zero if an unhandled JS error occurs in the Phantom context.
- add `thenClick` method (@andresgottlieb)
- fix #28

## License

SpookyJS is made available under the [MIT License](http://opensource.org/licenses/mit-license.php).
Expand Down
29 changes: 17 additions & 12 deletions lib/bootstrap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
function emit(event) {
var args = Array.prototype.slice.call(arguments);

console.log(JSON.stringify({
jsonrpc: '2.0',
method: 'emit',
params: args
}));
}

phantom.onError = function bootstrapOnError(msg, trace) {
emit('error', msg, trace);
phantom.exit(1);
};

var options = phantom.casperArgs.options;
phantom.requireBase = options.spooky_lib + 'node_modules/';
var emit = require(options.spooky_lib + 'lib/bootstrap/emit').emit;
var system = require('system');
var utils = require('utils');
var transport = (options.transport || '').toLowerCase();
Expand All @@ -10,20 +26,9 @@ if (transport === 'http') {
} else if (transport === 'stdio') {
server = require(options.spooky_lib + 'lib/bootstrap/stdio-server');
} else {
console.error('Unknown transport: ' + transport);
phantom.exit(1);
throw new Error('Unknown transport: ' + transport);
}

require(options.spooky_lib + 'lib/bootstrap/casper').provideAll(server);

function emit(event) {
var args = Array.prototype.slice.call(arguments);

console.log(JSON.stringify({
jsonrpc: '2.0',
method: 'emit',
params: args
}));
}

emit('ready');
26 changes: 26 additions & 0 deletions lib/bootstrap/casper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var createFunction =
require(options.spooky_lib + 'lib/bootstrap/create-function');
var casper = require('casper');
var emit = require(options.spooky_lib + 'lib/bootstrap/emit').emit;
var instance;

var methodsToProvide = {
Expand All @@ -11,6 +12,7 @@ var methodsToProvide = {
run: [function onComplete () {}, 'time'],
start: ['url', function then () {}],
then: [function fn () {}],
thenClick: ['selector', function fn () {}],
thenEvaluate: [function fn () {}, 'replacements'],
thenOpen: ['location', 'options'],
thenOpenAndEvaluate: ['location', function fn () {}, 'replacements'],
Expand Down Expand Up @@ -105,6 +107,30 @@ function getCreateFn(server) {
};
}

/*
* exec(code): Execute the specified code within the CasperJS context, a
* `Casper` instance is contained within the variable `casper`.
* Arguments:
* - code: A string of JavaScript to run in Casper/Phantom.
* - options: An object to make available to the eval'd code, optional.
*
* NOTE(jeresig): Added 2013-04-21
*/
methods.exec = function(code, options) {
try {
// Attempt to evaluate the code
// Note that we also redefine the `instance` variable to be
// `casper` in the context of this code, which makes more sense.
eval("(function(casper){\n" + code + "\n})(instance);");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will evaluate code in the local scope, giving it access to the internals of bootstrap/casper, which is probably not desirable.

Did you consider using createFunction here instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single quotes, please.


} catch(e) {
// Report any evaluation errors back to SpookyJS
instance.emit('log', e.message);
}

return true;
};

function provideAll(server) {
server.provide(getCreateFn(server));
server.provide(methods);
Expand Down
33 changes: 33 additions & 0 deletions lib/bootstrap/emit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function emit(event) {
var args = Array.prototype.slice.call(arguments);

console.log(JSON.stringify({
jsonrpc: '2.0',
method: 'emit',
params: args
}));
}

module.exports.emit = emit;

function getConsoleFn(level) {
var args = ['console'];

if (level) {
args.push(level);
}

return function () {
var args = (level ? [level] : []).
concat(Array.prototype.slice.apply(arguments));
emit('console', args.join(' '));
};
}

module.exports.console = {
debug: getConsoleFn('debug'),
info: getConsoleFn('info'),
log: getConsoleFn(),
warn: getConsoleFn('warn'),
error: getConsoleFn('error')
};
7 changes: 7 additions & 0 deletions lib/bootstrap/http-server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var emit = require(options.spooky_lib + 'lib/bootstrap/emit').emit;
var port = options.port || 8080;

var JsonRpcServer = require(options.spooky_lib +
Expand Down Expand Up @@ -44,6 +45,12 @@ var service = require('webserver').create().
return;
}

try {
JSON.parse(request.post);
} catch (e) {
throw new Error('Could not parse "' + request.post + '" as JSON');
}

var result = JSON.parse(server.respond(request.post));

if (result.error) {
Expand Down
41 changes: 23 additions & 18 deletions lib/bootstrap/stdio-server.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
var STDIN_POLL_INTERVAL = 10;
var fs = require('fs');
var stdin = fs.open('/dev/stdin', 'r');
var line;
var emptyLines = 0;
var start = Date.now();
var stdin = require('system').stdin;

// NOTE(jeresig): If the following methods are run then stop attempting to
// readLine and begin intialization.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo, should be 'initialization'

var startMethods = ['run', 'exec'];

var Stream = require(options.spooky_lib + 'lib/stream');
var stream = new Stream();
Expand All @@ -18,19 +18,24 @@ function timestamp() {
}

function loop() {
stdin.flush();

line = stdin.readLine();

line = line.replace(/\0/g, '');

if (JSON.parse(line).method !== 'run') {
setTimeout(loop, STDIN_POLL_INTERVAL);
}

if (line !== '') {
stream.emit('data', line);
}
var line = stdin.readLine();

try {
var method = JSON.parse(line).method;

// NOTE(jeresig): Only attempt to read another line of input if one of
// the startMethods has not been detected.
if (startMethods.indexOf(method) < 0) {
setTimeout(loop, STDIN_POLL_INTERVAL);
}

if (line !== '') {
stream.emit('data', line);
}
// NOTE(jeresig): Ignore malformed JSON strings
} catch(e) {
throw new Error('Could not parse "' + line + '" as JSON');
}
}

var StreamServer = require(options.spooky_lib +
Expand Down
71 changes: 58 additions & 13 deletions lib/spooky.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var spawn = require('child_process').spawn;
var util = require('util');

var _ = require('underscore');
var fs = require('fs');

var async = require('async');

Expand All @@ -12,7 +13,6 @@ var EventEmitter = require('events').EventEmitter;
var Stream = require('stream');

var RequestStream = require('./spooky/request-stream');
var BufferedStream = require('./spooky/buffered-stream');
var FilteredStream = require('./spooky/filtered-stream');

var tinyjsonrpc = require('tiny-jsonrpc');
Expand All @@ -28,8 +28,7 @@ var defaults = {
port: 8081,
script: __dirname + '/bootstrap.js',
spooky_lib: __dirname + '/../',
transport: 'stdio',
bufferSize: 16 * 1024 // 16KB
transport: 'stdio'
},
casper: {
verbose: true,
Expand All @@ -55,6 +54,7 @@ function isJsonRpcResponse(s) {
}

function Spooky(options, callback) {
EventEmitter.call(this);
this.options = options = _.defaults(_.clone(options || {}), defaults);

for (var k in defaults) {
Expand Down Expand Up @@ -106,13 +106,61 @@ function Spooky(options, callback) {
throw new Error('Unknown transport ' + options.child.transport);
}

/*
* If a exec option is provided then we wrap the callback to automatically
* execute the CasperJS file on load (or emit an error upon failure).
*
* Usage:
* - exec: "code string"
* Code string is executed, no options passed in.
* - exec: { code: "code string", options: {...} }
* Code string is executed and options are passed in.
* - exec: { file: "file_name.js", options: {...} }
* File is read and executed and options are passed in.
*/
if (options.exec) {
var oldCallback = callback;
callback = (function(err) {
// Execute the callback function that we replaced
if (typeof oldCallback === "function") {
oldCallback.apply(this, arguments);
}

// Emit an error if an error occured during loading of Spooky
if (err) {
this.emit('error', 'Failed to initialize SpookyJS: ' + err);

// If 'exec' is a string then we just execute it directly
} else if (typeof options.exec === 'string') {
this.exec(options.exec);

// Otherwise read in the specified file
} else if (options.exec.file) {
fs.readFile(options.exec.file, (function(err, data) {
// Emit an error if the file didn't load
if (err) {
this.emit('error', 'Failed to load: ' +
options.exec.file);

// Execute the contents of the file, passing in any
// options that the user may have specified
} else {
this.exec(data.toString(), options.exec.options);
}
}).bind(this));

// Otherwise execute the specified code string
} else if (options.exec.code) {
this.exec(options.exec.code.toString(), options.exec.options);
}
}).bind(this);
}

// listen for JSON-RPC requests from the child
this._rpcServer = new tinyjsonrpc.StreamServer();
this._rpcServer.listen(duplex(
this._child.stdin,
new FilteredStream(this._child.stdout, function (data) {
return isJsonRpcRequest(data);
})));
new FilteredStream(this._child.stdout, isJsonRpcRequest)));

this._rpcServer.provide({
emit: (function (event) {
Expand All @@ -130,8 +178,7 @@ function Spooky(options, callback) {
}).bind(this));
}

Spooky.prototype = new EventEmitter();
Spooky.prototype.constructor = Spooky;
util.inherits(Spooky, EventEmitter);

Spooky._instances = {};
Spooky._nextInstanceId = 0;
Expand Down Expand Up @@ -170,11 +217,9 @@ Spooky.prototype._spawnChild = function () {
// emit anything that isn't JSON-RPC traffic as a console event
(new FilteredStream(child.stdout, function (data) {
return !isJsonRpcResponse(data) && !isJsonRpcRequest(data);
})).on('data', this.emit.bind(this, 'console'));

var stdin = child.stdin;
child.stdin = new BufferedStream(options.bufferSize);
child.stdin.pipe(stdin);
})).on('data', (function (data) {
this.emit('console', data.toString());
}).bind(this));

child.on('exit', (function (code, signal) {
var e;
Expand Down