Commit c8a534cd c8a534cd131a9de8a1a7b2117ddaded3386640a3 by Nicolas Perriault

refs #26 - code modularization

1 parent 83ca92a6
...@@ -25,1970 +25,17 @@ ...@@ -25,1970 +25,17 @@
25 * DEALINGS IN THE SOFTWARE. 25 * DEALINGS IN THE SOFTWARE.
26 * 26 *
27 */ 27 */
28 (function(phantom) { 28 var fs = require('fs');
29 /** 29
30 * Main Casper object. 30 function pathJoin() {
31 * 31 return Array.prototype.join.call(arguments, fs.separator);
32 * @param Object options Casper options 32 }
33 * @return Casper 33
34 */ 34 var casperLibPath = pathJoin(fs.absolute('.'), 'lib');
35 phantom.Casper = function(options) { 35 phantom.injectJs(pathJoin(casperLibPath, 'casper.js'));
36 var DEFAULT_DIE_MESSAGE = "Suite explicitely interrupted without any message given."; 36 phantom.injectJs(pathJoin(casperLibPath, 'clientutils.js'));
37 var DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 Safari/535.1"; 37 phantom.injectJs(pathJoin(casperLibPath, 'colorizer.js'));
38 // init & checks 38 phantom.injectJs(pathJoin(casperLibPath, 'injector.js'));
39 if (!(this instanceof arguments.callee)) { 39 phantom.injectJs(pathJoin(casperLibPath, 'tester.js'));
40 return new Casper(options); 40 phantom.injectJs(pathJoin(casperLibPath, 'utils.js'));
41 } 41 phantom.injectJs(pathJoin(casperLibPath, 'xunit.js'));
42 // default options
43 this.defaults = {
44 clientScripts: [],
45 faultTolerant: true,
46 logLevel: "error",
47 httpStatusHandlers: {},
48 onAlert: null,
49 onDie: null,
50 onError: null,
51 onLoadError: null,
52 onPageInitialized: null,
53 onResourceReceived: null,
54 onResourceRequested: null,
55 onStepComplete: null,
56 onStepTimeout: null,
57 onTimeout: null,
58 page: null,
59 pageSettings: { userAgent: DEFAULT_USER_AGENT },
60 stepTimeout: null,
61 timeout: null,
62 verbose: false
63 };
64 // privates
65 // local properties
66 this.checker = null;
67 this.colorizer = new phantom.Casper.Colorizer();
68 this.currentUrl = 'about:blank';
69 this.currentHTTPStatus = 200;
70 this.defaultWaitTimeout = 5000;
71 this.delayedExecution = false;
72 this.history = [];
73 this.loadInProgress = false;
74 this.logFormats = {};
75 this.logLevels = ["debug", "info", "warning", "error"];
76 this.logStyles = {
77 debug: 'INFO',
78 info: 'PARAMETER',
79 warning: 'COMMENT',
80 error: 'ERROR'
81 };
82 this.options = mergeObjects(this.defaults, options);
83 this.page = null;
84 this.requestUrl = 'about:blank';
85 this.result = {
86 log: [],
87 status: "success",
88 time: 0
89 };
90 this.started = false;
91 this.step = 0;
92 this.steps = [];
93 this.test = new phantom.Casper.Tester(this);
94 };
95
96 /**
97 * Casper prototype
98 */
99 phantom.Casper.prototype = {
100 /**
101 * Go a step back in browser's history
102 *
103 * @return Casper
104 */
105 back: function() {
106 return this.then(function(self) {
107 self.evaluate(function() {
108 history.back();
109 });
110 });
111 },
112
113 /**
114 * Encodes a resource using the base64 algorithm synchroneously using
115 * client-side XMLHttpRequest.
116 *
117 * NOTE: we cannot use window.btoa() for some strange reasons here.
118 *
119 * @param String url The url to download
120 * @return string Base64 encoded result
121 */
122 base64encode: function(url) {
123 return this.evaluate(function(url) {
124 return __utils__.getBase64(url);
125 }, { url: url });
126 },
127
128 /**
129 * Proxy method for WebPage#render. Adds a clipRect parameter for
130 * automatically set page clipRect setting values and sets it back once
131 * done. If the cliprect parameter is omitted, the full page viewport
132 * area will be rendered.
133 *
134 * @param String targetFile A target filename
135 * @param mixed clipRect An optional clipRect object (optional)
136 * @return Casper
137 */
138 capture: function(targetFile, clipRect) {
139 var previousClipRect;
140 if (clipRect) {
141 if (!isType(clipRect, "object")) {
142 throw new Error("clipRect must be an Object instance.");
143 }
144 previousClipRect = this.page.clipRect;
145 this.page.clipRect = clipRect;
146 this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug");
147 } else {
148 this.log('Capturing page to ' + targetFile, "debug");
149 }
150 try {
151 this.page.render(targetFile);
152 } catch (e) {
153 this.log('Failed to capture screenshot as ' + targetFile + ': ' + e, "error");
154 }
155 if (previousClipRect) {
156 this.page.clipRect = previousClipRect;
157 }
158 return this;
159 },
160
161 /**
162 * Captures the page area containing the provided selector.
163 *
164 * @param String targetFile Target destination file path.
165 * @param String selector CSS3 selector
166 * @return Casper
167 */
168 captureSelector: function(targetFile, selector) {
169 return this.capture(targetFile, this.evaluate(function(selector) {
170 try {
171 var clipRect = document.querySelector(selector).getBoundingClientRect();
172 return {
173 top: clipRect.top,
174 left: clipRect.left,
175 width: clipRect.width,
176 height: clipRect.height
177 };
178 } catch (e) {
179 __utils__.log("Unable to fetch bounds for element " + selector, "warning");
180 }
181 }, { selector: selector }));
182 },
183
184 /**
185 * Checks for any further navigation step to process.
186 *
187 * @param Casper self A self reference
188 * @param function onComplete An options callback to apply on completion
189 */
190 checkStep: function(self, onComplete) {
191 var step = self.steps[self.step];
192 if (!self.loadInProgress && isType(step, "function")) {
193 self.runStep(step);
194 }
195 if (!isType(step, "function") && !self.delayedExecution) {
196 self.result.time = new Date().getTime() - self.startTime;
197 self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info");
198 clearInterval(self.checker);
199 if (isType(onComplete, "function")) {
200 try {
201 onComplete.call(self, self);
202 } catch (err) {
203 self.log("could not complete final step: " + err, "error");
204 }
205 } else {
206 // default behavior is to exit phantom
207 self.exit();
208 }
209 }
210 },
211
212 /**
213 * Emulates a click on the element from the provided selector, if
214 * possible. In case of success, `true` is returned.
215 *
216 * @param String selector A DOM CSS3 compatible selector
217 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
218 * @return Boolean
219 */
220 click: function(selector, fallbackToHref) {
221 fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
222 this.log("click on selector: " + selector, "debug");
223 return this.evaluate(function(selector, fallbackToHref) {
224 return __utils__.click(selector, fallbackToHref);
225 }, {
226 selector: selector,
227 fallbackToHref: fallbackToHref
228 });
229 },
230
231 /**
232 * Creates a step definition.
233 *
234 * @param Function fn The step function to call
235 * @param Object options Step options
236 * @return Function The final step function
237 */
238 createStep: function(fn, options) {
239 if (!isType(fn, "function")) {
240 throw "createStep(): a step definition must be a function";
241 }
242 fn.options = isType(options, "object") ? options : {};
243 return fn;
244 },
245
246 /**
247 * Logs the HTML code of the current page.
248 *
249 * @return Casper
250 */
251 debugHTML: function() {
252 this.echo(this.evaluate(function() {
253 return document.body.innerHTML;
254 }));
255 return this;
256 },
257
258 /**
259 * Logs the textual contents of the current page.
260 *
261 * @return Casper
262 */
263 debugPage: function() {
264 this.echo(this.evaluate(function() {
265 return document.body.innerText;
266 }));
267 return this;
268 },
269
270 /**
271 * Exit phantom on failure, with a logged error message.
272 *
273 * @param String message An optional error message
274 * @param Number status An optional exit status code (must be > 0)
275 * @return Casper
276 */
277 die: function(message, status) {
278 this.result.status = 'error';
279 this.result.time = new Date().getTime() - this.startTime;
280 message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
281 this.log(message, "error");
282 if (isType(this.options.onDie, "function")) {
283 this.options.onDie.call(this, this, message, status);
284 }
285 return this.exit(Number(status) > 0 ? Number(status) : 1);
286 },
287
288 /**
289 * Iterates over the values of a provided array and execute a callback
290 * for each item.
291 *
292 * @param Array array
293 * @param Function fn Callback: function(self, item, index)
294 * @return Casper
295 */
296 each: function(array, fn) {
297 if (!isType(array, "array")) {
298 self.log("each() only works with arrays", "error");
299 return this;
300 }
301 (function(self) {
302 array.forEach(function(item, i) {
303 fn(self, item, i);
304 });
305 })(this);
306 return this;
307 },
308
309 /**
310 * Prints something to stdout.
311 *
312 * @param String text A string to echo to stdout
313 * @return Casper
314 */
315 echo: function(text, style) {
316 console.log(style ? this.colorizer.colorize(text, style) : text);
317 return this;
318 },
319
320 /**
321 * Evaluates an expression in the page context, a bit like what
322 * WebPage#evaluate does, but the passed function can also accept
323 * parameters if a context Object is also passed:
324 *
325 * casper.evaluate(function(username, password) {
326 * document.querySelector('#username').value = username;
327 * document.querySelector('#password').value = password;
328 * document.querySelector('#submit').click();
329 * }, {
330 * username: 'Bazoonga',
331 * password: 'baz00nga'
332 * })
333 *
334 * FIXME: waiting for a patch of PhantomJS to allow direct passing of
335 * arguments to the function.
336 * TODO: don't forget to keep this backward compatible.
337 *
338 * @param Function fn The function to be evaluated within current page DOM
339 * @param Object context Object containing the parameters to inject into the function
340 * @return mixed
341 * @see WebPage#evaluate
342 */
343 evaluate: function(fn, context) {
344 context = isType(context, "object") ? context : {};
345 var newFn = new phantom.Casper.FunctionArgsInjector(fn).process(context);
346 return this.page.evaluate(newFn);
347 },
348
349 /**
350 * Evaluates an expression within the current page DOM and die() if it
351 * returns false.
352 *
353 * @param function fn The expression to evaluate
354 * @param String message The error message to log
355 * @return Casper
356 */
357 evaluateOrDie: function(fn, message) {
358 if (!this.evaluate(fn)) {
359 return this.die(message);
360 }
361 return this;
362 },
363
364 /**
365 * Checks if an element matching the provided CSS3 selector exists in
366 * current page DOM.
367 *
368 * @param String selector A CSS3 selector
369 * @return Boolean
370 */
371 exists: function(selector) {
372 return this.evaluate(function(selector) {
373 return __utils__.exists(selector);
374 }, { selector: selector });
375 },
376
377 /**
378 * Checks if an element matching the provided CSS3 selector is visible
379 * current page DOM by checking that offsetWidth and offsetHeight are
380 * both non-zero.
381 *
382 * @param String selector A CSS3 selector
383 * @return Boolean
384 */
385 visible: function(selector) {
386 return this.evaluate(function(selector) {
387 return __utils__.visible(selector);
388 }, { selector: selector });
389 },
390
391 /**
392 * Exits phantom.
393 *
394 * @param Number status Status
395 * @return Casper
396 */
397 exit: function(status) {
398 phantom.exit(status);
399 return this;
400 },
401
402 /**
403 * Fetches innerText within the element(s) matching a given CSS3
404 * selector.
405 *
406 * @param String selector A CSS3 selector
407 * @return String
408 */
409 fetchText: function(selector) {
410 return this.evaluate(function(selector) {
411 return __utils__.fetchText(selector);
412 }, { selector: selector });
413 },
414
415 /**
416 * Fills a form with provided field values.
417 *
418 * @param String selector A CSS3 selector to the target form to fill
419 * @param Object vals Field values
420 * @param Boolean submit Submit the form?
421 */
422 fill: function(selector, vals, submit) {
423 submit = submit === true ? submit : false;
424 if (!isType(selector, "string") || !selector.length) {
425 throw "form selector must be a non-empty string";
426 }
427 if (!isType(vals, "object")) {
428 throw "form values must be provided as an object";
429 }
430 var fillResults = this.evaluate(function(selector, values) {
431 return __utils__.fill(selector, values);
432 }, {
433 selector: selector,
434 values: vals
435 });
436 if (!fillResults) {
437 throw "unable to fill form";
438 } else if (fillResults.errors.length > 0) {
439 (function(self){
440 fillResults.errors.forEach(function(error) {
441 self.log("form error: " + error, "error");
442 });
443 })(this);
444 if (submit) {
445 this.log("errors encountered while filling form; submission aborted", "warning");
446 submit = false;
447 }
448 }
449 // File uploads
450 if (fillResults.files && fillResults.files.length > 0) {
451 (function(self) {
452 fillResults.files.forEach(function(file) {
453 var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
454 self.page.uploadFile(fileFieldSelector, file.path);
455 });
456 })(this);
457 }
458 // Form submission?
459 if (submit) {
460 this.evaluate(function(selector) {
461 var form = document.querySelector(selector);
462 var method = form.getAttribute('method').toUpperCase() || "GET";
463 var action = form.getAttribute('action') || "unknown";
464 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
465 form.submit();
466 }, { selector: selector });
467 }
468 },
469
470 /**
471 * Go a step forward in browser's history
472 *
473 * @return Casper
474 */
475 forward: function(then) {
476 return this.then(function(self) {
477 self.evaluate(function() {
478 history.forward();
479 });
480 });
481 },
482
483 /**
484 * Retrieves current document url.
485 *
486 * @return String
487 */
488 getCurrentUrl: function() {
489 return decodeURIComponent(this.evaluate(function() {
490 return document.location.href;
491 }));
492 },
493
494 /**
495 * Retrieves global variable.
496 *
497 * @param String name The name of the global variable to retrieve
498 * @return mixed
499 */
500 getGlobal: function(name) {
501 var result = this.evaluate(function(name) {
502 var result = {};
503 try {
504 result.value = JSON.stringify(window[name]);
505 } catch (e) {
506 result.error = 'Unable to JSON encode window.' + name + ': ' + e;
507 }
508 return result;
509 }, {'name': name});
510 if (result.error) {
511 throw result.error;
512 } else {
513 return JSON.parse(result.value);
514 }
515 },
516
517 /**
518 * Retrieves current page title, if any.
519 *
520 * @return String
521 */
522 getTitle: function() {
523 return this.evaluate(function() {
524 return document.title;
525 });
526 },
527
528 /**
529 * Logs a message.
530 *
531 * @param String message The message to log
532 * @param String level The log message level (from Casper.logLevels property)
533 * @param String space Space from where the logged event occured (default: "phantom")
534 * @return Casper
535 */
536 log: function(message, level, space) {
537 level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
538 space = space ? space : "phantom";
539 if (level === "error" && isType(this.options.onError, "function")) {
540 this.options.onError.call(this, this, message, space);
541 }
542 if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
543 return this; // skip logging
544 }
545 var entry = {
546 level: level,
547 space: space,
548 message: message,
549 date: new Date().toString()
550 };
551 if (level in this.logFormats && isType(this.logFormats[level], "function")) {
552 message = this.logFormats[level](message, level, space);
553 } else {
554 var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]);
555 message = levelStr + ' [' + space + '] ' + message;
556 }
557 if (this.options.verbose) {
558 this.echo(message); // direct output
559 }
560 this.result.log.push(entry);
561 return this;
562 },
563
564 /**
565 * Opens a page. Takes only one argument, the url to open (using the
566 * callback argument would defeat the whole purpose of Casper
567 * actually).
568 *
569 * @param String location The url to open
570 * @return Casper
571 */
572 open: function(location, options) {
573 options = isType(options, "object") ? options : {};
574 this.requestUrl = location;
575 this.page.open(location);
576 return this;
577 },
578
579 /**
580 * Repeats a step a given number of times.
581 *
582 * @param Number times Number of times to repeat step
583 * @aram function then The step closure
584 * @return Casper
585 * @see Casper#then
586 */
587 repeat: function(times, then) {
588 for (var i = 0; i < times; i++) {
589 this.then(then);
590 }
591 return this;
592 },
593
594 /**
595 * Runs the whole suite of steps.
596 *
597 * @param function onComplete an optional callback
598 * @param Number time an optional amount of milliseconds for interval checking
599 * @return Casper
600 */
601 run: function(onComplete, time) {
602 if (!this.steps || this.steps.length < 1) {
603 this.log("No steps defined, aborting", "error");
604 return this;
605 }
606 this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info");
607 this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete);
608 return this;
609 },
610
611 /**
612 * Runs a step.
613 *
614 * @param Function step
615 */
616 runStep: function(step) {
617 var skipLog = isType(step.options, "object") && step.options.skipLog === true;
618 var stepInfo = "Step " + (this.step + 1) + "/" + this.steps.length;
619 var stepResult;
620 if (!skipLog) {
621 this.log(stepInfo + ' ' + this.getCurrentUrl() + ' (HTTP ' + this.currentHTTPStatus + ')', "info");
622 }
623 if (isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) {
624 var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) {
625 if (new Date().getTime() - start > self.options.stepTimeout) {
626 if (self.step == stepNum + 1) {
627 if (isType(self.options.onStepTimeout, "function")) {
628 self.options.onStepTimeout.call(self, self);
629 } else {
630 self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
631 }
632 }
633 clearInterval(stepTimeoutCheckInterval);
634 }
635 }, this.options.stepTimeout, this, new Date().getTime(), this.step);
636 }
637 try {
638 stepResult = step.call(this, this);
639 } catch (e) {
640 if (this.options.faultTolerant) {
641 this.log("Step error: " + e, "error");
642 } else {
643 throw e;
644 }
645 }
646 if (isType(this.options.onStepComplete, "function")) {
647 this.options.onStepComplete.call(this, this, stepResult);
648 }
649 if (!skipLog) {
650 this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info");
651 }
652 this.step++;
653 },
654
655 /**
656 * Configures and starts Casper.
657 *
658 * @param String location An optional location to open on start
659 * @param function then Next step function to execute on page loaded (optional)
660 * @return Casper
661 */
662 start: function(location, then) {
663 if (this.started) {
664 this.log("start failed: Casper has already started!", "error");
665 }
666 this.log('Starting...', "info");
667 this.startTime = new Date().getTime();
668 this.steps = [];
669 this.step = 0;
670 // Option checks
671 if (this.logLevels.indexOf(this.options.logLevel) < 0) {
672 this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning");
673 this.options.logLevel = "warning";
674 }
675 // WebPage
676 if (!isWebPage(this.page)) {
677 if (isWebPage(this.options.page)) {
678 this.page = this.options.page;
679 } else {
680 this.page = createPage(this);
681 }
682 }
683 this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings);
684 if (isType(this.options.clipRect, "object")) {
685 this.page.clipRect = this.options.clipRect;
686 }
687 if (isType(this.options.viewportSize, "object")) {
688 this.page.viewportSize = this.options.viewportSize;
689 }
690 this.started = true;
691 if (isType(this.options.timeout, "number") && this.options.timeout > 0) {
692 this.log("Execution timeout set to " + this.options.timeout + 'ms', "info");
693 setTimeout(function(self) {
694 if (isType(self.options.onTimeout, "function")) {
695 self.options.onTimeout.call(self, self);
696 } else {
697 self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting.");
698 }
699 }, this.options.timeout, this);
700 }
701 if (isType(this.options.onPageInitialized, "function")) {
702 this.log("Post-configuring WebPage instance", "debug");
703 this.options.onPageInitialized.call(this, this.page);
704 }
705 if (isType(location, "string") && location.length > 0) {
706 return this.thenOpen(location, isType(then, "function") ? then : this.createStep(function(self) {
707 self.log("start page is loaded", "debug");
708 }));
709 }
710 return this;
711 },
712
713 /**
714 * Schedules the next step in the navigation process.
715 *
716 * @param function step A function to be called as a step
717 * @return Casper
718 */
719 then: function(step) {
720 if (!this.started) {
721 throw "Casper not started; please use Casper#start";
722 }
723 if (!isType(step, "function")) {
724 throw "You can only define a step as a function";
725 }
726 this.steps.push(step);
727 return this;
728 },
729
730 /**
731 * Adds a new navigation step for clicking on a provided link selector
732 * and execute an optional next step.
733 *
734 * @param String selector A DOM CSS3 compatible selector
735 * @param Function then Next step function to execute on page loaded (optional)
736 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
737 * @return Casper
738 * @see Casper#click
739 * @see Casper#then
740 */
741 thenClick: function(selector, then, fallbackToHref) {
742 this.then(function(self) {
743 self.click(selector, fallbackToHref);
744 });
745 return isType(then, "function") ? this.then(then) : this;
746 },
747
748 /**
749 * Adds a new navigation step to perform code evaluation within the
750 * current retrieved page DOM.
751 *
752 * @param function fn The function to be evaluated within current page DOM
753 * @param object context Optional function parameters context
754 * @return Casper
755 * @see Casper#evaluate
756 */
757 thenEvaluate: function(fn, context) {
758 return this.then(function(self) {
759 self.evaluate(fn, context);
760 });
761 },
762
763 /**
764 * Adds a new navigation step for opening the provided location.
765 *
766 * @param String location The URL to load
767 * @param function then Next step function to execute on page loaded (optional)
768 * @return Casper
769 * @see Casper#open
770 */
771 thenOpen: function(location, then) {
772 this.then(this.createStep(function(self) {
773 self.open(location);
774 }, {
775 skipLog: true
776 }));
777 return isType(then, "function") ? this.then(then) : this;
778 },
779
780 /**
781 * Adds a new navigation step for opening and evaluate an expression
782 * against the DOM retrieved from the provided location.
783 *
784 * @param String location The url to open
785 * @param function fn The function to be evaluated within current page DOM
786 * @param object context Optional function parameters context
787 * @return Casper
788 * @see Casper#evaluate
789 * @see Casper#open
790 */
791 thenOpenAndEvaluate: function(location, fn, context) {
792 return this.thenOpen(location).thenEvaluate(fn, context);
793 },
794
795 /**
796 * Changes the current viewport size.
797 *
798 * @param Number width The viewport width, in pixels
799 * @param Number height The viewport height, in pixels
800 * @return Casper
801 */
802 viewport: function(width, height) {
803 if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) {
804 throw new Error("Invalid viewport width/height set: " + width + 'x' + height);
805 }
806 this.page.viewportSize = {
807 width: width,
808 height: height
809 };
810 return this;
811 },
812
813 /**
814 * Adds a new step that will wait for a given amount of time (expressed
815 * in milliseconds) before processing an optional next one.
816 *
817 * @param Number timeout The max amount of time to wait, in milliseconds
818 * @param Function then Next step to process (optional)
819 * @return Casper
820 */
821 wait: function(timeout, then) {
822 timeout = Number(timeout, 10);
823 if (!isType(timeout, "number") || timeout < 1) {
824 this.die("wait() only accepts a positive integer > 0 as a timeout value");
825 }
826 if (then && !isType(then, "function")) {
827 this.die("wait() a step definition must be a function");
828 }
829 return this.then(function(self) {
830 self.delayedExecution = true;
831 var start = new Date().getTime();
832 var interval = setInterval(function(self, then) {
833 if (new Date().getTime() - start > timeout) {
834 self.delayedExecution = false;
835 self.log("wait() finished wating for " + timeout + "ms.", "info");
836 if (then) {
837 self.then(then);
838 }
839 clearInterval(interval);
840 }
841 }, 100, self, then);
842 });
843 },
844
845 /**
846 * Waits until a function returns true to process a next step.
847 *
848 * @param Function testFx A function to be evaluated for returning condition satisfecit
849 * @param Function then The next step to perform (optional)
850 * @param Function onTimeout A callback function to call on timeout (optional)
851 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
852 * @return Casper
853 */
854 waitFor: function(testFx, then, onTimeout, timeout) {
855 timeout = timeout ? timeout : this.defaultWaitTimeout;
856 if (!isType(testFx, "function")) {
857 this.die("waitFor() needs a test function");
858 }
859 if (then && !isType(then, "function")) {
860 this.die("waitFor() next step definition must be a function");
861 }
862 this.delayedExecution = true;
863 var start = new Date().getTime();
864 var condition = false;
865 var interval = setInterval(function(self, testFx, onTimeout) {
866 if ((new Date().getTime() - start < timeout) && !condition) {
867 condition = testFx(self);
868 } else {
869 self.delayedExecution = false;
870 if (!condition) {
871 self.log("Casper.waitFor() timeout", "warning");
872 if (isType(onTimeout, "function")) {
873 onTimeout.call(self, self);
874 } else {
875 self.die("Expired timeout, exiting.", "error");
876 }
877 clearInterval(interval);
878 } else {
879 self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info");
880 if (then) {
881 self.then(then);
882 }
883 clearInterval(interval);
884 }
885 }
886 }, 100, this, testFx, onTimeout);
887 return this;
888 },
889
890 /**
891 * Waits until an element matching the provided CSS3 selector exists in
892 * remote DOM to process a next step.
893 *
894 * @param String selector A CSS3 selector
895 * @param Function then The next step to perform (optional)
896 * @param Function onTimeout A callback function to call on timeout (optional)
897 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
898 * @return Casper
899 */
900 waitForSelector: function(selector, then, onTimeout, timeout) {
901 timeout = timeout ? timeout : this.defaultWaitTimeout;
902 return this.waitFor(function(self) {
903 return self.exists(selector);
904 }, then, onTimeout, timeout);
905 },
906
907 /**
908 * Waits until an element matching the provided CSS3 selector does not
909 * exist in the remote DOM to process a next step.
910 *
911 * @param String selector A CSS3 selector
912 * @param Function then The next step to perform (optional)
913 * @param Function onTimeout A callback function to call on timeout (optional)
914 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
915 * @return Casper
916 */
917 waitWhileSelector: function(selector, then, onTimeout, timeout) {
918 timeout = timeout ? timeout : this.defaultWaitTimeout;
919 return this.waitFor(function(self) {
920 return !self.exists(selector);
921 }, then, onTimeout, timeout);
922 },
923
924 /**
925 * Waits until an element matching the provided CSS3 selector is
926 * visible in the remote DOM to process a next step.
927 *
928 * @param String selector A CSS3 selector
929 * @param Function then The next step to perform (optional)
930 * @param Function onTimeout A callback function to call on timeout (optional)
931 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
932 * @return Casper
933 */
934 waitUntilVisible: function(selector, then, onTimeout, timeout) {
935 timeout = timeout ? timeout : this.defaultWaitTimeout;
936 return this.waitFor(function(self) {
937 return self.visible(selector);
938 }, then, onTimeout, timeout);
939 },
940
941 /**
942 * Waits until an element matching the provided CSS3 selector is no
943 * longer visible in remote DOM to process a next step.
944 *
945 * @param String selector A CSS3 selector
946 * @param Function then The next step to perform (optional)
947 * @param Function onTimeout A callback function to call on timeout (optional)
948 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
949 * @return Casper
950 */
951 waitWhileVisible: function(selector, then, onTimeout, timeout) {
952 timeout = timeout ? timeout : this.defaultWaitTimeout;
953 return this.waitFor(function(self) {
954 return !self.visible(selector);
955 }, then, onTimeout, timeout);
956 }
957 };
958
959 /**
960 * Extends Casper's prototype with provided one.
961 *
962 * @param Object proto Prototype methods to add to Casper
963 */
964 phantom.Casper.extend = function(proto) {
965 if (!isType(proto, "object")) {
966 throw "extends() only accept objects as prototypes";
967 }
968 mergeObjects(phantom.Casper.prototype, proto);
969 };
970
971 /**
972 * Casper client-side helpers.
973 */
974 phantom.Casper.ClientUtils = function() {
975 /**
976 * Clicks on the DOM element behind the provided selector.
977 *
978 * @param String selector A CSS3 selector to the element to click
979 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
980 * @return Boolean
981 */
982 this.click = function(selector, fallbackToHref) {
983 fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref;
984 var elem = this.findOne(selector);
985 if (!elem) {
986 return false;
987 }
988 var evt = document.createEvent("MouseEvents");
989 evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem);
990 if (elem.dispatchEvent(evt)) {
991 return true;
992 }
993 if (fallbackToHref && elem.hasAttribute('href')) {
994 document.location = elem.getAttribute('href');
995 return true;
996 }
997 return false;
998 };
999
1000 /**
1001 * Base64 encodes a string, even binary ones. Succeeds where
1002 * window.btoa() fails.
1003 *
1004 * @param String str
1005 * @return string
1006 */
1007 this.encode = function(str) {
1008 var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1009 var out = "", i = 0, len = str.length, c1, c2, c3;
1010 while (i < len) {
1011 c1 = str.charCodeAt(i++) & 0xff;
1012 if (i == len) {
1013 out += CHARS.charAt(c1 >> 2);
1014 out += CHARS.charAt((c1 & 0x3) << 4);
1015 out += "==";
1016 break;
1017 }
1018 c2 = str.charCodeAt(i++);
1019 if (i == len) {
1020 out += CHARS.charAt(c1 >> 2);
1021 out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
1022 out += CHARS.charAt((c2 & 0xF) << 2);
1023 out += "=";
1024 break;
1025 }
1026 c3 = str.charCodeAt(i++);
1027 out += CHARS.charAt(c1 >> 2);
1028 out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
1029 out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
1030 out += CHARS.charAt(c3 & 0x3F);
1031 }
1032 return out;
1033 };
1034
1035 /**
1036 * Checks if a given DOM element exists in remote page.
1037 *
1038 * @param String selector CSS3 selector
1039 * @return Boolean
1040 */
1041 this.exists = function(selector) {
1042 try {
1043 return document.querySelectorAll(selector).length > 0;
1044 } catch (e) {
1045 return false;
1046 }
1047 };
1048
1049 /**
1050 * Checks if a given DOM element is visible in remote page.
1051 *
1052 * @param String selector CSS3 selector
1053 * @return Boolean
1054 */
1055 this.visible = function(selector) {
1056 try {
1057 var el = document.querySelector(selector);
1058 return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0;
1059 } catch (e) {
1060 return false;
1061 }
1062 };
1063
1064 /**
1065 * Fetches innerText within the element(s) matching a given CSS3
1066 * selector.
1067 *
1068 * @param String selector A CSS3 selector
1069 * @return String
1070 */
1071 this.fetchText = function(selector) {
1072 var text = '', elements = this.findAll(selector);
1073 if (elements && elements.length) {
1074 Array.prototype.forEach.call(elements, function(element) {
1075 text += element.innerText;
1076 });
1077 }
1078 return text;
1079 };
1080
1081 /**
1082 * Fills a form with provided field values, and optionnaly submits it.
1083 *
1084 * @param HTMLElement|String form A form element, or a CSS3 selector to a form element
1085 * @param Object vals Field values
1086 * @return Object An object containing setting result for each field, including file uploads
1087 */
1088 this.fill = function(form, vals) {
1089 var out = {
1090 errors: [],
1091 fields: [],
1092 files: []
1093 };
1094 if (!(form instanceof HTMLElement) || typeof form === "string") {
1095 __utils__.log("attempting to fetch form element from selector: '" + form + "'", "info");
1096 try {
1097 form = document.querySelector(form);
1098 } catch (e) {
1099 if (e.name === "SYNTAX_ERR") {
1100 out.errors.push("invalid form selector provided: '" + form + "'");
1101 return out;
1102 }
1103 }
1104 }
1105 if (!form) {
1106 out.errors.push("form not found");
1107 return out;
1108 }
1109 for (var name in vals) {
1110 if (!vals.hasOwnProperty(name)) {
1111 continue;
1112 }
1113 var field = form.querySelectorAll('[name="' + name + '"]');
1114 var value = vals[name];
1115 if (!field) {
1116 out.errors.push('no field named "' + name + '" in form');
1117 continue;
1118 }
1119 try {
1120 out.fields[name] = this.setField(field, value);
1121 } catch (err) {
1122 if (err.name === "FileUploadError") {
1123 out.files.push({
1124 name: name,
1125 path: err.path
1126 });
1127 } else {
1128 this.log(err, "error");
1129 throw err;
1130 }
1131 }
1132 }
1133 return out;
1134 };
1135
1136 /**
1137 * Finds all DOM elements matching by the provided selector.
1138 *
1139 * @param String selector CSS3 selector
1140 * @return NodeList|undefined
1141 */
1142 this.findAll = function(selector) {
1143 try {
1144 return document.querySelectorAll(selector);
1145 } catch (e) {
1146 this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
1147 }
1148 };
1149
1150 /**
1151 * Finds a DOM element by the provided selector.
1152 *
1153 * @param String selector CSS3 selector
1154 * @return HTMLElement|undefined
1155 */
1156 this.findOne = function(selector) {
1157 try {
1158 return document.querySelector(selector);
1159 } catch (e) {
1160 this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors");
1161 }
1162 };
1163
1164 /**
1165 * Downloads a resource behind an url and returns its base64-encoded
1166 * contents.
1167 *
1168 * @param String url The resource url
1169 * @return String Base64 contents string
1170 */
1171 this.getBase64 = function(url) {
1172 return this.encode(this.getBinary(url));
1173 };
1174
1175 /**
1176 * Retrieves string contents from a binary file behind an url. Silently
1177 * fails but log errors.
1178 *
1179 * @param String url
1180 * @return string
1181 */
1182 this.getBinary = function(url) {
1183 try {
1184 var xhr = new XMLHttpRequest();
1185 xhr.open("GET", url, false);
1186 xhr.overrideMimeType("text/plain; charset=x-user-defined");
1187 xhr.send(null);
1188 return xhr.responseText;
1189 } catch (e) {
1190 if (e.name === "NETWORK_ERR" && e.code === 101) {
1191 this.log("unfortunately, casperjs cannot make cross domain ajax requests", "warning");
1192 }
1193 this.log("error while fetching " + url + ": " + e, "error");
1194 return "";
1195 }
1196 };
1197
1198 /**
1199 * Logs a message.
1200 *
1201 * @param String message
1202 * @param String level
1203 */
1204 this.log = function(message, level) {
1205 console.log("[casper:" + (level || "debug") + "] " + message);
1206 };
1207
1208 /**
1209 * Sets a field (or a set of fields) value. Fails silently, but log
1210 * error messages.
1211 *
1212 * @param HTMLElement|NodeList field One or more element defining a field
1213 * @param mixed value The field value to set
1214 */
1215 this.setField = function(field, value) {
1216 var fields, out;
1217 value = value || "";
1218 if (field instanceof NodeList) {
1219 fields = field;
1220 field = fields[0];
1221 }
1222 if (!field instanceof HTMLElement) {
1223 this.log("invalid field type; only HTMLElement and NodeList are supported", "error");
1224 }
1225 this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug");
1226 try {
1227 field.focus();
1228 } catch (e) {
1229 __utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
1230 }
1231 var nodeName = field.nodeName.toLowerCase();
1232 switch (nodeName) {
1233 case "input":
1234 var type = field.getAttribute('type') || "text";
1235 switch (type.toLowerCase()) {
1236 case "color":
1237 case "date":
1238 case "datetime":
1239 case "datetime-local":
1240 case "email":
1241 case "hidden":
1242 case "month":
1243 case "number":
1244 case "password":
1245 case "range":
1246 case "search":
1247 case "tel":
1248 case "text":
1249 case "time":
1250 case "url":
1251 case "week":
1252 field.value = value;
1253 break;
1254 case "checkbox":
1255 if (fields.length > 1) {
1256 var values = value;
1257 if (!Array.isArray(values)) {
1258 values = [values];
1259 }
1260 Array.prototype.forEach.call(fields, function(f) {
1261 f.checked = values.indexOf(f.value) !== -1 ? true : false;
1262 });
1263 } else {
1264 field.checked = value ? true : false;
1265 }
1266 break;
1267 case "file":
1268 throw {
1269 name: "FileUploadError",
1270 message: "file field must be filled using page.uploadFile",
1271 path: value
1272 };
1273 case "radio":
1274 if (fields) {
1275 Array.prototype.forEach.call(fields, function(e) {
1276 e.checked = (e.value === value);
1277 });
1278 } else {
1279 out = 'provided radio elements are empty';
1280 }
1281 break;
1282 default:
1283 out = "unsupported input field type: " + type;
1284 break;
1285 }
1286 break;
1287 case "select":
1288 case "textarea":
1289 field.value = value;
1290 break;
1291 default:
1292 out = 'unsupported field type: ' + nodeName;
1293 break;
1294 }
1295 try {
1296 field.blur();
1297 } catch (err) {
1298 __utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
1299 }
1300 return out;
1301 };
1302 };
1303
1304 /**
1305 * This is a port of lime colorizer.
1306 * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php)
1307 *
1308 * (c) Fabien Potencier, Symfony project, MIT license
1309 */
1310 phantom.Casper.Colorizer = function() {
1311 var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 };
1312 var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 };
1313 var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 };
1314 var styles = {
1315 'ERROR': { bg: 'red', fg: 'white', bold: true },
1316 'INFO': { fg: 'green', bold: true },
1317 'TRACE': { fg: 'green', bold: true },
1318 'PARAMETER': { fg: 'cyan' },
1319 'COMMENT': { fg: 'yellow' },
1320 'WARNING': { fg: 'red', bold: true },
1321 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true },
1322 'RED_BAR': { fg: 'white', bg: 'red', bold: true },
1323 'INFO_BAR': { fg: 'cyan', bold: true }
1324 };
1325
1326 /**
1327 * Adds a style to provided text.
1328 *
1329 * @params String text
1330 * @params String styleName
1331 * @return String
1332 */
1333 this.colorize = function(text, styleName) {
1334 if (styleName in styles) {
1335 return this.format(text, styles[styleName]);
1336 }
1337 return text;
1338 };
1339
1340 /**
1341 * Formats a text using a style declaration object.
1342 *
1343 * @param String text
1344 * @param Object style
1345 * @return String
1346 */
1347 this.format = function(text, style) {
1348 if (typeof style !== "object") {
1349 return text;
1350 }
1351 var codes = [];
1352 if (style.fg && foreground[style.fg]) {
1353 codes.push(foreground[style.fg]);
1354 }
1355 if (style.bg && background[style.bg]) {
1356 codes.push(background[style.bg]);
1357 }
1358 for (var option in options) {
1359 if (style[option] === true) {
1360 codes.push(options[option]);
1361 }
1362 }
1363 return "\033[" + codes.join(';') + 'm' + text + "\033[0m";
1364 };
1365 };
1366
1367 /**
1368 * Casper tester: makes assertions, stores test results and display then.
1369 *
1370 */
1371 phantom.Casper.Tester = function(casper, options) {
1372 this.options = isType(options, "object") ? options : {};
1373 if (!casper instanceof phantom.Casper) {
1374 throw "phantom.Casper.Tester needs a phantom.Casper instance";
1375 }
1376
1377 // locals
1378 var exporter = new phantom.Casper.XUnitExporter();
1379 var PASS = this.options.PASS || "PASS";
1380 var FAIL = this.options.FAIL || "FAIL";
1381
1382 function compareArrays(a, b) {
1383 if (a.length !== b.length) {
1384 return false;
1385 }
1386 a.forEach(function(item, i) {
1387 if (isType(item, "array") && !compareArrays(item, b[i])) {
1388 return false;
1389 }
1390 if (item !== b[i]) {
1391 return false;
1392 }
1393 });
1394 return true;
1395 }
1396
1397 // properties
1398 this.testResults = {
1399 passed: 0,
1400 failed: 0
1401 };
1402
1403 // methods
1404 /**
1405 * Asserts a condition resolves to true.
1406 *
1407 * @param Boolean condition
1408 * @param String message Test description
1409 */
1410 this.assert = function(condition, message) {
1411 var status = PASS;
1412 if (condition === true) {
1413 style = 'INFO';
1414 this.testResults.passed++;
1415 exporter.addSuccess("unknown", message);
1416 } else {
1417 status = FAIL;
1418 style = 'RED_BAR';
1419 this.testResults.failed++;
1420 exporter.addFailure("unknown", message, 'test failed', "assert");
1421 }
1422 casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' '));
1423 };
1424
1425 /**
1426 * Asserts that two values are strictly equals.
1427 *
1428 * @param Mixed testValue The value to test
1429 * @param Mixed expected The expected value
1430 * @param String message Test description
1431 */
1432 this.assertEquals = function(testValue, expected, message) {
1433 if (this.testEquals(testValue, expected)) {
1434 casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message));
1435 this.testResults.passed++;
1436 exporter.addSuccess("unknown", message);
1437 } else {
1438 casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING'));
1439 this.comment(' got: ' + testValue);
1440 this.comment(' expected: ' + expected);
1441 this.testResults.failed++;
1442 exporter.addFailure("unknown", message, "test failed; expected: " + expected + "; got: " + testValue, "assertEquals");
1443 }
1444 };
1445
1446 /**
1447 * Asserts that a code evaluation in remote DOM resolves to true.
1448 *
1449 * @param Function fn A function to be evaluated in remote DOM
1450 * @param String message Test description
1451 */
1452 this.assertEval = function(fn, message) {
1453 return this.assert(casper.evaluate(fn), message);
1454 };
1455
1456 /**
1457 * Asserts that the result of a code evaluation in remote DOM equals
1458 * an expected value.
1459 *
1460 * @param Function fn The function to be evaluated in remote DOM
1461 * @param Boolean expected The expected value
1462 * @param String message Test description
1463 */
1464 this.assertEvalEquals = function(fn, expected, message) {
1465 return this.assertEquals(casper.evaluate(fn), expected, message);
1466 };
1467
1468 /**
1469 * Asserts that an element matching the provided CSS3 selector exists in
1470 * remote DOM.
1471 *
1472 * @param String selector CSS3 selectore
1473 * @param String message Test description
1474 */
1475 this.assertExists = function(selector, message) {
1476 return this.assert(casper.exists(selector), message);
1477 };
1478
1479 /**
1480 * Asserts that a provided string matches a provided RegExp pattern.
1481 *
1482 * @param String subject The string to test
1483 * @param RegExp pattern A RegExp object instance
1484 * @param String message Test description
1485 */
1486 this.assertMatch = function(subject, pattern, message) {
1487 if (pattern.test(subject)) {
1488 casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message));
1489 this.testResults.passed++;
1490 exporter.addSuccess("unknown", message);
1491 } else {
1492 casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING'));
1493 this.comment(' subject: ' + subject);
1494 this.comment(' pattern: ' + pattern.toString());
1495 this.testResults.failed++;
1496 exporter.addFailure("unknown", message, "test failed; subject: " + subject + "; pattern: " + pattern.toString(), "assertMatch");
1497 }
1498 };
1499
1500 /**
1501 * Asserts a condition resolves to false.
1502 *
1503 * @param Boolean condition
1504 * @param String message Test description
1505 */
1506 this.assertNot = function(condition, message) {
1507 return this.assert(!condition, message);
1508 };
1509
1510 /**
1511 * Asserts that the provided function called with the given parameters
1512 * will raise an exception.
1513 *
1514 * @param Function fn The function to test
1515 * @param Array args The arguments to pass to the function
1516 * @param String message Test description
1517 */
1518 this.assertRaises = function(fn, args, message) {
1519 try {
1520 fn.apply(null, args);
1521 this.fail(message);
1522 } catch (e) {
1523 this.pass(message);
1524 }
1525 };
1526
1527 /**
1528 * Asserts that at least an element matching the provided CSS3 selector
1529 * exists in remote DOM.
1530 *
1531 * @param String selector A CSS3 selector string
1532 * @param String message Test description
1533 */
1534 this.assertSelectorExists = function(selector, message) {
1535 return this.assert(this.exists(selector), message);
1536 };
1537
1538 /**
1539 * Asserts that title of the remote page equals to the expected one.
1540 *
1541 * @param String expected The expected title string
1542 * @param String message Test description
1543 */
1544 this.assertTitle = function(expected, message) {
1545 return this.assertEquals(casper.getTitle(), expected, message);
1546 };
1547
1548 /**
1549 * Asserts that the provided input is of the given type.
1550 *
1551 * @param mixed input The value to test
1552 * @param String type The javascript type name
1553 * @param String message Test description
1554 */
1555 this.assertType = function(input, type, message) {
1556 return this.assertEquals(betterTypeOf(input), type, message);
1557 };
1558
1559 /**
1560 * Asserts that a the current page url matches the provided RegExp
1561 * pattern.
1562 *
1563 * @param RegExp pattern A RegExp object instance
1564 * @param String message Test description
1565 */
1566 this.assertUrlMatch = function(pattern, message) {
1567 return this.assertMatch(casper.getCurrentUrl(), pattern, message);
1568 };
1569
1570 /**
1571 * Render a colorized output. Basically a proxy method for
1572 * Casper.Colorizer#colorize()
1573 */
1574 this.colorize = function(message, style) {
1575 return casper.colorizer.colorize(message, style);
1576 };
1577
1578 /**
1579 * Writes a comment-style formatted message to stdout.
1580 *
1581 * @param String message
1582 */
1583 this.comment = function(message) {
1584 casper.echo('# ' + message, 'COMMENT');
1585 };
1586
1587 /**
1588 * Tests equality between the two passed arguments.
1589 *
1590 * @param Mixed v1
1591 * @param Mixed v2
1592 * @param Boolean
1593 */
1594 this.testEquals = function(v1, v2) {
1595 if (betterTypeOf(v1) !== betterTypeOf(v2)) {
1596 return false;
1597 }
1598 if (isType(v1, "function")) {
1599 return v1.toString() === v2.toString();
1600 }
1601 if (v1 instanceof Object && v2 instanceof Object) {
1602 if (Object.keys(v1).length !== Object.keys(v2).length) {
1603 return false;
1604 }
1605 for (var k in v1) {
1606 if (!this.testEquals(v1[k], v2[k])) {
1607 return false;
1608 }
1609 }
1610 return true;
1611 }
1612 return v1 === v2;
1613 };
1614
1615 /**
1616 * Writes an error-style formatted message to stdout.
1617 *
1618 * @param String message
1619 */
1620 this.error = function(message) {
1621 casper.echo(message, 'ERROR');
1622 };
1623
1624 /**
1625 * Adds a failed test entry to the stack.
1626 *
1627 * @param String message
1628 */
1629 this.fail = function(message) {
1630 this.assert(false, message);
1631 };
1632
1633 /**
1634 * Formats a message to highlight some parts of it.
1635 *
1636 * @param String message
1637 * @param String style
1638 */
1639 this.formatMessage = function(message, style) {
1640 var parts = /([a-z0-9_\.]+\(\))(.*)/i.exec(message);
1641 if (!parts) {
1642 return message;
1643 }
1644 return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style);
1645 };
1646
1647 /**
1648 * Writes an info-style formatted message to stdout.
1649 *
1650 * @param String message
1651 */
1652 this.info = function(message) {
1653 casper.echo(message, 'PARAMETER');
1654 };
1655
1656 /**
1657 * Adds a successful test entry to the stack.
1658 *
1659 * @param String message
1660 */
1661 this.pass = function(message) {
1662 this.assert(true, message);
1663 };
1664
1665 /**
1666 * Render tests results, an optionnaly exit phantomjs.
1667 *
1668 * @param Boolean exit
1669 */
1670 this.renderResults = function(exit, status, save) {
1671 save = isType(save, "string") ? save : this.options.save;
1672 var total = this.testResults.passed + this.testResults.failed, statusText, style, result;
1673 if (this.testResults.failed > 0) {
1674 statusText = FAIL;
1675 style = 'RED_BAR';
1676 } else {
1677 statusText = PASS;
1678 style = 'GREEN_BAR';
1679 }
1680 result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.';
1681 if (result.length < 80) {
1682 result += new Array(80 - result.length + 1).join(' ');
1683 }
1684 casper.echo(this.colorize(result, style));
1685 if (save && isType(require, "function")) {
1686 try {
1687 require('fs').write(save, exporter.getXML(), 'w');
1688 casper.echo('result log stored in ' + save, 'INFO');
1689 } catch (e) {
1690 casper.echo('unable to write results to ' + save + '; ' + e, 'ERROR');
1691 }
1692 }
1693 if (exit === true) {
1694 casper.exit(status || 0);
1695 }
1696 };
1697 };
1698
1699 /**
1700 * Function argument injector.
1701 *
1702 */
1703 phantom.Casper.FunctionArgsInjector = function(fn) {
1704 if (!isType(fn, "function")) {
1705 throw "FunctionArgsInjector() can only process functions";
1706 }
1707 this.fn = fn;
1708
1709 this.extract = function(fn) {
1710 var match = /^function\s?(\w+)?\s?\((.*)\)\s?\{([\s\S]*)\}/i.exec(fn.toString().trim());
1711 if (match && match.length > 1) {
1712 var args = match[2].split(',').map(function(arg) {
1713 return arg.replace(new RegExp(/\/\*+.*\*\//ig), "").trim();
1714 }).filter(function(arg) {
1715 return arg;
1716 }) || [];
1717 return {
1718 name: match[1] ? match[1].trim() : null,
1719 args: args,
1720 body: match[3] ? match[3].trim() : ''
1721 };
1722 }
1723 };
1724
1725 this.process = function(values) {
1726 var fnObj = this.extract(this.fn);
1727 if (!isType(fnObj, "object")) {
1728 throw "Unable to process function " + this.fn.toString();
1729 }
1730 var inject = this.getArgsInjectionString(fnObj.args, values);
1731 return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}';
1732 };
1733
1734 this.getArgsInjectionString = function(args, values) {
1735 values = typeof values === "object" ? values : {};
1736 var jsonValues = escape(encodeURIComponent(JSON.stringify(values)));
1737 var inject = [
1738 'var __casper_params__ = JSON.parse(decodeURIComponent(unescape(\'' + jsonValues + '\')));'
1739 ];
1740 args.forEach(function(arg) {
1741 if (arg in values) {
1742 inject.push('var ' + arg + '=__casper_params__["' + arg + '"];');
1743 }
1744 });
1745 return inject.join('\n') + '\n';
1746 };
1747 };
1748
1749 /**
1750 * JUnit XML (xUnit) exporter for test results.
1751 *
1752 */
1753 phantom.Casper.XUnitExporter = function() {
1754 var node = function(name, attributes) {
1755 var node = document.createElement(name);
1756 for (var attrName in attributes) {
1757 var value = attributes[attrName];
1758 if (attributes.hasOwnProperty(attrName) && isType(attrName, "string")) {
1759 node.setAttribute(attrName, value);
1760 }
1761 }
1762 return node;
1763 };
1764
1765 var xml = node('testsuite');
1766 xml.toString = function() {
1767 return this.outerHTML; // ouch
1768 };
1769
1770 /**
1771 * Adds a successful test result
1772 *
1773 * @param String classname
1774 * @param String name
1775 */
1776 this.addSuccess = function(classname, name) {
1777 xml.appendChild(node('testcase', {
1778 classname: classname,
1779 name: name
1780 }));
1781 };
1782
1783 /**
1784 * Adds a failed test result
1785 *
1786 * @param String classname
1787 * @param String name
1788 * @param String message
1789 * @param String type
1790 */
1791 this.addFailure = function(classname, name, message, type) {
1792 var fnode = node('testcase', {
1793 classname: classname,
1794 name: name
1795 });
1796 var failure = node('failure', {
1797 type: type || "unknown"
1798 });
1799 failure.appendChild(document.createTextNode(message || "no message left"));
1800 fnode.appendChild(failure);
1801 xml.appendChild(fnode);
1802 };
1803
1804 /**
1805 * Retrieves generated XML object - actually an HTMLElement.
1806 *
1807 * @return HTMLElement
1808 */
1809 this.getXML = function() {
1810 return xml;
1811 };
1812 };
1813
1814 /**
1815 * Provides a better typeof operator equivalent, able to retrieve the array
1816 * type.
1817 *
1818 * @param mixed input
1819 * @return String
1820 * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
1821 */
1822 function betterTypeOf(input) {
1823 try {
1824 return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase();
1825 } catch (e) {
1826 return typeof input;
1827 }
1828 }
1829
1830 /**
1831 * Creates a new WebPage instance for Casper use.
1832 *
1833 * @param Casper casper A Casper instance
1834 * @return WebPage
1835 */
1836 function createPage(casper) {
1837 var page;
1838 if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
1839 page = new WebPage();
1840 } else {
1841 page = require('webpage').create();
1842 }
1843 page.onAlert = function(message) {
1844 casper.log('[alert] ' + message, "info", "remote");
1845 if (isType(casper.options.onAlert, "function")) {
1846 casper.options.onAlert.call(casper, casper, message);
1847 }
1848 };
1849 page.onConsoleMessage = function(msg) {
1850 var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg);
1851 if (test && test.length === 3) {
1852 level = test[1];
1853 msg = test[2];
1854 }
1855 casper.log(msg, level, "remote");
1856 };
1857 page.onLoadStarted = function() {
1858 casper.loadInProgress = true;
1859 };
1860 page.onLoadFinished = function(status) {
1861 if (status !== "success") {
1862 var message = 'Loading resource failed with status=' + status;
1863 if (casper.currentHTTPStatus) {
1864 message += ' (HTTP ' + casper.currentHTTPStatus + ')';
1865 }
1866 message += ': ' + casper.requestUrl;
1867 casper.log(message, "warning");
1868 if (isType(casper.options.onLoadError, "function")) {
1869 casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
1870 }
1871 }
1872 if (casper.options.clientScripts) {
1873 if (betterTypeOf(casper.options.clientScripts) !== "array") {
1874 casper.log("The clientScripts option must be an array", "error");
1875 } else {
1876 for (var i = 0; i < casper.options.clientScripts.length; i++) {
1877 var script = casper.options.clientScripts[i];
1878 if (casper.page.injectJs(script)) {
1879 casper.log('Automatically injected ' + script + ' client side', "debug");
1880 } else {
1881 casper.log('Failed injecting ' + script + ' client side', "warning");
1882 }
1883 }
1884 }
1885 }
1886 // Client-side utils injection
1887 var injected = page.evaluate(replaceFunctionPlaceholders(function() {
1888 eval("var ClientUtils = " + decodeURIComponent("%utils%"));
1889 __utils__ = new ClientUtils();
1890 return __utils__ instanceof ClientUtils;
1891 }, {
1892 utils: encodeURIComponent(phantom.Casper.ClientUtils.toString())
1893 }));
1894 if (!injected) {
1895 casper.log("Failed to inject Casper client-side utilities!", "warning");
1896 } else {
1897 casper.log("Successfully injected Casper client-side utilities", "debug");
1898 }
1899 // history
1900 casper.history.push(casper.getCurrentUrl());
1901 casper.loadInProgress = false;
1902 };
1903 page.onResourceReceived = function(resource) {
1904 if (isType(casper.options.onResourceReceived, "function")) {
1905 casper.options.onResourceReceived.call(casper, casper, resource);
1906 }
1907 if (resource.url === casper.requestUrl && resource.stage === "start") {
1908 casper.currentHTTPStatus = resource.status;
1909 if (isType(casper.options.httpStatusHandlers, "object") && resource.status in casper.options.httpStatusHandlers) {
1910 casper.options.httpStatusHandlers[resource.status](casper, resource);
1911 }
1912 casper.currentUrl = resource.url;
1913 }
1914 };
1915 page.onResourceRequested = function(request) {
1916 if (isType(casper.options.onResourceRequested, "function")) {
1917 casper.options.onResourceRequested.call(casper, casper, request);
1918 }
1919 };
1920 return page;
1921 }
1922
1923 /**
1924 * Shorthands for checking if a value is of the given type. Can check for
1925 * arrays.
1926 *
1927 * @param mixed what The value to check
1928 * @param String typeName The type name ("string", "number", "function", etc.)
1929 * @return Boolean
1930 */
1931 function isType(what, typeName) {
1932 return betterTypeOf(what) === typeName;
1933 }
1934
1935 /**
1936 * Checks if the provided var is a WebPage instance
1937 *
1938 * @param mixed what
1939 * @return Boolean
1940 */
1941 function isWebPage(what) {
1942 if (!what || !isType(what, "object")) {
1943 return false;
1944 }
1945 if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
1946 return what instanceof WebPage;
1947 } else {
1948 return what.toString().indexOf('WebPage(') === 0;
1949 }
1950 }
1951
1952 /**
1953 * Object recursive merging utility.
1954 *
1955 * @param Object obj1 the destination object
1956 * @param Object obj2 the source object
1957 * @return Object
1958 */
1959 function mergeObjects(obj1, obj2) {
1960 for (var p in obj2) {
1961 try {
1962 if (obj2[p].constructor == Object) {
1963 obj1[p] = mergeObjects(obj1[p], obj2[p]);
1964 } else {
1965 obj1[p] = obj2[p];
1966 }
1967 } catch(e) {
1968 obj1[p] = obj2[p];
1969 }
1970 }
1971 return obj1;
1972 }
1973
1974 /**
1975 * Replaces a function string contents with placeholders provided by an
1976 * Object.
1977 *
1978 * @param Function fn The function
1979 * @param Object replacements Object containing placeholder replacements
1980 * @return String A function string representation
1981 */
1982 function replaceFunctionPlaceholders(fn, replacements) {
1983 if (replacements && isType(replacements, "object")) {
1984 fn = fn.toString();
1985 for (var placeholder in replacements) {
1986 var match = '%' + placeholder + '%';
1987 do {
1988 fn = fn.replace(match, replacements[placeholder]);
1989 } while(fn.indexOf(match) !== -1);
1990 }
1991 }
1992 return fn;
1993 }
1994 })(phantom);
......
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom) {
29 /**
30 * Main Casper object.
31 *
32 * @param Object options Casper options
33 * @return Casper
34 */
35 phantom.Casper = function(options) {
36 var DEFAULT_DIE_MESSAGE = "Suite explicitely interrupted without any message given.";
37 var DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 Safari/535.1";
38 // init & checks
39 if (!(this instanceof arguments.callee)) {
40 return new Casper(options);
41 }
42 // default options
43 this.defaults = {
44 clientScripts: [],
45 faultTolerant: true,
46 logLevel: "error",
47 httpStatusHandlers: {},
48 onAlert: null,
49 onDie: null,
50 onError: null,
51 onLoadError: null,
52 onPageInitialized: null,
53 onResourceReceived: null,
54 onResourceRequested: null,
55 onStepComplete: null,
56 onStepTimeout: null,
57 onTimeout: null,
58 page: null,
59 pageSettings: { userAgent: DEFAULT_USER_AGENT },
60 stepTimeout: null,
61 timeout: null,
62 verbose: false
63 };
64 // privates
65 // local properties
66 this.checker = null;
67 this.colorizer = new phantom.Casper.Colorizer();
68 this.currentUrl = 'about:blank';
69 this.currentHTTPStatus = 200;
70 this.defaultWaitTimeout = 5000;
71 this.delayedExecution = false;
72 this.history = [];
73 this.loadInProgress = false;
74 this.logFormats = {};
75 this.logLevels = ["debug", "info", "warning", "error"];
76 this.logStyles = {
77 debug: 'INFO',
78 info: 'PARAMETER',
79 warning: 'COMMENT',
80 error: 'ERROR'
81 };
82 this.options = mergeObjects(this.defaults, options);
83 this.page = null;
84 this.requestUrl = 'about:blank';
85 this.result = {
86 log: [],
87 status: "success",
88 time: 0
89 };
90 this.started = false;
91 this.step = 0;
92 this.steps = [];
93 this.test = new phantom.Casper.Tester(this);
94 };
95
96 /**
97 * Casper prototype
98 */
99 phantom.Casper.prototype = {
100 /**
101 * Go a step back in browser's history
102 *
103 * @return Casper
104 */
105 back: function() {
106 return this.then(function(self) {
107 self.evaluate(function() {
108 history.back();
109 });
110 });
111 },
112
113 /**
114 * Encodes a resource using the base64 algorithm synchroneously using
115 * client-side XMLHttpRequest.
116 *
117 * NOTE: we cannot use window.btoa() for some strange reasons here.
118 *
119 * @param String url The url to download
120 * @return string Base64 encoded result
121 */
122 base64encode: function(url) {
123 return this.evaluate(function(url) {
124 return __utils__.getBase64(url);
125 }, { url: url });
126 },
127
128 /**
129 * Proxy method for WebPage#render. Adds a clipRect parameter for
130 * automatically set page clipRect setting values and sets it back once
131 * done. If the cliprect parameter is omitted, the full page viewport
132 * area will be rendered.
133 *
134 * @param String targetFile A target filename
135 * @param mixed clipRect An optional clipRect object (optional)
136 * @return Casper
137 */
138 capture: function(targetFile, clipRect) {
139 var previousClipRect;
140 if (clipRect) {
141 if (!isType(clipRect, "object")) {
142 throw new Error("clipRect must be an Object instance.");
143 }
144 previousClipRect = this.page.clipRect;
145 this.page.clipRect = clipRect;
146 this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug");
147 } else {
148 this.log('Capturing page to ' + targetFile, "debug");
149 }
150 try {
151 this.page.render(targetFile);
152 } catch (e) {
153 this.log('Failed to capture screenshot as ' + targetFile + ': ' + e, "error");
154 }
155 if (previousClipRect) {
156 this.page.clipRect = previousClipRect;
157 }
158 return this;
159 },
160
161 /**
162 * Captures the page area containing the provided selector.
163 *
164 * @param String targetFile Target destination file path.
165 * @param String selector CSS3 selector
166 * @return Casper
167 */
168 captureSelector: function(targetFile, selector) {
169 return this.capture(targetFile, this.evaluate(function(selector) {
170 try {
171 var clipRect = document.querySelector(selector).getBoundingClientRect();
172 return {
173 top: clipRect.top,
174 left: clipRect.left,
175 width: clipRect.width,
176 height: clipRect.height
177 };
178 } catch (e) {
179 __utils__.log("Unable to fetch bounds for element " + selector, "warning");
180 }
181 }, { selector: selector }));
182 },
183
184 /**
185 * Checks for any further navigation step to process.
186 *
187 * @param Casper self A self reference
188 * @param function onComplete An options callback to apply on completion
189 */
190 checkStep: function(self, onComplete) {
191 var step = self.steps[self.step];
192 if (!self.loadInProgress && isType(step, "function")) {
193 self.runStep(step);
194 }
195 if (!isType(step, "function") && !self.delayedExecution) {
196 self.result.time = new Date().getTime() - self.startTime;
197 self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info");
198 clearInterval(self.checker);
199 if (isType(onComplete, "function")) {
200 try {
201 onComplete.call(self, self);
202 } catch (err) {
203 self.log("could not complete final step: " + err, "error");
204 }
205 } else {
206 // default behavior is to exit phantom
207 self.exit();
208 }
209 }
210 },
211
212 /**
213 * Emulates a click on the element from the provided selector, if
214 * possible. In case of success, `true` is returned.
215 *
216 * @param String selector A DOM CSS3 compatible selector
217 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
218 * @return Boolean
219 */
220 click: function(selector, fallbackToHref) {
221 fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
222 this.log("click on selector: " + selector, "debug");
223 return this.evaluate(function(selector, fallbackToHref) {
224 return __utils__.click(selector, fallbackToHref);
225 }, {
226 selector: selector,
227 fallbackToHref: fallbackToHref
228 });
229 },
230
231 /**
232 * Creates a step definition.
233 *
234 * @param Function fn The step function to call
235 * @param Object options Step options
236 * @return Function The final step function
237 */
238 createStep: function(fn, options) {
239 if (!isType(fn, "function")) {
240 throw "createStep(): a step definition must be a function";
241 }
242 fn.options = isType(options, "object") ? options : {};
243 return fn;
244 },
245
246 /**
247 * Logs the HTML code of the current page.
248 *
249 * @return Casper
250 */
251 debugHTML: function() {
252 this.echo(this.evaluate(function() {
253 return document.body.innerHTML;
254 }));
255 return this;
256 },
257
258 /**
259 * Logs the textual contents of the current page.
260 *
261 * @return Casper
262 */
263 debugPage: function() {
264 this.echo(this.evaluate(function() {
265 return document.body.innerText;
266 }));
267 return this;
268 },
269
270 /**
271 * Exit phantom on failure, with a logged error message.
272 *
273 * @param String message An optional error message
274 * @param Number status An optional exit status code (must be > 0)
275 * @return Casper
276 */
277 die: function(message, status) {
278 this.result.status = 'error';
279 this.result.time = new Date().getTime() - this.startTime;
280 message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
281 this.log(message, "error");
282 if (isType(this.options.onDie, "function")) {
283 this.options.onDie.call(this, this, message, status);
284 }
285 return this.exit(Number(status) > 0 ? Number(status) : 1);
286 },
287
288 /**
289 * Iterates over the values of a provided array and execute a callback
290 * for each item.
291 *
292 * @param Array array
293 * @param Function fn Callback: function(self, item, index)
294 * @return Casper
295 */
296 each: function(array, fn) {
297 if (!isType(array, "array")) {
298 self.log("each() only works with arrays", "error");
299 return this;
300 }
301 (function(self) {
302 array.forEach(function(item, i) {
303 fn(self, item, i);
304 });
305 })(this);
306 return this;
307 },
308
309 /**
310 * Prints something to stdout.
311 *
312 * @param String text A string to echo to stdout
313 * @return Casper
314 */
315 echo: function(text, style) {
316 console.log(style ? this.colorizer.colorize(text, style) : text);
317 return this;
318 },
319
320 /**
321 * Evaluates an expression in the page context, a bit like what
322 * WebPage#evaluate does, but the passed function can also accept
323 * parameters if a context Object is also passed:
324 *
325 * casper.evaluate(function(username, password) {
326 * document.querySelector('#username').value = username;
327 * document.querySelector('#password').value = password;
328 * document.querySelector('#submit').click();
329 * }, {
330 * username: 'Bazoonga',
331 * password: 'baz00nga'
332 * })
333 *
334 * FIXME: waiting for a patch of PhantomJS to allow direct passing of
335 * arguments to the function.
336 * TODO: don't forget to keep this backward compatible.
337 *
338 * @param Function fn The function to be evaluated within current page DOM
339 * @param Object context Object containing the parameters to inject into the function
340 * @return mixed
341 * @see WebPage#evaluate
342 */
343 evaluate: function(fn, context) {
344 context = isType(context, "object") ? context : {};
345 var newFn = new phantom.Casper.FunctionArgsInjector(fn).process(context);
346 return this.page.evaluate(newFn);
347 },
348
349 /**
350 * Evaluates an expression within the current page DOM and die() if it
351 * returns false.
352 *
353 * @param function fn The expression to evaluate
354 * @param String message The error message to log
355 * @return Casper
356 */
357 evaluateOrDie: function(fn, message) {
358 if (!this.evaluate(fn)) {
359 return this.die(message);
360 }
361 return this;
362 },
363
364 /**
365 * Checks if an element matching the provided CSS3 selector exists in
366 * current page DOM.
367 *
368 * @param String selector A CSS3 selector
369 * @return Boolean
370 */
371 exists: function(selector) {
372 return this.evaluate(function(selector) {
373 return __utils__.exists(selector);
374 }, { selector: selector });
375 },
376
377 /**
378 * Checks if an element matching the provided CSS3 selector is visible
379 * current page DOM by checking that offsetWidth and offsetHeight are
380 * both non-zero.
381 *
382 * @param String selector A CSS3 selector
383 * @return Boolean
384 */
385 visible: function(selector) {
386 return this.evaluate(function(selector) {
387 return __utils__.visible(selector);
388 }, { selector: selector });
389 },
390
391 /**
392 * Exits phantom.
393 *
394 * @param Number status Status
395 * @return Casper
396 */
397 exit: function(status) {
398 phantom.exit(status);
399 return this;
400 },
401
402 /**
403 * Fetches innerText within the element(s) matching a given CSS3
404 * selector.
405 *
406 * @param String selector A CSS3 selector
407 * @return String
408 */
409 fetchText: function(selector) {
410 return this.evaluate(function(selector) {
411 return __utils__.fetchText(selector);
412 }, { selector: selector });
413 },
414
415 /**
416 * Fills a form with provided field values.
417 *
418 * @param String selector A CSS3 selector to the target form to fill
419 * @param Object vals Field values
420 * @param Boolean submit Submit the form?
421 */
422 fill: function(selector, vals, submit) {
423 submit = submit === true ? submit : false;
424 if (!isType(selector, "string") || !selector.length) {
425 throw "form selector must be a non-empty string";
426 }
427 if (!isType(vals, "object")) {
428 throw "form values must be provided as an object";
429 }
430 var fillResults = this.evaluate(function(selector, values) {
431 return __utils__.fill(selector, values);
432 }, {
433 selector: selector,
434 values: vals
435 });
436 if (!fillResults) {
437 throw "unable to fill form";
438 } else if (fillResults.errors.length > 0) {
439 (function(self){
440 fillResults.errors.forEach(function(error) {
441 self.log("form error: " + error, "error");
442 });
443 })(this);
444 if (submit) {
445 this.log("errors encountered while filling form; submission aborted", "warning");
446 submit = false;
447 }
448 }
449 // File uploads
450 if (fillResults.files && fillResults.files.length > 0) {
451 (function(self) {
452 fillResults.files.forEach(function(file) {
453 var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
454 self.page.uploadFile(fileFieldSelector, file.path);
455 });
456 })(this);
457 }
458 // Form submission?
459 if (submit) {
460 this.evaluate(function(selector) {
461 var form = document.querySelector(selector);
462 var method = form.getAttribute('method').toUpperCase() || "GET";
463 var action = form.getAttribute('action') || "unknown";
464 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
465 form.submit();
466 }, { selector: selector });
467 }
468 },
469
470 /**
471 * Go a step forward in browser's history
472 *
473 * @return Casper
474 */
475 forward: function(then) {
476 return this.then(function(self) {
477 self.evaluate(function() {
478 history.forward();
479 });
480 });
481 },
482
483 /**
484 * Retrieves current document url.
485 *
486 * @return String
487 */
488 getCurrentUrl: function() {
489 return decodeURIComponent(this.evaluate(function() {
490 return document.location.href;
491 }));
492 },
493
494 /**
495 * Retrieves global variable.
496 *
497 * @param String name The name of the global variable to retrieve
498 * @return mixed
499 */
500 getGlobal: function(name) {
501 var result = this.evaluate(function(name) {
502 var result = {};
503 try {
504 result.value = JSON.stringify(window[name]);
505 } catch (e) {
506 result.error = 'Unable to JSON encode window.' + name + ': ' + e;
507 }
508 return result;
509 }, {'name': name});
510 if (result.error) {
511 throw result.error;
512 } else {
513 return JSON.parse(result.value);
514 }
515 },
516
517 /**
518 * Retrieves current page title, if any.
519 *
520 * @return String
521 */
522 getTitle: function() {
523 return this.evaluate(function() {
524 return document.title;
525 });
526 },
527
528 /**
529 * Logs a message.
530 *
531 * @param String message The message to log
532 * @param String level The log message level (from Casper.logLevels property)
533 * @param String space Space from where the logged event occured (default: "phantom")
534 * @return Casper
535 */
536 log: function(message, level, space) {
537 level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
538 space = space ? space : "phantom";
539 if (level === "error" && isType(this.options.onError, "function")) {
540 this.options.onError.call(this, this, message, space);
541 }
542 if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
543 return this; // skip logging
544 }
545 var entry = {
546 level: level,
547 space: space,
548 message: message,
549 date: new Date().toString()
550 };
551 if (level in this.logFormats && isType(this.logFormats[level], "function")) {
552 message = this.logFormats[level](message, level, space);
553 } else {
554 var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]);
555 message = levelStr + ' [' + space + '] ' + message;
556 }
557 if (this.options.verbose) {
558 this.echo(message); // direct output
559 }
560 this.result.log.push(entry);
561 return this;
562 },
563
564 /**
565 * Opens a page. Takes only one argument, the url to open (using the
566 * callback argument would defeat the whole purpose of Casper
567 * actually).
568 *
569 * @param String location The url to open
570 * @return Casper
571 */
572 open: function(location, options) {
573 options = isType(options, "object") ? options : {};
574 this.requestUrl = location;
575 this.page.open(location);
576 return this;
577 },
578
579 /**
580 * Repeats a step a given number of times.
581 *
582 * @param Number times Number of times to repeat step
583 * @aram function then The step closure
584 * @return Casper
585 * @see Casper#then
586 */
587 repeat: function(times, then) {
588 for (var i = 0; i < times; i++) {
589 this.then(then);
590 }
591 return this;
592 },
593
594 /**
595 * Runs the whole suite of steps.
596 *
597 * @param function onComplete an optional callback
598 * @param Number time an optional amount of milliseconds for interval checking
599 * @return Casper
600 */
601 run: function(onComplete, time) {
602 if (!this.steps || this.steps.length < 1) {
603 this.log("No steps defined, aborting", "error");
604 return this;
605 }
606 this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info");
607 this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete);
608 return this;
609 },
610
611 /**
612 * Runs a step.
613 *
614 * @param Function step
615 */
616 runStep: function(step) {
617 var skipLog = isType(step.options, "object") && step.options.skipLog === true;
618 var stepInfo = "Step " + (this.step + 1) + "/" + this.steps.length;
619 var stepResult;
620 if (!skipLog) {
621 this.log(stepInfo + ' ' + this.getCurrentUrl() + ' (HTTP ' + this.currentHTTPStatus + ')', "info");
622 }
623 if (isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) {
624 var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) {
625 if (new Date().getTime() - start > self.options.stepTimeout) {
626 if (self.step == stepNum + 1) {
627 if (isType(self.options.onStepTimeout, "function")) {
628 self.options.onStepTimeout.call(self, self);
629 } else {
630 self.die("Maximum step execution timeout exceeded for step " + stepNum, "error");
631 }
632 }
633 clearInterval(stepTimeoutCheckInterval);
634 }
635 }, this.options.stepTimeout, this, new Date().getTime(), this.step);
636 }
637 try {
638 stepResult = step.call(this, this);
639 } catch (e) {
640 if (this.options.faultTolerant) {
641 this.log("Step error: " + e, "error");
642 } else {
643 throw e;
644 }
645 }
646 if (isType(this.options.onStepComplete, "function")) {
647 this.options.onStepComplete.call(this, this, stepResult);
648 }
649 if (!skipLog) {
650 this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info");
651 }
652 this.step++;
653 },
654
655 /**
656 * Configures and starts Casper.
657 *
658 * @param String location An optional location to open on start
659 * @param function then Next step function to execute on page loaded (optional)
660 * @return Casper
661 */
662 start: function(location, then) {
663 if (this.started) {
664 this.log("start failed: Casper has already started!", "error");
665 }
666 this.log('Starting...', "info");
667 this.startTime = new Date().getTime();
668 this.steps = [];
669 this.step = 0;
670 // Option checks
671 if (this.logLevels.indexOf(this.options.logLevel) < 0) {
672 this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning");
673 this.options.logLevel = "warning";
674 }
675 // WebPage
676 if (!isWebPage(this.page)) {
677 if (isWebPage(this.options.page)) {
678 this.page = this.options.page;
679 } else {
680 this.page = createPage(this);
681 }
682 }
683 this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings);
684 if (isType(this.options.clipRect, "object")) {
685 this.page.clipRect = this.options.clipRect;
686 }
687 if (isType(this.options.viewportSize, "object")) {
688 this.page.viewportSize = this.options.viewportSize;
689 }
690 this.started = true;
691 if (isType(this.options.timeout, "number") && this.options.timeout > 0) {
692 this.log("Execution timeout set to " + this.options.timeout + 'ms', "info");
693 setTimeout(function(self) {
694 if (isType(self.options.onTimeout, "function")) {
695 self.options.onTimeout.call(self, self);
696 } else {
697 self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting.");
698 }
699 }, this.options.timeout, this);
700 }
701 if (isType(this.options.onPageInitialized, "function")) {
702 this.log("Post-configuring WebPage instance", "debug");
703 this.options.onPageInitialized.call(this, this.page);
704 }
705 if (isType(location, "string") && location.length > 0) {
706 return this.thenOpen(location, isType(then, "function") ? then : this.createStep(function(self) {
707 self.log("start page is loaded", "debug");
708 }));
709 }
710 return this;
711 },
712
713 /**
714 * Schedules the next step in the navigation process.
715 *
716 * @param function step A function to be called as a step
717 * @return Casper
718 */
719 then: function(step) {
720 if (!this.started) {
721 throw "Casper not started; please use Casper#start";
722 }
723 if (!isType(step, "function")) {
724 throw "You can only define a step as a function";
725 }
726 this.steps.push(step);
727 return this;
728 },
729
730 /**
731 * Adds a new navigation step for clicking on a provided link selector
732 * and execute an optional next step.
733 *
734 * @param String selector A DOM CSS3 compatible selector
735 * @param Function then Next step function to execute on page loaded (optional)
736 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
737 * @return Casper
738 * @see Casper#click
739 * @see Casper#then
740 */
741 thenClick: function(selector, then, fallbackToHref) {
742 this.then(function(self) {
743 self.click(selector, fallbackToHref);
744 });
745 return isType(then, "function") ? this.then(then) : this;
746 },
747
748 /**
749 * Adds a new navigation step to perform code evaluation within the
750 * current retrieved page DOM.
751 *
752 * @param function fn The function to be evaluated within current page DOM
753 * @param object context Optional function parameters context
754 * @return Casper
755 * @see Casper#evaluate
756 */
757 thenEvaluate: function(fn, context) {
758 return this.then(function(self) {
759 self.evaluate(fn, context);
760 });
761 },
762
763 /**
764 * Adds a new navigation step for opening the provided location.
765 *
766 * @param String location The URL to load
767 * @param function then Next step function to execute on page loaded (optional)
768 * @return Casper
769 * @see Casper#open
770 */
771 thenOpen: function(location, then) {
772 this.then(this.createStep(function(self) {
773 self.open(location);
774 }, {
775 skipLog: true
776 }));
777 return isType(then, "function") ? this.then(then) : this;
778 },
779
780 /**
781 * Adds a new navigation step for opening and evaluate an expression
782 * against the DOM retrieved from the provided location.
783 *
784 * @param String location The url to open
785 * @param function fn The function to be evaluated within current page DOM
786 * @param object context Optional function parameters context
787 * @return Casper
788 * @see Casper#evaluate
789 * @see Casper#open
790 */
791 thenOpenAndEvaluate: function(location, fn, context) {
792 return this.thenOpen(location).thenEvaluate(fn, context);
793 },
794
795 /**
796 * Changes the current viewport size.
797 *
798 * @param Number width The viewport width, in pixels
799 * @param Number height The viewport height, in pixels
800 * @return Casper
801 */
802 viewport: function(width, height) {
803 if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) {
804 throw new Error("Invalid viewport width/height set: " + width + 'x' + height);
805 }
806 this.page.viewportSize = {
807 width: width,
808 height: height
809 };
810 return this;
811 },
812
813 /**
814 * Adds a new step that will wait for a given amount of time (expressed
815 * in milliseconds) before processing an optional next one.
816 *
817 * @param Number timeout The max amount of time to wait, in milliseconds
818 * @param Function then Next step to process (optional)
819 * @return Casper
820 */
821 wait: function(timeout, then) {
822 timeout = Number(timeout, 10);
823 if (!isType(timeout, "number") || timeout < 1) {
824 this.die("wait() only accepts a positive integer > 0 as a timeout value");
825 }
826 if (then && !isType(then, "function")) {
827 this.die("wait() a step definition must be a function");
828 }
829 return this.then(function(self) {
830 self.delayedExecution = true;
831 var start = new Date().getTime();
832 var interval = setInterval(function(self, then) {
833 if (new Date().getTime() - start > timeout) {
834 self.delayedExecution = false;
835 self.log("wait() finished wating for " + timeout + "ms.", "info");
836 if (then) {
837 self.then(then);
838 }
839 clearInterval(interval);
840 }
841 }, 100, self, then);
842 });
843 },
844
845 /**
846 * Waits until a function returns true to process a next step.
847 *
848 * @param Function testFx A function to be evaluated for returning condition satisfecit
849 * @param Function then The next step to perform (optional)
850 * @param Function onTimeout A callback function to call on timeout (optional)
851 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
852 * @return Casper
853 */
854 waitFor: function(testFx, then, onTimeout, timeout) {
855 timeout = timeout ? timeout : this.defaultWaitTimeout;
856 if (!isType(testFx, "function")) {
857 this.die("waitFor() needs a test function");
858 }
859 if (then && !isType(then, "function")) {
860 this.die("waitFor() next step definition must be a function");
861 }
862 this.delayedExecution = true;
863 var start = new Date().getTime();
864 var condition = false;
865 var interval = setInterval(function(self, testFx, onTimeout) {
866 if ((new Date().getTime() - start < timeout) && !condition) {
867 condition = testFx(self);
868 } else {
869 self.delayedExecution = false;
870 if (!condition) {
871 self.log("Casper.waitFor() timeout", "warning");
872 if (isType(onTimeout, "function")) {
873 onTimeout.call(self, self);
874 } else {
875 self.die("Expired timeout, exiting.", "error");
876 }
877 clearInterval(interval);
878 } else {
879 self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info");
880 if (then) {
881 self.then(then);
882 }
883 clearInterval(interval);
884 }
885 }
886 }, 100, this, testFx, onTimeout);
887 return this;
888 },
889
890 /**
891 * Waits until an element matching the provided CSS3 selector exists in
892 * remote DOM to process a next step.
893 *
894 * @param String selector A CSS3 selector
895 * @param Function then The next step to perform (optional)
896 * @param Function onTimeout A callback function to call on timeout (optional)
897 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
898 * @return Casper
899 */
900 waitForSelector: function(selector, then, onTimeout, timeout) {
901 timeout = timeout ? timeout : this.defaultWaitTimeout;
902 return this.waitFor(function(self) {
903 return self.exists(selector);
904 }, then, onTimeout, timeout);
905 },
906
907 /**
908 * Waits until an element matching the provided CSS3 selector does not
909 * exist in the remote DOM to process a next step.
910 *
911 * @param String selector A CSS3 selector
912 * @param Function then The next step to perform (optional)
913 * @param Function onTimeout A callback function to call on timeout (optional)
914 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
915 * @return Casper
916 */
917 waitWhileSelector: function(selector, then, onTimeout, timeout) {
918 timeout = timeout ? timeout : this.defaultWaitTimeout;
919 return this.waitFor(function(self) {
920 return !self.exists(selector);
921 }, then, onTimeout, timeout);
922 },
923
924 /**
925 * Waits until an element matching the provided CSS3 selector is
926 * visible in the remote DOM to process a next step.
927 *
928 * @param String selector A CSS3 selector
929 * @param Function then The next step to perform (optional)
930 * @param Function onTimeout A callback function to call on timeout (optional)
931 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
932 * @return Casper
933 */
934 waitUntilVisible: function(selector, then, onTimeout, timeout) {
935 timeout = timeout ? timeout : this.defaultWaitTimeout;
936 return this.waitFor(function(self) {
937 return self.visible(selector);
938 }, then, onTimeout, timeout);
939 },
940
941 /**
942 * Waits until an element matching the provided CSS3 selector is no
943 * longer visible in remote DOM to process a next step.
944 *
945 * @param String selector A CSS3 selector
946 * @param Function then The next step to perform (optional)
947 * @param Function onTimeout A callback function to call on timeout (optional)
948 * @param Number timeout The max amount of time to wait, in milliseconds (optional)
949 * @return Casper
950 */
951 waitWhileVisible: function(selector, then, onTimeout, timeout) {
952 timeout = timeout ? timeout : this.defaultWaitTimeout;
953 return this.waitFor(function(self) {
954 return !self.visible(selector);
955 }, then, onTimeout, timeout);
956 }
957 };
958
959 /**
960 * Extends Casper's prototype with provided one.
961 *
962 * @param Object proto Prototype methods to add to Casper
963 */
964 phantom.Casper.extend = function(proto) {
965 if (!isType(proto, "object")) {
966 throw "extends() only accept objects as prototypes";
967 }
968 mergeObjects(phantom.Casper.prototype, proto);
969 };
970 })(phantom);
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom) {
29 /**
30 * Casper client-side helpers.
31 */
32 phantom.Casper.ClientUtils = function() {
33 /**
34 * Clicks on the DOM element behind the provided selector.
35 *
36 * @param String selector A CSS3 selector to the element to click
37 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
38 * @return Boolean
39 */
40 this.click = function(selector, fallbackToHref) {
41 fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref;
42 var elem = this.findOne(selector);
43 if (!elem) {
44 return false;
45 }
46 var evt = document.createEvent("MouseEvents");
47 evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem);
48 if (elem.dispatchEvent(evt)) {
49 return true;
50 }
51 if (fallbackToHref && elem.hasAttribute('href')) {
52 document.location = elem.getAttribute('href');
53 return true;
54 }
55 return false;
56 };
57
58 /**
59 * Base64 encodes a string, even binary ones. Succeeds where
60 * window.btoa() fails.
61 *
62 * @param String str
63 * @return string
64 */
65 this.encode = function(str) {
66 var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
67 var out = "", i = 0, len = str.length, c1, c2, c3;
68 while (i < len) {
69 c1 = str.charCodeAt(i++) & 0xff;
70 if (i == len) {
71 out += CHARS.charAt(c1 >> 2);
72 out += CHARS.charAt((c1 & 0x3) << 4);
73 out += "==";
74 break;
75 }
76 c2 = str.charCodeAt(i++);
77 if (i == len) {
78 out += CHARS.charAt(c1 >> 2);
79 out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
80 out += CHARS.charAt((c2 & 0xF) << 2);
81 out += "=";
82 break;
83 }
84 c3 = str.charCodeAt(i++);
85 out += CHARS.charAt(c1 >> 2);
86 out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
87 out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
88 out += CHARS.charAt(c3 & 0x3F);
89 }
90 return out;
91 };
92
93 /**
94 * Checks if a given DOM element exists in remote page.
95 *
96 * @param String selector CSS3 selector
97 * @return Boolean
98 */
99 this.exists = function(selector) {
100 try {
101 return document.querySelectorAll(selector).length > 0;
102 } catch (e) {
103 return false;
104 }
105 };
106
107 /**
108 * Checks if a given DOM element is visible in remote page.
109 *
110 * @param String selector CSS3 selector
111 * @return Boolean
112 */
113 this.visible = function(selector) {
114 try {
115 var el = document.querySelector(selector);
116 return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0;
117 } catch (e) {
118 return false;
119 }
120 };
121
122 /**
123 * Fetches innerText within the element(s) matching a given CSS3
124 * selector.
125 *
126 * @param String selector A CSS3 selector
127 * @return String
128 */
129 this.fetchText = function(selector) {
130 var text = '', elements = this.findAll(selector);
131 if (elements && elements.length) {
132 Array.prototype.forEach.call(elements, function(element) {
133 text += element.innerText;
134 });
135 }
136 return text;
137 };
138
139 /**
140 * Fills a form with provided field values, and optionnaly submits it.
141 *
142 * @param HTMLElement|String form A form element, or a CSS3 selector to a form element
143 * @param Object vals Field values
144 * @return Object An object containing setting result for each field, including file uploads
145 */
146 this.fill = function(form, vals) {
147 var out = {
148 errors: [],
149 fields: [],
150 files: []
151 };
152 if (!(form instanceof HTMLElement) || typeof form === "string") {
153 __utils__.log("attempting to fetch form element from selector: '" + form + "'", "info");
154 try {
155 form = document.querySelector(form);
156 } catch (e) {
157 if (e.name === "SYNTAX_ERR") {
158 out.errors.push("invalid form selector provided: '" + form + "'");
159 return out;
160 }
161 }
162 }
163 if (!form) {
164 out.errors.push("form not found");
165 return out;
166 }
167 for (var name in vals) {
168 if (!vals.hasOwnProperty(name)) {
169 continue;
170 }
171 var field = form.querySelectorAll('[name="' + name + '"]');
172 var value = vals[name];
173 if (!field) {
174 out.errors.push('no field named "' + name + '" in form');
175 continue;
176 }
177 try {
178 out.fields[name] = this.setField(field, value);
179 } catch (err) {
180 if (err.name === "FileUploadError") {
181 out.files.push({
182 name: name,
183 path: err.path
184 });
185 } else {
186 this.log(err, "error");
187 throw err;
188 }
189 }
190 }
191 return out;
192 };
193
194 /**
195 * Finds all DOM elements matching by the provided selector.
196 *
197 * @param String selector CSS3 selector
198 * @return NodeList|undefined
199 */
200 this.findAll = function(selector) {
201 try {
202 return document.querySelectorAll(selector);
203 } catch (e) {
204 this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
205 }
206 };
207
208 /**
209 * Finds a DOM element by the provided selector.
210 *
211 * @param String selector CSS3 selector
212 * @return HTMLElement|undefined
213 */
214 this.findOne = function(selector) {
215 try {
216 return document.querySelector(selector);
217 } catch (e) {
218 this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors");
219 }
220 };
221
222 /**
223 * Downloads a resource behind an url and returns its base64-encoded
224 * contents.
225 *
226 * @param String url The resource url
227 * @return String Base64 contents string
228 */
229 this.getBase64 = function(url) {
230 return this.encode(this.getBinary(url));
231 };
232
233 /**
234 * Retrieves string contents from a binary file behind an url. Silently
235 * fails but log errors.
236 *
237 * @param String url
238 * @return string
239 */
240 this.getBinary = function(url) {
241 try {
242 var xhr = new XMLHttpRequest();
243 xhr.open("GET", url, false);
244 xhr.overrideMimeType("text/plain; charset=x-user-defined");
245 xhr.send(null);
246 return xhr.responseText;
247 } catch (e) {
248 if (e.name === "NETWORK_ERR" && e.code === 101) {
249 this.log("unfortunately, casperjs cannot make cross domain ajax requests", "warning");
250 }
251 this.log("error while fetching " + url + ": " + e, "error");
252 return "";
253 }
254 };
255
256 /**
257 * Logs a message.
258 *
259 * @param String message
260 * @param String level
261 */
262 this.log = function(message, level) {
263 console.log("[casper:" + (level || "debug") + "] " + message);
264 };
265
266 /**
267 * Sets a field (or a set of fields) value. Fails silently, but log
268 * error messages.
269 *
270 * @param HTMLElement|NodeList field One or more element defining a field
271 * @param mixed value The field value to set
272 */
273 this.setField = function(field, value) {
274 var fields, out;
275 value = value || "";
276 if (field instanceof NodeList) {
277 fields = field;
278 field = fields[0];
279 }
280 if (!field instanceof HTMLElement) {
281 this.log("invalid field type; only HTMLElement and NodeList are supported", "error");
282 }
283 this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug");
284 try {
285 field.focus();
286 } catch (e) {
287 __utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
288 }
289 var nodeName = field.nodeName.toLowerCase();
290 switch (nodeName) {
291 case "input":
292 var type = field.getAttribute('type') || "text";
293 switch (type.toLowerCase()) {
294 case "color":
295 case "date":
296 case "datetime":
297 case "datetime-local":
298 case "email":
299 case "hidden":
300 case "month":
301 case "number":
302 case "password":
303 case "range":
304 case "search":
305 case "tel":
306 case "text":
307 case "time":
308 case "url":
309 case "week":
310 field.value = value;
311 break;
312 case "checkbox":
313 if (fields.length > 1) {
314 var values = value;
315 if (!Array.isArray(values)) {
316 values = [values];
317 }
318 Array.prototype.forEach.call(fields, function(f) {
319 f.checked = values.indexOf(f.value) !== -1 ? true : false;
320 });
321 } else {
322 field.checked = value ? true : false;
323 }
324 break;
325 case "file":
326 throw {
327 name: "FileUploadError",
328 message: "file field must be filled using page.uploadFile",
329 path: value
330 };
331 case "radio":
332 if (fields) {
333 Array.prototype.forEach.call(fields, function(e) {
334 e.checked = (e.value === value);
335 });
336 } else {
337 out = 'provided radio elements are empty';
338 }
339 break;
340 default:
341 out = "unsupported input field type: " + type;
342 break;
343 }
344 break;
345 case "select":
346 case "textarea":
347 field.value = value;
348 break;
349 default:
350 out = 'unsupported field type: ' + nodeName;
351 break;
352 }
353 try {
354 field.blur();
355 } catch (err) {
356 __utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
357 }
358 return out;
359 };
360 };
361 })(phantom);
...\ No newline at end of file ...\ No newline at end of file
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom){
29 /**
30 * This is a port of lime colorizer.
31 * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php)
32 *
33 * (c) Fabien Potencier, Symfony project, MIT license
34 */
35 phantom.Casper.Colorizer = function() {
36 var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 };
37 var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 };
38 var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 };
39 var styles = {
40 'ERROR': { bg: 'red', fg: 'white', bold: true },
41 'INFO': { fg: 'green', bold: true },
42 'TRACE': { fg: 'green', bold: true },
43 'PARAMETER': { fg: 'cyan' },
44 'COMMENT': { fg: 'yellow' },
45 'WARNING': { fg: 'red', bold: true },
46 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true },
47 'RED_BAR': { fg: 'white', bg: 'red', bold: true },
48 'INFO_BAR': { fg: 'cyan', bold: true }
49 };
50
51 /**
52 * Adds a style to provided text.
53 *
54 * @params String text
55 * @params String styleName
56 * @return String
57 */
58 this.colorize = function(text, styleName) {
59 if (styleName in styles) {
60 return this.format(text, styles[styleName]);
61 }
62 return text;
63 };
64
65 /**
66 * Formats a text using a style declaration object.
67 *
68 * @param String text
69 * @param Object style
70 * @return String
71 */
72 this.format = function(text, style) {
73 if (typeof style !== "object") {
74 return text;
75 }
76 var codes = [];
77 if (style.fg && foreground[style.fg]) {
78 codes.push(foreground[style.fg]);
79 }
80 if (style.bg && background[style.bg]) {
81 codes.push(background[style.bg]);
82 }
83 for (var option in options) {
84 if (style[option] === true) {
85 codes.push(options[option]);
86 }
87 }
88 return "\033[" + codes.join(';') + 'm' + text + "\033[0m";
89 };
90 };
91 })(phantom);
...\ No newline at end of file ...\ No newline at end of file
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom) {
29 /**
30 * Function argument injector.
31 *
32 */
33 phantom.Casper.FunctionArgsInjector = function(fn) {
34 if (!isType(fn, "function")) {
35 throw "FunctionArgsInjector() can only process functions";
36 }
37 this.fn = fn;
38
39 this.extract = function(fn) {
40 var match = /^function\s?(\w+)?\s?\((.*)\)\s?\{([\s\S]*)\}/i.exec(fn.toString().trim());
41 if (match && match.length > 1) {
42 var args = match[2].split(',').map(function(arg) {
43 return arg.replace(new RegExp(/\/\*+.*\*\//ig), "").trim();
44 }).filter(function(arg) {
45 return arg;
46 }) || [];
47 return {
48 name: match[1] ? match[1].trim() : null,
49 args: args,
50 body: match[3] ? match[3].trim() : ''
51 };
52 }
53 };
54
55 this.process = function(values) {
56 var fnObj = this.extract(this.fn);
57 if (!isType(fnObj, "object")) {
58 throw "Unable to process function " + this.fn.toString();
59 }
60 var inject = this.getArgsInjectionString(fnObj.args, values);
61 return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}';
62 };
63
64 this.getArgsInjectionString = function(args, values) {
65 values = typeof values === "object" ? values : {};
66 var jsonValues = escape(encodeURIComponent(JSON.stringify(values)));
67 var inject = [
68 'var __casper_params__ = JSON.parse(decodeURIComponent(unescape(\'' + jsonValues + '\')));'
69 ];
70 args.forEach(function(arg) {
71 if (arg in values) {
72 inject.push('var ' + arg + '=__casper_params__["' + arg + '"];');
73 }
74 });
75 return inject.join('\n') + '\n';
76 };
77 };
78 })(phantom);
...\ No newline at end of file ...\ No newline at end of file
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom) {
29 /**
30 * Casper tester: makes assertions, stores test results and display then.
31 *
32 */
33 phantom.Casper.Tester = function(casper, options) {
34 this.options = isType(options, "object") ? options : {};
35 if (!casper instanceof phantom.Casper) {
36 throw "phantom.Casper.Tester needs a phantom.Casper instance";
37 }
38
39 // locals
40 var exporter = new phantom.Casper.XUnitExporter();
41 var PASS = this.options.PASS || "PASS";
42 var FAIL = this.options.FAIL || "FAIL";
43
44 function compareArrays(a, b) {
45 if (a.length !== b.length) {
46 return false;
47 }
48 a.forEach(function(item, i) {
49 if (isType(item, "array") && !compareArrays(item, b[i])) {
50 return false;
51 }
52 if (item !== b[i]) {
53 return false;
54 }
55 });
56 return true;
57 }
58
59 // properties
60 this.testResults = {
61 passed: 0,
62 failed: 0
63 };
64
65 // methods
66 /**
67 * Asserts a condition resolves to true.
68 *
69 * @param Boolean condition
70 * @param String message Test description
71 */
72 this.assert = function(condition, message) {
73 var status = PASS;
74 if (condition === true) {
75 style = 'INFO';
76 this.testResults.passed++;
77 exporter.addSuccess("unknown", message);
78 } else {
79 status = FAIL;
80 style = 'RED_BAR';
81 this.testResults.failed++;
82 exporter.addFailure("unknown", message, 'test failed', "assert");
83 }
84 casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' '));
85 };
86
87 /**
88 * Asserts that two values are strictly equals.
89 *
90 * @param Mixed testValue The value to test
91 * @param Mixed expected The expected value
92 * @param String message Test description
93 */
94 this.assertEquals = function(testValue, expected, message) {
95 if (this.testEquals(testValue, expected)) {
96 casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message));
97 this.testResults.passed++;
98 exporter.addSuccess("unknown", message);
99 } else {
100 casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING'));
101 this.comment(' got: ' + testValue);
102 this.comment(' expected: ' + expected);
103 this.testResults.failed++;
104 exporter.addFailure("unknown", message, "test failed; expected: " + expected + "; got: " + testValue, "assertEquals");
105 }
106 };
107
108 /**
109 * Asserts that a code evaluation in remote DOM resolves to true.
110 *
111 * @param Function fn A function to be evaluated in remote DOM
112 * @param String message Test description
113 */
114 this.assertEval = function(fn, message) {
115 return this.assert(casper.evaluate(fn), message);
116 };
117
118 /**
119 * Asserts that the result of a code evaluation in remote DOM equals
120 * an expected value.
121 *
122 * @param Function fn The function to be evaluated in remote DOM
123 * @param Boolean expected The expected value
124 * @param String message Test description
125 */
126 this.assertEvalEquals = function(fn, expected, message) {
127 return this.assertEquals(casper.evaluate(fn), expected, message);
128 };
129
130 /**
131 * Asserts that an element matching the provided CSS3 selector exists in
132 * remote DOM.
133 *
134 * @param String selector CSS3 selectore
135 * @param String message Test description
136 */
137 this.assertExists = function(selector, message) {
138 return this.assert(casper.exists(selector), message);
139 };
140
141 /**
142 * Asserts that a provided string matches a provided RegExp pattern.
143 *
144 * @param String subject The string to test
145 * @param RegExp pattern A RegExp object instance
146 * @param String message Test description
147 */
148 this.assertMatch = function(subject, pattern, message) {
149 if (pattern.test(subject)) {
150 casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message));
151 this.testResults.passed++;
152 exporter.addSuccess("unknown", message);
153 } else {
154 casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING'));
155 this.comment(' subject: ' + subject);
156 this.comment(' pattern: ' + pattern.toString());
157 this.testResults.failed++;
158 exporter.addFailure("unknown", message, "test failed; subject: " + subject + "; pattern: " + pattern.toString(), "assertMatch");
159 }
160 };
161
162 /**
163 * Asserts a condition resolves to false.
164 *
165 * @param Boolean condition
166 * @param String message Test description
167 */
168 this.assertNot = function(condition, message) {
169 return this.assert(!condition, message);
170 };
171
172 /**
173 * Asserts that the provided function called with the given parameters
174 * will raise an exception.
175 *
176 * @param Function fn The function to test
177 * @param Array args The arguments to pass to the function
178 * @param String message Test description
179 */
180 this.assertRaises = function(fn, args, message) {
181 try {
182 fn.apply(null, args);
183 this.fail(message);
184 } catch (e) {
185 this.pass(message);
186 }
187 };
188
189 /**
190 * Asserts that at least an element matching the provided CSS3 selector
191 * exists in remote DOM.
192 *
193 * @param String selector A CSS3 selector string
194 * @param String message Test description
195 */
196 this.assertSelectorExists = function(selector, message) {
197 return this.assert(this.exists(selector), message);
198 };
199
200 /**
201 * Asserts that title of the remote page equals to the expected one.
202 *
203 * @param String expected The expected title string
204 * @param String message Test description
205 */
206 this.assertTitle = function(expected, message) {
207 return this.assertEquals(casper.getTitle(), expected, message);
208 };
209
210 /**
211 * Asserts that the provided input is of the given type.
212 *
213 * @param mixed input The value to test
214 * @param String type The javascript type name
215 * @param String message Test description
216 */
217 this.assertType = function(input, type, message) {
218 return this.assertEquals(betterTypeOf(input), type, message);
219 };
220
221 /**
222 * Asserts that a the current page url matches the provided RegExp
223 * pattern.
224 *
225 * @param RegExp pattern A RegExp object instance
226 * @param String message Test description
227 */
228 this.assertUrlMatch = function(pattern, message) {
229 return this.assertMatch(casper.getCurrentUrl(), pattern, message);
230 };
231
232 /**
233 * Render a colorized output. Basically a proxy method for
234 * Casper.Colorizer#colorize()
235 */
236 this.colorize = function(message, style) {
237 return casper.colorizer.colorize(message, style);
238 };
239
240 /**
241 * Writes a comment-style formatted message to stdout.
242 *
243 * @param String message
244 */
245 this.comment = function(message) {
246 casper.echo('# ' + message, 'COMMENT');
247 };
248
249 /**
250 * Tests equality between the two passed arguments.
251 *
252 * @param Mixed v1
253 * @param Mixed v2
254 * @param Boolean
255 */
256 this.testEquals = function(v1, v2) {
257 if (betterTypeOf(v1) !== betterTypeOf(v2)) {
258 return false;
259 }
260 if (isType(v1, "function")) {
261 return v1.toString() === v2.toString();
262 }
263 if (v1 instanceof Object && v2 instanceof Object) {
264 if (Object.keys(v1).length !== Object.keys(v2).length) {
265 return false;
266 }
267 for (var k in v1) {
268 if (!this.testEquals(v1[k], v2[k])) {
269 return false;
270 }
271 }
272 return true;
273 }
274 return v1 === v2;
275 };
276
277 /**
278 * Writes an error-style formatted message to stdout.
279 *
280 * @param String message
281 */
282 this.error = function(message) {
283 casper.echo(message, 'ERROR');
284 };
285
286 /**
287 * Adds a failed test entry to the stack.
288 *
289 * @param String message
290 */
291 this.fail = function(message) {
292 this.assert(false, message);
293 };
294
295 /**
296 * Formats a message to highlight some parts of it.
297 *
298 * @param String message
299 * @param String style
300 */
301 this.formatMessage = function(message, style) {
302 var parts = /([a-z0-9_\.]+\(\))(.*)/i.exec(message);
303 if (!parts) {
304 return message;
305 }
306 return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style);
307 };
308
309 /**
310 * Writes an info-style formatted message to stdout.
311 *
312 * @param String message
313 */
314 this.info = function(message) {
315 casper.echo(message, 'PARAMETER');
316 };
317
318 /**
319 * Adds a successful test entry to the stack.
320 *
321 * @param String message
322 */
323 this.pass = function(message) {
324 this.assert(true, message);
325 };
326
327 /**
328 * Render tests results, an optionnaly exit phantomjs.
329 *
330 * @param Boolean exit
331 */
332 this.renderResults = function(exit, status, save) {
333 save = isType(save, "string") ? save : this.options.save;
334 var total = this.testResults.passed + this.testResults.failed, statusText, style, result;
335 if (this.testResults.failed > 0) {
336 statusText = FAIL;
337 style = 'RED_BAR';
338 } else {
339 statusText = PASS;
340 style = 'GREEN_BAR';
341 }
342 result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.';
343 if (result.length < 80) {
344 result += new Array(80 - result.length + 1).join(' ');
345 }
346 casper.echo(this.colorize(result, style));
347 if (save && isType(require, "function")) {
348 try {
349 require('fs').write(save, exporter.getXML(), 'w');
350 casper.echo('result log stored in ' + save, 'INFO');
351 } catch (e) {
352 casper.echo('unable to write results to ' + save + '; ' + e, 'ERROR');
353 }
354 }
355 if (exit === true) {
356 casper.exit(status || 0);
357 }
358 };
359 };
360 })(phantom);
...\ No newline at end of file ...\ No newline at end of file
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28
29 /**
30 * Provides a better typeof operator equivalent, able to retrieve the array
31 * type.
32 *
33 * @param mixed input
34 * @return String
35 * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
36 */
37 function betterTypeOf(input) {
38 try {
39 return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase();
40 } catch (e) {
41 return typeof input;
42 }
43 }
44
45 /**
46 * Creates a new WebPage instance for Casper use.
47 *
48 * @param Casper casper A Casper instance
49 * @return WebPage
50 */
51 function createPage(casper) {
52 var page;
53 if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
54 page = new WebPage();
55 } else {
56 page = require('webpage').create();
57 }
58 page.onAlert = function(message) {
59 casper.log('[alert] ' + message, "info", "remote");
60 if (isType(casper.options.onAlert, "function")) {
61 casper.options.onAlert.call(casper, casper, message);
62 }
63 };
64 page.onConsoleMessage = function(msg) {
65 var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg);
66 if (test && test.length === 3) {
67 level = test[1];
68 msg = test[2];
69 }
70 casper.log(msg, level, "remote");
71 };
72 page.onLoadStarted = function() {
73 casper.loadInProgress = true;
74 };
75 page.onLoadFinished = function(status) {
76 if (status !== "success") {
77 var message = 'Loading resource failed with status=' + status;
78 if (casper.currentHTTPStatus) {
79 message += ' (HTTP ' + casper.currentHTTPStatus + ')';
80 }
81 message += ': ' + casper.requestUrl;
82 casper.log(message, "warning");
83 if (isType(casper.options.onLoadError, "function")) {
84 casper.options.onLoadError.call(casper, casper, casper.requestUrl, status);
85 }
86 }
87 if (casper.options.clientScripts) {
88 if (betterTypeOf(casper.options.clientScripts) !== "array") {
89 casper.log("The clientScripts option must be an array", "error");
90 } else {
91 for (var i = 0; i < casper.options.clientScripts.length; i++) {
92 var script = casper.options.clientScripts[i];
93 if (casper.page.injectJs(script)) {
94 casper.log('Automatically injected ' + script + ' client side', "debug");
95 } else {
96 casper.log('Failed injecting ' + script + ' client side', "warning");
97 }
98 }
99 }
100 }
101 // Client-side utils injection
102 var injected = page.evaluate(replaceFunctionPlaceholders(function() {
103 eval("var ClientUtils = " + decodeURIComponent("%utils%"));
104 __utils__ = new ClientUtils();
105 return __utils__ instanceof ClientUtils;
106 }, {
107 utils: encodeURIComponent(phantom.Casper.ClientUtils.toString())
108 }));
109 if (!injected) {
110 casper.log("Failed to inject Casper client-side utilities!", "warning");
111 } else {
112 casper.log("Successfully injected Casper client-side utilities", "debug");
113 }
114 // history
115 casper.history.push(casper.getCurrentUrl());
116 casper.loadInProgress = false;
117 };
118 page.onResourceReceived = function(resource) {
119 if (isType(casper.options.onResourceReceived, "function")) {
120 casper.options.onResourceReceived.call(casper, casper, resource);
121 }
122 if (resource.url === casper.requestUrl && resource.stage === "start") {
123 casper.currentHTTPStatus = resource.status;
124 if (isType(casper.options.httpStatusHandlers, "object") && resource.status in casper.options.httpStatusHandlers) {
125 casper.options.httpStatusHandlers[resource.status](casper, resource);
126 }
127 casper.currentUrl = resource.url;
128 }
129 };
130 page.onResourceRequested = function(request) {
131 if (isType(casper.options.onResourceRequested, "function")) {
132 casper.options.onResourceRequested.call(casper, casper, request);
133 }
134 };
135 return page;
136 }
137
138 /**
139 * Shorthands for checking if a value is of the given type. Can check for
140 * arrays.
141 *
142 * @param mixed what The value to check
143 * @param String typeName The type name ("string", "number", "function", etc.)
144 * @return Boolean
145 */
146 function isType(what, typeName) {
147 return betterTypeOf(what) === typeName;
148 }
149
150 /**
151 * Checks if the provided var is a WebPage instance
152 *
153 * @param mixed what
154 * @return Boolean
155 */
156 function isWebPage(what) {
157 if (!what || !isType(what, "object")) {
158 return false;
159 }
160 if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) {
161 return what instanceof WebPage;
162 } else {
163 return what.toString().indexOf('WebPage(') === 0;
164 }
165 }
166
167 /**
168 * Object recursive merging utility.
169 *
170 * @param Object obj1 the destination object
171 * @param Object obj2 the source object
172 * @return Object
173 */
174 function mergeObjects(obj1, obj2) {
175 for (var p in obj2) {
176 try {
177 if (obj2[p].constructor == Object) {
178 obj1[p] = mergeObjects(obj1[p], obj2[p]);
179 } else {
180 obj1[p] = obj2[p];
181 }
182 } catch(e) {
183 obj1[p] = obj2[p];
184 }
185 }
186 return obj1;
187 }
188
189 /**
190 * Replaces a function string contents with placeholders provided by an
191 * Object.
192 *
193 * @param Function fn The function
194 * @param Object replacements Object containing placeholder replacements
195 * @return String A function string representation
196 */
197 function replaceFunctionPlaceholders(fn, replacements) {
198 if (replacements && isType(replacements, "object")) {
199 fn = fn.toString();
200 for (var placeholder in replacements) {
201 var match = '%' + placeholder + '%';
202 do {
203 fn = fn.replace(match, replacements[placeholder]);
204 } while(fn.indexOf(match) !== -1);
205 }
206 }
207 return fn;
208 }
1 /*!
2 * Casper is a navigation utility for PhantomJS.
3 *
4 * Documentation: http://n1k0.github.com/casperjs/
5 * Repository: http://github.com/n1k0/casperjs
6 *
7 * Copyright (c) 2011 Nicolas Perriault
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a
10 * copy of this software and associated documentation files (the "Software"),
11 * to deal in the Software without restriction, including without limitation
12 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 * and/or sell copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included
17 * in all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 * DEALINGS IN THE SOFTWARE.
26 *
27 */
28 (function(phantom) {
29 /**
30 * JUnit XML (xUnit) exporter for test results.
31 *
32 */
33 phantom.Casper.XUnitExporter = function() {
34 var node = function(name, attributes) {
35 var node = document.createElement(name);
36 for (var attrName in attributes) {
37 var value = attributes[attrName];
38 if (attributes.hasOwnProperty(attrName) && isType(attrName, "string")) {
39 node.setAttribute(attrName, value);
40 }
41 }
42 return node;
43 };
44
45 var xml = node('testsuite');
46 xml.toString = function() {
47 return this.outerHTML; // ouch
48 };
49
50 /**
51 * Adds a successful test result
52 *
53 * @param String classname
54 * @param String name
55 */
56 this.addSuccess = function(classname, name) {
57 xml.appendChild(node('testcase', {
58 classname: classname,
59 name: name
60 }));
61 };
62
63 /**
64 * Adds a failed test result
65 *
66 * @param String classname
67 * @param String name
68 * @param String message
69 * @param String type
70 */
71 this.addFailure = function(classname, name, message, type) {
72 var fnode = node('testcase', {
73 classname: classname,
74 name: name
75 });
76 var failure = node('failure', {
77 type: type || "unknown"
78 });
79 failure.appendChild(document.createTextNode(message || "no message left"));
80 fnode.appendChild(failure);
81 xml.appendChild(fnode);
82 };
83
84 /**
85 * Retrieves generated XML object - actually an HTMLElement.
86 *
87 * @return HTMLElement
88 */
89 this.getXML = function() {
90 return xml;
91 };
92 };
93 })(phantom);
...\ No newline at end of file ...\ No newline at end of file