casperjs 1.0.0.RC1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. data/CHANGELOG.md +179 -0
  2. data/LICENSE.md +19 -0
  3. data/README.md +30 -0
  4. data/bin/bootstrap.js +292 -0
  5. data/bin/usage.txt +10 -0
  6. data/casperjs.gemspec +21 -0
  7. data/modules/casper.js +1679 -0
  8. data/modules/cli.js +138 -0
  9. data/modules/clientutils.js +595 -0
  10. data/modules/colorizer.js +129 -0
  11. data/modules/events.js +247 -0
  12. data/modules/injector.js +93 -0
  13. data/modules/mouse.js +110 -0
  14. data/modules/querystring.js +187 -0
  15. data/modules/tester.js +807 -0
  16. data/modules/utils.js +429 -0
  17. data/modules/vendors/coffee-script.js +8 -0
  18. data/modules/xunit.js +123 -0
  19. data/package.json +35 -0
  20. data/rubybin/casperjs +57 -0
  21. data/samples/bbcshots.coffee +64 -0
  22. data/samples/bbcshots.js +80 -0
  23. data/samples/cliplay.coffee +19 -0
  24. data/samples/cliplay.js +21 -0
  25. data/samples/customevents.coffee +11 -0
  26. data/samples/customevents.js +13 -0
  27. data/samples/customlogging.coffee +33 -0
  28. data/samples/customlogging.js +42 -0
  29. data/samples/download.coffee +10 -0
  30. data/samples/download.js +11 -0
  31. data/samples/dynamic.coffee +60 -0
  32. data/samples/dynamic.js +65 -0
  33. data/samples/each.coffee +14 -0
  34. data/samples/each.js +17 -0
  35. data/samples/events.coffee +34 -0
  36. data/samples/events.js +41 -0
  37. data/samples/extends.coffee +29 -0
  38. data/samples/extends.js +37 -0
  39. data/samples/googlelinks.coffee +27 -0
  40. data/samples/googlelinks.js +33 -0
  41. data/samples/googlematch.coffee +47 -0
  42. data/samples/googlematch.js +65 -0
  43. data/samples/googlepagination.coffee +40 -0
  44. data/samples/googlepagination.js +51 -0
  45. data/samples/googletesting.coffee +17 -0
  46. data/samples/googletesting.js +23 -0
  47. data/samples/logcolor.coffee +10 -0
  48. data/samples/logcolor.js +11 -0
  49. data/samples/metaextract.coffee +23 -0
  50. data/samples/metaextract.js +29 -0
  51. data/samples/multirun.coffee +37 -0
  52. data/samples/multirun.js +56 -0
  53. data/samples/screenshot.coffee +28 -0
  54. data/samples/screenshot.js +33 -0
  55. data/samples/statushandlers.coffee +15 -0
  56. data/samples/statushandlers.js +19 -0
  57. data/samples/steptimeout.coffee +37 -0
  58. data/samples/steptimeout.js +45 -0
  59. data/samples/timeout.coffee +39 -0
  60. data/samples/timeout.js +47 -0
  61. data/tests/run.js +76 -0
  62. data/tests/site/alert.html +10 -0
  63. data/tests/site/click.html +40 -0
  64. data/tests/site/confirm.html +12 -0
  65. data/tests/site/elementattribute.html +6 -0
  66. data/tests/site/error.html +10 -0
  67. data/tests/site/form.html +26 -0
  68. data/tests/site/global.html +9 -0
  69. data/tests/site/images/phantom.png +0 -0
  70. data/tests/site/index.html +17 -0
  71. data/tests/site/mouse-events.html +47 -0
  72. data/tests/site/multiple-forms.html +16 -0
  73. data/tests/site/page1.html +8 -0
  74. data/tests/site/page2.html +8 -0
  75. data/tests/site/page3.html +8 -0
  76. data/tests/site/prompt.html +12 -0
  77. data/tests/site/resources.html +16 -0
  78. data/tests/site/result.html +11 -0
  79. data/tests/site/test.html +10 -0
  80. data/tests/site/visible.html +17 -0
  81. data/tests/site/waitFor.html +22 -0
  82. data/tests/suites/casper/agent.js +24 -0
  83. data/tests/suites/casper/capture.js +31 -0
  84. data/tests/suites/casper/click.js +61 -0
  85. data/tests/suites/casper/confirm.js +21 -0
  86. data/tests/suites/casper/elementattribute.js +8 -0
  87. data/tests/suites/casper/encode.js +20 -0
  88. data/tests/suites/casper/evaluate.js +27 -0
  89. data/tests/suites/casper/events.js +38 -0
  90. data/tests/suites/casper/exists.js +9 -0
  91. data/tests/suites/casper/fetchtext.js +9 -0
  92. data/tests/suites/casper/flow.coffee +38 -0
  93. data/tests/suites/casper/formfill.js +69 -0
  94. data/tests/suites/casper/global.js +9 -0
  95. data/tests/suites/casper/history.js +21 -0
  96. data/tests/suites/casper/hooks.js +41 -0
  97. data/tests/suites/casper/logging.js +38 -0
  98. data/tests/suites/casper/mouseevents.js +27 -0
  99. data/tests/suites/casper/onerror.js +19 -0
  100. data/tests/suites/casper/open.js +73 -0
  101. data/tests/suites/casper/prompt.js +17 -0
  102. data/tests/suites/casper/resources.coffee +24 -0
  103. data/tests/suites/casper/start.js +15 -0
  104. data/tests/suites/casper/steps.js +32 -0
  105. data/tests/suites/casper/viewport.js +11 -0
  106. data/tests/suites/casper/visible.js +17 -0
  107. data/tests/suites/casper/wait.js +27 -0
  108. data/tests/suites/casper/xpath.js +32 -0
  109. data/tests/suites/cli.js +125 -0
  110. data/tests/suites/clientutils.js +84 -0
  111. data/tests/suites/coffee.coffee +19 -0
  112. data/tests/suites/fs.js +36 -0
  113. data/tests/suites/http_status.js +28 -0
  114. data/tests/suites/injector.js +64 -0
  115. data/tests/suites/tester.js +121 -0
  116. data/tests/suites/utils.js +209 -0
  117. data/tests/suites/xunit.js +16 -0
  118. data/tests/testdir/01_a/abc.js +0 -0
  119. data/tests/testdir/01_a/def.js +0 -0
  120. data/tests/testdir/02_b/abc.js +0 -0
  121. data/tests/testdir/03_a.js +0 -0
  122. data/tests/testdir/03_b.js +0 -0
  123. data/tests/testdir/04/01_init.js +0 -0
  124. data/tests/testdir/04/02_do.js +0 -0
  125. metadata +192 -0
@@ -0,0 +1,10 @@
1
+
2
+ Usage: casperjs [options] script.[js|coffee] [script argument [script argument ...]]
3
+ casperjs [options] test [test path [test path ...]]
4
+
5
+ Options:
6
+
7
+ --help Prints this help
8
+ --version Prints out CasperJS version
9
+
10
+ Read the docs http://casperjs.org/
@@ -0,0 +1,21 @@
1
+
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "casperjs"
5
+ s.version = "1.0.0.RC1"
6
+ s.homepage = "http://casperjs.org/"
7
+ s.authors = ["Nicolas Perriault", ]
8
+ s.email = ["nperriault@gmail.com",]
9
+ s.description = "CasperJS is a navigation scripting & testing utility for [PhantomJS](http://www.phantomjs.org/).
10
+ It eases the process of defining a full navigation scenario and provides useful
11
+ high-level functions, methods & syntaxic sugar for doing common tasks."
12
+ s.summary = "Navigation scripting & testing utility for PhantomJS"
13
+ 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",
16
+ "docs/**/*", "modules/**/*", "samples/**/*", "tests/**/*"]
17
+ s.bindir = "rubybin"
18
+ s.executables = [ "casperjs" ]
19
+ s.license = "MIT"
20
+ s.requirements = [ "PhantomJS v1.5" ]
21
+ end
@@ -0,0 +1,1679 @@
1
+ /*!
2
+ * Casper is a navigation utility for PhantomJS.
3
+ *
4
+ * Documentation: http://casperjs.org/
5
+ * Repository: http://github.com/n1k0/casperjs
6
+ *
7
+ * Copyright (c) 2011-2012 Nicolas Perriault
8
+ *
9
+ * Part of source code is Copyright Joyent, Inc. and other Node contributors.
10
+ *
11
+ * Permission is hereby granted, free of charge, to any person obtaining a
12
+ * copy of this software and associated documentation files (the "Software"),
13
+ * to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
+ * and/or sell copies of the Software, and to permit persons to whom the
16
+ * Software is furnished to do so, subject to the following conditions:
17
+ *
18
+ * The above copyright notice and this permission notice shall be included
19
+ * in all copies or substantial portions of the Software.
20
+ *
21
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
+ * DEALINGS IN THE SOFTWARE.
28
+ *
29
+ */
30
+
31
+ /*global CasperError console exports phantom require*/
32
+
33
+ var colorizer = require('colorizer');
34
+ var events = require('events');
35
+ var fs = require('fs');
36
+ var mouse = require('mouse');
37
+ var qs = require('querystring');
38
+ var tester = require('tester');
39
+ var utils = require('utils');
40
+ var f = utils.format;
41
+
42
+
43
+ var defaultUserAgent = phantom.defaultPageSettings.userAgent
44
+ .replace('PhantomJS', f("CasperJS/%s", phantom.casperVersion) + '+Phantomjs');
45
+
46
+ exports.create = function create(options) {
47
+ "use strict";
48
+ return new Casper(options);
49
+ };
50
+
51
+ /**
52
+ * Shortcut to build an XPath selector object.
53
+ *
54
+ * @param String expression The XPath expression
55
+ * @return Object
56
+ * @see http://casperjs.org/selectors.html
57
+ */
58
+ function selectXPath(expression) {
59
+ "use strict";
60
+ return {
61
+ type: 'xpath',
62
+ path: expression,
63
+ toString: function() {
64
+ return this.type + ' selector: ' + this.path;
65
+ }
66
+ };
67
+ }
68
+ exports.selectXPath = selectXPath;
69
+
70
+ /**
71
+ * Main Casper object.
72
+ *
73
+ * @param Object options Casper options
74
+ */
75
+ var Casper = function Casper(options) {
76
+ "use strict";
77
+ // init & checks
78
+ if (!(this instanceof Casper)) {
79
+ return new Casper(options);
80
+ }
81
+ // default options
82
+ this.defaults = {
83
+ clientScripts: [],
84
+ colorizerType: 'Colorizer',
85
+ exitOnError: true,
86
+ logLevel: "error",
87
+ httpStatusHandlers: {},
88
+ safeLogs: true,
89
+ onAlert: null,
90
+ onDie: null,
91
+ onError: null,
92
+ onLoadError: null,
93
+ onPageInitialized: null,
94
+ onResourceReceived: null,
95
+ onResourceRequested: null,
96
+ onStepComplete: null,
97
+ onStepTimeout: null,
98
+ onTimeout: null,
99
+ page: null,
100
+ pageSettings: {
101
+ localToRemoteUrlAccessEnabled: true,
102
+ userAgent: defaultUserAgent
103
+ },
104
+ stepTimeout: null,
105
+ timeout: null,
106
+ verbose: false
107
+ };
108
+ // options
109
+ this.options = utils.mergeObjects(this.defaults, options);
110
+ // properties
111
+ this.checker = null;
112
+ this.cli = phantom.casperArgs;
113
+ this.colorizer = this.getColorizer();
114
+ this.currentUrl = 'about:blank';
115
+ this.currentHTTPStatus = 0;
116
+ this.defaultWaitTimeout = 5000;
117
+ this.history = [];
118
+ this.loadInProgress = false;
119
+ this.logFormats = {};
120
+ this.logLevels = ["debug", "info", "warning", "error"];
121
+ this.logStyles = {
122
+ debug: 'INFO',
123
+ info: 'PARAMETER',
124
+ warning: 'COMMENT',
125
+ error: 'ERROR'
126
+ };
127
+ this.mouse = mouse.create(this);
128
+ this.page = null;
129
+ this.pendingWait = false;
130
+ this.requestUrl = 'about:blank';
131
+ this.resources = [];
132
+ this.result = {
133
+ log: [],
134
+ status: "success",
135
+ time: 0
136
+ };
137
+ this.started = false;
138
+ this.step = -1;
139
+ this.steps = [];
140
+ this.test = tester.create(this);
141
+
142
+ // init phantomjs error handler
143
+ this.initErrorHandler();
144
+
145
+ this.on('error', function(msg, backtrace) {
146
+ var c = this.getColorizer();
147
+ var match = /^(.*): __mod_error(.*):: (.*)/.exec(msg);
148
+ var notices = [];
149
+ if (match && match.length === 4) {
150
+ notices.push(' in module ' + match[2]);
151
+ notices.push(' NOTICE: errors within modules cannot be backtraced yet.');
152
+ msg = match[3];
153
+ }
154
+ console.error(c.colorize(msg, 'RED_BAR', 80));
155
+ notices.forEach(function(notice) {
156
+ console.error(c.colorize(notice, 'COMMENT'));
157
+ });
158
+ backtrace.forEach(function(item) {
159
+ var message = fs.absolute(item.file) + ":" + c.colorize(item.line, "COMMENT");
160
+ if (item['function']) {
161
+ message += " in " + c.colorize(item['function'], "PARAMETER");
162
+ }
163
+ console.error(" " + message);
164
+ });
165
+ });
166
+
167
+ // deprecated feature event handler
168
+ this.on('deprecated', function onDeprecated(message) {
169
+ this.warn('[deprecated] ' + message);
170
+ });
171
+
172
+ // dispatching an event when instance has been constructed
173
+ this.emit('init');
174
+ };
175
+
176
+ // Casper class is an EventEmitter
177
+ utils.inherits(Casper, events.EventEmitter);
178
+
179
+ /**
180
+ * Go a step back in browser's history
181
+ *
182
+ * @return Casper
183
+ */
184
+ Casper.prototype.back = function back() {
185
+ "use strict";
186
+ return this.then(function _step() {
187
+ this.emit('back');
188
+ this.evaluate(function _evaluate() {
189
+ history.back();
190
+ });
191
+ });
192
+ };
193
+
194
+ /**
195
+ * Encodes a resource using the base64 algorithm synchroneously using
196
+ * client-side XMLHttpRequest.
197
+ *
198
+ * NOTE: we cannot use window.btoa() for some strange reasons here.
199
+ *
200
+ * @param String url The url to download
201
+ * @param String method The method to use, optional: default GET
202
+ * @param String data The data to send, optional
203
+ * @return string Base64 encoded result
204
+ */
205
+ Casper.prototype.base64encode = function base64encode(url, method, data) {
206
+ "use strict";
207
+ return this.evaluate(function _evaluate(url, method, data) {
208
+ return window.__utils__.getBase64(url, method, data);
209
+ }, { url: url, method: method, data: data });
210
+ };
211
+
212
+ /**
213
+ * Proxy method for WebPage#render. Adds a clipRect parameter for
214
+ * automatically set page clipRect setting values and sets it back once
215
+ * done. If the cliprect parameter is omitted, the full page viewport
216
+ * area will be rendered.
217
+ *
218
+ * @param String targetFile A target filename
219
+ * @param mixed clipRect An optional clipRect object (optional)
220
+ * @return Casper
221
+ */
222
+ Casper.prototype.capture = function capture(targetFile, clipRect) {
223
+ "use strict";
224
+ if (!this.started) {
225
+ throw new CasperError("Casper not started, can't capture()");
226
+ }
227
+ var previousClipRect;
228
+ targetFile = fs.absolute(targetFile);
229
+ if (clipRect) {
230
+ if (!utils.isClipRect(clipRect)) {
231
+ throw new CasperError("clipRect must be a valid ClipRect object.");
232
+ }
233
+ previousClipRect = this.page.clipRect;
234
+ this.page.clipRect = clipRect;
235
+ this.log(f("Capturing page to %s with clipRect %s", targetFile, JSON.stringify(clipRect)), "debug");
236
+ } else {
237
+ this.log(f("Capturing page to %s", targetFile), "debug");
238
+ }
239
+ if (!this.page.render(this.filter('capture.target_filename', targetFile) || targetFile)) {
240
+ this.log(f("Failed to save screenshot to %s; please check permissions", targetFile), "error");
241
+ } else {
242
+ this.log(f("Capture saved to %s", targetFile), "info");
243
+ this.emit('capture.saved', targetFile);
244
+ }
245
+ if (previousClipRect) {
246
+ this.page.clipRect = previousClipRect;
247
+ }
248
+ return this;
249
+ };
250
+
251
+ /**
252
+ * Returns a Base64 representation of a binary image capture of the current
253
+ * page, or an area within the page, in a given format.
254
+ *
255
+ * Supported image formats are `bmp`, `jpg`, `jpeg`, `png`, `ppm`, `tiff`,
256
+ * `xbm` and `xpm`.
257
+ *
258
+ * @param String format The image format
259
+ * @param String|Object|undefined selector DOM CSS3/XPath selector or clipRect object (optional)
260
+ * @return Casper
261
+ */
262
+ Casper.prototype.captureBase64 = function captureBase64(format, area) {
263
+ "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'];
274
+ if (formats.indexOf(format.toLowerCase()) === -1) {
275
+ throw new CasperError(f('Unsupported format "%s"', format));
276
+ }
277
+ if (utils.isClipRect(area)) {
278
+ // if area is a clipRect object
279
+ this.log(f("Capturing base64 %s representation of %s", format, utils.serialize(area)), "debug");
280
+ previousClipRect = this.page.clipRect;
281
+ this.page.clipRect = area;
282
+ base64 = this.page.renderBase64(format);
283
+ } else if (utils.isValidSelector(area)) {
284
+ // if area is a selector string or object
285
+ this.log(f("Capturing base64 %s representation of %s", format, area), "debug");
286
+ base64 = this.captureBase64(format, this.getElementBounds(area));
287
+ } else {
288
+ // whole page capture
289
+ this.log(f("Capturing base64 %s representation of page", format), "debug");
290
+ base64 = this.page.renderBase64(format);
291
+ }
292
+ if (previousClipRect) {
293
+ this.page.clipRect = previousClipRect;
294
+ }
295
+ return base64;
296
+ };
297
+
298
+ /**
299
+ * Captures the page area matching the provided selector.
300
+ *
301
+ * @param String targetFile Target destination file path.
302
+ * @param String selector DOM CSS3/XPath selector
303
+ * @return Casper
304
+ */
305
+ Casper.prototype.captureSelector = function captureSelector(targetFile, selector) {
306
+ "use strict";
307
+ return this.capture(targetFile, this.getElementBounds(selector));
308
+ };
309
+
310
+ /**
311
+ * Checks for any further navigation step to process.
312
+ *
313
+ * @param Casper self A self reference
314
+ * @param function onComplete An options callback to apply on completion
315
+ */
316
+ Casper.prototype.checkStep = function checkStep(self, onComplete) {
317
+ "use strict";
318
+ if (self.pendingWait || self.loadInProgress) {
319
+ return;
320
+ }
321
+ var step = self.steps[self.step++];
322
+ if (utils.isFunction(step)) {
323
+ self.runStep(step);
324
+ } else {
325
+ self.result.time = new Date().getTime() - self.startTime;
326
+ self.log(f("Done %s steps in %dms", self.steps.length, self.result.time), "info");
327
+ clearInterval(self.checker);
328
+ self.emit('run.complete');
329
+ if (utils.isFunction(onComplete)) {
330
+ onComplete.call(self, self);
331
+ } else {
332
+ // default behavior is to exit
333
+ self.exit();
334
+ }
335
+ }
336
+ };
337
+
338
+ /**
339
+ * Clears the current page execution environment context. Useful to avoid
340
+ * having previously loaded DOM contents being still active (refs #34).
341
+ *
342
+ * Think of it as a way to stop javascript execution within the remote DOM
343
+ * environment.
344
+ *
345
+ * @return Casper
346
+ */
347
+ Casper.prototype.clear = function clear() {
348
+ "use strict";
349
+ this.page.content = '';
350
+ return this;
351
+ };
352
+
353
+ /**
354
+ * Emulates a click on the element from the provided selector using the mouse
355
+ * pointer, if possible.
356
+ *
357
+ * In case of success, `true` is returned, `false` otherwise.
358
+ *
359
+ * @param String selector A DOM CSS3 compatible selector
360
+ * @return Boolean
361
+ */
362
+ Casper.prototype.click = function click(selector) {
363
+ "use strict";
364
+ return this.mouseEvent('click', selector);
365
+ };
366
+
367
+ /**
368
+ * Emulates a click on the element having `label` as innerText. The first
369
+ * element matching this label will be selected, so use with caution.
370
+ *
371
+ * @param String label Element innerText value
372
+ * @param String tag An element tag name (eg. `a` or `button`) (optional)
373
+ * @return Boolean
374
+ */
375
+ Casper.prototype.clickLabel = function clickLabel(label, tag) {
376
+ "use strict";
377
+ tag = tag || "*";
378
+ var escapedLabel = label.toString().replace(/"/g, '\\"');
379
+ var selector = selectXPath(f('//%s[text()="%s"]', tag, escapedLabel));
380
+ return this.click(selector);
381
+ };
382
+
383
+ /**
384
+ * Creates a step definition.
385
+ *
386
+ * @param Function fn The step function to call
387
+ * @param Object options Step options
388
+ * @return Function The final step function
389
+ */
390
+ Casper.prototype.createStep = function createStep(fn, options) {
391
+ "use strict";
392
+ if (!utils.isFunction(fn)) {
393
+ throw new CasperError("createStep(): a step definition must be a function");
394
+ }
395
+ fn.options = utils.isObject(options) ? options : {};
396
+ this.emit('step.created', fn);
397
+ return fn;
398
+ };
399
+
400
+ /**
401
+ * Logs the HTML code of the current page.
402
+ *
403
+ * @return Casper
404
+ */
405
+ Casper.prototype.debugHTML = function debugHTML() {
406
+ "use strict";
407
+ this.echo(this.page.content);
408
+ return this;
409
+ };
410
+
411
+ /**
412
+ * Logs the textual contents of the current page.
413
+ *
414
+ * @return Casper
415
+ */
416
+ Casper.prototype.debugPage = function debugPage() {
417
+ "use strict";
418
+ this.echo(this.evaluate(function _evaluate() {
419
+ return document.body.textContent || document.body.innerText;
420
+ }));
421
+ return this;
422
+ };
423
+
424
+ /**
425
+ * Exit phantom on failure, with a logged error message.
426
+ *
427
+ * @param String message An optional error message
428
+ * @param Number status An optional exit status code (must be > 0)
429
+ * @return Casper
430
+ */
431
+ Casper.prototype.die = function die(message, status) {
432
+ "use strict";
433
+ this.result.status = "error";
434
+ this.result.time = new Date().getTime() - this.startTime;
435
+ if (!utils.isString(message) || !message.length) {
436
+ message = "Suite explicitely interrupted without any message given.";
437
+ }
438
+ this.log(message, "error");
439
+ this.emit('die', message, status);
440
+ if (utils.isFunction(this.options.onDie)) {
441
+ this.options.onDie.call(this, this, message, status);
442
+ }
443
+ return this.exit(~~status > 0 ? ~~status : 1);
444
+ };
445
+
446
+ /**
447
+ * Downloads a resource and saves it on the filesystem.
448
+ *
449
+ * @param String url The url of the resource to download
450
+ * @param String targetPath The destination file path
451
+ * @param String method The HTTP method to use (default: GET)
452
+ * @param String data Optional data to pass performing the request
453
+ * @return Casper
454
+ */
455
+ Casper.prototype.download = function download(url, targetPath, method, data) {
456
+ "use strict";
457
+ var cu = require('clientutils').create(this.options);
458
+ try {
459
+ fs.write(targetPath, cu.decode(this.base64encode(url, method, data)), 'wb');
460
+ this.emit('downloaded.file', targetPath);
461
+ this.log(f("Downloaded and saved resource in %s", targetPath));
462
+ } catch (e) {
463
+ this.log(f("Error while downloading %s to %s: %s", url, targetPath, e), "error");
464
+ }
465
+ return this;
466
+ };
467
+
468
+ /**
469
+ * Iterates over the values of a provided array and execute a callback
470
+ * for @ item.
471
+ *
472
+ * @param Array array
473
+ * @param Function fn Callback: function(self, item, index)
474
+ * @return Casper
475
+ */
476
+ Casper.prototype.each = function each(array, fn) {
477
+ "use strict";
478
+ if (!utils.isArray(array)) {
479
+ this.log("each() only works with arrays", "error");
480
+ return this;
481
+ }
482
+ (function _each(self) {
483
+ array.forEach(function _forEach(item, i) {
484
+ fn.call(self, self, item, i);
485
+ });
486
+ })(this);
487
+ return this;
488
+ };
489
+
490
+ /**
491
+ * Prints something to stdout.
492
+ *
493
+ * @param String text A string to echo to stdout
494
+ * @param String style An optional style name
495
+ * @param Number pad An optional pad value
496
+ * @return Casper
497
+ */
498
+ Casper.prototype.echo = function echo(text, style, pad) {
499
+ "use strict";
500
+ if (!utils.isString(text)) {
501
+ try {
502
+ text = text.toString();
503
+ } catch (e) {
504
+ try {
505
+ text = utils.serialize(text);
506
+ } catch (e) {
507
+ text = '';
508
+ }
509
+ }
510
+ }
511
+ var message = style ? this.colorizer.colorize(text, style, pad) : text;
512
+ console.log(this.filter('echo.message', message) || message);
513
+ return this;
514
+ };
515
+
516
+ /**
517
+ * Evaluates an expression in the page context, a bit like what
518
+ * WebPage#evaluate does, but the passed function can also accept
519
+ * parameters if a context Object is also passed:
520
+ *
521
+ * casper.evaluate(function(username, password) {
522
+ * document.querySelector('#username').value = username;
523
+ * document.querySelector('#password').value = password;
524
+ * 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.
533
+ *
534
+ * @param Function fn The function to be evaluated within current page DOM
535
+ * @param Object context Object containing the parameters to inject into the function
536
+ * @return mixed
537
+ * @see WebPage#evaluate
538
+ */
539
+ Casper.prototype.evaluate = function evaluate(fn, context) {
540
+ "use strict";
541
+ // ensure client utils are always injected
542
+ 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);
547
+ };
548
+
549
+ /**
550
+ * Evaluates an expression within the current page DOM and die() if it
551
+ * returns false.
552
+ *
553
+ * @param function fn The expression to evaluate
554
+ * @param String message The error message to log
555
+ * @return Casper
556
+ */
557
+ Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message) {
558
+ "use strict";
559
+ if (!this.evaluate(fn)) {
560
+ return this.die(message);
561
+ }
562
+ return this;
563
+ };
564
+
565
+ /**
566
+ * Checks if an element matching the provided DOM CSS3/XPath selector exists in
567
+ * current page DOM.
568
+ *
569
+ * @param String selector A DOM CSS3/XPath selector
570
+ * @return Boolean
571
+ */
572
+ Casper.prototype.exists = function exists(selector) {
573
+ "use strict";
574
+ return this.evaluate(function _evaluate(selector) {
575
+ return window.__utils__.exists(selector);
576
+ }, { selector: selector });
577
+ };
578
+
579
+ /**
580
+ * Exits phantom.
581
+ *
582
+ * @param Number status Status
583
+ * @return Casper
584
+ */
585
+ Casper.prototype.exit = function exit(status) {
586
+ "use strict";
587
+ this.emit('exit', status);
588
+ phantom.exit(status);
589
+ return this;
590
+ };
591
+
592
+ /**
593
+ * Fetches innerText within the element(s) matching a given CSS3
594
+ * selector.
595
+ *
596
+ * @param String selector A DOM CSS3/XPath selector
597
+ * @return String
598
+ */
599
+ Casper.prototype.fetchText = function fetchText(selector) {
600
+ "use strict";
601
+ return this.evaluate(function _evaluate(selector) {
602
+ return window.__utils__.fetchText(selector);
603
+ }, { selector: selector });
604
+ };
605
+
606
+ /**
607
+ * Fills a form with provided field values.
608
+ *
609
+ * @param String selector A DOM CSS3/XPath selector to the target form to fill
610
+ * @param Object vals Field values
611
+ * @param Boolean submit Submit the form?
612
+ */
613
+ Casper.prototype.fill = function fill(selector, vals, submit) {
614
+ "use strict";
615
+ submit = submit === true ? submit : false;
616
+ if (!utils.isObject(vals)) {
617
+ throw new CasperError("Form values must be provided as an object");
618
+ }
619
+ this.emit('fill', selector, vals, submit);
620
+ var fillResults = this.evaluate(function _evaluate(selector, values) {
621
+ return window.__utils__.fill(selector, values);
622
+ }, {
623
+ selector: selector,
624
+ values: vals
625
+ });
626
+ if (!fillResults) {
627
+ throw new CasperError("Unable to fill form");
628
+ } 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
+ }
638
+ }
639
+ // File uploads
640
+ if (fillResults.files && fillResults.files.length > 0) {
641
+ 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');
644
+ } else {
645
+ (function _each(self) {
646
+ fillResults.files.forEach(function _forEach(file) {
647
+ var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
648
+ self.page.uploadFile(fileFieldSelector, file.path);
649
+ });
650
+ })(this);
651
+ }
652
+ }
653
+ // Form submission?
654
+ if (submit) {
655
+ this.evaluate(function _evaluate(selector) {
656
+ var form = window.__utils__.findOne(selector);
657
+ var method = (form.getAttribute('method') || "GET").toUpperCase();
658
+ var action = form.getAttribute('action') || "unknown";
659
+ window.__utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
660
+ if (typeof form.submit === "function") {
661
+ form.submit();
662
+ } else {
663
+ // http://www.spiration.co.uk/post/1232/Submit-is-not-a-function
664
+ form.submit.click();
665
+ }
666
+ }, { selector: selector });
667
+ }
668
+ };
669
+
670
+ /**
671
+ * Go a step forward in browser's history
672
+ *
673
+ * @return Casper
674
+ */
675
+ Casper.prototype.forward = function forward(then) {
676
+ "use strict";
677
+ return this.then(function _step() {
678
+ this.emit('forward');
679
+ this.evaluate(function _evaluate() {
680
+ history.forward();
681
+ });
682
+ });
683
+ };
684
+
685
+ /**
686
+ * Creates a new Colorizer instance. Sets `Casper.options.type` to change the
687
+ * colorizer type name (see the `colorizer` module).
688
+ *
689
+ * @return Object
690
+ */
691
+ Casper.prototype.getColorizer = function getColorizer() {
692
+ "use strict";
693
+ return colorizer.create(this.options.colorizerType || 'Colorizer');
694
+ };
695
+
696
+ /**
697
+ * Retrieves current document url.
698
+ *
699
+ * @return String
700
+ */
701
+ Casper.prototype.getCurrentUrl = function getCurrentUrl() {
702
+ "use strict";
703
+ return decodeURIComponent(this.evaluate(function _evaluate() {
704
+ return document.location.href;
705
+ }));
706
+ };
707
+
708
+ /**
709
+ * Retrieves the value of an attribute on the first element matching the provided
710
+ * DOM CSS3/XPath selector.
711
+ *
712
+ * @param String selector A DOM CSS3/XPath selector
713
+ * @param String attribute The attribute name to lookup
714
+ * @return String The requested DOM element attribute value
715
+ */
716
+ Casper.prototype.getElementAttribute = Casper.prototype.getElementAttr = function getElementAttr(selector, attribute) {
717
+ "use strict";
718
+ return this.evaluate(function _evaluate(selector, attribute) {
719
+ return document.querySelector(selector).getAttribute(attribute);
720
+ }, { selector: selector, attribute: attribute });
721
+ };
722
+
723
+ /**
724
+ * Retrieves boundaries for a DOM element matching the provided DOM CSS3/XPath selector.
725
+ *
726
+ * @param String selector A DOM CSS3/XPath selector
727
+ * @return Object
728
+ */
729
+ Casper.prototype.getElementBounds = function getElementBounds(selector) {
730
+ "use strict";
731
+ if (!this.exists(selector)) {
732
+ throw new CasperError("No element matching selector found: " + selector);
733
+ }
734
+ var clipRect = this.evaluate(function _evaluate(selector) {
735
+ return window.__utils__.getElementBounds(selector);
736
+ }, { selector: selector });
737
+ if (!utils.isClipRect(clipRect)) {
738
+ throw new CasperError('Could not fetch boundaries for element matching selector: ' + selector);
739
+ }
740
+ return clipRect;
741
+ };
742
+
743
+ /**
744
+ * Retrieves global variable.
745
+ *
746
+ * @param String name The name of the global variable to retrieve
747
+ * @return mixed
748
+ */
749
+ Casper.prototype.getGlobal = function getGlobal(name) {
750
+ "use strict";
751
+ var result = this.evaluate(function _evaluate(name) {
752
+ var result = {};
753
+ try {
754
+ result.value = JSON.stringify(window[name]);
755
+ } catch (e) {
756
+ var message = f("Unable to JSON encode window.%s: %s", name, e);
757
+ window.__utils__.log(message, "error");
758
+ result.error = message;
759
+ }
760
+ return result;
761
+ }, {'name': name});
762
+ if (typeof result !== "object") {
763
+ throw new CasperError(f('Could not retrieve global value for "%s"', name));
764
+ } else if ('error' in result) {
765
+ throw new CasperError(result.error);
766
+ } else if (utils.isString(result.value)) {
767
+ return JSON.parse(result.value);
768
+ } else {
769
+ return undefined;
770
+ }
771
+ };
772
+
773
+ /**
774
+ * Retrieves current page title, if any.
775
+ *
776
+ * @return String
777
+ */
778
+ Casper.prototype.getTitle = function getTitle() {
779
+ "use strict";
780
+ return this.evaluate(function _evaluate() {
781
+ return document.title;
782
+ });
783
+ };
784
+
785
+ /**
786
+ * Initializes PhantomJS error handler.
787
+ *
788
+ */
789
+ Casper.prototype.initErrorHandler = function initErrorHandler() {
790
+ "use strict";
791
+ var casper = this;
792
+ phantom.onError = function phantom_onError(msg, backtrace) {
793
+ casper.emit('error', msg, backtrace);
794
+ if (casper.options.exitOnError === true) {
795
+ casper.exit(1);
796
+ }
797
+ };
798
+ };
799
+
800
+ /**
801
+ * Injects Client-side utilities in current page context.
802
+ *
803
+ */
804
+ Casper.prototype.injectClientUtils = function injectClientUtils() {
805
+ "use strict";
806
+ var clientUtilsInjected = this.page.evaluate(function() {
807
+ return typeof window.__utils__ === "object";
808
+ });
809
+ if (true === clientUtilsInjected) {
810
+ return;
811
+ }
812
+ var clientUtilsPath = require('fs').pathJoin(phantom.casperPath, 'modules', 'clientutils.js');
813
+ if (true === this.page.injectJs(clientUtilsPath)) {
814
+ this.log("Successfully injected Casper client-side utilities", "debug");
815
+ } else {
816
+ this.warn("Failed to inject Casper client-side utilities");
817
+ }
818
+ // ClientUtils and Casper shares the same options
819
+ // These are not the lines I'm the most proud of in my life, but it works.
820
+ this.page.evaluate(function() {
821
+ window.__utils__ = new ClientUtils(__options);
822
+ }.toString().replace('__options', JSON.stringify(this.options)));
823
+ };
824
+
825
+ /**
826
+ * Logs a message.
827
+ *
828
+ * @param String message The message to log
829
+ * @param String level The log message level (from Casper.logLevels property)
830
+ * @param String space Space from where the logged event occured (default: "phantom")
831
+ * @return Casper
832
+ */
833
+ Casper.prototype.log = function log(message, level, space) {
834
+ "use strict";
835
+ level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
836
+ space = space ? space : "phantom";
837
+ if (level === "error" && utils.isFunction(this.options.onError)) {
838
+ this.options.onError.call(this, this, message, space);
839
+ }
840
+ if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
841
+ return this; // skip logging
842
+ }
843
+ var entry = {
844
+ level: level,
845
+ space: space,
846
+ message: message,
847
+ date: new Date().toString()
848
+ };
849
+ if (level in this.logFormats && utils.isFunction(this.logFormats[level])) {
850
+ message = this.logFormats[level](message, level, space);
851
+ } else {
852
+ var levelStr = this.colorizer.colorize(f('[%s]', level), this.logStyles[level]);
853
+ message = f('%s [%s] %s', levelStr, space, message);
854
+ }
855
+ if (this.options.verbose) {
856
+ this.echo(this.filter('log.message', message) || message); // direct output
857
+ }
858
+ this.result.log.push(entry);
859
+ this.emit('log', entry);
860
+ return this;
861
+ };
862
+
863
+ /**
864
+ * Emulates an event on the element from the provided selector using the mouse
865
+ * pointer, if possible.
866
+ *
867
+ * In case of success, `true` is returned, `false` otherwise.
868
+ *
869
+ * @param String type Type of event to emulate
870
+ * @param String selector A DOM CSS3 compatible selector
871
+ * @return Boolean
872
+ */
873
+ Casper.prototype.mouseEvent = function mouseEvent(type, selector) {
874
+ "use strict";
875
+ this.log("Mouse event '" + type + "' on selector: " + selector, "debug");
876
+ if (!this.exists(selector)) {
877
+ throw new CasperError(f("Cannot dispatch %s event on nonexistent selector: %s", type, selector));
878
+ }
879
+ var eventSuccess = this.evaluate(function(type, selector) {
880
+ 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
+ }
893
+ }
894
+ return true;
895
+ };
896
+
897
+ /**
898
+ * Performs an HTTP request, with optional settings.
899
+ *
900
+ * Available settings are:
901
+ *
902
+ * - String method: The HTTP method to use
903
+ * - 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'}]
905
+ *
906
+ * @param String location The url to open
907
+ * @param Object settings The request settings (optional)
908
+ * @return Casper
909
+ */
910
+ Casper.prototype.open = function open(location, settings) {
911
+ "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
+ }
921
+ // http method
922
+ // taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302
923
+ var methods = ["get", "head", "put", "post", "delete"];
924
+ if (settings.method && (!utils.isString(settings.method) || methods.indexOf(settings.method) === -1)) {
925
+ throw new CasperError("open(): settings.method must be part of " + methods.join(', '));
926
+ }
927
+ // http data
928
+ if (settings.data) {
929
+ if (utils.isObject(settings.data)) { // query object
930
+ settings.data = qs.encode(settings.data);
931
+ } else if (!utils.isString(settings.data)) {
932
+ throw new CasperError("open(): invalid request settings data value: " + settings.data);
933
+ }
934
+ }
935
+ // current request url
936
+ 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
+ this.emit('open', this.requestUrl, settings);
951
+ 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
+ }
955
+ this.page.openUrl(this.requestUrl, {
956
+ operation: settings.method,
957
+ data: settings.data,
958
+ headers: settings.headers
959
+ }, this.page.settings);
960
+ this.resources = [];
961
+ return this;
962
+ };
963
+
964
+ /**
965
+ * Reloads current page.
966
+ *
967
+ * @param Function then a next step function
968
+ * @return Casper
969
+ */
970
+ Casper.prototype.reload = function reload(then) {
971
+ "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();
977
+ });
978
+ if (then && utils.isFunction(then)) {
979
+ this.then(this.createStep(then));
980
+ }
981
+ return this;
982
+ };
983
+
984
+ /**
985
+ * Repeats a step a given number of times.
986
+ *
987
+ * @param Number times Number of times to repeat step
988
+ * @aram function then The step closure
989
+ * @return Casper
990
+ * @see Casper#then
991
+ */
992
+ Casper.prototype.repeat = function repeat(times, then) {
993
+ "use strict";
994
+ for (var i = 0; i < times; i++) {
995
+ this.then(then);
996
+ }
997
+ return this;
998
+ };
999
+
1000
+ /**
1001
+ * Checks if a given resource was loaded by the remote page.
1002
+ *
1003
+ * @param Function/String/RegExp test A test function, string or regular expression.
1004
+ * In case a string is passed, url matching will be tested.
1005
+ * @return Boolean
1006
+ */
1007
+ Casper.prototype.resourceExists = function resourceExists(test) {
1008
+ "use strict";
1009
+ var testFn;
1010
+ switch (utils.betterTypeOf(test)) {
1011
+ case "string":
1012
+ testFn = function _testResourceExists_String(res) {
1013
+ return res.url.search(test) !== -1;
1014
+ };
1015
+ break;
1016
+ case "regexp":
1017
+ testFn = function _testResourceExists_Regexp(res) {
1018
+ return test.test(res.url);
1019
+ };
1020
+ break;
1021
+ case "function":
1022
+ testFn = test;
1023
+ testFn.name = "_testResourceExists_Function";
1024
+ break;
1025
+ default:
1026
+ throw new CasperError("Invalid type");
1027
+ }
1028
+ return this.resources.some(testFn);
1029
+ };
1030
+
1031
+ /**
1032
+ * Runs the whole suite of steps.
1033
+ *
1034
+ * @param function onComplete an optional callback
1035
+ * @param Number time an optional amount of milliseconds for interval checking
1036
+ * @return Casper
1037
+ */
1038
+ Casper.prototype.run = function run(onComplete, time) {
1039
+ "use strict";
1040
+ if (!this.steps || this.steps.length < 1) {
1041
+ this.log("No steps defined, aborting", "error");
1042
+ return this;
1043
+ }
1044
+ this.log(f("Running suite: %d step%s", this.steps.length, this.steps.length > 1 ? "s" : ""), "info");
1045
+ this.emit('run.start');
1046
+ this.checker = setInterval(this.checkStep, (time ? time: 100), this, onComplete);
1047
+ return this;
1048
+ };
1049
+
1050
+ /**
1051
+ * Runs a step.
1052
+ *
1053
+ * @param Function step
1054
+ */
1055
+ Casper.prototype.runStep = function runStep(step) {
1056
+ "use strict";
1057
+ var skipLog = utils.isObject(step.options) && step.options.skipLog === true;
1058
+ var stepInfo = f("Step %d/%d", this.step, this.steps.length);
1059
+ var stepResult;
1060
+ if (!skipLog) {
1061
+ this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info");
1062
+ }
1063
+ if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
1064
+ var stepTimeoutCheckInterval = setInterval(function _check(self, start, stepNum) {
1065
+ if (new Date().getTime() - start > self.options.stepTimeout) {
1066
+ if (self.step === stepNum) {
1067
+ self.emit('step.timeout');
1068
+ 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");
1072
+ }
1073
+ }
1074
+ clearInterval(stepTimeoutCheckInterval);
1075
+ }
1076
+ }, this.options.stepTimeout, this, new Date().getTime(), this.step);
1077
+ }
1078
+ this.emit('step.start', step);
1079
+ stepResult = step.call(this, this);
1080
+ if (utils.isFunction(this.options.onStepComplete)) {
1081
+ this.options.onStepComplete.call(this, this, stepResult);
1082
+ }
1083
+ if (!skipLog) {
1084
+ this.emit('step.complete', stepResult);
1085
+ this.log(stepInfo + f(": done in %dms.", new Date().getTime() - this.startTime), "info");
1086
+ }
1087
+ };
1088
+
1089
+ /**
1090
+ * Sets HTTP authentication parameters.
1091
+ *
1092
+ * @param String username The HTTP_AUTH_USER value
1093
+ * @param String password The HTTP_AUTH_PW value
1094
+ * @return Casper
1095
+ */
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");
1100
+ }
1101
+ if (!utils.isString(username) || !utils.isString(password)) {
1102
+ throw new CasperError("Both username and password must be strings");
1103
+ }
1104
+ this.page.settings.userName = username;
1105
+ this.page.settings.password = password;
1106
+ this.emit('http.auth', username, password);
1107
+ this.log("Setting HTTP authentication for user " + username, "info");
1108
+ return this;
1109
+ };
1110
+
1111
+ /**
1112
+ * Configures and starts Casper.
1113
+ *
1114
+ * @param String location An optional location to open on start
1115
+ * @param function then Next step function to execute on page loaded (optional)
1116
+ * @return Casper
1117
+ */
1118
+ Casper.prototype.start = function start(location, then) {
1119
+ "use strict";
1120
+ this.emit('starting');
1121
+ this.log('Starting...', "info");
1122
+ this.startTime = new Date().getTime();
1123
+ this.history = [];
1124
+ this.steps = [];
1125
+ this.step = 0;
1126
+ // Option checks
1127
+ if (this.logLevels.indexOf(this.options.logLevel) < 0) {
1128
+ this.log(f("Unknown log level '%d', defaulting to 'warning'", this.options.logLevel), "warning");
1129
+ this.options.logLevel = "warning";
1130
+ }
1131
+ // WebPage
1132
+ 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
+ }
1138
+ }
1139
+ this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings);
1140
+ if (utils.isClipRect(this.options.clipRect)) {
1141
+ this.page.clipRect = this.options.clipRect;
1142
+ }
1143
+ if (utils.isObject(this.options.viewportSize)) {
1144
+ this.page.viewportSize = this.options.viewportSize;
1145
+ }
1146
+ this.started = true;
1147
+ this.emit('started');
1148
+ if (utils.isNumber(this.options.timeout) && this.options.timeout > 0) {
1149
+ this.log(f("Execution timeout set to %dms", this.options.timeout), "info");
1150
+ setTimeout(function _check(self) {
1151
+ self.emit('timeout');
1152
+ 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));
1156
+ }
1157
+ }, this.options.timeout, this);
1158
+ }
1159
+ if (utils.isString(location) && location.length > 0) {
1160
+ return this.thenOpen(location, utils.isFunction(then) ? then : this.createStep(function _step() {
1161
+ this.log("start page is loaded", "debug");
1162
+ }));
1163
+ }
1164
+ return this;
1165
+ };
1166
+
1167
+ /**
1168
+ * Schedules the next step in the navigation process.
1169
+ *
1170
+ * @param function step A function to be called as a step
1171
+ * @return Casper
1172
+ */
1173
+ Casper.prototype.then = function then(step) {
1174
+ "use strict";
1175
+ if (!this.started) {
1176
+ throw new CasperError("Casper not started; please use Casper#start");
1177
+ }
1178
+ if (!utils.isFunction(step)) {
1179
+ throw new CasperError("You can only define a step as a function");
1180
+ }
1181
+ // check if casper is running
1182
+ if (this.checker === null) {
1183
+ // append step to the end of the queue
1184
+ step.level = 0;
1185
+ this.steps.push(step);
1186
+ } else {
1187
+ // insert substep a level deeper
1188
+ try {
1189
+ step.level = this.steps[this.step - 1].level + 1;
1190
+ } catch (e) {
1191
+ step.level = 0;
1192
+ }
1193
+ var insertIndex = this.step;
1194
+ while (this.steps[insertIndex] && step.level === this.steps[insertIndex].level) {
1195
+ insertIndex++;
1196
+ }
1197
+ this.steps.splice(insertIndex, 0, step);
1198
+ }
1199
+ this.emit('step.added', step);
1200
+ return this;
1201
+ };
1202
+
1203
+ /**
1204
+ * Adds a new navigation step for clicking on a provided link selector
1205
+ * and execute an optional next step.
1206
+ *
1207
+ * @param String selector A DOM CSS3 compatible selector
1208
+ * @param Function then Next step function to execute on page loaded (optional)
1209
+ * @return Casper
1210
+ * @see Casper#click
1211
+ * @see Casper#then
1212
+ */
1213
+ Casper.prototype.thenClick = function thenClick(selector, then, fallbackToHref) {
1214
+ "use strict";
1215
+ if (arguments.length > 2) {
1216
+ this.emit("deprecated", "The thenClick() method does not process the fallbackToHref argument since 0.6");
1217
+ }
1218
+ this.then(function _step() {
1219
+ this.click(selector);
1220
+ });
1221
+ return utils.isFunction(then) ? this.then(then) : this;
1222
+ };
1223
+
1224
+ /**
1225
+ * Adds a new navigation step to perform code evaluation within the
1226
+ * current retrieved page DOM.
1227
+ *
1228
+ * @param function fn The function to be evaluated within current page DOM
1229
+ * @param object context Optional function parameters context
1230
+ * @return Casper
1231
+ * @see Casper#evaluate
1232
+ */
1233
+ Casper.prototype.thenEvaluate = function thenEvaluate(fn, context) {
1234
+ "use strict";
1235
+ return this.then(function _step() {
1236
+ this.evaluate(fn, context);
1237
+ });
1238
+ };
1239
+
1240
+ /**
1241
+ * Adds a new navigation step for opening the provided location.
1242
+ *
1243
+ * @param String location The URL to load
1244
+ * @param function then Next step function to execute on page loaded (optional)
1245
+ * @return Casper
1246
+ * @see Casper#open
1247
+ */
1248
+ Casper.prototype.thenOpen = function thenOpen(location, then) {
1249
+ "use strict";
1250
+ this.then(this.createStep(function _step() {
1251
+ this.open(location);
1252
+ }, {
1253
+ skipLog: true
1254
+ }));
1255
+ return utils.isFunction(then) ? this.then(then) : this;
1256
+ };
1257
+
1258
+ /**
1259
+ * Adds a new navigation step for opening and evaluate an expression
1260
+ * against the DOM retrieved from the provided location.
1261
+ *
1262
+ * @param String location The url to open
1263
+ * @param function fn The function to be evaluated within current page DOM
1264
+ * @param object context Optional function parameters context
1265
+ * @return Casper
1266
+ * @see Casper#evaluate
1267
+ * @see Casper#open
1268
+ */
1269
+ Casper.prototype.thenOpenAndEvaluate = function thenOpenAndEvaluate(location, fn, context) {
1270
+ "use strict";
1271
+ return this.thenOpen(location).thenEvaluate(fn, context);
1272
+ };
1273
+
1274
+ /**
1275
+ * Sets the user-agent string currently used when requesting urls.
1276
+ *
1277
+ * @param String userAgent User agent string
1278
+ * @return String
1279
+ */
1280
+ Casper.prototype.userAgent = function userAgent(agent) {
1281
+ "use strict";
1282
+ if (!this.started) {
1283
+ throw new CasperError("Casper not started, can't set userAgent");
1284
+ }
1285
+ this.options.pageSettings.userAgent = this.page.settings.userAgent = agent;
1286
+ return this;
1287
+ };
1288
+
1289
+ /**
1290
+ * Changes the current viewport size.
1291
+ *
1292
+ * @param Number width The viewport width, in pixels
1293
+ * @param Number height The viewport height, in pixels
1294
+ * @return Casper
1295
+ */
1296
+ Casper.prototype.viewport = function viewport(width, height) {
1297
+ "use strict";
1298
+ if (!this.started) {
1299
+ throw new CasperError("Casper must be started in order to set viewport at runtime");
1300
+ }
1301
+ if (!utils.isNumber(width) || !utils.isNumber(height) || width <= 0 || height <= 0) {
1302
+ throw new CasperError(f("Invalid viewport: %dx%d", width, height));
1303
+ }
1304
+ this.page.viewportSize = {
1305
+ width: width,
1306
+ height: height
1307
+ };
1308
+ this.emit('viewport.changed', [width, height]);
1309
+ return this;
1310
+ };
1311
+
1312
+ /**
1313
+ * Checks if an element matching the provided DOM CSS3/XPath selector is visible
1314
+ * current page DOM by checking that offsetWidth and offsetHeight are
1315
+ * both non-zero.
1316
+ *
1317
+ * @param String selector A DOM CSS3/XPath selector
1318
+ * @return Boolean
1319
+ */
1320
+ Casper.prototype.visible = function visible(selector) {
1321
+ "use strict";
1322
+ return this.evaluate(function _evaluate(selector) {
1323
+ return window.__utils__.visible(selector);
1324
+ }, { selector: selector });
1325
+ };
1326
+
1327
+ /**
1328
+ * Displays a warning message onto the console and logs the event.
1329
+ *
1330
+ * @param String message
1331
+ * @return Casper
1332
+ */
1333
+ Casper.prototype.warn = function warn(message) {
1334
+ "use strict";
1335
+ this.log(message, "warning", "phantom");
1336
+ var formatted = f.apply(null, ["⚠  " + message].concat([].slice.call(arguments, 1)));
1337
+ return this.echo(formatted, 'COMMENT');
1338
+ };
1339
+
1340
+ /**
1341
+ * Adds a new step that will wait for a given amount of time (expressed
1342
+ * in milliseconds) before processing an optional next one.
1343
+ *
1344
+ * @param Number timeout The max amount of time to wait, in milliseconds
1345
+ * @param Function then Next step to process (optional)
1346
+ * @return Casper
1347
+ */
1348
+ Casper.prototype.wait = function wait(timeout, then) {
1349
+ "use strict";
1350
+ timeout = ~~timeout;
1351
+ if (timeout < 1) {
1352
+ this.die("wait() only accepts a positive integer > 0 as a timeout value");
1353
+ }
1354
+ if (then && !utils.isFunction(then)) {
1355
+ this.die("wait() a step definition must be a function");
1356
+ }
1357
+ return this.then(function _step() {
1358
+ this.waitStart();
1359
+ setTimeout(function _check(self) {
1360
+ self.log(f("wait() finished waiting for %dms.", timeout), "info");
1361
+ if (then) {
1362
+ then.call(self, self);
1363
+ }
1364
+ self.waitDone();
1365
+ }, timeout, this);
1366
+ });
1367
+ };
1368
+
1369
+ Casper.prototype.waitStart = function waitStart() {
1370
+ "use strict";
1371
+ this.emit('wait.start');
1372
+ this.pendingWait = true;
1373
+ };
1374
+
1375
+ Casper.prototype.waitDone = function waitDone() {
1376
+ "use strict";
1377
+ this.emit('wait.done');
1378
+ this.pendingWait = false;
1379
+ };
1380
+
1381
+ /**
1382
+ * Waits until a function returns true to process a next step.
1383
+ *
1384
+ * @param Function testFx A function to be evaluated for returning condition satisfecit
1385
+ * @param Function then The next step to perform (optional)
1386
+ * @param Function onTimeout A callback function to call on timeout (optional)
1387
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1388
+ * @return Casper
1389
+ */
1390
+ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1391
+ "use strict";
1392
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1393
+ if (!utils.isFunction(testFx)) {
1394
+ this.die("waitFor() needs a test function");
1395
+ }
1396
+ if (then && !utils.isFunction(then)) {
1397
+ this.die("waitFor() next step definition must be a function");
1398
+ }
1399
+ return this.then(function _step() {
1400
+ this.waitStart();
1401
+ var start = new Date().getTime();
1402
+ var condition = false;
1403
+ var interval = setInterval(function _check(self, testFx, timeout, onTimeout) {
1404
+ if ((new Date().getTime() - start < timeout) && !condition) {
1405
+ condition = testFx.call(self, self);
1406
+ } else {
1407
+ self.waitDone();
1408
+ if (!condition) {
1409
+ self.log("Casper.waitFor() timeout", "warning");
1410
+ 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");
1415
+ }
1416
+ } else {
1417
+ self.log(f("waitFor() finished in %dms.", new Date().getTime() - start), "info");
1418
+ if (then) {
1419
+ self.then(then);
1420
+ }
1421
+ }
1422
+ clearInterval(interval);
1423
+ }
1424
+ }, 100, this, testFx, timeout, onTimeout);
1425
+ });
1426
+ };
1427
+
1428
+ /**
1429
+ * Waits until a given resource is loaded
1430
+ *
1431
+ * @param String/Function test A function to test if the resource exists.
1432
+ * A string will be matched against the resources url.
1433
+ * @param Function then The next step to perform (optional)
1434
+ * @param Function onTimeout A callback function to call on timeout (optional)
1435
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1436
+ * @return Casper
1437
+ */
1438
+ Casper.prototype.waitForResource = function waitForResource(test, then, onTimeout, timeout) {
1439
+ "use strict";
1440
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1441
+ return this.waitFor(function _check() {
1442
+ return this.resourceExists(test);
1443
+ }, then, onTimeout, timeout);
1444
+ };
1445
+
1446
+ /**
1447
+ * Waits until an element matching the provided DOM CSS3/XPath selector exists in
1448
+ * remote DOM to process a next step.
1449
+ *
1450
+ * @param String selector A DOM CSS3/XPath selector
1451
+ * @param Function then The next step to perform (optional)
1452
+ * @param Function onTimeout A callback function to call on timeout (optional)
1453
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1454
+ * @return Casper
1455
+ */
1456
+ Casper.prototype.waitForSelector = function waitForSelector(selector, then, onTimeout, timeout) {
1457
+ "use strict";
1458
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1459
+ return this.waitFor(function _check() {
1460
+ return this.exists(selector);
1461
+ }, then, onTimeout, timeout);
1462
+ };
1463
+
1464
+ /**
1465
+ * Waits until an element matching the provided DOM CSS3/XPath selector does not
1466
+ * exist in the remote DOM to process a next step.
1467
+ *
1468
+ * @param String selector A DOM CSS3/XPath selector
1469
+ * @param Function then The next step to perform (optional)
1470
+ * @param Function onTimeout A callback function to call on timeout (optional)
1471
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1472
+ * @return Casper
1473
+ */
1474
+ Casper.prototype.waitWhileSelector = function waitWhileSelector(selector, then, onTimeout, timeout) {
1475
+ "use strict";
1476
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1477
+ return this.waitFor(function _check() {
1478
+ return !this.exists(selector);
1479
+ }, then, onTimeout, timeout);
1480
+ };
1481
+
1482
+ /**
1483
+ * Waits until an element matching the provided DOM CSS3/XPath selector is
1484
+ * visible in the remote DOM to process a next step.
1485
+ *
1486
+ * @param String selector A DOM CSS3/XPath selector
1487
+ * @param Function then The next step to perform (optional)
1488
+ * @param Function onTimeout A callback function to call on timeout (optional)
1489
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1490
+ * @return Casper
1491
+ */
1492
+ Casper.prototype.waitUntilVisible = function waitUntilVisible(selector, then, onTimeout, timeout) {
1493
+ "use strict";
1494
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1495
+ return this.waitFor(function _check() {
1496
+ return this.visible(selector);
1497
+ }, then, onTimeout, timeout);
1498
+ };
1499
+
1500
+ /**
1501
+ * Waits until an element matching the provided DOM CSS3/XPath selector is no
1502
+ * longer visible in remote DOM to process a next step.
1503
+ *
1504
+ * @param String selector A DOM CSS3/XPath selector
1505
+ * @param Function then The next step to perform (optional)
1506
+ * @param Function onTimeout A callback function to call on timeout (optional)
1507
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1508
+ * @return Casper
1509
+ */
1510
+ Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, onTimeout, timeout) {
1511
+ "use strict";
1512
+ timeout = timeout ? timeout : this.defaultWaitTimeout;
1513
+ return this.waitFor(function _check() {
1514
+ return !this.visible(selector);
1515
+ }, then, onTimeout, timeout);
1516
+ };
1517
+
1518
+ /**
1519
+ * Changes the current page zoom factor.
1520
+ *
1521
+ * @param Number factor The zoom factor
1522
+ * @return Casper
1523
+ */
1524
+ Casper.prototype.zoom = function zoom(factor) {
1525
+ "use strict";
1526
+ if (!this.started) {
1527
+ throw new CasperError("Casper has not been started, can't set zoom factor");
1528
+ }
1529
+ if (!utils.isNumber(factor) || factor <= 0) {
1530
+ throw new CasperError("Invalid zoom factor: " + factor);
1531
+ }
1532
+ if ('zoomFactor' in this.page) {
1533
+ this.page.zoomFactor = factor;
1534
+ } else {
1535
+ this.warn("zoom() requires PhantomJS >= 1.6");
1536
+ }
1537
+ return this;
1538
+ };
1539
+
1540
+ /**
1541
+ * Extends Casper's prototype with provided one.
1542
+ *
1543
+ * @param Object proto Prototype methods to add to Casper
1544
+ * @deprecated
1545
+ * @since 0.6
1546
+ */
1547
+ Casper.extend = function(proto) {
1548
+ "use strict";
1549
+ this.warn('Casper.extend() has been deprecated since 0.6; check the docs');
1550
+ if (!utils.isObject(proto)) {
1551
+ throw new CasperError("extends() only accept objects as prototypes");
1552
+ }
1553
+ utils.mergeObjects(Casper.prototype, proto);
1554
+ };
1555
+
1556
+ exports.Casper = Casper;
1557
+
1558
+ /**
1559
+ * Creates a new WebPage instance for Casper use.
1560
+ *
1561
+ * @param Casper casper A Casper instance
1562
+ * @return WebPage
1563
+ */
1564
+ function createPage(casper) {
1565
+ "use strict";
1566
+ var page = require('webpage').create();
1567
+ page.onAlert = function onAlert(message) {
1568
+ casper.log('[alert] ' + message, "info", "remote");
1569
+ casper.emit('remote.alert', message);
1570
+ if (utils.isFunction(casper.options.onAlert)) {
1571
+ casper.options.onAlert.call(casper, casper, message);
1572
+ }
1573
+ };
1574
+ page.onConfirm = function onConfirm(message) {
1575
+ return casper.filter('page.confirm', message) || true;
1576
+ };
1577
+ 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];
1582
+ }
1583
+ casper.log(msg, level, "remote");
1584
+ casper.emit('remote.message', msg);
1585
+ };
1586
+ page.onError = function onError(msg, trace) {
1587
+ casper.emit('page.error', msg, trace);
1588
+ };
1589
+ page.onInitialized = function onInitialized() {
1590
+ casper.emit('page.initialized', this);
1591
+ if (utils.isFunction(casper.options.onPageInitialized)) {
1592
+ this.log("Post-configuring WebPage instance", "debug");
1593
+ casper.options.onPageInitialized.call(casper, page);
1594
+ }
1595
+ };
1596
+ page.onLoadStarted = function onLoadStarted() {
1597
+ casper.loadInProgress = true;
1598
+ casper.emit('load.started');
1599
+ };
1600
+ page.onLoadFinished = function onLoadFinished(status) {
1601
+ if (status !== "success") {
1602
+ casper.emit('load.failed', {
1603
+ status: status,
1604
+ http_status: casper.currentHTTPStatus,
1605
+ url: casper.requestUrl
1606
+ });
1607
+ var message = 'Loading resource failed with status=' + status;
1608
+ if (casper.currentHTTPStatus) {
1609
+ message += f(' (HTTP %d)', casper.currentHTTPStatus);
1610
+ }
1611
+ message += ': ' + casper.requestUrl;
1612
+ casper.log(message, "warning");
1613
+ if (utils.isFunction(casper.options.onLoadError)) {
1614
+ casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
1615
+ }
1616
+ }
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
+ }
1632
+ // Client-side utils injection
1633
+ casper.injectClientUtils();
1634
+ // history
1635
+ casper.history.push(casper.getCurrentUrl());
1636
+ casper.emit('load.finished', status);
1637
+ casper.loadInProgress = false;
1638
+ };
1639
+ page.onNavigationRequested = function onNavigationRequested(url, navigationType, navigationLocked, isMainFrame) {
1640
+ casper.log(f('Navigation requested: url=%s, type=%s, lock=%s, isMainFrame=%s',
1641
+ url, navigationType, navigationLocked, isMainFrame), "debug");
1642
+ casper.emit('navigation.requested', url, navigationType, navigationLocked, isMainFrame);
1643
+ };
1644
+ page.onPrompt = function onPrompt(message, value) {
1645
+ return casper.filter('page.prompt', message, value);
1646
+ };
1647
+ page.onResourceReceived = function onResourceReceived(resource) {
1648
+ casper.emit('resource.received', resource);
1649
+ if (utils.isFunction(casper.options.onResourceReceived)) {
1650
+ casper.options.onResourceReceived.call(casper, casper, resource);
1651
+ }
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
+ }
1666
+ };
1667
+ page.onResourceRequested = function onResourceRequested(request) {
1668
+ casper.emit('resource.requested', request);
1669
+ if (utils.isFunction(casper.options.onResourceRequested)) {
1670
+ casper.options.onResourceRequested.call(casper, casper, request);
1671
+ }
1672
+ };
1673
+ page.onUrlChanged = function onUrlChanged(url) {
1674
+ casper.log(f('url changed to "%s"', url), "debug");
1675
+ casper.emit('url.changed', url);
1676
+ };
1677
+ casper.emit('page.created', page);
1678
+ return page;
1679
+ }