Introduction

David Buezas edited this page Aug 3, 2015 · 9 revisions
Clone this wiki locally

SpookyJS makes it possible to drive CasperJS suites from Node.js. At a high level, Spooky accomplishes this by spawning Casper as a child process and controlling it via RPC.

Specifically, each Spooky instance spawns a child Casper process that runs a bootstrap script. The bootstrap script sets up a JSON-RPC server that listens for commands from the parent Spooky instance over a transport (either HTTP or stdio). The script also sets up a JSON-RPC client that sends events to the parent Spooky instance via stdout.

JavaScript Environments

The tricky part of this arrangement is that there are now three distinct JavaScript environments to consider: Spooky (Node.js), Casper (PhantomJS), and the web page under test.

spooky.start('http://example.com/the-page.html');

spooky.then(function () {
  // this function runs in Casper's environment
});

spooky.thenEvaluate(function () {
  // this function runs in the page's environment
})

// this function (and the three spooky calls above) runs in Spooky's environment
spooky.run();

JavaScript environments are isolated

The Node.js, PhantomJS, and web page JavaScript environments are isolated from one another. It is not possible to reference values in one environment from another. Spooky serializes the functions it is passed and sends them to the child, which deserializes them in its environment. This means that variables can appear to be in scope when in fact they aren't even in the same JavaScript environment!

var x = 'spooky';
spooky.start('http://example.com/the-page.html');

spooky.then(function () {
  var y = 'casper';
  console.log('x:', x); // -> x: undefined
});

spooky.thenEvaluate(function () {
  console.log('x:', x); // -> x: undefined
  console.log('y:', y); // -> y: undefined
});

spooky.run();

Passing values between JavaScript environments

However, it is possible to copy values from one environment to another - provided the value can be serialized to JSON. Spooky provides two ways to do this.

Argument hashes

First, Spooky's analogues of Casper methods that accept an argument hash also accept a hash. Spooky does not yet support the new calling convention introduced in Casper 1.0; see #68.

var x = 'spooky';

// spooky.thenEvaluate accepts an options argument (just like Casper)
spooky.thenEvaluate(function (x) {
  console.log('x:', x); // -> x: spooky
}, {
  x: x
});

Function tuples

Second, wherever a Casper method accepts a function, its Spooky analogue accepts a function tuple. A function tuple is an array of length two. The first element is an argument hash. The second is the function that will be passed to the Casper method. Each key in the argument hash is made available as a variable of the same name in the function's scope, initialized to the key's value in the hash. This makes it possible to use a function tuple and an argument hash in the same call.

var x = 'spooky';
var y = 'kooky';

// spooky.then accepts a function tuple
spooky.then([{
  x: x
}, function () {
  console.log('x:', x); // -> x: spooky
}]);

// spooky.thenEvaluate accepts both a function tuple and an argument hash
spooky.thenEvaluate([{
  y: y
}, function (x) {
  console.log('x:', x); // -> x: spooky
  console.log('y:', y); // -> y: kooky
}], {
  x: x
});

The observed variable values in Casper are the result of serializing to JSON and then deserializing. This means that values like undefined will not be passed through to Casper.

spooky.then([{
  x: undefined
}, function () {
  console.log('x:', x); // -> ReferenceError: `x` is not defined
}]);

Arguments are passed by value

Remember that function tuples receive the value of any variables passed to them. If the function modifies its copy, that change is not observed in Spooky's environment. This is true of argument hashes as well.

var x = 'spooky';

spooky.then([{
  x: x
}, function () {
  x = 'casper';
  console.log('x:', x); // -> x: casper
}]);

console.log('x:', x); // -> x: spooky

Putting it all together

It's possible to do complicated things with these tools, but use restraint: it can quickly become difficult to keep the scopes and environments straight.

spooky.start('http://example.com/the-page.html');

var x = 'spooky';
spooky.then([{
  x: x
}, function () {
  var y = 'casper';
  var z = this.evaluate(function (x, y) {
    console.log('x:', x); // -> x: spooky
    console.log('y:', y); // -> y: casper

    return [x, y, 'and the-page.html'].join(', ');
  }, {
    x: x,
    y: y
  });

  console.log('z:', z); // -> z: spooky, casper, and the-page.html
}]);

spooky.run();