├── deps └── js │ ├── config.js │ ├── casperjs │ ├── package.json │ ├── modules │ │ ├── http.js │ │ ├── mouse.js │ │ ├── colorizer.js │ │ ├── pagestack.js │ │ ├── cli.js │ │ ├── querystring.js │ │ ├── xunit.js │ │ ├── events.js │ │ ├── utils.js │ │ ├── clientutils.js │ │ └── tester.js │ └── bootstrap.js │ ├── phjs-srv.js │ └── base64.js ├── external-program-fix.lisp ├── phantomjs-test.asd ├── phantomjs.asd ├── package.lisp ├── phantomjs-test.lisp ├── README.md └── phantomjs.lisp /deps/js/config.js: -------------------------------------------------------------------------------- 1 | { 2 | "localToRemoteUrlAccessEnabled": true, 3 | "diskCacheEnabled": true, 4 | "maxDiskCacheSize": 5000000 5 | } 6 | -------------------------------------------------------------------------------- /external-program-fix.lisp: -------------------------------------------------------------------------------- 1 | (in-package :external-program) 2 | 3 | #+sbcl 4 | (defmethod process-id (process) 5 | (sb-ext:process-pid process)) 6 | -------------------------------------------------------------------------------- /phantomjs-test.asd: -------------------------------------------------------------------------------- 1 | ;;;; phantomjs.asd 2 | 3 | (asdf:defsystem #:phantomjs-test 4 | :serial t 5 | :description "Tests and examples for PHANTOMJS" 6 | :author "Vladimir G. Sekissov " 7 | :license "BSD" 8 | :depends-on (#:phantomjs #:fiveam #:parenscript #:alexandria) 9 | :components ((:file "phantomjs-test"))) 10 | -------------------------------------------------------------------------------- /phantomjs.asd: -------------------------------------------------------------------------------- 1 | ;;;; phantomjs.asd 2 | 3 | (asdf:defsystem #:phantomjs 4 | :serial t 5 | :description "Interfacing with PhantomJS" 6 | :author "Vladimir G. Sekissov " 7 | :license "BSD" 8 | :depends-on (#:alexandria 9 | #:external-program 10 | #:drakma 11 | #:babel 12 | #:puri 13 | #:hu.dwim.defclass-star 14 | #:parenscript 15 | #:cl-fad 16 | #:cl-base64 17 | #:yason 18 | #:temporary-file) 19 | :components ((:file "package") 20 | (:file "external-program-fix") 21 | (:file "phantomjs"))) 22 | -------------------------------------------------------------------------------- /deps/js/casperjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casperjs", 3 | "description": "Navigation scripting & testing utility for PhantomJS", 4 | "version": "1.1.0-DEV", 5 | "keywords": [ 6 | "phantomjs", 7 | "javascript" 8 | ], 9 | "maintainers": [ 10 | { 11 | "name": "Nicolas Perriault", 12 | "email": "nperriault@gmail.com", 13 | "web": "http://www.akei.com" 14 | } 15 | ], 16 | "dependencies": { 17 | "http://www.phantomjs.org/": "master" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/n1k0/casperjs/issues" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "git", 25 | "url": "git://github.com/n1k0/casperjs.git" 26 | } 27 | ], 28 | "licenses": [ 29 | { 30 | "name": "MIT", 31 | "url": "http://www.opensource.org/licenses/mit-license.php" 32 | } 33 | ], 34 | "homepage": "http://casperjs.org" 35 | } 36 | -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | ;;;; package.lisp 2 | 3 | (defpackage #:phantomjs 4 | (:nicknames #:phjs) 5 | (:use #:cl) 6 | (:import-from #:alexandria 7 | #:when-let 8 | #:if-let 9 | #:switch 10 | #:hash-table-plist 11 | #:with-unique-names 12 | #:once-only) 13 | (:import-from #:hu.dwim.defclass-star 14 | #:defclass* 15 | #:defcondition*) 16 | (:import-from #:parenscript 17 | #:*ps-print-pretty*) 18 | (:export #:*phantomjs-bin* 19 | #:*phantomjs-default-args* 20 | #:*cookies-file* 21 | #:*scripts-path* 22 | #:*config-file* 23 | #:instance-start 24 | #:instance-receive 25 | #:instance-stop 26 | #:instance-kill 27 | #:instance-alivep 28 | #:instance-call 29 | #:inject-file 30 | #:inject-script 31 | #:phjs-send 32 | #:wrap-ps 33 | #:with-phantomjs 34 | #:return-from-receive 35 | #:call 36 | #:receive)) 37 | -------------------------------------------------------------------------------- /phantomjs-test.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :phantomjs-test 2 | (:use #:cl #:fiveam) 3 | (:import-from #:alexandria 4 | #:hash-table-plist)) 5 | 6 | (in-package :phantomjs-test) 7 | 8 | (def-suite :phantomjs) 9 | (in-suite :phantomjs) 10 | 11 | (test get-google 12 | (let ((script (phjs:wrap-ps 13 | (defvar casper (ps:chain (require "casper") (create))) 14 | (defvar links (list)) 15 | 16 | (defun get-links () 17 | (defvar links ((ps:@ document query-selector-all) "h3.r a")) 18 | (*array.prototype.map.call links 19 | (lambda (e) ((ps:@ e get-attribute) "href")))) 20 | 21 | (casper.start "http://google.fr" 22 | (lambda () 23 | (this.fill "form[action=\"/search\"]" 24 | (ps:create :q "casperjs") 25 | t))) 26 | (casper.then (lambda () 27 | (setq links (this.evaluate get-links)) 28 | (this.fill "form[action=\"/search\"]" 29 | (ps:create :q "phantomjs") 30 | t))) 31 | (casper.then (lambda () 32 | (setq links (links.concat (this.evaluate get-links))) 33 | (return links))) 34 | (casper.run (lambda () 35 | (phjs:phjs-send (ps:create :found links.length :links links)))) 36 | (return t) 37 | ))) 38 | (let ((res (phjs:with-phantomjs (srv) 39 | (phjs:call srv script) 40 | (phjs:receive (srv msg) 41 | (phjs:return-from-receive msg))))) 42 | (is (numberp (gethash "found" res))) 43 | (is (= (gethash "found" res) (length (gethash "links" res)))) 44 | ))) 45 | -------------------------------------------------------------------------------- /deps/js/phjs-srv.js: -------------------------------------------------------------------------------- 1 | // Derived from ghostweb: phantomjs controlling server of Nic Ferrier 2 | try { 3 | 4 | var system = require('system'); 5 | // initialize CasperJS 6 | phantom.casperPath = system.args[2] + '/casperjs'; 7 | // we want to use Casper patched require 8 | phantom.casperScriptBaseDir = system.args[2]; 9 | phantom.injectJs(phantom.casperPath + '/bootstrap.js'); 10 | 11 | var base64 = require('base64'); 12 | var server = require('webserver').create(); 13 | // Start the server on whatever we were told to listen to. 14 | 15 | var service = server.listen( 16 | system.args[1], function (request, response) { 17 | if (request.headers.command == "call") { 18 | 19 | var cmdArg = base64.decode(request.headers.commandarg) 20 | .replace(/^\x00+|\x00+$/g, ''); 21 | 22 | var f = Function( 23 | "try { return " 24 | + cmdArg 25 | + "} catch (e) { return {'type': 'error', 'name': e.name, 'message': e.message }; }" 26 | ); 27 | var retval = f(); 28 | 29 | if (retval["type"] == 'error') { 30 | response.statusCode = 400; 31 | retval.command = cmdArg; 32 | } 33 | else { 34 | response.statusCode = 200; 35 | } 36 | response.write(base64.encode(JSON.stringify(retval))); 37 | response.close(); 38 | } 39 | else if (request.headers.command == "exit") { 40 | response.statusCode = 200; 41 | response.write("Ok\n"); 42 | response.close(); 43 | phantom.exit(); 44 | } 45 | else { 46 | response.statusCode = 404; 47 | response.write("Unknown\n"); 48 | response.close(); 49 | } 50 | }); 51 | 52 | console.log(service ? "$phjs> started" : "$phjs> failed"); 53 | } 54 | catch (e) { 55 | console.log(e); 56 | phantom.exit(); 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhantomJS # 2 | 3 | Helper classes for working with [PhantomJS](http://phantomjs.org) 4 | and [CasperJS](http://http://casperjs.org/) 5 | 6 | ``` lisp 7 | (let ((script (phjs:wrap-ps 8 | (defvar casper (ps:chain (require "casper") (create))) 9 | (defvar links (list)) 10 | 11 | (defun get-links () 12 | (defvar links ((ps:@ document query-selector-all) "h3.r a")) 13 | (*array.prototype.map.call links 14 | (lambda (e) 15 | ((ps:@ e get-attribute) "href")))) 16 | 17 | (casper.start "http://google.fr" 18 | (lambda () 19 | (this.fill "form[action=\"/search\"]" 20 | (ps:create :q "casperjs") 21 | t))) 22 | (casper.then (lambda () 23 | (setq links (this.evaluate get-links)) 24 | (this.fill "form[action=\"/search\"]" 25 | (ps:create :q "phantomjs") 26 | t))) 27 | (casper.then (lambda () 28 | (setq links 29 | (links.concat (this.evaluate get-links))) 30 | (return links))) 31 | (casper.run (lambda () 32 | (phjs:phjs-send 33 | (ps:create :found links.length :links links)))) 34 | (return t) 35 | ))) 36 | (phjs:with-phantomjs (srv) 37 | (phjs:call srv script) 38 | (phjs:receive (srv msg) 39 | (phjs:return-from-receive (hash-table-plist msg))))) 40 | ;;=> ("links" 41 | ;; ("/url?q=http://casperjs.org/&sa=..." 42 | ;; "/url?q=http://docs.casperjs.org/&sa=..." 43 | ;; ... 44 | ;; "/url?q=http://docs.casperjs.org/en/latest/..." 45 | ;; ) 46 | ;; "found" 19) 47 | ``` 48 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/http.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var utils = require('utils'); 35 | 36 | /* 37 | * Building an Array subclass 38 | */ 39 | function responseHeaders(){} 40 | responseHeaders.prototype = []; 41 | 42 | /** 43 | * Retrieves a given header based on its name 44 | * 45 | * @param String name A case-insensitive response header name 46 | * @return mixed A header string or `null` if not found 47 | */ 48 | responseHeaders.prototype.get = function get(name){ 49 | "use strict"; 50 | var headerValue = null; 51 | name = name.toLowerCase(); 52 | this.some(function(header){ 53 | if (header.name.toLowerCase() === name){ 54 | headerValue = header.value; 55 | return true; 56 | } 57 | }); 58 | return headerValue; 59 | }; 60 | 61 | /** 62 | * Augments the response with proper prototypes. 63 | * 64 | * @param Mixed response Phantom response or undefined (generally with local files) 65 | * @return Object Augmented response 66 | */ 67 | exports.augmentResponse = function(response) { 68 | "use strict"; 69 | /*jshint proto:true*/ 70 | if (!utils.isHTTPResource(response)) { 71 | return; 72 | } 73 | response.headers.__proto__ = responseHeaders.prototype; 74 | return response; 75 | }; 76 | -------------------------------------------------------------------------------- /deps/js/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Vassilis Petroulias [DRDigit] 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | (function(exports) { 18 | var B64 = { 19 | alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', 20 | lookup: null, 21 | encode: function (s) { 22 | var buffer = B64.toUtf8(s), 23 | position = -1, 24 | len = buffer.length, 25 | nan1, nan2, enc = [, , , ]; 26 | 27 | result = ''; 28 | while (++position < len) { 29 | nan1 = buffer[position + 1], nan2 = buffer[position + 2]; 30 | enc[0] = buffer[position] >> 2; 31 | enc[1] = ((buffer[position] & 3) << 4) | (buffer[++position] >> 4); 32 | if (isNaN(nan1)) enc[2] = enc[3] = 64; 33 | else { 34 | enc[2] = ((buffer[position] & 15) << 2) | (buffer[++position] >> 6); 35 | enc[3] = (isNaN(nan2)) ? 64 : buffer[position] & 63; 36 | } 37 | result += B64.alphabet[enc[0]] + B64.alphabet[enc[1]] + B64.alphabet[enc[2]] + B64.alphabet[enc[3]]; 38 | } 39 | return result; 40 | 41 | }, 42 | decode: function (s) { 43 | var buffer = B64.fromUtf8(s), 44 | position = 0, 45 | len = buffer.length; 46 | 47 | result = ''; 48 | while (position < len) { 49 | if (buffer[position] < 128) result += String.fromCharCode(buffer[position++]); 50 | else if (buffer[position] > 191 && buffer[position] < 224) result += String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63)); 51 | else result += String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63)); 52 | } 53 | return result; 54 | 55 | }, 56 | toUtf8: function (s) { 57 | var position = -1, 58 | len = s.length, 59 | chr, buffer = []; 60 | if (/^[\x00-\x7f]*$/.test(s)) while (++position < len) 61 | buffer.push(s.charCodeAt(position)); 62 | else while (++position < len) { 63 | chr = s.charCodeAt(position); 64 | if (chr < 128) buffer.push(chr); 65 | else if (chr < 2048) buffer.push((chr >> 6) | 192, (chr & 63) | 128); 66 | else buffer.push((chr >> 12) | 224, ((chr >> 6) & 63) | 128, (chr & 63) | 128); 67 | } 68 | return buffer; 69 | }, 70 | fromUtf8: function (s) { 71 | var position = -1, 72 | len, buffer = [], 73 | enc = [, , , ]; 74 | if (!B64.lookup) { 75 | len = B64.alphabet.length; 76 | B64.lookup = {}; 77 | while (++position < len) 78 | B64.lookup[B64.alphabet[position]] = position; 79 | position = -1; 80 | } 81 | len = s.length; 82 | while (position < len) { 83 | enc[0] = B64.lookup[s.charAt(++position)]; 84 | enc[1] = B64.lookup[s.charAt(++position)]; 85 | buffer.push((enc[0] << 2) | (enc[1] >> 4)); 86 | enc[2] = B64.lookup[s.charAt(++position)]; 87 | if (enc[2] == 64) break; 88 | buffer.push(((enc[1] & 15) << 4) | (enc[2] >> 2)); 89 | enc[3] = B64.lookup[s.charAt(++position)]; 90 | if (enc[3] == 64) break; 91 | buffer.push(((enc[2] & 3) << 6) | enc[3]); 92 | } 93 | return buffer; 94 | } 95 | }; 96 | exports.encode = B64.encode; 97 | exports.decode = B64.decode; 98 | 99 | })(typeof exports === "object" ? exports : window); -------------------------------------------------------------------------------- /deps/js/casperjs/modules/mouse.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, exports, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var utils = require('utils'); 35 | 36 | exports.create = function create(casper) { 37 | "use strict"; 38 | return new Mouse(casper); 39 | }; 40 | 41 | var Mouse = function Mouse(casper) { 42 | "use strict"; 43 | if (!utils.isCasperObject(casper)) { 44 | throw new CasperError('Mouse() needs a Casper instance'); 45 | } 46 | 47 | var slice = Array.prototype.slice, 48 | nativeEvents = ['mouseup', 'mousedown', 'click', 'mousemove']; 49 | if (utils.gteVersion(phantom.version, '1.8.0')) { 50 | nativeEvents.push('doubleclick'); 51 | } 52 | var emulatedEvents = ['mouseover', 'mouseout'], 53 | supportedEvents = nativeEvents.concat(emulatedEvents); 54 | 55 | function computeCenter(selector) { 56 | var bounds = casper.getElementBounds(selector); 57 | if (utils.isClipRect(bounds)) { 58 | var x = Math.round(bounds.left + bounds.width / 2); 59 | var y = Math.round(bounds.top + bounds.height / 2); 60 | return [x, y]; 61 | } 62 | } 63 | 64 | function processEvent(type, args) { 65 | if (!utils.isString(type) || supportedEvents.indexOf(type) === -1) { 66 | throw new CasperError('Mouse.processEvent(): Unsupported mouse event type: ' + type); 67 | } 68 | if (emulatedEvents.indexOf(type) > -1) { 69 | casper.log("Mouse.processEvent(): no native fallback for type " + type, "warning"); 70 | } 71 | args = slice.call(args); // cast Arguments -> Array 72 | casper.emit('mouse.' + type.replace('mouse', ''), args); 73 | switch (args.length) { 74 | case 0: 75 | throw new CasperError('Mouse.processEvent(): Too few arguments'); 76 | case 1: 77 | // selector 78 | casper.page.sendEvent.apply(casper.page, [type].concat(computeCenter(args[0]))); 79 | break; 80 | case 2: 81 | // coordinates 82 | if (!utils.isNumber(args[0]) || !utils.isNumber(args[1])) { 83 | throw new CasperError('Mouse.processEvent(): No valid coordinates passed: ' + args); 84 | } 85 | casper.page.sendEvent(type, args[0], args[1]); 86 | break; 87 | default: 88 | throw new CasperError('Mouse.processEvent(): Too many arguments'); 89 | } 90 | } 91 | 92 | this.processEvent = function() { 93 | processEvent(arguments[0], slice.call(arguments, 1)); 94 | }; 95 | 96 | this.click = function click() { 97 | processEvent('click', arguments); 98 | }; 99 | 100 | this.doubleclick = function doubleclick() { 101 | processEvent('doubleclick', arguments); 102 | }; 103 | 104 | this.down = function down() { 105 | processEvent('mousedown', arguments); 106 | }; 107 | 108 | this.move = function move() { 109 | processEvent('mousemove', arguments); 110 | }; 111 | 112 | this.up = function up() { 113 | processEvent('mouseup', arguments); 114 | }; 115 | }; 116 | exports.Mouse = Mouse; 117 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/colorizer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global exports, console, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var fs = require('fs'); 35 | var utils = require('utils'); 36 | var env = require('system').env; 37 | 38 | exports.create = function create(type) { 39 | "use strict"; 40 | if (!type) { 41 | return; 42 | } 43 | if (!(type in exports)) { 44 | throw new Error(utils.format('Unsupported colorizer type "%s"', type)); 45 | } 46 | return new exports[type](); 47 | }; 48 | 49 | /** 50 | * This is a port of lime colorizer. 51 | * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php 52 | * 53 | * (c) Fabien Potencier, Symfony project, MIT license 54 | */ 55 | var Colorizer = function Colorizer() { 56 | "use strict"; 57 | var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 }; 58 | var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 }; 59 | var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 }; 60 | var styles = { 61 | 'ERROR': { bg: 'red', fg: 'white', bold: true }, 62 | 'INFO': { fg: 'green', bold: true }, 63 | 'TRACE': { fg: 'green', bold: true }, 64 | 'PARAMETER': { fg: 'cyan' }, 65 | 'COMMENT': { fg: 'yellow' }, 66 | 'WARNING': { fg: 'red', bold: true }, 67 | 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true }, 68 | 'RED_BAR': { fg: 'white', bg: 'red', bold: true }, 69 | 'INFO_BAR': { bg: 'cyan', fg: 'white', bold: true }, 70 | 'WARN_BAR': { bg: 'yellow', fg: 'white', bold: true }, 71 | 'SKIP': { fg: 'magenta', bold: true }, 72 | 'SKIP_BAR': { bg: 'magenta', fg: 'white', bold: true } 73 | }; 74 | 75 | /** 76 | * Adds a style to provided text. 77 | * 78 | * @param String text 79 | * @param String styleName 80 | * @return String 81 | */ 82 | this.colorize = function colorize(text, styleName, pad) { 83 | if ((fs.isWindows() && !env['ANSICON']) || !(styleName in styles)) { 84 | return text; 85 | } 86 | return this.format(text, styles[styleName], pad); 87 | }; 88 | 89 | /** 90 | * Formats a text using a style declaration object. 91 | * 92 | * @param String text 93 | * @param Object style 94 | * @return String 95 | */ 96 | this.format = function format(text, style, pad) { 97 | if ((fs.isWindows() && !env['ANSICON']) || !utils.isObject(style)) { 98 | return text; 99 | } 100 | var codes = []; 101 | if (style.fg && foreground[style.fg]) { 102 | codes.push(foreground[style.fg]); 103 | } 104 | if (style.bg && background[style.bg]) { 105 | codes.push(background[style.bg]); 106 | } 107 | for (var option in options) { 108 | if (option in style && style[option] === true) { 109 | codes.push(options[option]); 110 | } 111 | } 112 | // pad 113 | if (typeof pad === "number" && text.length < pad) { 114 | text += new Array(pad - text.length + 1).join(' '); 115 | } 116 | return "\u001b[" + codes.join(';') + 'm' + text + "\u001b[0m"; 117 | }; 118 | }; 119 | exports.Colorizer = Colorizer; 120 | 121 | /** 122 | * Dummy colorizer. Does basically nothing. 123 | * 124 | */ 125 | var Dummy = function Dummy() { 126 | "use strict"; 127 | this.colorize = function colorize(text, styleName, pad) { 128 | return text; 129 | }; 130 | this.format = function format(text, style, pad){ 131 | return text; 132 | }; 133 | }; 134 | exports.Dummy = Dummy; 135 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/pagestack.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, console, exports, phantom, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var utils = require('utils'); 35 | var f = utils.format; 36 | 37 | function create() { 38 | "use strict"; 39 | return new Stack(); 40 | } 41 | exports.create = create; 42 | 43 | /** 44 | * Popups container. Implements Array prototype. 45 | * 46 | */ 47 | var Stack = function Stack(){}; 48 | exports.Stack = Stack; 49 | 50 | Stack.prototype = []; 51 | 52 | /** 53 | * Cleans the stack from closed popup. 54 | * 55 | * @param WebPage closed Closed popup page instance 56 | * @return Number New stack length 57 | */ 58 | Stack.prototype.clean = function clean(closed) { 59 | "use strict"; 60 | var closedIndex = -1; 61 | this.forEach(function(popup, index) { 62 | if (closed === popup) { 63 | closedIndex = index; 64 | } 65 | }); 66 | if (closedIndex > -1) { 67 | this.splice(closedIndex, 1); 68 | } 69 | return this.length; 70 | }; 71 | 72 | /** 73 | * Finds a popup matching the provided information. Information can be: 74 | * 75 | * - RegExp: matching page url 76 | * - String: strict page url value 77 | * - WebPage: a direct WebPage instance 78 | * 79 | * @param Mixed popupInfo 80 | * @return WebPage 81 | */ 82 | Stack.prototype.find = function find(popupInfo) { 83 | "use strict"; 84 | var popup, type = utils.betterTypeOf(popupInfo); 85 | switch (type) { 86 | case "regexp": 87 | popup = this.findByRegExp(popupInfo); 88 | break; 89 | case "string": 90 | popup = this.findByURL(popupInfo); 91 | break; 92 | case "qtruntimeobject": // WebPage 93 | popup = popupInfo; 94 | if (!utils.isWebPage(popup) || !this.some(function(popupPage) { 95 | if (popupInfo.id && popupPage.id) { 96 | return popupPage.id === popup.id; 97 | } 98 | return popupPage.url === popup.url; 99 | })) { 100 | throw new CasperError("Invalid or missing popup."); 101 | } 102 | break; 103 | default: 104 | throw new CasperError(f("Invalid popupInfo type: %s.", type)); 105 | } 106 | return popup; 107 | }; 108 | 109 | /** 110 | * Finds the first popup which url matches a given RegExp. 111 | * 112 | * @param RegExp regexp 113 | * @return WebPage 114 | */ 115 | Stack.prototype.findByRegExp = function findByRegExp(regexp) { 116 | "use strict"; 117 | var popup = this.filter(function(popupPage) { 118 | return regexp.test(popupPage.url); 119 | })[0]; 120 | if (!popup) { 121 | throw new CasperError(f("Couldn't find popup with url matching pattern %s", regexp)); 122 | } 123 | return popup; 124 | }; 125 | 126 | /** 127 | * Finds the first popup matching a given url. 128 | * 129 | * @param String url The child WebPage url 130 | * @return WebPage 131 | */ 132 | Stack.prototype.findByURL = function findByURL(string) { 133 | "use strict"; 134 | var popup = this.filter(function(popupPage) { 135 | return popupPage.url.indexOf(string) !== -1; 136 | })[0]; 137 | if (!popup) { 138 | throw new CasperError(f("Couldn't find popup with url containing '%s'", string)); 139 | } 140 | return popup; 141 | }; 142 | 143 | /** 144 | * Returns a human readable list of current active popup urls. 145 | * 146 | * @return Array Mapped stack. 147 | */ 148 | Stack.prototype.list = function list() { 149 | "use strict"; 150 | return this.map(function(popup) { 151 | try { 152 | return popup.url; 153 | } catch (e) { 154 | return ''; 155 | } 156 | }); 157 | }; 158 | 159 | /** 160 | * String representation of current instance. 161 | * 162 | * @return String 163 | */ 164 | Stack.prototype.toString = function toString() { 165 | "use strict"; 166 | return f("[Object Stack], having %d popup(s)" % this.length); 167 | }; 168 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/cli.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, console, exports, phantom, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var utils = require('utils'); 35 | var system = require('system'); 36 | 37 | /** 38 | * Extracts, normalize ad organize PhantomJS CLI arguments in a dedicated 39 | * Object. 40 | * 41 | * @param array phantomArgs system.args value 42 | * @return Object 43 | */ 44 | exports.parse = function parse(phantomArgs) { 45 | "use strict"; 46 | var extract = { 47 | args: [], 48 | options: {}, 49 | raw: { 50 | args: [], 51 | options: {} 52 | }, 53 | drop: function drop(what) { 54 | if (utils.isNumber(what)) { 55 | // deleting an arg by its position 56 | this.args = this.args.filter(function _filter(arg, index) { 57 | return index !== what; 58 | }); 59 | // raw 60 | if ('raw' in this) { 61 | this.raw.args = this.raw.args.filter(function _filter(arg, index) { 62 | return index !== what; 63 | }); 64 | } 65 | } else if (utils.isString(what)) { 66 | // deleting an arg by its value 67 | this.args = this.args.filter(function _filter(arg) { 68 | return arg !== what; 69 | }); 70 | // deleting an option by its name (key) 71 | delete this.options[what]; 72 | // raw 73 | if ('raw' in this) { 74 | this.raw.args = this.raw.args.filter(function _filter(arg) { 75 | return arg !== what; 76 | }); 77 | delete this.raw.options[what]; 78 | } 79 | } else { 80 | throw new CasperError("Cannot drop argument of type " + typeof what); 81 | } 82 | }, 83 | has: function has(what) { 84 | if (utils.isNumber(what)) { 85 | return what in this.args; 86 | } 87 | if (utils.isString(what)) { 88 | return what in this.options; 89 | } 90 | throw new CasperError("Unsupported cli arg tester " + typeof what); 91 | }, 92 | get: function get(what, def) { 93 | if (utils.isNumber(what)) { 94 | return what in this.args ? this.args[what] : def; 95 | } 96 | if (utils.isString(what)) { 97 | return what in this.options ? this.options[what] : def; 98 | } 99 | throw new CasperError("Unsupported cli arg getter " + typeof what); 100 | } 101 | }; 102 | phantomArgs.forEach(function _forEach(arg) { 103 | if (arg.indexOf('--') === 0) { 104 | // named option 105 | var optionMatch = arg.match(/^--(.*?)=(.*)/i); 106 | if (optionMatch) { 107 | extract.options[optionMatch[1]] = castArgument(optionMatch[2]); 108 | extract.raw.options[optionMatch[1]] = optionMatch[2]; 109 | } else { 110 | // flag 111 | var flagMatch = arg.match(/^--(.*)/); 112 | if (flagMatch) { 113 | extract.options[flagMatch[1]] = extract.raw.options[flagMatch[1]] = true; 114 | } 115 | } 116 | } else { 117 | // positional arg 118 | extract.args.push(castArgument(arg)); 119 | extract.raw.args.push(arg); 120 | } 121 | }); 122 | extract.raw = utils.mergeObjects(extract.raw, { 123 | drop: function() { 124 | return extract.drop.apply(extract, arguments); 125 | }, 126 | has: extract.has, 127 | get: extract.get 128 | }); 129 | return extract; 130 | }; 131 | 132 | /** 133 | * Cast a string argument to its typed equivalent. 134 | * 135 | * @param String arg 136 | * @return Mixed 137 | */ 138 | function castArgument(arg) { 139 | "use strict"; 140 | if (arg.match(/^-?\d+$/)) { 141 | return parseInt(arg, 10); 142 | } else if (arg.match(/^-?\d+\.\d+$/)) { 143 | return parseFloat(arg); 144 | } else if (arg.match(/^(true|false)$/i)) { 145 | return arg.trim().toLowerCase() === "true" ? true : false; 146 | } else { 147 | return arg; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/querystring.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | // Query String Utilities 23 | 24 | var QueryString = exports; 25 | 26 | 27 | // If obj.hasOwnProperty has been overridden, then calling 28 | // obj.hasOwnProperty(prop) will break. 29 | // See: https://github.com/joyent/node/issues/1707 30 | function hasOwnProperty(obj, prop) { 31 | return Object.prototype.hasOwnProperty.call(obj, prop); 32 | } 33 | 34 | 35 | function charCode(c) { 36 | return c.charCodeAt(0); 37 | } 38 | 39 | 40 | // a safe fast alternative to decodeURIComponent 41 | QueryString.unescapeBuffer = function(s, decodeSpaces) { 42 | var out = new Buffer(s.length); 43 | var state = 'CHAR'; // states: CHAR, HEX0, HEX1 44 | var n, m, hexchar; 45 | 46 | for (var inIndex = 0, outIndex = 0; inIndex <= s.length; inIndex++) { 47 | var c = s.charCodeAt(inIndex); 48 | switch (state) { 49 | case 'CHAR': 50 | switch (c) { 51 | case charCode('%'): 52 | n = 0; 53 | m = 0; 54 | state = 'HEX0'; 55 | break; 56 | case charCode('+'): 57 | if (decodeSpaces) c = charCode(' '); 58 | // pass thru 59 | default: 60 | out[outIndex++] = c; 61 | break; 62 | } 63 | break; 64 | 65 | case 'HEX0': 66 | state = 'HEX1'; 67 | hexchar = c; 68 | if (charCode('0') <= c && c <= charCode('9')) { 69 | n = c - charCode('0'); 70 | } else if (charCode('a') <= c && c <= charCode('f')) { 71 | n = c - charCode('a') + 10; 72 | } else if (charCode('A') <= c && c <= charCode('F')) { 73 | n = c - charCode('A') + 10; 74 | } else { 75 | out[outIndex++] = charCode('%'); 76 | out[outIndex++] = c; 77 | state = 'CHAR'; 78 | break; 79 | } 80 | break; 81 | 82 | case 'HEX1': 83 | state = 'CHAR'; 84 | if (charCode('0') <= c && c <= charCode('9')) { 85 | m = c - charCode('0'); 86 | } else if (charCode('a') <= c && c <= charCode('f')) { 87 | m = c - charCode('a') + 10; 88 | } else if (charCode('A') <= c && c <= charCode('F')) { 89 | m = c - charCode('A') + 10; 90 | } else { 91 | out[outIndex++] = charCode('%'); 92 | out[outIndex++] = hexchar; 93 | out[outIndex++] = c; 94 | break; 95 | } 96 | out[outIndex++] = 16 * n + m; 97 | break; 98 | } 99 | } 100 | 101 | // TODO support returning arbitrary buffers. 102 | 103 | return out.slice(0, outIndex - 1); 104 | }; 105 | 106 | 107 | QueryString.unescape = function(s, decodeSpaces) { 108 | return QueryString.unescapeBuffer(s, decodeSpaces).toString(); 109 | }; 110 | 111 | 112 | QueryString.escape = function(str) { 113 | return encodeURIComponent(str); 114 | }; 115 | 116 | var stringifyPrimitive = function(v) { 117 | switch (typeof v) { 118 | case 'string': 119 | return v; 120 | 121 | case 'boolean': 122 | return v ? 'true' : 'false'; 123 | 124 | case 'number': 125 | return isFinite(v) ? v : ''; 126 | 127 | default: 128 | return ''; 129 | } 130 | }; 131 | 132 | 133 | QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) { 134 | sep = sep || '&'; 135 | eq = eq || '='; 136 | if (obj === null) { 137 | obj = undefined; 138 | } 139 | 140 | if (typeof obj === 'object') { 141 | return Object.keys(obj).map(function(k) { 142 | var ks = QueryString.escape(stringifyPrimitive(k)) + eq; 143 | if (Array.isArray(obj[k])) { 144 | return obj[k].map(function(v) { 145 | return ks + QueryString.escape(stringifyPrimitive(v)); 146 | }).join(sep); 147 | } else { 148 | return ks + QueryString.escape(stringifyPrimitive(obj[k])); 149 | } 150 | }).join(sep); 151 | 152 | } 153 | 154 | if (!name) return ''; 155 | return QueryString.escape(stringifyPrimitive(name)) + eq + 156 | QueryString.escape(stringifyPrimitive(obj)); 157 | }; 158 | 159 | // Parse a key=val string. 160 | QueryString.parse = QueryString.decode = function(qs, sep, eq, options) { 161 | sep = sep || '&'; 162 | eq = eq || '='; 163 | var obj = {}; 164 | 165 | if (typeof qs !== 'string' || qs.length === 0) { 166 | return obj; 167 | } 168 | 169 | var regexp = /\+/g; 170 | qs = qs.split(sep); 171 | 172 | var maxKeys = 1000; 173 | if (options && typeof options.maxKeys === 'number') { 174 | maxKeys = options.maxKeys; 175 | } 176 | 177 | var len = qs.length; 178 | // maxKeys <= 0 means that we should not limit keys count 179 | if (maxKeys > 0 && len > maxKeys) { 180 | len = maxKeys; 181 | } 182 | 183 | for (var i = 0; i < len; ++i) { 184 | var x = qs[i].replace(regexp, '%20'), 185 | idx = x.indexOf(eq), 186 | kstr, vstr, k, v; 187 | 188 | if (idx >= 0) { 189 | kstr = x.substr(0, idx); 190 | vstr = x.substr(idx + 1); 191 | } else { 192 | kstr = x; 193 | vstr = ''; 194 | } 195 | 196 | try { 197 | k = decodeURIComponent(kstr); 198 | v = decodeURIComponent(vstr); 199 | } catch (e) { 200 | k = QueryString.unescape(kstr, true); 201 | v = QueryString.unescape(vstr, true); 202 | } 203 | 204 | if (!hasOwnProperty(obj, k)) { 205 | obj[k] = v; 206 | } else if (Array.isArray(obj[k])) { 207 | obj[k].push(v); 208 | } else { 209 | obj[k] = [obj[k], v]; 210 | } 211 | } 212 | 213 | return obj; 214 | }; 215 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/xunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, console, exports, phantom, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var utils = require('utils'); 35 | var fs = require('fs'); 36 | var TestSuiteResult = require('tester').TestSuiteResult; 37 | 38 | /** 39 | * Generates a value for 'classname' attribute of the JUnit XML report. 40 | * 41 | * Uses the (relative) file name of the current casper script without file 42 | * extension as classname. 43 | * 44 | * @param String classname 45 | * @return String 46 | */ 47 | function generateClassName(classname) { 48 | "use strict"; 49 | classname = (classname || "").replace(phantom.casperPath, "").trim(); 50 | var script = classname || phantom.casperScript || ""; 51 | if (script.indexOf(fs.workingDirectory) === 0) { 52 | script = script.substring(fs.workingDirectory.length + 1); 53 | } 54 | if (script.indexOf('/') === 0) { 55 | script = script.substring(1, script.length); 56 | } 57 | if (~script.indexOf('.')) { 58 | script = script.substring(0, script.lastIndexOf('.')); 59 | } 60 | // If we have trimmed our string down to nothing, default to script name 61 | if (!script && phantom.casperScript) { 62 | script = phantom.casperScript; 63 | } 64 | return script || "unknown"; 65 | } 66 | 67 | /** 68 | * Creates a XUnit instance 69 | * 70 | * @return XUnit 71 | */ 72 | exports.create = function create() { 73 | "use strict"; 74 | return new XUnitExporter(); 75 | }; 76 | 77 | /** 78 | * JUnit XML (xUnit) exporter for test results. 79 | * 80 | */ 81 | function XUnitExporter() { 82 | "use strict"; 83 | this.results = undefined; 84 | this._xml = utils.node('testsuites'); 85 | this._xml.toString = function toString() { 86 | return '' + this.outerHTML; // ouch 87 | }; 88 | } 89 | exports.XUnitExporter = XUnitExporter; 90 | 91 | /** 92 | * Retrieves generated XML object - actually an HTMLElement. 93 | * 94 | * @return HTMLElement 95 | */ 96 | XUnitExporter.prototype.getXML = function getXML() { 97 | "use strict"; 98 | if (!(this.results instanceof TestSuiteResult)) { 99 | throw new CasperError('Results not set, cannot get XML.'); 100 | } 101 | this.results.forEach(function(result) { 102 | var suiteNode = utils.node('testsuite', { 103 | name: result.name, 104 | tests: result.assertions, 105 | failures: result.failed, 106 | errors: result.crashed, 107 | time: utils.ms2seconds(result.calculateDuration()), 108 | timestamp: (new Date()).toISOString(), 109 | 'package': generateClassName(result.file) 110 | }); 111 | // succesful test cases 112 | result.passes.forEach(function(success) { 113 | var testCase = utils.node('testcase', { 114 | name: success.message || success.standard, 115 | classname: generateClassName(success.file), 116 | time: utils.ms2seconds(~~success.time) 117 | }); 118 | suiteNode.appendChild(testCase); 119 | }); 120 | // failed test cases 121 | result.failures.forEach(function(failure) { 122 | var testCase = utils.node('testcase', { 123 | name: failure.message || failure.standard, 124 | classname: generateClassName(failure.file), 125 | time: utils.ms2seconds(~~failure.time) 126 | }); 127 | var failureNode = utils.node('failure', { 128 | type: failure.type || "failure" 129 | }); 130 | failureNode.appendChild(document.createTextNode(failure.message || "no message left")); 131 | if (failure.values && failure.values.error instanceof Error) { 132 | var errorNode = utils.node('error', { 133 | type: utils.betterTypeOf(failure.values.error) 134 | }); 135 | errorNode.appendChild(document.createTextNode(failure.values.error.stack)); 136 | testCase.appendChild(errorNode); 137 | } 138 | testCase.appendChild(failureNode); 139 | suiteNode.appendChild(testCase); 140 | }); 141 | // errors 142 | result.errors.forEach(function(error) { 143 | var errorNode = utils.node('error', { 144 | type: error.name 145 | }); 146 | errorNode.appendChild(document.createTextNode(error.stack ? error.stack : error.message)); 147 | suiteNode.appendChild(errorNode); 148 | }); 149 | // warnings 150 | var warningNode = utils.node('system-out'); 151 | warningNode.appendChild(document.createTextNode(result.warnings.join('\n'))); 152 | suiteNode.appendChild(warningNode); 153 | this._xml.appendChild(suiteNode); 154 | }.bind(this)); 155 | this._xml.setAttribute('duration', utils.ms2seconds(this.results.calculateDuration())); 156 | return this._xml; 157 | }; 158 | 159 | /** 160 | * Sets test results. 161 | * 162 | * @param TestSuite results 163 | */ 164 | XUnitExporter.prototype.setResults = function setResults(results) { 165 | "use strict"; 166 | if (!(results instanceof TestSuiteResult)) { 167 | throw new CasperError('Invalid results type.'); 168 | } 169 | this.results = results; 170 | return results; 171 | }; 172 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/events.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError*/ 32 | 33 | var isArray = Array.isArray; 34 | 35 | function EventEmitter() { 36 | this._filters = {}; 37 | } 38 | exports.EventEmitter = EventEmitter; 39 | 40 | // By default EventEmitters will print a warning if more than 41 | // 10 listeners are added to it. This is a useful default which 42 | // helps finding memory leaks. 43 | // 44 | // Obviously not all Emitters should be limited to 10. This function allows 45 | // that to be increased. Set to zero for unlimited. 46 | var defaultMaxListeners = 10; 47 | EventEmitter.prototype.setMaxListeners = function(n) { 48 | if (!this._events) this._events = {}; 49 | this._maxListeners = n; 50 | }; 51 | 52 | 53 | EventEmitter.prototype.emit = function emit() { 54 | var type = arguments[0]; 55 | // If there is no 'error' event listener then throw. 56 | if (type === 'error') { 57 | if (!this._events || !this._events.error || 58 | (isArray(this._events.error) && !this._events.error.length)) 59 | { 60 | if (arguments[1] instanceof Error) { 61 | throw arguments[1]; // Unhandled 'error' event 62 | } else { 63 | throw new CasperError("Uncaught, unspecified 'error' event."); 64 | } 65 | } 66 | } 67 | 68 | if (!this._events) return false; 69 | var handler = this._events[type]; 70 | if (!handler) return false; 71 | 72 | if (typeof handler === 'function') { 73 | try { 74 | switch (arguments.length) { 75 | // fast cases 76 | case 1: 77 | handler.call(this); 78 | break; 79 | case 2: 80 | handler.call(this, arguments[1]); 81 | break; 82 | case 3: 83 | handler.call(this, arguments[1], arguments[2]); 84 | break; 85 | // slower 86 | default: 87 | var l = arguments.length; 88 | var args = new Array(l - 1); 89 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 90 | handler.apply(this, args); 91 | } 92 | } catch (err) { 93 | this.emit('event.error', err); 94 | } 95 | return true; 96 | 97 | } else if (isArray(handler)) { 98 | var l = arguments.length; 99 | var args = new Array(l - 1); 100 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 101 | 102 | var listeners = handler.slice(); 103 | for (var i = 0, l = listeners.length; i < l; i++) { 104 | listeners[i].apply(this, args); 105 | } 106 | return true; 107 | 108 | } else { 109 | return false; 110 | } 111 | }; 112 | 113 | // EventEmitter is defined in src/node_events.cc 114 | // EventEmitter.prototype.emit() is also defined there. 115 | EventEmitter.prototype.addListener = function addListener(type, listener) { 116 | if ('function' !== typeof listener) { 117 | throw new CasperError('addListener only takes instances of Function'); 118 | } 119 | 120 | if (!this._events) this._events = {}; 121 | 122 | // To avoid recursion in the case that type == "newListeners"! Before 123 | // adding it to the listeners, first emit "newListeners". 124 | this.emit('newListener', type, listener); 125 | 126 | if (!this._events[type]) { 127 | // Optimize the case of one listener. Don't need the extra array object. 128 | this._events[type] = listener; 129 | } else if (isArray(this._events[type])) { 130 | 131 | // If we've already got an array, just append. 132 | this._events[type].push(listener); 133 | 134 | // Check for listener leak 135 | if (!this._events[type].warned) { 136 | var m; 137 | if (this._maxListeners !== undefined) { 138 | m = this._maxListeners; 139 | } else { 140 | m = defaultMaxListeners; 141 | } 142 | 143 | if (m && m > 0 && this._events[type].length > m) { 144 | this._events[type].warned = true; 145 | console.error('(node) warning: possible EventEmitter memory ' + 146 | 'leak detected. %d listeners added. ' + 147 | 'Use emitter.setMaxListeners() to increase limit.', 148 | this._events[type].length); 149 | console.trace(); 150 | } 151 | } 152 | } else { 153 | // Adding the second element, need to change to array. 154 | this._events[type] = [this._events[type], listener]; 155 | } 156 | 157 | return this; 158 | }; 159 | 160 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 161 | 162 | EventEmitter.prototype.once = function once(type, listener) { 163 | if ('function' !== typeof listener) { 164 | throw new CasperError('.once only takes instances of Function'); 165 | } 166 | 167 | var self = this; 168 | function g() { 169 | self.removeListener(type, g); 170 | listener.apply(this, arguments); 171 | } 172 | 173 | g.listener = listener; 174 | self.on(type, g); 175 | 176 | return this; 177 | }; 178 | 179 | EventEmitter.prototype.removeListener = function removeListener(type, listener) { 180 | if ('function' !== typeof listener) { 181 | throw new CasperError('removeListener only takes instances of Function'); 182 | } 183 | 184 | // does not use listeners(), so no side effect of creating _events[type] 185 | if (!this._events || !this._events[type]) return this; 186 | 187 | var list = this._events[type]; 188 | 189 | if (isArray(list)) { 190 | var position = -1; 191 | for (var i = 0, length = list.length; i < length; i++) { 192 | if (list[i] === listener || 193 | (list[i].listener && list[i].listener === listener)) 194 | { 195 | position = i; 196 | break; 197 | } 198 | } 199 | 200 | if (position < 0) return this; 201 | list.splice(position, 1); 202 | if (list.length === 0) 203 | delete this._events[type]; 204 | } else if (list === listener || 205 | (list.listener && list.listener === listener)) 206 | { 207 | delete this._events[type]; 208 | } 209 | 210 | return this; 211 | }; 212 | 213 | EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { 214 | if (arguments.length === 0) { 215 | this._events = {}; 216 | return this; 217 | } 218 | 219 | // does not use listeners(), so no side effect of creating _events[type] 220 | if (type && this._events && this._events[type]) this._events[type] = null; 221 | return this; 222 | }; 223 | 224 | EventEmitter.prototype.listeners = function listeners(type) { 225 | if (!this._events) this._events = {}; 226 | if (!this._events[type]) this._events[type] = []; 227 | if (!isArray(this._events[type])) { 228 | this._events[type] = [this._events[type]]; 229 | } 230 | return this._events[type]; 231 | }; 232 | 233 | // Added for CasperJS: filters a value attached to an event 234 | EventEmitter.prototype.filter = function filter() { 235 | var type = arguments[0]; 236 | if (!this._filters) { 237 | this._filters = {}; 238 | return; 239 | } 240 | 241 | var _filter = this._filters[type]; 242 | if (typeof _filter !== 'function') { 243 | return; 244 | } 245 | return _filter.apply(this, Array.prototype.splice.call(arguments, 1)); 246 | }; 247 | 248 | EventEmitter.prototype.removeAllFilters = function removeAllFilters(type) { 249 | if (arguments.length === 0) { 250 | this._filters = {}; 251 | return this; 252 | } 253 | if (type && this._filters && this._filters[type]) { 254 | this._filters[type] = null; 255 | } 256 | return this; 257 | }; 258 | 259 | EventEmitter.prototype.setFilter = function setFilter(type, filterFn) { 260 | if (!this._filters) { 261 | this._filters = {}; 262 | } 263 | if ('function' !== typeof filterFn) { 264 | throw new CasperError('setFilter only takes instances of Function'); 265 | } 266 | if (!this._filters[type]) { 267 | this._filters[type] = filterFn; 268 | return true; 269 | } 270 | // TODO: process multiple filters? in which order? disallow? 271 | return false; 272 | }; 273 | -------------------------------------------------------------------------------- /phantomjs.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:phantomjs) 2 | 3 | (defvar *phantomjs-bin* #p"/usr/bin/phantomjs") 4 | (defvar *slimerjs-bin* (ignore-errors (truename (merge-pathnames 5 | #p"../slimerjs-nightly/slimerjs" 6 | (asdf:system-source-directory :phantomjs))))) 7 | (defvar *debugp* t) 8 | (defvar *phantomjs-default-args* (list "--debug=true")) 9 | (defvar *cookies-file* (merge-pathnames #p".phjs.cookies" 10 | (user-homedir-pathname))) 11 | (defvar *scripts-path* (merge-pathnames 12 | #p"deps/js/" 13 | (asdf:system-source-directory :phantomjs))) 14 | (defvar *config-file* #p"config.js") 15 | (defvar *response-prefix* "$phjs> ") 16 | (defvar *server-file* #p"phjs-srv.js") 17 | 18 | (defvar *instances* (make-hash-table)) 19 | 20 | (let ((port #x2fff)) 21 | (defun next-portnum () 22 | (incf port) 23 | (when (> port #xffff) 24 | (setf port #x2fff)) 25 | port)) 26 | 27 | (defcondition* instance-error (error) 28 | ((message) 29 | (value)) 30 | (:documentation "PhantomJS error.") 31 | (:export-accessor-names-p t) 32 | (:export-class-name-p t)) 33 | 34 | (defmethod print-object ((self instance-error) stream) 35 | (with-slots (message value) self 36 | (print-unreadable-object (self stream :type t :identity t) 37 | (format stream "~a ~@[~{~a => ~a~^, ~}~]~@[~a~]" 38 | message 39 | (when (hash-table-p value) 40 | (hash-table-plist value)) 41 | (unless (hash-table-p value) 42 | value))))) 43 | 44 | (defcondition* instance-call-error (instance-error) 45 | ((message) 46 | (value)) 47 | (:documentation "Raised when javascript side returns error.")) 48 | 49 | (defun instance-sentinel (proc) 50 | (let ((status (external-program:process-status proc))) 51 | (when (not (eql :running status)) 52 | (let ((pid (external-program:process-id proc))) 53 | (ignore-errors (close (external-program:process-output-stream proc))) 54 | (format t "Process phantomjs ~a finished with status ~a~%" 55 | pid status) 56 | (setf (proc-of (gethash pid *instances*)) nil) 57 | (remhash pid *instances*))))) 58 | 59 | (defclass* phantomjs-instance () 60 | ((proc :documentation "system process") 61 | (port :type integer :initform (next-portnum))) 62 | (:documentation "PhantomJS instance") 63 | (:export-accessor-names-p t) 64 | (:export-class-name-p t)) 65 | 66 | (defmethod instance-start ((self phantomjs-instance) 67 | &key (engine :phantomjs) cmd-args) 68 | (loop 69 | for startp = t then nil 70 | for port = (port-of self) then (next-portnum) 71 | for proc = (phantomjs-server port engine cmd-args) 72 | when (and (not startp) (= port (port-of self))) 73 | do (error 'instance-error :message "Can't allocate port") 74 | while (not proc) 75 | finally (setf (proc-of self) proc 76 | (port-of self) port 77 | (gethash (external-program:process-id proc) *instances*) self))) 78 | 79 | (defmethod instance-receive ((self phantomjs-instance) (handler function) 80 | &key (timeout 5) (oncep nil)) 81 | (let ((in (external-program:process-output-stream (proc-of self)))) 82 | (sb-sys:with-fd-handler ((sb-sys:fd-stream-fd in) 83 | :input 84 | #'(lambda (fd) 85 | (declare (ignore fd)) 86 | (funcall handler 87 | (phantomjs-decode-response 88 | (phantomjs-read-response in))))) 89 | (if oncep 90 | (sb-sys:serve-event timeout) 91 | (loop 92 | while (sb-sys:serve-event timeout) 93 | do (sleep 0.05)))))) 94 | 95 | (defmethod instance-stop ((self phantomjs-instance)) 96 | (when-let (proc (proc-of self)) 97 | (phantomjs-exit (port-of self)))) 98 | 99 | (defmethod instance-kill ((self phantomjs-instance)) 100 | (when-let (proc (proc-of self)) 101 | (external-program:signal-process proc :interrupt))) 102 | 103 | (defmethod instance-alivep ((self phantomjs-instance)) 104 | (when (proc-of self) 105 | t)) 106 | 107 | (defmethod instance-call ((self phantomjs-instance) script &key (errorp t)) 108 | (multiple-value-bind (res validp) (phantomjs-call (port-of self) script) 109 | (if validp 110 | (values res validp) 111 | (if errorp 112 | (error 'instance-call-error :message "call error" :value res) 113 | (values res validp))))) 114 | 115 | (defmethod inject-file ((self phantomjs-instance) file) 116 | (let ((path (namestring file))) 117 | (instance-call self (ps:ps (ps:chain phantom (inject-js (ps:lisp path))))))) 118 | 119 | (defmethod inject-script ((self phantomjs-instance) (script string)) 120 | (temporary-file:with-open-temporary-file (out :template "TEMPORARY-FILES:TEMP-%.js" 121 | :keep nil) 122 | (write-string script out) 123 | (finish-output out) 124 | (inject-file self (pathname out)))) 125 | 126 | (ps:defpsmacro phjs-send (msg) 127 | `(console.log (+ ,*response-prefix* (ps:chain (require "base64") 128 | (encode (ps:chain *json* (stringify ,msg))))))) 129 | 130 | (defmacro wrap-ps (&body body) 131 | `(ps:ps (funcall (lambda () ,@body)))) 132 | 133 | (defmacro with-phantomjs ((var &rest instance-args) &body body) 134 | "Helper for PhantomJS. 135 | `VAR` -- PhantomJS instance variable. 136 | Special forms used in macro are `RECEIVE` and `CALL`. 137 | `CALL` -- (CALL VAR SCRIPT). 138 | `RECEIVE -- (RECEIVE (VAR MSG-VAR &KEY (TIMEOUT 5) (ONCEP NIL)) &BODY BODY) 139 | If not `ONCEP` body is called repeatedly before `TIMEOUT` occur. 140 | (RETURN-FROM-RECEIVE VAL) can be used in this case to exit receive loop." 141 | (with-unique-names (srv) 142 | `(let* ((,srv (make-instance 'phantomjs-instance)) 143 | (,var ,srv)) 144 | (instance-start ,srv ,@instance-args) 145 | (macrolet ((call (srv script) 146 | `(instance-call ,srv ,script)) 147 | (receive ((srv msg-var &rest args) &body body) 148 | (with-unique-names (receive-block) 149 | `(block ,receive-block 150 | (flet ((return-from-receive (val) 151 | (return-from ,receive-block val))) 152 | (instance-receive ,srv 153 | #'(lambda (,msg-var) ,@body) 154 | ,@args)))))) 155 | (unwind-protect 156 | (progn 157 | ,@body) 158 | (instance-stop ,srv)))))) 159 | 160 | (defun phantomjs-server (port engine cmd-args) 161 | "Run a phantomjs process with a webserver on `PORT`. 162 | 163 | The webserver running on `PORT` is used to send commands to the 164 | phantomjs instance using a special protocol. 165 | 166 | Returns `EXTERNAL-PROGRAM:PROCESS`." 167 | 168 | (let* ((start-args (append cmd-args 169 | (list (format nil "--config=~a" 170 | (merge-pathnames *config-file* 171 | *scripts-path*)) 172 | (namestring (merge-pathnames *server-file* 173 | *scripts-path*)) 174 | (format nil "~a" port) 175 | (namestring (cl-fad:pathname-as-file *scripts-path*)))))) 176 | (phantomjs-start engine start-args))) 177 | 178 | (defun phantomjs-read-response (in) 179 | (loop 180 | with prefix-len = (length *response-prefix*) 181 | for str = (read-line in nil nil) 182 | for str-prefix = (when (and str 183 | (> (length str) prefix-len)) 184 | (subseq str 0 prefix-len)) 185 | unless str do 186 | (error 'instance-error :message "Read response" :value :eof) 187 | when *debugp* do 188 | (format *error-output* "~&phantomjs=>~a~%" str) 189 | while (not (and str-prefix 190 | (string-equal *response-prefix* str-prefix))) 191 | finally (return (subseq str prefix-len)))) 192 | 193 | (defun phantomjs-start (engine args) 194 | "Run phantomjs process. Returns `PROCESS`. 195 | SCRIPTS is a list of scripts to be passed to the process." 196 | (let* ((binary (ecase engine 197 | (:phantomjs *phantomjs-bin*) 198 | (:slimerjs *slimerjs-bin*))) 199 | (args (append 200 | (when *cookies-file* 201 | (list (format nil "--cookies-file=~a" *cookies-file*))) 202 | (append *phantomjs-default-args* args))) 203 | (proc (external-program:start binary args 204 | :status-hook #'instance-sentinel 205 | :input t 206 | :output :stream 207 | :error t))) 208 | (loop 209 | for resp = (phantomjs-read-response (external-program:process-output-stream proc)) 210 | while (not (member resp '("started" "failed") :test #'string-equal)) 211 | finally (return (if (string-equal resp "started") 212 | proc 213 | (progn 214 | (external-program:signal-process proc :interrupt) 215 | nil)))))) 216 | 217 | (defun phantomjs-exec (port command &optional arg) 218 | "Call `COMMAND` with `ARG` in `PORT` of the phantomjs instance. 219 | * `COMMAND` - :call | :exit. 220 | Returns FIXME." 221 | (assert (member command '(:call :exit))) 222 | (if (member command '(:call)) 223 | (unless arg 224 | (error "argument required for command ~a" command)) 225 | (setf arg nil)) 226 | (let ((headers (cons `("command" . ,(string-downcase command)) 227 | (when arg 228 | (list `("commandarg" . ,arg)))))) 229 | (multiple-value-bind (body status-code) 230 | (drakma:http-request (puri:parse-uri (format nil "http://localhost:~a" port)) 231 | :additional-headers headers) 232 | (let ((body-str (and body 233 | (if (stringp body) 234 | body 235 | (babel:octets-to-string body :encoding :utf-8))))) 236 | (case status-code 237 | (200 (values body-str t)) 238 | (t (values body-str nil)) 239 | ))))) 240 | 241 | (defun phantomjs-call (port script) 242 | "Execute `SCRIPT` on `PORT` of the phantomjs instance." 243 | (multiple-value-bind (body validp) (phantomjs-exec port 244 | :call 245 | (phantomjs-encode-request script)) 246 | (values (phantomjs-decode-response body) validp))) 247 | 248 | (defun phantomjs-exit (port) 249 | "Exit the phantomjs instance on `PORT`." 250 | (phantomjs-exec port :exit)) 251 | 252 | (defun phantomjs-encode-request (str) 253 | (cl-base64:string-to-base64-string str)) 254 | 255 | (defun phantomjs-decode-response (str) 256 | (yason:parse (babel:octets-to-string (cl-base64:base64-string-to-usb8-array str)))) 257 | -------------------------------------------------------------------------------- /deps/js/casperjs/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global process, console, phantom, slimer, require:true*/ 32 | /*jshint maxstatements:34, maxcomplexity:10*/ 33 | // node check 34 | if ('process' in this && process.title === "node") { 35 | console.error('CasperJS cannot be executed within a nodejs environment'); 36 | process.exit(1); 37 | } 38 | 39 | // phantom check 40 | if (!('phantom' in this)) { 41 | console.error('CasperJS needs to be executed in a PhantomJS environment http://phantomjs.org/'); 42 | } 43 | 44 | // Common polyfills 45 | if (typeof Function.prototype.bind !== "function") { 46 | Function.prototype.bind = function(scope) { 47 | "use strict"; 48 | var _function = this; 49 | return function() { 50 | return _function.apply(scope, arguments); 51 | }; 52 | }; 53 | } 54 | 55 | // Custom base error 56 | var CasperError = function CasperError(msg) { 57 | "use strict"; 58 | Error.call(this); 59 | this.message = msg; 60 | this.name = 'CasperError'; 61 | }; 62 | CasperError.prototype = Object.getPrototypeOf(new Error()); 63 | 64 | // casperjs env initialization 65 | (function(global, phantom){ 66 | "use strict"; 67 | // phantom args 68 | // NOTE: we can't use require('system').args here for some very obscure reason 69 | // do not even attempt at using it as it creates infinite recursion 70 | var phantomArgs = phantom.args; 71 | 72 | if (phantom.casperLoaded) { 73 | return; 74 | } 75 | 76 | function __die(message) { 77 | if (message) { 78 | console.error(message); 79 | } 80 | phantom.exit(1); 81 | } 82 | 83 | function __terminate(message) { 84 | if (message) { 85 | console.log(message); 86 | } 87 | phantom.exit(); 88 | } 89 | 90 | (function(version) { 91 | // required version check 92 | if (version.major !== 1) { 93 | return __die('CasperJS needs PhantomJS v1.x'); 94 | } if (version.minor < 8) { 95 | return __die('CasperJS needs at least PhantomJS v1.8 or later.'); 96 | } 97 | if (version.minor === 8 && version.patch < 1) { 98 | return __die('CasperJS needs at least PhantomJS v1.8.1 or later.'); 99 | } 100 | })(phantom.version); 101 | 102 | // Hooks in default phantomjs error handler 103 | phantom.onError = function onPhantomError(msg, trace) { 104 | phantom.defaultErrorHandler.apply(phantom, arguments); 105 | // print a hint when a possible casperjs command misuse is detected 106 | if (msg.indexOf("ReferenceError: Can't find variable: casper") === 0) { 107 | console.error('Hint: you may want to use the `casperjs test` command.'); 108 | } 109 | // exits on syntax error 110 | if (msg.indexOf('SyntaxError: Parse error') === 0) { 111 | __die(); 112 | } 113 | }; 114 | 115 | // Patching fs 116 | var fs = (function patchFs(fs) { 117 | if (!fs.hasOwnProperty('basename')) { 118 | fs.basename = function basename(path) { 119 | return path.replace(/.*\//, ''); 120 | }; 121 | } 122 | if (!fs.hasOwnProperty('dirname')) { 123 | fs.dirname = function dirname(path) { 124 | if (!path) return undefined; 125 | return path.toString().replace(/\\/g, '/').replace(/\/[^\/]*$/, ''); 126 | }; 127 | } 128 | if (!fs.hasOwnProperty('isWindows')) { 129 | fs.isWindows = function isWindows() { 130 | var testPath = arguments[0] || this.workingDirectory; 131 | return (/^[a-z]{1,2}:/i).test(testPath) || testPath.indexOf("\\\\") === 0; 132 | }; 133 | } 134 | if (fs.hasOwnProperty('joinPath')) { 135 | fs.pathJoin = fs.joinPath; 136 | } else if (!fs.hasOwnProperty('pathJoin')) { 137 | fs.pathJoin = function pathJoin() { 138 | return Array.prototype.join.call(arguments, this.separator); 139 | }; 140 | } 141 | return fs; 142 | })(require('fs')); 143 | 144 | // CasperJS root path 145 | if (!phantom.casperPath) { 146 | try { 147 | phantom.casperPath = phantom.args.map(function _map(arg) { 148 | var match = arg.match(/^--casper-path=(.*)/); 149 | if (match) { 150 | return fs.absolute(match[1]); 151 | } 152 | }).filter(function _filter(path) { 153 | return fs.isDirectory(path); 154 | }).pop(); 155 | } catch (e) { 156 | return __die("Couldn't find nor compute phantom.casperPath, exiting."); 157 | } 158 | } 159 | 160 | /** 161 | * Prints CasperJS help. 162 | */ 163 | function printHelp() { 164 | var engine = phantom.casperEngine === 'slimerjs' ? slimer : phantom; 165 | var version = [engine.version.major, engine.version.minor, engine.version.patch].join('.'); 166 | return __terminate([ 167 | 'CasperJS version ' + phantom.casperVersion.toString() + 168 | ' at ' + phantom.casperPath + ', using ' + phantom.casperEngine + ' version ' + version, 169 | fs.read(fs.pathJoin(phantom.casperPath, 'bin', 'usage.txt')) 170 | ].join('\n')) 171 | } 172 | 173 | /** 174 | * Patched require to allow loading of native casperjs modules. 175 | * Every casperjs module have to first call this function in order to 176 | * load a native casperjs module: 177 | * 178 | * var require = patchRequire(require); 179 | * var utils = require('utils'); 180 | * 181 | * Useless for SlimerJS 182 | */ 183 | function patchRequire(require) { 184 | if (require.patched) { 185 | return require; 186 | } 187 | function casperBuiltinPath(path) { 188 | var absPath = fs.pathJoin(phantom.casperPath, 'modules', path + '.js'); 189 | return fs.isFile(absPath) ? absPath : undefined; 190 | } 191 | function localModulePath(path) { 192 | var baseDir = phantom.casperScriptBaseDir || fs.workingDirectory; 193 | var paths = [ 194 | fs.absolute(fs.pathJoin(baseDir, path)), 195 | fs.absolute(fs.pathJoin(baseDir, path + '.js')) 196 | ]; 197 | return paths.filter(function(path) { 198 | return fs.isFile(path); 199 | }).pop(); 200 | } 201 | var patchedRequire = function patchedRequire(path) { 202 | var moduleFilePath = casperBuiltinPath(path); 203 | if (moduleFilePath) { 204 | return require(moduleFilePath); 205 | } 206 | moduleFilePath = localModulePath(path); 207 | if (moduleFilePath) { 208 | return require(moduleFilePath); 209 | } 210 | try { 211 | return require(path); 212 | } catch (e) { 213 | throw new CasperError("Can't find module " + path); 214 | } 215 | }; 216 | patchedRequire.cache = require.cache; 217 | patchedRequire.extensions = require.extensions; 218 | patchedRequire.stubs = require.stubs; 219 | patchedRequire.patched = true; 220 | return patchedRequire; 221 | } 222 | 223 | /** 224 | * Initializes the CasperJS Command Line Interface. 225 | */ 226 | function initCasperCli(casperArgs) { 227 | var baseTestsPath = fs.pathJoin(phantom.casperPath, 'tests'); 228 | 229 | if (!!casperArgs.options.version) { 230 | return __terminate(phantom.casperVersion.toString()) 231 | } else if (casperArgs.get(0) === "test") { 232 | phantom.casperScript = fs.absolute(fs.pathJoin(baseTestsPath, 'run.js')); 233 | phantom.casperTest = true; 234 | casperArgs.drop("test"); 235 | phantom.casperScriptBaseDir = fs.dirname(casperArgs.get(0)); 236 | } else if (casperArgs.get(0) === "selftest") { 237 | phantom.casperScript = fs.absolute(fs.pathJoin(baseTestsPath, 'run.js')); 238 | phantom.casperSelfTest = phantom.casperTest = true; 239 | casperArgs.options.includes = fs.pathJoin(baseTestsPath, 'selftest.js'); 240 | if (casperArgs.args.length <= 1) { 241 | casperArgs.args.push(fs.pathJoin(baseTestsPath, 'suites')); 242 | } 243 | casperArgs.drop("selftest"); 244 | phantom.casperScriptBaseDir = fs.dirname(casperArgs.get(1) || fs.dirname(phantom.casperScript)); 245 | } else if (casperArgs.args.length === 0 || !!casperArgs.options.help) { 246 | return printHelp(); 247 | } 248 | 249 | if (!phantom.casperScript) { 250 | phantom.casperScript = casperArgs.get(0); 251 | } 252 | 253 | if (!fs.isFile(phantom.casperScript)) { 254 | return __die('Unable to open file: ' + phantom.casperScript); 255 | } 256 | 257 | if (!phantom.casperScriptBaseDir) { 258 | var scriptDir = fs.dirname(phantom.casperScript); 259 | if (scriptDir === phantom.casperScript) { 260 | scriptDir = '.'; 261 | } 262 | phantom.casperScriptBaseDir = fs.absolute(scriptDir); 263 | } 264 | 265 | // filter out the called script name from casper args 266 | casperArgs.drop(phantom.casperScript); 267 | } 268 | 269 | // CasperJS version, extracted from package.json - see http://semver.org/ 270 | phantom.casperVersion = (function getCasperVersion(path) { 271 | var parts, patchPart, pkg, pkgFile; 272 | pkgFile = fs.absolute(fs.pathJoin(path, 'package.json')); 273 | if (!fs.exists(pkgFile)) { 274 | throw new CasperError('Cannot find package.json at ' + pkgFile); 275 | } 276 | try { 277 | pkg = JSON.parse(require('fs').read(pkgFile)); 278 | } catch (e) { 279 | throw new CasperError('Cannot read package file contents: ' + e); 280 | } 281 | parts = pkg.version.trim().split("."); 282 | if (parts.length < 3) { 283 | throw new CasperError("Invalid version number"); 284 | } 285 | patchPart = parts[2].split('-'); 286 | return { 287 | major: ~~parts[0] || 0, 288 | minor: ~~parts[1] || 0, 289 | patch: ~~patchPart[0] || 0, 290 | ident: patchPart[1] || "", 291 | toString: function toString() { 292 | var version = [this.major, this.minor, this.patch].join('.'); 293 | if (this.ident) { 294 | version = [version, this.ident].join('-'); 295 | } 296 | return version; 297 | } 298 | }; 299 | })(phantom.casperPath); 300 | 301 | if ("slimer" in global) { 302 | // for SlimerJS, use the standard API to declare directories 303 | // where to search modules 304 | require.paths.push(fs.pathJoin(phantom.casperPath, 'modules')); 305 | require.paths.push(fs.workingDirectory); 306 | 307 | // declare a dummy patchRequire function 308 | require.globals.patchRequire = global.patchRequire = function(req) { return req;}; 309 | require.globals.CasperError = CasperError; 310 | phantom.casperEngine = "slimerjs"; 311 | } 312 | else { 313 | // patch require 314 | global.__require = require; 315 | global.patchRequire = patchRequire; // must be called in every casperjs module as of 1.1 316 | global.require = patchRequire(global.require); 317 | phantom.casperEngine = "phantomjs"; 318 | } 319 | 320 | // casper cli args 321 | phantom.casperArgs = require('cli').parse(phantomArgs); 322 | 323 | if (true === phantom.casperArgs.get('cli')) { 324 | initCasperCli(phantom.casperArgs); 325 | } 326 | 327 | if ("slimer" in global && phantom.casperScriptBaseDir) { 328 | // initCasperCli has set casperScriptBaseDir 329 | // use it instead of fs.workingDirectory 330 | require.paths.pop(); 331 | require.paths.push(phantom.casperScriptBaseDir); 332 | } 333 | 334 | // casper loading status flag 335 | phantom.casperLoaded = true; 336 | 337 | // passed casperjs script execution 338 | if (phantom.casperScript && !phantom.injectJs(phantom.casperScript)) { 339 | return __die('Unable to load script ' + phantom.casperScript + '; check file syntax'); 340 | } 341 | })(this, phantom); 342 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, console, exports, phantom, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | 35 | /** 36 | * Provides a better typeof operator equivalent, able to retrieve the array 37 | * type. 38 | * 39 | * CAVEAT: this function does not necessarilly map to classical js "type" names, 40 | * notably a `null` will map to "null" instead of "object". 41 | * 42 | * @param mixed input 43 | * @return String 44 | * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ 45 | */ 46 | function betterTypeOf(input) { 47 | "use strict"; 48 | switch (input) { 49 | case undefined: 50 | return 'undefined'; 51 | case null: 52 | return 'null'; 53 | default: 54 | try { 55 | var type = Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); 56 | if (type === 'object' && 57 | phantom.casperEngine !== "phantomjs" && 58 | '__type' in input) { 59 | type = input.__type; 60 | } 61 | // gecko returns window instead of domwindow 62 | else if (type === 'window') { 63 | return 'domwindow'; 64 | } 65 | return type; 66 | } catch (e) { 67 | return typeof input; 68 | } 69 | } 70 | } 71 | exports.betterTypeOf = betterTypeOf; 72 | 73 | /** 74 | * Cleans a passed URL. 75 | * 76 | * @param String url An HTTP URL 77 | * @return String 78 | */ 79 | function cleanUrl(url) { 80 | "use strict"; 81 | if (url.toLowerCase().indexOf('http') !== 0) { 82 | return url; 83 | } 84 | var a = document.createElement('a'); 85 | a.href = url; 86 | return a.href; 87 | } 88 | exports.cleanUrl = cleanUrl; 89 | 90 | /** 91 | * Clones an object. 92 | * 93 | * @param Mixed o 94 | * @return Mixed 95 | */ 96 | function clone(o) { 97 | "use strict"; 98 | return JSON.parse(JSON.stringify(o)); 99 | } 100 | exports.clone = clone; 101 | 102 | /** 103 | * Computes a modifier string to its PhantomJS equivalent. A modifier string is 104 | * in the form "ctrl+alt+shift". 105 | * 106 | * @param String modifierString Modifier string, eg. "ctrl+alt+shift" 107 | * @param Object modifiers Modifiers definitions 108 | * @return Number 109 | */ 110 | function computeModifier(modifierString, modifiers) { 111 | "use strict"; 112 | var modifier = 0, 113 | checkKey = function(key) { 114 | if (key in modifiers) return; 115 | throw new CasperError(format('%s is not a supported key modifier', key)); 116 | }; 117 | if (!modifierString) return modifier; 118 | var keys = modifierString.split('+'); 119 | keys.forEach(checkKey); 120 | return keys.reduce(function(acc, key) { 121 | return acc | modifiers[key]; 122 | }, modifier); 123 | } 124 | exports.computeModifier = computeModifier; 125 | 126 | /** 127 | * Dumps a JSON representation of passed value to the console. Used for 128 | * debugging purpose only. 129 | * 130 | * @param Mixed value 131 | */ 132 | function dump(value) { 133 | "use strict"; 134 | console.log(serialize(value, 4)); 135 | } 136 | exports.dump = dump; 137 | 138 | /** 139 | * Tests equality between the two passed arguments. 140 | * 141 | * @param Mixed v1 142 | * @param Mixed v2 143 | * @param Boolean 144 | */ 145 | function equals(v1, v2) { 146 | "use strict"; 147 | if (isFunction(v1)) { 148 | return v1.toString() === v2.toString(); 149 | } 150 | // with Gecko, instanceof is not enough to test object 151 | if (v1 instanceof Object || isObject(v1)) { 152 | if (!(v2 instanceof Object || isObject(v2)) || 153 | Object.keys(v1).length !== Object.keys(v2).length) { 154 | return false; 155 | } 156 | for (var k in v1) { 157 | if (!equals(v1[k], v2[k])) { 158 | return false; 159 | } 160 | } 161 | return true; 162 | } 163 | return v1 === v2; 164 | } 165 | exports.equals = equals; 166 | 167 | /** 168 | * Returns the file extension in lower case. 169 | * 170 | * @param String file File path 171 | * @return string 172 | */ 173 | function fileExt(file) { 174 | "use strict"; 175 | try { 176 | return file.split('.').pop().toLowerCase().trim(); 177 | } catch(e) { 178 | return ''; 179 | } 180 | } 181 | exports.fileExt = fileExt; 182 | 183 | /** 184 | * Takes a string and append blanks until the pad value is reached. 185 | * 186 | * @param String text 187 | * @param Number pad Pad value (optional; default: 80) 188 | * @return String 189 | */ 190 | function fillBlanks(text, pad) { 191 | "use strict"; 192 | pad = pad || 80; 193 | if (text.length < pad) { 194 | text += new Array(pad - text.length + 1).join(' '); 195 | } 196 | return text; 197 | } 198 | exports.fillBlanks = fillBlanks; 199 | 200 | /** 201 | * Formats a string with passed parameters. Ported from nodejs `util.format()`. 202 | * 203 | * @return String 204 | */ 205 | function format(f) { 206 | "use strict"; 207 | var i = 1; 208 | var args = arguments; 209 | var len = args.length; 210 | var str = String(f).replace(/%[sdj%]/g, function _replace(x) { 211 | if (i >= len) return x; 212 | switch (x) { 213 | case '%s': 214 | return String(args[i++]); 215 | case '%d': 216 | return Number(args[i++]); 217 | case '%j': 218 | return JSON.stringify(args[i++]); 219 | case '%%': 220 | return '%'; 221 | default: 222 | return x; 223 | } 224 | }); 225 | for (var x = args[i]; i < len; x = args[++i]) { 226 | if (x === null || typeof x !== 'object') { 227 | str += ' ' + x; 228 | } else { 229 | str += '[obj]'; 230 | } 231 | } 232 | return str; 233 | } 234 | exports.format = format; 235 | 236 | /** 237 | * Formats a test value. 238 | * 239 | * @param Mixed value 240 | * @return String 241 | */ 242 | function formatTestValue(value, name) { 243 | "use strict"; 244 | var formatted = ''; 245 | if (value instanceof Error) { 246 | formatted += value.message + '\n'; 247 | if (value.stack) { 248 | formatted += indent(value.stack, 12, '#'); 249 | } 250 | } else if (name === 'stack') { 251 | if (isArray(value)) { 252 | formatted += value.map(function(entry) { 253 | return format('in %s() in %s:%d', (entry['function'] || "anonymous"), entry.file, entry.line); 254 | }).join('\n'); 255 | } else { 256 | formatted += 'not provided'; 257 | } 258 | } else { 259 | try { 260 | formatted += serialize(value); 261 | } catch (e) { 262 | try { 263 | formatted += serialize(value.toString()); 264 | } catch (e2) { 265 | formatted += '(unserializable value)'; 266 | } 267 | } 268 | } 269 | return formatted; 270 | } 271 | exports.formatTestValue = formatTestValue; 272 | 273 | /** 274 | * Retrieves the value of an Object foreign property using a dot-separated 275 | * path string. 276 | * 277 | * Beware, this function doesn't handle object key names containing a dot. 278 | * 279 | * @param Object obj The source object 280 | * @param String path Dot separated path, eg. "x.y.z" 281 | */ 282 | function getPropertyPath(obj, path) { 283 | "use strict"; 284 | if (!isObject(obj) || !isString(path)) { 285 | return undefined; 286 | } 287 | var value = obj; 288 | path.split('.').forEach(function(property) { 289 | if (typeof value === "object" && property in value) { 290 | value = value[property]; 291 | } else { 292 | value = undefined; 293 | } 294 | }); 295 | return value; 296 | } 297 | exports.getPropertyPath = getPropertyPath; 298 | 299 | /** 300 | * Indents a string. 301 | * 302 | * @param String string 303 | * @param Number nchars 304 | * @param String prefix 305 | * @return String 306 | */ 307 | function indent(string, nchars, prefix) { 308 | "use strict"; 309 | return string.split('\n').map(function(line) { 310 | return (prefix || '') + new Array(nchars).join(' ') + line; 311 | }).join('\n'); 312 | } 313 | exports.indent = indent; 314 | 315 | /** 316 | * Inherit the prototype methods from one constructor into another. 317 | * 318 | * @param {function} ctor Constructor function which needs to inherit the 319 | * prototype. 320 | * @param {function} superCtor Constructor function to inherit prototype from. 321 | */ 322 | function inherits(ctor, superCtor) { 323 | "use strict"; 324 | ctor.super_ = ctor.__super__ = superCtor; 325 | ctor.prototype = Object.create(superCtor.prototype, { 326 | constructor: { 327 | value: ctor, 328 | enumerable: false, 329 | writable: true, 330 | configurable: true 331 | } 332 | }); 333 | } 334 | exports.inherits = inherits; 335 | 336 | /** 337 | * Checks if value is a javascript Array 338 | * 339 | * @param mixed value 340 | * @return Boolean 341 | */ 342 | function isArray(value) { 343 | "use strict"; 344 | return Array.isArray(value) || isType(value, "array"); 345 | } 346 | exports.isArray = isArray; 347 | 348 | /** 349 | * Checks if passed argument is an instance of Capser object. 350 | * 351 | * @param mixed value 352 | * @return Boolean 353 | */ 354 | function isCasperObject(value) { 355 | "use strict"; 356 | return value instanceof require('casper').Casper; 357 | } 358 | exports.isCasperObject = isCasperObject; 359 | 360 | /** 361 | * Checks if value is a phantomjs clipRect-compatible object 362 | * 363 | * @param mixed value 364 | * @return Boolean 365 | */ 366 | function isClipRect(value) { 367 | "use strict"; 368 | return isType(value, "cliprect") || ( 369 | isObject(value) && 370 | isNumber(value.top) && isNumber(value.left) && 371 | isNumber(value.width) && isNumber(value.height) 372 | ); 373 | } 374 | exports.isClipRect = isClipRect; 375 | 376 | /** 377 | * Checks that the subject is falsy. 378 | * 379 | * @param Mixed subject Test subject 380 | * @return Boolean 381 | */ 382 | function isFalsy(subject) { 383 | "use strict"; 384 | /*jshint eqeqeq:false*/ 385 | return !subject; 386 | } 387 | exports.isFalsy = isFalsy; 388 | /** 389 | * Checks if value is a javascript Function 390 | * 391 | * @param mixed value 392 | * @return Boolean 393 | */ 394 | function isFunction(value) { 395 | "use strict"; 396 | return isType(value, "function"); 397 | } 398 | exports.isFunction = isFunction; 399 | 400 | /** 401 | * Checks if passed resource involves an HTTP url. 402 | * 403 | * @param Object resource The PhantomJS HTTP resource object 404 | * @return Boolean 405 | */ 406 | function isHTTPResource(resource) { 407 | "use strict"; 408 | return isObject(resource) && /^http/i.test(resource.url); 409 | } 410 | exports.isHTTPResource = isHTTPResource; 411 | 412 | /** 413 | * Checks if a file is apparently javascript compatible (.js or .coffee). 414 | * 415 | * @param String file Path to the file to test 416 | * @return Boolean 417 | */ 418 | function isJsFile(file) { 419 | "use strict"; 420 | var ext = fileExt(file); 421 | return isString(ext, "string") && ['js', 'coffee'].indexOf(ext) !== -1; 422 | } 423 | exports.isJsFile = isJsFile; 424 | 425 | /** 426 | * Checks if the provided value is null 427 | * 428 | * @return Boolean 429 | */ 430 | function isNull(value) { 431 | "use strict"; 432 | return isType(value, "null"); 433 | } 434 | exports.isNull = isNull; 435 | 436 | /** 437 | * Checks if value is a javascript Number 438 | * 439 | * @param mixed value 440 | * @return Boolean 441 | */ 442 | function isNumber(value) { 443 | "use strict"; 444 | return isType(value, "number"); 445 | } 446 | exports.isNumber = isNumber; 447 | 448 | /** 449 | * Checks if value is a javascript Object 450 | * 451 | * @param mixed value 452 | * @return Boolean 453 | */ 454 | function isObject(value) { 455 | "use strict"; 456 | var objectTypes = ["array", "object", "qtruntimeobject"]; 457 | return objectTypes.indexOf(betterTypeOf(value)) >= 0; 458 | } 459 | exports.isObject = isObject; 460 | 461 | /** 462 | * Checks if value is a RegExp 463 | * 464 | * @param mixed value 465 | * @return Boolean 466 | */ 467 | function isRegExp(value) { 468 | "use strict"; 469 | return isType(value, "regexp"); 470 | } 471 | exports.isRegExp = isRegExp; 472 | 473 | /** 474 | * Checks if value is a javascript String 475 | * 476 | * @param mixed value 477 | * @return Boolean 478 | */ 479 | function isString(value) { 480 | "use strict"; 481 | return isType(value, "string"); 482 | } 483 | exports.isString = isString; 484 | 485 | /** 486 | * Checks that the subject is truthy. 487 | * 488 | * @param Mixed subject Test subject 489 | * @return Boolean 490 | */ 491 | function isTruthy(subject) { 492 | "use strict"; 493 | /*jshint eqeqeq:false*/ 494 | return !!subject; 495 | } 496 | exports.isTruthy = isTruthy; 497 | 498 | /** 499 | * Shorthands for checking if a value is of the given type. Can check for 500 | * arrays. 501 | * 502 | * @param mixed what The value to check 503 | * @param String typeName The type name ("string", "number", "function", etc.) 504 | * @return Boolean 505 | */ 506 | function isType(what, typeName) { 507 | "use strict"; 508 | if (typeof typeName !== "string" || !typeName) { 509 | throw new CasperError("You must pass isType() a typeName string"); 510 | } 511 | return betterTypeOf(what).toLowerCase() === typeName.toLowerCase(); 512 | } 513 | exports.isType = isType; 514 | 515 | /** 516 | * Checks if the provided value is undefined 517 | * 518 | * @return Boolean 519 | */ 520 | function isUndefined(value) { 521 | "use strict"; 522 | return isType(value, "undefined"); 523 | } 524 | exports.isUndefined = isUndefined; 525 | 526 | /** 527 | * Checks if value is a valid selector Object. 528 | * 529 | * @param mixed value 530 | * @return Boolean 531 | */ 532 | function isValidSelector(value) { 533 | "use strict"; 534 | if (isString(value)) { 535 | try { 536 | // phantomjs env has a working document object, let's use it 537 | document.querySelector(value); 538 | } catch(e) { 539 | if ('name' in e && (e.name === 'SYNTAX_ERR' || e.name === 'SyntaxError')) { 540 | return false; 541 | } 542 | } 543 | return true; 544 | } else if (isObject(value)) { 545 | if (!value.hasOwnProperty('type')) { 546 | return false; 547 | } 548 | if (!value.hasOwnProperty('path')) { 549 | return false; 550 | } 551 | if (['css', 'xpath'].indexOf(value.type) === -1) { 552 | return false; 553 | } 554 | return true; 555 | } 556 | return false; 557 | } 558 | exports.isValidSelector = isValidSelector; 559 | 560 | /** 561 | * Checks if the provided var is a WebPage instance 562 | * 563 | * @param mixed what 564 | * @return Boolean 565 | */ 566 | function isWebPage(what) { 567 | "use strict"; 568 | return betterTypeOf(what) === "qtruntimeobject" && what.objectName === 'WebPage'; 569 | } 570 | exports.isWebPage = isWebPage; 571 | 572 | 573 | 574 | function isPlainObject(obj) { 575 | "use strict"; 576 | if (!obj || typeof(obj) !== 'object') 577 | return false; 578 | var type = Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); 579 | return (type === 'object'); 580 | } 581 | 582 | function mergeObjectsInSlimerjs(origin, add) { 583 | "use strict"; 584 | for (var p in add) { 585 | if (isPlainObject(add[p])) { 586 | if (isPlainObject(origin[p])) { 587 | origin[p] = mergeObjects(origin[p], add[p]); 588 | } else { 589 | origin[p] = clone(add[p]); 590 | } 591 | } else { 592 | origin[p] = add[p]; 593 | } 594 | } 595 | return origin; 596 | } 597 | 598 | /** 599 | * Object recursive merging utility. 600 | * 601 | * @param Object origin the origin object 602 | * @param Object add the object to merge data into origin 603 | * @return Object 604 | */ 605 | function mergeObjects(origin, add) { 606 | "use strict"; 607 | 608 | if (phantom.casperEngine === 'slimerjs') { 609 | // Because of an issue in the module system of slimerjs (security membranes?) 610 | // constructor is undefined. 611 | // let's use an other algorithm 612 | return mergeObjectsInSlimerjs(origin, add); 613 | } 614 | for (var p in add) { 615 | if (add[p] && add[p].constructor === Object) { 616 | if (origin[p] && origin[p].constructor === Object) { 617 | origin[p] = mergeObjects(origin[p], add[p]); 618 | } else { 619 | origin[p] = clone(add[p]); 620 | } 621 | } else { 622 | origin[p] = add[p]; 623 | } 624 | } 625 | return origin; 626 | } 627 | exports.mergeObjects = mergeObjects; 628 | 629 | /** 630 | * Converts milliseconds to seconds and rounds the results to 3 digits accuracy. 631 | * 632 | * @param Number milliseconds 633 | * @return Number seconds 634 | */ 635 | function ms2seconds(milliseconds) { 636 | "use strict"; 637 | return Math.round(milliseconds / 1000 * 1000) / 1000; 638 | } 639 | exports.ms2seconds = ms2seconds; 640 | 641 | /** 642 | * Creates an (SG|X)ML node element. 643 | * 644 | * @param String name The node name 645 | * @param Object attributes Optional attributes 646 | * @return HTMLElement 647 | */ 648 | function node(name, attributes) { 649 | "use strict"; 650 | var _node = document.createElement(name); 651 | for (var attrName in attributes) { 652 | var value = attributes[attrName]; 653 | if (attributes.hasOwnProperty(attrName) && isString(attrName)) { 654 | _node.setAttribute(attrName, value); 655 | } 656 | } 657 | return _node; 658 | } 659 | exports.node = node; 660 | 661 | /** 662 | * Maps an object to an array made from its values. 663 | * 664 | * @param Object obj 665 | * @return Array 666 | */ 667 | function objectValues(obj) { 668 | "use strict"; 669 | return Object.keys(obj).map(function(arg) { 670 | return obj[arg]; 671 | }); 672 | } 673 | exports.objectValues = objectValues; 674 | 675 | /** 676 | * Prepares a string for xpath expression with the condition [text()=]. 677 | * 678 | * @param String string 679 | * @return String 680 | */ 681 | function quoteXPathAttributeString(string) { 682 | "use strict"; 683 | if (/"/g.test(string)) { 684 | return 'concat("' + string.toString().replace(/"/g, '", \'"\', "') + '")'; 685 | } else { 686 | return '"' + string + '"'; 687 | } 688 | } 689 | exports.quoteXPathAttributeString = quoteXPathAttributeString; 690 | 691 | /** 692 | * Serializes a value using JSON. 693 | * 694 | * @param Mixed value 695 | * @return String 696 | */ 697 | function serialize(value, indent) { 698 | "use strict"; 699 | if (isArray(value)) { 700 | value = value.map(function _map(prop) { 701 | return isFunction(prop) ? prop.toString().replace(/\s{2,}/, '') : prop; 702 | }); 703 | } 704 | return JSON.stringify(value, null, indent); 705 | } 706 | exports.serialize = serialize; 707 | 708 | /** 709 | * Returns unique values from an array. 710 | * 711 | * Note: ugly code is ugly, but efficient: http://jsperf.com/array-unique2/8 712 | * 713 | * @param Array array 714 | * @return Array 715 | */ 716 | function unique(array) { 717 | "use strict"; 718 | var o = {}, 719 | r = []; 720 | for (var i = 0, len = array.length; i !== len; i++) { 721 | var d = array[i]; 722 | if (o[d] !== 1) { 723 | o[d] = 1; 724 | r[r.length] = d; 725 | } 726 | } 727 | return r; 728 | } 729 | exports.unique = unique; 730 | 731 | /** 732 | * Compare two version numbers represented as strings. 733 | * 734 | * @param String a Version a 735 | * @param String b Version b 736 | * @return Number 737 | */ 738 | function cmpVersion(a, b) { 739 | "use strict"; 740 | var i, cmp, len, re = /(\.0)+[^\.]*$/; 741 | function versionToString(version) { 742 | if (isObject(version)) { 743 | try { 744 | return [version.major, version.minor, version.patch].join('.'); 745 | } catch (e) {} 746 | } 747 | return version; 748 | } 749 | a = versionToString(a); 750 | b = versionToString(b); 751 | a = (a + '').replace(re, '').split('.'); 752 | b = (b + '').replace(re, '').split('.'); 753 | len = Math.min(a.length, b.length); 754 | for (i = 0; i < len; i++) { 755 | cmp = parseInt(a[i], 10) - parseInt(b[i], 10); 756 | if (cmp !== 0) { 757 | return cmp; 758 | } 759 | } 760 | return a.length - b.length; 761 | } 762 | exports.cmpVersion = cmpVersion; 763 | 764 | /** 765 | * Checks if a version number string is greater or equals another. 766 | * 767 | * @param String a Version a 768 | * @param String b Version b 769 | * @return Boolean 770 | */ 771 | function gteVersion(a, b) { 772 | "use strict"; 773 | return cmpVersion(a, b) >= 0; 774 | } 775 | exports.gteVersion = gteVersion; 776 | 777 | /** 778 | * Checks if a version number string is less than another. 779 | * 780 | * @param String a Version a 781 | * @param String b Version b 782 | * @return Boolean 783 | */ 784 | function ltVersion(a, b) { 785 | "use strict"; 786 | return cmpVersion(a, b) < 0; 787 | } 788 | exports.ltVersion = ltVersion; 789 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/clientutils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global console, escape, exports, NodeList, window*/ 32 | 33 | (function(exports) { 34 | "use strict"; 35 | 36 | exports.create = function create(options) { 37 | return new this.ClientUtils(options); 38 | }; 39 | 40 | /** 41 | * Casper client-side helpers. 42 | */ 43 | exports.ClientUtils = function ClientUtils(options) { 44 | /*jshint maxstatements:40*/ 45 | // private members 46 | var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 47 | var BASE64_DECODE_CHARS = new Array( 48 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 49 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 50 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 51 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, 52 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 53 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 54 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 55 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 56 | ); 57 | var SUPPORTED_SELECTOR_TYPES = ['css', 'xpath']; 58 | 59 | // public members 60 | this.options = options || {}; 61 | this.options.scope = this.options.scope || document; 62 | /** 63 | * Clicks on the DOM element behind the provided selector. 64 | * 65 | * @param String selector A CSS3 selector to the element to click 66 | * @return Boolean 67 | */ 68 | this.click = function click(selector) { 69 | return this.mouseEvent('click', selector); 70 | }; 71 | 72 | /** 73 | * Decodes a base64 encoded string. Succeeds where window.atob() fails. 74 | * 75 | * @param String str The base64 encoded contents 76 | * @return string 77 | */ 78 | this.decode = function decode(str) { 79 | /*jshint maxstatements:30, maxcomplexity:30 */ 80 | var c1, c2, c3, c4, i = 0, len = str.length, out = ""; 81 | while (i < len) { 82 | do { 83 | c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; 84 | } while (i < len && c1 === -1); 85 | if (c1 === -1) { 86 | break; 87 | } 88 | do { 89 | c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; 90 | } while (i < len && c2 === -1); 91 | if (c2 === -1) { 92 | break; 93 | } 94 | out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4)); 95 | do { 96 | c3 = str.charCodeAt(i++) & 0xff; 97 | if (c3 === 61) 98 | return out; 99 | c3 = BASE64_DECODE_CHARS[c3]; 100 | } while (i < len && c3 === -1); 101 | if (c3 === -1) { 102 | break; 103 | } 104 | out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2)); 105 | do { 106 | c4 = str.charCodeAt(i++) & 0xff; 107 | if (c4 === 61) { 108 | return out; 109 | } 110 | c4 = BASE64_DECODE_CHARS[c4]; 111 | } while (i < len && c4 === -1); 112 | if (c4 === -1) { 113 | break; 114 | } 115 | out += String.fromCharCode(((c3 & 0x03) << 6) | c4); 116 | } 117 | return out; 118 | }; 119 | 120 | /** 121 | * Echoes something to casper console. 122 | * 123 | * @param String message 124 | * @return 125 | */ 126 | this.echo = function echo(message) { 127 | console.log("[casper.echo] " + message); 128 | }; 129 | 130 | /** 131 | * Checks if a given DOM element is visible in remove page. 132 | * 133 | * @param Object element DOM element 134 | * @return Boolean 135 | */ 136 | this.elementVisible = function elementVisible(elem) { 137 | try { 138 | var comp = window.getComputedStyle(elem, null); 139 | return comp.visibility !== 'hidden' && 140 | comp.display !== 'none' && 141 | elem.offsetHeight > 0 && 142 | elem.offsetWidth > 0; 143 | } catch (e) { 144 | return false; 145 | } 146 | } 147 | 148 | /** 149 | * Base64 encodes a string, even binary ones. Succeeds where 150 | * window.btoa() fails. 151 | * 152 | * @param String str The string content to encode 153 | * @return string 154 | */ 155 | this.encode = function encode(str) { 156 | /*jshint maxstatements:30 */ 157 | var out = "", i = 0, len = str.length, c1, c2, c3; 158 | while (i < len) { 159 | c1 = str.charCodeAt(i++) & 0xff; 160 | if (i === len) { 161 | out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); 162 | out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4); 163 | out += "=="; 164 | break; 165 | } 166 | c2 = str.charCodeAt(i++); 167 | if (i === len) { 168 | out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); 169 | out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4)); 170 | out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2); 171 | out += "="; 172 | break; 173 | } 174 | c3 = str.charCodeAt(i++); 175 | out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); 176 | out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); 177 | out += BASE64_ENCODE_CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); 178 | out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F); 179 | } 180 | return out; 181 | }; 182 | 183 | /** 184 | * Checks if a given DOM element exists in remote page. 185 | * 186 | * @param String selector CSS3 selector 187 | * @return Boolean 188 | */ 189 | this.exists = function exists(selector) { 190 | try { 191 | return this.findAll(selector).length > 0; 192 | } catch (e) { 193 | return false; 194 | } 195 | }; 196 | 197 | /** 198 | * Fetches innerText within the element(s) matching a given CSS3 199 | * selector. 200 | * 201 | * @param String selector A CSS3 selector 202 | * @return String 203 | */ 204 | this.fetchText = function fetchText(selector) { 205 | var text = '', elements = this.findAll(selector); 206 | if (elements && elements.length) { 207 | Array.prototype.forEach.call(elements, function _forEach(element) { 208 | text += element.textContent || element.innerText; 209 | }); 210 | } 211 | return text; 212 | }; 213 | 214 | /** 215 | * Fills a form with provided field values, and optionally submits it. 216 | * 217 | * @param HTMLElement|String form A form element, or a CSS3 selector to a form element 218 | * @param Object vals Field values 219 | * @param Function findType Element finder type (css, names, xpath) 220 | * @return Object An object containing setting result for each field, including file uploads 221 | */ 222 | this.fill = function fill(form, vals, findType) { 223 | /*jshint maxcomplexity:8*/ 224 | var out = { 225 | errors: [], 226 | fields: [], 227 | files: [] 228 | }; 229 | 230 | if (!(form instanceof HTMLElement) || typeof form === "string") { 231 | this.log("attempting to fetch form element from selector: '" + form + "'", "info"); 232 | try { 233 | form = this.findOne(form); 234 | } catch (e) { 235 | if (e.name === "SYNTAX_ERR") { 236 | out.errors.push("invalid form selector provided: '" + form + "'"); 237 | return out; 238 | } 239 | } 240 | } 241 | 242 | if (!form) { 243 | out.errors.push("form not found"); 244 | return out; 245 | } 246 | 247 | var finders = { 248 | css: function(inputSelector, formSelector) { 249 | return this.findAll(inputSelector, form); 250 | }, 251 | names: function(elementName, formSelector) { 252 | return this.findAll('[name="' + elementName + '"]', form); 253 | }, 254 | xpath: function(xpath, formSelector) { 255 | return this.findAll({type: "xpath", path: xpath}, form); 256 | } 257 | }; 258 | 259 | for (var fieldSelector in vals) { 260 | if (!vals.hasOwnProperty(fieldSelector)) { 261 | continue; 262 | } 263 | var field = finders[findType || "names"].call(this, fieldSelector, form), 264 | value = vals[fieldSelector]; 265 | if (!field || field.length === 0) { 266 | out.errors.push('no field matching ' + findType + ' selector "' + fieldSelector + '" in form'); 267 | continue; 268 | } 269 | try { 270 | out.fields[fieldSelector] = this.setField(field, value); 271 | } catch (err) { 272 | if (err.name === "FileUploadError") { 273 | out.files.push({ 274 | type: findType, 275 | selector: fieldSelector, 276 | path: err.path 277 | }); 278 | } else if (err.name === "FieldNotFound") { 279 | out.errors.push('Unable to find field element in form: ' + err.toString()); 280 | } else { 281 | out.errors.push(err.toString()); 282 | } 283 | } 284 | } 285 | return out; 286 | }; 287 | 288 | /** 289 | * Finds all DOM elements matching by the provided selector. 290 | * 291 | * @param String selector CSS3 selector 292 | * @param HTMLElement|null scope Element to search child elements within 293 | * @return NodeList|undefined 294 | */ 295 | this.findAll = function findAll(selector, scope) { 296 | scope = scope || this.options.scope; 297 | try { 298 | var pSelector = this.processSelector(selector); 299 | if (pSelector.type === 'xpath') { 300 | return this.getElementsByXPath(pSelector.path, scope); 301 | } else { 302 | return scope.querySelectorAll(pSelector.path); 303 | } 304 | } catch (e) { 305 | this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error"); 306 | } 307 | }; 308 | 309 | /** 310 | * Finds a DOM element by the provided selector. 311 | * 312 | * @param String selector CSS3 selector 313 | * @param HTMLElement|null scope Element to search child elements within 314 | * @return HTMLElement|undefined 315 | */ 316 | this.findOne = function findOne(selector, scope) { 317 | scope = scope || this.options.scope; 318 | try { 319 | var pSelector = this.processSelector(selector); 320 | if (pSelector.type === 'xpath') { 321 | return this.getElementByXPath(pSelector.path, scope); 322 | } else { 323 | return scope.querySelector(pSelector.path); 324 | } 325 | } catch (e) { 326 | this.log('findOne(): invalid selector provided "' + selector + '":' + e, "error"); 327 | } 328 | }; 329 | 330 | /** 331 | * Downloads a resource behind an url and returns its base64-encoded 332 | * contents. 333 | * 334 | * @param String url The resource url 335 | * @param String method The request method, optional (default: GET) 336 | * @param Object data The request data, optional 337 | * @return String Base64 contents string 338 | */ 339 | this.getBase64 = function getBase64(url, method, data) { 340 | return this.encode(this.getBinary(url, method, data)); 341 | }; 342 | 343 | /** 344 | * Retrieves string contents from a binary file behind an url. Silently 345 | * fails but log errors. 346 | * 347 | * @param String url Url. 348 | * @param String method HTTP method. 349 | * @param Object data Request parameters. 350 | * @return String 351 | */ 352 | this.getBinary = function getBinary(url, method, data) { 353 | try { 354 | return this.sendAJAX(url, method, data, false); 355 | } catch (e) { 356 | if (e.name === "NETWORK_ERR" && e.code === 101) { 357 | this.log("getBinary(): Unfortunately, casperjs cannot make cross domain ajax requests", "warning"); 358 | } 359 | this.log("getBinary(): Error while fetching " + url + ": " + e, "error"); 360 | return ""; 361 | } 362 | }; 363 | 364 | /** 365 | * Retrieves total document height. 366 | * http://james.padolsey.com/javascript/get-document-height-cross-browser/ 367 | * 368 | * @return {Number} 369 | */ 370 | this.getDocumentHeight = function getDocumentHeight() { 371 | return Math.max( 372 | Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), 373 | Math.max(document.body.offsetHeight, document.documentElement.offsetHeight), 374 | Math.max(document.body.clientHeight, document.documentElement.clientHeight) 375 | ); 376 | }; 377 | 378 | /** 379 | * Retrieves bounding rect coordinates of the HTML element matching the 380 | * provided CSS3 selector in the following form: 381 | * 382 | * {top: y, left: x, width: w, height:, h} 383 | * 384 | * @param String selector 385 | * @return Object or null 386 | */ 387 | this.getElementBounds = function getElementBounds(selector) { 388 | try { 389 | var clipRect = this.findOne(selector).getBoundingClientRect(); 390 | return { 391 | top: clipRect.top, 392 | left: clipRect.left, 393 | width: clipRect.width, 394 | height: clipRect.height 395 | }; 396 | } catch (e) { 397 | this.log("Unable to fetch bounds for element " + selector, "warning"); 398 | } 399 | }; 400 | 401 | /** 402 | * Retrieves the list of bounding rect coordinates for all the HTML elements matching the 403 | * provided CSS3 selector, in the following form: 404 | * 405 | * [{top: y, left: x, width: w, height:, h}, 406 | * {top: y, left: x, width: w, height:, h}, 407 | * ...] 408 | * 409 | * @param String selector 410 | * @return Array 411 | */ 412 | this.getElementsBounds = function getElementsBounds(selector) { 413 | var elements = this.findAll(selector); 414 | var self = this; 415 | try { 416 | return Array.prototype.map.call(elements, function(element) { 417 | var clipRect = element.getBoundingClientRect(); 418 | return { 419 | top: clipRect.top, 420 | left: clipRect.left, 421 | width: clipRect.width, 422 | height: clipRect.height 423 | }; 424 | }); 425 | } catch (e) { 426 | this.log("Unable to fetch bounds for elements matching " + selector, "warning"); 427 | } 428 | }; 429 | 430 | /** 431 | * Retrieves information about the node matching the provided selector. 432 | * 433 | * @param String|Object selector CSS3/XPath selector 434 | * @return Object 435 | */ 436 | this.getElementInfo = function getElementInfo(selector) { 437 | var element = this.findOne(selector); 438 | var bounds = this.getElementBounds(selector); 439 | var attributes = {}; 440 | [].forEach.call(element.attributes, function(attr) { 441 | attributes[attr.name.toLowerCase()] = attr.value; 442 | }); 443 | return { 444 | nodeName: element.nodeName.toLowerCase(), 445 | attributes: attributes, 446 | tag: element.outerHTML, 447 | html: element.innerHTML, 448 | text: element.textContent || element.innerText, 449 | x: bounds.left, 450 | y: bounds.top, 451 | width: bounds.width, 452 | height: bounds.height, 453 | visible: this.visible(selector) 454 | }; 455 | }; 456 | 457 | /** 458 | * Retrieves information about the nodes matching the provided selector. 459 | * 460 | * @param String|Object selector CSS3/XPath selector 461 | * @return Array 462 | */ 463 | this.getElementsInfo = function getElementsInfo(selector) { 464 | var bounds = this.getElementsBounds(selector); 465 | var eleVisible = this.elementVisible; 466 | return [].map.call(this.findAll(selector), function(element, index) { 467 | var attributes = {}; 468 | [].forEach.call(element.attributes, function(attr) { 469 | attributes[attr.name.toLowerCase()] = attr.value; 470 | }); 471 | return { 472 | nodeName: element.nodeName.toLowerCase(), 473 | attributes: attributes, 474 | tag: element.outerHTML, 475 | html: element.innerHTML, 476 | text: element.textContent || element.innerText, 477 | x: bounds[index].left, 478 | y: bounds[index].top, 479 | width: bounds[index].width, 480 | height: bounds[index].height, 481 | visible: eleVisible(element) 482 | }; 483 | }); 484 | }; 485 | 486 | /** 487 | * Retrieves a single DOM element matching a given XPath expression. 488 | * 489 | * @param String expression The XPath expression 490 | * @param HTMLElement|null scope Element to search child elements within 491 | * @return HTMLElement or null 492 | */ 493 | this.getElementByXPath = function getElementByXPath(expression, scope) { 494 | scope = scope || this.options.scope; 495 | var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 496 | if (a.snapshotLength > 0) { 497 | return a.snapshotItem(0); 498 | } 499 | }; 500 | 501 | /** 502 | * Retrieves all DOM elements matching a given XPath expression. 503 | * 504 | * @param String expression The XPath expression 505 | * @param HTMLElement|null scope Element to search child elements within 506 | * @return Array 507 | */ 508 | this.getElementsByXPath = function getElementsByXPath(expression, scope) { 509 | scope = scope || this.options.scope; 510 | var nodes = []; 511 | var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 512 | for (var i = 0; i < a.snapshotLength; i++) { 513 | nodes.push(a.snapshotItem(i)); 514 | } 515 | return nodes; 516 | }; 517 | 518 | /** 519 | * Retrieves the value of a form field. 520 | * 521 | * @param String inputName The for input name attr value 522 | * @param Object options Object with formSelector, optional 523 | * @return Mixed 524 | */ 525 | this.getFieldValue = function getFieldValue(inputName, options) { 526 | options = options || {}; 527 | function getSingleValue(input) { 528 | var type; 529 | try { 530 | type = input.getAttribute('type').toLowerCase(); 531 | } catch (e) { 532 | type = 'other'; 533 | } 534 | if (['checkbox', 'radio'].indexOf(type) === -1) { 535 | return input.value; 536 | } 537 | // single checkbox or… radio button (weird, I know) 538 | if (input.hasAttribute('value')) { 539 | return input.checked ? input.getAttribute('value') : undefined; 540 | } 541 | return input.checked; 542 | } 543 | function getMultipleValues(inputs) { 544 | var type; 545 | type = inputs[0].getAttribute('type').toLowerCase(); 546 | if (type === 'radio') { 547 | var value; 548 | [].forEach.call(inputs, function(radio) { 549 | value = radio.checked ? radio.value : value; 550 | }); 551 | return value; 552 | } else if (type === 'checkbox') { 553 | var values = []; 554 | [].forEach.call(inputs, function(checkbox) { 555 | if (checkbox.checked) { 556 | values.push(checkbox.value); 557 | } 558 | }); 559 | return values; 560 | } 561 | } 562 | var formSelector = ''; 563 | if (options && options.formSelector) { 564 | formSelector = options.formSelector + ' '; 565 | } 566 | var inputs = this.findAll(formSelector + '[name="' + inputName + '"]'); 567 | switch (inputs.length) { 568 | case 0: return undefined; 569 | case 1: return getSingleValue(inputs[0]); 570 | default: return getMultipleValues(inputs); 571 | } 572 | }; 573 | 574 | /** 575 | * Retrieves a given form all of its field values. 576 | * 577 | * @param String selector A DOM CSS3/XPath selector 578 | * @return Object 579 | */ 580 | this.getFormValues = function getFormValues(selector) { 581 | var form = this.findOne(selector); 582 | var values = {}; 583 | var self = this; 584 | [].forEach.call(form.elements, function(element) { 585 | var name = element.getAttribute('name'); 586 | if (name && !values[name]) { 587 | values[name] = self.getFieldValue(name, {formSelector: selector}); 588 | } 589 | }); 590 | return values; 591 | }; 592 | 593 | /** 594 | * Logs a message. Will format the message a way CasperJS will be able 595 | * to log phantomjs side. 596 | * 597 | * @param String message The message to log 598 | * @param String level The log level 599 | */ 600 | this.log = function log(message, level) { 601 | console.log("[casper:" + (level || "debug") + "] " + message); 602 | }; 603 | 604 | /** 605 | * Dispatches a mouse event to the DOM element behind the provided selector. 606 | * 607 | * @param String type Type of event to dispatch 608 | * @param String selector A CSS3 selector to the element to click 609 | * @return Boolean 610 | */ 611 | this.mouseEvent = function mouseEvent(type, selector) { 612 | var elem = this.findOne(selector); 613 | if (!elem) { 614 | this.log("mouseEvent(): Couldn't find any element matching '" + selector + "' selector", "error"); 615 | return false; 616 | } 617 | try { 618 | var evt = document.createEvent("MouseEvents"); 619 | var center_x = 1, center_y = 1; 620 | try { 621 | var pos = elem.getBoundingClientRect(); 622 | center_x = Math.floor((pos.left + pos.right) / 2), 623 | center_y = Math.floor((pos.top + pos.bottom) / 2); 624 | } catch(e) {} 625 | evt.initMouseEvent(type, true, true, window, 1, 1, 1, center_x, center_y, false, false, false, false, 0, elem); 626 | // dispatchEvent return value is false if at least one of the event 627 | // handlers which handled this event called preventDefault; 628 | // so we cannot returns this results as it cannot accurately informs on the status 629 | // of the operation 630 | // let's assume the event has been sent ok it didn't raise any error 631 | elem.dispatchEvent(evt); 632 | return true; 633 | } catch (e) { 634 | this.log("Failed dispatching " + type + "mouse event on " + selector + ": " + e, "error"); 635 | return false; 636 | } 637 | }; 638 | 639 | /** 640 | * Processes a selector input, either as a string or an object. 641 | * 642 | * If passed an object, if must be of the form: 643 | * 644 | * selectorObject = { 645 | * type: <'css' or 'xpath'>, 646 | * path: 647 | * } 648 | * 649 | * @param String|Object selector The selector string or object 650 | * 651 | * @return an object containing 'type' and 'path' keys 652 | */ 653 | this.processSelector = function processSelector(selector) { 654 | var selectorObject = { 655 | toString: function toString() { 656 | return this.type + ' selector: ' + this.path; 657 | } 658 | }; 659 | if (typeof selector === "string") { 660 | // defaults to CSS selector 661 | selectorObject.type = "css"; 662 | selectorObject.path = selector; 663 | return selectorObject; 664 | } else if (typeof selector === "object") { 665 | // validation 666 | if (!selector.hasOwnProperty('type') || !selector.hasOwnProperty('path')) { 667 | throw new Error("Incomplete selector object"); 668 | } else if (SUPPORTED_SELECTOR_TYPES.indexOf(selector.type) === -1) { 669 | throw new Error("Unsupported selector type: " + selector.type); 670 | } 671 | if (!selector.hasOwnProperty('toString')) { 672 | selector.toString = selectorObject.toString; 673 | } 674 | return selector; 675 | } 676 | throw new Error("Unsupported selector type: " + typeof selector); 677 | }; 678 | 679 | /** 680 | * Removes all DOM elements matching a given XPath expression. 681 | * 682 | * @param String expression The XPath expression 683 | * @return Array 684 | */ 685 | this.removeElementsByXPath = function removeElementsByXPath(expression) { 686 | var a = document.evaluate(expression, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); 687 | for (var i = 0; i < a.snapshotLength; i++) { 688 | a.snapshotItem(i).parentNode.removeChild(a.snapshotItem(i)); 689 | } 690 | }; 691 | 692 | /** 693 | * Performs an AJAX request. 694 | * 695 | * @param String url Url. 696 | * @param String method HTTP method (default: GET). 697 | * @param Object data Request parameters. 698 | * @param Boolean async Asynchroneous request? (default: false) 699 | * @param Object settings Other settings when perform the ajax request 700 | * @return String Response text. 701 | */ 702 | this.sendAJAX = function sendAJAX(url, method, data, async, settings) { 703 | var xhr = new XMLHttpRequest(), 704 | dataString = "", 705 | dataList = []; 706 | method = method && method.toUpperCase() || "GET"; 707 | var contentType = settings && settings.contentType || "application/x-www-form-urlencoded"; 708 | xhr.open(method, url, !!async); 709 | this.log("sendAJAX(): Using HTTP method: '" + method + "'", "debug"); 710 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); 711 | if (method === "POST") { 712 | if (typeof data === "object") { 713 | for (var k in data) { 714 | dataList.push(encodeURIComponent(k) + "=" + encodeURIComponent(data[k].toString())); 715 | } 716 | dataString = dataList.join('&'); 717 | this.log("sendAJAX(): Using request data: '" + dataString + "'", "debug"); 718 | } else if (typeof data === "string") { 719 | dataString = data; 720 | } 721 | xhr.setRequestHeader("Content-Type", contentType); 722 | } 723 | xhr.send(method === "POST" ? dataString : null); 724 | return xhr.responseText; 725 | }; 726 | 727 | /** 728 | * Sets a field (or a set of fields) value. Fails silently, but log 729 | * error messages. 730 | * 731 | * @param HTMLElement|NodeList field One or more element defining a field 732 | * @param mixed value The field value to set 733 | */ 734 | this.setField = function setField(field, value) { 735 | /*jshint maxcomplexity:99 */ 736 | var logValue, fields, out; 737 | value = logValue = (value || ""); 738 | 739 | if (field instanceof NodeList || field instanceof Array) { 740 | fields = field; 741 | field = fields[0]; 742 | } 743 | 744 | if (!(field instanceof HTMLElement)) { 745 | var error = new Error('Invalid field type; only HTMLElement and NodeList are supported'); 746 | error.name = 'FieldNotFound'; 747 | throw error; 748 | } 749 | 750 | if (this.options && this.options.safeLogs && field.getAttribute('type') === "password") { 751 | // obfuscate password value 752 | logValue = new Array(value.length + 1).join("*"); 753 | } 754 | 755 | this.log('Set "' + field.getAttribute('name') + '" field value to ' + logValue, "debug"); 756 | 757 | try { 758 | field.focus(); 759 | } catch (e) { 760 | this.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning"); 761 | } 762 | 763 | var nodeName = field.nodeName.toLowerCase(); 764 | 765 | switch (nodeName) { 766 | case "input": 767 | var type = field.getAttribute('type') || "text"; 768 | switch (type.toLowerCase()) { 769 | case "color": 770 | case "date": 771 | case "datetime": 772 | case "datetime-local": 773 | case "email": 774 | case "hidden": 775 | case "month": 776 | case "number": 777 | case "password": 778 | case "range": 779 | case "search": 780 | case "tel": 781 | case "text": 782 | case "time": 783 | case "url": 784 | case "week": 785 | field.value = value; 786 | break; 787 | case "checkbox": 788 | if (fields.length > 1) { 789 | var values = value; 790 | if (!Array.isArray(values)) { 791 | values = [values]; 792 | } 793 | Array.prototype.forEach.call(fields, function _forEach(f) { 794 | f.checked = values.indexOf(f.value) !== -1 ? true : false; 795 | }); 796 | } else { 797 | field.checked = value ? true : false; 798 | } 799 | break; 800 | case "file": 801 | throw { 802 | name: "FileUploadError", 803 | message: "File field must be filled using page.uploadFile", 804 | path: value 805 | }; 806 | case "radio": 807 | if (fields) { 808 | Array.prototype.forEach.call(fields, function _forEach(e) { 809 | e.checked = (e.value === value); 810 | }); 811 | } else { 812 | out = 'Provided radio elements are empty'; 813 | } 814 | break; 815 | default: 816 | out = "Unsupported input field type: " + type; 817 | break; 818 | } 819 | break; 820 | case "select": 821 | case "textarea": 822 | field.value = value; 823 | break; 824 | default: 825 | out = 'Unsupported field type: ' + nodeName; 826 | break; 827 | } 828 | 829 | // firing the `change` and `input` events 830 | ['change', 'input'].forEach(function(name) { 831 | var event = document.createEvent("HTMLEvents"); 832 | event.initEvent(name, true, true); 833 | field.dispatchEvent(event); 834 | }); 835 | 836 | // blur the field 837 | try { 838 | field.blur(); 839 | } catch (err) { 840 | this.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning"); 841 | } 842 | return out; 843 | }; 844 | 845 | /** 846 | * Checks if any element matching a given selector is visible in remote page. 847 | * 848 | * @param String selector CSS3 selector 849 | * @return Boolean 850 | */ 851 | this.visible = function visible(selector) { 852 | return [].some.call(this.findAll(selector), this.elementVisible); 853 | }; 854 | }; 855 | })(typeof exports === "object" ? exports : window); 856 | -------------------------------------------------------------------------------- /deps/js/casperjs/modules/tester.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Casper is a navigation utility for PhantomJS. 3 | * 4 | * Documentation: http://casperjs.org/ 5 | * Repository: http://github.com/n1k0/casperjs 6 | * 7 | * Copyright (c) 2011-2012 Nicolas Perriault 8 | * 9 | * Part of source code is Copyright Joyent, Inc. and other Node contributors. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | * 29 | */ 30 | 31 | /*global CasperError, exports, phantom, __utils__, patchRequire, require:true*/ 32 | 33 | var require = patchRequire(require); 34 | var fs = require('fs'); 35 | var events = require('events'); 36 | var utils = require('utils'); 37 | var f = utils.format; 38 | 39 | function AssertionError(msg, result) { 40 | "use strict"; 41 | Error.call(this); 42 | this.message = msg; 43 | this.name = 'AssertionError'; 44 | this.result = result; 45 | } 46 | AssertionError.prototype = new Error(); 47 | exports.AssertionError = AssertionError; 48 | 49 | function TerminationError(msg) { 50 | "use strict"; 51 | Error.call(this); 52 | this.message = msg; 53 | this.name = 'TerminationError'; 54 | } 55 | TerminationError.prototype = new Error(); 56 | exports.TerminationError = TerminationError; 57 | 58 | function TimedOutError(msg) { 59 | "use strict"; 60 | Error.call(this); 61 | this.message = msg; 62 | this.name = 'TimedOutError'; 63 | } 64 | TimedOutError.prototype = new Error(); 65 | exports.TimedOutError = TimedOutError; 66 | 67 | /** 68 | * Creates a tester instance. 69 | * 70 | * @param Casper casper A Casper instance 71 | * @param Object options Tester options 72 | * @return Tester 73 | */ 74 | exports.create = function create(casper, options) { 75 | "use strict"; 76 | return new Tester(casper, options); 77 | }; 78 | 79 | /** 80 | * Casper tester: makes assertions, stores test results and display then. 81 | * 82 | * @param Casper casper A valid Casper instance 83 | * @param Object|null options Options object 84 | */ 85 | var Tester = function Tester(casper, options) { 86 | "use strict"; 87 | /*jshint maxstatements:99*/ 88 | if (!utils.isCasperObject(casper)) { 89 | throw new CasperError("Tester needs a Casper instance"); 90 | } 91 | 92 | // self reference 93 | var self = this; 94 | 95 | // casper reference 96 | this.casper = casper; 97 | 98 | // public properties 99 | this._setUp = undefined; 100 | this._tearDown = undefined; 101 | this.aborted = false; 102 | this.executed = 0; 103 | this.currentTestFile = null; 104 | this.currentTestStartTime = new Date(); 105 | this.currentSuite = undefined; 106 | this.currentSuiteNum = 0; 107 | this.lastAssertTime = 0; 108 | this.loadIncludes = { 109 | includes: [], 110 | pre: [], 111 | post: [] 112 | }; 113 | this.options = utils.mergeObjects({ 114 | concise: false, // concise output? 115 | failFast: false, // terminates a suite as soon as a test fails? 116 | failText: "FAIL", // text to use for a failed test 117 | passText: "PASS", // text to use for a succesful test 118 | skipText: "SKIP", // text to use for a skipped test 119 | pad: 80 , // maximum number of chars for a result line 120 | warnText: "WARN" // text to use for a dubious test 121 | }, options); 122 | this.queue = []; 123 | this.running = false; 124 | this.started = false; 125 | this.suiteResults = new TestSuiteResult(); 126 | 127 | this.on('success', function onSuccess(success) { 128 | var timeElapsed = new Date() - this.currentTestStartTime; 129 | this.currentSuite.addSuccess(success, timeElapsed - this.lastAssertTime); 130 | this.lastAssertTime = timeElapsed; 131 | }); 132 | 133 | this.on('skipped', function onSkipped(skipped) { 134 | var timeElapsed = new Date() - this.currentTestStartTime; 135 | this.currentSuite.addSkip(skipped, timeElapsed - this.lastAssertTime); 136 | this.lastAssertTime = timeElapsed; 137 | }); 138 | 139 | this.on('fail', function onFail(failure) { 140 | // export 141 | var valueKeys = Object.keys(failure.values), 142 | timeElapsed = new Date() - this.currentTestStartTime; 143 | this.currentSuite.addFailure(failure, timeElapsed - this.lastAssertTime); 144 | this.lastAssertTime = timeElapsed; 145 | // special printing 146 | if (failure.type) { 147 | this.comment(' type: ' + failure.type); 148 | } 149 | if (failure.file) { 150 | this.comment(' file: ' + failure.file + (failure.line ? ':' + failure.line : '')); 151 | } 152 | if (failure.lineContents) { 153 | this.comment(' code: ' + failure.lineContents); 154 | } 155 | if (!failure.values || valueKeys.length === 0) { 156 | return; 157 | } 158 | valueKeys.forEach(function(name) { 159 | this.comment(f(' %s: %s', name, utils.formatTestValue(failure.values[name], name))); 160 | }.bind(this)); 161 | // check for fast failing 162 | if (this.options.failFast) { 163 | return this.terminate('--fail-fast: aborted all remaining tests'); 164 | } 165 | }); 166 | 167 | // casper events 168 | this.casper.on('error', function onCasperError(msg, backtrace) { 169 | self.processPhantomError(msg, backtrace); 170 | }); 171 | 172 | this.casper.on('waitFor.timeout', function onWaitForTimeout(timeout) { 173 | this.warn(f('wait timeout of %dms reached', timeout)); 174 | }); 175 | 176 | function errorHandler(error, backtrace) { 177 | self.casper.unwait(); 178 | if (error instanceof Error) { 179 | self.processError(error); 180 | return; 181 | } 182 | if (utils.isString(error) && /^(Assertion|Termination|TimedOut)Error/.test(error)) { 183 | return; 184 | } 185 | var line = 0; 186 | try { 187 | line = (backtrace || []).filter(function(entry) { 188 | return self.currentTestFile === entry.file; 189 | })[0].line; 190 | } catch (e) {} 191 | self.uncaughtError(error, self.currentTestFile, line, backtrace); 192 | } 193 | 194 | function errorHandlerAndDone(error, backtrace) { 195 | errorHandler(error, backtrace); 196 | self.done(); 197 | } 198 | 199 | [ 200 | 'wait.error', 201 | 'waitFor.timeout.error', 202 | 'event.error', 203 | 'complete.error' 204 | ].forEach(function(event) { 205 | self.casper.on(event, errorHandlerAndDone); 206 | }); 207 | 208 | self.casper.on('step.error', errorHandler); 209 | 210 | this.casper.on('warn', function(warning) { 211 | if (self.currentSuite) { 212 | self.currentSuite.addWarning(warning); 213 | } 214 | }); 215 | 216 | // Do not hook casper if we're not testing 217 | if (!phantom.casperTest) { 218 | return; 219 | } 220 | 221 | // specific timeout callbacks 222 | this.casper.options.onStepTimeout = function test_onStepTimeout(timeout, step) { 223 | throw new TimedOutError(f("Step timeout occured at step %s (%dms)", step, timeout)); 224 | }; 225 | 226 | this.casper.options.onTimeout = function test_onTimeout(timeout) { 227 | throw new TimedOutError(f("Timeout occured (%dms)", timeout)); 228 | }; 229 | 230 | this.casper.options.onWaitTimeout = function test_onWaitTimeout(timeout) { 231 | throw new TimedOutError(f("Wait timeout occured (%dms)", timeout)); 232 | }; 233 | }; 234 | 235 | // Tester class is an EventEmitter 236 | utils.inherits(Tester, events.EventEmitter); 237 | exports.Tester = Tester; 238 | 239 | /** 240 | * Aborts current test suite. 241 | * 242 | * @param String message Warning message (optional) 243 | */ 244 | Tester.prototype.abort = function abort(message) { 245 | "use strict"; 246 | throw new TerminationError(message || 'test suite aborted'); 247 | }; 248 | 249 | /** 250 | * Skip `nb` tests. 251 | * 252 | * @param Integer nb Number of tests to skip 253 | * @param String message Message to display 254 | * @return Object 255 | */ 256 | Tester.prototype.skip = function skip(nb, message) { 257 | "use strict"; 258 | return this.processAssertionResult({ 259 | success: null, 260 | standard: f("%d test%s skipped", nb, nb > 1 ? "s" : ""), 261 | message: message, 262 | type: "skip", 263 | number: nb, 264 | skipped: true 265 | }); 266 | }; 267 | 268 | /** 269 | * Asserts that a condition strictly resolves to true. Also returns an 270 | * "assertion object" containing useful informations about the test case 271 | * results. 272 | * 273 | * This method is also used as the base one used for all other `assert*` 274 | * family methods; supplementary informations are then passed using the 275 | * `context` argument. 276 | * 277 | * Note: an AssertionError is thrown if the assertion fails. 278 | * 279 | * @param Boolean subject The condition to test 280 | * @param String message Test description 281 | * @param Object|null context Assertion context object (Optional) 282 | * @return Object An assertion result object if test passed 283 | * @throws AssertionError in case the test failed 284 | */ 285 | Tester.prototype.assert = 286 | Tester.prototype.assertTrue = function assert(subject, message, context) { 287 | "use strict"; 288 | this.executed++; 289 | var result = utils.mergeObjects({ 290 | success: subject === true, 291 | type: "assert", 292 | standard: "Subject is strictly true", 293 | message: message, 294 | file: this.currentTestFile, 295 | doThrow: true, 296 | values: { 297 | subject: utils.getPropertyPath(context, 'values.subject') || subject 298 | } 299 | }, context || {}); 300 | if (!result.success && result.doThrow) { 301 | throw new AssertionError(message || result.standard, result); 302 | } 303 | return this.processAssertionResult(result); 304 | }; 305 | 306 | /** 307 | * Asserts that two values are strictly equals. 308 | * 309 | * @param Mixed subject The value to test 310 | * @param Mixed expected The expected value 311 | * @param String message Test description (Optional) 312 | * @return Object An assertion result object 313 | */ 314 | Tester.prototype.assertEquals = 315 | Tester.prototype.assertEqual = function assertEquals(subject, expected, message) { 316 | "use strict"; 317 | return this.assert(utils.equals(subject, expected), message, { 318 | type: "assertEquals", 319 | standard: "Subject equals the expected value", 320 | values: { 321 | subject: subject, 322 | expected: expected 323 | } 324 | }); 325 | }; 326 | 327 | /** 328 | * Asserts that two values are strictly not equals. 329 | * 330 | * @param Mixed subject The value to test 331 | * @param Mixed expected The unwanted value 332 | * @param String|null message Test description (Optional) 333 | * @return Object An assertion result object 334 | */ 335 | Tester.prototype.assertNotEquals = function assertNotEquals(subject, shouldnt, message) { 336 | "use strict"; 337 | return this.assert(!this.testEquals(subject, shouldnt), message, { 338 | type: "assertNotEquals", 339 | standard: "Subject doesn't equal what it shouldn't be", 340 | values: { 341 | subject: subject, 342 | shouldnt: shouldnt 343 | } 344 | }); 345 | }; 346 | 347 | /** 348 | * Asserts that a selector expression matches n elements. 349 | * 350 | * @param Mixed selector A selector expression 351 | * @param Number count Expected number of matching elements 352 | * @param String message Test description (Optional) 353 | * @return Object An assertion result object 354 | */ 355 | Tester.prototype.assertElementCount = function assertElementCount(selector, count, message) { 356 | "use strict"; 357 | if (!utils.isNumber(count) || count < 0) { 358 | throw new CasperError('assertElementCount() needs a positive integer count'); 359 | } 360 | var elementCount = this.casper.evaluate(function(selector) { 361 | try { 362 | return __utils__.findAll(selector).length; 363 | } catch (e) { 364 | return -1; 365 | } 366 | }, selector); 367 | return this.assert(elementCount === count, message, { 368 | type: "assertElementCount", 369 | standard: f('%d element%s matching selector "%s" found', 370 | count, 371 | count > 1 ? 's' : '', 372 | selector), 373 | values: { 374 | selector: selector, 375 | expected: count, 376 | obtained: elementCount 377 | } 378 | }); 379 | }; 380 | 381 | /** 382 | * Asserts that a code evaluation in remote DOM resolves to true. 383 | * 384 | * @param Function fn A function to be evaluated in remote DOM 385 | * @param String message Test description 386 | * @param Object params Object/Array containing the parameters to inject into 387 | * the function (optional) 388 | * @return Object An assertion result object 389 | */ 390 | Tester.prototype.assertEval = 391 | Tester.prototype.assertEvaluate = function assertEval(fn, message, params) { 392 | "use strict"; 393 | return this.assert(this.casper.evaluate(fn, params), message, { 394 | type: "assertEval", 395 | standard: "Evaluated function returns true", 396 | values: { 397 | fn: fn, 398 | params: params 399 | } 400 | }); 401 | }; 402 | 403 | /** 404 | * Asserts that the result of a code evaluation in remote DOM equals 405 | * an expected value. 406 | * 407 | * @param Function fn The function to be evaluated in remote DOM 408 | * @param Boolean expected The expected value 409 | * @param String|null message Test description 410 | * @param Object|null params Object containing the parameters to inject into the 411 | * function (optional) 412 | * @return Object An assertion result object 413 | */ 414 | Tester.prototype.assertEvalEquals = 415 | Tester.prototype.assertEvalEqual = function assertEvalEquals(fn, expected, message, params) { 416 | "use strict"; 417 | var subject = this.casper.evaluate(fn, params); 418 | return this.assert(utils.equals(subject, expected), message, { 419 | type: "assertEvalEquals", 420 | standard: "Evaluated function returns the expected value", 421 | values: { 422 | fn: fn, 423 | params: params, 424 | subject: subject, 425 | expected: expected 426 | } 427 | }); 428 | }; 429 | 430 | /** 431 | * Asserts that the provided assertion fails (used for internal testing). 432 | * 433 | * @param Function fn A closure calling an assertion 434 | * @param String|null message Test description 435 | * @return Object An assertion result object 436 | */ 437 | Tester.prototype.assertFail = function assertFail(fn, message) { 438 | "use strict"; 439 | var failed = false; 440 | try { 441 | fn(); 442 | } catch (e) { 443 | failed = true; 444 | } 445 | return this.assert(failed, message, { 446 | type: "assertFail", 447 | standard: "Assertion fails as expected" 448 | }); 449 | }; 450 | 451 | /** 452 | * Asserts that a given input field has the provided value. 453 | * 454 | * @param String inputName The name attribute of the input element 455 | * @param String expected The expected value of the input element 456 | * @param String message Test description 457 | * @param Object options ClientUtils#getFieldValue options (optional) 458 | * @return Object An assertion result object 459 | */ 460 | Tester.prototype.assertField = function assertField(inputName, expected, message, options) { 461 | "use strict"; 462 | var actual = this.casper.evaluate(function(inputName, options) { 463 | return __utils__.getFieldValue(inputName, options); 464 | }, inputName, options); 465 | return this.assert(utils.equals(actual, expected), message, { 466 | type: 'assertField', 467 | standard: f('"%s" input field has the value "%s"', inputName, expected), 468 | values: { 469 | inputName: inputName, 470 | actual: actual, 471 | expected: expected 472 | } 473 | }); 474 | }; 475 | 476 | /** 477 | * Asserts that an element matching the provided selector expression exists in 478 | * remote DOM. 479 | * 480 | * @param String selector Selector expression 481 | * @param String message Test description 482 | * @return Object An assertion result object 483 | */ 484 | Tester.prototype.assertExists = 485 | Tester.prototype.assertExist = 486 | Tester.prototype.assertSelectorExists = 487 | Tester.prototype.assertSelectorExist = function assertExists(selector, message) { 488 | "use strict"; 489 | return this.assert(this.casper.exists(selector), message, { 490 | type: "assertExists", 491 | standard: f("Found an element matching: %s", selector), 492 | values: { 493 | selector: selector 494 | } 495 | }); 496 | }; 497 | 498 | /** 499 | * Asserts that an element matching the provided selector expression does not 500 | * exist in remote DOM. 501 | * 502 | * @param String selector Selector expression 503 | * @param String message Test description 504 | * @return Object An assertion result object 505 | */ 506 | Tester.prototype.assertDoesntExist = 507 | Tester.prototype.assertNotExists = function assertDoesntExist(selector, message) { 508 | "use strict"; 509 | return this.assert(!this.casper.exists(selector), message, { 510 | type: "assertDoesntExist", 511 | standard: f("No element found matching selector: %s", selector), 512 | values: { 513 | selector: selector 514 | } 515 | }); 516 | }; 517 | 518 | /** 519 | * Asserts that current HTTP status is the one passed as argument. 520 | * 521 | * @param Number status HTTP status code 522 | * @param String message Test description 523 | * @return Object An assertion result object 524 | */ 525 | Tester.prototype.assertHttpStatus = function assertHttpStatus(status, message) { 526 | "use strict"; 527 | var currentHTTPStatus = this.casper.currentHTTPStatus; 528 | return this.assert(utils.equals(this.casper.currentHTTPStatus, status), message, { 529 | type: "assertHttpStatus", 530 | standard: f("HTTP status code is: %s", status), 531 | values: { 532 | current: currentHTTPStatus, 533 | expected: status 534 | } 535 | }); 536 | }; 537 | 538 | /** 539 | * Asserts that a provided string matches a provided RegExp pattern. 540 | * 541 | * @param String subject The string to test 542 | * @param RegExp pattern A RegExp object instance 543 | * @param String message Test description 544 | * @return Object An assertion result object 545 | */ 546 | Tester.prototype.assertMatch = 547 | Tester.prototype.assertMatches = function assertMatch(subject, pattern, message) { 548 | "use strict"; 549 | if (utils.betterTypeOf(pattern) !== "regexp") { 550 | throw new CasperError('Invalid regexp.'); 551 | } 552 | return this.assert(pattern.test(subject), message, { 553 | type: "assertMatch", 554 | standard: "Subject matches the provided pattern", 555 | values: { 556 | subject: subject, 557 | pattern: pattern.toString() 558 | } 559 | }); 560 | }; 561 | 562 | /** 563 | * Asserts a condition resolves to false. 564 | * 565 | * @param Boolean condition The condition to test 566 | * @param String message Test description 567 | * @return Object An assertion result object 568 | */ 569 | Tester.prototype.assertNot = 570 | Tester.prototype.assertFalse = function assertNot(condition, message) { 571 | "use strict"; 572 | return this.assert(!condition, message, { 573 | type: "assertNot", 574 | standard: "Subject is falsy", 575 | values: { 576 | condition: condition 577 | } 578 | }); 579 | }; 580 | 581 | /** 582 | * Asserts that a selector expression is not currently visible. 583 | * 584 | * @param String expected selector expression 585 | * @param String message Test description 586 | * @return Object An assertion result object 587 | */ 588 | Tester.prototype.assertNotVisible = 589 | Tester.prototype.assertInvisible = function assertNotVisible(selector, message) { 590 | "use strict"; 591 | return this.assert(!this.casper.visible(selector), message, { 592 | type: "assertVisible", 593 | standard: "Selector is not visible", 594 | values: { 595 | selector: selector 596 | } 597 | }); 598 | }; 599 | 600 | /** 601 | * Asserts that the provided function called with the given parameters 602 | * will raise an exception. 603 | * 604 | * @param Function fn The function to test 605 | * @param Array args The arguments to pass to the function 606 | * @param String message Test description 607 | * @return Object An assertion result object 608 | */ 609 | Tester.prototype.assertRaises = 610 | Tester.prototype.assertRaise = 611 | Tester.prototype.assertThrows = function assertRaises(fn, args, message) { 612 | "use strict"; 613 | var context = { 614 | type: "assertRaises", 615 | standard: "Function raises an error" 616 | }; 617 | try { 618 | fn.apply(null, args); 619 | this.assert(false, message, context); 620 | } catch (error) { 621 | this.assert(true, message, utils.mergeObjects(context, { 622 | values: { 623 | error: error 624 | } 625 | })); 626 | } 627 | }; 628 | 629 | /** 630 | * Asserts that the current page has a resource that matches the provided test 631 | * 632 | * @param Function/String test A test function that is called with every response 633 | * @param String message Test description 634 | * @return Object An assertion result object 635 | */ 636 | Tester.prototype.assertResourceExists = 637 | Tester.prototype.assertResourceExist = function assertResourceExists(test, message) { 638 | "use strict"; 639 | return this.assert(this.casper.resourceExists(test), message, { 640 | type: "assertResourceExists", 641 | standard: "Expected resource has been found", 642 | values: { 643 | test: test 644 | } 645 | }); 646 | }; 647 | 648 | /** 649 | * Asserts that given text doesn't exist in the document body. 650 | * 651 | * @param String text Text not to be found 652 | * @param String message Test description 653 | * @return Object An assertion result object 654 | */ 655 | Tester.prototype.assertTextDoesntExist = 656 | Tester.prototype.assertTextDoesntExist = function assertTextDoesntExist(text, message) { 657 | "use strict"; 658 | var textFound = (this.casper.evaluate(function _evaluate() { 659 | return document.body.textContent || document.body.innerText; 660 | }).indexOf(text) === -1); 661 | return this.assert(textFound, message, { 662 | type: "assertTextDoesntExists", 663 | standard: "Text doesn't exist within the document body", 664 | values: { 665 | text: text 666 | } 667 | }); 668 | }; 669 | 670 | /** 671 | * Asserts that given text exists in the document body. 672 | * 673 | * @param String text Text to be found 674 | * @param String message Test description 675 | * @return Object An assertion result object 676 | */ 677 | Tester.prototype.assertTextExists = 678 | Tester.prototype.assertTextExist = function assertTextExists(text, message) { 679 | "use strict"; 680 | var textFound = (this.casper.evaluate(function _evaluate() { 681 | return document.body.textContent || document.body.innerText; 682 | }).indexOf(text) !== -1); 683 | return this.assert(textFound, message, { 684 | type: "assertTextExists", 685 | standard: "Found expected text within the document body", 686 | values: { 687 | text: text 688 | } 689 | }); 690 | }; 691 | 692 | /** 693 | * Asserts a subject is truthy. 694 | * 695 | * @param Mixed subject Test subject 696 | * @param String message Test description 697 | * @return Object An assertion result object 698 | */ 699 | Tester.prototype.assertTruthy = function assertTruthy(subject, message) { 700 | "use strict"; 701 | /*jshint eqeqeq:false*/ 702 | return this.assert(utils.isTruthy(subject), message, { 703 | type: "assertTruthy", 704 | standard: "Subject is truthy", 705 | values: { 706 | subject: subject 707 | } 708 | }); 709 | }; 710 | 711 | /** 712 | * Asserts a subject is falsy. 713 | * 714 | * @param Mixed subject Test subject 715 | * @param String message Test description 716 | * @return Object An assertion result object 717 | */ 718 | Tester.prototype.assertFalsy = function assertFalsy(subject, message) { 719 | "use strict"; 720 | /*jshint eqeqeq:false*/ 721 | return this.assert(utils.isFalsy(subject), message, { 722 | type: "assertFalsy", 723 | standard: "Subject is falsy", 724 | values: { 725 | subject: subject 726 | } 727 | }); 728 | }; 729 | 730 | /** 731 | * Asserts that given text exists in the provided selector. 732 | * 733 | * @param String selector Selector expression 734 | * @param String text Text to be found 735 | * @param String message Test description 736 | * @return Object An assertion result object 737 | */ 738 | Tester.prototype.assertSelectorHasText = 739 | Tester.prototype.assertSelectorContains = function assertSelectorHasText(selector, text, message) { 740 | "use strict"; 741 | var got = this.casper.fetchText(selector); 742 | var textFound = got.indexOf(text) !== -1; 743 | return this.assert(textFound, message, { 744 | type: "assertSelectorHasText", 745 | standard: f('Found "%s" within the selector "%s"', text, selector), 746 | values: { 747 | selector: selector, 748 | text: text, 749 | actualContent: got 750 | } 751 | }); 752 | }; 753 | 754 | /** 755 | * Asserts that given text does not exist in the provided selector. 756 | * 757 | * @param String selector Selector expression 758 | * @param String text Text not to be found 759 | * @param String message Test description 760 | * @return Object An assertion result object 761 | */ 762 | Tester.prototype.assertSelectorDoesntHaveText = 763 | Tester.prototype.assertSelectorDoesntContain = function assertSelectorDoesntHaveText(selector, text, message) { 764 | "use strict"; 765 | var textFound = this.casper.fetchText(selector).indexOf(text) === -1; 766 | return this.assert(textFound, message, { 767 | type: "assertSelectorDoesntHaveText", 768 | standard: f('Did not find "%s" within the selector "%s"', text, selector), 769 | values: { 770 | selector: selector, 771 | text: text 772 | } 773 | }); 774 | }; 775 | 776 | /** 777 | * Asserts that title of the remote page equals to the expected one. 778 | * 779 | * @param String expected The expected title string 780 | * @param String message Test description 781 | * @return Object An assertion result object 782 | */ 783 | Tester.prototype.assertTitle = function assertTitle(expected, message) { 784 | "use strict"; 785 | var currentTitle = this.casper.getTitle(); 786 | return this.assert(utils.equals(currentTitle, expected), message, { 787 | type: "assertTitle", 788 | standard: f('Page title is: "%s"', expected), 789 | values: { 790 | subject: currentTitle, 791 | expected: expected 792 | } 793 | }); 794 | }; 795 | 796 | /** 797 | * Asserts that title of the remote page matched the provided pattern. 798 | * 799 | * @param RegExp pattern The pattern to test the title against 800 | * @param String message Test description 801 | * @return Object An assertion result object 802 | */ 803 | Tester.prototype.assertTitleMatch = 804 | Tester.prototype.assertTitleMatches = function assertTitleMatch(pattern, message) { 805 | "use strict"; 806 | if (utils.betterTypeOf(pattern) !== "regexp") { 807 | throw new CasperError('Invalid regexp.'); 808 | } 809 | var currentTitle = this.casper.getTitle(); 810 | return this.assert(pattern.test(currentTitle), message, { 811 | type: "assertTitle", 812 | details: "Page title does not match the provided pattern", 813 | values: { 814 | subject: currentTitle, 815 | pattern: pattern.toString() 816 | } 817 | }); 818 | }; 819 | 820 | /** 821 | * Asserts that the provided subject is of the given type. 822 | * 823 | * @param mixed subject The value to test 824 | * @param String type The javascript type name 825 | * @param String message Test description 826 | * @return Object An assertion result object 827 | */ 828 | Tester.prototype.assertType = function assertType(subject, type, message) { 829 | "use strict"; 830 | var actual = utils.betterTypeOf(subject); 831 | return this.assert(utils.equals(actual, type), message, { 832 | type: "assertType", 833 | standard: f('Subject type is: "%s"', type), 834 | values: { 835 | subject: subject, 836 | type: type, 837 | actual: actual 838 | } 839 | }); 840 | }; 841 | 842 | /** 843 | * Asserts that a the current page url matches a given pattern. A pattern may be 844 | * either a RegExp object or a String. The method will test if the URL matches 845 | * the pattern or contains the String. 846 | * 847 | * @param RegExp|String pattern The test pattern 848 | * @param String message Test description 849 | * @return Object An assertion result object 850 | */ 851 | Tester.prototype.assertUrlMatch = 852 | Tester.prototype.assertUrlMatches = function assertUrlMatch(pattern, message) { 853 | "use strict"; 854 | var currentUrl = this.casper.getCurrentUrl(), 855 | patternType = utils.betterTypeOf(pattern), 856 | result; 857 | if (patternType === "regexp") { 858 | result = pattern.test(currentUrl); 859 | } else if (patternType === "string") { 860 | result = currentUrl.indexOf(pattern) !== -1; 861 | } else { 862 | throw new CasperError("assertUrlMatch() only accepts strings or regexps"); 863 | } 864 | return this.assert(result, message, { 865 | type: "assertUrlMatch", 866 | standard: "Current url matches the provided pattern", 867 | values: { 868 | currentUrl: currentUrl, 869 | pattern: pattern.toString() 870 | } 871 | }); 872 | }; 873 | 874 | /** 875 | * Asserts that a selector expression is currently visible. 876 | * 877 | * @param String expected selector expression 878 | * @param String message Test description 879 | * @return Object An assertion result object 880 | */ 881 | Tester.prototype.assertVisible = function assertVisible(selector, message) { 882 | "use strict"; 883 | return this.assert(this.casper.visible(selector), message, { 884 | type: "assertVisible", 885 | standard: "Selector is visible", 886 | values: { 887 | selector: selector 888 | } 889 | }); 890 | }; 891 | 892 | /** 893 | * Prints out a colored bar onto the console. 894 | * 895 | */ 896 | Tester.prototype.bar = function bar(text, style) { 897 | "use strict"; 898 | this.casper.echo(text, style, this.options.pad); 899 | }; 900 | 901 | /** 902 | * Defines a function which will be executed before every test. 903 | * 904 | * @param Function fn 905 | */ 906 | Tester.prototype.setUp = function setUp(fn) { 907 | "use strict"; 908 | this._setUp = fn; 909 | }; 910 | 911 | /** 912 | * Defines a function which will be executed after every test. 913 | * 914 | * @param Function fn 915 | */ 916 | Tester.prototype.tearDown = function tearDown(fn) { 917 | "use strict"; 918 | this._tearDown = fn; 919 | }; 920 | 921 | /** 922 | * Starts a suite. 923 | * 924 | * Can be invoked different ways: 925 | * 926 | * casper.test.begin("suite description", plannedTests, function(test){}) 927 | * casper.test.begin("suite description", function(test){}) 928 | */ 929 | Tester.prototype.begin = function begin() { 930 | "use strict"; 931 | if (this.started && this.running) 932 | return this.queue.push(arguments); 933 | 934 | function getConfig(args) { 935 | var config = { 936 | setUp: function(){}, 937 | tearDown: function(){} 938 | }; 939 | 940 | if (utils.isFunction(args[1])) { 941 | config.test = args[1]; 942 | } else if (utils.isObject(args[1])) { 943 | config = utils.mergeObjects(config, args[1]); 944 | } else if (utils.isNumber(args[1]) && utils.isFunction(args[2])) { 945 | config.planned = ~~args[1] || undefined; 946 | config.test = args[2]; 947 | } else if (utils.isNumber(args[1]) && utils.isObject(args[2])) { 948 | config.config = utils.mergeObjects(config, args[2]); 949 | config.planned = ~~args[1] || undefined; 950 | } else { 951 | throw new CasperError('Invalid call'); 952 | } 953 | 954 | if (!utils.isFunction(config.test)) 955 | throw new CasperError('begin() is missing a mandatory test function'); 956 | 957 | return config; 958 | } 959 | 960 | var description = arguments[0] || f("Untitled suite in %s", this.currentTestFile), 961 | config = getConfig([].slice.call(arguments)), 962 | next = function() { 963 | config.test(this, this.casper); 964 | if (this.options.concise) 965 | this.casper.echo([ 966 | this.colorize('PASS', 'INFO'), 967 | this.formatMessage(description), 968 | this.colorize(f('(%d test%s)', 969 | config.planned, 970 | config.planned > 1 ? 's' : ''), 'INFO') 971 | ].join(' ')); 972 | }.bind(this); 973 | 974 | if (!this.options.concise) 975 | this.comment(description); 976 | 977 | this.currentSuite = new TestCaseResult({ 978 | name: description, 979 | file: this.currentTestFile, 980 | config: config, 981 | planned: config.planned || undefined 982 | }); 983 | 984 | this.executed = 0; 985 | this.running = this.started = true; 986 | 987 | try { 988 | if (config.setUp) 989 | config.setUp(this, this.casper); 990 | 991 | if (!this._setUp) 992 | return next(); 993 | 994 | if (this._setUp.length > 0) 995 | return this._setUp.call(this, next); // async 996 | 997 | this._setUp.call(this); // sync 998 | next(); 999 | } catch (err) { 1000 | this.processError(err); 1001 | this.done(); 1002 | } 1003 | }; 1004 | 1005 | /** 1006 | * Render a colorized output. Basically a proxy method for 1007 | * `Casper.Colorizer#colorize()`. 1008 | * 1009 | * @param String message 1010 | * @param String style The style name 1011 | * @return String 1012 | */ 1013 | Tester.prototype.colorize = function colorize(message, style) { 1014 | "use strict"; 1015 | return this.casper.getColorizer().colorize(message, style); 1016 | }; 1017 | 1018 | /** 1019 | * Writes a comment-style formatted message to stdout. 1020 | * 1021 | * @param String message 1022 | */ 1023 | Tester.prototype.comment = function comment(message) { 1024 | "use strict"; 1025 | this.casper.echo('# ' + message, 'COMMENT'); 1026 | }; 1027 | 1028 | /** 1029 | * Declares the current test suite done. 1030 | * 1031 | */ 1032 | Tester.prototype.done = function done() { 1033 | "use strict"; 1034 | /*jshint maxstatements:20, maxcomplexity:20*/ 1035 | var planned, config = this.currentSuite && this.currentSuite.config || {}; 1036 | 1037 | if (arguments.length && utils.isNumber(arguments[0])) { 1038 | this.casper.warn('done() `planned` arg is deprecated as of 1.1'); 1039 | planned = arguments[0]; 1040 | } 1041 | 1042 | if (config && config.tearDown && utils.isFunction(config.tearDown)) { 1043 | try { 1044 | config.tearDown(this, this.casper); 1045 | } catch (error) { 1046 | this.processError(error); 1047 | } 1048 | } 1049 | 1050 | var next = function() { 1051 | if (this.currentSuite && this.currentSuite.planned && 1052 | this.currentSuite.planned !== this.executed + this.currentSuite.skipped && 1053 | !this.currentSuite.failed) { 1054 | this.dubious(this.currentSuite.planned, this.executed, this.currentSuite.name); 1055 | } else if (planned && planned !== this.executed) { 1056 | // BC 1057 | this.dubious(planned, this.executed); 1058 | } 1059 | if (this.currentSuite) { 1060 | this.suiteResults.push(this.currentSuite); 1061 | this.currentSuite = undefined; 1062 | this.executed = 0; 1063 | } 1064 | this.emit('test.done'); 1065 | this.casper.currentHTTPResponse = {}; 1066 | this.running = this.started = false; 1067 | var nextTest = this.queue.shift(); 1068 | if (nextTest) { 1069 | this.begin.apply(this, nextTest); 1070 | } 1071 | }.bind(this); 1072 | 1073 | if (!this._tearDown) { 1074 | return next(); 1075 | } 1076 | 1077 | try { 1078 | if (this._tearDown.length > 0) { 1079 | // async 1080 | this._tearDown.call(this, next); 1081 | } else { 1082 | // sync 1083 | this._tearDown.call(this); 1084 | next(); 1085 | } 1086 | } catch (error) { 1087 | this.processError(error); 1088 | } 1089 | }; 1090 | 1091 | /** 1092 | * Marks a test as dubious, when the number of planned tests doesn't match the 1093 | * number of actually executed one. 1094 | * 1095 | * @param String message 1096 | */ 1097 | Tester.prototype.dubious = function dubious(planned, executed, suite) { 1098 | "use strict"; 1099 | var message = f('%s: %d tests planned, %d tests executed', suite || 'global', planned, executed); 1100 | this.casper.warn(message); 1101 | if (!this.currentSuite) return; 1102 | this.currentSuite.addFailure({ 1103 | type: "dubious", 1104 | file: this.currentTestFile, 1105 | standard: message 1106 | }); 1107 | }; 1108 | 1109 | /** 1110 | * Writes an error-style formatted message to stdout. 1111 | * 1112 | * @param String message 1113 | */ 1114 | Tester.prototype.error = function error(message) { 1115 | "use strict"; 1116 | this.casper.echo(message, 'ERROR'); 1117 | }; 1118 | 1119 | /** 1120 | * Executes a file, wraping and evaluating its code in an isolated 1121 | * environment where only the current `casper` instance is passed. 1122 | * 1123 | * @param String file Absolute path to some js/coffee file 1124 | */ 1125 | Tester.prototype.exec = function exec(file) { 1126 | "use strict"; 1127 | file = this.filter('exec.file', file) || file; 1128 | if (!fs.isFile(file) || !utils.isJsFile(file)) { 1129 | var e = new CasperError(f("Cannot exec %s: can only exec() files with .js or .coffee extensions", 1130 | file)); 1131 | e.fileName = e.file = e.sourceURL = file; 1132 | throw e; 1133 | } 1134 | this.currentTestFile = file; 1135 | phantom.injectJs(file); 1136 | }; 1137 | 1138 | /** 1139 | * Adds a failed test entry to the stack. 1140 | * 1141 | * @param String message 1142 | * @param Object Failure context (optional) 1143 | */ 1144 | Tester.prototype.fail = function fail(message, context) { 1145 | "use strict"; 1146 | context = context || {}; 1147 | return this.assert(false, message, utils.mergeObjects({ 1148 | type: "fail", 1149 | standard: "explicit call to fail()" 1150 | }, context)); 1151 | }; 1152 | 1153 | /** 1154 | * Recursively finds all test files contained in a given directory. 1155 | * 1156 | * @param String dir Path to some directory to scan 1157 | */ 1158 | Tester.prototype.findTestFiles = function findTestFiles(dir) { 1159 | "use strict"; 1160 | var self = this; 1161 | if (!fs.isDirectory(dir)) { 1162 | return []; 1163 | } 1164 | var entries = fs.list(dir).filter(function _filter(entry) { 1165 | return entry !== '.' && entry !== '..'; 1166 | }).map(function _map(entry) { 1167 | return fs.absolute(fs.pathJoin(dir, entry)); 1168 | }); 1169 | entries.forEach(function _forEach(entry) { 1170 | if (fs.isDirectory(entry)) { 1171 | entries = entries.concat(self.findTestFiles(entry)); 1172 | } 1173 | }); 1174 | return entries.filter(function _filter(entry) { 1175 | return utils.isJsFile(fs.absolute(fs.pathJoin(dir, entry))); 1176 | }).sort(); 1177 | }; 1178 | 1179 | /** 1180 | * Computes current suite identifier. 1181 | * 1182 | * @return String 1183 | */ 1184 | Tester.prototype.getCurrentSuiteId = function getCurrentSuiteId() { 1185 | "use strict"; 1186 | return this.casper.test.currentSuiteNum + "-" + this.casper.step; 1187 | }; 1188 | 1189 | /** 1190 | * Formats a message to highlight some parts of it. 1191 | * 1192 | * @param String message 1193 | * @param String style 1194 | */ 1195 | Tester.prototype.formatMessage = function formatMessage(message, style) { 1196 | "use strict"; 1197 | var parts = /^([a-z0-9_\.]+\(\))(.*)/i.exec(message); 1198 | if (!parts) { 1199 | return message; 1200 | } 1201 | return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style); 1202 | }; 1203 | 1204 | /** 1205 | * Writes an info-style formatted message to stdout. 1206 | * 1207 | * @param String message 1208 | */ 1209 | Tester.prototype.info = function info(message) { 1210 | "use strict"; 1211 | this.casper.echo(message, 'PARAMETER'); 1212 | }; 1213 | 1214 | /** 1215 | * Adds a succesful test entry to the stack. 1216 | * 1217 | * @param String message 1218 | */ 1219 | Tester.prototype.pass = function pass(message) { 1220 | "use strict"; 1221 | return this.assert(true, message, { 1222 | type: "pass", 1223 | standard: "explicit call to pass()" 1224 | }); 1225 | }; 1226 | 1227 | function getStackEntry(error, testFile) { 1228 | "use strict"; 1229 | if ("stackArray" in error) { 1230 | // PhantomJS has changed the API of the Error object :-/ 1231 | // https://github.com/ariya/phantomjs/commit/c9cf14f221f58a3daf585c47313da6fced0276bc 1232 | return error.stackArray.filter(function(entry) { 1233 | return testFile === entry.sourceURL; 1234 | })[0]; 1235 | } 1236 | 1237 | if (! ('stack' in error)) 1238 | return null; 1239 | 1240 | var r = /^\s*(.*)@(.*):(\d+)\s*$/gm; 1241 | var m; 1242 | while ((m = r.exec(error.stack))) { 1243 | var sourceURL = m[2]; 1244 | if (sourceURL.indexOf('->') !== -1) { 1245 | sourceURL = sourceURL.split('->')[1].trim(); 1246 | } 1247 | if (sourceURL === testFile) { 1248 | return { sourceURL: sourceURL, line: m[3]} 1249 | } 1250 | } 1251 | return null; 1252 | } 1253 | 1254 | /** 1255 | * Processes an assertion error. 1256 | * 1257 | * @param AssertionError error 1258 | */ 1259 | Tester.prototype.processAssertionError = function(error) { 1260 | "use strict"; 1261 | var result = error && error.result || {}, 1262 | testFile = this.currentTestFile, 1263 | stackEntry; 1264 | try { 1265 | stackEntry = getStackEntry(error, testFile); 1266 | } catch (e) {} 1267 | if (stackEntry) { 1268 | result.line = stackEntry.line; 1269 | try { 1270 | result.lineContents = fs.read(this.currentTestFile).split('\n')[result.line - 1].trim(); 1271 | } catch (e) {} 1272 | } 1273 | return this.processAssertionResult(result); 1274 | }; 1275 | 1276 | /** 1277 | * Processes an assertion result by emitting the appropriate event and 1278 | * printing result onto the console. 1279 | * 1280 | * @param Object result An assertion result object 1281 | * @return Object The passed assertion result Object 1282 | */ 1283 | Tester.prototype.processAssertionResult = function processAssertionResult(result) { 1284 | "use strict"; 1285 | if (!this.currentSuite) { 1286 | // this is for BC when begin() didn't exist 1287 | this.currentSuite = new TestCaseResult({ 1288 | name: "Untitled suite in " + this.currentTestFile, 1289 | file: this.currentTestFile, 1290 | planned: undefined 1291 | }); 1292 | } 1293 | var eventName = 'success', 1294 | message = result.message || result.standard, 1295 | style = 'INFO', 1296 | status = this.options.passText; 1297 | if (null === result.success) { 1298 | eventName = 'skipped'; 1299 | style = 'SKIP'; 1300 | status = this.options.skipText; 1301 | } else if (!result.success) { 1302 | eventName = 'fail'; 1303 | style = 'RED_BAR'; 1304 | status = this.options.failText; 1305 | } 1306 | if (!this.options.concise) { 1307 | this.casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' ')); 1308 | } 1309 | this.emit(eventName, result); 1310 | return result; 1311 | }; 1312 | 1313 | /** 1314 | * Processes an error. 1315 | * 1316 | * @param Error error 1317 | */ 1318 | Tester.prototype.processError = function processError(error) { 1319 | "use strict"; 1320 | if (error instanceof AssertionError) { 1321 | return this.processAssertionError(error); 1322 | } 1323 | if (error instanceof TerminationError) { 1324 | return this.terminate(error.message); 1325 | } 1326 | return this.uncaughtError(error, this.currentTestFile, error.line); 1327 | }; 1328 | 1329 | /** 1330 | * Processes a PhantomJS error, which is an error message and a backtrace. 1331 | * 1332 | * @param String message 1333 | * @param Array backtrace 1334 | */ 1335 | Tester.prototype.processPhantomError = function processPhantomError(msg, backtrace) { 1336 | "use strict"; 1337 | if (/^AssertionError/.test(msg)) { 1338 | this.casper.warn('looks you did not use begin() which is mandatory since 1.1'); 1339 | } 1340 | var termination = /^TerminationError:?\s?(.*)/.exec(msg); 1341 | if (termination) { 1342 | var message = termination[1]; 1343 | if (backtrace && backtrace[0]) { 1344 | message += ' at ' + backtrace[0].file + backtrace[0].line; 1345 | } 1346 | return this.terminate(message); 1347 | } 1348 | this.fail(msg, { 1349 | type: "error", 1350 | doThrow: false, 1351 | values: { 1352 | error: msg, 1353 | stack: backtrace 1354 | } 1355 | }); 1356 | this.done(); 1357 | }; 1358 | 1359 | /** 1360 | * Renders a detailed report for each failed test. 1361 | * 1362 | */ 1363 | Tester.prototype.renderFailureDetails = function renderFailureDetails() { 1364 | "use strict"; 1365 | if (!this.suiteResults.isFailed()) { 1366 | return; 1367 | } 1368 | var failures = this.suiteResults.getAllFailures(); 1369 | this.casper.echo(f("\nDetails for the %d failed test%s:\n", 1370 | failures.length, failures.length > 1 ? "s" : ""), "PARAMETER"); 1371 | failures.forEach(function _forEach(failure) { 1372 | this.casper.echo(f('In %s%s', failure.file, ~~failure.line ? ':' + ~~failure.line : '')); 1373 | if (failure.suite) { 1374 | this.casper.echo(f(' %s', failure.suite), "PARAMETER"); 1375 | } 1376 | this.casper.echo(f(' %s: %s', failure.type || "unknown", 1377 | failure.message || failure.standard || "(no message was entered)"), "COMMENT"); 1378 | }.bind(this)); 1379 | }; 1380 | 1381 | /** 1382 | * Render tests results, an optionally exit phantomjs. 1383 | * 1384 | * @param Boolean exit 1385 | */ 1386 | Tester.prototype.renderResults = function renderResults(exit, status, save) { 1387 | "use strict"; 1388 | /*jshint maxstatements:20*/ 1389 | save = save || this.options.save; 1390 | var exitStatus = 0, 1391 | failed = this.suiteResults.countFailed(), 1392 | total = this.suiteResults.countExecuted(), 1393 | statusText, 1394 | style, 1395 | result; 1396 | if (total === 0) { 1397 | exitStatus = 1; 1398 | statusText = this.options.warnText; 1399 | style = 'WARN_BAR'; 1400 | result = f("%s Looks like you didn't run any test.", statusText); 1401 | } else { 1402 | if (this.suiteResults.isFailed()) { 1403 | exitStatus = 1; 1404 | statusText = this.options.failText; 1405 | style = 'RED_BAR'; 1406 | } else { 1407 | statusText = this.options.passText; 1408 | style = 'GREEN_BAR'; 1409 | } 1410 | result = f('%s %d test%s executed in %ss, %d passed, %d failed, %d dubious, %d skipped.', 1411 | statusText, 1412 | total, 1413 | total > 1 ? "s" : "", 1414 | utils.ms2seconds(this.suiteResults.calculateDuration()), 1415 | this.suiteResults.countPassed(), 1416 | failed, 1417 | this.suiteResults.countDubious(), 1418 | this.suiteResults.countSkipped()); 1419 | } 1420 | this.casper.echo(result, style, this.options.pad); 1421 | this.renderFailureDetails(); 1422 | if (save) { 1423 | this.saveResults(save); 1424 | } 1425 | if (exit === true) { 1426 | this.casper.exit(status ? ~~status : exitStatus); 1427 | } 1428 | }; 1429 | 1430 | /** 1431 | * Runs al suites contained in the paths passed as arguments. 1432 | * 1433 | */ 1434 | Tester.prototype.runSuites = function runSuites() { 1435 | "use strict"; 1436 | var testFiles = [], self = this; 1437 | if (arguments.length === 0) { 1438 | throw new CasperError("runSuites() needs at least one path argument"); 1439 | } 1440 | this.loadIncludes.includes.forEach(function _forEachInclude(include) { 1441 | phantom.injectJs(include); 1442 | }); 1443 | this.loadIncludes.pre.forEach(function _forEachPreTest(preTestFile) { 1444 | testFiles = testFiles.concat(preTestFile); 1445 | }); 1446 | Array.prototype.forEach.call(arguments, function _forEachArgument(path) { 1447 | if (!fs.exists(path)) { 1448 | self.bar(f("Path %s doesn't exist", path), "RED_BAR"); 1449 | } 1450 | if (fs.isDirectory(path)) { 1451 | testFiles = testFiles.concat(self.findTestFiles(path)); 1452 | } else if (fs.isFile(path)) { 1453 | testFiles.push(path); 1454 | } 1455 | }); 1456 | this.loadIncludes.post.forEach(function _forEachPostTest(postTestFile) { 1457 | testFiles = testFiles.concat(postTestFile); 1458 | }); 1459 | if (testFiles.length === 0) { 1460 | this.bar(f("No test file found in %s, terminating.", 1461 | Array.prototype.slice.call(arguments)), "RED_BAR"); 1462 | this.casper.exit(1); 1463 | } 1464 | self.currentSuiteNum = 0; 1465 | self.currentTestStartTime = new Date(); 1466 | self.lastAssertTime = 0; 1467 | var interval = setInterval(function _check(self) { 1468 | if (self.running) { 1469 | return; 1470 | } 1471 | if (self.currentSuiteNum === testFiles.length || self.aborted) { 1472 | self.emit('tests.complete'); 1473 | clearInterval(interval); 1474 | self.aborted = false; 1475 | } else { 1476 | self.runTest(testFiles[self.currentSuiteNum]); 1477 | self.currentSuiteNum++; 1478 | } 1479 | }, 20, this); 1480 | }; 1481 | 1482 | /** 1483 | * Runs a test file 1484 | * 1485 | */ 1486 | Tester.prototype.runTest = function runTest(testFile) { 1487 | "use strict"; 1488 | this.bar(f('Test file: %s', testFile), 'INFO_BAR'); 1489 | this.running = true; // this.running is set back to false with done() 1490 | this.executed = 0; 1491 | this.exec(testFile); 1492 | }; 1493 | 1494 | /** 1495 | * Terminates current suite. 1496 | * 1497 | */ 1498 | Tester.prototype.terminate = function(message) { 1499 | "use strict"; 1500 | if (message) { 1501 | this.casper.warn(message); 1502 | } 1503 | this.done(); 1504 | this.aborted = true; 1505 | this.emit('tests.complete'); 1506 | }; 1507 | 1508 | /** 1509 | * Saves results to file. 1510 | * 1511 | * @param String filename Target file path. 1512 | */ 1513 | Tester.prototype.saveResults = function saveResults(filepath) { 1514 | "use strict"; 1515 | var exporter = require('xunit').create(); 1516 | exporter.setResults(this.suiteResults); 1517 | try { 1518 | fs.write(filepath, exporter.getXML(), 'w'); 1519 | this.casper.echo(f('Result log stored in %s', filepath), 'INFO', 80); 1520 | } catch (e) { 1521 | this.casper.echo(f('Unable to write results to %s: %s', filepath, e), 'ERROR', 80); 1522 | } 1523 | }; 1524 | 1525 | /** 1526 | * Tests equality between the two passed arguments. 1527 | * 1528 | * @param Mixed v1 1529 | * @param Mixed v2 1530 | * @param Boolean 1531 | */ 1532 | Tester.prototype.testEquals = Tester.prototype.testEqual = function testEquals(v1, v2) { 1533 | "use strict"; 1534 | return utils.equals(v1, v2); 1535 | }; 1536 | 1537 | /** 1538 | * Processes an error caught while running tests contained in a given test 1539 | * file. 1540 | * 1541 | * @param Error|String error The error 1542 | * @param String file Test file where the error occurred 1543 | * @param Number line Line number (optional) 1544 | * @param Array backtrace Error stack trace (optional) 1545 | */ 1546 | Tester.prototype.uncaughtError = function uncaughtError(error, file, line, backtrace) { 1547 | "use strict"; 1548 | // XXX: this is NOT an assertion scratch that 1549 | return this.processAssertionResult({ 1550 | success: false, 1551 | type: "uncaughtError", 1552 | file: file, 1553 | line: ~~line, 1554 | message: utils.isObject(error) ? error.message : error, 1555 | values: { 1556 | error: error, 1557 | stack: backtrace 1558 | } 1559 | }); 1560 | }; 1561 | 1562 | /** 1563 | * Test suites array. 1564 | * 1565 | */ 1566 | function TestSuiteResult() {} 1567 | TestSuiteResult.prototype = []; 1568 | exports.TestSuiteResult = TestSuiteResult; 1569 | 1570 | /** 1571 | * Returns the number of tests. 1572 | * 1573 | * @return Number 1574 | */ 1575 | TestSuiteResult.prototype.countTotal = function countTotal() { 1576 | "use strict"; 1577 | return this.countPassed() + this.countFailed() + this.countDubious(); 1578 | }; 1579 | 1580 | /** 1581 | * Returns the number of dubious results. 1582 | * 1583 | * @return Number 1584 | */ 1585 | TestSuiteResult.prototype.countDubious = function countDubious() { 1586 | "use strict"; 1587 | return this.map(function(result) { 1588 | return result.dubious; 1589 | }).reduce(function(a, b) { 1590 | return a + b; 1591 | }, 0); 1592 | }; 1593 | 1594 | /** 1595 | * Returns the number of executed tests. 1596 | * 1597 | * @return Number 1598 | */ 1599 | TestSuiteResult.prototype.countExecuted = function countTotal() { 1600 | "use strict"; 1601 | return this.countTotal() - this.countDubious(); 1602 | }; 1603 | 1604 | /** 1605 | * Returns the number of errors. 1606 | * 1607 | * @return Number 1608 | */ 1609 | TestSuiteResult.prototype.countErrors = function countErrors() { 1610 | "use strict"; 1611 | return this.map(function(result) { 1612 | return result.crashed; 1613 | }).reduce(function(a, b) { 1614 | return a + b; 1615 | }, 0); 1616 | }; 1617 | 1618 | /** 1619 | * Returns the number of failed tests. 1620 | * 1621 | * @return Number 1622 | */ 1623 | TestSuiteResult.prototype.countFailed = function countFailed() { 1624 | "use strict"; 1625 | return this.map(function(result) { 1626 | return result.failed - result.dubious; 1627 | }).reduce(function(a, b) { 1628 | return a + b; 1629 | }, 0); 1630 | }; 1631 | 1632 | /** 1633 | * Returns the number of succesful tests. 1634 | * 1635 | * @return Number 1636 | */ 1637 | TestSuiteResult.prototype.countPassed = function countPassed() { 1638 | "use strict"; 1639 | return this.map(function(result) { 1640 | return result.passed; 1641 | }).reduce(function(a, b) { 1642 | return a + b; 1643 | }, 0); 1644 | }; 1645 | 1646 | /** 1647 | * Returns the number of skipped tests. 1648 | * 1649 | * @return Number 1650 | */ 1651 | TestSuiteResult.prototype.countSkipped = function countSkipped() { 1652 | "use strict"; 1653 | return this.map(function(result) { 1654 | return result.skipped; 1655 | }).reduce(function(a, b) { 1656 | return a + b; 1657 | }, 0); 1658 | }; 1659 | 1660 | /** 1661 | * Returns the number of warnings. 1662 | * 1663 | * @return Number 1664 | */ 1665 | TestSuiteResult.prototype.countWarnings = function countWarnings() { 1666 | "use strict"; 1667 | return this.map(function(result) { 1668 | return result.warned; 1669 | }).reduce(function(a, b) { 1670 | return a + b; 1671 | }, 0); 1672 | }; 1673 | 1674 | /** 1675 | * Checks if the suite has failed. 1676 | * 1677 | * @return Number 1678 | */ 1679 | TestSuiteResult.prototype.isFailed = function isFailed() { 1680 | "use strict"; 1681 | return this.countErrors() + this.countFailed() + this.countDubious() > 0; 1682 | }; 1683 | 1684 | /** 1685 | * Checks if the suite has skipped tests. 1686 | * 1687 | * @return Number 1688 | */ 1689 | TestSuiteResult.prototype.isSkipped = function isSkipped() { 1690 | "use strict"; 1691 | return this.countSkipped() > 0; 1692 | }; 1693 | 1694 | /** 1695 | * Returns all failures from this suite. 1696 | * 1697 | * @return Array 1698 | */ 1699 | TestSuiteResult.prototype.getAllFailures = function getAllFailures() { 1700 | "use strict"; 1701 | var failures = []; 1702 | this.forEach(function(result) { 1703 | failures = failures.concat(result.failures); 1704 | }); 1705 | return failures; 1706 | }; 1707 | 1708 | /** 1709 | * Returns all succesful tests from this suite. 1710 | * 1711 | * @return Array 1712 | */ 1713 | TestSuiteResult.prototype.getAllPasses = function getAllPasses() { 1714 | "use strict"; 1715 | var passes = []; 1716 | this.forEach(function(result) { 1717 | passes = passes.concat(result.passes); 1718 | }); 1719 | return passes; 1720 | }; 1721 | 1722 | /** 1723 | * Returns all skipped tests from this suite. 1724 | * 1725 | * @return Array 1726 | */ 1727 | TestSuiteResult.prototype.getAllSkips = function getAllSkips() { 1728 | "use strict"; 1729 | var skipped = []; 1730 | this.forEach(function(result) { 1731 | skipped = skipped.concat(result.skipped); 1732 | }); 1733 | return skipped; 1734 | }; 1735 | 1736 | /** 1737 | * Returns all results from this suite. 1738 | * 1739 | * @return Array 1740 | */ 1741 | TestSuiteResult.prototype.getAllResults = function getAllResults() { 1742 | "use strict"; 1743 | return this.getAllPasses().concat(this.getAllFailures()); 1744 | }; 1745 | 1746 | /** 1747 | * Computes the sum of all durations of the tests which were executed in the 1748 | * current suite. 1749 | * 1750 | * @return Number 1751 | */ 1752 | TestSuiteResult.prototype.calculateDuration = function calculateDuration() { 1753 | "use strict"; 1754 | return this.getAllResults().map(function(result) { 1755 | return ~~result.time; 1756 | }).reduce(function add(a, b) { 1757 | return a + b; 1758 | }, 0); 1759 | }; 1760 | 1761 | /** 1762 | * Test suite results object. 1763 | * 1764 | * @param Object options 1765 | */ 1766 | function TestCaseResult(options) { 1767 | "use strict"; 1768 | this.name = options && options.name; 1769 | this.file = options && options.file; 1770 | this.planned = ~~(options && options.planned) || undefined; 1771 | this.errors = []; 1772 | this.failures = []; 1773 | this.passes = []; 1774 | this.skips = []; 1775 | this.warnings = []; 1776 | this.config = options && options.config; 1777 | this.__defineGetter__("assertions", function() { 1778 | return this.passed + this.failed; 1779 | }); 1780 | this.__defineGetter__("crashed", function() { 1781 | return this.errors.length; 1782 | }); 1783 | this.__defineGetter__("failed", function() { 1784 | return this.failures.length; 1785 | }); 1786 | this.__defineGetter__("dubious", function() { 1787 | return this.failures.filter(function(failure) { 1788 | return failure.type === "dubious"; 1789 | }).length; 1790 | }); 1791 | this.__defineGetter__("passed", function() { 1792 | return this.passes.length; 1793 | }); 1794 | this.__defineGetter__("skipped", function() { 1795 | return this.skips.map(function(skip) { 1796 | return skip.number; 1797 | }).reduce(function(a, b) { 1798 | return a + b; 1799 | }, 0); 1800 | }); 1801 | } 1802 | exports.TestCaseResult = TestCaseResult; 1803 | 1804 | /** 1805 | * Adds a failure record and its execution time. 1806 | * 1807 | * @param Object failure 1808 | * @param Number time 1809 | */ 1810 | TestCaseResult.prototype.addFailure = function addFailure(failure, time) { 1811 | "use strict"; 1812 | failure.suite = this.name; 1813 | failure.time = time; 1814 | this.failures.push(failure); 1815 | }; 1816 | 1817 | /** 1818 | * Adds an error record. 1819 | * 1820 | * @param Object failure 1821 | */ 1822 | TestCaseResult.prototype.addError = function addFailure(error) { 1823 | "use strict"; 1824 | error.suite = this.name; 1825 | this.errors.push(error); 1826 | }; 1827 | 1828 | /** 1829 | * Adds a success record and its execution time. 1830 | * 1831 | * @param Object success 1832 | * @param Number time 1833 | */ 1834 | TestCaseResult.prototype.addSuccess = function addSuccess(success, time) { 1835 | "use strict"; 1836 | success.suite = this.name; 1837 | success.time = time; 1838 | this.passes.push(success); 1839 | }; 1840 | 1841 | /** 1842 | * Adds a success record and its execution time. 1843 | * 1844 | * @param Object success 1845 | * @param Number time 1846 | */ 1847 | TestCaseResult.prototype.addSkip = function addSkip(skipped, time) { 1848 | "use strict"; 1849 | skipped.suite = this.name; 1850 | skipped.time = time; 1851 | this.skips.push(skipped); 1852 | }; 1853 | 1854 | 1855 | /** 1856 | * Adds a warning record. 1857 | * 1858 | * @param Object warning 1859 | */ 1860 | TestCaseResult.prototype.addWarning = function addWarning(warning) { 1861 | "use strict"; 1862 | warning.suite = this.name; 1863 | this.warnings.push(warning); 1864 | }; 1865 | 1866 | /** 1867 | * Computes total duration for this suite. 1868 | * 1869 | * @return Number 1870 | */ 1871 | TestCaseResult.prototype.calculateDuration = function calculateDuration() { 1872 | "use strict"; 1873 | function add(a, b) { 1874 | return a + b; 1875 | } 1876 | var passedTimes = this.passes.map(function(success) { 1877 | return ~~success.time; 1878 | }).reduce(add, 0); 1879 | var failedTimes = this.failures.map(function(failure) { 1880 | return ~~failure.time; 1881 | }).reduce(add, 0); 1882 | return passedTimes + failedTimes; 1883 | }; 1884 | --------------------------------------------------------------------------------