Version 1
[yaffs-website] / node_modules / phridge / test / Phantom.test.js
1 "use strict";
2
3 /* eslint-env browser */
4 /* global config */
5
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");
20
21 require("./helpers/setup.js");
22
23 describe("Phantom", function () {
24     var childProcessMock = createChildProcessMock();
25     var phantom;
26     var spawnPhantom;
27     var exitPhantom;
28     var stdout;
29     var stderr;
30
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();
36     }
37
38     function unmockConfigStreams() {
39         phridge.config.stdout = stdout;
40         phridge.config.stderr = stderr;
41     }
42
43     spawnPhantom = slow(function () {
44         if (phantom && phantom._isDisposed === false) {
45             return undefined;
46         }
47         return phridge.spawn({ someConfig: true })
48             .then(function (newPhantom) {
49                 phantom = newPhantom;
50             });
51     });
52     exitPhantom = slow(function () {
53         if (!phantom) {
54             return undefined;
55         }
56         return phantom.dispose();
57     });
58
59     before(testServer.start);
60     after(exitPhantom);
61     after(testServer.stop);
62
63     describe(".prototype", function () {
64
65         beforeEach(spawnPhantom);
66
67         it("should inherit from EventEmitter", function () {
68             expect(phantom).to.be.instanceOf(EventEmitter);
69         });
70
71         describe("when an unexpected error on the childProcess occurs", function () {
72
73             it("should emit an 'unexpectedExit'-event", function (done) {
74                 var error = new Error("Something bad happened");
75
76                 phantom.on("unexpectedExit", function (err) {
77                     expect(err).to.equal(error);
78                     done();
79                 });
80                 phantom.childProcess.emit("error", error);
81             });
82
83         });
84
85         describe("when the childProcess was killed autonomously", function () {
86
87             it("should be safe to call .dispose() after the process was killed", function () {
88                 phantom.childProcess.kill();
89                 return phantom.dispose();
90             });
91
92             it("should emit an 'unexpectedExit'-event", function (done) {
93                 phantom.on("unexpectedExit", function () {
94                     done();
95                 });
96                 phantom.childProcess.kill();
97             });
98
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
102                 });
103                 phantom.childProcess.kill();
104                 phantom.dispose().then(done, done);
105             });
106
107         });
108
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.
112
113             after(function () {
114                 // Remove mocked Phantom instances from the instances-array
115                 instances.length = 0;
116             });
117
118             it("should return an instance of Phantom", function () {
119                 phantom = new Phantom(childProcessMock);
120                 expect(phantom).to.be.an.instanceof(Phantom);
121             });
122
123             it("should set the childProcess", function () {
124                 phantom = new Phantom(childProcessMock);
125                 expect(phantom.childProcess).to.equal(childProcessMock);
126             });
127
128             it("should add the instance to the instances array", function () {
129                 expect(instances).to.contain(phantom);
130             });
131
132         });
133
134         describe(".childProcess", function () {
135
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");
141             });
142
143         });
144
145         describe(".run(arg1, arg2, arg3, fn)", function () {
146
147             describe("with fn being an asynchronous function", function () {
148
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");
153                 });
154
155                 it("should provide the possibility to resolve with any stringify-able data", function () {
156                     return Promise.all([
157                         expect(phantom.run(function (resolve) {
158                             resolve();
159                         })).to.eventually.equal(undefined),
160                         expect(phantom.run(function (resolve) {
161                             resolve(true);
162                         })).to.eventually.equal(true),
163                         expect(phantom.run(function (resolve) {
164                             resolve(2);
165                         })).to.eventually.equal(2),
166                         expect(phantom.run(function (resolve) {
167                             resolve(null);
168                         })).to.eventually.equal(null),
169                         expect(phantom.run(function (resolve) {
170                             resolve([1, 2, 3]);
171                         })).to.eventually.deep.equal([1, 2, 3]),
172                         expect(phantom.run(function (resolve) {
173                             resolve({
174                                 someArr: [1, 2, 3],
175                                 otherObj: {}
176                             });
177                         })).to.eventually.deep.equal({
178                             someArr: [1, 2, 3],
179                             otherObj: {}
180                         })
181                     ]);
182                 });
183
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");
189                     });
190                 });
191
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 + '" ';
194
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");
198                         done();
199                     });
200                 }));
201
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 + '" ';
204
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");
208                         done();
209                     });
210                 }));
211
212             });
213
214             describe("with fn being a synchronous function", function () {
215
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");
220                 });
221
222                 it("should provide the possibility to resolve with any stringify-able data", function () {
223                     return Promise.all([
224                         expect(phantom.run(function () {
225                             // returns undefined
226                         })).to.eventually.equal(undefined),
227                         expect(phantom.run(function () {
228                             return true;
229                         })).to.eventually.equal(true),
230                         expect(phantom.run(function () {
231                             return 2;
232                         })).to.eventually.equal(2),
233                         expect(phantom.run(function () {
234                             return null;
235                         })).to.eventually.equal(null),
236                         expect(phantom.run(function () {
237                             return [1, 2, 3];
238                         })).to.eventually.deep.equal([1, 2, 3]),
239                         expect(phantom.run(function () {
240                             return {
241                                 someArr: [1, 2, 3],
242                                 otherObj: {}
243                             };
244                         })).to.eventually.deep.equal({
245                             someArr: [1, 2, 3],
246                             otherObj: {}
247                         })
248                     ]);
249                 });
250
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");
256                     });
257                 });
258
259             });
260
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);
265             });
266
267             it("should provide the config object to store all kind of configuration", function () {
268                 return expect(phantom.run(function () {
269                     return config;
270                 })).to.eventually.deep.equal({
271                     someConfig: true
272                 });
273             });
274
275             it("should provide the possibility to pass params", function () {
276                 var params = {
277                     some: ["param"],
278                     withSome: "crazy",
279                     values: {
280                         number1: 1
281                     }
282                 };
283
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]);
287             });
288
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");
293             });
294
295             it("should preserve all error details like stack traces", function () {
296                 return Promise.all([
297                     phantom
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);
304                         }),
305                     phantom
306                         .run(function (resolve, reject) {
307                             reject(new Error("Custom Error"));
308                         })
309                         .catch(function (err) {
310                             expect(err).to.have.property("message", "Custom Error");
311                             expect(err).to.have.property("stack");
312                         })
313                 ]);
314             });
315
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");
320                     }
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");
326                         }
327                     });
328                 });
329             });
330
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 () {})
337                     .then(function () {
338                         throw new Error("There should be an error");
339                     }, function (err) {
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);
344                     });
345             });
346
347         });
348
349         describe(".createPage()", function () {
350
351             it("should return an instance of Page", function () {
352                 expect(phantom.createPage()).to.be.an.instanceof(Page);
353             });
354
355         });
356
357         describe(".openPage(url)", function () {
358
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);
361             }));
362
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 () {
366                         var headline;
367                         var imgIsLoaded;
368
369                         headline = this.evaluate(function () {
370                             return document.querySelector("h1").innerText;
371                         });
372                         imgIsLoaded = this.evaluate(function () {
373                             return document.querySelector("img").width > 0;
374                         });
375
376                         if (headline !== "This is a test page") {
377                             throw new Error("Unexpected headline: " + headline);
378                         }
379                         if (imgIsLoaded !== true) {
380                             throw new Error("The image has not loaded yet");
381                         }
382                     });
383                 });
384             }));
385
386             it("should reject when the page is not available", slow(function () {
387                 return expect(
388                     phantom.openPage("http://localhost:1")
389                 ).to.be.rejectedWith("Cannot load http://localhost:1: PhantomJS returned status fail");
390             }));
391
392         });
393
394         describe(".dispose()", function () {
395
396             before(mockConfigStreams);
397             beforeEach(spawnPhantom);
398             after(unmockConfigStreams);
399
400             it("should terminate the child process with exit-code 0 and then resolve", slow(function () {
401                 var exit = false;
402
403                 phantom.childProcess.on("exit", function (code) {
404                     expect(code).to.equal(0);
405                     exit = true;
406                 });
407
408                 return phantom.dispose().then(function () {
409                     expect(exit).to.equal(true);
410                     phantom = null;
411                 });
412             }));
413
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);
417                     phantom = null;
418                 });
419             }));
420
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();
425
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);
429                     phantom = null;
430                 });
431             });
432
433             it("should be safe to call .dispose() multiple times", slow(function () {
434                 return Promise.all([
435                     phantom.dispose(),
436                     phantom.dispose(),
437                     phantom.dispose()
438                 ]);
439             }));
440
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");
445             });
446
447             it("should not be possible to call .run() after an unexpected exit", function () {
448                 phantom.childProcess.emit("error");
449                 return phantom.run(function () {})
450                     .then(function () {
451                         throw new Error("There should be an error");
452                     }, function (err) {
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);
457                     });
458             });
459
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();
464
465                 // Simulate a pong message from PhantomJS
466                 phantom._receive(message);
467
468                 return promise;
469             });
470
471         });
472
473     });
474
475 });
476
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 () {
481         ps.lookup({
482             command: "phantomjs"
483         }, function onLookUp(err, phantomJsProcesses) {
484             if (err) {
485                 throw new Error(err);
486             }
487             if (phantomJsProcesses.length > 0) {
488                 throw new Error("PhantomJS zombies detected");
489             }
490             done();
491         });
492     }, 2000);
493 }));