phantomjs-binaries 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/modules/casper.js ADDED
@@ -0,0 +1,2101 @@
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 __utils__*/
32
+
33
+ var colorizer = require('colorizer');
34
+ var events = require('events');
35
+ var fs = require('fs');
36
+ var http = require('http');
37
+ var mouse = require('mouse');
38
+ var pagestack = require('pagestack');
39
+ var qs = require('querystring');
40
+ var tester = require('tester');
41
+ var utils = require('utils');
42
+ var f = utils.format;
43
+
44
+
45
+ var defaultUserAgent = phantom.defaultPageSettings.userAgent
46
+ .replace('PhantomJS', f("CasperJS/%s", phantom.casperVersion) + '+Phantomjs');
47
+
48
+ exports.create = function create(options) {
49
+ "use strict";
50
+ return new Casper(options);
51
+ };
52
+
53
+ /**
54
+ * Shortcut to build an XPath selector object.
55
+ *
56
+ * @param String expression The XPath expression
57
+ * @return Object
58
+ * @see http://casperjs.org/selectors.html
59
+ */
60
+ function selectXPath(expression) {
61
+ "use strict";
62
+ return {
63
+ type: 'xpath',
64
+ path: expression,
65
+ toString: function() {
66
+ return this.type + ' selector: ' + this.path;
67
+ }
68
+ };
69
+ }
70
+ exports.selectXPath = selectXPath;
71
+
72
+ /**
73
+ * Main Casper object.
74
+ *
75
+ * @param Object options Casper options
76
+ */
77
+ var Casper = function Casper(options) {
78
+ "use strict";
79
+ /*jshint maxstatements:40*/
80
+ // init & checks
81
+ if (!(this instanceof Casper)) {
82
+ return new Casper(options);
83
+ }
84
+ // default options
85
+ this.defaults = {
86
+ clientScripts: [],
87
+ colorizerType: 'Colorizer',
88
+ exitOnError: true,
89
+ logLevel: "error",
90
+ httpStatusHandlers: {},
91
+ safeLogs: true,
92
+ onAlert: null,
93
+ onDie: null,
94
+ onError: null,
95
+ onLoadError: null,
96
+ onPageInitialized: null,
97
+ onResourceReceived: null,
98
+ onResourceRequested: null,
99
+ onStepComplete: null,
100
+ onStepTimeout: function _onStepTimeout(timeout, stepNum) {
101
+ this.die("Maximum step execution timeout exceeded for step " + stepNum);
102
+ },
103
+ onTimeout: function _onTimeout(timeout) {
104
+ this.die(f("Script timeout of %dms reached, exiting.", timeout));
105
+ },
106
+ onWaitTimeout: function _onWaitTimeout(timeout) {
107
+ this.die(f("Wait timeout of %dms expired, exiting.", timeout));
108
+ },
109
+ page: null,
110
+ pageSettings: {
111
+ localToRemoteUrlAccessEnabled: true,
112
+ userAgent: defaultUserAgent
113
+ },
114
+ remoteScripts: [],
115
+ stepTimeout: null,
116
+ timeout: null,
117
+ verbose: false,
118
+ waitTimeout: 5000
119
+ };
120
+ // options
121
+ this.options = utils.mergeObjects(this.defaults, options);
122
+ // properties
123
+ this.checker = null;
124
+ this.cli = phantom.casperArgs;
125
+ this.colorizer = this.getColorizer();
126
+ this.currentResponse = undefined;
127
+ this.currentUrl = 'about:blank';
128
+ this.currentHTTPStatus = null;
129
+ this.history = [];
130
+ this.loadInProgress = false;
131
+ this.navigationRequested = false;
132
+ this.logFormats = {};
133
+ this.logLevels = ["debug", "info", "warning", "error"];
134
+ this.logStyles = {
135
+ debug: 'INFO',
136
+ info: 'PARAMETER',
137
+ warning: 'COMMENT',
138
+ error: 'ERROR'
139
+ };
140
+ this.mouse = mouse.create(this);
141
+ this.page = null;
142
+ this.pendingWait = false;
143
+ this.popups = pagestack.create();
144
+ this.requestUrl = 'about:blank';
145
+ this.resources = [];
146
+ this.result = {
147
+ log: [],
148
+ status: "success",
149
+ time: 0
150
+ };
151
+ this.started = false;
152
+ this.step = -1;
153
+ this.steps = [];
154
+ this.test = tester.create(this);
155
+
156
+ // init phantomjs error handler
157
+ this.initErrorHandler();
158
+
159
+ this.on('error', function(msg, backtrace) {
160
+ if (msg === this.test.SKIP_MESSAGE) {
161
+ return;
162
+ }
163
+ var c = this.getColorizer();
164
+ var match = /^(.*): __mod_error(.*):: (.*)/.exec(msg);
165
+ var notices = [];
166
+ if (match && match.length === 4) {
167
+ notices.push(' in module ' + match[2]);
168
+ msg = match[3];
169
+ }
170
+ console.error(c.colorize(msg, 'RED_BAR', 80));
171
+ notices.forEach(function(notice) {
172
+ console.error(c.colorize(notice, 'COMMENT'));
173
+ });
174
+ backtrace.forEach(function(item) {
175
+ var message = fs.absolute(item.file) + ":" + c.colorize(item.line, "COMMENT");
176
+ if (item['function']) {
177
+ message += " in " + c.colorize(item['function'], "PARAMETER");
178
+ }
179
+ console.error(" " + message);
180
+ });
181
+ });
182
+
183
+ // deprecated feature event handler
184
+ this.on('deprecated', function onDeprecated(message) {
185
+ this.warn('[deprecated] ' + message);
186
+ });
187
+
188
+ // dispatching an event when instance has been constructed
189
+ this.emit('init');
190
+ };
191
+
192
+ // Casper class is an EventEmitter
193
+ utils.inherits(Casper, events.EventEmitter);
194
+
195
+ /**
196
+ * Go a step back in browser's history
197
+ *
198
+ * @return Casper
199
+ */
200
+ Casper.prototype.back = function back() {
201
+ "use strict";
202
+ this.checkStarted();
203
+ return this.then(function _step() {
204
+ this.emit('back');
205
+ this.evaluate(function _evaluate() {
206
+ history.back();
207
+ });
208
+ });
209
+ };
210
+
211
+ /**
212
+ * Encodes a resource using the base64 algorithm synchronously using
213
+ * client-side XMLHttpRequest.
214
+ *
215
+ * NOTE: we cannot use window.btoa() for some strange reasons here.
216
+ *
217
+ * @param String url The url to download
218
+ * @param String method The method to use, optional: default GET
219
+ * @param String data The data to send, optional
220
+ * @return string Base64 encoded result
221
+ */
222
+ Casper.prototype.base64encode = function base64encode(url, method, data) {
223
+ "use strict";
224
+ return this.evaluate(function _evaluate(url, method, data) {
225
+ return __utils__.getBase64(url, method, data);
226
+ }, url, method, data);
227
+ };
228
+
229
+ /**
230
+ * Proxy method for WebPage#render. Adds a clipRect parameter for
231
+ * automatically set page clipRect setting values and sets it back once
232
+ * done. If the cliprect parameter is omitted, the full page viewport
233
+ * area will be rendered.
234
+ *
235
+ * @param String targetFile A target filename
236
+ * @param mixed clipRect An optional clipRect object (optional)
237
+ * @return Casper
238
+ */
239
+ Casper.prototype.capture = function capture(targetFile, clipRect) {
240
+ "use strict";
241
+ /*jshint maxstatements:20*/
242
+ this.checkStarted();
243
+ var previousClipRect;
244
+ targetFile = fs.absolute(targetFile);
245
+ if (clipRect) {
246
+ if (!utils.isClipRect(clipRect)) {
247
+ throw new CasperError("clipRect must be a valid ClipRect object.");
248
+ }
249
+ previousClipRect = this.page.clipRect;
250
+ this.page.clipRect = clipRect;
251
+ this.log(f("Capturing page to %s with clipRect %s", targetFile, JSON.stringify(clipRect)), "debug");
252
+ } else {
253
+ this.log(f("Capturing page to %s", targetFile), "debug");
254
+ }
255
+ if (!this.page.render(this.filter('capture.target_filename', targetFile) || targetFile)) {
256
+ this.log(f("Failed to save screenshot to %s; please check permissions", targetFile), "error");
257
+ } else {
258
+ this.log(f("Capture saved to %s", targetFile), "info");
259
+ this.emit('capture.saved', targetFile);
260
+ }
261
+ if (previousClipRect) {
262
+ this.page.clipRect = previousClipRect;
263
+ }
264
+ return this;
265
+ };
266
+
267
+ /**
268
+ * Returns a Base64 representation of a binary image capture of the current
269
+ * page, or an area within the page, in a given format.
270
+ *
271
+ * Supported image formats are `bmp`, `jpg`, `jpeg`, `png`, `ppm`, `tiff`,
272
+ * `xbm` and `xpm`.
273
+ *
274
+ * @param String format The image format
275
+ * @param String|Object|undefined selector DOM CSS3/XPath selector or clipRect object (optional)
276
+ * @return Casper
277
+ */
278
+ Casper.prototype.captureBase64 = function captureBase64(format, area) {
279
+ "use strict";
280
+ /*jshint maxstatements:20*/
281
+ this.checkStarted();
282
+ var base64, previousClipRect, formats = ['bmp', 'jpg', 'jpeg', 'png', 'ppm', 'tiff', 'xbm', 'xpm'];
283
+ if (formats.indexOf(format.toLowerCase()) === -1) {
284
+ throw new CasperError(f('Unsupported format "%s"', format));
285
+ }
286
+ if (utils.isClipRect(area)) {
287
+ // if area is a clipRect object
288
+ this.log(f("Capturing base64 %s representation of %s", format, utils.serialize(area)), "debug");
289
+ previousClipRect = this.page.clipRect;
290
+ this.page.clipRect = area;
291
+ base64 = this.page.renderBase64(format);
292
+ } else if (utils.isValidSelector(area)) {
293
+ // if area is a selector string or object
294
+ this.log(f("Capturing base64 %s representation of %s", format, area), "debug");
295
+ base64 = this.captureBase64(format, this.getElementBounds(area));
296
+ } else {
297
+ // whole page capture
298
+ this.log(f("Capturing base64 %s representation of page", format), "debug");
299
+ base64 = this.page.renderBase64(format);
300
+ }
301
+ if (previousClipRect) {
302
+ this.page.clipRect = previousClipRect;
303
+ }
304
+ return base64;
305
+ };
306
+
307
+ /**
308
+ * Captures the page area matching the provided selector.
309
+ *
310
+ * @param String targetFile Target destination file path.
311
+ * @param String selector DOM CSS3/XPath selector
312
+ * @return Casper
313
+ */
314
+ Casper.prototype.captureSelector = function captureSelector(targetFile, selector) {
315
+ "use strict";
316
+ return this.capture(targetFile, this.getElementBounds(selector));
317
+ };
318
+
319
+ /**
320
+ * Checks for any further navigation step to process.
321
+ *
322
+ * @param Casper self A self reference
323
+ * @param function onComplete An options callback to apply on completion
324
+ */
325
+ Casper.prototype.checkStep = function checkStep(self, onComplete) {
326
+ "use strict";
327
+ if (self.pendingWait || self.loadInProgress || self.navigationRequested) {
328
+ return;
329
+ }
330
+ var step = self.steps[self.step++];
331
+ if (utils.isFunction(step)) {
332
+ self.runStep(step);
333
+ } else {
334
+ self.result.time = new Date().getTime() - self.startTime;
335
+ self.log(f("Done %s steps in %dms", self.steps.length, self.result.time), "info");
336
+ clearInterval(self.checker);
337
+ self.step -= 1;
338
+ self.emit('run.complete');
339
+ if (utils.isFunction(onComplete)) {
340
+ onComplete.call(self, self);
341
+ } else {
342
+ // default behavior is to exit
343
+ self.exit();
344
+ }
345
+ }
346
+ };
347
+
348
+ /**
349
+ * Checks if this instance is started.
350
+ *
351
+ * @return Boolean
352
+ * @throws CasperError
353
+ */
354
+ Casper.prototype.checkStarted = function checkStarted() {
355
+ "use strict";
356
+ if (!this.started) {
357
+ throw new CasperError(f("Casper is not started, can't execute `%s()`",
358
+ checkStarted.caller.name));
359
+ }
360
+ };
361
+
362
+ /**
363
+ * Clears the current page execution environment context. Useful to avoid
364
+ * having previously loaded DOM contents being still active (refs #34).
365
+ *
366
+ * Think of it as a way to stop javascript execution within the remote DOM
367
+ * environment.
368
+ *
369
+ * @return Casper
370
+ */
371
+ Casper.prototype.clear = function clear() {
372
+ "use strict";
373
+ this.checkStarted();
374
+ this.page.content = '';
375
+ return this;
376
+ };
377
+
378
+ /**
379
+ * Emulates a click on the element from the provided selector using the mouse
380
+ * pointer, if possible.
381
+ *
382
+ * In case of success, `true` is returned, `false` otherwise.
383
+ *
384
+ * @param String selector A DOM CSS3 compatible selector
385
+ * @return Boolean
386
+ */
387
+ Casper.prototype.click = function click(selector) {
388
+ "use strict";
389
+ this.checkStarted();
390
+ var success = this.mouseEvent('click', selector);
391
+ this.evaluate(function(selector) {
392
+ document.querySelector(selector).focus();
393
+ }, selector);
394
+ return success;
395
+ };
396
+
397
+ /**
398
+ * Emulates a click on the element having `label` as innerText. The first
399
+ * element matching this label will be selected, so use with caution.
400
+ *
401
+ * @param String label Element innerText value
402
+ * @param String tag An element tag name (eg. `a` or `button`) (optional)
403
+ * @return Boolean
404
+ */
405
+ Casper.prototype.clickLabel = function clickLabel(label, tag) {
406
+ "use strict";
407
+ this.checkStarted();
408
+ tag = tag || "*";
409
+ var escapedLabel = label.toString().replace(/"/g, '\\"');
410
+ var selector = selectXPath(f('//%s[text()="%s"]', tag, escapedLabel));
411
+ return this.click(selector);
412
+ };
413
+
414
+ /**
415
+ * Configures HTTP authentication parameters. Will try parsing auth credentials from
416
+ * the passed location first, then check for configured settings if any.
417
+ *
418
+ * @param String location Requested url
419
+ * @param Object settings Request settings
420
+ * @return Casper
421
+ */
422
+ Casper.prototype.configureHttpAuth = function configureHttpAuth(location, settings) {
423
+ "use strict";
424
+ var username, password, httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
425
+ this.checkStarted();
426
+ if (httpAuthMatch) {
427
+ this.page.settings.userName = httpAuthMatch[1];
428
+ this.page.settings.password = httpAuthMatch[2];
429
+ } else if (utils.isObject(settings) && settings.username) {
430
+ this.page.settings.userName = settings.username;
431
+ this.page.settings.password = settings.password;
432
+ } else {
433
+ return;
434
+ }
435
+ this.emit('http.auth', username, password);
436
+ this.log("Setting HTTP authentication for user " + username, "info");
437
+ return this;
438
+ };
439
+
440
+ /**
441
+ * Creates a step definition.
442
+ *
443
+ * @param Function fn The step function to call
444
+ * @param Object options Step options
445
+ * @return Function The final step function
446
+ */
447
+ Casper.prototype.createStep = function createStep(fn, options) {
448
+ "use strict";
449
+ if (!utils.isFunction(fn)) {
450
+ throw new CasperError("createStep(): a step definition must be a function");
451
+ }
452
+ fn.options = utils.isObject(options) ? options : {};
453
+ this.emit('step.created', fn);
454
+ return fn;
455
+ };
456
+
457
+ /**
458
+ * Logs the HTML code of the current page.
459
+ *
460
+ * @param String selector A DOM CSS3/XPath selector (optional)
461
+ * @param Boolean outer Whether to fetch outer HTML contents (default: false)
462
+ * @return Casper
463
+ */
464
+ Casper.prototype.debugHTML = function debugHTML(selector, outer) {
465
+ "use strict";
466
+ this.checkStarted();
467
+ return this.echo(this.getHTML(selector, outer));
468
+ };
469
+
470
+ /**
471
+ * Logs the textual contents of the current page.
472
+ *
473
+ * @return Casper
474
+ */
475
+ Casper.prototype.debugPage = function debugPage() {
476
+ "use strict";
477
+ this.checkStarted();
478
+ this.echo(this.evaluate(function _evaluate() {
479
+ return document.body.textContent || document.body.innerText;
480
+ }));
481
+ return this;
482
+ };
483
+
484
+ /**
485
+ * Exit phantom on failure, with a logged error message.
486
+ *
487
+ * @param String message An optional error message
488
+ * @param Number status An optional exit status code (must be > 0)
489
+ * @return Casper
490
+ */
491
+ Casper.prototype.die = function die(message, status) {
492
+ "use strict";
493
+ this.result.status = "error";
494
+ this.result.time = new Date().getTime() - this.startTime;
495
+ if (!utils.isString(message) || !message.length) {
496
+ message = "Suite explicitely interrupted without any message given.";
497
+ }
498
+ this.log(message, "error");
499
+ this.echo(message, "ERROR");
500
+ this.emit('die', message, status);
501
+ if (utils.isFunction(this.options.onDie)) {
502
+ this.options.onDie.call(this, this, message, status);
503
+ }
504
+ return this.exit(~~status > 0 ? ~~status : 1);
505
+ };
506
+
507
+ /**
508
+ * Downloads a resource and saves it on the filesystem.
509
+ *
510
+ * @param String url The url of the resource to download
511
+ * @param String targetPath The destination file path
512
+ * @param String method The HTTP method to use (default: GET)
513
+ * @param String data Optional data to pass performing the request
514
+ * @return Casper
515
+ */
516
+ Casper.prototype.download = function download(url, targetPath, method, data) {
517
+ "use strict";
518
+ this.checkStarted();
519
+ var cu = require('clientutils').create(utils.mergeObjects({}, this.options));
520
+ try {
521
+ fs.write(targetPath, cu.decode(this.base64encode(url, method, data)), 'wb');
522
+ this.emit('downloaded.file', targetPath);
523
+ this.log(f("Downloaded and saved resource in %s", targetPath));
524
+ } catch (e) {
525
+ this.log(f("Error while downloading %s to %s: %s", url, targetPath, e), "error");
526
+ }
527
+ return this;
528
+ };
529
+
530
+ /**
531
+ * Iterates over the values of a provided array and execute a callback
532
+ * for @ item.
533
+ *
534
+ * @param Array array
535
+ * @param Function fn Callback: function(self, item, index)
536
+ * @return Casper
537
+ */
538
+ Casper.prototype.each = function each(array, fn) {
539
+ "use strict";
540
+ if (!utils.isArray(array)) {
541
+ this.log("each() only works with arrays", "error");
542
+ return this;
543
+ }
544
+ (function _each(self) {
545
+ array.forEach(function _forEach(item, i) {
546
+ fn.call(self, self, item, i);
547
+ });
548
+ })(this);
549
+ return this;
550
+ };
551
+
552
+ /**
553
+ * Prints something to stdout.
554
+ *
555
+ * @param String text A string to echo to stdout
556
+ * @param String style An optional style name
557
+ * @param Number pad An optional pad value
558
+ * @return Casper
559
+ */
560
+ Casper.prototype.echo = function echo(text, style, pad) {
561
+ "use strict";
562
+ if (!utils.isString(text)) {
563
+ try {
564
+ text = text.toString();
565
+ } catch (e) {
566
+ try {
567
+ text = utils.serialize(text);
568
+ } catch (e2) {
569
+ text = '';
570
+ }
571
+ }
572
+ }
573
+ var message = style ? this.colorizer.colorize(text, style, pad) : text;
574
+ console.log(this.filter('echo.message', message) || message);
575
+ return this;
576
+ };
577
+
578
+ /**
579
+ * Evaluates an expression in the page context, a bit like what
580
+ * WebPage#evaluate does, but the passed function can also accept
581
+ * parameters if a context Object is also passed:
582
+ *
583
+ * casper.evaluate(function(username, password) {
584
+ * document.querySelector('#username').value = username;
585
+ * document.querySelector('#password').value = password;
586
+ * document.querySelector('#submit').click();
587
+ * }, 'Bazoonga', 'baz00nga');
588
+ *
589
+ * @param Function fn The function to be evaluated within current page DOM
590
+ * @param Object context Object containing the parameters to inject into the function
591
+ * @return mixed
592
+ * @see WebPage#evaluate
593
+ */
594
+ Casper.prototype.evaluate = function evaluate(fn, context) {
595
+ "use strict";
596
+ this.checkStarted();
597
+ // preliminary checks
598
+ if (!utils.isFunction(fn) && !utils.isString(fn)) { // phantomjs allows functions defs as string
599
+ throw new CasperError("evaluate() only accepts functions or strings");
600
+ }
601
+ // ensure client utils are always injected
602
+ this.injectClientUtils();
603
+ // function context
604
+ if (arguments.length === 1) {
605
+ return this.page.evaluate(fn);
606
+ } else if (arguments.length === 2) {
607
+ // check for closure signature if it matches context
608
+ if (utils.isObject(context) && eval(fn).length === Object.keys(context).length) {
609
+ context = utils.objectValues(context);
610
+ } else {
611
+ context = [context];
612
+ }
613
+ } else {
614
+ // phantomjs-style signature
615
+ context = [].slice.call(arguments).slice(1);
616
+ }
617
+ return this.page.evaluate.apply(this.page, [fn].concat(context));
618
+ };
619
+
620
+ /**
621
+ * Evaluates an expression within the current page DOM and die() if it
622
+ * returns false.
623
+ *
624
+ * @param function fn The expression to evaluate
625
+ * @param String message The error message to log
626
+ * @param Number status An optional exit status code (must be > 0)
627
+ *
628
+ * @return Casper
629
+ */
630
+ Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message, status) {
631
+ "use strict";
632
+ this.checkStarted();
633
+ if (!this.evaluate(fn)) {
634
+ return this.die(message, status);
635
+ }
636
+ return this;
637
+ };
638
+
639
+ /**
640
+ * Checks if an element matching the provided DOM CSS3/XPath selector exists in
641
+ * current page DOM.
642
+ *
643
+ * @param String selector A DOM CSS3/XPath selector
644
+ * @return Boolean
645
+ */
646
+ Casper.prototype.exists = function exists(selector) {
647
+ "use strict";
648
+ this.checkStarted();
649
+ return this.evaluate(function _evaluate(selector) {
650
+ return __utils__.exists(selector);
651
+ }, selector);
652
+ };
653
+
654
+ /**
655
+ * Exits phantom.
656
+ *
657
+ * @param Number status Status
658
+ * @return Casper
659
+ */
660
+ Casper.prototype.exit = function exit(status) {
661
+ "use strict";
662
+ this.emit('exit', status);
663
+ phantom.exit(status);
664
+ };
665
+
666
+ /**
667
+ * Fetches plain text contents contained in the DOM element(s) matching a given CSS3/XPath
668
+ * selector.
669
+ *
670
+ * @param String selector A DOM CSS3/XPath selector
671
+ * @return String
672
+ */
673
+ Casper.prototype.fetchText = function fetchText(selector) {
674
+ "use strict";
675
+ this.checkStarted();
676
+ return this.evaluate(function _evaluate(selector) {
677
+ return __utils__.fetchText(selector);
678
+ }, selector);
679
+ };
680
+
681
+ /**
682
+ * Fills a form with provided field values.
683
+ *
684
+ * @param String selector A DOM CSS3/XPath selector to the target form to fill
685
+ * @param Object vals Field values
686
+ * @param Boolean submit Submit the form?
687
+ */
688
+ Casper.prototype.fill = function fill(selector, vals, submit) {
689
+ "use strict";
690
+ this.checkStarted();
691
+ submit = submit === true ? submit : false;
692
+ if (!utils.isObject(vals)) {
693
+ throw new CasperError("Form values must be provided as an object");
694
+ }
695
+ this.emit('fill', selector, vals, submit);
696
+ var fillResults = this.evaluate(function _evaluate(selector, values) {
697
+ return __utils__.fill(selector, values);
698
+ }, selector, vals);
699
+ if (!fillResults) {
700
+ throw new CasperError("Unable to fill form");
701
+ } else if (fillResults.errors.length > 0) {
702
+ throw new CasperError(f('Errors encountered while filling form: %s',
703
+ fillResults.errors.join('; ')));
704
+ }
705
+ // File uploads
706
+ if (fillResults.files && fillResults.files.length > 0) {
707
+ if (utils.isObject(selector) && selector.type === 'xpath') {
708
+ this.warn('Filling file upload fields is currently not supported using ' +
709
+ 'XPath selectors; Please use a CSS selector instead.');
710
+ } else {
711
+ (function _each(self) {
712
+ fillResults.files.forEach(function _forEach(file) {
713
+ var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
714
+ self.page.uploadFile(fileFieldSelector, file.path);
715
+ });
716
+ })(this);
717
+ }
718
+ }
719
+ // Form submission?
720
+ if (submit) {
721
+ this.evaluate(function _evaluate(selector) {
722
+ var form = __utils__.findOne(selector);
723
+ var method = (form.getAttribute('method') || "GET").toUpperCase();
724
+ var action = form.getAttribute('action') || "unknown";
725
+ __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
726
+ if (typeof form.submit === "function") {
727
+ form.submit();
728
+ } else {
729
+ // http://www.spiration.co.uk/post/1232/Submit-is-not-a-function
730
+ form.submit.click();
731
+ }
732
+ }, selector);
733
+ }
734
+ };
735
+
736
+ /**
737
+ * Go a step forward in browser's history
738
+ *
739
+ * @return Casper
740
+ */
741
+ Casper.prototype.forward = function forward(then) {
742
+ "use strict";
743
+ this.checkStarted();
744
+ return this.then(function _step() {
745
+ this.emit('forward');
746
+ this.evaluate(function _evaluate() {
747
+ history.forward();
748
+ });
749
+ });
750
+ };
751
+
752
+ /**
753
+ * Creates a new Colorizer instance. Sets `Casper.options.type` to change the
754
+ * colorizer type name (see the `colorizer` module).
755
+ *
756
+ * @return Object
757
+ */
758
+ Casper.prototype.getColorizer = function getColorizer() {
759
+ "use strict";
760
+ return colorizer.create(this.options.colorizerType || 'Colorizer');
761
+ };
762
+
763
+ /**
764
+ * Retrieves current page contents, dealing with exotic other content types than HTML.
765
+ *
766
+ * @return String
767
+ */
768
+ Casper.prototype.getPageContent = function getPageContent() {
769
+ "use strict";
770
+ this.checkStarted();
771
+ var contentType = utils.getPropertyPath(this, 'currentResponse.contentType');
772
+ if (!utils.isString(contentType)) {
773
+ return this.page.frameContent;
774
+ }
775
+ // for some reason webkit/qtwebkit will always enclose body contents within html tags
776
+ var sanitizedHtml = this.evaluate(function checkHtml() {
777
+ if (__utils__.findOne('head').childNodes.length === 0 &&
778
+ __utils__.findOne('body').childNodes.length === 1 &&
779
+ __utils__.findOne('body pre[style]')) {
780
+ return __utils__.findOne('body pre').textContent.trim();
781
+ }
782
+ });
783
+ return sanitizedHtml ? sanitizedHtml : this.page.frameContent;
784
+ };
785
+
786
+ /**
787
+ * Retrieves current document url.
788
+ *
789
+ * @return String
790
+ */
791
+ Casper.prototype.getCurrentUrl = function getCurrentUrl() {
792
+ "use strict";
793
+ this.checkStarted();
794
+ var url = this.evaluate(function _evaluate() {
795
+ return document.location.href;
796
+ });
797
+ try {
798
+ return decodeURIComponent(url);
799
+ } catch (e) {
800
+ /*global unescape*/
801
+ return unescape(url);
802
+ }
803
+ };
804
+
805
+ /**
806
+ * Retrieves the value of an attribute on the first element matching the provided
807
+ * DOM CSS3/XPath selector.
808
+ *
809
+ * @param String selector A DOM CSS3/XPath selector
810
+ * @param String attribute The attribute name to lookup
811
+ * @return String The requested DOM element attribute value
812
+ */
813
+ Casper.prototype.getElementAttribute =
814
+ Casper.prototype.getElementAttr = function getElementAttr(selector, attribute) {
815
+ "use strict";
816
+ this.checkStarted();
817
+ return this.evaluate(function _evaluate(selector, attribute) {
818
+ return document.querySelector(selector).getAttribute(attribute);
819
+ }, selector, attribute);
820
+ };
821
+
822
+ /**
823
+ * Retrieves boundaries for a DOM element matching the provided DOM CSS3/XPath selector.
824
+ *
825
+ * @param String selector A DOM CSS3/XPath selector
826
+ * @return Object
827
+ */
828
+ Casper.prototype.getElementBounds = function getElementBounds(selector) {
829
+ "use strict";
830
+ this.checkStarted();
831
+ if (!this.exists(selector)) {
832
+ throw new CasperError("No element matching selector found: " + selector);
833
+ }
834
+ var clipRect = this.evaluate(function _evaluate(selector) {
835
+ return __utils__.getElementBounds(selector);
836
+ }, selector);
837
+ if (!utils.isClipRect(clipRect)) {
838
+ throw new CasperError('Could not fetch boundaries for element matching selector: ' + selector);
839
+ }
840
+ return clipRect;
841
+ };
842
+
843
+ /**
844
+ * Retrieves information about the node matching the provided selector.
845
+ *
846
+ * @param String|Objects selector CSS3/XPath selector
847
+ * @return Object
848
+ */
849
+ Casper.prototype.getElementInfo = function getElementInfo(selector) {
850
+ "use strict";
851
+ this.checkStarted();
852
+ if (!this.exists(selector)) {
853
+ throw new CasperError(f("Cannot get informations from %s: element not found.", selector));
854
+ }
855
+ return this.evaluate(function(selector) {
856
+ return __utils__.getElementInfo(selector);
857
+ }, selector);
858
+ };
859
+
860
+ /**
861
+ * Retrieves boundaries for all the DOM elements matching the provided DOM CSS3/XPath selector.
862
+ *
863
+ * @param String selector A DOM CSS3/XPath selector
864
+ * @return Array
865
+ */
866
+ Casper.prototype.getElementsBounds = function getElementBounds(selector) {
867
+ "use strict";
868
+ this.checkStarted();
869
+ if (!this.exists(selector)) {
870
+ throw new CasperError("No element matching selector found: " + selector);
871
+ }
872
+ return this.evaluate(function _evaluate(selector) {
873
+ return __utils__.getElementsBounds(selector);
874
+ }, selector);
875
+ };
876
+
877
+ /**
878
+ * Retrieves a given form all of its field values.
879
+ *
880
+ * @param String selector A DOM CSS3/XPath selector
881
+ * @return Object
882
+ */
883
+ Casper.prototype.getFormValues = function(selector) {
884
+ "use strict";
885
+ this.checkStarted();
886
+ if (!this.exists(selector)) {
887
+ throw new CasperError(f('Form matching selector "%s" not found', selector));
888
+ }
889
+ return this.evaluate(function(selector) {
890
+ return __utils__.getFormValues(selector);
891
+ }, selector);
892
+ };
893
+
894
+ /**
895
+ * Retrieves global variable.
896
+ *
897
+ * @param String name The name of the global variable to retrieve
898
+ * @return mixed
899
+ */
900
+ Casper.prototype.getGlobal = function getGlobal(name) {
901
+ "use strict";
902
+ this.checkStarted();
903
+ var result = this.evaluate(function _evaluate(name) {
904
+ var result = {};
905
+ try {
906
+ result.value = JSON.stringify(window[name]);
907
+ } catch (e) {
908
+ var message = f("Unable to JSON encode window.%s: %s", name, e);
909
+ __utils__.log(message, "error");
910
+ result.error = message;
911
+ }
912
+ return result;
913
+ }, name);
914
+ if (!utils.isObject(result)) {
915
+ throw new CasperError(f('Could not retrieve global value for "%s"', name));
916
+ } else if ('error' in result) {
917
+ throw new CasperError(result.error);
918
+ } else if (utils.isString(result.value)) {
919
+ return JSON.parse(result.value);
920
+ }
921
+ };
922
+
923
+ /**
924
+ * Retrieves current HTML code matching the provided CSS3/XPath selector.
925
+ * Returns the HTML contents for the whole page if no arg is passed.
926
+ *
927
+ * @param String selector A DOM CSS3/XPath selector
928
+ * @param Boolean outer Whether to fetch outer HTML contents (default: false)
929
+ * @return String
930
+ */
931
+ Casper.prototype.getHTML = function getHTML(selector, outer) {
932
+ "use strict";
933
+ this.checkStarted();
934
+ if (!selector) {
935
+ return this.page.frameContent;
936
+ }
937
+ if (!this.exists(selector)) {
938
+ throw new CasperError("No element matching selector found: " + selector);
939
+ }
940
+ return this.evaluate(function getSelectorHTML(selector, outer) {
941
+ var element = __utils__.findOne(selector);
942
+ return outer ? element.outerHTML : element.innerHTML;
943
+ }, selector, !!outer);
944
+ };
945
+
946
+ /**
947
+ * Retrieves current page title, if any.
948
+ *
949
+ * @return String
950
+ */
951
+ Casper.prototype.getTitle = function getTitle() {
952
+ "use strict";
953
+ this.checkStarted();
954
+ return this.evaluate(function _evaluate() {
955
+ return document.title;
956
+ });
957
+ };
958
+
959
+ /**
960
+ * Handles received HTTP resource.
961
+ *
962
+ * @param Object resource PhantomJS HTTP resource
963
+ */
964
+ Casper.prototype.handleReceivedResource = function(resource) {
965
+ "use strict";
966
+ /*jshint maxstatements:20*/
967
+ if (resource.stage !== "end") {
968
+ return;
969
+ }
970
+ this.resources.push(resource);
971
+ if (resource.url !== this.requestUrl) {
972
+ return;
973
+ }
974
+ this.currentHTTPStatus = null;
975
+ this.currentResponse = undefined;
976
+ if (utils.isHTTPResource(resource)) {
977
+ this.emit('page.resource.received', resource);
978
+ this.currentResponse = resource;
979
+ this.currentHTTPStatus = resource.status;
980
+ this.emit('http.status.' + resource.status, resource);
981
+ if (utils.isObject(this.options.httpStatusHandlers) &&
982
+ resource.status in this.options.httpStatusHandlers &&
983
+ utils.isFunction(this.options.httpStatusHandlers[resource.status])) {
984
+ this.options.httpStatusHandlers[resource.status].call(this, this, resource);
985
+ }
986
+ }
987
+ this.currentUrl = resource.url;
988
+ this.emit('location.changed', resource.url);
989
+ };
990
+
991
+ /**
992
+ * Initializes PhantomJS error handler.
993
+ *
994
+ */
995
+ Casper.prototype.initErrorHandler = function initErrorHandler() {
996
+ "use strict";
997
+ var casper = this;
998
+ phantom.onError = function phantom_onError(msg, backtrace) {
999
+ casper.emit('error', msg, backtrace);
1000
+ if (casper.options.exitOnError === true) {
1001
+ casper.exit(1);
1002
+ }
1003
+ };
1004
+ };
1005
+
1006
+ /**
1007
+ * Injects configured local client scripts.
1008
+ *
1009
+ * @return Casper
1010
+ */
1011
+ Casper.prototype.injectClientScripts = function injectClientScripts() {
1012
+ "use strict";
1013
+ this.checkStarted();
1014
+ if (!this.options.clientScripts) {
1015
+ return;
1016
+ }
1017
+ if (utils.isString(this.options.clientScripts)) {
1018
+ this.options.clientScripts = [this.options.clientScripts];
1019
+ }
1020
+ if (!utils.isArray(this.options.clientScripts)) {
1021
+ throw new CasperError("The clientScripts option must be an array");
1022
+ }
1023
+ this.options.clientScripts.forEach(function _forEach(script) {
1024
+ if (this.page.injectJs(script)) {
1025
+ this.log(f('Automatically injected %s client side', script), "debug");
1026
+ } else {
1027
+ this.warn('Failed injecting %s client side', script);
1028
+ }
1029
+ }.bind(this));
1030
+ return this;
1031
+ };
1032
+
1033
+ /**
1034
+ * Injects Client-side utilities in current page context.
1035
+ *
1036
+ */
1037
+ Casper.prototype.injectClientUtils = function injectClientUtils() {
1038
+ "use strict";
1039
+ this.checkStarted();
1040
+ var clientUtilsInjected = this.page.evaluate(function() {
1041
+ return typeof __utils__ === "object";
1042
+ });
1043
+ if (true === clientUtilsInjected) {
1044
+ return;
1045
+ }
1046
+ var clientUtilsPath = require('fs').pathJoin(phantom.casperPath, 'modules', 'clientutils.js');
1047
+ if (true === this.page.injectJs(clientUtilsPath)) {
1048
+ this.log("Successfully injected Casper client-side utilities", "debug");
1049
+ } else {
1050
+ this.warn("Failed to inject Casper client-side utilities");
1051
+ }
1052
+ // ClientUtils and Casper shares the same options
1053
+ // These are not the lines I'm the most proud of in my life, but it works.
1054
+ /*global __options*/
1055
+ this.page.evaluate(function() {
1056
+ window.__utils__ = new window.ClientUtils(__options);
1057
+ }.toString().replace('__options', JSON.stringify(this.options)));
1058
+ };
1059
+
1060
+ /**
1061
+ * Loads and include remote client scripts to current page.
1062
+ *
1063
+ * @return Casper
1064
+ */
1065
+ Casper.prototype.includeRemoteScripts = function includeRemoteScripts() {
1066
+ "use strict";
1067
+ var numScripts = this.options.remoteScripts.length, loaded = 0;
1068
+ if (numScripts === 0) {
1069
+ return this;
1070
+ }
1071
+ this.waitStart();
1072
+ this.options.remoteScripts.forEach(function(scriptUrl) {
1073
+ this.log(f("Loading remote script: %s", scriptUrl), "debug");
1074
+ this.page.includeJs(scriptUrl, function() {
1075
+ loaded++;
1076
+ this.log(f("Remote script %s loaded", scriptUrl), "debug");
1077
+ if (loaded === numScripts) {
1078
+ this.log("All remote scripts loaded.", "debug");
1079
+ this.waitDone();
1080
+ }
1081
+ }.bind(this));
1082
+ }.bind(this));
1083
+ return this;
1084
+ };
1085
+
1086
+ /**
1087
+ * Logs a message.
1088
+ *
1089
+ * @param String message The message to log
1090
+ * @param String level The log message level (from Casper.logLevels property)
1091
+ * @param String space Space from where the logged event occurred (default: "phantom")
1092
+ * @return Casper
1093
+ */
1094
+ Casper.prototype.log = function log(message, level, space) {
1095
+ "use strict";
1096
+ level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
1097
+ space = space ? space : "phantom";
1098
+ if (level === "error" && utils.isFunction(this.options.onError)) {
1099
+ this.options.onError.call(this, this, message, space);
1100
+ }
1101
+ if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
1102
+ return this; // skip logging
1103
+ }
1104
+ var entry = {
1105
+ level: level,
1106
+ space: space,
1107
+ message: message,
1108
+ date: new Date().toString()
1109
+ };
1110
+ if (level in this.logFormats && utils.isFunction(this.logFormats[level])) {
1111
+ message = this.logFormats[level](message, level, space);
1112
+ } else {
1113
+ message = f('%s [%s] %s',
1114
+ this.colorizer.colorize(f('[%s]', level), this.logStyles[level]),
1115
+ space,
1116
+ message);
1117
+ }
1118
+ if (this.options.verbose) {
1119
+ this.echo(this.filter('log.message', message) || message); // direct output
1120
+ }
1121
+ this.result.log.push(entry);
1122
+ this.emit('log', entry);
1123
+ return this;
1124
+ };
1125
+
1126
+ /**
1127
+ * Emulates an event on the element from the provided selector using the mouse
1128
+ * pointer, if possible.
1129
+ *
1130
+ * In case of success, `true` is returned, `false` otherwise.
1131
+ *
1132
+ * @param String type Type of event to emulate
1133
+ * @param String selector A DOM CSS3 compatible selector
1134
+ * @return Boolean
1135
+ */
1136
+ Casper.prototype.mouseEvent = function mouseEvent(type, selector) {
1137
+ "use strict";
1138
+ this.checkStarted();
1139
+ this.log("Mouse event '" + type + "' on selector: " + selector, "debug");
1140
+ if (!this.exists(selector)) {
1141
+ throw new CasperError(f("Cannot dispatch %s event on nonexistent selector: %s", type, selector));
1142
+ }
1143
+ if (this.evaluate(function(type, selector) {
1144
+ return window.__utils__.mouseEvent(type, selector);
1145
+ }, type, selector)) {
1146
+ return true;
1147
+ }
1148
+ // fallback onto native QtWebKit mouse events
1149
+ try {
1150
+ return this.mouse.processEvent(type, selector);
1151
+ } catch (e) {
1152
+ this.log(f("Couldn't emulate '%s' event on %s: %s", type, selector, e), "error");
1153
+ }
1154
+ return false;
1155
+ };
1156
+
1157
+ /**
1158
+ * Performs an HTTP request, with optional settings.
1159
+ *
1160
+ * Available settings are:
1161
+ *
1162
+ * - String method: The HTTP method to use
1163
+ * - Object data: The data to use to perform the request, eg. {foo: 'bar'}
1164
+ * - Object headers: Custom request headers object, eg. {'Cache-Control': 'max-age=0'}
1165
+ *
1166
+ * @param String location The url to open
1167
+ * @param Object settings The request settings (optional)
1168
+ * @return Casper
1169
+ */
1170
+ Casper.prototype.open = function open(location, settings) {
1171
+ "use strict";
1172
+ /*jshint maxstatements:30*/
1173
+ var baseCustomHeaders = this.page.customHeaders,
1174
+ customHeaders = settings && settings.headers || {};
1175
+ this.checkStarted();
1176
+ settings = utils.isObject(settings) ? settings : {};
1177
+ settings.method = settings.method || "get";
1178
+ // http method
1179
+ // taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302
1180
+ var methods = ["get", "head", "put", "post", "delete"];
1181
+ if (settings.method && (!utils.isString(settings.method) || methods.indexOf(settings.method) === -1)) {
1182
+ throw new CasperError("open(): settings.method must be part of " + methods.join(', '));
1183
+ }
1184
+ // http data
1185
+ if (settings.data) {
1186
+ if (utils.isObject(settings.data)) { // query object
1187
+ settings.data = qs.encode(settings.data);
1188
+ } else if (!utils.isString(settings.data)) {
1189
+ throw new CasperError("open(): invalid request settings data value: " + settings.data);
1190
+ }
1191
+ }
1192
+ // clean location
1193
+ location = utils.cleanUrl(location);
1194
+ // current request url
1195
+ this.configureHttpAuth(location, settings);
1196
+ this.requestUrl = this.filter('open.location', location) || location;
1197
+ this.emit('open', this.requestUrl, settings);
1198
+ this.log(f('opening url: %s, HTTP %s', this.requestUrl, settings.method.toUpperCase()), "debug");
1199
+ // reset resources
1200
+ this.resources = [];
1201
+ // custom headers
1202
+ this.page.customHeaders = utils.mergeObjects(utils.clone(baseCustomHeaders), customHeaders);
1203
+ // perfom request
1204
+ this.page.openUrl(this.requestUrl, {
1205
+ operation: settings.method,
1206
+ data: settings.data
1207
+ }, this.page.settings);
1208
+ // revert base custom headers
1209
+ this.page.customHeaders = baseCustomHeaders;
1210
+ return this;
1211
+ };
1212
+
1213
+ /**
1214
+ * Reloads current page.
1215
+ *
1216
+ * @param Function then a next step function
1217
+ * @return Casper
1218
+ */
1219
+ Casper.prototype.reload = function reload(then) {
1220
+ "use strict";
1221
+ this.checkStarted();
1222
+ // window.location.reload() is broken under phantomjs
1223
+ this.then(function() {
1224
+ this.open(this.getCurrentUrl());
1225
+ });
1226
+ if (utils.isFunction(then)) {
1227
+ this.then(this.createStep(then));
1228
+ }
1229
+ };
1230
+
1231
+ /**
1232
+ * Repeats a step a given number of times.
1233
+ *
1234
+ * @param Number times Number of times to repeat step
1235
+ * @aram function then The step closure
1236
+ * @return Casper
1237
+ * @see Casper#then
1238
+ */
1239
+ Casper.prototype.repeat = function repeat(times, then) {
1240
+ "use strict";
1241
+ for (var i = 0; i < times; i++) {
1242
+ this.then(then);
1243
+ }
1244
+ return this;
1245
+ };
1246
+
1247
+ /**
1248
+ * Checks if a given resource was loaded by the remote page.
1249
+ *
1250
+ * @param Function/String/RegExp test A test function, string or regular expression.
1251
+ * In case a string is passed, url matching will be tested.
1252
+ * @return Boolean
1253
+ */
1254
+ Casper.prototype.resourceExists = function resourceExists(test) {
1255
+ "use strict";
1256
+ this.checkStarted();
1257
+ var testFn;
1258
+ switch (utils.betterTypeOf(test)) {
1259
+ case "string":
1260
+ testFn = function _testResourceExists_String(res) {
1261
+ return res.url.search(test) !== -1 && res.status !== 404;
1262
+ };
1263
+ break;
1264
+ case "regexp":
1265
+ testFn = function _testResourceExists_Regexp(res) {
1266
+ return test.test(res.url) && res.status !== 404;
1267
+ };
1268
+ break;
1269
+ case "function":
1270
+ testFn = test;
1271
+ testFn.name = "_testResourceExists_Function";
1272
+ break;
1273
+ default:
1274
+ throw new CasperError("Invalid type");
1275
+ }
1276
+ return this.resources.some(testFn);
1277
+ };
1278
+
1279
+ /**
1280
+ * Runs the whole suite of steps.
1281
+ *
1282
+ * @param function onComplete an optional callback
1283
+ * @param Number time an optional amount of milliseconds for interval checking
1284
+ * @return Casper
1285
+ */
1286
+ Casper.prototype.run = function run(onComplete, time) {
1287
+ "use strict";
1288
+ this.checkStarted();
1289
+ if (!this.steps || this.steps.length < 1) {
1290
+ throw new CasperError('No steps defined, aborting');
1291
+ }
1292
+ this.log(f("Running suite: %d step%s", this.steps.length, this.steps.length > 1 ? "s" : ""), "info");
1293
+ this.emit('run.start');
1294
+ this.checker = setInterval(this.checkStep, (time ? time: 100), this, onComplete);
1295
+ return this;
1296
+ };
1297
+
1298
+ /**
1299
+ * Runs a step.
1300
+ *
1301
+ * @param Function step
1302
+ */
1303
+ Casper.prototype.runStep = function runStep(step) {
1304
+ "use strict";
1305
+ this.checkStarted();
1306
+ var skipLog = utils.isObject(step.options) && step.options.skipLog === true;
1307
+ var stepInfo = f("Step %d/%d", this.step, this.steps.length);
1308
+ var stepResult;
1309
+ if (!skipLog && /^http/.test(this.getCurrentUrl())) {
1310
+ this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info");
1311
+ }
1312
+ if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
1313
+ var stepTimeoutCheckInterval = setInterval(function _check(self, start, stepNum) {
1314
+ if (new Date().getTime() - start > self.options.stepTimeout) {
1315
+ if ((self.test.currentSuiteNum + "-" + self.step) === stepNum) {
1316
+ self.emit('step.timeout');
1317
+ if (utils.isFunction(self.options.onStepTimeout)) {
1318
+ self.options.onStepTimeout.call(self, self.options.stepTimeout, stepNum);
1319
+ }
1320
+ }
1321
+ clearInterval(stepTimeoutCheckInterval);
1322
+ }
1323
+ }, this.options.stepTimeout, this, new Date().getTime(), this.test.currentSuiteNum + "-" + this.step);
1324
+ }
1325
+ this.emit('step.start', step);
1326
+ stepResult = step.call(this, this.currentResponse);
1327
+ if (utils.isFunction(this.options.onStepComplete)) {
1328
+ this.options.onStepComplete.call(this, this, stepResult);
1329
+ }
1330
+ if (!skipLog) {
1331
+ this.emit('step.complete', stepResult);
1332
+ this.log(stepInfo + f(": done in %dms.", new Date().getTime() - this.startTime), "info");
1333
+ }
1334
+ };
1335
+
1336
+ /**
1337
+ * Sends keys to given element.
1338
+ *
1339
+ * @param String selector A DOM CSS3 compatible selector
1340
+ * @param String keys A string representing the sequence of char codes to send
1341
+ * @param Object options Options
1342
+ * @return Casper
1343
+ */
1344
+ Casper.prototype.sendKeys = function(selector, keys, options) {
1345
+ "use strict";
1346
+ this.checkStarted();
1347
+ options = utils.mergeObjects({
1348
+ eventType: 'keypress'
1349
+ }, options || {});
1350
+ var elemInfos = this.getElementInfo(selector),
1351
+ tag = elemInfos.nodeName.toLowerCase(),
1352
+ type = utils.getPropertyPath(elemInfos, 'attributes.type'),
1353
+ supported = ["color", "date", "datetime", "datetime-local", "email",
1354
+ "hidden", "month", "number", "password", "range", "search",
1355
+ "tel", "text", "time", "url", "week"];
1356
+ var isTextInput = false;
1357
+ if (tag === 'textarea' || (tag === 'input' && supported.indexOf(type) !== -1)) {
1358
+ // clicking on the input element brings it focus
1359
+ isTextInput = true;
1360
+ this.click(selector);
1361
+ }
1362
+ this.page.sendEvent(options.eventType, keys);
1363
+ if (isTextInput) {
1364
+ // remove the focus
1365
+ this.evaluate(function(selector) {
1366
+ __utils__.findOne(selector).blur();
1367
+ }, selector);
1368
+ }
1369
+ return this;
1370
+ };
1371
+
1372
+ /**
1373
+ * Sets current WebPage instance the credentials for HTTP authentication.
1374
+ *
1375
+ * @param String username
1376
+ * @param String password
1377
+ * @return Casper
1378
+ */
1379
+ Casper.prototype.setHttpAuth = function setHttpAuth(username, password) {
1380
+ "use strict";
1381
+ this.checkStarted();
1382
+ this.page.settings.userName = username;
1383
+ this.page.settings.password = password;
1384
+ return this;
1385
+ };
1386
+
1387
+ /**
1388
+ * Configures and starts Casper.
1389
+ *
1390
+ * @param String location An optional location to open on start
1391
+ * @param function then Next step function to execute on page loaded (optional)
1392
+ * @return Casper
1393
+ */
1394
+ Casper.prototype.start = function start(location, then) {
1395
+ "use strict";
1396
+ /*jshint maxstatements:30*/
1397
+ this.emit('starting');
1398
+ this.log('Starting...', "info");
1399
+ this.startTime = new Date().getTime();
1400
+ this.history = [];
1401
+ this.popups = pagestack.create();
1402
+ this.steps = [];
1403
+ this.step = 0;
1404
+ // Option checks
1405
+ if (this.logLevels.indexOf(this.options.logLevel) < 0) {
1406
+ this.log(f("Unknown log level '%d', defaulting to 'warning'", this.options.logLevel), "warning");
1407
+ this.options.logLevel = "warning";
1408
+ }
1409
+ if (!utils.isWebPage(this.page)) {
1410
+ this.page = this.mainPage = utils.isWebPage(this.options.page) ? this.options.page : createPage(this);
1411
+ }
1412
+ this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings);
1413
+ if (utils.isClipRect(this.options.clipRect)) {
1414
+ this.page.clipRect = this.options.clipRect;
1415
+ }
1416
+ if (utils.isObject(this.options.viewportSize)) {
1417
+ this.page.viewportSize = this.options.viewportSize;
1418
+ }
1419
+ // timeout handling
1420
+ if (utils.isNumber(this.options.timeout) && this.options.timeout > 0) {
1421
+ this.log(f("Execution timeout set to %dms", this.options.timeout), "info");
1422
+ setTimeout(function _check(self) {
1423
+ self.emit('timeout');
1424
+ if (utils.isFunction(self.options.onTimeout)) {
1425
+ self.options.onTimeout.call(self, self.options.timeout);
1426
+ }
1427
+ }, this.options.timeout, this);
1428
+ }
1429
+ this.started = true;
1430
+ this.emit('started');
1431
+ if (utils.isString(location) && location.length > 0) {
1432
+ return this.thenOpen(location, utils.isFunction(then) ? then : this.createStep(function _step() {
1433
+ this.log("start page is loaded", "debug");
1434
+ }, {skipLog: true}));
1435
+ }
1436
+ return this;
1437
+ };
1438
+
1439
+ /**
1440
+ * Returns the current status of current instance
1441
+ *
1442
+ * @param Boolean asString Export status object as string
1443
+ * @return Object
1444
+ */
1445
+ Casper.prototype.status = function status(asString) {
1446
+ "use strict";
1447
+ var properties = ['currentHTTPStatus', 'loadInProgress', 'navigationRequested',
1448
+ 'options', 'pendingWait', 'requestUrl', 'started', 'step', 'url'];
1449
+ var currentStatus = {};
1450
+ properties.forEach(function(property) {
1451
+ currentStatus[property] = this[property];
1452
+ }.bind(this));
1453
+ return asString === true ? utils.dump(currentStatus) : currentStatus;
1454
+ };
1455
+
1456
+ /**
1457
+ * Schedules the next step in the navigation process.
1458
+ *
1459
+ * @param function step A function to be called as a step
1460
+ * @return Casper
1461
+ */
1462
+ Casper.prototype.then = function then(step) {
1463
+ "use strict";
1464
+ this.checkStarted();
1465
+ if (!utils.isFunction(step)) {
1466
+ throw new CasperError("You can only define a step as a function");
1467
+ }
1468
+ // check if casper is running
1469
+ if (this.checker === null) {
1470
+ // append step to the end of the queue
1471
+ step.level = 0;
1472
+ this.steps.push(step);
1473
+ } else {
1474
+ // insert substep a level deeper
1475
+ try {
1476
+ step.level = this.steps[this.step - 1].level + 1;
1477
+ } catch (e) {
1478
+ step.level = 0;
1479
+ }
1480
+ var insertIndex = this.step;
1481
+ while (this.steps[insertIndex] && step.level === this.steps[insertIndex].level) {
1482
+ insertIndex++;
1483
+ }
1484
+ this.steps.splice(insertIndex, 0, step);
1485
+ }
1486
+ this.emit('step.added', step);
1487
+ return this;
1488
+ };
1489
+
1490
+ /**
1491
+ * Adds a new navigation step for clicking on a provided link selector
1492
+ * and execute an optional next step.
1493
+ *
1494
+ * @param String selector A DOM CSS3 compatible selector
1495
+ * @param Function then Next step function to execute on page loaded (optional)
1496
+ * @return Casper
1497
+ * @see Casper#click
1498
+ * @see Casper#then
1499
+ */
1500
+ Casper.prototype.thenClick = function thenClick(selector, then, fallbackToHref) {
1501
+ "use strict";
1502
+ this.checkStarted();
1503
+ if (arguments.length > 2) {
1504
+ this.emit("deprecated", "The thenClick() method does not process the fallbackToHref argument since 0.6");
1505
+ }
1506
+ this.then(function _step() {
1507
+ this.click(selector);
1508
+ });
1509
+ return utils.isFunction(then) ? this.then(then) : this;
1510
+ };
1511
+
1512
+ /**
1513
+ * Adds a new navigation step to perform code evaluation within the
1514
+ * current retrieved page DOM.
1515
+ *
1516
+ * @param function fn The function to be evaluated within current page DOM
1517
+ * @param object context Optional function parameters context
1518
+ * @return Casper
1519
+ * @see Casper#evaluate
1520
+ */
1521
+ Casper.prototype.thenEvaluate = function thenEvaluate(fn, context) {
1522
+ "use strict";
1523
+ this.checkStarted();
1524
+ var args = [fn].concat([].slice.call(arguments, 1));
1525
+ return this.then(function _step() {
1526
+ this.evaluate.apply(this, args);
1527
+ });
1528
+ };
1529
+
1530
+ /**
1531
+ * Adds a new navigation step for opening the provided location.
1532
+ *
1533
+ * @param String location The URL to load
1534
+ * @param function then Next step function to execute on page loaded (optional)
1535
+ * @return Casper
1536
+ * @see Casper#open
1537
+ */
1538
+ Casper.prototype.thenOpen = function thenOpen(location, settings, then) {
1539
+ "use strict";
1540
+ this.checkStarted();
1541
+ if (!(settings && !utils.isFunction(settings))) {
1542
+ then = settings;
1543
+ settings = null;
1544
+ }
1545
+ this.then(this.createStep(function _step() {
1546
+ this.open(location, settings);
1547
+ }, {
1548
+ skipLog: true
1549
+ }));
1550
+ return utils.isFunction(then) ? this.then(then) : this;
1551
+ };
1552
+
1553
+ /**
1554
+ * Adds a new navigation step for opening and evaluate an expression
1555
+ * against the DOM retrieved from the provided location.
1556
+ *
1557
+ * @param String location The url to open
1558
+ * @param function fn The function to be evaluated within current page DOM
1559
+ * @param object context Optional function parameters context
1560
+ * @return Casper
1561
+ * @see Casper#evaluate
1562
+ * @see Casper#open
1563
+ */
1564
+ Casper.prototype.thenOpenAndEvaluate = function thenOpenAndEvaluate(location, fn, context) {
1565
+ "use strict";
1566
+ this.checkStarted();
1567
+ return this.thenOpen(location).thenEvaluate(fn, context);
1568
+ };
1569
+
1570
+ /**
1571
+ * Returns a string representation of current instance
1572
+ *
1573
+ * @return String
1574
+ */
1575
+ Casper.prototype.toString = function toString() {
1576
+ "use strict";
1577
+ return '[object Casper], currently at ' + this.getCurrentUrl();
1578
+ };
1579
+
1580
+ /**
1581
+ * Sets the user-agent string currently used when requesting urls.
1582
+ *
1583
+ * @param String userAgent User agent string
1584
+ * @return String
1585
+ */
1586
+ Casper.prototype.userAgent = function userAgent(agent) {
1587
+ "use strict";
1588
+ this.options.pageSettings.userAgent = agent;
1589
+ if (this.started && this.page) {
1590
+ this.page.settings.userAgent = agent;
1591
+ }
1592
+ return this;
1593
+ };
1594
+
1595
+ /**
1596
+ * Changes the current viewport size.
1597
+ *
1598
+ * @param Number width The viewport width, in pixels
1599
+ * @param Number height The viewport height, in pixels
1600
+ * @return Casper
1601
+ */
1602
+ Casper.prototype.viewport = function viewport(width, height) {
1603
+ "use strict";
1604
+ this.checkStarted();
1605
+ if (!utils.isNumber(width) || !utils.isNumber(height) || width <= 0 || height <= 0) {
1606
+ throw new CasperError(f("Invalid viewport: %dx%d", width, height));
1607
+ }
1608
+ this.page.viewportSize = {
1609
+ width: width,
1610
+ height: height
1611
+ };
1612
+ this.emit('viewport.changed', [width, height]);
1613
+ return this;
1614
+ };
1615
+
1616
+ /**
1617
+ * Checks if an element matching the provided DOM CSS3/XPath selector is visible
1618
+ * current page DOM by checking that offsetWidth and offsetHeight are
1619
+ * both non-zero.
1620
+ *
1621
+ * @param String selector A DOM CSS3/XPath selector
1622
+ * @return Boolean
1623
+ */
1624
+ Casper.prototype.visible = function visible(selector) {
1625
+ "use strict";
1626
+ this.checkStarted();
1627
+ return this.evaluate(function _evaluate(selector) {
1628
+ return __utils__.visible(selector);
1629
+ }, selector);
1630
+ };
1631
+
1632
+ /**
1633
+ * Displays a warning message onto the console and logs the event.
1634
+ *
1635
+ * @param String message
1636
+ * @return Casper
1637
+ */
1638
+ Casper.prototype.warn = function warn(message) {
1639
+ "use strict";
1640
+ this.log(message, "warning", "phantom");
1641
+ var formatted = f.apply(null, ["⚠  " + message].concat([].slice.call(arguments, 1)));
1642
+ return this.echo(formatted, 'COMMENT');
1643
+ };
1644
+
1645
+ /**
1646
+ * Adds a new step that will wait for a given amount of time (expressed
1647
+ * in milliseconds) before processing an optional next one.
1648
+ *
1649
+ * @param Number timeout The max amount of time to wait, in milliseconds
1650
+ * @param Function then Next step to process (optional)
1651
+ * @return Casper
1652
+ */
1653
+ Casper.prototype.wait = function wait(timeout, then) {
1654
+ "use strict";
1655
+ this.checkStarted();
1656
+ timeout = ~~timeout;
1657
+ if (timeout < 1) {
1658
+ this.die("wait() only accepts a positive integer > 0 as a timeout value");
1659
+ }
1660
+ if (then && !utils.isFunction(then)) {
1661
+ this.die("wait() a step definition must be a function");
1662
+ }
1663
+ return this.then(function _step() {
1664
+ this.waitStart();
1665
+ setTimeout(function _check(self) {
1666
+ self.log(f("wait() finished waiting for %dms.", timeout), "info");
1667
+ if (then) {
1668
+ then.call(self, self);
1669
+ }
1670
+ self.waitDone();
1671
+ }, timeout, this);
1672
+ });
1673
+ };
1674
+
1675
+ Casper.prototype.waitStart = function waitStart() {
1676
+ "use strict";
1677
+ this.emit('wait.start');
1678
+ this.pendingWait = true;
1679
+ };
1680
+
1681
+ Casper.prototype.waitDone = function waitDone() {
1682
+ "use strict";
1683
+ this.emit('wait.done');
1684
+ this.pendingWait = false;
1685
+ };
1686
+
1687
+ /**
1688
+ * Waits until a function returns true to process a next step.
1689
+ *
1690
+ * @param Function testFx A function to be evaluated for returning condition satisfecit
1691
+ * @param Function then The next step to perform (optional)
1692
+ * @param Function onTimeout A callback function to call on timeout (optional)
1693
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1694
+ * @return Casper
1695
+ */
1696
+ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
1697
+ "use strict";
1698
+ this.checkStarted();
1699
+ timeout = timeout ? timeout : this.options.waitTimeout;
1700
+ if (!utils.isFunction(testFx)) {
1701
+ this.die("waitFor() needs a test function");
1702
+ }
1703
+ if (then && !utils.isFunction(then)) {
1704
+ this.die("waitFor() next step definition must be a function");
1705
+ }
1706
+ return this.then(function _step() {
1707
+ this.waitStart();
1708
+ var start = new Date().getTime();
1709
+ var condition = false;
1710
+ var interval = setInterval(function _check(self, testFx, timeout, onTimeout) {
1711
+ if ((new Date().getTime() - start < timeout) && !condition) {
1712
+ condition = testFx.call(self, self);
1713
+ } else {
1714
+ self.waitDone();
1715
+ if (!condition) {
1716
+ self.log("Casper.waitFor() timeout", "warning");
1717
+ self.emit('waitFor.timeout');
1718
+ var onWaitTimeout = onTimeout ? onTimeout : self.options.onWaitTimeout;
1719
+ if (!utils.isFunction(onWaitTimeout)) {
1720
+ throw new CasperError('Invalid timeout function, exiting.');
1721
+ }
1722
+ onWaitTimeout.call(self, timeout);
1723
+ } else {
1724
+ self.log(f("waitFor() finished in %dms.", new Date().getTime() - start), "info");
1725
+ if (then) {
1726
+ self.then(then);
1727
+ }
1728
+ }
1729
+ clearInterval(interval);
1730
+ }
1731
+ }, 100, this, testFx, timeout, onTimeout);
1732
+ });
1733
+ };
1734
+
1735
+ /**
1736
+ * Waits for a popup page having its url matching the provided pattern to be opened
1737
+ * and loaded.
1738
+ *
1739
+ * @param String|RegExp urlPattern The popup url pattern
1740
+ * @param Function then The next step function (optional)
1741
+ * @param Function onTimeout Function to call on operation timeout (optional)
1742
+ * @param Number timeout Timeout in milliseconds (optional)
1743
+ * @return Casper
1744
+ */
1745
+ Casper.prototype.waitForPopup = function waitForPopup(urlPattern, then, onTimeout, timeout) {
1746
+ "use strict";
1747
+ return this.waitFor(function() {
1748
+ try {
1749
+ this.popups.find(urlPattern);
1750
+ return true;
1751
+ } catch (e) {
1752
+ return false;
1753
+ }
1754
+ }, then, onTimeout, timeout);
1755
+ };
1756
+
1757
+ /**
1758
+ * Waits until a given resource is loaded
1759
+ *
1760
+ * @param String/Function test A function to test if the resource exists.
1761
+ * A string will be matched against the resources url.
1762
+ * @param Function then The next step to perform (optional)
1763
+ * @param Function onTimeout A callback function to call on timeout (optional)
1764
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1765
+ * @return Casper
1766
+ */
1767
+ Casper.prototype.waitForResource = function waitForResource(test, then, onTimeout, timeout) {
1768
+ "use strict";
1769
+ this.checkStarted();
1770
+ timeout = timeout ? timeout : this.options.waitTimeout;
1771
+ return this.waitFor(function _check() {
1772
+ return this.resourceExists(test);
1773
+ }, then, onTimeout, timeout);
1774
+ };
1775
+
1776
+ /**
1777
+ * Waits until an element matching the provided DOM CSS3/XPath selector exists in
1778
+ * remote DOM to process a next step.
1779
+ *
1780
+ * @param String selector A DOM CSS3/XPath selector
1781
+ * @param Function then The next step to perform (optional)
1782
+ * @param Function onTimeout A callback function to call on timeout (optional)
1783
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1784
+ * @return Casper
1785
+ */
1786
+ Casper.prototype.waitForSelector = function waitForSelector(selector, then, onTimeout, timeout) {
1787
+ "use strict";
1788
+ this.checkStarted();
1789
+ timeout = timeout ? timeout : this.options.waitTimeout;
1790
+ return this.waitFor(function _check() {
1791
+ return this.exists(selector);
1792
+ }, then, onTimeout, timeout);
1793
+ };
1794
+
1795
+ /**
1796
+ * Waits until the page contains given HTML text.
1797
+ *
1798
+ * @param String text Text to wait for
1799
+ * @param Function then The next step to perform (optional)
1800
+ * @param Function onTimeout A callback function to call on timeout (optional)
1801
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1802
+ * @return Casper
1803
+ */
1804
+ Casper.prototype.waitForText = function(text, then, onTimeout, timeout) {
1805
+ "use strict";
1806
+ this.checkStarted();
1807
+ timeout = timeout ? timeout : this.options.waitTimeout;
1808
+ return this.waitFor(function _check() {
1809
+ return this.getPageContent().indexOf(text) !== -1;
1810
+ }, then, onTimeout, timeout);
1811
+ };
1812
+
1813
+ /**
1814
+ * Waits until an element matching the provided DOM CSS3/XPath selector does not
1815
+ * exist in the remote DOM to process a next step.
1816
+ *
1817
+ * @param String selector A DOM CSS3/XPath selector
1818
+ * @param Function then The next step to perform (optional)
1819
+ * @param Function onTimeout A callback function to call on timeout (optional)
1820
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1821
+ * @return Casper
1822
+ */
1823
+ Casper.prototype.waitWhileSelector = function waitWhileSelector(selector, then, onTimeout, timeout) {
1824
+ "use strict";
1825
+ this.checkStarted();
1826
+ timeout = timeout ? timeout : this.options.waitTimeout;
1827
+ return this.waitFor(function _check() {
1828
+ return !this.exists(selector);
1829
+ }, then, onTimeout, timeout);
1830
+ };
1831
+
1832
+ /**
1833
+ * Waits until an element matching the provided DOM CSS3/XPath selector is
1834
+ * visible in the remote DOM to process a next step.
1835
+ *
1836
+ * @param String selector A DOM CSS3/XPath selector
1837
+ * @param Function then The next step to perform (optional)
1838
+ * @param Function onTimeout A callback function to call on timeout (optional)
1839
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1840
+ * @return Casper
1841
+ */
1842
+ Casper.prototype.waitUntilVisible = function waitUntilVisible(selector, then, onTimeout, timeout) {
1843
+ "use strict";
1844
+ this.checkStarted();
1845
+ timeout = timeout ? timeout : this.options.waitTimeout;
1846
+ return this.waitFor(function _check() {
1847
+ return this.visible(selector);
1848
+ }, then, onTimeout, timeout);
1849
+ };
1850
+
1851
+ /**
1852
+ * Waits until an element matching the provided DOM CSS3/XPath selector is no
1853
+ * longer visible in remote DOM to process a next step.
1854
+ *
1855
+ * @param String selector A DOM CSS3/XPath selector
1856
+ * @param Function then The next step to perform (optional)
1857
+ * @param Function onTimeout A callback function to call on timeout (optional)
1858
+ * @param Number timeout The max amount of time to wait, in milliseconds (optional)
1859
+ * @return Casper
1860
+ */
1861
+ Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, onTimeout, timeout) {
1862
+ "use strict";
1863
+ this.checkStarted();
1864
+ timeout = timeout ? timeout : this.options.waitTimeout;
1865
+ return this.waitFor(function _check() {
1866
+ return !this.visible(selector);
1867
+ }, then, onTimeout, timeout);
1868
+ };
1869
+
1870
+ /**
1871
+ * Makes the provided frame page as the currently active one. Note that the
1872
+ * active page will be reverted when finished.
1873
+ *
1874
+ * @param String|Number frameInfo Target frame name or number
1875
+ * @param Function then Next step function
1876
+ * @return Casper
1877
+ */
1878
+ Casper.prototype.withFrame = function withFrame(frameInfo, then) {
1879
+ "use strict";
1880
+ this.then(function _step() {
1881
+ if (utils.isNumber(frameInfo)) {
1882
+ if (frameInfo > this.page.childFramesCount() - 1) {
1883
+ throw new CasperError(f('Frame number "%d" is out of bounds.', frameInfo));
1884
+ }
1885
+ } else if (this.page.childFramesName().indexOf(frameInfo) === -1) {
1886
+ throw new CasperError(f('No frame named "%s" was found.', frameInfo));
1887
+ }
1888
+ // make the frame page the currently active one
1889
+ this.page.switchToChildFrame(frameInfo);
1890
+ });
1891
+ try {
1892
+ this.then(then);
1893
+ } catch (e) {
1894
+ // revert to main page on error
1895
+ this.warn("Error while processing frame step: " + e);
1896
+ this.page.switchToMainFrame();
1897
+ throw e;
1898
+ }
1899
+ return this.then(function _step() {
1900
+ // revert to main page
1901
+ this.page.switchToMainFrame();
1902
+ });
1903
+ };
1904
+
1905
+ /**
1906
+ * Makes the provided frame page as the currently active one. Note that the
1907
+ * active page will be reverted when finished.
1908
+ *
1909
+ * @param String|RegExp|WebPage popup Target frame page information
1910
+ * @param Function then Next step function
1911
+ * @return Casper
1912
+ */
1913
+ Casper.prototype.withPopup = function withPopup(popupInfo, then) {
1914
+ "use strict";
1915
+ this.then(function _step() {
1916
+ var popupPage = this.popups.find(popupInfo);
1917
+ if (!utils.isFunction(then)) {
1918
+ throw new CasperError("withPopup() requires a step function.");
1919
+ }
1920
+ // make the popup page the currently active one
1921
+ this.page = popupPage;
1922
+ });
1923
+ try {
1924
+ this.then(then);
1925
+ } catch (e) {
1926
+ // revert to main page on error
1927
+ this.log("error while processing popup step: " + e, "error");
1928
+ this.page = this.mainPage;
1929
+ throw e;
1930
+ }
1931
+ return this.then(function _step() {
1932
+ // revert to main page
1933
+ this.page = this.mainPage;
1934
+ });
1935
+ };
1936
+
1937
+ /**
1938
+ * Changes the current page zoom factor.
1939
+ *
1940
+ * @param Number factor The zoom factor
1941
+ * @return Casper
1942
+ */
1943
+ Casper.prototype.zoom = function zoom(factor) {
1944
+ "use strict";
1945
+ this.checkStarted();
1946
+ if (!utils.isNumber(factor) || factor <= 0) {
1947
+ throw new CasperError("Invalid zoom factor: " + factor);
1948
+ }
1949
+ this.page.zoomFactor = factor;
1950
+ return this;
1951
+ };
1952
+
1953
+ /**
1954
+ * Extends Casper's prototype with provided one.
1955
+ *
1956
+ * @param Object proto Prototype methods to add to Casper
1957
+ * @deprecated
1958
+ * @since 0.6
1959
+ */
1960
+ Casper.extend = function(proto) {
1961
+ "use strict";
1962
+ this.emit("deprecated", "Casper.extend() has been deprecated since 0.6; check the docs")
1963
+ if (!utils.isObject(proto)) {
1964
+ throw new CasperError("extends() only accept objects as prototypes");
1965
+ }
1966
+ utils.mergeObjects(Casper.prototype, proto);
1967
+ };
1968
+
1969
+ exports.Casper = Casper;
1970
+
1971
+ /**
1972
+ * Creates a new WebPage instance for Casper use.
1973
+ *
1974
+ * @param Casper casper A Casper instance
1975
+ * @return WebPage
1976
+ */
1977
+ function createPage(casper) {
1978
+ /*jshint maxstatements:20*/
1979
+ "use strict";
1980
+ var page = require('webpage').create();
1981
+ page.onAlert = function onAlert(message) {
1982
+ casper.log('[alert] ' + message, "info", "remote");
1983
+ casper.emit('remote.alert', message);
1984
+ if (utils.isFunction(casper.options.onAlert)) {
1985
+ casper.options.onAlert.call(casper, casper, message);
1986
+ }
1987
+ };
1988
+ page.onConfirm = function onConfirm(message) {
1989
+ if ('page.confirm' in casper._filters) {
1990
+ return casper.filter('page.confirm', message);
1991
+ }
1992
+ return true;
1993
+ };
1994
+ page.onConsoleMessage = function onConsoleMessage(msg) {
1995
+ // client utils casper console message
1996
+ var consoleTest = /^\[casper\.echo\]\s?([\s\S]*)/.exec(msg);
1997
+ if (consoleTest && consoleTest.length === 2) {
1998
+ casper.echo(consoleTest[1]);
1999
+ return; // don't trigger remote.message event for these
2000
+ }
2001
+ // client utils log messages
2002
+ var logLevel = "info",
2003
+ logTest = /^\[casper:(\w+)\]\s?([\s\S]*)/m.exec(msg);
2004
+ if (logTest && logTest.length === 3) {
2005
+ logLevel = logTest[1];
2006
+ msg = logTest[2];
2007
+ }
2008
+ casper.log(msg, logLevel, "remote");
2009
+ casper.emit('remote.message', msg);
2010
+ };
2011
+ page.onError = function onError(msg, trace) {
2012
+ casper.emit('page.error', msg, trace);
2013
+ };
2014
+ page.onInitialized = function onInitialized() {
2015
+ casper.emit('page.initialized', page);
2016
+ if (utils.isFunction(casper.options.onPageInitialized)) {
2017
+ casper.log("Post-configuring WebPage instance", "debug");
2018
+ casper.options.onPageInitialized.call(casper, page);
2019
+ }
2020
+ };
2021
+ page.onLoadStarted = function onLoadStarted() {
2022
+ casper.loadInProgress = true;
2023
+ casper.emit('load.started');
2024
+ };
2025
+ page.onLoadFinished = function onLoadFinished(status) {
2026
+ /*jshint maxstatements:20*/
2027
+ if (status !== "success") {
2028
+ casper.emit('load.failed', {
2029
+ status: status,
2030
+ http_status: casper.currentHTTPStatus,
2031
+ url: casper.requestUrl
2032
+ });
2033
+ var message = 'Loading resource failed with status=' + status;
2034
+ if (casper.currentHTTPStatus) {
2035
+ message += f(' (HTTP %d)', casper.currentHTTPStatus);
2036
+ }
2037
+ message += ': ' + casper.requestUrl;
2038
+ casper.log(message, "warning");
2039
+ casper.navigationRequested = false;
2040
+ if (utils.isFunction(casper.options.onLoadError)) {
2041
+ casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
2042
+ }
2043
+ }
2044
+ // local client scripts
2045
+ casper.injectClientScripts();
2046
+ // remote client scripts
2047
+ casper.includeRemoteScripts();
2048
+ // Client-side utils injection
2049
+ casper.injectClientUtils();
2050
+ // history
2051
+ casper.history.push(casper.getCurrentUrl());
2052
+ casper.emit('load.finished', status);
2053
+ casper.loadInProgress = false;
2054
+ };
2055
+ page.onNavigationRequested = function onNavigationRequested(url, navigationType, navigationLocked, isMainFrame) {
2056
+ casper.log(f('Navigation requested: url=%s, type=%s, lock=%s, isMainFrame=%s',
2057
+ url, navigationType, navigationLocked, isMainFrame), "debug");
2058
+ if(isMainFrame) {
2059
+ casper.navigationRequested = true;
2060
+ }
2061
+ casper.emit('navigation.requested', url, navigationType, navigationLocked, isMainFrame);
2062
+ };
2063
+ page.onPageCreated = function onPageCreated(popupPage) {
2064
+ casper.emit('popup.created', popupPage);
2065
+ popupPage.onLoadFinished = function onLoadFinished() {
2066
+ casper.popups.push(popupPage);
2067
+ casper.emit('popup.loaded', popupPage);
2068
+ };
2069
+ popupPage.onClosing = function onClosing(closedPopup) {
2070
+ casper.popups.clean(closedPopup);
2071
+ casper.emit('popup.closed', closedPopup);
2072
+ };
2073
+ };
2074
+ page.onPrompt = function onPrompt(message, value) {
2075
+ return casper.filter('page.prompt', message, value);
2076
+ };
2077
+ page.onResourceReceived = function onResourceReceived(resource) {
2078
+ http.augmentResponse(resource);
2079
+ casper.emit('resource.received', resource);
2080
+ if (utils.isFunction(casper.options.onResourceReceived)) {
2081
+ casper.options.onResourceReceived.call(casper, casper, resource);
2082
+ }
2083
+ casper.handleReceivedResource(resource);
2084
+ };
2085
+ page.onResourceRequested = function onResourceRequested(request) {
2086
+ casper.emit('resource.requested', request);
2087
+ if (request.url === casper.requestUrl) {
2088
+ casper.emit('page.resource.requested', request);
2089
+ }
2090
+ if (utils.isFunction(casper.options.onResourceRequested)) {
2091
+ casper.options.onResourceRequested.call(casper, casper, request);
2092
+ }
2093
+ };
2094
+ page.onUrlChanged = function onUrlChanged(url) {
2095
+ casper.log(f('url changed to "%s"', url), "debug");
2096
+ casper.navigationRequested = false;
2097
+ casper.emit('url.changed', url);
2098
+ };
2099
+ casper.emit('page.created', page);
2100
+ return page;
2101
+ }