casperjs 1.0.0.RC1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/CHANGELOG.md +289 -1
  2. data/CONTRIBUTING.md +93 -0
  3. data/CONTRIBUTORS.md +53 -0
  4. data/README.md +29 -1
  5. data/batchbin/casperjs.bat +5 -0
  6. data/bin/bootstrap.js +164 -114
  7. data/bin/casperjs +84 -0
  8. data/bin/usage.txt +2 -1
  9. data/casperjs.gemspec +7 -4
  10. data/modules/casper.js +661 -239
  11. data/modules/clientutils.js +240 -54
  12. data/modules/colorizer.js +2 -1
  13. data/modules/events.js +13 -1
  14. data/modules/http.js +70 -0
  15. data/modules/mouse.js +1 -2
  16. data/modules/pagestack.js +166 -0
  17. data/modules/tester.js +1013 -659
  18. data/modules/utils.js +519 -367
  19. data/modules/vendors/coffee-script.js +2 -2
  20. data/modules/xunit.js +56 -24
  21. data/package.json +2 -2
  22. data/rpm/casperjs.spec +203 -0
  23. data/rpm/mkrpm.sh +25 -0
  24. data/rubybin/casperjs +9 -4
  25. data/samples/bbcshots.coffee +11 -10
  26. data/samples/bbcshots.js +16 -15
  27. data/samples/cliplay.js +3 -0
  28. data/samples/customevents.js +3 -0
  29. data/samples/customlogging.coffee +17 -19
  30. data/samples/customlogging.js +26 -27
  31. data/samples/download.js +4 -1
  32. data/samples/dynamic.js +3 -0
  33. data/samples/each.js +3 -0
  34. data/samples/events.js +4 -2
  35. data/samples/extends.js +4 -1
  36. data/samples/googlelinks.coffee +4 -1
  37. data/samples/googlelinks.js +10 -3
  38. data/samples/googlematch.coffee +1 -1
  39. data/samples/googlematch.js +5 -2
  40. data/samples/googlepagination.js +4 -1
  41. data/samples/googletesting.js +3 -0
  42. data/samples/logcolor.js +3 -0
  43. data/samples/metaextract.js +3 -0
  44. data/samples/multirun.js +3 -0
  45. data/samples/screenshot.js +4 -1
  46. data/samples/statushandlers.js +4 -1
  47. data/samples/steptimeout.js +3 -0
  48. data/samples/timeout.js +4 -1
  49. data/samples/translate.coffee +23 -0
  50. data/samples/translate.js +30 -0
  51. data/tests/commands/mytest.js +14 -0
  52. data/tests/commands/script.js +3 -0
  53. data/tests/run.js +82 -37
  54. data/tests/sample_modules/config.json +1 -0
  55. data/tests/sample_modules/csmodule.coffee +1 -0
  56. data/tests/sample_modules/jsmodule.js +1 -0
  57. data/tests/selftest.js +57 -0
  58. data/tests/site/dummy.js +1 -0
  59. data/tests/site/field-array.html +14 -0
  60. data/tests/site/frame1.html +10 -0
  61. data/tests/site/frame2.html +11 -0
  62. data/tests/site/frame3.html +11 -0
  63. data/tests/site/frames.html +12 -0
  64. data/tests/site/global.html +6 -1
  65. data/tests/site/includes/include1.js +6 -0
  66. data/tests/site/includes/include2.js +6 -0
  67. data/tests/site/index.html +3 -0
  68. data/tests/site/popup.html +19 -0
  69. data/tests/site/resources.html +7 -8
  70. data/tests/site/urls.html +14 -0
  71. data/tests/suites/casper/agent.js +3 -1
  72. data/tests/suites/casper/alert.js +14 -0
  73. data/tests/suites/casper/auth.js +24 -0
  74. data/tests/suites/casper/capture.js +12 -12
  75. data/tests/suites/casper/click.js +11 -1
  76. data/tests/suites/casper/confirm.js +24 -16
  77. data/tests/suites/casper/debug.js +10 -0
  78. data/tests/suites/casper/elementattribute.js +3 -1
  79. data/tests/suites/casper/encode.js +6 -2
  80. data/tests/suites/casper/evaluate.js +78 -18
  81. data/tests/suites/casper/events.js +3 -1
  82. data/tests/suites/casper/exists.js +3 -1
  83. data/tests/suites/casper/fetchtext.js +3 -1
  84. data/tests/suites/casper/flow.coffee +1 -1
  85. data/tests/suites/casper/formfill.js +39 -4
  86. data/tests/suites/casper/frames.js +43 -0
  87. data/tests/suites/casper/global.js +9 -3
  88. data/tests/suites/casper/headers.js +41 -0
  89. data/tests/suites/casper/history.js +3 -1
  90. data/tests/suites/casper/hooks.js +3 -1
  91. data/tests/suites/casper/keys.js +15 -0
  92. data/tests/suites/casper/location.js +21 -0
  93. data/tests/suites/casper/logging.js +3 -1
  94. data/tests/suites/casper/mouseevents.js +3 -1
  95. data/tests/suites/casper/onerror.js +3 -1
  96. data/tests/suites/casper/open.js +63 -1
  97. data/tests/suites/casper/popup.js +86 -0
  98. data/tests/suites/casper/prompt.js +11 -15
  99. data/tests/suites/casper/request.js +36 -0
  100. data/tests/suites/casper/resources.coffee +5 -5
  101. data/tests/suites/casper/scripts.js +32 -0
  102. data/tests/suites/casper/start.js +3 -1
  103. data/tests/suites/casper/steps.js +4 -2
  104. data/tests/suites/casper/urls.js +21 -0
  105. data/tests/suites/casper/viewport.js +3 -1
  106. data/tests/suites/casper/visible.js +3 -1
  107. data/tests/suites/casper/wait.js +22 -12
  108. data/tests/suites/casper/xpath.js +3 -1
  109. data/tests/suites/cli.js +3 -1
  110. data/tests/suites/clientutils.js +57 -3
  111. data/tests/suites/coffee.coffee +1 -1
  112. data/tests/suites/fs.js +3 -1
  113. data/tests/suites/http_status.js +19 -3
  114. data/tests/suites/popup.js +33 -0
  115. data/tests/suites/require.js +31 -0
  116. data/tests/suites/tester.js +134 -29
  117. data/tests/suites/utils.js +108 -8
  118. data/tests/suites/xunit.js +37 -6
  119. metadata +49 -15
  120. data/modules/injector.js +0 -93
  121. data/tests/suites/injector.js +0 -64
@@ -1,8 +1,10 @@
1
+ # by hannyu
1
2
 
3
+ CASPERJS_VERSION = File.read("package.json")[/version.*:.*"(.*?)"/,1].gsub(/[\-_\+]/,".")
2
4
 
3
5
  Gem::Specification.new do |s|
4
6
  s.name = "casperjs"
5
- s.version = "1.0.0.RC1"
7
+ s.version = CASPERJS_VERSION
6
8
  s.homepage = "http://casperjs.org/"
7
9
  s.authors = ["Nicolas Perriault", ]
8
10
  s.email = ["nperriault@gmail.com",]
@@ -10,12 +12,13 @@ Gem::Specification.new do |s|
10
12
  It eases the process of defining a full navigation scenario and provides useful
11
13
  high-level functions, methods & syntaxic sugar for doing common tasks."
12
14
  s.summary = "Navigation scripting & testing utility for PhantomJS"
15
+ s.has_rdoc = false
13
16
  s.extra_rdoc_files = ["LICENSE.md", "README.md"]
14
- s.files = Dir["LICENSE.md", "README.md", "CHANGELOG.md", "package.json", "casperjs.gemspec",
15
- "bin/bootstrap.js", "bin/usage.txt", "bin/casperjs_python",
17
+ s.files = Dir["*.md", "package.json", "casperjs.gemspec",
18
+ "bin/*", "batchbin/*", "rpm/*",
16
19
  "docs/**/*", "modules/**/*", "samples/**/*", "tests/**/*"]
17
20
  s.bindir = "rubybin"
18
21
  s.executables = [ "casperjs" ]
19
22
  s.license = "MIT"
20
- s.requirements = [ "PhantomJS v1.5" ]
23
+ s.requirements = [ "PhantomJS v1.7" ]
21
24
  end
@@ -28,12 +28,14 @@
28
28
  *
29
29
  */
30
30
 
31
- /*global CasperError console exports phantom require*/
31
+ /*global CasperError console exports phantom require __utils__*/
32
32
 
33
33
  var colorizer = require('colorizer');
34
34
  var events = require('events');
35
35
  var fs = require('fs');
36
+ var http = require('http');
36
37
  var mouse = require('mouse');
38
+ var pagestack = require('pagestack');
37
39
  var qs = require('querystring');
38
40
  var tester = require('tester');
39
41
  var utils = require('utils');
@@ -74,6 +76,7 @@ exports.selectXPath = selectXPath;
74
76
  */
75
77
  var Casper = function Casper(options) {
76
78
  "use strict";
79
+ /*jshint maxstatements:40*/
77
80
  // init & checks
78
81
  if (!(this instanceof Casper)) {
79
82
  return new Casper(options);
@@ -94,16 +97,25 @@ var Casper = function Casper(options) {
94
97
  onResourceReceived: null,
95
98
  onResourceRequested: null,
96
99
  onStepComplete: null,
97
- onStepTimeout: null,
98
- onTimeout: null,
100
+ onStepTimeout: function _onStepTimeout(timeout, stepNum) {
101
+ this.die("Maximum step execution timeout exceeded for step " + stepNum);
102
+ },
103
+ onTimeout: function _onTimeout(timeout) {
104
+ this.die(f("Script timeout of %dms reached, exiting.", timeout));
105
+ },
106
+ onWaitTimeout: function _onWaitTimeout(timeout) {
107
+ this.die(f("Wait timeout of %dms expired, exiting.", timeout));
108
+ },
99
109
  page: null,
100
110
  pageSettings: {
101
111
  localToRemoteUrlAccessEnabled: true,
102
112
  userAgent: defaultUserAgent
103
113
  },
114
+ remoteScripts: [],
104
115
  stepTimeout: null,
105
116
  timeout: null,
106
- verbose: false
117
+ verbose: false,
118
+ waitTimeout: 5000
107
119
  };
108
120
  // options
109
121
  this.options = utils.mergeObjects(this.defaults, options);
@@ -111,11 +123,12 @@ var Casper = function Casper(options) {
111
123
  this.checker = null;
112
124
  this.cli = phantom.casperArgs;
113
125
  this.colorizer = this.getColorizer();
126
+ this.currentResponse = undefined;
114
127
  this.currentUrl = 'about:blank';
115
- this.currentHTTPStatus = 0;
116
- this.defaultWaitTimeout = 5000;
128
+ this.currentHTTPStatus = null;
117
129
  this.history = [];
118
130
  this.loadInProgress = false;
131
+ this.navigationRequested = false;
119
132
  this.logFormats = {};
120
133
  this.logLevels = ["debug", "info", "warning", "error"];
121
134
  this.logStyles = {
@@ -127,6 +140,7 @@ var Casper = function Casper(options) {
127
140
  this.mouse = mouse.create(this);
128
141
  this.page = null;
129
142
  this.pendingWait = false;
143
+ this.popups = pagestack.create();
130
144
  this.requestUrl = 'about:blank';
131
145
  this.resources = [];
132
146
  this.result = {
@@ -143,12 +157,14 @@ var Casper = function Casper(options) {
143
157
  this.initErrorHandler();
144
158
 
145
159
  this.on('error', function(msg, backtrace) {
160
+ if (msg === this.test.SKIP_MESSAGE) {
161
+ return;
162
+ }
146
163
  var c = this.getColorizer();
147
164
  var match = /^(.*): __mod_error(.*):: (.*)/.exec(msg);
148
165
  var notices = [];
149
166
  if (match && match.length === 4) {
150
167
  notices.push(' in module ' + match[2]);
151
- notices.push(' NOTICE: errors within modules cannot be backtraced yet.');
152
168
  msg = match[3];
153
169
  }
154
170
  console.error(c.colorize(msg, 'RED_BAR', 80));
@@ -183,6 +199,7 @@ utils.inherits(Casper, events.EventEmitter);
183
199
  */
184
200
  Casper.prototype.back = function back() {
185
201
  "use strict";
202
+ this.checkStarted();
186
203
  return this.then(function _step() {
187
204
  this.emit('back');
188
205
  this.evaluate(function _evaluate() {
@@ -192,7 +209,7 @@ Casper.prototype.back = function back() {
192
209
  };
193
210
 
194
211
  /**
195
- * Encodes a resource using the base64 algorithm synchroneously using
212
+ * Encodes a resource using the base64 algorithm synchronously using
196
213
  * client-side XMLHttpRequest.
197
214
  *
198
215
  * NOTE: we cannot use window.btoa() for some strange reasons here.
@@ -205,8 +222,8 @@ Casper.prototype.back = function back() {
205
222
  Casper.prototype.base64encode = function base64encode(url, method, data) {
206
223
  "use strict";
207
224
  return this.evaluate(function _evaluate(url, method, data) {
208
- return window.__utils__.getBase64(url, method, data);
209
- }, { url: url, method: method, data: data });
225
+ return __utils__.getBase64(url, method, data);
226
+ }, url, method, data);
210
227
  };
211
228
 
212
229
  /**
@@ -221,9 +238,8 @@ Casper.prototype.base64encode = function base64encode(url, method, data) {
221
238
  */
222
239
  Casper.prototype.capture = function capture(targetFile, clipRect) {
223
240
  "use strict";
224
- if (!this.started) {
225
- throw new CasperError("Casper not started, can't capture()");
226
- }
241
+ /*jshint maxstatements:20*/
242
+ this.checkStarted();
227
243
  var previousClipRect;
228
244
  targetFile = fs.absolute(targetFile);
229
245
  if (clipRect) {
@@ -261,16 +277,9 @@ Casper.prototype.capture = function capture(targetFile, clipRect) {
261
277
  */
262
278
  Casper.prototype.captureBase64 = function captureBase64(format, area) {
263
279
  "use strict";
264
- if (!this.started) {
265
- throw new CasperError("Casper not started, can't captureBase64()");
266
- }
267
- if (!('renderBase64' in this.page)) {
268
- this.warn('captureBase64() requires PhantomJS >= 1.6');
269
- return;
270
- }
271
- var base64;
272
- var previousClipRect;
273
- var formats = ['bmp', 'jpg', 'jpeg', 'png', 'ppm', 'tiff', 'xbm', 'xpm'];
280
+ /*jshint maxstatements:20*/
281
+ this.checkStarted();
282
+ var base64, previousClipRect, formats = ['bmp', 'jpg', 'jpeg', 'png', 'ppm', 'tiff', 'xbm', 'xpm'];
274
283
  if (formats.indexOf(format.toLowerCase()) === -1) {
275
284
  throw new CasperError(f('Unsupported format "%s"', format));
276
285
  }
@@ -315,7 +324,7 @@ Casper.prototype.captureSelector = function captureSelector(targetFile, selector
315
324
  */
316
325
  Casper.prototype.checkStep = function checkStep(self, onComplete) {
317
326
  "use strict";
318
- if (self.pendingWait || self.loadInProgress) {
327
+ if (self.pendingWait || self.loadInProgress || self.navigationRequested) {
319
328
  return;
320
329
  }
321
330
  var step = self.steps[self.step++];
@@ -325,6 +334,7 @@ Casper.prototype.checkStep = function checkStep(self, onComplete) {
325
334
  self.result.time = new Date().getTime() - self.startTime;
326
335
  self.log(f("Done %s steps in %dms", self.steps.length, self.result.time), "info");
327
336
  clearInterval(self.checker);
337
+ self.step -= 1;
328
338
  self.emit('run.complete');
329
339
  if (utils.isFunction(onComplete)) {
330
340
  onComplete.call(self, self);
@@ -335,6 +345,20 @@ Casper.prototype.checkStep = function checkStep(self, onComplete) {
335
345
  }
336
346
  };
337
347
 
348
+ /**
349
+ * Checks if this instance is started.
350
+ *
351
+ * @return Boolean
352
+ * @throws CasperError
353
+ */
354
+ Casper.prototype.checkStarted = function checkStarted() {
355
+ "use strict";
356
+ if (!this.started) {
357
+ throw new CasperError(f("Casper is not started, can't execute `%s()`",
358
+ checkStarted.caller.name));
359
+ }
360
+ };
361
+
338
362
  /**
339
363
  * Clears the current page execution environment context. Useful to avoid
340
364
  * having previously loaded DOM contents being still active (refs #34).
@@ -346,6 +370,7 @@ Casper.prototype.checkStep = function checkStep(self, onComplete) {
346
370
  */
347
371
  Casper.prototype.clear = function clear() {
348
372
  "use strict";
373
+ this.checkStarted();
349
374
  this.page.content = '';
350
375
  return this;
351
376
  };
@@ -361,7 +386,12 @@ Casper.prototype.clear = function clear() {
361
386
  */
362
387
  Casper.prototype.click = function click(selector) {
363
388
  "use strict";
364
- return this.mouseEvent('click', selector);
389
+ this.checkStarted();
390
+ var success = this.mouseEvent('click', selector);
391
+ this.evaluate(function(selector) {
392
+ document.querySelector(selector).focus();
393
+ }, selector);
394
+ return success;
365
395
  };
366
396
 
367
397
  /**
@@ -374,12 +404,39 @@ Casper.prototype.click = function click(selector) {
374
404
  */
375
405
  Casper.prototype.clickLabel = function clickLabel(label, tag) {
376
406
  "use strict";
407
+ this.checkStarted();
377
408
  tag = tag || "*";
378
409
  var escapedLabel = label.toString().replace(/"/g, '\\"');
379
410
  var selector = selectXPath(f('//%s[text()="%s"]', tag, escapedLabel));
380
411
  return this.click(selector);
381
412
  };
382
413
 
414
+ /**
415
+ * Configures HTTP authentication parameters. Will try parsing auth credentials from
416
+ * the passed location first, then check for configured settings if any.
417
+ *
418
+ * @param String location Requested url
419
+ * @param Object settings Request settings
420
+ * @return Casper
421
+ */
422
+ Casper.prototype.configureHttpAuth = function configureHttpAuth(location, settings) {
423
+ "use strict";
424
+ var username, password, httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
425
+ this.checkStarted();
426
+ if (httpAuthMatch) {
427
+ this.page.settings.userName = httpAuthMatch[1];
428
+ this.page.settings.password = httpAuthMatch[2];
429
+ } else if (utils.isObject(settings) && settings.username) {
430
+ this.page.settings.userName = settings.username;
431
+ this.page.settings.password = settings.password;
432
+ } else {
433
+ return;
434
+ }
435
+ this.emit('http.auth', username, password);
436
+ this.log("Setting HTTP authentication for user " + username, "info");
437
+ return this;
438
+ };
439
+
383
440
  /**
384
441
  * Creates a step definition.
385
442
  *
@@ -400,12 +457,14 @@ Casper.prototype.createStep = function createStep(fn, options) {
400
457
  /**
401
458
  * Logs the HTML code of the current page.
402
459
  *
460
+ * @param String selector A DOM CSS3/XPath selector (optional)
461
+ * @param Boolean outer Whether to fetch outer HTML contents (default: false)
403
462
  * @return Casper
404
463
  */
405
- Casper.prototype.debugHTML = function debugHTML() {
464
+ Casper.prototype.debugHTML = function debugHTML(selector, outer) {
406
465
  "use strict";
407
- this.echo(this.page.content);
408
- return this;
466
+ this.checkStarted();
467
+ return this.echo(this.getHTML(selector, outer));
409
468
  };
410
469
 
411
470
  /**
@@ -415,6 +474,7 @@ Casper.prototype.debugHTML = function debugHTML() {
415
474
  */
416
475
  Casper.prototype.debugPage = function debugPage() {
417
476
  "use strict";
477
+ this.checkStarted();
418
478
  this.echo(this.evaluate(function _evaluate() {
419
479
  return document.body.textContent || document.body.innerText;
420
480
  }));
@@ -436,6 +496,7 @@ Casper.prototype.die = function die(message, status) {
436
496
  message = "Suite explicitely interrupted without any message given.";
437
497
  }
438
498
  this.log(message, "error");
499
+ this.echo(message, "ERROR");
439
500
  this.emit('die', message, status);
440
501
  if (utils.isFunction(this.options.onDie)) {
441
502
  this.options.onDie.call(this, this, message, status);
@@ -454,7 +515,8 @@ Casper.prototype.die = function die(message, status) {
454
515
  */
455
516
  Casper.prototype.download = function download(url, targetPath, method, data) {
456
517
  "use strict";
457
- var cu = require('clientutils').create(this.options);
518
+ this.checkStarted();
519
+ var cu = require('clientutils').create(utils.mergeObjects({}, this.options));
458
520
  try {
459
521
  fs.write(targetPath, cu.decode(this.base64encode(url, method, data)), 'wb');
460
522
  this.emit('downloaded.file', targetPath);
@@ -503,7 +565,7 @@ Casper.prototype.echo = function echo(text, style, pad) {
503
565
  } catch (e) {
504
566
  try {
505
567
  text = utils.serialize(text);
506
- } catch (e) {
568
+ } catch (e2) {
507
569
  text = '';
508
570
  }
509
571
  }
@@ -522,14 +584,7 @@ Casper.prototype.echo = function echo(text, style, pad) {
522
584
  * document.querySelector('#username').value = username;
523
585
  * document.querySelector('#password').value = password;
524
586
  * document.querySelector('#submit').click();
525
- * }, {
526
- * username: 'Bazoonga',
527
- * password: 'baz00nga'
528
- * })
529
- *
530
- * FIXME: waiting for a patch of PhantomJS to allow direct passing of
531
- * arguments to the function.
532
- * TODO: don't forget to keep this backward compatible.
587
+ * }, 'Bazoonga', 'baz00nga');
533
588
  *
534
589
  * @param Function fn The function to be evaluated within current page DOM
535
590
  * @param Object context Object containing the parameters to inject into the function
@@ -538,12 +593,28 @@ Casper.prototype.echo = function echo(text, style, pad) {
538
593
  */
539
594
  Casper.prototype.evaluate = function evaluate(fn, context) {
540
595
  "use strict";
596
+ this.checkStarted();
597
+ // preliminary checks
598
+ if (!utils.isFunction(fn) && !utils.isString(fn)) { // phantomjs allows functions defs as string
599
+ throw new CasperError("evaluate() only accepts functions or strings");
600
+ }
541
601
  // ensure client utils are always injected
542
602
  this.injectClientUtils();
543
- // function processing
544
- context = utils.isObject(context) ? context : {};
545
- var newFn = require('injector').create(fn).process(context);
546
- return this.page.evaluate(newFn);
603
+ // function context
604
+ if (arguments.length === 1) {
605
+ return this.page.evaluate(fn);
606
+ } else if (arguments.length === 2) {
607
+ // check for closure signature if it matches context
608
+ if (utils.isObject(context) && eval(fn).length === Object.keys(context).length) {
609
+ context = utils.objectValues(context);
610
+ } else {
611
+ context = [context];
612
+ }
613
+ } else {
614
+ // phantomjs-style signature
615
+ context = [].slice.call(arguments).slice(1);
616
+ }
617
+ return this.page.evaluate.apply(this.page, [fn].concat(context));
547
618
  };
548
619
 
549
620
  /**
@@ -552,12 +623,15 @@ Casper.prototype.evaluate = function evaluate(fn, context) {
552
623
  *
553
624
  * @param function fn The expression to evaluate
554
625
  * @param String message The error message to log
626
+ * @param Number status An optional exit status code (must be > 0)
627
+ *
555
628
  * @return Casper
556
629
  */
557
- Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message) {
630
+ Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message, status) {
558
631
  "use strict";
632
+ this.checkStarted();
559
633
  if (!this.evaluate(fn)) {
560
- return this.die(message);
634
+ return this.die(message, status);
561
635
  }
562
636
  return this;
563
637
  };
@@ -571,9 +645,10 @@ Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message) {
571
645
  */
572
646
  Casper.prototype.exists = function exists(selector) {
573
647
  "use strict";
648
+ this.checkStarted();
574
649
  return this.evaluate(function _evaluate(selector) {
575
- return window.__utils__.exists(selector);
576
- }, { selector: selector });
650
+ return __utils__.exists(selector);
651
+ }, selector);
577
652
  };
578
653
 
579
654
  /**
@@ -586,11 +661,10 @@ Casper.prototype.exit = function exit(status) {
586
661
  "use strict";
587
662
  this.emit('exit', status);
588
663
  phantom.exit(status);
589
- return this;
590
664
  };
591
665
 
592
666
  /**
593
- * Fetches innerText within the element(s) matching a given CSS3
667
+ * Fetches plain text contents contained in the DOM element(s) matching a given CSS3/XPath
594
668
  * selector.
595
669
  *
596
670
  * @param String selector A DOM CSS3/XPath selector
@@ -598,9 +672,10 @@ Casper.prototype.exit = function exit(status) {
598
672
  */
599
673
  Casper.prototype.fetchText = function fetchText(selector) {
600
674
  "use strict";
675
+ this.checkStarted();
601
676
  return this.evaluate(function _evaluate(selector) {
602
- return window.__utils__.fetchText(selector);
603
- }, { selector: selector });
677
+ return __utils__.fetchText(selector);
678
+ }, selector);
604
679
  };
605
680
 
606
681
  /**
@@ -612,35 +687,26 @@ Casper.prototype.fetchText = function fetchText(selector) {
612
687
  */
613
688
  Casper.prototype.fill = function fill(selector, vals, submit) {
614
689
  "use strict";
690
+ this.checkStarted();
615
691
  submit = submit === true ? submit : false;
616
692
  if (!utils.isObject(vals)) {
617
693
  throw new CasperError("Form values must be provided as an object");
618
694
  }
619
695
  this.emit('fill', selector, vals, submit);
620
696
  var fillResults = this.evaluate(function _evaluate(selector, values) {
621
- return window.__utils__.fill(selector, values);
622
- }, {
623
- selector: selector,
624
- values: vals
625
- });
697
+ return __utils__.fill(selector, values);
698
+ }, selector, vals);
626
699
  if (!fillResults) {
627
700
  throw new CasperError("Unable to fill form");
628
701
  } else if (fillResults.errors.length > 0) {
629
- (function _each(self){
630
- fillResults.errors.forEach(function _forEach(error) {
631
- self.log("form error: " + error, "error");
632
- });
633
- })(this);
634
- if (submit) {
635
- this.log("Errors encountered while filling form; submission aborted", "warning");
636
- submit = false;
637
- }
702
+ throw new CasperError(f('Errors encountered while filling form: %s',
703
+ fillResults.errors.join('; ')));
638
704
  }
639
705
  // File uploads
640
706
  if (fillResults.files && fillResults.files.length > 0) {
641
707
  if (utils.isObject(selector) && selector.type === 'xpath') {
642
- this.echo('Filling file upload fields is currently not supported using', 'COMMENT');
643
- this.echo(' XPath selectors; Please use a CSS selector instead.', 'COMMENT');
708
+ this.warn('Filling file upload fields is currently not supported using ' +
709
+ 'XPath selectors; Please use a CSS selector instead.');
644
710
  } else {
645
711
  (function _each(self) {
646
712
  fillResults.files.forEach(function _forEach(file) {
@@ -653,17 +719,17 @@ Casper.prototype.fill = function fill(selector, vals, submit) {
653
719
  // Form submission?
654
720
  if (submit) {
655
721
  this.evaluate(function _evaluate(selector) {
656
- var form = window.__utils__.findOne(selector);
722
+ var form = __utils__.findOne(selector);
657
723
  var method = (form.getAttribute('method') || "GET").toUpperCase();
658
724
  var action = form.getAttribute('action') || "unknown";
659
- window.__utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
725
+ __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
660
726
  if (typeof form.submit === "function") {
661
727
  form.submit();
662
728
  } else {
663
729
  // http://www.spiration.co.uk/post/1232/Submit-is-not-a-function
664
730
  form.submit.click();
665
731
  }
666
- }, { selector: selector });
732
+ }, selector);
667
733
  }
668
734
  };
669
735
 
@@ -674,6 +740,7 @@ Casper.prototype.fill = function fill(selector, vals, submit) {
674
740
  */
675
741
  Casper.prototype.forward = function forward(then) {
676
742
  "use strict";
743
+ this.checkStarted();
677
744
  return this.then(function _step() {
678
745
  this.emit('forward');
679
746
  this.evaluate(function _evaluate() {
@@ -693,6 +760,29 @@ Casper.prototype.getColorizer = function getColorizer() {
693
760
  return colorizer.create(this.options.colorizerType || 'Colorizer');
694
761
  };
695
762
 
763
+ /**
764
+ * Retrieves current page contents, dealing with exotic other content types than HTML.
765
+ *
766
+ * @return String
767
+ */
768
+ Casper.prototype.getPageContent = function getPageContent() {
769
+ "use strict";
770
+ this.checkStarted();
771
+ var contentType = utils.getPropertyPath(this, 'currentResponse.contentType');
772
+ if (!utils.isString(contentType)) {
773
+ return this.page.frameContent;
774
+ }
775
+ // for some reason webkit/qtwebkit will always enclose body contents within html tags
776
+ var sanitizedHtml = this.evaluate(function checkHtml() {
777
+ if (__utils__.findOne('head').childNodes.length === 0 &&
778
+ __utils__.findOne('body').childNodes.length === 1 &&
779
+ __utils__.findOne('body pre[style]')) {
780
+ return __utils__.findOne('body pre').textContent.trim();
781
+ }
782
+ });
783
+ return sanitizedHtml ? sanitizedHtml : this.page.frameContent;
784
+ };
785
+
696
786
  /**
697
787
  * Retrieves current document url.
698
788
  *
@@ -700,9 +790,16 @@ Casper.prototype.getColorizer = function getColorizer() {
700
790
  */
701
791
  Casper.prototype.getCurrentUrl = function getCurrentUrl() {
702
792
  "use strict";
703
- return decodeURIComponent(this.evaluate(function _evaluate() {
793
+ this.checkStarted();
794
+ var url = this.evaluate(function _evaluate() {
704
795
  return document.location.href;
705
- }));
796
+ });
797
+ try {
798
+ return decodeURIComponent(url);
799
+ } catch (e) {
800
+ /*global unescape*/
801
+ return unescape(url);
802
+ }
706
803
  };
707
804
 
708
805
  /**
@@ -713,11 +810,13 @@ Casper.prototype.getCurrentUrl = function getCurrentUrl() {
713
810
  * @param String attribute The attribute name to lookup
714
811
  * @return String The requested DOM element attribute value
715
812
  */
716
- Casper.prototype.getElementAttribute = Casper.prototype.getElementAttr = function getElementAttr(selector, attribute) {
813
+ Casper.prototype.getElementAttribute =
814
+ Casper.prototype.getElementAttr = function getElementAttr(selector, attribute) {
717
815
  "use strict";
816
+ this.checkStarted();
718
817
  return this.evaluate(function _evaluate(selector, attribute) {
719
818
  return document.querySelector(selector).getAttribute(attribute);
720
- }, { selector: selector, attribute: attribute });
819
+ }, selector, attribute);
721
820
  };
722
821
 
723
822
  /**
@@ -728,18 +827,70 @@ Casper.prototype.getElementAttribute = Casper.prototype.getElementAttr = functio
728
827
  */
729
828
  Casper.prototype.getElementBounds = function getElementBounds(selector) {
730
829
  "use strict";
830
+ this.checkStarted();
731
831
  if (!this.exists(selector)) {
732
832
  throw new CasperError("No element matching selector found: " + selector);
733
833
  }
734
834
  var clipRect = this.evaluate(function _evaluate(selector) {
735
- return window.__utils__.getElementBounds(selector);
736
- }, { selector: selector });
835
+ return __utils__.getElementBounds(selector);
836
+ }, selector);
737
837
  if (!utils.isClipRect(clipRect)) {
738
838
  throw new CasperError('Could not fetch boundaries for element matching selector: ' + selector);
739
839
  }
740
840
  return clipRect;
741
841
  };
742
842
 
843
+ /**
844
+ * Retrieves information about the node matching the provided selector.
845
+ *
846
+ * @param String|Objects selector CSS3/XPath selector
847
+ * @return Object
848
+ */
849
+ Casper.prototype.getElementInfo = function getElementInfo(selector) {
850
+ "use strict";
851
+ this.checkStarted();
852
+ if (!this.exists(selector)) {
853
+ throw new CasperError(f("Cannot get informations from %s: element not found.", selector));
854
+ }
855
+ return this.evaluate(function(selector) {
856
+ return __utils__.getElementInfo(selector);
857
+ }, selector);
858
+ };
859
+
860
+ /**
861
+ * Retrieves boundaries for all the DOM elements matching the provided DOM CSS3/XPath selector.
862
+ *
863
+ * @param String selector A DOM CSS3/XPath selector
864
+ * @return Array
865
+ */
866
+ Casper.prototype.getElementsBounds = function getElementBounds(selector) {
867
+ "use strict";
868
+ this.checkStarted();
869
+ if (!this.exists(selector)) {
870
+ throw new CasperError("No element matching selector found: " + selector);
871
+ }
872
+ return this.evaluate(function _evaluate(selector) {
873
+ return __utils__.getElementsBounds(selector);
874
+ }, selector);
875
+ };
876
+
877
+ /**
878
+ * Retrieves a given form all of its field values.
879
+ *
880
+ * @param String selector A DOM CSS3/XPath selector
881
+ * @return Object
882
+ */
883
+ Casper.prototype.getFormValues = function(selector) {
884
+ "use strict";
885
+ this.checkStarted();
886
+ if (!this.exists(selector)) {
887
+ throw new CasperError(f('Form matching selector "%s" not found', selector));
888
+ }
889
+ return this.evaluate(function(selector) {
890
+ return __utils__.getFormValues(selector);
891
+ }, selector);
892
+ };
893
+
743
894
  /**
744
895
  * Retrieves global variable.
745
896
  *
@@ -748,28 +899,50 @@ Casper.prototype.getElementBounds = function getElementBounds(selector) {
748
899
  */
749
900
  Casper.prototype.getGlobal = function getGlobal(name) {
750
901
  "use strict";
902
+ this.checkStarted();
751
903
  var result = this.evaluate(function _evaluate(name) {
752
904
  var result = {};
753
905
  try {
754
906
  result.value = JSON.stringify(window[name]);
755
907
  } catch (e) {
756
908
  var message = f("Unable to JSON encode window.%s: %s", name, e);
757
- window.__utils__.log(message, "error");
909
+ __utils__.log(message, "error");
758
910
  result.error = message;
759
911
  }
760
912
  return result;
761
- }, {'name': name});
762
- if (typeof result !== "object") {
913
+ }, name);
914
+ if (!utils.isObject(result)) {
763
915
  throw new CasperError(f('Could not retrieve global value for "%s"', name));
764
916
  } else if ('error' in result) {
765
917
  throw new CasperError(result.error);
766
918
  } else if (utils.isString(result.value)) {
767
919
  return JSON.parse(result.value);
768
- } else {
769
- return undefined;
770
920
  }
771
921
  };
772
922
 
923
+ /**
924
+ * Retrieves current HTML code matching the provided CSS3/XPath selector.
925
+ * Returns the HTML contents for the whole page if no arg is passed.
926
+ *
927
+ * @param String selector A DOM CSS3/XPath selector
928
+ * @param Boolean outer Whether to fetch outer HTML contents (default: false)
929
+ * @return String
930
+ */
931
+ Casper.prototype.getHTML = function getHTML(selector, outer) {
932
+ "use strict";
933
+ this.checkStarted();
934
+ if (!selector) {
935
+ return this.page.frameContent;
936
+ }
937
+ if (!this.exists(selector)) {
938
+ throw new CasperError("No element matching selector found: " + selector);
939
+ }
940
+ return this.evaluate(function getSelectorHTML(selector, outer) {
941
+ var element = __utils__.findOne(selector);
942
+ return outer ? element.outerHTML : element.innerHTML;
943
+ }, selector, !!outer);
944
+ };
945
+
773
946
  /**
774
947
  * Retrieves current page title, if any.
775
948
  *
@@ -777,11 +950,44 @@ Casper.prototype.getGlobal = function getGlobal(name) {
777
950
  */
778
951
  Casper.prototype.getTitle = function getTitle() {
779
952
  "use strict";
953
+ this.checkStarted();
780
954
  return this.evaluate(function _evaluate() {
781
955
  return document.title;
782
956
  });
783
957
  };
784
958
 
959
+ /**
960
+ * Handles received HTTP resource.
961
+ *
962
+ * @param Object resource PhantomJS HTTP resource
963
+ */
964
+ Casper.prototype.handleReceivedResource = function(resource) {
965
+ "use strict";
966
+ /*jshint maxstatements:20*/
967
+ if (resource.stage !== "end") {
968
+ return;
969
+ }
970
+ this.resources.push(resource);
971
+ if (resource.url !== this.requestUrl) {
972
+ return;
973
+ }
974
+ this.currentHTTPStatus = null;
975
+ this.currentResponse = undefined;
976
+ if (utils.isHTTPResource(resource)) {
977
+ this.emit('page.resource.received', resource);
978
+ this.currentResponse = resource;
979
+ this.currentHTTPStatus = resource.status;
980
+ this.emit('http.status.' + resource.status, resource);
981
+ if (utils.isObject(this.options.httpStatusHandlers) &&
982
+ resource.status in this.options.httpStatusHandlers &&
983
+ utils.isFunction(this.options.httpStatusHandlers[resource.status])) {
984
+ this.options.httpStatusHandlers[resource.status].call(this, this, resource);
985
+ }
986
+ }
987
+ this.currentUrl = resource.url;
988
+ this.emit('location.changed', resource.url);
989
+ };
990
+
785
991
  /**
786
992
  * Initializes PhantomJS error handler.
787
993
  *
@@ -797,14 +1003,42 @@ Casper.prototype.initErrorHandler = function initErrorHandler() {
797
1003
  };
798
1004
  };
799
1005
 
1006
+ /**
1007
+ * Injects configured local client scripts.
1008
+ *
1009
+ * @return Casper
1010
+ */
1011
+ Casper.prototype.injectClientScripts = function injectClientScripts() {
1012
+ "use strict";
1013
+ this.checkStarted();
1014
+ if (!this.options.clientScripts) {
1015
+ return;
1016
+ }
1017
+ if (utils.isString(this.options.clientScripts)) {
1018
+ this.options.clientScripts = [this.options.clientScripts];
1019
+ }
1020
+ if (!utils.isArray(this.options.clientScripts)) {
1021
+ throw new CasperError("The clientScripts option must be an array");
1022
+ }
1023
+ this.options.clientScripts.forEach(function _forEach(script) {
1024
+ if (this.page.injectJs(script)) {
1025
+ this.log(f('Automatically injected %s client side', script), "debug");
1026
+ } else {
1027
+ this.warn('Failed injecting %s client side', script);
1028
+ }
1029
+ }.bind(this));
1030
+ return this;
1031
+ };
1032
+
800
1033
  /**
801
1034
  * Injects Client-side utilities in current page context.
802
1035
  *
803
1036
  */
804
1037
  Casper.prototype.injectClientUtils = function injectClientUtils() {
805
1038
  "use strict";
1039
+ this.checkStarted();
806
1040
  var clientUtilsInjected = this.page.evaluate(function() {
807
- return typeof window.__utils__ === "object";
1041
+ return typeof __utils__ === "object";
808
1042
  });
809
1043
  if (true === clientUtilsInjected) {
810
1044
  return;
@@ -817,17 +1051,44 @@ Casper.prototype.injectClientUtils = function injectClientUtils() {
817
1051
  }
818
1052
  // ClientUtils and Casper shares the same options
819
1053
  // These are not the lines I'm the most proud of in my life, but it works.
1054
+ /*global __options*/
820
1055
  this.page.evaluate(function() {
821
- window.__utils__ = new ClientUtils(__options);
1056
+ window.__utils__ = new window.ClientUtils(__options);
822
1057
  }.toString().replace('__options', JSON.stringify(this.options)));
823
1058
  };
824
1059
 
1060
+ /**
1061
+ * Loads and include remote client scripts to current page.
1062
+ *
1063
+ * @return Casper
1064
+ */
1065
+ Casper.prototype.includeRemoteScripts = function includeRemoteScripts() {
1066
+ "use strict";
1067
+ var numScripts = this.options.remoteScripts.length, loaded = 0;
1068
+ if (numScripts === 0) {
1069
+ return this;
1070
+ }
1071
+ this.waitStart();
1072
+ this.options.remoteScripts.forEach(function(scriptUrl) {
1073
+ this.log(f("Loading remote script: %s", scriptUrl), "debug");
1074
+ this.page.includeJs(scriptUrl, function() {
1075
+ loaded++;
1076
+ this.log(f("Remote script %s loaded", scriptUrl), "debug");
1077
+ if (loaded === numScripts) {
1078
+ this.log("All remote scripts loaded.", "debug");
1079
+ this.waitDone();
1080
+ }
1081
+ }.bind(this));
1082
+ }.bind(this));
1083
+ return this;
1084
+ };
1085
+
825
1086
  /**
826
1087
  * Logs a message.
827
1088
  *
828
1089
  * @param String message The message to log
829
1090
  * @param String level The log message level (from Casper.logLevels property)
830
- * @param String space Space from where the logged event occured (default: "phantom")
1091
+ * @param String space Space from where the logged event occurred (default: "phantom")
831
1092
  * @return Casper
832
1093
  */
833
1094
  Casper.prototype.log = function log(message, level, space) {
@@ -849,8 +1110,10 @@ Casper.prototype.log = function log(message, level, space) {
849
1110
  if (level in this.logFormats && utils.isFunction(this.logFormats[level])) {
850
1111
  message = this.logFormats[level](message, level, space);
851
1112
  } else {
852
- var levelStr = this.colorizer.colorize(f('[%s]', level), this.logStyles[level]);
853
- message = f('%s [%s] %s', levelStr, space, message);
1113
+ message = f('%s [%s] %s',
1114
+ this.colorizer.colorize(f('[%s]', level), this.logStyles[level]),
1115
+ space,
1116
+ message);
854
1117
  }
855
1118
  if (this.options.verbose) {
856
1119
  this.echo(this.filter('log.message', message) || message); // direct output
@@ -872,26 +1135,23 @@ Casper.prototype.log = function log(message, level, space) {
872
1135
  */
873
1136
  Casper.prototype.mouseEvent = function mouseEvent(type, selector) {
874
1137
  "use strict";
1138
+ this.checkStarted();
875
1139
  this.log("Mouse event '" + type + "' on selector: " + selector, "debug");
876
1140
  if (!this.exists(selector)) {
877
1141
  throw new CasperError(f("Cannot dispatch %s event on nonexistent selector: %s", type, selector));
878
1142
  }
879
- var eventSuccess = this.evaluate(function(type, selector) {
1143
+ if (this.evaluate(function(type, selector) {
880
1144
  return window.__utils__.mouseEvent(type, selector);
881
- }, {
882
- type: type,
883
- selector: selector
884
- });
885
- if (!eventSuccess) {
886
- // fallback onto native QtWebKit mouse events
887
- try {
888
- this.mouse.processEvent(type, selector);
889
- } catch (e) {
890
- this.log(f("Couldn't emulate '%s' event on %s: %s", type, selector, e), "error");
891
- return false;
892
- }
1145
+ }, type, selector)) {
1146
+ return true;
893
1147
  }
894
- return true;
1148
+ // fallback onto native QtWebKit mouse events
1149
+ try {
1150
+ return this.mouse.processEvent(type, selector);
1151
+ } catch (e) {
1152
+ this.log(f("Couldn't emulate '%s' event on %s: %s", type, selector, e), "error");
1153
+ }
1154
+ return false;
895
1155
  };
896
1156
 
897
1157
  /**
@@ -901,7 +1161,7 @@ Casper.prototype.mouseEvent = function mouseEvent(type, selector) {
901
1161
  *
902
1162
  * - String method: The HTTP method to use
903
1163
  * - Object data: The data to use to perform the request, eg. {foo: 'bar'}
904
- * - Array headers: An array of request headers, eg. [{'Cache-Control': 'max-age=0'}]
1164
+ * - Object headers: Custom request headers object, eg. {'Cache-Control': 'max-age=0'}
905
1165
  *
906
1166
  * @param String location The url to open
907
1167
  * @param Object settings The request settings (optional)
@@ -909,15 +1169,12 @@ Casper.prototype.mouseEvent = function mouseEvent(type, selector) {
909
1169
  */
910
1170
  Casper.prototype.open = function open(location, settings) {
911
1171
  "use strict";
912
- // settings validation
913
- if (!settings) {
914
- settings = {
915
- method: "get"
916
- };
917
- }
918
- if (!utils.isObject(settings)) {
919
- throw new CasperError("open(): request settings must be an Object");
920
- }
1172
+ /*jshint maxstatements:30*/
1173
+ var baseCustomHeaders = this.page.customHeaders,
1174
+ customHeaders = settings && settings.headers || {};
1175
+ this.checkStarted();
1176
+ settings = utils.isObject(settings) ? settings : {};
1177
+ settings.method = settings.method || "get";
921
1178
  // http method
922
1179
  // taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302
923
1180
  var methods = ["get", "head", "put", "post", "delete"];
@@ -932,32 +1189,24 @@ Casper.prototype.open = function open(location, settings) {
932
1189
  throw new CasperError("open(): invalid request settings data value: " + settings.data);
933
1190
  }
934
1191
  }
1192
+ // clean location
1193
+ location = utils.cleanUrl(location);
935
1194
  // current request url
1195
+ this.configureHttpAuth(location, settings);
936
1196
  this.requestUrl = this.filter('open.location', location) || location;
937
- // http auth
938
- if (settings.username && settings.password) {
939
- this.setHttpAuth(settings.username, settings.password);
940
- } else {
941
- var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
942
- if (httpAuthMatch) {
943
- var httpAuth = {
944
- username: httpAuthMatch[1],
945
- password: httpAuthMatch[2]
946
- };
947
- this.setHttpAuth(httpAuth.username, httpAuth.password);
948
- }
949
- }
950
1197
  this.emit('open', this.requestUrl, settings);
951
1198
  this.log(f('opening url: %s, HTTP %s', this.requestUrl, settings.method.toUpperCase()), "debug");
952
- if ('headers' in settings && phantom.version.minor < 6) {
953
- this.warn('Custom headers in outgoing requests are supported in PhantomJS >= 1.6');
954
- }
1199
+ // reset resources
1200
+ this.resources = [];
1201
+ // custom headers
1202
+ this.page.customHeaders = utils.mergeObjects(utils.clone(baseCustomHeaders), customHeaders);
1203
+ // perfom request
955
1204
  this.page.openUrl(this.requestUrl, {
956
1205
  operation: settings.method,
957
- data: settings.data,
958
- headers: settings.headers
1206
+ data: settings.data
959
1207
  }, this.page.settings);
960
- this.resources = [];
1208
+ // revert base custom headers
1209
+ this.page.customHeaders = baseCustomHeaders;
961
1210
  return this;
962
1211
  };
963
1212
 
@@ -969,16 +1218,14 @@ Casper.prototype.open = function open(location, settings) {
969
1218
  */
970
1219
  Casper.prototype.reload = function reload(then) {
971
1220
  "use strict";
972
- if (!this.started) {
973
- throw new CasperError("Casper not started, can't reload()");
974
- }
975
- this.evaluate(function() {
976
- window.location.reload();
1221
+ this.checkStarted();
1222
+ // window.location.reload() is broken under phantomjs
1223
+ this.then(function() {
1224
+ this.open(this.getCurrentUrl());
977
1225
  });
978
- if (then && utils.isFunction(then)) {
1226
+ if (utils.isFunction(then)) {
979
1227
  this.then(this.createStep(then));
980
1228
  }
981
- return this;
982
1229
  };
983
1230
 
984
1231
  /**
@@ -1006,16 +1253,17 @@ Casper.prototype.repeat = function repeat(times, then) {
1006
1253
  */
1007
1254
  Casper.prototype.resourceExists = function resourceExists(test) {
1008
1255
  "use strict";
1256
+ this.checkStarted();
1009
1257
  var testFn;
1010
1258
  switch (utils.betterTypeOf(test)) {
1011
1259
  case "string":
1012
1260
  testFn = function _testResourceExists_String(res) {
1013
- return res.url.search(test) !== -1;
1261
+ return res.url.search(test) !== -1 && res.status !== 404;
1014
1262
  };
1015
1263
  break;
1016
1264
  case "regexp":
1017
1265
  testFn = function _testResourceExists_Regexp(res) {
1018
- return test.test(res.url);
1266
+ return test.test(res.url) && res.status !== 404;
1019
1267
  };
1020
1268
  break;
1021
1269
  case "function":
@@ -1037,9 +1285,9 @@ Casper.prototype.resourceExists = function resourceExists(test) {
1037
1285
  */
1038
1286
  Casper.prototype.run = function run(onComplete, time) {
1039
1287
  "use strict";
1288
+ this.checkStarted();
1040
1289
  if (!this.steps || this.steps.length < 1) {
1041
- this.log("No steps defined, aborting", "error");
1042
- return this;
1290
+ throw new CasperError('No steps defined, aborting');
1043
1291
  }
1044
1292
  this.log(f("Running suite: %d step%s", this.steps.length, this.steps.length > 1 ? "s" : ""), "info");
1045
1293
  this.emit('run.start');
@@ -1054,29 +1302,28 @@ Casper.prototype.run = function run(onComplete, time) {
1054
1302
  */
1055
1303
  Casper.prototype.runStep = function runStep(step) {
1056
1304
  "use strict";
1305
+ this.checkStarted();
1057
1306
  var skipLog = utils.isObject(step.options) && step.options.skipLog === true;
1058
1307
  var stepInfo = f("Step %d/%d", this.step, this.steps.length);
1059
1308
  var stepResult;
1060
- if (!skipLog) {
1309
+ if (!skipLog && /^http/.test(this.getCurrentUrl())) {
1061
1310
  this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info");
1062
1311
  }
1063
1312
  if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
1064
1313
  var stepTimeoutCheckInterval = setInterval(function _check(self, start, stepNum) {
1065
1314
  if (new Date().getTime() - start > self.options.stepTimeout) {
1066
- if (self.step === stepNum) {
1315
+ if ((self.test.currentSuiteNum + "-" + self.step) === stepNum) {
1067
1316
  self.emit('step.timeout');
1068
1317
  if (utils.isFunction(self.options.onStepTimeout)) {
1069
- self.options.onStepTimeout.call(self, self);
1070
- } else {
1071
- self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
1318
+ self.options.onStepTimeout.call(self, self.options.stepTimeout, stepNum);
1072
1319
  }
1073
1320
  }
1074
1321
  clearInterval(stepTimeoutCheckInterval);
1075
1322
  }
1076
- }, this.options.stepTimeout, this, new Date().getTime(), this.step);
1323
+ }, this.options.stepTimeout, this, new Date().getTime(), this.test.currentSuiteNum + "-" + this.step);
1077
1324
  }
1078
1325
  this.emit('step.start', step);
1079
- stepResult = step.call(this, this);
1326
+ stepResult = step.call(this, this.currentResponse);
1080
1327
  if (utils.isFunction(this.options.onStepComplete)) {
1081
1328
  this.options.onStepComplete.call(this, this, stepResult);
1082
1329
  }
@@ -1087,24 +1334,53 @@ Casper.prototype.runStep = function runStep(step) {
1087
1334
  };
1088
1335
 
1089
1336
  /**
1090
- * Sets HTTP authentication parameters.
1337
+ * Sends keys to given element.
1091
1338
  *
1092
- * @param String username The HTTP_AUTH_USER value
1093
- * @param String password The HTTP_AUTH_PW value
1339
+ * @param String selector A DOM CSS3 compatible selector
1340
+ * @param String keys A string representing the sequence of char codes to send
1341
+ * @param Object options Options
1094
1342
  * @return Casper
1095
1343
  */
1096
- Casper.prototype.setHttpAuth = function setHttpAuth(username, password) {
1097
- "use strict";
1098
- if (!this.started) {
1099
- throw new CasperError("Casper must be started in order to use the setHttpAuth() method");
1344
+ Casper.prototype.sendKeys = function(selector, keys, options) {
1345
+ "use strict";
1346
+ this.checkStarted();
1347
+ options = utils.mergeObjects({
1348
+ eventType: 'keypress'
1349
+ }, options || {});
1350
+ var elemInfos = this.getElementInfo(selector),
1351
+ tag = elemInfos.nodeName.toLowerCase(),
1352
+ type = utils.getPropertyPath(elemInfos, 'attributes.type'),
1353
+ supported = ["color", "date", "datetime", "datetime-local", "email",
1354
+ "hidden", "month", "number", "password", "range", "search",
1355
+ "tel", "text", "time", "url", "week"];
1356
+ var isTextInput = false;
1357
+ if (tag === 'textarea' || (tag === 'input' && supported.indexOf(type) !== -1)) {
1358
+ // clicking on the input element brings it focus
1359
+ isTextInput = true;
1360
+ this.click(selector);
1100
1361
  }
1101
- if (!utils.isString(username) || !utils.isString(password)) {
1102
- throw new CasperError("Both username and password must be strings");
1362
+ this.page.sendEvent(options.eventType, keys);
1363
+ if (isTextInput) {
1364
+ // remove the focus
1365
+ this.evaluate(function(selector) {
1366
+ __utils__.findOne(selector).blur();
1367
+ }, selector);
1103
1368
  }
1369
+ return this;
1370
+ };
1371
+
1372
+ /**
1373
+ * Sets current WebPage instance the credentials for HTTP authentication.
1374
+ *
1375
+ * @param String username
1376
+ * @param String password
1377
+ * @return Casper
1378
+ */
1379
+ Casper.prototype.setHttpAuth = function setHttpAuth(username, password) {
1380
+ "use strict";
1381
+ this.checkStarted();
1104
1382
  this.page.settings.userName = username;
1105
1383
  this.page.settings.password = password;
1106
- this.emit('http.auth', username, password);
1107
- this.log("Setting HTTP authentication for user " + username, "info");
1108
1384
  return this;
1109
1385
  };
1110
1386
 
@@ -1117,10 +1393,12 @@ Casper.prototype.setHttpAuth = function setHttpAuth(username, password) {
1117
1393
  */
1118
1394
  Casper.prototype.start = function start(location, then) {
1119
1395
  "use strict";
1396
+ /*jshint maxstatements:30*/
1120
1397
  this.emit('starting');
1121
1398
  this.log('Starting...', "info");
1122
1399
  this.startTime = new Date().getTime();
1123
1400
  this.history = [];
1401
+ this.popups = pagestack.create();
1124
1402
  this.steps = [];
1125
1403
  this.step = 0;
1126
1404
  // Option checks
@@ -1128,13 +1406,8 @@ Casper.prototype.start = function start(location, then) {
1128
1406
  this.log(f("Unknown log level '%d', defaulting to 'warning'", this.options.logLevel), "warning");
1129
1407
  this.options.logLevel = "warning";
1130
1408
  }
1131
- // WebPage
1132
1409
  if (!utils.isWebPage(this.page)) {
1133
- if (utils.isWebPage(this.options.page)) {
1134
- this.page = this.options.page;
1135
- } else {
1136
- this.page = createPage(this);
1137
- }
1410
+ this.page = this.mainPage = utils.isWebPage(this.options.page) ? this.options.page : createPage(this);
1138
1411
  }
1139
1412
  this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings);
1140
1413
  if (utils.isClipRect(this.options.clipRect)) {
@@ -1143,27 +1416,43 @@ Casper.prototype.start = function start(location, then) {
1143
1416
  if (utils.isObject(this.options.viewportSize)) {
1144
1417
  this.page.viewportSize = this.options.viewportSize;
1145
1418
  }
1146
- this.started = true;
1147
- this.emit('started');
1419
+ // timeout handling
1148
1420
  if (utils.isNumber(this.options.timeout) && this.options.timeout > 0) {
1149
1421
  this.log(f("Execution timeout set to %dms", this.options.timeout), "info");
1150
1422
  setTimeout(function _check(self) {
1151
1423
  self.emit('timeout');
1152
1424
  if (utils.isFunction(self.options.onTimeout)) {
1153
- self.options.onTimeout.call(self, self);
1154
- } else {
1155
- self.die(f("Timeout of %dms exceeded, exiting.", self.options.timeout));
1425
+ self.options.onTimeout.call(self, self.options.timeout);
1156
1426
  }
1157
1427
  }, this.options.timeout, this);
1158
1428
  }
1429
+ this.started = true;
1430
+ this.emit('started');
1159
1431
  if (utils.isString(location) && location.length > 0) {
1160
1432
  return this.thenOpen(location, utils.isFunction(then) ? then : this.createStep(function _step() {
1161
1433
  this.log("start page is loaded", "debug");
1162
- }));
1434
+ }, {skipLog: true}));
1163
1435
  }
1164
1436
  return this;
1165
1437
  };
1166
1438
 
1439
+ /**
1440
+ * Returns the current status of current instance
1441
+ *
1442
+ * @param Boolean asString Export status object as string
1443
+ * @return Object
1444
+ */
1445
+ Casper.prototype.status = function status(asString) {
1446
+ "use strict";
1447
+ var properties = ['currentHTTPStatus', 'loadInProgress', 'navigationRequested',
1448
+ 'options', 'pendingWait', 'requestUrl', 'started', 'step', 'url'];
1449
+ var currentStatus = {};
1450
+ properties.forEach(function(property) {
1451
+ currentStatus[property] = this[property];
1452
+ }.bind(this));
1453
+ return asString === true ? utils.dump(currentStatus) : currentStatus;
1454
+ };
1455
+
1167
1456
  /**
1168
1457
  * Schedules the next step in the navigation process.
1169
1458
  *
@@ -1172,9 +1461,7 @@ Casper.prototype.start = function start(location, then) {
1172
1461
  */
1173
1462
  Casper.prototype.then = function then(step) {
1174
1463
  "use strict";
1175
- if (!this.started) {
1176
- throw new CasperError("Casper not started; please use Casper#start");
1177
- }
1464
+ this.checkStarted();
1178
1465
  if (!utils.isFunction(step)) {
1179
1466
  throw new CasperError("You can only define a step as a function");
1180
1467
  }
@@ -1212,6 +1499,7 @@ Casper.prototype.then = function then(step) {
1212
1499
  */
1213
1500
  Casper.prototype.thenClick = function thenClick(selector, then, fallbackToHref) {
1214
1501
  "use strict";
1502
+ this.checkStarted();
1215
1503
  if (arguments.length > 2) {
1216
1504
  this.emit("deprecated", "The thenClick() method does not process the fallbackToHref argument since 0.6");
1217
1505
  }
@@ -1232,8 +1520,10 @@ Casper.prototype.thenClick = function thenClick(selector, then, fallbackToHref)
1232
1520
  */
1233
1521
  Casper.prototype.thenEvaluate = function thenEvaluate(fn, context) {
1234
1522
  "use strict";
1523
+ this.checkStarted();
1524
+ var args = [fn].concat([].slice.call(arguments, 1));
1235
1525
  return this.then(function _step() {
1236
- this.evaluate(fn, context);
1526
+ this.evaluate.apply(this, args);
1237
1527
  });
1238
1528
  };
1239
1529
 
@@ -1245,10 +1535,15 @@ Casper.prototype.thenEvaluate = function thenEvaluate(fn, context) {
1245
1535
  * @return Casper
1246
1536
  * @see Casper#open
1247
1537
  */
1248
- Casper.prototype.thenOpen = function thenOpen(location, then) {
1538
+ Casper.prototype.thenOpen = function thenOpen(location, settings, then) {
1249
1539
  "use strict";
1540
+ this.checkStarted();
1541
+ if (!(settings && !utils.isFunction(settings))) {
1542
+ then = settings;
1543
+ settings = null;
1544
+ }
1250
1545
  this.then(this.createStep(function _step() {
1251
- this.open(location);
1546
+ this.open(location, settings);
1252
1547
  }, {
1253
1548
  skipLog: true
1254
1549
  }));
@@ -1268,9 +1563,20 @@ Casper.prototype.thenOpen = function thenOpen(location, then) {
1268
1563
  */
1269
1564
  Casper.prototype.thenOpenAndEvaluate = function thenOpenAndEvaluate(location, fn, context) {
1270
1565
  "use strict";
1566
+ this.checkStarted();
1271
1567
  return this.thenOpen(location).thenEvaluate(fn, context);
1272
1568
  };
1273
1569
 
1570
+ /**
1571
+ * Returns a string representation of current instance
1572
+ *
1573
+ * @return String
1574
+ */
1575
+ Casper.prototype.toString = function toString() {
1576
+ "use strict";
1577
+ return '[object Casper], currently at ' + this.getCurrentUrl();
1578
+ };
1579
+
1274
1580
  /**
1275
1581
  * Sets the user-agent string currently used when requesting urls.
1276
1582
  *
@@ -1279,10 +1585,10 @@ Casper.prototype.thenOpenAndEvaluate = function thenOpenAndEvaluate(location, fn
1279
1585
  */
1280
1586
  Casper.prototype.userAgent = function userAgent(agent) {
1281
1587
  "use strict";
1282
- if (!this.started) {
1283
- throw new CasperError("Casper not started, can't set userAgent");
1588
+ this.options.pageSettings.userAgent = agent;
1589
+ if (this.started && this.page) {
1590
+ this.page.settings.userAgent = agent;
1284
1591
  }
1285
- this.options.pageSettings.userAgent = this.page.settings.userAgent = agent;
1286
1592
  return this;
1287
1593
  };
1288
1594
 
@@ -1295,9 +1601,7 @@ Casper.prototype.userAgent = function userAgent(agent) {
1295
1601
  */
1296
1602
  Casper.prototype.viewport = function viewport(width, height) {
1297
1603
  "use strict";
1298
- if (!this.started) {
1299
- throw new CasperError("Casper must be started in order to set viewport at runtime");
1300
- }
1604
+ this.checkStarted();
1301
1605
  if (!utils.isNumber(width) || !utils.isNumber(height) || width <= 0 || height <= 0) {
1302
1606
  throw new CasperError(f("Invalid viewport: %dx%d", width, height));
1303
1607
  }
@@ -1319,9 +1623,10 @@ Casper.prototype.viewport = function viewport(width, height) {
1319
1623
  */
1320
1624
  Casper.prototype.visible = function visible(selector) {
1321
1625
  "use strict";
1626
+ this.checkStarted();
1322
1627
  return this.evaluate(function _evaluate(selector) {
1323
- return window.__utils__.visible(selector);
1324
- }, { selector: selector });
1628
+ return __utils__.visible(selector);
1629
+ }, selector);
1325
1630
  };
1326
1631
 
1327
1632
  /**
@@ -1347,6 +1652,7 @@ Casper.prototype.warn = function warn(message) {
1347
1652
  */
1348
1653
  Casper.prototype.wait = function wait(timeout, then) {
1349
1654
  "use strict";
1655
+ this.checkStarted();
1350
1656
  timeout = ~~timeout;
1351
1657
  if (timeout < 1) {
1352
1658
  this.die("wait() only accepts a positive integer > 0 as a timeout value");
@@ -1389,7 +1695,8 @@ Casper.prototype.waitDone = function waitDone() {
1389
1695
  */
1390
1696
  Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1391
1697
  "use strict";
1392
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1698
+ this.checkStarted();
1699
+ timeout = timeout ? timeout : this.options.waitTimeout;
1393
1700
  if (!utils.isFunction(testFx)) {
1394
1701
  this.die("waitFor() needs a test function");
1395
1702
  }
@@ -1408,11 +1715,11 @@ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1408
1715
  if (!condition) {
1409
1716
  self.log("Casper.waitFor() timeout", "warning");
1410
1717
  self.emit('waitFor.timeout');
1411
- if (utils.isFunction(onTimeout)) {
1412
- onTimeout.call(self, self);
1413
- } else {
1414
- self.die(f("Timeout of %dms expired, exiting.", timeout), "error");
1718
+ var onWaitTimeout = onTimeout ? onTimeout : self.options.onWaitTimeout;
1719
+ if (!utils.isFunction(onWaitTimeout)) {
1720
+ throw new CasperError('Invalid timeout function, exiting.');
1415
1721
  }
1722
+ onWaitTimeout.call(self, timeout);
1416
1723
  } else {
1417
1724
  self.log(f("waitFor() finished in %dms.", new Date().getTime() - start), "info");
1418
1725
  if (then) {
@@ -1425,6 +1732,28 @@ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1425
1732
  });
1426
1733
  };
1427
1734
 
1735
+ /**
1736
+ * Waits for a popup page having its url matching the provided pattern to be opened
1737
+ * and loaded.
1738
+ *
1739
+ * @param String|RegExp urlPattern The popup url pattern
1740
+ * @param Function then The next step function (optional)
1741
+ * @param Function onTimeout Function to call on operation timeout (optional)
1742
+ * @param Number timeout Timeout in milliseconds (optional)
1743
+ * @return Casper
1744
+ */
1745
+ Casper.prototype.waitForPopup = function waitForPopup(urlPattern, then, onTimeout, timeout) {
1746
+ "use strict";
1747
+ return this.waitFor(function() {
1748
+ try {
1749
+ this.popups.find(urlPattern);
1750
+ return true;
1751
+ } catch (e) {
1752
+ return false;
1753
+ }
1754
+ }, then, onTimeout, timeout);
1755
+ };
1756
+
1428
1757
  /**
1429
1758
  * Waits until a given resource is loaded
1430
1759
  *
@@ -1437,7 +1766,8 @@ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1437
1766
  */
1438
1767
  Casper.prototype.waitForResource = function waitForResource(test, then, onTimeout, timeout) {
1439
1768
  "use strict";
1440
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1769
+ this.checkStarted();
1770
+ timeout = timeout ? timeout : this.options.waitTimeout;
1441
1771
  return this.waitFor(function _check() {
1442
1772
  return this.resourceExists(test);
1443
1773
  }, then, onTimeout, timeout);
@@ -1455,12 +1785,31 @@ Casper.prototype.waitForResource = function waitForResource(test, then, onTimeou
1455
1785
  */
1456
1786
  Casper.prototype.waitForSelector = function waitForSelector(selector, then, onTimeout, timeout) {
1457
1787
  "use strict";
1458
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1788
+ this.checkStarted();
1789
+ timeout = timeout ? timeout : this.options.waitTimeout;
1459
1790
  return this.waitFor(function _check() {
1460
1791
  return this.exists(selector);
1461
1792
  }, then, onTimeout, timeout);
1462
1793
  };
1463
1794
 
1795
+ /**
1796
+ * Waits until the page contains given HTML text.
1797
+ *
1798
+ * @param String text Text to wait for
1799
+ * @param Function then The next step to perform (optional)
1800
+ * @param Function onTimeout A callback function to call on timeout (optional)
1801
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1802
+ * @return Casper
1803
+ */
1804
+ Casper.prototype.waitForText = function(text, then, onTimeout, timeout) {
1805
+ "use strict";
1806
+ this.checkStarted();
1807
+ timeout = timeout ? timeout : this.options.waitTimeout;
1808
+ return this.waitFor(function _check() {
1809
+ return this.getPageContent().indexOf(text) !== -1;
1810
+ }, then, onTimeout, timeout);
1811
+ };
1812
+
1464
1813
  /**
1465
1814
  * Waits until an element matching the provided DOM CSS3/XPath selector does not
1466
1815
  * exist in the remote DOM to process a next step.
@@ -1473,7 +1822,8 @@ Casper.prototype.waitForSelector = function waitForSelector(selector, then, onTi
1473
1822
  */
1474
1823
  Casper.prototype.waitWhileSelector = function waitWhileSelector(selector, then, onTimeout, timeout) {
1475
1824
  "use strict";
1476
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1825
+ this.checkStarted();
1826
+ timeout = timeout ? timeout : this.options.waitTimeout;
1477
1827
  return this.waitFor(function _check() {
1478
1828
  return !this.exists(selector);
1479
1829
  }, then, onTimeout, timeout);
@@ -1491,7 +1841,8 @@ Casper.prototype.waitWhileSelector = function waitWhileSelector(selector, then,
1491
1841
  */
1492
1842
  Casper.prototype.waitUntilVisible = function waitUntilVisible(selector, then, onTimeout, timeout) {
1493
1843
  "use strict";
1494
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1844
+ this.checkStarted();
1845
+ timeout = timeout ? timeout : this.options.waitTimeout;
1495
1846
  return this.waitFor(function _check() {
1496
1847
  return this.visible(selector);
1497
1848
  }, then, onTimeout, timeout);
@@ -1509,12 +1860,80 @@ Casper.prototype.waitUntilVisible = function waitUntilVisible(selector, then, on
1509
1860
  */
1510
1861
  Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, onTimeout, timeout) {
1511
1862
  "use strict";
1512
- timeout = timeout ? timeout : this.defaultWaitTimeout;
1863
+ this.checkStarted();
1864
+ timeout = timeout ? timeout : this.options.waitTimeout;
1513
1865
  return this.waitFor(function _check() {
1514
1866
  return !this.visible(selector);
1515
1867
  }, then, onTimeout, timeout);
1516
1868
  };
1517
1869
 
1870
+ /**
1871
+ * Makes the provided frame page as the currently active one. Note that the
1872
+ * active page will be reverted when finished.
1873
+ *
1874
+ * @param String|Number frameInfo Target frame name or number
1875
+ * @param Function then Next step function
1876
+ * @return Casper
1877
+ */
1878
+ Casper.prototype.withFrame = function withFrame(frameInfo, then) {
1879
+ "use strict";
1880
+ this.then(function _step() {
1881
+ if (utils.isNumber(frameInfo)) {
1882
+ if (frameInfo > this.page.childFramesCount() - 1) {
1883
+ throw new CasperError(f('Frame number "%d" is out of bounds.', frameInfo));
1884
+ }
1885
+ } else if (this.page.childFramesName().indexOf(frameInfo) === -1) {
1886
+ throw new CasperError(f('No frame named "%s" was found.', frameInfo));
1887
+ }
1888
+ // make the frame page the currently active one
1889
+ this.page.switchToChildFrame(frameInfo);
1890
+ });
1891
+ try {
1892
+ this.then(then);
1893
+ } catch (e) {
1894
+ // revert to main page on error
1895
+ this.warn("Error while processing frame step: " + e);
1896
+ this.page.switchToMainFrame();
1897
+ throw e;
1898
+ }
1899
+ return this.then(function _step() {
1900
+ // revert to main page
1901
+ this.page.switchToMainFrame();
1902
+ });
1903
+ };
1904
+
1905
+ /**
1906
+ * Makes the provided frame page as the currently active one. Note that the
1907
+ * active page will be reverted when finished.
1908
+ *
1909
+ * @param String|RegExp|WebPage popup Target frame page information
1910
+ * @param Function then Next step function
1911
+ * @return Casper
1912
+ */
1913
+ Casper.prototype.withPopup = function withPopup(popupInfo, then) {
1914
+ "use strict";
1915
+ this.then(function _step() {
1916
+ var popupPage = this.popups.find(popupInfo);
1917
+ if (!utils.isFunction(then)) {
1918
+ throw new CasperError("withPopup() requires a step function.");
1919
+ }
1920
+ // make the popup page the currently active one
1921
+ this.page = popupPage;
1922
+ });
1923
+ try {
1924
+ this.then(then);
1925
+ } catch (e) {
1926
+ // revert to main page on error
1927
+ this.log("error while processing popup step: " + e, "error");
1928
+ this.page = this.mainPage;
1929
+ throw e;
1930
+ }
1931
+ return this.then(function _step() {
1932
+ // revert to main page
1933
+ this.page = this.mainPage;
1934
+ });
1935
+ };
1936
+
1518
1937
  /**
1519
1938
  * Changes the current page zoom factor.
1520
1939
  *
@@ -1523,17 +1942,11 @@ Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, on
1523
1942
  */
1524
1943
  Casper.prototype.zoom = function zoom(factor) {
1525
1944
  "use strict";
1526
- if (!this.started) {
1527
- throw new CasperError("Casper has not been started, can't set zoom factor");
1528
- }
1945
+ this.checkStarted();
1529
1946
  if (!utils.isNumber(factor) || factor <= 0) {
1530
1947
  throw new CasperError("Invalid zoom factor: " + factor);
1531
1948
  }
1532
- if ('zoomFactor' in this.page) {
1533
- this.page.zoomFactor = factor;
1534
- } else {
1535
- this.warn("zoom() requires PhantomJS >= 1.6");
1536
- }
1949
+ this.page.zoomFactor = factor;
1537
1950
  return this;
1538
1951
  };
1539
1952
 
@@ -1546,7 +1959,7 @@ Casper.prototype.zoom = function zoom(factor) {
1546
1959
  */
1547
1960
  Casper.extend = function(proto) {
1548
1961
  "use strict";
1549
- this.warn('Casper.extend() has been deprecated since 0.6; check the docs');
1962
+ this.emit("deprecated", "Casper.extend() has been deprecated since 0.6; check the docs")
1550
1963
  if (!utils.isObject(proto)) {
1551
1964
  throw new CasperError("extends() only accept objects as prototypes");
1552
1965
  }
@@ -1562,6 +1975,7 @@ exports.Casper = Casper;
1562
1975
  * @return WebPage
1563
1976
  */
1564
1977
  function createPage(casper) {
1978
+ /*jshint maxstatements:20*/
1565
1979
  "use strict";
1566
1980
  var page = require('webpage').create();
1567
1981
  page.onAlert = function onAlert(message) {
@@ -1572,24 +1986,35 @@ function createPage(casper) {
1572
1986
  }
1573
1987
  };
1574
1988
  page.onConfirm = function onConfirm(message) {
1575
- return casper.filter('page.confirm', message) || true;
1989
+ if ('page.confirm' in casper._filters) {
1990
+ return casper.filter('page.confirm', message);
1991
+ }
1992
+ return true;
1576
1993
  };
1577
1994
  page.onConsoleMessage = function onConsoleMessage(msg) {
1578
- var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg);
1579
- if (test && test.length === 3) {
1580
- level = test[1];
1581
- msg = test[2];
1995
+ // client utils casper console message
1996
+ var consoleTest = /^\[casper\.echo\]\s?([\s\S]*)/.exec(msg);
1997
+ if (consoleTest && consoleTest.length === 2) {
1998
+ casper.echo(consoleTest[1]);
1999
+ return; // don't trigger remote.message event for these
2000
+ }
2001
+ // client utils log messages
2002
+ var logLevel = "info",
2003
+ logTest = /^\[casper:(\w+)\]\s?([\s\S]*)/m.exec(msg);
2004
+ if (logTest && logTest.length === 3) {
2005
+ logLevel = logTest[1];
2006
+ msg = logTest[2];
1582
2007
  }
1583
- casper.log(msg, level, "remote");
2008
+ casper.log(msg, logLevel, "remote");
1584
2009
  casper.emit('remote.message', msg);
1585
2010
  };
1586
2011
  page.onError = function onError(msg, trace) {
1587
2012
  casper.emit('page.error', msg, trace);
1588
2013
  };
1589
2014
  page.onInitialized = function onInitialized() {
1590
- casper.emit('page.initialized', this);
2015
+ casper.emit('page.initialized', page);
1591
2016
  if (utils.isFunction(casper.options.onPageInitialized)) {
1592
- this.log("Post-configuring WebPage instance", "debug");
2017
+ casper.log("Post-configuring WebPage instance", "debug");
1593
2018
  casper.options.onPageInitialized.call(casper, page);
1594
2019
  }
1595
2020
  };
@@ -1598,6 +2023,7 @@ function createPage(casper) {
1598
2023
  casper.emit('load.started');
1599
2024
  };
1600
2025
  page.onLoadFinished = function onLoadFinished(status) {
2026
+ /*jshint maxstatements:20*/
1601
2027
  if (status !== "success") {
1602
2028
  casper.emit('load.failed', {
1603
2029
  status: status,
@@ -1610,25 +2036,15 @@ function createPage(casper) {
1610
2036
  }
1611
2037
  message += ': ' + casper.requestUrl;
1612
2038
  casper.log(message, "warning");
2039
+ casper.navigationRequested = false;
1613
2040
  if (utils.isFunction(casper.options.onLoadError)) {
1614
2041
  casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
1615
2042
  }
1616
2043
  }
1617
- if (casper.options.clientScripts) {
1618
- if (utils.isString(casper.options.clientScripts)) {
1619
- casper.options.clientScripts = [casper.options.clientScripts];
1620
- }
1621
- if (!utils.isArray(casper.options.clientScripts)) {
1622
- throw new CasperError("The clientScripts option must be an array");
1623
- }
1624
- casper.options.clientScripts.forEach(function _forEach(script) {
1625
- if (casper.page.injectJs(script)) {
1626
- casper.log(f('Automatically injected %s client side', script), "debug");
1627
- } else {
1628
- casper.warn('Failed injecting %s client side', script);
1629
- }
1630
- });
1631
- }
2044
+ // local client scripts
2045
+ casper.injectClientScripts();
2046
+ // remote client scripts
2047
+ casper.includeRemoteScripts();
1632
2048
  // Client-side utils injection
1633
2049
  casper.injectClientUtils();
1634
2050
  // history
@@ -1639,39 +2055,45 @@ function createPage(casper) {
1639
2055
  page.onNavigationRequested = function onNavigationRequested(url, navigationType, navigationLocked, isMainFrame) {
1640
2056
  casper.log(f('Navigation requested: url=%s, type=%s, lock=%s, isMainFrame=%s',
1641
2057
  url, navigationType, navigationLocked, isMainFrame), "debug");
2058
+ if(isMainFrame) {
2059
+ casper.navigationRequested = true;
2060
+ }
1642
2061
  casper.emit('navigation.requested', url, navigationType, navigationLocked, isMainFrame);
1643
2062
  };
2063
+ page.onPageCreated = function onPageCreated(popupPage) {
2064
+ casper.emit('popup.created', popupPage);
2065
+ popupPage.onLoadFinished = function onLoadFinished() {
2066
+ casper.popups.push(popupPage);
2067
+ casper.emit('popup.loaded', popupPage);
2068
+ };
2069
+ popupPage.onClosing = function onClosing(closedPopup) {
2070
+ casper.popups.clean(closedPopup);
2071
+ casper.emit('popup.closed', closedPopup);
2072
+ };
2073
+ };
1644
2074
  page.onPrompt = function onPrompt(message, value) {
1645
2075
  return casper.filter('page.prompt', message, value);
1646
2076
  };
1647
2077
  page.onResourceReceived = function onResourceReceived(resource) {
2078
+ http.augmentResponse(resource);
1648
2079
  casper.emit('resource.received', resource);
1649
2080
  if (utils.isFunction(casper.options.onResourceReceived)) {
1650
2081
  casper.options.onResourceReceived.call(casper, casper, resource);
1651
2082
  }
1652
- if (resource.stage === "end") {
1653
- casper.resources.push(resource);
1654
- }
1655
- if (resource.url === casper.requestUrl && resource.stage === "end") {
1656
- casper.currentHTTPStatus = resource.status;
1657
- casper.emit('http.status.' + resource.status, resource);
1658
- if (utils.isObject(casper.options.httpStatusHandlers) &&
1659
- resource.status in casper.options.httpStatusHandlers &&
1660
- utils.isFunction(casper.options.httpStatusHandlers[resource.status])) {
1661
- casper.options.httpStatusHandlers[resource.status].call(casper, casper, resource);
1662
- }
1663
- casper.currentUrl = resource.url;
1664
- casper.emit('location.changed', resource.url);
1665
- }
2083
+ casper.handleReceivedResource(resource);
1666
2084
  };
1667
2085
  page.onResourceRequested = function onResourceRequested(request) {
1668
2086
  casper.emit('resource.requested', request);
2087
+ if (request.url === casper.requestUrl) {
2088
+ casper.emit('page.resource.requested', request);
2089
+ }
1669
2090
  if (utils.isFunction(casper.options.onResourceRequested)) {
1670
2091
  casper.options.onResourceRequested.call(casper, casper, request);
1671
2092
  }
1672
2093
  };
1673
2094
  page.onUrlChanged = function onUrlChanged(url) {
1674
2095
  casper.log(f('url changed to "%s"', url), "debug");
2096
+ casper.navigationRequested = false;
1675
2097
  casper.emit('url.changed', url);
1676
2098
  };
1677
2099
  casper.emit('page.created', page);