3 /* eslint-env browser */
6 var chai = require("chai");
7 var sinon = require("sinon");
8 var EventEmitter = require("events").EventEmitter;
9 var childProcess = require("child_process");
10 var ps = require("ps-node");
11 var expect = chai.expect;
12 var phridge = require("../lib/main.js");
13 var Phantom = require("../lib/Phantom.js");
14 var Page = require("../lib/Page.js");
15 var instances = require("../lib/instances.js");
16 var slow = require("./helpers/slow.js");
17 var testServer = require("./helpers/testServer.js");
18 var Writable = require("stream").Writable;
19 var createChildProcessMock = require("./helpers/createChildProcessMock.js");
21 require("./helpers/setup.js");
23 describe("Phantom", function () {
24 var childProcessMock = createChildProcessMock();
31 function mockConfigStreams() {
32 stdout = phridge.config.stdout;
33 stderr = phridge.config.stderr;
34 phridge.config.stdout = new Writable();
35 phridge.config.stderr = new Writable();
38 function unmockConfigStreams() {
39 phridge.config.stdout = stdout;
40 phridge.config.stderr = stderr;
43 spawnPhantom = slow(function () {
44 if (phantom && phantom._isDisposed === false) {
47 return phridge.spawn({ someConfig: true })
48 .then(function (newPhantom) {
52 exitPhantom = slow(function () {
56 return phantom.dispose();
59 before(testServer.start);
61 after(testServer.stop);
63 describe(".prototype", function () {
65 beforeEach(spawnPhantom);
67 it("should inherit from EventEmitter", function () {
68 expect(phantom).to.be.instanceOf(EventEmitter);
71 describe("when an unexpected error on the childProcess occurs", function () {
73 it("should emit an 'unexpectedExit'-event", function (done) {
74 var error = new Error("Something bad happened");
76 phantom.on("unexpectedExit", function (err) {
77 expect(err).to.equal(error);
80 phantom.childProcess.emit("error", error);
85 describe("when the childProcess was killed autonomously", function () {
87 it("should be safe to call .dispose() after the process was killed", function () {
88 phantom.childProcess.kill();
89 return phantom.dispose();
92 it("should emit an 'unexpectedExit'-event", function (done) {
93 phantom.on("unexpectedExit", function () {
96 phantom.childProcess.kill();
99 it("should not emit an 'unexpectedExit'-event when the phantom instance was disposed in the meantime", function (done) {
100 phantom.on("unexpectedExit", function () {
101 done(); // Will trigger an error that done() has been called twice
103 phantom.childProcess.kill();
104 phantom.dispose().then(done, done);
109 describe(".constructor(childProcess)", function () {
110 var phantom; // It's important to shadow the phantom variable inside this describe block.
111 // This way spawnPhantom and exitPhantom don't use our mocked Phantom instance.
114 // Remove mocked Phantom instances from the instances-array
115 instances.length = 0;
118 it("should return an instance of Phantom", function () {
119 phantom = new Phantom(childProcessMock);
120 expect(phantom).to.be.an.instanceof(Phantom);
123 it("should set the childProcess", function () {
124 phantom = new Phantom(childProcessMock);
125 expect(phantom.childProcess).to.equal(childProcessMock);
128 it("should add the instance to the instances array", function () {
129 expect(instances).to.contain(phantom);
134 describe(".childProcess", function () {
136 it("should provide a reference on the child process object created by node", function () {
137 expect(phantom.childProcess).to.be.an("object");
138 expect(phantom.childProcess.stdin).to.be.an("object");
139 expect(phantom.childProcess.stdout).to.be.an("object");
140 expect(phantom.childProcess.stderr).to.be.an("object");
145 describe(".run(arg1, arg2, arg3, fn)", function () {
147 describe("with fn being an asynchronous function", function () {
149 it("should provide a resolve function", function () {
150 return expect(phantom.run(function (resolve) {
151 resolve("everything ok");
152 })).to.eventually.equal("everything ok");
155 it("should provide the possibility to resolve with any stringify-able data", function () {
157 expect(phantom.run(function (resolve) {
159 })).to.eventually.equal(undefined),
160 expect(phantom.run(function (resolve) {
162 })).to.eventually.equal(true),
163 expect(phantom.run(function (resolve) {
165 })).to.eventually.equal(2),
166 expect(phantom.run(function (resolve) {
168 })).to.eventually.equal(null),
169 expect(phantom.run(function (resolve) {
171 })).to.eventually.deep.equal([1, 2, 3]),
172 expect(phantom.run(function (resolve) {
177 })).to.eventually.deep.equal({
184 it("should provide a reject function", function () {
185 return phantom.run(function (resolve, reject) {
186 reject(new Error("not ok"));
187 }).catch(function (err) {
188 expect(err.message).to.equal("not ok");
192 it("should print an error when resolve is called and the request has already been finished", slow(function (done) {
193 var execPath = '"' + process.execPath + '" ';
195 childProcess.exec(execPath + require.resolve("./cases/callResolveTwice"), function (error, stdout, stderr) {
196 expect(error).to.equal(null);
197 expect(stderr).to.contain("Cannot call resolve() after the promise has already been resolved or rejected");
202 it("should print an error when reject is called and the request has already been finished", slow(function (done) {
203 var execPath = '"' + process.execPath + '" ';
205 childProcess.exec(execPath + require.resolve("./cases/callRejectTwice"), function (error, stdout, stderr) {
206 expect(error).to.equal(null);
207 expect(stderr).to.contain("Cannot call reject() after the promise has already been resolved or rejected");
214 describe("with fn being a synchronous function", function () {
216 it("should resolve to the returned value", function () {
217 return expect(phantom.run(function () {
218 return "everything ok";
219 })).to.eventually.equal("everything ok");
222 it("should provide the possibility to resolve with any stringify-able data", function () {
224 expect(phantom.run(function () {
226 })).to.eventually.equal(undefined),
227 expect(phantom.run(function () {
229 })).to.eventually.equal(true),
230 expect(phantom.run(function () {
232 })).to.eventually.equal(2),
233 expect(phantom.run(function () {
235 })).to.eventually.equal(null),
236 expect(phantom.run(function () {
238 })).to.eventually.deep.equal([1, 2, 3]),
239 expect(phantom.run(function () {
244 })).to.eventually.deep.equal({
251 it("should reject the promise if fn throws an error", function () {
252 return phantom.run(function () {
253 throw new Error("not ok");
254 }).catch(function (err) {
255 expect(err.message).to.equal("not ok");
261 it("should provide all phantomjs default modules as convenience", function () {
262 return expect(phantom.run(function () {
263 return Boolean(webpage && system && fs && webserver && child_process); // eslint-disable-line
264 })).to.eventually.equal(true);
267 it("should provide the config object to store all kind of configuration", function () {
268 return expect(phantom.run(function () {
270 })).to.eventually.deep.equal({
275 it("should provide the possibility to pass params", function () {
284 return expect(phantom.run(params, params, params, function (params1, params2, params3) {
285 return [params1, params2, params3];
286 })).to.eventually.deep.equal([params, params, params]);
289 it("should report errors", function () {
290 return expect(phantom.run(function () {
291 undefinedVariable; // eslint-disable-line
292 })).to.be.rejectedWith("Can't find variable: undefinedVariable");
295 it("should preserve all error details like stack traces", function () {
298 .run(function brokenFunction() {
299 undefinedVariable; // eslint-disable-line
300 }).catch(function (err) {
301 expect(err).to.have.property("message", "Can't find variable: undefinedVariable");
302 expect(err).to.have.property("stack");
303 //console.log(err.stack);
306 .run(function (resolve, reject) {
307 reject(new Error("Custom Error"));
309 .catch(function (err) {
310 expect(err).to.have.property("message", "Custom Error");
311 expect(err).to.have.property("stack");
316 it("should run all functions on the same empty context", function () {
317 return phantom.run(/** @this Object */function () {
318 if (JSON.stringify(this) !== "{}") {
319 throw new Error("The context is not an empty object");
321 this.message = "Hi from the first run";
322 }).then(function () {
323 return phantom.run(/** @this Object */function () {
324 if (this.message !== "Hi from the first run") {
325 throw new Error("The context is not persistent");
331 it("should reject with an error if PhantomJS process is killed", function () {
332 // Phantom will eventually emit an error event when the childProcess was killed
333 // In order to prevent node from throwing the error, we need to add a dummy error event listener
334 phantom.on("error", Function.prototype);
335 phantom.childProcess.kill();
336 return phantom.run(function () {})
338 throw new Error("There should be an error");
340 expect(err).to.be.an.instanceOf(Error);
341 expect(err.message).to.contain("Cannot communicate with PhantomJS process");
342 expect(err.originalError).to.be.an.instanceOf(Error);
343 expect(err.message).to.contain(err.originalError.message);
349 describe(".createPage()", function () {
351 it("should return an instance of Page", function () {
352 expect(phantom.createPage()).to.be.an.instanceof(Page);
357 describe(".openPage(url)", function () {
359 it("should resolve to an instance of Page", slow(/** @this Runner */function () {
360 return expect(phantom.openPage(this.testServerUrl)).to.eventually.be.an.instanceof(Page);
363 it("should resolve when the given page has loaded", slow(/** @this Runner */function () {
364 return phantom.openPage(this.testServerUrl).then(function (page) {
365 return page.run(/** @this WebPage */function () {
369 headline = this.evaluate(function () {
370 return document.querySelector("h1").innerText;
372 imgIsLoaded = this.evaluate(function () {
373 return document.querySelector("img").width > 0;
376 if (headline !== "This is a test page") {
377 throw new Error("Unexpected headline: " + headline);
379 if (imgIsLoaded !== true) {
380 throw new Error("The image has not loaded yet");
386 it("should reject when the page is not available", slow(function () {
388 phantom.openPage("http://localhost:1")
389 ).to.be.rejectedWith("Cannot load http://localhost:1: PhantomJS returned status fail");
394 describe(".dispose()", function () {
396 before(mockConfigStreams);
397 beforeEach(spawnPhantom);
398 after(unmockConfigStreams);
400 it("should terminate the child process with exit-code 0 and then resolve", slow(function () {
403 phantom.childProcess.on("exit", function (code) {
404 expect(code).to.equal(0);
408 return phantom.dispose().then(function () {
409 expect(exit).to.equal(true);
414 it("should remove the instance from the instances array", slow(function () {
415 return phantom.dispose().then(function () {
416 expect(instances).to.not.contain(phantom);
421 // @see https://github.com/peerigon/phridge/issues/27
422 it("should neither call end() on config.stdout nor config.stderr", function () {
423 phridge.config.stdout.end = sinon.spy();
424 phridge.config.stderr.end = sinon.spy();
426 return phantom.dispose().then(function () {
427 expect(phridge.config.stdout.end).to.have.callCount(0);
428 expect(phridge.config.stderr.end).to.have.callCount(0);
433 it("should be safe to call .dispose() multiple times", slow(function () {
441 it("should not be possible to call .run() after .dispose()", function () {
442 expect(phantom.dispose().then(function () {
443 return phantom.run(function () {});
444 })).to.be.rejectedWith("Cannot run function: Phantom instance is already disposed");
447 it("should not be possible to call .run() after an unexpected exit", function () {
448 phantom.childProcess.emit("error");
449 return phantom.run(function () {})
451 throw new Error("There should be an error");
453 expect(err).to.be.an.instanceOf(Error);
454 expect(err.message).to.contain("Cannot run function");
455 expect(err.originalError).to.be.an.instanceOf(Error);
456 expect(err.message).to.contain(err.originalError.message);
460 // @see https://github.com/peerigon/phridge/issues/41
461 it("should not schedule a new ping when a pong message is received right after calling dispose()", function () {
462 var message = JSON.stringify({ status: "pong" });
463 var promise = phantom.dispose();
465 // Simulate a pong message from PhantomJS
466 phantom._receive(message);
477 // This last test checks for the presence of PhantomJS zombies that might have been spawned during tests.
478 // We don't want phridge to leave zombies at all circumstances.
479 after(slow(function (done) {
480 setTimeout(function () {
483 }, function onLookUp(err, phantomJsProcesses) {
485 throw new Error(err);
487 if (phantomJsProcesses.length > 0) {
488 throw new Error("PhantomJS zombies detected");