casperjs 1.0.0.RC1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }