├── README.md ├── ppx.jquery.js ├── ppx.js ├── server.py └── test ├── index.html ├── jquery.min.js ├── qunit.css ├── qunit.js ├── real-cors-server.html ├── sample.txt ├── server.html ├── test-jquery-plugin.js ├── test-real-cors.js ├── test-request.js └── test-utils.js /README.md: -------------------------------------------------------------------------------- 1 | PostMessage Proxied XMLHttpRequest (PPX) is a simple [polyfill][] that allows browsers without support for cross-origin XMLHttpRequests to do so via postMessage. 2 | 3 | The code has no dependencies and does not require JSON, which makes it about 2KB minified and gzipped. 4 | 5 | A simple jQuery plugin that allows jQuery-based ajax requests to transparently use the polyfill is also available. 6 | 7 | ## Usage 8 | 9 | Suppose you have a website at http://foo.com which exposes a cross-origin REST API that you'd like to access from http://bar.com. 10 | 11 | Create a file at http://foo.com/server.html and put the following code in it: 12 | 13 | ```html 14 | 15 | 16 | PPX Server Frame 17 | 18 | 19 | ``` 20 | 21 | This is the host iframe which will proxy requests for you. 22 | 23 | ### Basic Use 24 | 25 | From a page on bar.com, you can access foo.com like so: 26 | 27 | ```html 28 | 29 | 39 | ``` 40 | 41 | As you can probably guess, `PPX.buildClientConstructor()` returns an object much like `window.XMLHttpRequest`. This can then be used as-is, or given to another third-party library to make cross-origin communication as familiar as a normal ajax request. 42 | 43 | ### Using PPX with jQuery 44 | 45 | The above example can be made simpler using the PPX jQuery plugin: 46 | 47 | ```html 48 | 49 | 50 | 51 | 57 | ``` 58 | 59 | The call `jQuery.proxyAjaxThroughPostMessage()` sets up an [ajax prefilter][] which will automatically proxy requests to foo.com if the host browser doesn't already support CORS. 60 | 61 | ### Using PPX with jQuery and yepnope.js 62 | 63 | You can use PPX with [yepnope.js][] and jQuery, too: 64 | 65 | ```html 66 | 67 | 68 | 81 | ``` 82 | 83 | This will only load PPX's JS code if CORS support isn't detected in the host browser. 84 | 85 | ## Development 86 | 87 | After cloning the git repository and entering its directory, you can start the development server by running: 88 | 89 | python server.py 90 | 91 | This will start two local web servers on ports 9000 and 9001. The functional tests make CORS requests from one to the other to ensure that everything works as expected. 92 | 93 | To start the tests, browse to http://localhost:9000/test/. 94 | 95 | ## Limitations 96 | 97 | Currently, the following features of the [XMLHttpRequest API][] are unsupported: 98 | 99 | * username and password arguments to `open()` 100 | * `getResponseHeader()` (though `getAllResponseHeaders()` is supported) 101 | * `responseXML` 102 | 103 | Several features of the massive [CORS Specification][] are unsupported: 104 | 105 | * Only [simple requests][] can be sent; anything requiring a preflighted request will be rejected for security purposes. 106 | 107 | * Response headers aren't automatically culled down to the [simple response header][] list as prescribed by the spec. 108 | 109 | * Because the `Origin` header can't be set by the same-origin proxied request, PPX sets an `X-Original-Origin` header with the origin of the window making the request. This may be used by servers in place of `Origin`, e.g. to set the appropriate value for `Access-Control-Allow-Origin` in the response. 110 | 111 | * Because the same-origin proxied request can't control whether or not a cookie is transmitted during its request, all cross-origin requests sent should be assumed to have them. Note that we don't currently check the value of `Access-Control-Allow-Credentials` before returning responses, either, so be *very careful* if your site uses cookies. 112 | 113 | ## Similar Projects 114 | 115 | [pmxdr][] provides similar functionality but doesn't provide an XMLHttpRequest API, so it can't necessarily be used as a drop-in replacement. It's also larger than PPX, but supports more features out-of-the-box. 116 | 117 | There's [xdomain](https://github.com/jpillora/xdomain). It doesn't use postMessage. 118 | 119 | ## License 120 | 121 | ``` 122 | The MIT License (MIT) 123 | 124 | Copyright (c) 2011-2014 Atul Varma 125 | 126 | Permission is hereby granted, free of charge, to any person obtaining a copy 127 | of this software and associated documentation files (the "Software"), to deal 128 | in the Software without restriction, including without limitation the rights 129 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 130 | copies of the Software, and to permit persons to whom the Software is 131 | furnished to do so, subject to the following conditions: 132 | 133 | The above copyright notice and this permission notice shall be included in 134 | all copies or substantial portions of the Software. 135 | 136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 137 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 138 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 139 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 140 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 141 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 142 | THE SOFTWARE. 143 | ``` 144 | 145 | [Polyfill]: http://remysharp.com/2010/10/08/what-is-a-polyfill/ 146 | [pmxdr]: https://github.com/eligrey/pmxdr 147 | [XMLHttpRequest API]: http://www.w3.org/TR/XMLHttpRequest/ 148 | [CORS Specification]: http://www.w3.org/TR/cors/ 149 | [simple requests]: https://developer.mozilla.org/En/HTTP_access_control#Simple_requests 150 | [simple response header]: http://www.w3.org/TR/cors/#simple-response-header 151 | [ajax prefilter]: http://api.jquery.com/extending-ajax/#Prefilters 152 | [yepnope.js]: http://yepnopejs.com/ 153 | -------------------------------------------------------------------------------- /ppx.jquery.js: -------------------------------------------------------------------------------- 1 | (function(jQuery) { 2 | jQuery.extend({ 3 | proxyAjaxThroughPostMessage: function(url) { 4 | var Request = PPX.buildClientConstructor(url); 5 | var utils = PPX.utils; 6 | url = utils.absolutifyURL(url); 7 | jQuery.ajaxPrefilter(function(options, originalOptions, jqXHR) { 8 | if (((options.crossDomain && !jQuery.support.cors) || 9 | options.usePostMessage) && 10 | utils.isSameOrigin(url, utils.absolutifyURL(options.url))) { 11 | options.xhr = Request; 12 | options.crossDomain = false; 13 | jqXHR.isProxiedThroughPostMessage = true; 14 | } 15 | }); 16 | } 17 | }); 18 | })(jQuery); 19 | -------------------------------------------------------------------------------- /ppx.js: -------------------------------------------------------------------------------- 1 | var PPX = (function() { 2 | var config = { 3 | requestHeaders: [ 4 | "Accept", 5 | "Content-Type" 6 | ], 7 | requestMethods: [ 8 | "GET", 9 | "POST", 10 | "HEAD" 11 | ], 12 | requestContentTypes: [ 13 | "application/x-www-form-urlencoded", 14 | "multipart/form-data", 15 | "text/plain" 16 | ] 17 | }; 18 | 19 | var utils = { 20 | warn: function warn(msg) { 21 | if (window.console && window.console.warn) 22 | window.console.warn(msg); 23 | }, 24 | absolutifyURL: function absolutifyURL(url) { 25 | var a = document.createElement('a'); 26 | a.setAttribute("href", url); 27 | return a.href; 28 | }, 29 | on: function on(element, event, cb) { 30 | if (element.attachEvent) 31 | element.attachEvent("on" + event, cb); 32 | else 33 | element.addEventListener(event, cb, false); 34 | }, 35 | off: function off(element, event, cb) { 36 | if (element.detachEvent) 37 | element.detachEvent("on" + event, cb); 38 | else 39 | element.removeEventListener(event, cb, false); 40 | }, 41 | encode: function encode(data) { 42 | var parts = []; 43 | for (var name in data) { 44 | parts.push(name + "=" + encodeURIComponent(data[name])); 45 | } 46 | return parts.join("&"); 47 | }, 48 | decode: function decode(string) { 49 | return utils.parseUri("?" + string).queryKey; 50 | }, 51 | isSameOrigin: function isSameOrigin(a, b) { 52 | a = utils.parseUri(a); 53 | b = utils.parseUri(b); 54 | return (a.protocol == b.protocol && a.authority == b.authority); 55 | }, 56 | map: function map(array, cb) { 57 | var result = []; 58 | for (var i = 0; i < array.length; i++) 59 | result.push(cb(array[i])); 60 | return result; 61 | }, 62 | trim: function trim(str) { 63 | return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); 64 | }, 65 | // parseUri 1.2.2 66 | // (c) Steven Levithan 67 | // MIT License 68 | parseUri: function parseUri(str) { 69 | var o = utils.parseUriOptions, 70 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), 71 | uri = {}, 72 | i = 14; 73 | 74 | while (i--) uri[o.key[i]] = m[i] || ""; 75 | 76 | uri[o.q.name] = {}; 77 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { 78 | if ($1) uri[o.q.name][$1] = decodeURIComponent($2); 79 | }); 80 | 81 | return uri; 82 | }, 83 | parseUriOptions: { 84 | strictMode: false, 85 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 86 | q: { 87 | name: "queryKey", 88 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 89 | }, 90 | parser: { 91 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 92 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 93 | } 94 | }, 95 | // Taken from jQuery. 96 | inArray: function inArray(elem, array, i) { 97 | var len; 98 | var indexOf = Array.prototype.indexOf; 99 | 100 | if (array) { 101 | if (indexOf) { 102 | return indexOf.call(array, elem, i); 103 | } 104 | 105 | len = array.length; 106 | i = i ? i < 0 ? Math.max(0, len + i) : i : 0; 107 | 108 | for (;i < len; i++) { 109 | // Skip accessing in sparse arrays 110 | if (i in array && array[i] === elem) { 111 | return i; 112 | } 113 | } 114 | } 115 | 116 | return -1; 117 | } 118 | }; 119 | 120 | function validateRequest(data, access, channel) { 121 | if (!access.allowOrigin) { 122 | channel.error("CORS is unsupported at that path."); 123 | return false; 124 | } 125 | 126 | if (access.allowOrigin != "*" && data.origin != access.allowOrigin) { 127 | channel.error("message from invalid origin: " + data.origin); 128 | return; 129 | } 130 | 131 | return true; 132 | } 133 | 134 | function parseAccessControlHeaders(req) { 135 | return { 136 | allowOrigin: req.getResponseHeader('Access-Control-Allow-Origin'), 137 | }; 138 | } 139 | 140 | function SimpleChannel(other, onMessage, onError) { 141 | function getOtherWindow() { 142 | return other.postMessage ? other : other.contentWindow; 143 | } 144 | 145 | var self = { 146 | onMessage: onMessage, 147 | onError: onError || function defaultOnError(message) { 148 | if (window.console && window.console.error) 149 | window.console.error(message); 150 | }, 151 | destroy: function() { 152 | utils.off(window, "message", messageHandler); 153 | other = null; 154 | onMessage = null; 155 | }, 156 | send: function(data) { 157 | getOtherWindow().postMessage(utils.encode(data), "*"); 158 | }, 159 | error: function(message) { 160 | getOtherWindow().postMessage(utils.encode({ 161 | __simpleChannelError: message 162 | }), "*"); 163 | } 164 | }; 165 | 166 | function messageHandler(event) { 167 | if (event.source != getOtherWindow()) 168 | return; 169 | var data = utils.decode(event.data); 170 | if ('__simpleChannelError' in data) 171 | self.onError(data.__simpleChannelError, event.origin); 172 | else 173 | self.onMessage(data, event.origin); 174 | } 175 | 176 | utils.on(window, "message", messageHandler); 177 | return self; 178 | } 179 | 180 | return { 181 | version: "0.1", 182 | utils: utils, 183 | config: config, 184 | startServer: function startServer(options) { 185 | options = options || {}; 186 | 187 | var otherWindow = options.window || window.parent; 188 | var channel = SimpleChannel(otherWindow, function(data, origin) { 189 | if (data.cmd == "send") { 190 | var req = new XMLHttpRequest(); 191 | data.origin = origin; 192 | data.headers = utils.decode(data.headers); 193 | 194 | if (!utils.isSameOrigin(window.location.href, data.url)) { 195 | channel.error("url does not have same origin: " + data.url); 196 | return; 197 | } 198 | if (utils.inArray(data.method, config.requestMethods) == -1) { 199 | channel.error("not a simple request method: " + data.method); 200 | return; 201 | } 202 | 203 | req.open(data.method, data.url); 204 | req.onreadystatechange = function() { 205 | if (req.readyState == 2) { 206 | var access = parseAccessControlHeaders(req); 207 | if (options.modifyAccessControl) 208 | options.modifyAccessControl(access, data); 209 | if (!validateRequest(data, access, channel)) { 210 | req.abort(); 211 | return; 212 | } 213 | } 214 | channel.send({ 215 | cmd: "readystatechange", 216 | readyState: req.readyState, 217 | status: req.status, 218 | statusText: req.statusText, 219 | responseText: req.responseText, 220 | responseHeaders: req.getAllResponseHeaders() 221 | }); 222 | }; 223 | 224 | var contentType = data.headers['Content-Type']; 225 | if (contentType && 226 | utils.inArray(contentType, config.requestContentTypes) == -1) { 227 | channel.error("invalid content type for a simple request: " + 228 | contentType); 229 | return; 230 | } 231 | 232 | for (var name in data.headers) 233 | if (utils.inArray(name, config.requestHeaders) == -1) { 234 | if (name == 'X-Requested-With') { 235 | /* Just ignore jQuery's X-Requested-With header. */ 236 | } else { 237 | channel.error("header '" + name + "' is not allowed."); 238 | return; 239 | } 240 | } else 241 | req.setRequestHeader(name, data.headers[name]); 242 | 243 | req.setRequestHeader("X-Original-Origin", data.origin); 244 | req.send(data.body || null); 245 | } 246 | }); 247 | channel.send({cmd: "ready"}); 248 | }, 249 | buildClientConstructor: function buildClientConstructor(iframeURL) { 250 | return function PostMessageProxiedXMLHttpRequest() { 251 | var method; 252 | var url; 253 | var channel; 254 | var iframe; 255 | var headers = {}; 256 | var responseHeaders = ""; 257 | 258 | function cleanup() { 259 | if (channel) { 260 | channel.destroy(); 261 | channel = null; 262 | } 263 | if (iframe) { 264 | document.body.removeChild(iframe); 265 | iframe = null; 266 | } 267 | } 268 | 269 | var self = { 270 | UNSENT: 0, 271 | OPENED: 1, 272 | HEADERS_RECEIVED: 2, 273 | LOADING: 3, 274 | DONE: 4, 275 | readyState: 0, 276 | status: 0, 277 | statusText: "", 278 | responseText: "", 279 | open: function(aMethod, aUrl) { 280 | method = aMethod; 281 | url = aUrl; 282 | self.readyState = self.OPENED; 283 | if (self.onreadystatechange) 284 | self.onreadystatechange(); 285 | }, 286 | setRequestHeader: function(name, value) { 287 | headers[name] = value; 288 | }, 289 | getAllResponseHeaders: function() { 290 | return responseHeaders; 291 | }, 292 | abort: function() { 293 | if (iframe) { 294 | cleanup(); 295 | self.readyState = self.DONE; 296 | if (self.onreadystatechange) 297 | self.onreadystatechange(); 298 | self.readyState = self.UNSENT; 299 | } 300 | }, 301 | send: function(body) { 302 | if (self.readyState == self.UNSENT) 303 | throw new Error("request not initialized"); 304 | 305 | iframe = document.createElement("iframe"); 306 | channel = SimpleChannel(iframe, function(data) { 307 | switch (data.cmd) { 308 | case "ready": 309 | channel.send({ 310 | cmd: "send", 311 | method: method, 312 | url: utils.absolutifyURL(url), 313 | headers: utils.encode(headers), 314 | body: body || "" 315 | }); 316 | break; 317 | 318 | case "readystatechange": 319 | self.readyState = parseInt(data.readyState); 320 | self.status = parseInt(data.status); 321 | self.statusText = data.statusText; 322 | self.responseText = data.responseText; 323 | responseHeaders = data.responseHeaders; 324 | if (self.readyState == 4) 325 | cleanup(); 326 | if (self.onreadystatechange) 327 | self.onreadystatechange(); 328 | break; 329 | } 330 | }, function onError(message) { 331 | utils.warn(message); 332 | self.responseText = message; 333 | self.readyState = self.DONE; 334 | cleanup(); 335 | if (self.onreadystatechange) 336 | self.onreadystatechange(); 337 | }); 338 | 339 | iframe.setAttribute("src", utils.absolutifyURL(iframeURL)); 340 | iframe.style.display = "none"; 341 | document.body.appendChild(iframe); 342 | } 343 | }; 344 | 345 | return self; 346 | }; 347 | } 348 | }; 349 | })(); 350 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mimetypes 3 | import traceback 4 | from wsgiref.simple_server import make_server 5 | from wsgiref.util import FileWrapper 6 | 7 | ROOT = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | mimetypes.add_type('application/x-font-woff', '.woff') 10 | 11 | def simple_response(start, contents, code='200 OK', mimetype='text/plain'): 12 | start(code, [('Content-Type', mimetype), 13 | ('Content-Length', str(len(contents)))]) 14 | return [contents] 15 | 16 | def handle_request(env, start, handlers): 17 | try: 18 | for handler in handlers: 19 | response = handler(env, start) 20 | if response is not None: 21 | return response 22 | return simple_response(start, "Not Found: %s" % env['PATH_INFO'], 23 | code='404 Not Found') 24 | except Exception: 25 | msg = "500 INTERNAL SERVER ERROR\n\n%s" % traceback.format_exc() 26 | return simple_response(start, msg, code='500 Internal Server Error') 27 | 28 | class BasicFileServer(object): 29 | def __init__(self, static_files_dir): 30 | self.ext_handlers = {} 31 | self.default_filenames = ['index.html'] 32 | self.static_files_dir = static_files_dir 33 | 34 | def try_loading(self, filename, env, start): 35 | static_files_dir = self.static_files_dir 36 | fileparts = filename[1:].split('/') 37 | fullpath = os.path.join(static_files_dir, *fileparts) 38 | fullpath = os.path.normpath(fullpath) 39 | if (fullpath.startswith(static_files_dir) and 40 | not fullpath.startswith('.')): 41 | if os.path.isfile(fullpath): 42 | ext = os.path.splitext(fullpath)[1] 43 | handler = self.ext_handlers.get(ext) 44 | if handler: 45 | mimetype, contents = handler(env, static_files_dir, fullpath) 46 | return simple_response(start, contents, mimetype=mimetype) 47 | (mimetype, encoding) = mimetypes.guess_type(fullpath) 48 | if mimetype: 49 | filesize = os.stat(fullpath).st_size 50 | start('200 OK', [('Content-Type', mimetype), 51 | ('Content-Length', str(filesize))]) 52 | return FileWrapper(open(fullpath, 'rb')) 53 | elif os.path.isdir(fullpath) and not filename.endswith('/'): 54 | start('302 Found', [('Location', env['SCRIPT_NAME'] + 55 | filename + '/')]) 56 | return [] 57 | return None 58 | 59 | def handle_request(self, env, start): 60 | filename = env['PATH_INFO'] 61 | 62 | if filename.endswith('/'): 63 | for index in self.default_filenames: 64 | result = self.try_loading(filename + index, env, start) 65 | if result is not None: 66 | return result 67 | return self.try_loading(filename, env, start) 68 | 69 | def cors_handler(env, start): 70 | def response(contents, origin=None): 71 | final_headers = [ 72 | ('Content-Type', 'text/plain'), 73 | ('Content-Length', str(len(contents))) 74 | ] 75 | if origin: 76 | final_headers.append(('Access-Control-Allow-Origin', origin)) 77 | start('200 OK', final_headers) 78 | return [contents] 79 | 80 | if env['PATH_INFO'] == '/cors/origin-only-me': 81 | origin = (env.get('HTTP_ORIGIN') or 82 | env.get('HTTP_X_ORIGINAL_ORIGIN')) 83 | return response('hai2u', origin=origin) 84 | 85 | if env['PATH_INFO'] == '/cors/origin-all': 86 | return response('hai2u', origin='*') 87 | 88 | if env['PATH_INFO'] == '/cors/origin-all/post': 89 | length = env.get('CONTENT_LENGTH', '') 90 | data = 'nothing' 91 | if length: 92 | data = env['wsgi.input'].read(int(length)) 93 | return response('received ' + data, origin='*') 94 | 95 | if env['PATH_INFO'] == '/cors/origin-foo.com': 96 | return response('hai2u', origin='http://foo.com') 97 | 98 | return None 99 | 100 | def run_cors_server(port, static_files_dir): 101 | file_server = BasicFileServer(static_files_dir) 102 | handlers = [cors_handler, file_server.handle_request] 103 | 104 | def application(env, start): 105 | return handle_request(env, start, handlers=handlers) 106 | 107 | httpd = make_server('', port, application) 108 | httpd.serve_forever() 109 | 110 | def run_server(port, static_files_dir): 111 | import threading 112 | 113 | s1 = threading.Thread(target=run_cors_server, args=(port+1, ROOT)) 114 | s1.setDaemon(True) 115 | s1.start() 116 | 117 | file_server = BasicFileServer(static_files_dir) 118 | handlers = [file_server.handle_request] 119 | 120 | def application(env, start): 121 | return handle_request(env, start, handlers=handlers) 122 | 123 | httpd = make_server('', port, application) 124 | 125 | url = "http://127.0.0.1:%s/" % port 126 | print "development server started at %s" % url 127 | print "press CTRL-C to stop it" 128 | 129 | httpd.serve_forever() 130 | 131 | if __name__ == '__main__': 132 | run_server(9000, ROOT) 133 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PostMessageProxiedXHR Unit Tests 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

PostMessageProxiedXHR Unit Tests

14 |

15 |
16 |

17 |
    18 |
    19 | -------------------------------------------------------------------------------- /test/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.7.1 jquery.com | jquery.org/license */ 2 | (function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
    "+""+"
    ",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
    t
    ",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
    ",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; 3 | f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() 4 | {for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); -------------------------------------------------------------------------------- /test/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | white-space: pre; 190 | } 191 | 192 | #qunit-tests > li:last-child { 193 | border-radius: 0 0 15px 15px; 194 | -moz-border-radius: 0 0 15px 15px; 195 | -webkit-border-bottom-right-radius: 15px; 196 | -webkit-border-bottom-left-radius: 15px; 197 | } 198 | 199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 200 | #qunit-tests .fail .test-name, 201 | #qunit-tests .fail .module-name { color: #000000; } 202 | 203 | #qunit-tests .fail .test-actual { color: #EE5757; } 204 | #qunit-tests .fail .test-expected { color: green; } 205 | 206 | #qunit-banner.qunit-fail { background-color: #EE5757; } 207 | 208 | 209 | /** Result */ 210 | 211 | #qunit-testresult { 212 | padding: 0.5em 0.5em 0.5em 2.5em; 213 | 214 | color: #2b81af; 215 | background-color: #D2E0E6; 216 | 217 | border-bottom: 1px solid white; 218 | } 219 | 220 | /** Fixture */ 221 | 222 | #qunit-fixture { 223 | position: absolute; 224 | top: -10000px; 225 | left: -10000px; 226 | } 227 | -------------------------------------------------------------------------------- /test/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e) { 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild( li ); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if ( config.previousModule ) { 51 | runLoggingCallbacks('moduleDone', QUnit, { 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | } ); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { all: 0, bad: 0 }; 60 | runLoggingCallbacks( 'moduleStart', QUnit, { 61 | name: this.module 62 | } ); 63 | } 64 | 65 | config.current = this; 66 | this.testEnvironment = extend({ 67 | setup: function() {}, 68 | teardown: function() {} 69 | }, this.moduleTestEnvironment); 70 | if (this.testEnvironmentArg) { 71 | extend(this.testEnvironment, this.testEnvironmentArg); 72 | } 73 | 74 | runLoggingCallbacks( 'testStart', QUnit, { 75 | name: this.testName, 76 | module: this.module 77 | }); 78 | 79 | // allow utility functions to access the current test environment 80 | // TODO why?? 81 | QUnit.current_testEnvironment = this.testEnvironment; 82 | 83 | try { 84 | if ( !config.pollution ) { 85 | saveGlobal(); 86 | } 87 | 88 | this.testEnvironment.setup.call(this.testEnvironment); 89 | } catch(e) { 90 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 91 | } 92 | }, 93 | run: function() { 94 | if ( this.async ) { 95 | QUnit.stop(); 96 | } 97 | 98 | if ( config.notrycatch ) { 99 | this.callback.call(this.testEnvironment); 100 | return; 101 | } 102 | try { 103 | this.callback.call(this.testEnvironment); 104 | } catch(e) { 105 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 106 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 107 | // else next test will carry the responsibility 108 | saveGlobal(); 109 | 110 | // Restart the tests if they're blocking 111 | if ( config.blocking ) { 112 | start(); 113 | } 114 | } 115 | }, 116 | teardown: function() { 117 | try { 118 | this.testEnvironment.teardown.call(this.testEnvironment); 119 | checkPollution(); 120 | } catch(e) { 121 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 122 | } 123 | }, 124 | finish: function() { 125 | if ( this.expected && this.expected != this.assertions.length ) { 126 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 127 | } 128 | 129 | var good = 0, bad = 0, 130 | tests = id("qunit-tests"); 131 | 132 | config.stats.all += this.assertions.length; 133 | config.moduleStats.all += this.assertions.length; 134 | 135 | if ( tests ) { 136 | var ol = document.createElement("ol"); 137 | 138 | for ( var i = 0; i < this.assertions.length; i++ ) { 139 | var assertion = this.assertions[i]; 140 | 141 | var li = document.createElement("li"); 142 | li.className = assertion.result ? "pass" : "fail"; 143 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 144 | ol.appendChild( li ); 145 | 146 | if ( assertion.result ) { 147 | good++; 148 | } else { 149 | bad++; 150 | config.stats.bad++; 151 | config.moduleStats.bad++; 152 | } 153 | } 154 | 155 | // store result when possible 156 | if ( QUnit.config.reorder && defined.sessionStorage ) { 157 | if (bad) { 158 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 159 | } else { 160 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 161 | } 162 | } 163 | 164 | if (bad == 0) { 165 | ol.style.display = "none"; 166 | } 167 | 168 | var b = document.createElement("strong"); 169 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 170 | 171 | var a = document.createElement("a"); 172 | a.innerHTML = "Rerun"; 173 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 174 | 175 | addEvent(b, "click", function() { 176 | var next = b.nextSibling.nextSibling, 177 | display = next.style.display; 178 | next.style.display = display === "none" ? "block" : "none"; 179 | }); 180 | 181 | addEvent(b, "dblclick", function(e) { 182 | var target = e && e.target ? e.target : window.event.srcElement; 183 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 184 | target = target.parentNode; 185 | } 186 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 187 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 188 | } 189 | }); 190 | 191 | var li = id(this.id); 192 | li.className = bad ? "fail" : "pass"; 193 | li.removeChild( li.firstChild ); 194 | li.appendChild( b ); 195 | li.appendChild( a ); 196 | li.appendChild( ol ); 197 | 198 | } else { 199 | for ( var i = 0; i < this.assertions.length; i++ ) { 200 | if ( !this.assertions[i].result ) { 201 | bad++; 202 | config.stats.bad++; 203 | config.moduleStats.bad++; 204 | } 205 | } 206 | } 207 | 208 | try { 209 | QUnit.reset(); 210 | } catch(e) { 211 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 212 | } 213 | 214 | runLoggingCallbacks( 'testDone', QUnit, { 215 | name: this.testName, 216 | module: this.module, 217 | failed: bad, 218 | passed: this.assertions.length - bad, 219 | total: this.assertions.length 220 | } ); 221 | }, 222 | 223 | queue: function() { 224 | var test = this; 225 | synchronize(function() { 226 | test.init(); 227 | }); 228 | function run() { 229 | // each of these can by async 230 | synchronize(function() { 231 | test.setup(); 232 | }); 233 | synchronize(function() { 234 | test.run(); 235 | }); 236 | synchronize(function() { 237 | test.teardown(); 238 | }); 239 | synchronize(function() { 240 | test.finish(); 241 | }); 242 | } 243 | // defer when previous test run passed, if storage is available 244 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 245 | if (bad) { 246 | run(); 247 | } else { 248 | synchronize(run); 249 | }; 250 | } 251 | 252 | }; 253 | 254 | var QUnit = { 255 | 256 | // call on start of module test to prepend name to all tests 257 | module: function(name, testEnvironment) { 258 | config.currentModule = name; 259 | config.currentModuleTestEnviroment = testEnvironment; 260 | }, 261 | 262 | asyncTest: function(testName, expected, callback) { 263 | if ( arguments.length === 2 ) { 264 | callback = expected; 265 | expected = 0; 266 | } 267 | 268 | QUnit.test(testName, expected, callback, true); 269 | }, 270 | 271 | test: function(testName, expected, callback, async) { 272 | var name = '' + testName + '', testEnvironmentArg; 273 | 274 | if ( arguments.length === 2 ) { 275 | callback = expected; 276 | expected = null; 277 | } 278 | // is 2nd argument a testEnvironment? 279 | if ( expected && typeof expected === 'object') { 280 | testEnvironmentArg = expected; 281 | expected = null; 282 | } 283 | 284 | if ( config.currentModule ) { 285 | name = '' + config.currentModule + ": " + name; 286 | } 287 | 288 | if ( !validTest(config.currentModule + ": " + testName) ) { 289 | return; 290 | } 291 | 292 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 293 | test.module = config.currentModule; 294 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 295 | test.queue(); 296 | }, 297 | 298 | /** 299 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 300 | */ 301 | expect: function(asserts) { 302 | config.current.expected = asserts; 303 | }, 304 | 305 | /** 306 | * Asserts true. 307 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 308 | */ 309 | ok: function(a, msg) { 310 | a = !!a; 311 | var details = { 312 | result: a, 313 | message: msg 314 | }; 315 | msg = escapeInnerText(msg); 316 | runLoggingCallbacks( 'log', QUnit, details ); 317 | config.current.assertions.push({ 318 | result: a, 319 | message: msg 320 | }); 321 | }, 322 | 323 | /** 324 | * Checks that the first two arguments are equal, with an optional message. 325 | * Prints out both actual and expected values. 326 | * 327 | * Prefered to ok( actual == expected, message ) 328 | * 329 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 330 | * 331 | * @param Object actual 332 | * @param Object expected 333 | * @param String message (optional) 334 | */ 335 | equal: function(actual, expected, message) { 336 | QUnit.push(expected == actual, actual, expected, message); 337 | }, 338 | 339 | notEqual: function(actual, expected, message) { 340 | QUnit.push(expected != actual, actual, expected, message); 341 | }, 342 | 343 | deepEqual: function(actual, expected, message) { 344 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 345 | }, 346 | 347 | notDeepEqual: function(actual, expected, message) { 348 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 349 | }, 350 | 351 | strictEqual: function(actual, expected, message) { 352 | QUnit.push(expected === actual, actual, expected, message); 353 | }, 354 | 355 | notStrictEqual: function(actual, expected, message) { 356 | QUnit.push(expected !== actual, actual, expected, message); 357 | }, 358 | 359 | raises: function(block, expected, message) { 360 | var actual, ok = false; 361 | 362 | if (typeof expected === 'string') { 363 | message = expected; 364 | expected = null; 365 | } 366 | 367 | try { 368 | block(); 369 | } catch (e) { 370 | actual = e; 371 | } 372 | 373 | if (actual) { 374 | // we don't want to validate thrown error 375 | if (!expected) { 376 | ok = true; 377 | // expected is a regexp 378 | } else if (QUnit.objectType(expected) === "regexp") { 379 | ok = expected.test(actual); 380 | // expected is a constructor 381 | } else if (actual instanceof expected) { 382 | ok = true; 383 | // expected is a validation function which returns true is validation passed 384 | } else if (expected.call({}, actual) === true) { 385 | ok = true; 386 | } 387 | } 388 | 389 | QUnit.ok(ok, message); 390 | }, 391 | 392 | start: function() { 393 | config.semaphore--; 394 | if (config.semaphore > 0) { 395 | // don't start until equal number of stop-calls 396 | return; 397 | } 398 | if (config.semaphore < 0) { 399 | // ignore if start is called more often then stop 400 | config.semaphore = 0; 401 | } 402 | // A slight delay, to avoid any current callbacks 403 | if ( defined.setTimeout ) { 404 | window.setTimeout(function() { 405 | if (config.semaphore > 0) { 406 | return; 407 | } 408 | if ( config.timeout ) { 409 | clearTimeout(config.timeout); 410 | } 411 | 412 | config.blocking = false; 413 | process(); 414 | }, 13); 415 | } else { 416 | config.blocking = false; 417 | process(); 418 | } 419 | }, 420 | 421 | stop: function(timeout) { 422 | config.semaphore++; 423 | config.blocking = true; 424 | 425 | if ( timeout && defined.setTimeout ) { 426 | clearTimeout(config.timeout); 427 | config.timeout = window.setTimeout(function() { 428 | QUnit.ok( false, "Test timed out" ); 429 | QUnit.start(); 430 | }, timeout); 431 | } 432 | } 433 | }; 434 | 435 | //We want access to the constructor's prototype 436 | (function() { 437 | function F(){}; 438 | F.prototype = QUnit; 439 | QUnit = new F(); 440 | //Make F QUnit's constructor so that we can add to the prototype later 441 | QUnit.constructor = F; 442 | })(); 443 | 444 | // Backwards compatibility, deprecated 445 | QUnit.equals = QUnit.equal; 446 | QUnit.same = QUnit.deepEqual; 447 | 448 | // Maintain internal state 449 | var config = { 450 | // The queue of tests to run 451 | queue: [], 452 | 453 | // block until document ready 454 | blocking: true, 455 | 456 | // when enabled, show only failing tests 457 | // gets persisted through sessionStorage and can be changed in UI via checkbox 458 | hidepassed: false, 459 | 460 | // by default, run previously failed tests first 461 | // very useful in combination with "Hide passed tests" checked 462 | reorder: true, 463 | 464 | // by default, modify document.title when suite is done 465 | altertitle: true, 466 | 467 | urlConfig: ['noglobals', 'notrycatch'], 468 | 469 | //logging callback queues 470 | begin: [], 471 | done: [], 472 | log: [], 473 | testStart: [], 474 | testDone: [], 475 | moduleStart: [], 476 | moduleDone: [] 477 | }; 478 | 479 | // Load paramaters 480 | (function() { 481 | var location = window.location || { search: "", protocol: "file:" }, 482 | params = location.search.slice( 1 ).split( "&" ), 483 | length = params.length, 484 | urlParams = {}, 485 | current; 486 | 487 | if ( params[ 0 ] ) { 488 | for ( var i = 0; i < length; i++ ) { 489 | current = params[ i ].split( "=" ); 490 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 491 | // allow just a key to turn on a flag, e.g., test.html?noglobals 492 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 493 | urlParams[ current[ 0 ] ] = current[ 1 ]; 494 | } 495 | } 496 | 497 | QUnit.urlParams = urlParams; 498 | config.filter = urlParams.filter; 499 | 500 | // Figure out if we're running the tests from a server or not 501 | QUnit.isLocal = !!(location.protocol === 'file:'); 502 | })(); 503 | 504 | // Expose the API as global variables, unless an 'exports' 505 | // object exists, in that case we assume we're in CommonJS 506 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 507 | extend(window, QUnit); 508 | window.QUnit = QUnit; 509 | } else { 510 | extend(exports, QUnit); 511 | exports.QUnit = QUnit; 512 | } 513 | 514 | // define these after exposing globals to keep them in these QUnit namespace only 515 | extend(QUnit, { 516 | config: config, 517 | 518 | // Initialize the configuration options 519 | init: function() { 520 | extend(config, { 521 | stats: { all: 0, bad: 0 }, 522 | moduleStats: { all: 0, bad: 0 }, 523 | started: +new Date, 524 | updateRate: 1000, 525 | blocking: false, 526 | autostart: true, 527 | autorun: false, 528 | filter: "", 529 | queue: [], 530 | semaphore: 0 531 | }); 532 | 533 | var tests = id( "qunit-tests" ), 534 | banner = id( "qunit-banner" ), 535 | result = id( "qunit-testresult" ); 536 | 537 | if ( tests ) { 538 | tests.innerHTML = ""; 539 | } 540 | 541 | if ( banner ) { 542 | banner.className = ""; 543 | } 544 | 545 | if ( result ) { 546 | result.parentNode.removeChild( result ); 547 | } 548 | 549 | if ( tests ) { 550 | result = document.createElement( "p" ); 551 | result.id = "qunit-testresult"; 552 | result.className = "result"; 553 | tests.parentNode.insertBefore( result, tests ); 554 | result.innerHTML = 'Running...
     '; 555 | } 556 | }, 557 | 558 | /** 559 | * Resets the test setup. Useful for tests that modify the DOM. 560 | * 561 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 562 | */ 563 | reset: function() { 564 | if ( window.jQuery ) { 565 | jQuery( "#qunit-fixture" ).html( config.fixture ); 566 | } else { 567 | var main = id( 'qunit-fixture' ); 568 | if ( main ) { 569 | main.innerHTML = config.fixture; 570 | } 571 | } 572 | }, 573 | 574 | /** 575 | * Trigger an event on an element. 576 | * 577 | * @example triggerEvent( document.body, "click" ); 578 | * 579 | * @param DOMElement elem 580 | * @param String type 581 | */ 582 | triggerEvent: function( elem, type, event ) { 583 | if ( document.createEvent ) { 584 | event = document.createEvent("MouseEvents"); 585 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 586 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 587 | elem.dispatchEvent( event ); 588 | 589 | } else if ( elem.fireEvent ) { 590 | elem.fireEvent("on"+type); 591 | } 592 | }, 593 | 594 | // Safe object type checking 595 | is: function( type, obj ) { 596 | return QUnit.objectType( obj ) == type; 597 | }, 598 | 599 | objectType: function( obj ) { 600 | if (typeof obj === "undefined") { 601 | return "undefined"; 602 | 603 | // consider: typeof null === object 604 | } 605 | if (obj === null) { 606 | return "null"; 607 | } 608 | 609 | var type = Object.prototype.toString.call( obj ) 610 | .match(/^\[object\s(.*)\]$/)[1] || ''; 611 | 612 | switch (type) { 613 | case 'Number': 614 | if (isNaN(obj)) { 615 | return "nan"; 616 | } else { 617 | return "number"; 618 | } 619 | case 'String': 620 | case 'Boolean': 621 | case 'Array': 622 | case 'Date': 623 | case 'RegExp': 624 | case 'Function': 625 | return type.toLowerCase(); 626 | } 627 | if (typeof obj === "object") { 628 | return "object"; 629 | } 630 | return undefined; 631 | }, 632 | 633 | push: function(result, actual, expected, message) { 634 | var details = { 635 | result: result, 636 | message: message, 637 | actual: actual, 638 | expected: expected 639 | }; 640 | 641 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 642 | message = '' + message + ""; 643 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 644 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 645 | var output = message + '
    '; 646 | if (actual != expected) { 647 | output += ''; 648 | output += ''; 649 | } 650 | if (!result) { 651 | var source = sourceFromStacktrace(); 652 | if (source) { 653 | details.source = source; 654 | output += ''; 655 | } 656 | } 657 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 658 | 659 | runLoggingCallbacks( 'log', QUnit, details ); 660 | 661 | config.current.assertions.push({ 662 | result: !!result, 663 | message: output 664 | }); 665 | }, 666 | 667 | url: function( params ) { 668 | params = extend( extend( {}, QUnit.urlParams ), params ); 669 | var querystring = "?", 670 | key; 671 | for ( key in params ) { 672 | querystring += encodeURIComponent( key ) + "=" + 673 | encodeURIComponent( params[ key ] ) + "&"; 674 | } 675 | return window.location.pathname + querystring.slice( 0, -1 ); 676 | }, 677 | 678 | extend: extend, 679 | id: id, 680 | addEvent: addEvent 681 | }); 682 | 683 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 684 | //Doing this allows us to tell if the following methods have been overwritten on the actual 685 | //QUnit object, which is a deprecated way of using the callbacks. 686 | extend(QUnit.constructor.prototype, { 687 | // Logging callbacks; all receive a single argument with the listed properties 688 | // run test/logs.html for any related changes 689 | begin: registerLoggingCallback('begin'), 690 | // done: { failed, passed, total, runtime } 691 | done: registerLoggingCallback('done'), 692 | // log: { result, actual, expected, message } 693 | log: registerLoggingCallback('log'), 694 | // testStart: { name } 695 | testStart: registerLoggingCallback('testStart'), 696 | // testDone: { name, failed, passed, total } 697 | testDone: registerLoggingCallback('testDone'), 698 | // moduleStart: { name } 699 | moduleStart: registerLoggingCallback('moduleStart'), 700 | // moduleDone: { name, failed, passed, total } 701 | moduleDone: registerLoggingCallback('moduleDone') 702 | }); 703 | 704 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 705 | config.autorun = true; 706 | } 707 | 708 | QUnit.load = function() { 709 | runLoggingCallbacks( 'begin', QUnit, {} ); 710 | 711 | // Initialize the config, saving the execution queue 712 | var oldconfig = extend({}, config); 713 | QUnit.init(); 714 | extend(config, oldconfig); 715 | 716 | config.blocking = false; 717 | 718 | var urlConfigHtml = '', len = config.urlConfig.length; 719 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 720 | config[val] = QUnit.urlParams[val]; 721 | urlConfigHtml += ''; 722 | } 723 | 724 | var userAgent = id("qunit-userAgent"); 725 | if ( userAgent ) { 726 | userAgent.innerHTML = navigator.userAgent; 727 | } 728 | var banner = id("qunit-header"); 729 | if ( banner ) { 730 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 731 | addEvent( banner, "change", function( event ) { 732 | var params = {}; 733 | params[ event.target.name ] = event.target.checked ? true : undefined; 734 | window.location = QUnit.url( params ); 735 | }); 736 | } 737 | 738 | var toolbar = id("qunit-testrunner-toolbar"); 739 | if ( toolbar ) { 740 | var filter = document.createElement("input"); 741 | filter.type = "checkbox"; 742 | filter.id = "qunit-filter-pass"; 743 | addEvent( filter, "click", function() { 744 | var ol = document.getElementById("qunit-tests"); 745 | if ( filter.checked ) { 746 | ol.className = ol.className + " hidepass"; 747 | } else { 748 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 749 | ol.className = tmp.replace(/ hidepass /, " "); 750 | } 751 | if ( defined.sessionStorage ) { 752 | if (filter.checked) { 753 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 754 | } else { 755 | sessionStorage.removeItem("qunit-filter-passed-tests"); 756 | } 757 | } 758 | }); 759 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 760 | filter.checked = true; 761 | var ol = document.getElementById("qunit-tests"); 762 | ol.className = ol.className + " hidepass"; 763 | } 764 | toolbar.appendChild( filter ); 765 | 766 | var label = document.createElement("label"); 767 | label.setAttribute("for", "qunit-filter-pass"); 768 | label.innerHTML = "Hide passed tests"; 769 | toolbar.appendChild( label ); 770 | } 771 | 772 | var main = id('qunit-fixture'); 773 | if ( main ) { 774 | config.fixture = main.innerHTML; 775 | } 776 | 777 | if (config.autostart) { 778 | QUnit.start(); 779 | } 780 | }; 781 | 782 | addEvent(window, "load", QUnit.load); 783 | 784 | function done() { 785 | config.autorun = true; 786 | 787 | // Log the last module results 788 | if ( config.currentModule ) { 789 | runLoggingCallbacks( 'moduleDone', QUnit, { 790 | name: config.currentModule, 791 | failed: config.moduleStats.bad, 792 | passed: config.moduleStats.all - config.moduleStats.bad, 793 | total: config.moduleStats.all 794 | } ); 795 | } 796 | 797 | var banner = id("qunit-banner"), 798 | tests = id("qunit-tests"), 799 | runtime = +new Date - config.started, 800 | passed = config.stats.all - config.stats.bad, 801 | html = [ 802 | 'Tests completed in ', 803 | runtime, 804 | ' milliseconds.
    ', 805 | '', 806 | passed, 807 | ' tests of ', 808 | config.stats.all, 809 | ' passed, ', 810 | config.stats.bad, 811 | ' failed.' 812 | ].join(''); 813 | 814 | if ( banner ) { 815 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 816 | } 817 | 818 | if ( tests ) { 819 | id( "qunit-testresult" ).innerHTML = html; 820 | } 821 | 822 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 823 | // show ✖ for good, ✔ for bad suite result in title 824 | // use escape sequences in case file gets loaded with non-utf-8-charset 825 | document.title = [ 826 | (config.stats.bad ? "\u2716" : "\u2714"), 827 | document.title.replace(/^[\u2714\u2716] /i, "") 828 | ].join(" "); 829 | } 830 | 831 | runLoggingCallbacks( 'done', QUnit, { 832 | failed: config.stats.bad, 833 | passed: passed, 834 | total: config.stats.all, 835 | runtime: runtime 836 | } ); 837 | } 838 | 839 | function validTest( name ) { 840 | var filter = config.filter, 841 | run = false; 842 | 843 | if ( !filter ) { 844 | return true; 845 | } 846 | 847 | var not = filter.charAt( 0 ) === "!"; 848 | if ( not ) { 849 | filter = filter.slice( 1 ); 850 | } 851 | 852 | if ( name.indexOf( filter ) !== -1 ) { 853 | return !not; 854 | } 855 | 856 | if ( not ) { 857 | run = true; 858 | } 859 | 860 | return run; 861 | } 862 | 863 | // so far supports only Firefox, Chrome and Opera (buggy) 864 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 865 | function sourceFromStacktrace() { 866 | try { 867 | throw new Error(); 868 | } catch ( e ) { 869 | if (e.stacktrace) { 870 | // Opera 871 | return e.stacktrace.split("\n")[6]; 872 | } else if (e.stack) { 873 | // Firefox, Chrome 874 | return e.stack.split("\n")[4]; 875 | } else if (e.sourceURL) { 876 | // Safari, PhantomJS 877 | // TODO sourceURL points at the 'throw new Error' line above, useless 878 | //return e.sourceURL + ":" + e.line; 879 | } 880 | } 881 | } 882 | 883 | function escapeInnerText(s) { 884 | if (!s) { 885 | return ""; 886 | } 887 | s = s + ""; 888 | return s.replace(/[\&<>]/g, function(s) { 889 | switch(s) { 890 | case "&": return "&"; 891 | case "<": return "<"; 892 | case ">": return ">"; 893 | default: return s; 894 | } 895 | }); 896 | } 897 | 898 | function synchronize( callback ) { 899 | config.queue.push( callback ); 900 | 901 | if ( config.autorun && !config.blocking ) { 902 | process(); 903 | } 904 | } 905 | 906 | function process() { 907 | var start = (new Date()).getTime(); 908 | 909 | while ( config.queue.length && !config.blocking ) { 910 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 911 | config.queue.shift()(); 912 | } else { 913 | window.setTimeout( process, 13 ); 914 | break; 915 | } 916 | } 917 | if (!config.blocking && !config.queue.length) { 918 | done(); 919 | } 920 | } 921 | 922 | function saveGlobal() { 923 | config.pollution = []; 924 | 925 | if ( config.noglobals ) { 926 | for ( var key in window ) { 927 | config.pollution.push( key ); 928 | } 929 | } 930 | } 931 | 932 | function checkPollution( name ) { 933 | var old = config.pollution; 934 | saveGlobal(); 935 | 936 | var newGlobals = diff( config.pollution, old ); 937 | if ( newGlobals.length > 0 ) { 938 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 939 | } 940 | 941 | var deletedGlobals = diff( old, config.pollution ); 942 | if ( deletedGlobals.length > 0 ) { 943 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 944 | } 945 | } 946 | 947 | // returns a new Array with the elements that are in a but not in b 948 | function diff( a, b ) { 949 | var result = a.slice(); 950 | for ( var i = 0; i < result.length; i++ ) { 951 | for ( var j = 0; j < b.length; j++ ) { 952 | if ( result[i] === b[j] ) { 953 | result.splice(i, 1); 954 | i--; 955 | break; 956 | } 957 | } 958 | } 959 | return result; 960 | } 961 | 962 | function fail(message, exception, callback) { 963 | if ( typeof console !== "undefined" && console.error && console.warn ) { 964 | console.error(message); 965 | console.error(exception); 966 | console.warn(callback.toString()); 967 | 968 | } else if ( window.opera && opera.postError ) { 969 | opera.postError(message, exception, callback.toString); 970 | } 971 | } 972 | 973 | function extend(a, b) { 974 | for ( var prop in b ) { 975 | if ( b[prop] === undefined ) { 976 | delete a[prop]; 977 | } else { 978 | a[prop] = b[prop]; 979 | } 980 | } 981 | 982 | return a; 983 | } 984 | 985 | function addEvent(elem, type, fn) { 986 | if ( elem.addEventListener ) { 987 | elem.addEventListener( type, fn, false ); 988 | } else if ( elem.attachEvent ) { 989 | elem.attachEvent( "on" + type, fn ); 990 | } else { 991 | fn(); 992 | } 993 | } 994 | 995 | function id(name) { 996 | return !!(typeof document !== "undefined" && document && document.getElementById) && 997 | document.getElementById( name ); 998 | } 999 | 1000 | function registerLoggingCallback(key){ 1001 | return function(callback){ 1002 | config[key].push( callback ); 1003 | }; 1004 | } 1005 | 1006 | // Supports deprecated method of completely overwriting logging callbacks 1007 | function runLoggingCallbacks(key, scope, args) { 1008 | //debugger; 1009 | var callbacks; 1010 | if ( QUnit.hasOwnProperty(key) ) { 1011 | QUnit[key].call(scope, args); 1012 | } else { 1013 | callbacks = config[key]; 1014 | for( var i = 0; i < callbacks.length; i++ ) { 1015 | callbacks[i].call( scope, args ); 1016 | } 1017 | } 1018 | } 1019 | 1020 | // Test for equality any JavaScript type. 1021 | // Author: Philippe Rathé 1022 | QUnit.equiv = function () { 1023 | 1024 | var innerEquiv; // the real equiv function 1025 | var callers = []; // stack to decide between skip/abort functions 1026 | var parents = []; // stack to avoiding loops from circular referencing 1027 | 1028 | // Call the o related callback with the given arguments. 1029 | function bindCallbacks(o, callbacks, args) { 1030 | var prop = QUnit.objectType(o); 1031 | if (prop) { 1032 | if (QUnit.objectType(callbacks[prop]) === "function") { 1033 | return callbacks[prop].apply(callbacks, args); 1034 | } else { 1035 | return callbacks[prop]; // or undefined 1036 | } 1037 | } 1038 | } 1039 | 1040 | var callbacks = function () { 1041 | 1042 | // for string, boolean, number and null 1043 | function useStrictEquality(b, a) { 1044 | if (b instanceof a.constructor || a instanceof b.constructor) { 1045 | // to catch short annotaion VS 'new' annotation of a 1046 | // declaration 1047 | // e.g. var i = 1; 1048 | // var j = new Number(1); 1049 | return a == b; 1050 | } else { 1051 | return a === b; 1052 | } 1053 | } 1054 | 1055 | return { 1056 | "string" : useStrictEquality, 1057 | "boolean" : useStrictEquality, 1058 | "number" : useStrictEquality, 1059 | "null" : useStrictEquality, 1060 | "undefined" : useStrictEquality, 1061 | 1062 | "nan" : function(b) { 1063 | return isNaN(b); 1064 | }, 1065 | 1066 | "date" : function(b, a) { 1067 | return QUnit.objectType(b) === "date" 1068 | && a.valueOf() === b.valueOf(); 1069 | }, 1070 | 1071 | "regexp" : function(b, a) { 1072 | return QUnit.objectType(b) === "regexp" 1073 | && a.source === b.source && // the regex itself 1074 | a.global === b.global && // and its modifers 1075 | // (gmi) ... 1076 | a.ignoreCase === b.ignoreCase 1077 | && a.multiline === b.multiline; 1078 | }, 1079 | 1080 | // - skip when the property is a method of an instance (OOP) 1081 | // - abort otherwise, 1082 | // initial === would have catch identical references anyway 1083 | "function" : function() { 1084 | var caller = callers[callers.length - 1]; 1085 | return caller !== Object && typeof caller !== "undefined"; 1086 | }, 1087 | 1088 | "array" : function(b, a) { 1089 | var i, j, loop; 1090 | var len; 1091 | 1092 | // b could be an object literal here 1093 | if (!(QUnit.objectType(b) === "array")) { 1094 | return false; 1095 | } 1096 | 1097 | len = a.length; 1098 | if (len !== b.length) { // safe and faster 1099 | return false; 1100 | } 1101 | 1102 | // track reference to avoid circular references 1103 | parents.push(a); 1104 | for (i = 0; i < len; i++) { 1105 | loop = false; 1106 | for (j = 0; j < parents.length; j++) { 1107 | if (parents[j] === a[i]) { 1108 | loop = true;// dont rewalk array 1109 | } 1110 | } 1111 | if (!loop && !innerEquiv(a[i], b[i])) { 1112 | parents.pop(); 1113 | return false; 1114 | } 1115 | } 1116 | parents.pop(); 1117 | return true; 1118 | }, 1119 | 1120 | "object" : function(b, a) { 1121 | var i, j, loop; 1122 | var eq = true; // unless we can proove it 1123 | var aProperties = [], bProperties = []; // collection of 1124 | // strings 1125 | 1126 | // comparing constructors is more strict than using 1127 | // instanceof 1128 | if (a.constructor !== b.constructor) { 1129 | return false; 1130 | } 1131 | 1132 | // stack constructor before traversing properties 1133 | callers.push(a.constructor); 1134 | // track reference to avoid circular references 1135 | parents.push(a); 1136 | 1137 | for (i in a) { // be strict: don't ensures hasOwnProperty 1138 | // and go deep 1139 | loop = false; 1140 | for (j = 0; j < parents.length; j++) { 1141 | if (parents[j] === a[i]) 1142 | loop = true; // don't go down the same path 1143 | // twice 1144 | } 1145 | aProperties.push(i); // collect a's properties 1146 | 1147 | if (!loop && !innerEquiv(a[i], b[i])) { 1148 | eq = false; 1149 | break; 1150 | } 1151 | } 1152 | 1153 | callers.pop(); // unstack, we are done 1154 | parents.pop(); 1155 | 1156 | for (i in b) { 1157 | bProperties.push(i); // collect b's properties 1158 | } 1159 | 1160 | // Ensures identical properties name 1161 | return eq 1162 | && innerEquiv(aProperties.sort(), bProperties 1163 | .sort()); 1164 | } 1165 | }; 1166 | }(); 1167 | 1168 | innerEquiv = function() { // can take multiple arguments 1169 | var args = Array.prototype.slice.apply(arguments); 1170 | if (args.length < 2) { 1171 | return true; // end transition 1172 | } 1173 | 1174 | return (function(a, b) { 1175 | if (a === b) { 1176 | return true; // catch the most you can 1177 | } else if (a === null || b === null || typeof a === "undefined" 1178 | || typeof b === "undefined" 1179 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1180 | return false; // don't lose time with error prone cases 1181 | } else { 1182 | return bindCallbacks(a, callbacks, [ b, a ]); 1183 | } 1184 | 1185 | // apply transition with (1..n) arguments 1186 | })(args[0], args[1]) 1187 | && arguments.callee.apply(this, args.splice(1, 1188 | args.length - 1)); 1189 | }; 1190 | 1191 | return innerEquiv; 1192 | 1193 | }(); 1194 | 1195 | /** 1196 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1197 | * http://flesler.blogspot.com Licensed under BSD 1198 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1199 | * 1200 | * @projectDescription Advanced and extensible data dumping for Javascript. 1201 | * @version 1.0.0 1202 | * @author Ariel Flesler 1203 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1204 | */ 1205 | QUnit.jsDump = (function() { 1206 | function quote( str ) { 1207 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1208 | }; 1209 | function literal( o ) { 1210 | return o + ''; 1211 | }; 1212 | function join( pre, arr, post ) { 1213 | var s = jsDump.separator(), 1214 | base = jsDump.indent(), 1215 | inner = jsDump.indent(1); 1216 | if ( arr.join ) 1217 | arr = arr.join( ',' + s + inner ); 1218 | if ( !arr ) 1219 | return pre + post; 1220 | return [ pre, inner + arr, base + post ].join(s); 1221 | }; 1222 | function array( arr, stack ) { 1223 | var i = arr.length, ret = Array(i); 1224 | this.up(); 1225 | while ( i-- ) 1226 | ret[i] = this.parse( arr[i] , undefined , stack); 1227 | this.down(); 1228 | return join( '[', ret, ']' ); 1229 | }; 1230 | 1231 | var reName = /^function (\w+)/; 1232 | 1233 | var jsDump = { 1234 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1235 | stack = stack || [ ]; 1236 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1237 | type = typeof parser; 1238 | var inStack = inArray(obj, stack); 1239 | if (inStack != -1) { 1240 | return 'recursion('+(inStack - stack.length)+')'; 1241 | } 1242 | //else 1243 | if (type == 'function') { 1244 | stack.push(obj); 1245 | var res = parser.call( this, obj, stack ); 1246 | stack.pop(); 1247 | return res; 1248 | } 1249 | // else 1250 | return (type == 'string') ? parser : this.parsers.error; 1251 | }, 1252 | typeOf:function( obj ) { 1253 | var type; 1254 | if ( obj === null ) { 1255 | type = "null"; 1256 | } else if (typeof obj === "undefined") { 1257 | type = "undefined"; 1258 | } else if (QUnit.is("RegExp", obj)) { 1259 | type = "regexp"; 1260 | } else if (QUnit.is("Date", obj)) { 1261 | type = "date"; 1262 | } else if (QUnit.is("Function", obj)) { 1263 | type = "function"; 1264 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1265 | type = "window"; 1266 | } else if (obj.nodeType === 9) { 1267 | type = "document"; 1268 | } else if (obj.nodeType) { 1269 | type = "node"; 1270 | } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { 1271 | type = "array"; 1272 | } else { 1273 | type = typeof obj; 1274 | } 1275 | return type; 1276 | }, 1277 | separator:function() { 1278 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1279 | }, 1280 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1281 | if ( !this.multiline ) 1282 | return ''; 1283 | var chr = this.indentChar; 1284 | if ( this.HTML ) 1285 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1286 | return Array( this._depth_ + (extra||0) ).join(chr); 1287 | }, 1288 | up:function( a ) { 1289 | this._depth_ += a || 1; 1290 | }, 1291 | down:function( a ) { 1292 | this._depth_ -= a || 1; 1293 | }, 1294 | setParser:function( name, parser ) { 1295 | this.parsers[name] = parser; 1296 | }, 1297 | // The next 3 are exposed so you can use them 1298 | quote:quote, 1299 | literal:literal, 1300 | join:join, 1301 | // 1302 | _depth_: 1, 1303 | // This is the list of parsers, to modify them, use jsDump.setParser 1304 | parsers:{ 1305 | window: '[Window]', 1306 | document: '[Document]', 1307 | error:'[ERROR]', //when no parser is found, shouldn't happen 1308 | unknown: '[Unknown]', 1309 | 'null':'null', 1310 | 'undefined':'undefined', 1311 | 'function':function( fn ) { 1312 | var ret = 'function', 1313 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1314 | if ( name ) 1315 | ret += ' ' + name; 1316 | ret += '('; 1317 | 1318 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1319 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1320 | }, 1321 | array: array, 1322 | nodelist: array, 1323 | arguments: array, 1324 | object:function( map, stack ) { 1325 | var ret = [ ]; 1326 | QUnit.jsDump.up(); 1327 | for ( var key in map ) { 1328 | var val = map[key]; 1329 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); 1330 | } 1331 | QUnit.jsDump.down(); 1332 | return join( '{', ret, '}' ); 1333 | }, 1334 | node:function( node ) { 1335 | var open = QUnit.jsDump.HTML ? '<' : '<', 1336 | close = QUnit.jsDump.HTML ? '>' : '>'; 1337 | 1338 | var tag = node.nodeName.toLowerCase(), 1339 | ret = open + tag; 1340 | 1341 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1342 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1343 | if ( val ) 1344 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1345 | } 1346 | return ret + close + open + '/' + tag + close; 1347 | }, 1348 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1349 | var l = fn.length; 1350 | if ( !l ) return ''; 1351 | 1352 | var args = Array(l); 1353 | while ( l-- ) 1354 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1355 | return ' ' + args.join(', ') + ' '; 1356 | }, 1357 | key:quote, //object calls it internally, the key part of an item in a map 1358 | functionCode:'[code]', //function calls it internally, it's the content of the function 1359 | attribute:quote, //node calls it internally, it's an html attribute value 1360 | string:quote, 1361 | date:quote, 1362 | regexp:literal, //regex 1363 | number:literal, 1364 | 'boolean':literal 1365 | }, 1366 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1367 | id:'id', 1368 | name:'name', 1369 | 'class':'className' 1370 | }, 1371 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1372 | indentChar:' ',//indentation unit 1373 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1374 | }; 1375 | 1376 | return jsDump; 1377 | })(); 1378 | 1379 | // from Sizzle.js 1380 | function getText( elems ) { 1381 | var ret = "", elem; 1382 | 1383 | for ( var i = 0; elems[i]; i++ ) { 1384 | elem = elems[i]; 1385 | 1386 | // Get the text from text nodes and CDATA nodes 1387 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1388 | ret += elem.nodeValue; 1389 | 1390 | // Traverse everything else, except comment nodes 1391 | } else if ( elem.nodeType !== 8 ) { 1392 | ret += getText( elem.childNodes ); 1393 | } 1394 | } 1395 | 1396 | return ret; 1397 | }; 1398 | 1399 | //from jquery.js 1400 | function inArray( elem, array ) { 1401 | if ( array.indexOf ) { 1402 | return array.indexOf( elem ); 1403 | } 1404 | 1405 | for ( var i = 0, length = array.length; i < length; i++ ) { 1406 | if ( array[ i ] === elem ) { 1407 | return i; 1408 | } 1409 | } 1410 | 1411 | return -1; 1412 | } 1413 | 1414 | /* 1415 | * Javascript Diff Algorithm 1416 | * By John Resig (http://ejohn.org/) 1417 | * Modified by Chu Alan "sprite" 1418 | * 1419 | * Released under the MIT license. 1420 | * 1421 | * More Info: 1422 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1423 | * 1424 | * Usage: QUnit.diff(expected, actual) 1425 | * 1426 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1427 | */ 1428 | QUnit.diff = (function() { 1429 | function diff(o, n) { 1430 | var ns = {}; 1431 | var os = {}; 1432 | 1433 | for (var i = 0; i < n.length; i++) { 1434 | if (ns[n[i]] == null) 1435 | ns[n[i]] = { 1436 | rows: [], 1437 | o: null 1438 | }; 1439 | ns[n[i]].rows.push(i); 1440 | } 1441 | 1442 | for (var i = 0; i < o.length; i++) { 1443 | if (os[o[i]] == null) 1444 | os[o[i]] = { 1445 | rows: [], 1446 | n: null 1447 | }; 1448 | os[o[i]].rows.push(i); 1449 | } 1450 | 1451 | for (var i in ns) { 1452 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1453 | n[ns[i].rows[0]] = { 1454 | text: n[ns[i].rows[0]], 1455 | row: os[i].rows[0] 1456 | }; 1457 | o[os[i].rows[0]] = { 1458 | text: o[os[i].rows[0]], 1459 | row: ns[i].rows[0] 1460 | }; 1461 | } 1462 | } 1463 | 1464 | for (var i = 0; i < n.length - 1; i++) { 1465 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1466 | n[i + 1] == o[n[i].row + 1]) { 1467 | n[i + 1] = { 1468 | text: n[i + 1], 1469 | row: n[i].row + 1 1470 | }; 1471 | o[n[i].row + 1] = { 1472 | text: o[n[i].row + 1], 1473 | row: i + 1 1474 | }; 1475 | } 1476 | } 1477 | 1478 | for (var i = n.length - 1; i > 0; i--) { 1479 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1480 | n[i - 1] == o[n[i].row - 1]) { 1481 | n[i - 1] = { 1482 | text: n[i - 1], 1483 | row: n[i].row - 1 1484 | }; 1485 | o[n[i].row - 1] = { 1486 | text: o[n[i].row - 1], 1487 | row: i - 1 1488 | }; 1489 | } 1490 | } 1491 | 1492 | return { 1493 | o: o, 1494 | n: n 1495 | }; 1496 | } 1497 | 1498 | return function(o, n) { 1499 | o = o.replace(/\s+$/, ''); 1500 | n = n.replace(/\s+$/, ''); 1501 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1502 | 1503 | var str = ""; 1504 | 1505 | var oSpace = o.match(/\s+/g); 1506 | if (oSpace == null) { 1507 | oSpace = [" "]; 1508 | } 1509 | else { 1510 | oSpace.push(" "); 1511 | } 1512 | var nSpace = n.match(/\s+/g); 1513 | if (nSpace == null) { 1514 | nSpace = [" "]; 1515 | } 1516 | else { 1517 | nSpace.push(" "); 1518 | } 1519 | 1520 | if (out.n.length == 0) { 1521 | for (var i = 0; i < out.o.length; i++) { 1522 | str += '' + out.o[i] + oSpace[i] + ""; 1523 | } 1524 | } 1525 | else { 1526 | if (out.n[0].text == null) { 1527 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1528 | str += '' + out.o[n] + oSpace[n] + ""; 1529 | } 1530 | } 1531 | 1532 | for (var i = 0; i < out.n.length; i++) { 1533 | if (out.n[i].text == null) { 1534 | str += '' + out.n[i] + nSpace[i] + ""; 1535 | } 1536 | else { 1537 | var pre = ""; 1538 | 1539 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1540 | pre += '' + out.o[n] + oSpace[n] + ""; 1541 | } 1542 | str += " " + out.n[i].text + nSpace[i] + pre; 1543 | } 1544 | } 1545 | } 1546 | 1547 | return str; 1548 | }; 1549 | })(); 1550 | 1551 | })(this); 1552 | -------------------------------------------------------------------------------- /test/real-cors-server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PostMessageProxiedXHR Server Frame 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/sample.txt: -------------------------------------------------------------------------------- 1 | hello there, I am sample text. -------------------------------------------------------------------------------- /test/server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PostMessageProxiedXHR Server Frame 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/test-jquery-plugin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | module("jquery-plugin"); 3 | 4 | jQuery.proxyAjaxThroughPostMessage("server.html"); 5 | 6 | test("success works", function() { 7 | jQuery.ajax({ 8 | url: "sample.txt", 9 | usePostMessage: true, 10 | success: function(data, textStatus, req) { 11 | ok(req.isProxiedThroughPostMessage); 12 | equal(data, "hello there, I am sample text."); 13 | start(); 14 | } 15 | }); 16 | stop(); 17 | }); 18 | 19 | test("error works for 404", function() { 20 | jQuery.ajax({ 21 | url: "nonexistent.txt", 22 | usePostMessage: true, 23 | error: function(req) { 24 | ok(req.isProxiedThroughPostMessage); 25 | equal(req.status, 404); 26 | start(); 27 | } 28 | }); 29 | stop(); 30 | }); 31 | 32 | test("error works for header not allowed", function() { 33 | jQuery.ajax({ 34 | url: "nonexistent.txt", 35 | usePostMessage: true, 36 | headers: {'X-blarg': 'hi'}, 37 | error: function(req) { 38 | ok(req.isProxiedThroughPostMessage); 39 | equal(req.status, 0); 40 | equal(req.responseText, "header 'X-blarg' is not allowed."); 41 | start(); 42 | } 43 | }); 44 | stop(); 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /test/test-real-cors.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var loc = window.location; 3 | 4 | if (!loc.port) 5 | // We're not being run on a development server, just skip 6 | // these tests. 7 | return; 8 | 9 | module("real-cors"); 10 | 11 | var myOrigin = loc.protocol + "//" + loc.hostname + ":" + 12 | parseInt(loc.port); 13 | var corsURL = loc.protocol + "//" + loc.hostname + ":" + 14 | (parseInt(loc.port) + 1); 15 | var serverPath = "/test/real-cors-server.html"; 16 | var Request = PPX.buildClientConstructor(corsURL + serverPath); 17 | 18 | function corsTest(options) { 19 | var name = options.name || options.path; 20 | 21 | function makeTest(xhr) { 22 | return function() { 23 | var req = new xhr(); 24 | req.open(options.method || "GET", corsURL + options.path); 25 | if (options.headers) 26 | for (var name in options.headers) 27 | req.setRequestHeader(name, options.headers[name]); 28 | req.onreadystatechange = function() { 29 | if (req.readyState == 4) { 30 | options.test(req); 31 | start(); 32 | } 33 | }; 34 | req.send(options.body || null); 35 | } 36 | } 37 | 38 | if (jQuery.support.cors) 39 | asyncTest("(CORS XMLHttpRequest) " + name, makeTest(XMLHttpRequest)); 40 | asyncTest("(PostMessage Proxied XHR) " + name, makeTest(Request)); 41 | } 42 | 43 | corsTest({ 44 | path: "/cors/origin-only-me", 45 | test: function(req) { 46 | equal(req.responseText, "hai2u"); 47 | } 48 | }); 49 | 50 | corsTest({ 51 | path: "/cors/origin-all", 52 | test: function(req) { 53 | equal(req.responseText, "hai2u"); 54 | } 55 | }); 56 | 57 | corsTest({ 58 | path: "/cors/origin-all/post", 59 | method: 'POST', 60 | body: 'supdog', 61 | headers: {'Content-Type': 'text/plain'}, 62 | test: function(req) { 63 | equal(req.responseText, "received supdog"); 64 | } 65 | }); 66 | 67 | corsTest({ 68 | name: "POST with a non-simple content-type fails", 69 | path: "/cors/origin-all/post", 70 | method: 'POST', 71 | body: 'supdog', 72 | headers: {'Content-Type': 'super/crazy'}, 73 | test: function(req) { 74 | expectCORSError(req, "invalid content type for a simple request: super/crazy"); 75 | } 76 | }); 77 | 78 | function expectCORSError(req, message) { 79 | if (req instanceof XMLHttpRequest) 80 | equal(req.responseText, ""); 81 | else 82 | equal(req.responseText, message); 83 | equal(req.status, 0); 84 | } 85 | 86 | corsTest({ 87 | path: "/cors/origin-foo.com", 88 | test: function(req) { 89 | expectCORSError(req, "message from invalid origin: " + myOrigin); 90 | } 91 | }); 92 | 93 | corsTest({ 94 | name: "CORS request to path w/ no CORS headers fails", 95 | path: "/test/sample.txt", 96 | test: function(req) { 97 | expectCORSError(req, "CORS is unsupported at that path."); 98 | } 99 | }); 100 | })(); 101 | -------------------------------------------------------------------------------- /test/test-request.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | module("request"); 3 | 4 | var Request = PPX.buildClientConstructor("server.html"); 5 | 6 | test("only non-preflighted request methods are supported", function() { 7 | var req = Request(); 8 | req.open("PUT", "sample.txt"); 9 | req.onreadystatechange = function() { 10 | equal(req.readyState, req.DONE); 11 | equal(req.responseText, "not a simple request method: PUT"); 12 | start(); 13 | }; 14 | req.send(null); 15 | stop(); 16 | }); 17 | 18 | test("exception raised if send() called before open()", function() { 19 | raises(function() { 20 | var req = Request(); 21 | req.send(null); 22 | }, new RegExp("request not initialized")); 23 | }); 24 | 25 | test("same origin violation error works", function() { 26 | var req = Request(); 27 | req.open("get", "http://example.com/foo"); 28 | req.onreadystatechange = function() { 29 | equal(req.readyState, req.DONE); 30 | equal(req.responseText, "url does not have same origin: http://example.com/foo"); 31 | start(); 32 | }; 33 | req.send(null); 34 | stop(); 35 | }); 36 | 37 | test("header not allowed error works", function() { 38 | var req = Request(); 39 | req.open("GET", "sample.txt"); 40 | req.setRequestHeader('X-blarg', 'hi'); 41 | req.onreadystatechange = function() { 42 | equal(req.readyState, req.DONE); 43 | equal(req.responseText, "header 'X-blarg' is not allowed."); 44 | start(); 45 | }; 46 | req.send(null); 47 | stop(); 48 | }); 49 | 50 | test("abort() works", function() { 51 | var req = Request(); 52 | var readyStates = []; 53 | req.open("get", "sample.txt"); 54 | equal(req.readyState, 1); 55 | req.send(null); 56 | req.onreadystatechange = function() { 57 | readyStates.push(req.readyState); 58 | }; 59 | equal(req.readyState, 1); 60 | req.abort(); 61 | equal(req.readyState, 0); 62 | deepEqual(readyStates, [4]); 63 | }); 64 | 65 | test("response '200 OK' works", function() { 66 | function checkTypes() { 67 | equal(typeof(req.responseText), 'string', 'responseText is a string'); 68 | equal(typeof(req.statusText), 'string', 'statusText is a string'); 69 | equal(typeof(req.status), 'number', 'status is a number'); 70 | equal(typeof(req.readyState), 'number', 'readyState is a number'); 71 | if (req.readyState >= req.HEADERS_RECEIVED) 72 | ok(req.getAllResponseHeaders().length, 73 | 'getAllResponseHeaders() returns a non-empty string'); 74 | else 75 | equal(typeof(req.getAllResponseHeaders()), 'string', 76 | 'getAllResponseHeaders() returns a string'); 77 | } 78 | 79 | var req = Request(); 80 | req.open("GET", "sample.txt"); 81 | req.onreadystatechange = function() { 82 | checkTypes(); 83 | if (req.readyState == 4 && req.status == 200) { 84 | equal(req.responseText, "hello there, I am sample text.", 85 | "responseText is as expected."); 86 | start(); 87 | } 88 | }; 89 | checkTypes(); 90 | req.send(null); 91 | stop(); 92 | }); 93 | 94 | test("response '404 Not Found' works", function() { 95 | var req = Request(); 96 | req.open("GET", "nonexistent.txt"); 97 | req.onreadystatechange = function() { 98 | if (req.readyState == 4 && req.status == 404) { 99 | ok(true, "Status 404 was returned."); 100 | start(); 101 | } 102 | }; 103 | req.send(null); 104 | stop(); 105 | }); 106 | 107 | test("HEAD request works", function() { 108 | var req = Request(); 109 | req.open("HEAD", "sample.txt"); 110 | req.onreadystatechange = function() { 111 | if (req.readyState == 4 && req.status == 200) { 112 | ok(req.getAllResponseHeaders().indexOf("text/plain") != -1); 113 | start(); 114 | } 115 | }; 116 | req.send(null); 117 | stop(); 118 | }); 119 | })(); 120 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | module("utils"); 3 | 4 | var utils = PPX.utils; 5 | 6 | test("utils.inArray() works", function() { 7 | equal(utils.inArray("foo", ["bar", "foo", "baz"]), 1); 8 | ok(utils.inArray("foo", ["bar", "baz"]), -1); 9 | }); 10 | 11 | test("utils.encode() works", function() { 12 | deepEqual(utils.encode({a: 'foo', b: 'blarg'}), "a=foo&b=blarg"); 13 | deepEqual(utils.encode({a: '', b: 'blarg'}), "a=&b=blarg"); 14 | deepEqual(utils.encode({a: 'foo&', b: 'b'}), "a=foo%26&b=b", 15 | "Ensure '&' is encoded properly"); 16 | }); 17 | 18 | test("utils.decode() works", function() { 19 | deepEqual(utils.decode("a=foo&b=blarg"), {a: 'foo', b: 'blarg'}); 20 | deepEqual(utils.decode("a=&b=blarg"), {a: '', b: 'blarg'}); 21 | deepEqual(utils.decode("a=foo%26&b=b"), {a: 'foo&', b: 'b'}, 22 | "Ensure '&' is decoded properly"); 23 | }); 24 | 25 | test("utils.isSameOrigin() works", function() { 26 | ok(utils.isSameOrigin("http://foo.com/bar", "http://foo.com/baz")); 27 | ok(!utils.isSameOrigin("http://foo.com/bar", "https://foo.com/baz")); 28 | ok(!utils.isSameOrigin("http://a.com/", "http://b.com/")); 29 | ok(!utils.isSameOrigin("http://a.com:8000/", "http://a.com/")); 30 | }); 31 | })(); 32 | --------------------------------------------------------------------------------