├── LICENSE ├── perf └── perf.html ├── readme.md ├── src ├── XSSB-min.js └── XSSBuster.js └── test ├── QUnit ├── qunit-git.css └── qunit-git.js ├── iframe.html ├── styles.css ├── test-suite.html └── tests.js /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /perf/perf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # XSSBuster 2 | **XSSB** is a proactive DOM sanitizer, defending against client-side injection attacks. 3 | 4 | # The Problem: 5 | With every unaudited third-party JS library you include into your DOM, the risk of accidental DOM-based cross-site-scripting issues rises linearly. It being for advertisement, web analytics, social widgets, et al., all sorts of third-party code is susceptible to injection attacks. 6 | 7 | Examples of this are: 8 | * [http://www.troyhunt.com/2015/07/how-i-got-xssd-by-my-ad-network.html](http://www.troyhunt.com/2015/07/how-i-got-xssd-by-my-ad-network.html) 9 | * [https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/](https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval) 10 | * [http://www.fuzzysecurity.com/tutorials/14.html](http://www.fuzzysecurity.com/tutorials/14.html) 11 | * [http://blog.mindedsecurity.com/2011/04/god-save-omniture-quine.html](http://blog.mindedsecurity.com/2011/04/god-save-omniture-quine.html) 12 | * [https://hackerone.com/reports/125386#activity-888336](https://hackerone.com/reports/125386#activity-888336) 13 | 14 | # The Solution: 15 | **XSSB** mainly utilizes [taint checking](https://en.wikipedia.org/wiki/Taint_checking) to guard against accidental mistakes and poor security practices commonly employed by JS libraries that may lead to DOM-based XSS vulnerabilities. 16 | 17 | A rough idea of how it works is: Data from untrusted input sources such as `window.name`, `location.hash`, `document.referrer`, `window.onmessage`, et al. are tainted and are constantly tracked for any changes. **XSSB** then overrides security-sensitive functions and DOM APIs (e.g., `eval()`, `document.write()`, `Element.prototype.appendChild()`, etc.) to enforce taint checking and prevent insecure operations such as `eval(location.hash.slice(1))`, `document.write(window.name)`, and the like. 18 | 19 | So, basically, **XSSB** offers you the freedom to deploy any given third-party code into your DOM while at the same time covering your DOM's back! 20 | 21 | # Usage Instructions: 22 | Simply place the script element of _XSSBuster.js_ right before any other third-party scripts you include into your webpage(s), typically at the very top of the head tag: 23 | ```html 24 | 25 | Example 26 | 27 | 28 | 29 | ``` 30 | #### Notes: 31 | * Make sure to host _XSSBuster.js_ on the same origin as the hosting webpage or use the _"X-XSS-Protection: 0"_ HTTP header to guard against the potential abuse of browsers' integrated XSS auditors. 32 | * For the minified version, see [_XSSB-min.js_](/src/XSSB-min.js). 33 | 34 | # Demo: 35 | A live demo can be found at: [https://xssb.herokuapp.com](https://xssb.herokuapp.com). 36 | 37 | # Performance: 38 | 39 | Based on tests, **XSSB** only takes [10 milliseconds on average](/perf/perf.html) to do all required security checks besides the registration of a few necessary event listeners. 40 | 41 | # Compatibility: 42 | **XSSB** is compatible with the latest versions of all major web browsers (Firefox, Chrome, IE, Edge, Safari, and Opera) as well as most legacy web browsers through fallback functionality. 43 | 44 | # Known Issues: 45 | * **XSSB** only allows for [Basic Latin](https://en.wikipedia.org/wiki/Basic_Latin_(Unicode_block)) characters within the pathname, search query and hash of the hosting webpage's URL; that somewhat also applies to HTML5 messaging.... If your web application deals with a different set of characters, you may consider [base64](https://en.wikipedia.org/wiki/Base64) encoding as a workaround. 46 | * **XSSB** overrides security-sensitive functions like `eval` in order to enforce taint checking. A side effect of this is that `eval` will behave more like jQuery's [`globalEval`](https://api.jquery.com/jquery.globaleval/) than the native implementation of `eval` in most web browsers. 47 | 48 | # Credits: 49 | * [@0xSobky](https://twitter.com/0xsobky) 50 | -------------------------------------------------------------------------------- /src/XSSB-min.js: -------------------------------------------------------------------------------- 1 | !function(window,Object,Array){var NativeFunction,Rprototype,cookie,cookieDesc,cookieIndex,cookiePair,cookiePairs,elPrototype,innerHTML,nativeAppendChild,nativeAtob,nativeCreateContextualFragment,nativeEval,nativeExecScript,nativeInsertAdjacentElement,nativeInsertAdjacentHTML,nativeInsertBefore,nativeLocalStorage,nativeReplaceChild,nativeSessionStorage,nativeSetImmediate,nativeSetInterval,nativeSetTimeout,nativeWrite,nativeWriteln,outerHTML,valIndex,win,winOrigin,taintedStrings=[],origin=window.location.origin||window.location.protocol+"//"+window.location.host,blacklistRe=/{{|}}|&#?\w{2,7};?|\b(?:on[a-z]+\W*?=|(?:(?:d\W*a\W*t\W*a)|(?:v\W*b|j\W*a\W*v\W*a)\W*s\W*c\W*r\W*i\W*p\W*t)\W*?:)/gi,getType=function(e){var t=Object.prototype.toString;return"string"==typeof e||"[object String]"===t.call(e)?"string":"[object Object]"===t.call(e)?"object":Array.isArray&&Array.isArray(e)||"[object Array]"===t.call(e)?"array":"[object Map]"===t.call(e)?"map":"[object Set]"===t.call(e)?"set":"[object RegExp]"===t.call(e)?"regex":"[object File]"===t.call(e)?"file":"[object FileList]"===t.call(e)?"fileList":"other"},toPlain=function(e){var t=window.encodeURI,n=window.decodeURI,i=window.encodeURIComponent,r=window.decodeURIComponent,o=0,a=[],c=function(e){var s,l,u,d;try{for(;r(e)!==e;)u=r(e),n(e)===u?(e=n(e),a.push(t)):(e=u,a.push(i)),++o}catch(t){if("function"==typeof window.escape&&"function"==typeof window.unescape?(s=window.escape,d=window.unescape):(s=(l=function(e){var t=/(?:[^%]|%(?:40|2[b-f]|2[0-9]|3[a-e]|[57][b-d]))+/gi;return function(n){return(n=n.match(t))?e(n.join("")):null}})(i),d=l(r)),e=d(e),a.push(s),++o,d(e)!==e)return c(e)}return e};return{output:e=c(e),depth:o,revMethod:a}},reEncode=function(e,t,n){for(;t--;)e=n[t](e);return e},sanitize=function(e){var t,n,i,r,o,a,c,s,l,u=/[^\w\s\/+=$#@!&*|,;:.?%()[\]{}^-]/g,d=!1,f=getType(e);if("string"===f)/\S/.test(e)&&(/%/.test(e)&&(a=toPlain(e),e=a.output),u.test(e)&&(e=e.replace(u,""),d=!0),blacklistRe.test(e)&&(e=e.replace(blacklistRe,""),d=!0),taintedStrings.push(e),a&&(e=reEncode(e,a.depth,a.revMethod)));else if("object"===f){s=function(t){var n=sanitize(e[t]);!1!==n&&(e[t]=n,d=!0)};try{for(i in o=Object.getOwnPropertyNames(e))s(o[i])}catch(t){for(c in n=Object.prototype.hasOwnProperty,e)n.call(e,c)&&s(c)}}else if("set"===f){try{l=new Set,e.forEach(function(e){var t=sanitize(e);e=t||e,l.add(e)}),e=l}catch(t){e=null}d=!0}else if("map"===f){try{l=new Map,e.forEach(function(e,t){var n=sanitize(e),i=sanitize(t);e=n||e,t=i||t,l.set(t,e)}),e=l}catch(t){e=null}d=!0}else if("regex"===f)!1!==(l=sanitize(e.source))&&(e=new RegExp(l),d=!0);else if("file"===f)try{!1!==(l=sanitize(e.name))&&((t=new FormData).append("file",e,l),e=t.get("file"),!1!==sanitize(e.name)&&(e=null),d=!0)}catch(t){e=null}else if("array"===f||"fileList"===f){if("fileList"===f){for(l=[],i=e.length;i--;)l[i]=e[i];l.item=function(e){return this[e]}}else l=e;for(i=l.length;i--;)!1!==(r=sanitize(l[i]))&&(l[i]=r,d=!0);d&&(e=l)}return!!d&&e},parseUrl=function(e){var t;try{e=new URL(e)}catch(n){(t=document.createElement("a")).href=e,e=t}return e},auditUrl=function(e){var t,n,i,r,o,a,c,s,l=!1;if(!1!==(o=sanitize(e.pathname))&&(e.pathname=o,l=!0),c=e.search){for(r=!1,i=(c=c.slice(1).split("&")).length;i--;){if((n=c[i].split("=")).length<3)!1!==(a=sanitize(n[0]))&&(n[0]=a,r=!0),n[1]&&!1!==(a=sanitize(n[1]))&&(n[1]=a,r=!0);else for(s=n.length;s--;)!1!==(a=sanitize(n[s]))&&(r=!0,n[s]=a);r&&(c[i]=n.join("="))}r&&(e.search=c.join("&"),l=!0)}return(t=e.hash.slice(1))&&!1!==(t=sanitize(t))&&(e.hash=t,l=!0),!!l&&e.href},addListener=window.addEventListener?function(e,t,n,i){("window"===t?window.addEventListener:document.addEventListener).call(e,n,i)}:function(e,t,n,i){var r;"DOMContentLoaded"===n?(r=function(){"interactive"===e.readyState&&i()},e.attachEvent("onreadystatechange",r)):e.attachEvent("on"+n,i)},defineProperties=function(e,t){for(var n,i,r=t.length;r--;)n=(i=t[r]).value,i=i.isDefault?{value:n,enumerable:!0,writable:!0,configurable:!0}:i;try{Object.defineProperties(e,t)}catch(e){}},auditWinName=function(e){var t=e.name;defineProperties(e,{name:{get:function(){return t},set:function(e){var n=sanitize(e);t=!1!==n?n:e},enumerable:!0}}),e.name=t},auditWin=function(e){var t,n,i,r,o=function(e){var t,n,i,r=e.ports;try{t=e.origin||e.originalEvent.origin}catch(e){}if(t!==origin&&(!1!==(n=sanitize(e.data))&&defineProperties(e,{data:{value:n,isDefault:!0}}),r))for(i=r.length;i--;)r[i].onmessage=o};addListener(e,"window","hashchange",function(){var t=sanitize(e.location.hash.slice(1));!1!==t&&(e.location.hash=t)}),addListener(e,"window","message",o),auditUrl(e.location),(n=e.name)&&!1!==(n=sanitize(n))&&(e.name=n),!1!==(r=sanitize(e.document.title))&&(e.document.title=r),(i=e.document.referrer)&&!1!==(i=auditUrl(parseUrl(i)))&&defineProperties(e.document,{referrer:{value:i,isDefault:!0}}),t=function(){var t,n=document.getElementsByTagName.call(e.document,"iframe"),i=function(e){var t;try{t=e.contentWindow,e.src!==t.location.href&&(auditWinName(t),auditWin(t))}catch(e){}};for(t=n.length;t--;)!function(e){addListener(e,"document","load",function(){i(e)})}(n[t])},addListener(e.document,"document","DOMContentLoaded",t)},getPrototypeOf=function(){try{return Object.getPrototypeOf.apply(this,arguments)}catch(e){}},guardWrite=function(e){return function(t){var n;isSafeArg(t)?e.call(document,t):(t=toSafeStr(t),(n=document.getElementsByTagName("*"))[n.length-1].parentElement.innerHTML=t)}},some=Array.prototype.some||function(e){for(var t=this.length;t--;)if(e(this[t]))return!0;return!1},isSafeArg=function(){return!some.call(arguments,function(e){return e=toPlain(e).output,some.call(taintedStrings,function(t){return isNaN(t)&&t.length>6&&-1!==e.indexOf(t)})})},guardSink=function(e){return function(){if(isSafeArg.apply(null,arguments))return e.apply(this,arguments)}},isUnsafeNode=function(e){var t,n,i,r,o,a,c=e.nodeName;try{if(e.hasChildNodes()){if((t=e.getElementsByTagName("applet")).length>0)return some.call(t,isUnsafeNode);if((n=e.getElementsByTagName("embed")).length>0)return some.call(n,isUnsafeNode);if((i=e.getElementsByTagName("frame")).length>0)return some.call(i,isUnsafeNode);if((r=e.getElementsByTagName("iframe")).length>0)return some.call(r,isUnsafeNode);if((o=e.getElementsByTagName("object")).length>0)return some.call(o,isUnsafeNode);if((a=e.getElementsByTagName("script")).length>0)return some.call(a,isUnsafeNode)}}catch(e){}return"SCRIPT"===c?!isSafeArg(e.text)||!isSafeArg(e.src):"OBJECT"===c?!isSafeArg(e.data):"IFRAME"===c||"FRAME"===c||"EMBED"===c?!(isSafeArg(e.src)&&(!e.srcdoc||isSafeArg(e.srcdoc))):"APPLET"===c?!!(!isSafeArg(e.code)||e.codebase&&!isSafeArg(e.codebase)||e.archive&&!isSafeArg(e.archive)):void 0},toSafeNode=function(e){var t,n,i,r;e.innerHTML="",e.hasAttribute("src")&&e.removeAttribute("src"),e.hasAttribute("srcdoc")&&e.removeAttribute("srcdoc"),e.hasAttribute("data")&&e.removeAttribute("data"),e.hasAttribute("code")&&e.removeAttribute("code"),e.hasAttribute("archive")&&e.removeAttribute("archive"),e.hasAttribute("codebase")&&e.removeAttribute("codebase"),e.hasAttribute("object")&&e.removeAttribute("object");try{if(e.hasAttributes())for(r=(i=e.attributes).length;r--;)n=(t=i[r]).name,/^on./.test(n)&&!isSafeArg(t.value)&&e.removeAttribute(n)}catch(e){}return e},guardMethod=function(e){return function(t){return isUnsafeNode(t)&&(t=toSafeNode(t)),e.apply(this,arguments)}},getOwnPropertyDescriptor=function(){try{return Object.getOwnPropertyDescriptor.apply(this,arguments)}catch(e){}},guardStorage=function(e){return{setItem:function(t,n){isSafeArg(t,n)&&e.setItem(t,n)},getItem:function(t){return e.getItem(t)}}},toSafeStr=function(e){return-1!==e.indexOf("<")&&blacklistRe.test(e)&&(e=(e=e.replace(blacklistRe,"")).replace(/\bsrcdoc=/gi,"redacted=")),e},genDescriptor=function(e){return{get:function(){return e.get.call(this)},set:function(t){return isSafeArg(t)||(t=toSafeStr(t)),e.set.call(this,t)}}};if(auditWin(window),window!==top){win=parent;try{winOrigin=win.location.origin||win.location.protocol+"//"+win.location.host}catch(e){}do{try{winOrigin!==origin&&(auditWinName(win),auditWin(win))}catch(e){continue}finally{win=win.parent}}while(win!==top)}NativeFunction=window.Function,nativeEval=window.eval,nativeSetInterval=window.setInterval,nativeSetTimeout=window.setTimeout,nativeWrite=document.write,nativeWriteln=document.writeln,document.write=guardWrite(nativeWrite),document.writeln=guardWrite(nativeWriteln),window.eval=guardSink(nativeEval),window.setTimeout=guardSink(nativeSetTimeout),window.setInterval=guardSink(nativeSetInterval),window.Function=function(){var e=function(){var e=NativeFunction.apply(null,arguments);e.constructor=Function;try{Object.setPrototypeOf(e,Function)}catch(t){e.__proto__=Function}return e};return isSafeArg.apply(null,arguments)?e.apply(null,arguments):e()},window.Function.prototype=Function;try{elPrototype=window.Element.prototype,nativeAppendChild=elPrototype.appendChild,nativeReplaceChild=elPrototype.replaceChild,nativeInsertBefore=elPrototype.insertBefore,nativeInsertAdjacentHTML=elPrototype.insertAdjacentHTML,nativeInsertAdjacentElement=elPrototype.insertAdjacentElement,innerHTML=getOwnPropertyDescriptor(elPrototype,"innerHTML"),outerHTML=getOwnPropertyDescriptor(elPrototype,"outerHTML"),elPrototype.appendChild=guardMethod(nativeAppendChild),elPrototype.replaceChild=guardMethod(nativeReplaceChild),elPrototype.insertBefore=guardMethod(nativeInsertBefore),elPrototype.insertAdjacentHTML=function(e,t){return isSafeArg(t)||(t=toSafeStr(t)),nativeInsertAdjacentHTML.call(this,e,t)},elPrototype.insertAdjacentElement=function(e,t){return isUnsafeNode(t)&&(t=toSafeNode(t)),nativeInsertAdjacentElement.call(this,e,t)},defineProperties(elPrototype,{innerHTML:genDescriptor(innerHTML),outerHTML:genDescriptor(outerHTML)})}catch(e){}window.execScript&&(nativeExecScript=window.execScript,eval("var execScript;"),window.execScript=guardSink(nativeExecScript)),window.setImmediate&&(nativeSetImmediate=window.setImmediate,window.setImmediate=guardSink(nativeSetImmediate)),window.atob&&(nativeAtob=window.atob,window.atob=function(e){return isSafeArg(e)?nativeAtob(e):e=sanitize(nativeAtob(e))});try{Rprototype=window.Range.prototype,nativeCreateContextualFragment=Rprototype.createContextualFragment,Rprototype.createContextualFragment=function(e){return isSafeArg(e)||(e=""),nativeCreateContextualFragment.call(this,e)}}catch(e){}for(cookie=document.cookie,cookieDesc=function(){try{return getOwnPropertyDescriptor(document,"cookie")||getOwnPropertyDescriptor(getPrototypeOf(document),"cookie")||{get:document.__lookupGetter__("cookie"),set:document.__lookupSetter__("cookie")}}catch(e){}}(),defineProperties(document,{cookie:{get:function(){try{return cookieDesc.get.call(this)}catch(e){return cookie}},set:function(e){if(isSafeArg(e))try{return cookieDesc.set.call(this,e)}catch(t){cookie+=";"+e}}}}),cookiePairs=cookie.split(";"),cookieIndex=cookiePairs.length;cookieIndex--;)for(cookiePair=cookiePairs[cookieIndex].split("="),valIndex=cookiePair.length;valIndex--;)taintedStrings.push(cookiePair[valIndex]);try{window.localStorage&&(nativeLocalStorage=window.localStorage,delete window.localStorage,window.localStorage=guardStorage(nativeLocalStorage)),window.sessionStorage&&(nativeSessionStorage=window.sessionStorage,delete window.sessionStorage,window.sessionStorage=guardStorage(nativeSessionStorage))}catch(e){}}(window,Object,Array); -------------------------------------------------------------------------------- /src/XSSBuster.js: -------------------------------------------------------------------------------- 1 | (function(window, Object, Array) { 2 | // Version 1.1.3. 3 | var NativeFunction, Rprototype, cookie, cookieDesc, cookieIndex, 4 | cookiePair, cookiePairs, elPrototype, innerHTML, nativeAppendChild, 5 | nativeAtob, nativeCreateContextualFragment, nativeEval, 6 | nativeExecScript, nativeInsertAdjacentElement, nativeInsertAdjacentHTML, 7 | nativeInsertBefore, nativeLocalStorage, nativeReplaceChild, 8 | nativeSessionStorage, nativeSetImmediate, nativeSetInterval, 9 | nativeSetTimeout, nativeWrite, nativeWriteln, outerHTML, valIndex, win, 10 | winOrigin; 11 | 12 | var taintedStrings = []; 13 | 14 | var origin = window.location.origin || 15 | window.location.protocol + '//' + window.location.host; 16 | 17 | /* 18 | * Matches evil URI schemes, event handlers, HTML entities, 19 | * and scary curly notations! 20 | */ 21 | var blacklistRe = /{{|}}|&#?\w{2,7};?|\b(?:on[a-z]+\W*?=|(?:(?:d\W*a\W*t\W*a)|(?:v\W*b|j\W*a\W*v\W*a)\W*s\W*c\W*r\W*i\W*p\W*t)\W*?:)/gi; 22 | 23 | /** 24 | * Take an input and return its data type. 25 | * 26 | * @param input {string|array|object}, some data. 27 | * @return {string}, data type. 28 | */ 29 | var getType = function(input) { 30 | var toString = Object.prototype.toString; 31 | // Is it a string? 32 | if (typeof input === 'string' || 33 | toString.call(input) === '[object String]') { 34 | return 'string'; 35 | // An object? 36 | } else if (toString.call(input) === '[object Object]') { 37 | return 'object'; 38 | // An array? 39 | } else if (Array.isArray && Array.isArray(input) || 40 | toString.call(input) === '[object Array]') { 41 | return 'array'; 42 | // A map? 43 | } else if (toString.call(input) === '[object Map]') { 44 | return 'map'; 45 | // A set? 46 | } else if (toString.call(input) === '[object Set]') { 47 | return 'set'; 48 | // A regex? 49 | } else if (toString.call(input) === '[object RegExp]') { 50 | return 'regex'; 51 | // A file? 52 | } else if (toString.call(input) === '[object File]') { 53 | return 'file'; 54 | // A fileList? 55 | } else if (toString.call(input) === '[object FileList]') { 56 | return 'fileList'; 57 | } 58 | return 'other'; 59 | }; 60 | 61 | /** 62 | * Take a URL-encoded input and return it in plaintext. 63 | * 64 | * @param input {string}, data string to decode. 65 | * @return {object}, a container for decoded data. 66 | */ 67 | var toPlain = function(input) { 68 | var encURI = window.encodeURI; 69 | var decURI = window.decodeURI; 70 | var encURIComp = window.encodeURIComponent; 71 | var decURIComp = window.decodeURIComponent; 72 | var depth = 0; 73 | var revMethod = []; 74 | /** 75 | * URL-decode a given string. 76 | * 77 | * @param input {string}, a URL-encoded string. 78 | * @return {string}, a URL-decoded string. 79 | */ 80 | var deEncode = function(input) { 81 | var es, eusExtend, origInput, ues; 82 | /* A try/catch clause to handle any URIError exceptions. */ 83 | try { 84 | // Recursively URL-decode the input data. 85 | while (decURIComp(input) !== input) { 86 | origInput = decURIComp(input); 87 | if (decURI(input) === origInput) { 88 | input = decURI(input); 89 | revMethod.push(encURI); 90 | } else { 91 | input = origInput; 92 | revMethod.push(encURIComp); 93 | } 94 | ++depth; 95 | } 96 | } catch (e) { 97 | // Make sure `escape()` and `unescape()` are still supported. 98 | if (typeof window.escape === 'function' && 99 | typeof window.unescape === 'function') { 100 | es = window.escape; 101 | ues = window.unescape; 102 | // A just-in-case fallback. 103 | } else { 104 | /** 105 | * Extend a URL-encoding/decoding function's functionality. 106 | * 107 | * @param func {function}, a URL-encoding/decoding function. 108 | * @return {function}. 109 | */ 110 | eusExtend = function(func) { 111 | var charsetRe = /(?:[^%]|%(?:40|2[b-f]|2[0-9]|3[a-e]|[57][b-d]))+/gi; 112 | return function(string) { 113 | string = string.match(charsetRe); 114 | return (string) ? func(string.join('')) : null; 115 | }; 116 | }; 117 | es = eusExtend(encURIComp); 118 | ues = eusExtend(decURIComp); 119 | } 120 | input = ues(input); 121 | revMethod.push(es); 122 | ++depth; 123 | if (ues(input) !== input) { 124 | return deEncode(input); 125 | } 126 | } 127 | return input; 128 | }; 129 | input = deEncode(input); 130 | return { 131 | output: input, 132 | depth: depth, 133 | revMethod: revMethod 134 | }; 135 | }; 136 | 137 | /** 138 | * Take a `toPlain()` output and re-encode it. 139 | * 140 | * @param input {string}, a `toPlain().output` property. 141 | * @param depth {integer}, a `toPlain().depth` property. 142 | * @param revMethod {function}, a `toPlain().revMethod` property. 143 | * @return {string}, URL-encoded data. 144 | */ 145 | var reEncode = function(input, depth, revMethod) { 146 | while (depth--) { 147 | input = revMethod[depth](input); 148 | } 149 | return input; 150 | }; 151 | 152 | /** 153 | * Take a raw input and sanitize it as needed. 154 | * 155 | * @param input {string|array|object}, a string literal, array or object. 156 | * @return {string|array|object|boolean}, sanitized data or `false`. 157 | */ 158 | var sanitize = function(input) { 159 | var formData, hasOwnProperty, index, item, keys, origInput, prop, propSanitize, 160 | tmpVar; 161 | // Matches safe Basic Latin characters. 162 | var whitelistRe = /[^\w\s\/+=$#@!&*|,;:.?%()[\]{}^-]/g; 163 | var isModified = false; 164 | var inptType = getType(input); 165 | // Check if `input` is a string. 166 | if (inptType === 'string') { 167 | // Assert it's not a whitespace string. 168 | if (/\S/.test(input)) { 169 | // Check if `input` is URL-encoded. 170 | if (/%/.test(input)) { 171 | origInput = toPlain(input); 172 | input = origInput.output; 173 | } 174 | if (whitelistRe.test(input)) { 175 | input = input.replace(whitelistRe, ''); 176 | isModified = true; 177 | } 178 | if (blacklistRe.test(input)) { 179 | input = input.replace(blacklistRe, ''); 180 | isModified = true; 181 | } 182 | // Add `input` to the list of tainted strings. 183 | taintedStrings.push(input); 184 | // Re-encode `input` if it has been decoded. 185 | if (origInput) { 186 | input = reEncode(input, origInput.depth, origInput.revMethod); 187 | } 188 | } 189 | // Check if it's an object. 190 | } else if (inptType === 'object') { 191 | /** 192 | * Take an object property and audit it. 193 | * 194 | * @param prop {string}, an object property name. 195 | * @return void. 196 | */ 197 | propSanitize = function(prop) { 198 | var value = sanitize(input[prop]); 199 | if (value !== false) { 200 | input[prop] = value; 201 | isModified = true; 202 | } 203 | }; 204 | try { 205 | keys = Object.getOwnPropertyNames(input); 206 | for (index in keys) { 207 | propSanitize(keys[index]); 208 | } 209 | } catch (e) { 210 | hasOwnProperty = Object.prototype.hasOwnProperty; 211 | for (prop in input) { 212 | if (hasOwnProperty.call(input, prop)) { 213 | propSanitize(prop); 214 | } 215 | } 216 | } 217 | // Check if it's a set. 218 | } else if (inptType === 'set') { 219 | try { 220 | tmpVar = new Set(); 221 | input.forEach(function(val) { 222 | var sVal = sanitize(val); 223 | val = sVal ? sVal : val; 224 | tmpVar.add(val); 225 | }); 226 | input = tmpVar; 227 | } catch (e) { 228 | input = null; 229 | } 230 | isModified = true; 231 | // Check if it's a map. 232 | } else if (inptType === 'map') { 233 | try { 234 | tmpVar = new Map(); 235 | input.forEach(function(val, key) { 236 | var sVal = sanitize(val); 237 | var sKey = sanitize(key); 238 | val = sVal ? sVal : val; 239 | key = sKey ? sKey : key; 240 | tmpVar.set(key, val); 241 | }); 242 | input = tmpVar; 243 | } catch (e) { 244 | input = null; 245 | } 246 | isModified = true; 247 | // Check if it's a regex. 248 | } else if (inptType === 'regex') { 249 | tmpVar = sanitize(input.source); 250 | if (tmpVar !== false) { 251 | input = new RegExp(tmpVar); 252 | isModified = true; 253 | } 254 | // Check if it's a file. 255 | } else if (inptType === 'file') { 256 | try { 257 | tmpVar = sanitize(input.name); 258 | if (tmpVar !== false) { 259 | formData = new FormData(); 260 | formData.append('file', input, tmpVar); 261 | input = formData.get('file'); 262 | if (sanitize(input.name) !== false) { 263 | input = null; 264 | } 265 | isModified = true; 266 | } 267 | } catch (e) { 268 | input = null; 269 | } 270 | // Check if it's an array-like object. 271 | } else if (inptType === 'array' || inptType === 'fileList') { 272 | if (inptType === 'fileList') { 273 | tmpVar = []; 274 | index = input.length; 275 | while (index--) { 276 | tmpVar[index] = input[index]; 277 | } 278 | tmpVar.item = function(index) { 279 | return this[index]; 280 | }; 281 | } else { 282 | tmpVar = input; 283 | } 284 | index = tmpVar.length; 285 | // Iterate over array items and sanitize them one by one. 286 | while (index--) { 287 | item = sanitize(tmpVar[index]); 288 | if (item !== false) { 289 | tmpVar[index] = item; 290 | isModified = true; 291 | } 292 | } 293 | if (isModified) { 294 | input = tmpVar; 295 | } 296 | } 297 | return (isModified) ? input : false; 298 | }; 299 | 300 | /** 301 | * Parse a URL string. 302 | * 303 | * @param url {string}, a URL string. 304 | * @return {object}, a URL object. 305 | */ 306 | var parseUrl = function(url) { 307 | var parser; 308 | try { 309 | url = new URL(url); 310 | } catch (e) { 311 | parser = document.createElement('a'); 312 | parser.href = url; 313 | url = parser; 314 | } 315 | return url; 316 | }; 317 | 318 | /** 319 | * Take a URL object and sanitize it. 320 | * 321 | * @param urlObj {object}, a URL object. 322 | * @return {string|boolean}, a string URL or `false`. 323 | */ 324 | var auditUrl = function(urlObj) { 325 | var hash, paramPair, paramIndex, paramModified, pathname, sanParam, 326 | search, subIndex; 327 | var isModified = false; 328 | /* 329 | * For sanitizing the pathname property of 330 | * the current window location object. 331 | */ 332 | pathname = sanitize(urlObj.pathname); 333 | if (pathname !== false) { 334 | urlObj.pathname = pathname; 335 | isModified = true; 336 | } 337 | /* 338 | * For sanitizing the search property of 339 | * the current window location object. 340 | */ 341 | search = urlObj.search; 342 | if (search) { 343 | paramModified = false; 344 | search = search.slice(1).split('&'); 345 | paramIndex = search.length; 346 | while (paramIndex--) { 347 | paramPair = search[paramIndex].split('='); 348 | if (paramPair.length < 3) { 349 | sanParam = sanitize(paramPair[0]); 350 | if (sanParam !== false) { 351 | paramPair[0] = sanParam; 352 | paramModified = true; 353 | } 354 | if (paramPair[1]) { 355 | sanParam = sanitize(paramPair[1]); 356 | if (sanParam !== false) { 357 | paramPair[1] = sanParam; 358 | paramModified = true; 359 | } 360 | } 361 | } else { 362 | subIndex = paramPair.length; 363 | while (subIndex--) { 364 | sanParam = sanitize(paramPair[subIndex]); 365 | if (sanParam !== false) { 366 | paramModified = true; 367 | paramPair[subIndex] = sanParam; 368 | } 369 | } 370 | } 371 | if (paramModified) { 372 | search[paramIndex] = paramPair.join('='); 373 | } 374 | } 375 | if (paramModified) { 376 | urlObj.search = search.join('&'); 377 | isModified = true; 378 | } 379 | } 380 | /* 381 | * For sanitizing the hash property of 382 | * the current window location object. 383 | */ 384 | hash = urlObj.hash.slice(1); 385 | if (hash) { 386 | hash = sanitize(hash); 387 | if (hash !== false) { 388 | urlObj.hash = hash; 389 | isModified = true; 390 | } 391 | } 392 | return (isModified) ? urlObj.href : false; 393 | }; 394 | 395 | /** 396 | * Register a new cross-browser event listener. 397 | * 398 | * @param target {object}, a target object to bind the event listener to. 399 | * @param equiv {string}, a corresponding DOM property name for @target. 400 | * @param evName {string}, the name of the event to register. 401 | * @param callback {function}, a callback function for the event listener. 402 | * @return void. 403 | */ 404 | var addListener = (function() { 405 | if (window.addEventListener) { 406 | return function(target, equiv, evName, callback) { 407 | var nativeAddEventListener = (equiv === 'window') ? 408 | window.addEventListener : document.addEventListener; 409 | nativeAddEventListener.call(target, evName, callback); 410 | }; 411 | } 412 | // For IE8 and earlier versions support. 413 | return function(target, _, evName, callback) { 414 | var domLoadedCallback; 415 | if (evName === 'DOMContentLoaded') { 416 | /** 417 | * A proxy function to `callback()`. 418 | * @return void. 419 | */ 420 | domLoadedCallback = function() { 421 | if (target.readyState === 'interactive') { 422 | callback(); 423 | } 424 | }; 425 | target.attachEvent('onreadystatechange', domLoadedCallback); 426 | } else { 427 | target.attachEvent('on' + evName, callback); 428 | } 429 | }; 430 | })(); 431 | 432 | /** 433 | * A proxy function to `Object.defineProperties()`. 434 | * 435 | * @param obj {object}, a target object. 436 | * @param properties {object}, a property descriptor. 437 | * @return void. 438 | */ 439 | var defineProperties = function(obj, properties) { 440 | var origValue, prop; 441 | var index = properties.length; 442 | while (index--) { 443 | prop = properties[index]; 444 | origValue = prop.value; 445 | prop = prop.isDefault ? { 446 | value: origValue, 447 | enumerable: true, 448 | writable: true, 449 | configurable: true 450 | } : prop; 451 | } 452 | try { 453 | Object.defineProperties(obj, properties); 454 | } catch (e) {} 455 | }; 456 | 457 | /** 458 | * Redefine the name property of a given window object, 459 | * so we add a setter to sanitize it whenever it changes. 460 | * 461 | * @param winObj {object}, a window object. 462 | * @return void. 463 | */ 464 | var auditWinName = function(winObj) { 465 | var origName = winObj.name; 466 | defineProperties(winObj, { 467 | 'name': { 468 | get: function() { 469 | return origName; 470 | }, 471 | set: function(val) { 472 | var sanVal = sanitize(val); 473 | if (sanVal !== false) { 474 | origName = sanVal; 475 | } else { 476 | origName = val; 477 | } 478 | }, 479 | enumerable: true 480 | } 481 | }); 482 | winObj.name = origName; 483 | }; 484 | 485 | /** 486 | * Take a window object and audit its default properties. 487 | * 488 | * @param winObj {object}, a window object. 489 | * @return void. 490 | */ 491 | var auditWin = function(winObj) { 492 | var auditFrames, name, referrer, title; 493 | /** 494 | * Audit the `location.hash` window property on change. 495 | * 496 | * @return void. 497 | */ 498 | var onhashchangeFn = function() { 499 | var hash = sanitize(winObj.location.hash.slice(1)); 500 | if (hash !== false) { 501 | winObj.location.hash = hash; 502 | } 503 | }; 504 | /** 505 | * Intercept HTML5 messages and audit them. 506 | * 507 | * @param ev {object}, a message event. 508 | * @return void. 509 | */ 510 | var onmessageFn = function(ev) { 511 | var winOrigin, data, index, port; 512 | var ports = ev.ports; 513 | try { 514 | winOrigin = ev.origin || ev.originalEvent.origin; 515 | } catch (e) {} 516 | if (winOrigin !== origin) { 517 | data = sanitize(ev.data); 518 | if (data !== false) { 519 | defineProperties(ev, { 520 | 'data': { 521 | value: data, 522 | isDefault: true 523 | } 524 | }); 525 | } 526 | if (ports) { 527 | index = ports.length; 528 | while (index--) { 529 | port = ports[index]; 530 | port.onmessage = onmessageFn; 531 | } 532 | } 533 | } 534 | }; 535 | // For hash re-sanitization whenever it gets modified. 536 | addListener(winObj, 'window', 'hashchange', onhashchangeFn); 537 | // For cross-document messaging sanitization. 538 | addListener(winObj, 'window', 'message', onmessageFn); 539 | // Audit the current window URL. 540 | auditUrl(winObj.location); 541 | /* 542 | * For sanitizing the name property 543 | * of the current window object. 544 | */ 545 | name = winObj.name; 546 | if (name) { 547 | name = sanitize(name); 548 | if (name !== false) { 549 | winObj.name = name; 550 | } 551 | } 552 | /* 553 | * For sanitizing the title of the current document. 554 | * And yep, `document.title` can be -partially?- 555 | * manipulated by attackers as in search pages! 556 | */ 557 | title = sanitize(winObj.document.title); 558 | if (title !== false) { 559 | winObj.document.title = title; 560 | } 561 | // For sanitizing any given document referrer. 562 | referrer = winObj.document.referrer; 563 | if (referrer) { 564 | referrer = auditUrl(parseUrl(referrer)); 565 | if (referrer !== false) { 566 | defineProperties(winObj.document, { 567 | 'referrer': { 568 | value: referrer, 569 | isDefault: true 570 | } 571 | }); 572 | } 573 | } 574 | /** 575 | * For auditing child frames. 576 | * 577 | * @return void. 578 | */ 579 | auditFrames = function() { 580 | var fIndex, currentFrame; 581 | var getElementsByTagName = document.getElementsByTagName; 582 | var frames = getElementsByTagName.call(winObj.document, 583 | 'iframe'); 584 | /** 585 | * For auditing a given frame node. 586 | * 587 | * @param currentFrame {object}, a frame node. 588 | * @return void. 589 | */ 590 | var auditFrame = function(currentFrame) { 591 | var fWindow; 592 | try { 593 | fWindow = currentFrame.contentWindow; 594 | if (currentFrame.src !== fWindow.location.href) { 595 | auditWinName(fWindow); 596 | auditWin(fWindow); 597 | } 598 | } catch (e) {} 599 | }; 600 | fIndex = frames.length; 601 | while (fIndex--) { 602 | currentFrame = frames[fIndex]; 603 | (function(currentFrame) { 604 | addListener(currentFrame, 'document', 'load', 605 | function() { 606 | auditFrame(currentFrame); 607 | }); 608 | })(currentFrame); 609 | } 610 | }; 611 | addListener(winObj.document, 'document', 612 | 'DOMContentLoaded', auditFrames); 613 | }; 614 | 615 | /** 616 | * A proxy function to `Object.getPrototypeOf()`. 617 | * 618 | * @return {object}, a prototype object. 619 | */ 620 | var getPrototypeOf = function () { 621 | try { 622 | return Object.getPrototypeOf.apply(this, arguments); 623 | } catch (e) {} 624 | }; 625 | 626 | /** 627 | * Guard write functions against tainted strings. 628 | * 629 | * @param nativeWrite {function}, a write-like function. 630 | * @return {function}. 631 | */ 632 | var guardWrite = function(nativeWrite) { 633 | return function(str) { 634 | var el, els; 635 | if (!isSafeArg(str)) { 636 | str = toSafeStr(str); 637 | els = document.getElementsByTagName('*'); 638 | el = els[els.length - 1]; 639 | el.parentElement.innerHTML = str; 640 | } else { 641 | nativeWrite.call(document, str); 642 | } 643 | }; 644 | }; 645 | 646 | /** 647 | * A proxy function to `Array.prototype.some()`. 648 | * 649 | * @param fn {function}, a test function. 650 | * @return {boolean}. 651 | */ 652 | var some = Array.prototype.some || function(fn) { 653 | var index = this.length; 654 | while (index--) { 655 | if (fn(this[index])) { 656 | return true; 657 | } 658 | } 659 | return false; 660 | }; 661 | 662 | /** 663 | * Check if a given string is safe. 664 | * 665 | * @return void. 666 | */ 667 | var isSafeArg = function() { 668 | /** 669 | * Validate any given string argument. 670 | * 671 | * @param arg {string}, a given string. 672 | * @return {boolean}. 673 | */ 674 | var validate = function(arg) { 675 | /** 676 | * Check if a given string is tainted. 677 | * 678 | * @param taint {string}, a given string. 679 | * @return {boolean}. 680 | */ 681 | var isTainted = function(taint) { 682 | return (isNaN(taint) && taint.length > 6 && 683 | arg.indexOf(taint) !== -1); 684 | }; 685 | arg = toPlain(arg).output; 686 | return (some.call(taintedStrings, isTainted)); 687 | }; 688 | if (some.call(arguments, validate)) { 689 | return false; 690 | } 691 | return true; 692 | }; 693 | 694 | /** 695 | * Guard a given sink function. 696 | * 697 | * @param sinkFn {function}, a sink function. 698 | * @return {function}, a safe sink function. 699 | */ 700 | var guardSink = function(sinkFn) { 701 | return function() { 702 | if (isSafeArg.apply(null, arguments)) { 703 | return sinkFn.apply(this, arguments); 704 | } 705 | }; 706 | }; 707 | 708 | /** 709 | * Check if a given node is unsafe. 710 | * 711 | * @param node {object}, a given DOM node. 712 | * @return {boolean}. 713 | */ 714 | var isUnsafeNode = function(node) { 715 | var childApplets, childEmbeds, childFrames, childIframes, childObjects, childScripts; 716 | var nodeName = node.nodeName; 717 | try { 718 | if (node.hasChildNodes()) { 719 | childApplets = node.getElementsByTagName('applet'); 720 | if (childApplets.length > 0) { 721 | return some.call(childApplets, isUnsafeNode); 722 | } 723 | childEmbeds = node.getElementsByTagName('embed'); 724 | if (childEmbeds.length > 0) { 725 | return some.call(childEmbeds, isUnsafeNode); 726 | } 727 | childFrames = node.getElementsByTagName('frame'); 728 | if (childFrames.length > 0) { 729 | return some.call(childFrames, isUnsafeNode); 730 | } 731 | childIframes = node.getElementsByTagName('iframe'); 732 | if (childIframes.length > 0) { 733 | return some.call(childIframes, isUnsafeNode); 734 | } 735 | childObjects = node.getElementsByTagName('object'); 736 | if (childObjects.length > 0) { 737 | return some.call(childObjects, isUnsafeNode); 738 | } 739 | childScripts = node.getElementsByTagName('script'); 740 | if (childScripts.length > 0) { 741 | return some.call(childScripts, isUnsafeNode); 742 | } 743 | } 744 | } catch (e) {} 745 | if (nodeName === 'SCRIPT') { 746 | if (isSafeArg(node.text) && isSafeArg(node.src)) { 747 | return false; 748 | } 749 | return true; 750 | } else if (nodeName === 'OBJECT') { 751 | if (isSafeArg(node.data)) { 752 | return false; 753 | } 754 | return true; 755 | } else if (nodeName === 'IFRAME' || nodeName === 'FRAME' || 756 | nodeName === 'EMBED') { 757 | if (isSafeArg(node.src) && 758 | (!node.srcdoc || isSafeArg(node.srcdoc))) { 759 | return false; 760 | } 761 | return true; 762 | } else if (nodeName === 'APPLET') { 763 | if (isSafeArg(node.code) && 764 | (!node.codebase || isSafeArg(node.codebase)) && 765 | (!node.archive || isSafeArg(node.archive))) { 766 | return false; 767 | } 768 | return true; 769 | } 770 | }; 771 | 772 | /** 773 | * Neutralize a given DOM node. 774 | * 775 | * @param node {object}, an unsafe DOM node. 776 | * @return {object}, a neutralized DOM node. 777 | */ 778 | var toSafeNode = function(node) { 779 | var attrib, attribName, attribs, index; 780 | node.innerHTML = ''; 781 | if (node.hasAttribute('src')) { 782 | node.removeAttribute('src'); 783 | } 784 | if (node.hasAttribute('srcdoc')) { 785 | node.removeAttribute('srcdoc'); 786 | } 787 | if (node.hasAttribute('data')) { 788 | node.removeAttribute('data'); 789 | } 790 | if (node.hasAttribute('code')) { 791 | node.removeAttribute('code'); 792 | } 793 | if (node.hasAttribute('archive')) { 794 | node.removeAttribute('archive'); 795 | } 796 | if (node.hasAttribute('codebase')) { 797 | node.removeAttribute('codebase'); 798 | } 799 | if (node.hasAttribute('object')) { 800 | node.removeAttribute('object'); 801 | } 802 | try { 803 | if (node.hasAttributes()) { 804 | attribs = node.attributes; 805 | index = attribs.length; 806 | while (index--) { 807 | attrib = attribs[index]; 808 | attribName = attrib.name; 809 | if (/^on./.test(attribName) && !isSafeArg(attrib.value)) { 810 | node.removeAttribute(attribName); 811 | } 812 | } 813 | } 814 | } catch (e) {} 815 | return node; 816 | }; 817 | 818 | /** 819 | * Guard `appendChild()` and alike methods. 820 | * 821 | * @param method {function}, a given function. 822 | * @return {function}. 823 | */ 824 | var guardMethod = function(method) { 825 | return function(node) { 826 | if (isUnsafeNode(node)) { 827 | node = toSafeNode(node); 828 | } 829 | return method.apply(this, arguments); 830 | }; 831 | }; 832 | 833 | /** 834 | * A proxy function to `Object.getOwnPropertyDescriptor()`. 835 | * 836 | * @return {object}, a property descriptor. 837 | */ 838 | var getOwnPropertyDescriptor = function () { 839 | try { 840 | return Object.getOwnPropertyDescriptor.apply(this, arguments); 841 | } catch (e) {} 842 | }; 843 | 844 | /** 845 | * Guard a given storage object. 846 | * 847 | * @param storageObj {object}, a storage object. 848 | * @return {object}, a safe storage object. 849 | */ 850 | var guardStorage = function(storageObj) { 851 | return { 852 | setItem: function(key, value) { 853 | if (isSafeArg(key, value)) { 854 | storageObj.setItem(key, value); 855 | } 856 | }, 857 | getItem: function(key) { 858 | return storageObj.getItem(key); 859 | } 860 | }; 861 | }; 862 | 863 | /** 864 | * Take a suspicious string and neutralize it. 865 | * 866 | * @param str {string}, a suspicious string. 867 | * @return {string}, a neutralized string. 868 | */ 869 | var toSafeStr = function(str) { 870 | if (str.indexOf('<') !== -1 && blacklistRe.test(str)) { 871 | str = str.replace(blacklistRe, ''); 872 | str = str.replace(/\bsrcdoc=/gi, 'redacted='); 873 | } 874 | return str; 875 | }; 876 | 877 | /** 878 | * Generate a safe property descriptor. 879 | * 880 | * @param prop {object}, a descriptor object. 881 | * @return {object}. 882 | */ 883 | var genDescriptor = function(prop) { 884 | return { 885 | get: function() { 886 | return prop.get.call(this); 887 | }, 888 | set: function(val) { 889 | if (!isSafeArg(val)) { 890 | val = toSafeStr(val); 891 | } 892 | return prop.set.call(this, val); 893 | } 894 | }; 895 | }; 896 | 897 | // Audit `self` window. 898 | auditWin(window); 899 | 900 | // Audit `parent` window(s). 901 | if (window !== top) { 902 | win = parent; 903 | try { 904 | winOrigin = win.location.origin || 905 | win.location.protocol + '//' + win.location.host; 906 | } catch(e) {} 907 | do { 908 | try { 909 | if (winOrigin !== origin) { 910 | auditWinName(win); 911 | auditWin(win); 912 | } 913 | } catch (e) { 914 | continue; 915 | } finally { 916 | win = win.parent; 917 | } 918 | } while (win !== top); 919 | } 920 | 921 | /* Monkey-patch JS sinks. */ 922 | NativeFunction = window.Function; 923 | nativeEval = window.eval; 924 | nativeSetInterval = window.setInterval; 925 | nativeSetTimeout = window.setTimeout; 926 | nativeWrite = document.write; 927 | nativeWriteln = document.writeln; 928 | document.write = guardWrite(nativeWrite); 929 | document.writeln = guardWrite(nativeWriteln); 930 | window.eval = guardSink(nativeEval); 931 | window.setTimeout = guardSink(nativeSetTimeout); 932 | window.setInterval = guardSink(nativeSetInterval); 933 | window.Function = function() { 934 | /** 935 | * Construct a new `Function()` instance. 936 | * 937 | * @return {function}. 938 | */ 939 | var construct = function() { 940 | var fn = NativeFunction.apply(null, arguments); 941 | fn.constructor = Function; 942 | try { 943 | Object.setPrototypeOf(fn, Function); 944 | } catch (e) { 945 | fn.__proto__ = Function; 946 | } 947 | return fn; 948 | }; 949 | if (isSafeArg.apply(null, arguments)) { 950 | return construct.apply(null, arguments); 951 | } 952 | return construct(); 953 | }; 954 | window.Function.prototype = Function; 955 | try { 956 | elPrototype = window.Element.prototype; 957 | nativeAppendChild = elPrototype.appendChild; 958 | nativeReplaceChild = elPrototype.replaceChild; 959 | nativeInsertBefore = elPrototype.insertBefore; 960 | nativeInsertAdjacentHTML = elPrototype.insertAdjacentHTML; 961 | nativeInsertAdjacentElement = elPrototype.insertAdjacentElement; 962 | innerHTML = getOwnPropertyDescriptor(elPrototype, 'innerHTML'); 963 | outerHTML = getOwnPropertyDescriptor(elPrototype, 'outerHTML'); 964 | elPrototype.appendChild = guardMethod(nativeAppendChild); 965 | elPrototype.replaceChild = guardMethod(nativeReplaceChild); 966 | elPrototype.insertBefore = guardMethod(nativeInsertBefore); 967 | elPrototype.insertAdjacentHTML = function(position, html) { 968 | if (!isSafeArg(html)) { 969 | html = toSafeStr(html); 970 | } 971 | return nativeInsertAdjacentHTML.call(this, position, html); 972 | }; 973 | elPrototype.insertAdjacentElement = function(position, el) { 974 | if (isUnsafeNode(el)) { 975 | el = toSafeNode(el); 976 | } 977 | return nativeInsertAdjacentElement.call(this, position, el); 978 | }; 979 | defineProperties(elPrototype, { 980 | 'innerHTML': genDescriptor(innerHTML), 981 | 'outerHTML': genDescriptor(outerHTML) 982 | }); 983 | } catch (e) {} 984 | if (window.execScript) { 985 | nativeExecScript = window.execScript; 986 | // A nasty workaround to override `execScript()`. 987 | eval('var execScript;'); 988 | window.execScript = guardSink(nativeExecScript); 989 | } 990 | if (window.setImmediate) { 991 | nativeSetImmediate = window.setImmediate; 992 | window.setImmediate = guardSink(nativeSetImmediate); 993 | } 994 | 995 | // Override `atob()` to sanitize tainted base64-encoded strings. 996 | if (window.atob) { 997 | nativeAtob = window.atob; 998 | window.atob = function(str) { 999 | if (isSafeArg(str)) { 1000 | return nativeAtob(str); 1001 | } 1002 | str = sanitize(nativeAtob(str)); 1003 | return str; 1004 | }; 1005 | } 1006 | 1007 | /* Guard `createContextualFragment()`. */ 1008 | try { 1009 | Rprototype = window.Range.prototype; 1010 | nativeCreateContextualFragment = Rprototype.createContextualFragment; 1011 | Rprototype.createContextualFragment = function(tagStr) { 1012 | if (!isSafeArg(tagStr)) { 1013 | tagStr = ''; 1014 | } 1015 | return nativeCreateContextualFragment.call(this, tagStr); 1016 | }; 1017 | } catch (e) {} 1018 | 1019 | /* Monkey-patch storage sources. */ 1020 | cookie = document.cookie; 1021 | cookieDesc = (function () { 1022 | try { 1023 | return getOwnPropertyDescriptor(document, 'cookie') || 1024 | getOwnPropertyDescriptor(getPrototypeOf(document), 'cookie') || 1025 | { 1026 | get: document.__lookupGetter__('cookie'), 1027 | set: document.__lookupSetter__('cookie') 1028 | }; 1029 | } catch (e) {} 1030 | })(); 1031 | defineProperties(document, { 1032 | 'cookie': { 1033 | get: function() { 1034 | try { 1035 | return cookieDesc.get.call(this); 1036 | } catch (e) { 1037 | return cookie; 1038 | } 1039 | }, 1040 | set: function(val) { 1041 | if (isSafeArg(val)) { 1042 | try { 1043 | return cookieDesc.set.call(this, val); 1044 | } catch (e) { 1045 | cookie += ';' + val; 1046 | } 1047 | } 1048 | } 1049 | } 1050 | }); 1051 | // Add cookie values to tainted strings 1052 | cookiePairs = cookie.split(';'); 1053 | cookieIndex = cookiePairs.length; 1054 | while (cookieIndex--) { 1055 | cookiePair = cookiePairs[cookieIndex].split('='); 1056 | valIndex = cookiePair.length; 1057 | while (valIndex--) { 1058 | taintedStrings.push(cookiePair[valIndex]); 1059 | } 1060 | } 1061 | try { 1062 | if (window.localStorage) { 1063 | nativeLocalStorage = window.localStorage; 1064 | delete window.localStorage; 1065 | window.localStorage = guardStorage(nativeLocalStorage); 1066 | } 1067 | if (window.sessionStorage) { 1068 | nativeSessionStorage = window.sessionStorage; 1069 | delete window.sessionStorage; 1070 | window.sessionStorage = guardStorage(nativeSessionStorage); 1071 | } 1072 | } catch (e) {} 1073 | })(window, Object, Array); 1074 | -------------------------------------------------------------------------------- /test/QUnit/qunit-git.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.20.1-pre 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-01-10T19:20Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-filteredTest { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #F4FF77; 74 | color: #366097; 75 | } 76 | 77 | #qunit-userAgent { 78 | padding: 0.5em 1em 0.5em 1em; 79 | background-color: #2B81AF; 80 | color: #FFF; 81 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 82 | } 83 | 84 | #qunit-modulefilter-container { 85 | float: right; 86 | padding: 0.2em; 87 | } 88 | 89 | .qunit-url-config { 90 | display: inline-block; 91 | padding: 0.1em; 92 | } 93 | 94 | .qunit-filter { 95 | display: block; 96 | float: right; 97 | margin-left: 1em; 98 | } 99 | 100 | /** Tests: Pass/Fail */ 101 | 102 | #qunit-tests { 103 | list-style-position: inside; 104 | } 105 | 106 | #qunit-tests li { 107 | padding: 0.4em 1em 0.4em 1em; 108 | border-bottom: 1px solid #FFF; 109 | list-style-position: inside; 110 | } 111 | 112 | #qunit-tests > li { 113 | display: none; 114 | } 115 | 116 | #qunit-tests li.running, 117 | #qunit-tests li.pass, 118 | #qunit-tests li.fail, 119 | #qunit-tests li.skipped { 120 | display: list-item; 121 | } 122 | 123 | #qunit-tests.hidepass li.running, 124 | #qunit-tests.hidepass li.pass { 125 | visibility: hidden; 126 | position: absolute; 127 | width: 0; 128 | height: 0; 129 | padding: 0; 130 | border: 0; 131 | margin: 0; 132 | } 133 | 134 | #qunit-tests li strong { 135 | cursor: pointer; 136 | } 137 | 138 | #qunit-tests li.skipped strong { 139 | cursor: default; 140 | } 141 | 142 | #qunit-tests li a { 143 | padding: 0.5em; 144 | color: #C2CCD1; 145 | text-decoration: none; 146 | } 147 | 148 | #qunit-tests li p a { 149 | padding: 0.25em; 150 | color: #6B6464; 151 | } 152 | #qunit-tests li a:hover, 153 | #qunit-tests li a:focus { 154 | color: #000; 155 | } 156 | 157 | #qunit-tests li .runtime { 158 | float: right; 159 | font-size: smaller; 160 | } 161 | 162 | .qunit-assert-list { 163 | margin-top: 0.5em; 164 | padding: 0.5em; 165 | 166 | background-color: #FFF; 167 | 168 | border-radius: 5px; 169 | } 170 | 171 | .qunit-source { 172 | margin: 0.6em 0 0.3em; 173 | } 174 | 175 | .qunit-collapsed { 176 | display: none; 177 | } 178 | 179 | #qunit-tests table { 180 | border-collapse: collapse; 181 | margin-top: 0.2em; 182 | } 183 | 184 | #qunit-tests th { 185 | text-align: right; 186 | vertical-align: top; 187 | padding: 0 0.5em 0 0; 188 | } 189 | 190 | #qunit-tests td { 191 | vertical-align: top; 192 | } 193 | 194 | #qunit-tests pre { 195 | margin: 0; 196 | white-space: pre-wrap; 197 | word-wrap: break-word; 198 | } 199 | 200 | #qunit-tests del { 201 | background-color: #E0F2BE; 202 | color: #374E0C; 203 | text-decoration: none; 204 | } 205 | 206 | #qunit-tests ins { 207 | background-color: #FFCACA; 208 | color: #500; 209 | text-decoration: none; 210 | } 211 | 212 | /*** Test Counts */ 213 | 214 | #qunit-tests b.counts { color: #000; } 215 | #qunit-tests b.passed { color: #5E740B; } 216 | #qunit-tests b.failed { color: #710909; } 217 | 218 | #qunit-tests li li { 219 | padding: 5px; 220 | background-color: #FFF; 221 | border-bottom: none; 222 | list-style-position: inside; 223 | } 224 | 225 | /*** Passing Styles */ 226 | 227 | #qunit-tests li li.pass { 228 | color: #3C510C; 229 | background-color: #FFF; 230 | border-left: 10px solid #C6E746; 231 | } 232 | 233 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 234 | #qunit-tests .pass .test-name { color: #366097; } 235 | 236 | #qunit-tests .pass .test-actual, 237 | #qunit-tests .pass .test-expected { color: #999; } 238 | 239 | #qunit-banner.qunit-pass { background-color: #C6E746; } 240 | 241 | /*** Failing Styles */ 242 | 243 | #qunit-tests li li.fail { 244 | color: #710909; 245 | background-color: #FFF; 246 | border-left: 10px solid #EE5757; 247 | white-space: pre; 248 | } 249 | 250 | #qunit-tests > li:last-child { 251 | border-radius: 0 0 5px 5px; 252 | } 253 | 254 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 255 | #qunit-tests .fail .test-name, 256 | #qunit-tests .fail .module-name { color: #000; } 257 | 258 | #qunit-tests .fail .test-actual { color: #EE5757; } 259 | #qunit-tests .fail .test-expected { color: #008000; } 260 | 261 | #qunit-banner.qunit-fail { background-color: #EE5757; } 262 | 263 | /*** Skipped tests */ 264 | 265 | #qunit-tests .skipped { 266 | background-color: #EBECE9; 267 | } 268 | 269 | #qunit-tests .qunit-skipped-label { 270 | background-color: #F4FF77; 271 | display: inline-block; 272 | font-style: normal; 273 | color: #366097; 274 | line-height: 1.8em; 275 | padding: 0 0.5em; 276 | margin: -0.4em 0.4em -0.4em 0; 277 | } 278 | 279 | /** Result */ 280 | 281 | #qunit-testresult { 282 | padding: 0.5em 1em 0.5em 1em; 283 | 284 | color: #2B81AF; 285 | background-color: #D2E0E6; 286 | 287 | border-bottom: 1px solid #FFF; 288 | } 289 | #qunit-testresult .module-name { 290 | font-weight: 700; 291 | } 292 | 293 | /** Fixture */ 294 | 295 | #qunit-fixture { 296 | position: absolute; 297 | top: -10000px; 298 | left: -10000px; 299 | width: 1000px; 300 | height: 1000px; 301 | } 302 | -------------------------------------------------------------------------------- /test/QUnit/qunit-git.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.20.1-pre 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-01-10T19:20Z 10 | */ 11 | 12 | (function( global ) { 13 | 14 | var QUnit = {}; 15 | 16 | var Date = global.Date; 17 | var now = Date.now || function() { 18 | return new Date().getTime(); 19 | }; 20 | 21 | var setTimeout = global.setTimeout; 22 | var clearTimeout = global.clearTimeout; 23 | 24 | // Store a local window from the global to allow direct references. 25 | var window = global.window; 26 | 27 | var defined = { 28 | document: window && window.document !== undefined, 29 | setTimeout: setTimeout !== undefined, 30 | sessionStorage: (function() { 31 | var x = "qunit-test-string"; 32 | try { 33 | sessionStorage.setItem( x, x ); 34 | sessionStorage.removeItem( x ); 35 | return true; 36 | } catch ( e ) { 37 | return false; 38 | } 39 | }() ) 40 | }; 41 | 42 | var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); 43 | var globalStartCalled = false; 44 | var runStarted = false; 45 | 46 | var toString = Object.prototype.toString, 47 | hasOwn = Object.prototype.hasOwnProperty; 48 | 49 | // returns a new Array with the elements that are in a but not in b 50 | function diff( a, b ) { 51 | var i, j, 52 | result = a.slice(); 53 | 54 | for ( i = 0; i < result.length; i++ ) { 55 | for ( j = 0; j < b.length; j++ ) { 56 | if ( result[ i ] === b[ j ] ) { 57 | result.splice( i, 1 ); 58 | i--; 59 | break; 60 | } 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | // from jquery.js 67 | function inArray( elem, array ) { 68 | if ( array.indexOf ) { 69 | return array.indexOf( elem ); 70 | } 71 | 72 | for ( var i = 0, length = array.length; i < length; i++ ) { 73 | if ( array[ i ] === elem ) { 74 | return i; 75 | } 76 | } 77 | 78 | return -1; 79 | } 80 | 81 | /** 82 | * Makes a clone of an object using only Array or Object as base, 83 | * and copies over the own enumerable properties. 84 | * 85 | * @param {Object} obj 86 | * @return {Object} New object with only the own properties (recursively). 87 | */ 88 | function objectValues ( obj ) { 89 | var key, val, 90 | vals = QUnit.is( "array", obj ) ? [] : {}; 91 | for ( key in obj ) { 92 | if ( hasOwn.call( obj, key ) ) { 93 | val = obj[ key ]; 94 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 95 | } 96 | } 97 | return vals; 98 | } 99 | 100 | function extend( a, b, undefOnly ) { 101 | for ( var prop in b ) { 102 | if ( hasOwn.call( b, prop ) ) { 103 | 104 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 105 | // This block runs on every environment, so `global` is being used instead of `window` 106 | // to avoid errors on node. 107 | if ( prop !== "constructor" || a !== global ) { 108 | if ( b[ prop ] === undefined ) { 109 | delete a[ prop ]; 110 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 111 | a[ prop ] = b[ prop ]; 112 | } 113 | } 114 | } 115 | } 116 | 117 | return a; 118 | } 119 | 120 | function objectType( obj ) { 121 | if ( typeof obj === "undefined" ) { 122 | return "undefined"; 123 | } 124 | 125 | // Consider: typeof null === object 126 | if ( obj === null ) { 127 | return "null"; 128 | } 129 | 130 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 131 | type = match && match[ 1 ]; 132 | 133 | switch ( type ) { 134 | case "Number": 135 | if ( isNaN( obj ) ) { 136 | return "nan"; 137 | } 138 | return "number"; 139 | case "String": 140 | case "Boolean": 141 | case "Array": 142 | case "Set": 143 | case "Map": 144 | case "Date": 145 | case "RegExp": 146 | case "Function": 147 | case "Symbol": 148 | return type.toLowerCase(); 149 | } 150 | if ( typeof obj === "object" ) { 151 | return "object"; 152 | } 153 | } 154 | 155 | // Safe object type checking 156 | function is( type, obj ) { 157 | return QUnit.objectType( obj ) === type; 158 | } 159 | 160 | var getUrlParams = function() { 161 | var i, current; 162 | var urlParams = {}; 163 | var location = window.location; 164 | var params = location.search.slice( 1 ).split( "&" ); 165 | var length = params.length; 166 | 167 | if ( params[ 0 ] ) { 168 | for ( i = 0; i < length; i++ ) { 169 | current = params[ i ].split( "=" ); 170 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 171 | 172 | // allow just a key to turn on a flag, e.g., test.html?noglobals 173 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 174 | if ( urlParams[ current[ 0 ] ] ) { 175 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); 176 | } else { 177 | urlParams[ current[ 0 ] ] = current[ 1 ]; 178 | } 179 | } 180 | } 181 | 182 | return urlParams; 183 | }; 184 | 185 | // Doesn't support IE6 to IE9, it will return undefined on these browsers 186 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 187 | function extractStacktrace( e, offset ) { 188 | offset = offset === undefined ? 4 : offset; 189 | 190 | var stack, include, i; 191 | 192 | if ( e.stack ) { 193 | stack = e.stack.split( "\n" ); 194 | if ( /^error$/i.test( stack[ 0 ] ) ) { 195 | stack.shift(); 196 | } 197 | if ( fileName ) { 198 | include = []; 199 | for ( i = offset; i < stack.length; i++ ) { 200 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 201 | break; 202 | } 203 | include.push( stack[ i ] ); 204 | } 205 | if ( include.length ) { 206 | return include.join( "\n" ); 207 | } 208 | } 209 | return stack[ offset ]; 210 | 211 | // Support: Safari <=6 only 212 | } else if ( e.sourceURL ) { 213 | 214 | // exclude useless self-reference for generated Error objects 215 | if ( /qunit.js$/.test( e.sourceURL ) ) { 216 | return; 217 | } 218 | 219 | // for actual exceptions, this is useful 220 | return e.sourceURL + ":" + e.line; 221 | } 222 | } 223 | 224 | function sourceFromStacktrace( offset ) { 225 | var error = new Error(); 226 | 227 | // Support: Safari <=7 only, IE <=10 - 11 only 228 | // Not all browsers generate the `stack` property for `new Error()`, see also #636 229 | if ( !error.stack ) { 230 | try { 231 | throw error; 232 | } catch ( err ) { 233 | error = err; 234 | } 235 | } 236 | 237 | return extractStacktrace( error, offset ); 238 | } 239 | 240 | /** 241 | * Config object: Maintain internal state 242 | * Later exposed as QUnit.config 243 | * `config` initialized at top of scope 244 | */ 245 | var config = { 246 | // The queue of tests to run 247 | queue: [], 248 | 249 | // block until document ready 250 | blocking: true, 251 | 252 | // by default, run previously failed tests first 253 | // very useful in combination with "Hide passed tests" checked 254 | reorder: true, 255 | 256 | // by default, modify document.title when suite is done 257 | altertitle: true, 258 | 259 | // HTML Reporter: collapse every test except the first failing test 260 | // If false, all failing tests will be expanded 261 | collapse: true, 262 | 263 | // by default, scroll to top of the page when suite is done 264 | scrolltop: true, 265 | 266 | // depth up-to which object will be dumped 267 | maxDepth: 5, 268 | 269 | // when enabled, all tests must call expect() 270 | requireExpects: false, 271 | 272 | // add checkboxes that are persisted in the query-string 273 | // when enabled, the id is set to `true` as a `QUnit.config` property 274 | urlConfig: [ 275 | { 276 | id: "hidepassed", 277 | label: "Hide passed tests", 278 | tooltip: "Only show tests and assertions that fail. Stored as query-strings." 279 | }, 280 | { 281 | id: "noglobals", 282 | label: "Check for Globals", 283 | tooltip: "Enabling this will test if any test introduces new properties on the " + 284 | "global object (`window` in Browsers). Stored as query-strings." 285 | }, 286 | { 287 | id: "notrycatch", 288 | label: "No try-catch", 289 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + 290 | "exceptions in IE reasonable. Stored as query-strings." 291 | } 292 | ], 293 | 294 | // Set of all modules. 295 | modules: [], 296 | 297 | // Stack of nested modules 298 | moduleStack: [], 299 | 300 | // The first unnamed module 301 | currentModule: { 302 | name: "", 303 | tests: [] 304 | }, 305 | 306 | callbacks: {} 307 | }; 308 | 309 | var urlParams = defined.document ? getUrlParams() : {}; 310 | 311 | // Push a loose unnamed module to the modules collection 312 | config.modules.push( config.currentModule ); 313 | 314 | if ( urlParams.filter === true ) { 315 | delete urlParams.filter; 316 | } 317 | 318 | // String search anywhere in moduleName+testName 319 | config.filter = urlParams.filter; 320 | 321 | config.testId = []; 322 | if ( urlParams.testId ) { 323 | // Ensure that urlParams.testId is an array 324 | urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); 325 | for (var i = 0; i < urlParams.testId.length; i++ ) { 326 | config.testId.push( urlParams.testId[ i ] ); 327 | } 328 | } 329 | 330 | var loggingCallbacks = {}; 331 | 332 | // Register logging callbacks 333 | function registerLoggingCallbacks( obj ) { 334 | var i, l, key, 335 | callbackNames = [ "begin", "done", "log", "testStart", "testDone", 336 | "moduleStart", "moduleDone" ]; 337 | 338 | function registerLoggingCallback( key ) { 339 | var loggingCallback = function( callback ) { 340 | if ( objectType( callback ) !== "function" ) { 341 | throw new Error( 342 | "QUnit logging methods require a callback function as their first parameters." 343 | ); 344 | } 345 | 346 | config.callbacks[ key ].push( callback ); 347 | }; 348 | 349 | // DEPRECATED: This will be removed on QUnit 2.0.0+ 350 | // Stores the registered functions allowing restoring 351 | // at verifyLoggingCallbacks() if modified 352 | loggingCallbacks[ key ] = loggingCallback; 353 | 354 | return loggingCallback; 355 | } 356 | 357 | for ( i = 0, l = callbackNames.length; i < l; i++ ) { 358 | key = callbackNames[ i ]; 359 | 360 | // Initialize key collection of logging callback 361 | if ( objectType( config.callbacks[ key ] ) === "undefined" ) { 362 | config.callbacks[ key ] = []; 363 | } 364 | 365 | obj[ key ] = registerLoggingCallback( key ); 366 | } 367 | } 368 | 369 | function runLoggingCallbacks( key, args ) { 370 | var i, l, callbacks; 371 | 372 | callbacks = config.callbacks[ key ]; 373 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 374 | callbacks[ i ]( args ); 375 | } 376 | } 377 | 378 | // DEPRECATED: This will be removed on 2.0.0+ 379 | // This function verifies if the loggingCallbacks were modified by the user 380 | // If so, it will restore it, assign the given callback and print a console warning 381 | function verifyLoggingCallbacks() { 382 | var loggingCallback, userCallback; 383 | 384 | for ( loggingCallback in loggingCallbacks ) { 385 | if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { 386 | 387 | userCallback = QUnit[ loggingCallback ]; 388 | 389 | // Restore the callback function 390 | QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; 391 | 392 | // Assign the deprecated given callback 393 | QUnit[ loggingCallback ]( userCallback ); 394 | 395 | if ( global.console && global.console.warn ) { 396 | global.console.warn( 397 | "QUnit." + loggingCallback + " was replaced with a new value.\n" + 398 | "Please, check out the documentation on how to apply logging callbacks.\n" + 399 | "Reference: https://api.qunitjs.com/category/callbacks/" 400 | ); 401 | } 402 | } 403 | } 404 | } 405 | 406 | ( function() { 407 | if ( !defined.document ) { 408 | return; 409 | } 410 | 411 | // `onErrorFnPrev` initialized at top of scope 412 | // Preserve other handlers 413 | var onErrorFnPrev = window.onerror; 414 | 415 | // Cover uncaught exceptions 416 | // Returning true will suppress the default browser handler, 417 | // returning false will let it run. 418 | window.onerror = function( error, filePath, linerNr ) { 419 | var ret = false; 420 | if ( onErrorFnPrev ) { 421 | ret = onErrorFnPrev( error, filePath, linerNr ); 422 | } 423 | 424 | // Treat return value as window.onerror itself does, 425 | // Only do our handling if not suppressed. 426 | if ( ret !== true ) { 427 | if ( QUnit.config.current ) { 428 | if ( QUnit.config.current.ignoreGlobalErrors ) { 429 | return true; 430 | } 431 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 432 | } else { 433 | QUnit.test( "global failure", extend(function() { 434 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 435 | }, { validTest: true } ) ); 436 | } 437 | return false; 438 | } 439 | 440 | return ret; 441 | }; 442 | } )(); 443 | 444 | QUnit.urlParams = urlParams; 445 | 446 | // Figure out if we're running the tests from a server or not 447 | QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); 448 | 449 | // Expose the current QUnit version 450 | QUnit.version = "1.20.1-pre"; 451 | 452 | extend( QUnit, { 453 | 454 | // call on start of module test to prepend name to all tests 455 | module: function( name, testEnvironment, executeNow ) { 456 | var module, moduleFns; 457 | var currentModule = config.currentModule; 458 | 459 | if ( arguments.length === 2 ) { 460 | if ( testEnvironment instanceof Function ) { 461 | executeNow = testEnvironment; 462 | testEnvironment = undefined; 463 | } 464 | } 465 | 466 | // DEPRECATED: handles setup/teardown functions, 467 | // beforeEach and afterEach should be used instead 468 | if ( testEnvironment && testEnvironment.setup ) { 469 | testEnvironment.beforeEach = testEnvironment.setup; 470 | delete testEnvironment.setup; 471 | } 472 | if ( testEnvironment && testEnvironment.teardown ) { 473 | testEnvironment.afterEach = testEnvironment.teardown; 474 | delete testEnvironment.teardown; 475 | } 476 | 477 | module = createModule(); 478 | 479 | moduleFns = { 480 | beforeEach: setHook( module, "beforeEach" ), 481 | afterEach: setHook( module, "afterEach" ) 482 | }; 483 | 484 | if ( executeNow instanceof Function ) { 485 | config.moduleStack.push( module ); 486 | setCurrentModule( module ); 487 | executeNow.call( module.testEnvironment, moduleFns ); 488 | config.moduleStack.pop(); 489 | module = module.parentModule || currentModule; 490 | } 491 | 492 | setCurrentModule( module ); 493 | 494 | function createModule() { 495 | var parentModule = config.moduleStack.length ? 496 | config.moduleStack.slice( -1 )[ 0 ] : null; 497 | var moduleName = parentModule !== null ? 498 | [ parentModule.name, name ].join( " > " ) : name; 499 | var module = { 500 | name: moduleName, 501 | parentModule: parentModule, 502 | tests: [] 503 | }; 504 | 505 | var env = {}; 506 | if ( parentModule ) { 507 | extend( env, parentModule.testEnvironment ); 508 | delete env.beforeEach; 509 | delete env.afterEach; 510 | } 511 | extend( env, testEnvironment ); 512 | module.testEnvironment = env; 513 | 514 | config.modules.push( module ); 515 | return module; 516 | } 517 | 518 | function setCurrentModule( module ) { 519 | config.currentModule = module; 520 | } 521 | 522 | }, 523 | 524 | // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. 525 | asyncTest: asyncTest, 526 | 527 | test: test, 528 | 529 | skip: skip, 530 | 531 | only: only, 532 | 533 | // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. 534 | // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. 535 | start: function( count ) { 536 | var globalStartAlreadyCalled = globalStartCalled; 537 | 538 | if ( !config.current ) { 539 | globalStartCalled = true; 540 | 541 | if ( runStarted ) { 542 | throw new Error( "Called start() outside of a test context while already started" ); 543 | } else if ( globalStartAlreadyCalled || count > 1 ) { 544 | throw new Error( "Called start() outside of a test context too many times" ); 545 | } else if ( config.autostart ) { 546 | throw new Error( "Called start() outside of a test context when " + 547 | "QUnit.config.autostart was true" ); 548 | } else if ( !config.pageLoaded ) { 549 | 550 | // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it 551 | config.autostart = true; 552 | return; 553 | } 554 | } else { 555 | 556 | // If a test is running, adjust its semaphore 557 | config.current.semaphore -= count || 1; 558 | 559 | // If semaphore is non-numeric, throw error 560 | if ( isNaN( config.current.semaphore ) ) { 561 | config.current.semaphore = 0; 562 | 563 | QUnit.pushFailure( 564 | "Called start() with a non-numeric decrement.", 565 | sourceFromStacktrace( 2 ) 566 | ); 567 | return; 568 | } 569 | 570 | // Don't start until equal number of stop-calls 571 | if ( config.current.semaphore > 0 ) { 572 | return; 573 | } 574 | 575 | // throw an Error if start is called more often than stop 576 | if ( config.current.semaphore < 0 ) { 577 | config.current.semaphore = 0; 578 | 579 | QUnit.pushFailure( 580 | "Called start() while already started (test's semaphore was 0 already)", 581 | sourceFromStacktrace( 2 ) 582 | ); 583 | return; 584 | } 585 | } 586 | 587 | resumeProcessing(); 588 | }, 589 | 590 | // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. 591 | stop: function( count ) { 592 | 593 | // If there isn't a test running, don't allow QUnit.stop() to be called 594 | if ( !config.current ) { 595 | throw new Error( "Called stop() outside of a test context" ); 596 | } 597 | 598 | // If a test is running, adjust its semaphore 599 | config.current.semaphore += count || 1; 600 | 601 | pauseProcessing(); 602 | }, 603 | 604 | config: config, 605 | 606 | is: is, 607 | 608 | objectType: objectType, 609 | 610 | extend: extend, 611 | 612 | load: function() { 613 | config.pageLoaded = true; 614 | 615 | // Initialize the configuration options 616 | extend( config, { 617 | stats: { all: 0, bad: 0 }, 618 | moduleStats: { all: 0, bad: 0 }, 619 | started: 0, 620 | updateRate: 1000, 621 | autostart: true, 622 | filter: "" 623 | }, true ); 624 | 625 | config.blocking = false; 626 | 627 | if ( config.autostart ) { 628 | resumeProcessing(); 629 | } 630 | }, 631 | 632 | stack: function( offset ) { 633 | offset = ( offset || 0 ) + 2; 634 | return sourceFromStacktrace( offset ); 635 | } 636 | }); 637 | 638 | registerLoggingCallbacks( QUnit ); 639 | 640 | function begin() { 641 | var i, l, 642 | modulesLog = []; 643 | 644 | // If the test run hasn't officially begun yet 645 | if ( !config.started ) { 646 | 647 | // Record the time of the test run's beginning 648 | config.started = now(); 649 | 650 | verifyLoggingCallbacks(); 651 | 652 | // Delete the loose unnamed module if unused. 653 | if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { 654 | config.modules.shift(); 655 | } 656 | 657 | // Avoid unnecessary information by not logging modules' test environments 658 | for ( i = 0, l = config.modules.length; i < l; i++ ) { 659 | modulesLog.push({ 660 | name: config.modules[ i ].name, 661 | tests: config.modules[ i ].tests 662 | }); 663 | } 664 | 665 | // The test run is officially beginning now 666 | runLoggingCallbacks( "begin", { 667 | totalTests: Test.count, 668 | modules: modulesLog 669 | }); 670 | } 671 | 672 | config.blocking = false; 673 | process( true ); 674 | } 675 | 676 | function process( last ) { 677 | function next() { 678 | process( last ); 679 | } 680 | var start = now(); 681 | config.depth = ( config.depth || 0 ) + 1; 682 | 683 | while ( config.queue.length && !config.blocking ) { 684 | if ( !defined.setTimeout || config.updateRate <= 0 || 685 | ( ( now() - start ) < config.updateRate ) ) { 686 | if ( config.current ) { 687 | 688 | // Reset async tracking for each phase of the Test lifecycle 689 | config.current.usedAsync = false; 690 | } 691 | config.queue.shift()(); 692 | } else { 693 | setTimeout( next, 13 ); 694 | break; 695 | } 696 | } 697 | config.depth--; 698 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 699 | done(); 700 | } 701 | } 702 | 703 | function pauseProcessing() { 704 | config.blocking = true; 705 | 706 | if ( config.testTimeout && defined.setTimeout ) { 707 | clearTimeout( config.timeout ); 708 | config.timeout = setTimeout(function() { 709 | if ( config.current ) { 710 | config.current.semaphore = 0; 711 | QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); 712 | } else { 713 | throw new Error( "Test timed out" ); 714 | } 715 | resumeProcessing(); 716 | }, config.testTimeout ); 717 | } 718 | } 719 | 720 | function resumeProcessing() { 721 | runStarted = true; 722 | 723 | // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) 724 | if ( defined.setTimeout ) { 725 | setTimeout(function() { 726 | if ( config.current && config.current.semaphore > 0 ) { 727 | return; 728 | } 729 | if ( config.timeout ) { 730 | clearTimeout( config.timeout ); 731 | } 732 | 733 | begin(); 734 | }, 13 ); 735 | } else { 736 | begin(); 737 | } 738 | } 739 | 740 | function done() { 741 | var runtime, passed; 742 | 743 | config.autorun = true; 744 | 745 | // Log the last module results 746 | if ( config.previousModule ) { 747 | runLoggingCallbacks( "moduleDone", { 748 | name: config.previousModule.name, 749 | tests: config.previousModule.tests, 750 | failed: config.moduleStats.bad, 751 | passed: config.moduleStats.all - config.moduleStats.bad, 752 | total: config.moduleStats.all, 753 | runtime: now() - config.moduleStats.started 754 | }); 755 | } 756 | delete config.previousModule; 757 | 758 | runtime = now() - config.started; 759 | passed = config.stats.all - config.stats.bad; 760 | 761 | runLoggingCallbacks( "done", { 762 | failed: config.stats.bad, 763 | passed: passed, 764 | total: config.stats.all, 765 | runtime: runtime 766 | }); 767 | } 768 | 769 | function setHook( module, hookName ) { 770 | if ( module.testEnvironment === undefined ) { 771 | module.testEnvironment = {}; 772 | } 773 | 774 | return function( callback ) { 775 | module.testEnvironment[ hookName ] = callback; 776 | }; 777 | } 778 | 779 | var focused = false; 780 | var priorityCount = 0; 781 | 782 | function Test( settings ) { 783 | var i, l; 784 | 785 | ++Test.count; 786 | 787 | extend( this, settings ); 788 | this.assertions = []; 789 | this.semaphore = 0; 790 | this.usedAsync = false; 791 | this.module = config.currentModule; 792 | this.stack = sourceFromStacktrace( 3 ); 793 | 794 | // Register unique strings 795 | for ( i = 0, l = this.module.tests; i < l.length; i++ ) { 796 | if ( this.module.tests[ i ].name === this.testName ) { 797 | this.testName += " "; 798 | } 799 | } 800 | 801 | this.testId = generateHash( this.module.name, this.testName ); 802 | 803 | this.module.tests.push({ 804 | name: this.testName, 805 | testId: this.testId 806 | }); 807 | 808 | if ( settings.skip ) { 809 | 810 | // Skipped tests will fully ignore any sent callback 811 | this.callback = function() {}; 812 | this.async = false; 813 | this.expected = 0; 814 | } else { 815 | this.assert = new Assert( this ); 816 | } 817 | } 818 | 819 | Test.count = 0; 820 | 821 | Test.prototype = { 822 | before: function() { 823 | if ( 824 | 825 | // Emit moduleStart when we're switching from one module to another 826 | this.module !== config.previousModule || 827 | 828 | // They could be equal (both undefined) but if the previousModule property doesn't 829 | // yet exist it means this is the first test in a suite that isn't wrapped in a 830 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 831 | // Without this, reporters can get testStart before moduleStart which is a problem. 832 | !hasOwn.call( config, "previousModule" ) 833 | ) { 834 | if ( hasOwn.call( config, "previousModule" ) ) { 835 | runLoggingCallbacks( "moduleDone", { 836 | name: config.previousModule.name, 837 | tests: config.previousModule.tests, 838 | failed: config.moduleStats.bad, 839 | passed: config.moduleStats.all - config.moduleStats.bad, 840 | total: config.moduleStats.all, 841 | runtime: now() - config.moduleStats.started 842 | }); 843 | } 844 | config.previousModule = this.module; 845 | config.moduleStats = { all: 0, bad: 0, started: now() }; 846 | runLoggingCallbacks( "moduleStart", { 847 | name: this.module.name, 848 | tests: this.module.tests 849 | }); 850 | } 851 | 852 | config.current = this; 853 | 854 | if ( this.module.testEnvironment ) { 855 | delete this.module.testEnvironment.beforeEach; 856 | delete this.module.testEnvironment.afterEach; 857 | } 858 | this.testEnvironment = extend( {}, this.module.testEnvironment ); 859 | 860 | this.started = now(); 861 | runLoggingCallbacks( "testStart", { 862 | name: this.testName, 863 | module: this.module.name, 864 | testId: this.testId 865 | }); 866 | 867 | if ( !config.pollution ) { 868 | saveGlobal(); 869 | } 870 | }, 871 | 872 | run: function() { 873 | var promise; 874 | 875 | config.current = this; 876 | 877 | if ( this.async ) { 878 | QUnit.stop(); 879 | } 880 | 881 | this.callbackStarted = now(); 882 | 883 | if ( config.notrycatch ) { 884 | runTest( this ); 885 | return; 886 | } 887 | 888 | try { 889 | runTest( this ); 890 | } catch ( e ) { 891 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + 892 | this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 893 | 894 | // else next test will carry the responsibility 895 | saveGlobal(); 896 | 897 | // Restart the tests if they're blocking 898 | if ( config.blocking ) { 899 | QUnit.start(); 900 | } 901 | } 902 | 903 | function runTest( test ) { 904 | promise = test.callback.call( test.testEnvironment, test.assert ); 905 | test.resolvePromise( promise ); 906 | } 907 | }, 908 | 909 | after: function() { 910 | checkPollution(); 911 | }, 912 | 913 | queueHook: function( hook, hookName ) { 914 | var promise, 915 | test = this; 916 | return function runHook() { 917 | config.current = test; 918 | if ( config.notrycatch ) { 919 | callHook(); 920 | return; 921 | } 922 | try { 923 | callHook(); 924 | } catch ( error ) { 925 | test.pushFailure( hookName + " failed on " + test.testName + ": " + 926 | ( error.message || error ), extractStacktrace( error, 0 ) ); 927 | } 928 | 929 | function callHook() { 930 | promise = hook.call( test.testEnvironment, test.assert ); 931 | test.resolvePromise( promise, hookName ); 932 | } 933 | }; 934 | }, 935 | 936 | // Currently only used for module level hooks, can be used to add global level ones 937 | hooks: function( handler ) { 938 | var hooks = []; 939 | 940 | function processHooks( test, module ) { 941 | if ( module.parentModule ) { 942 | processHooks( test, module.parentModule ); 943 | } 944 | if ( module.testEnvironment && 945 | QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { 946 | hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); 947 | } 948 | } 949 | 950 | // Hooks are ignored on skipped tests 951 | if ( !this.skip ) { 952 | processHooks( this, this.module ); 953 | } 954 | return hooks; 955 | }, 956 | 957 | finish: function() { 958 | config.current = this; 959 | if ( config.requireExpects && this.expected === null ) { 960 | this.pushFailure( "Expected number of assertions to be defined, but expect() was " + 961 | "not called.", this.stack ); 962 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 963 | this.pushFailure( "Expected " + this.expected + " assertions, but " + 964 | this.assertions.length + " were run", this.stack ); 965 | } else if ( this.expected === null && !this.assertions.length ) { 966 | this.pushFailure( "Expected at least one assertion, but none were run - call " + 967 | "expect(0) to accept zero assertions.", this.stack ); 968 | } 969 | 970 | var i, 971 | bad = 0; 972 | 973 | this.runtime = now() - this.started; 974 | config.stats.all += this.assertions.length; 975 | config.moduleStats.all += this.assertions.length; 976 | 977 | for ( i = 0; i < this.assertions.length; i++ ) { 978 | if ( !this.assertions[ i ].result ) { 979 | bad++; 980 | config.stats.bad++; 981 | config.moduleStats.bad++; 982 | } 983 | } 984 | 985 | runLoggingCallbacks( "testDone", { 986 | name: this.testName, 987 | module: this.module.name, 988 | skipped: !!this.skip, 989 | failed: bad, 990 | passed: this.assertions.length - bad, 991 | total: this.assertions.length, 992 | runtime: this.runtime, 993 | 994 | // HTML Reporter use 995 | assertions: this.assertions, 996 | testId: this.testId, 997 | 998 | // Source of Test 999 | source: this.stack, 1000 | 1001 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 1002 | duration: this.runtime 1003 | }); 1004 | 1005 | // QUnit.reset() is deprecated and will be replaced for a new 1006 | // fixture reset function on QUnit 2.0/2.1. 1007 | // It's still called here for backwards compatibility handling 1008 | QUnit.reset(); 1009 | 1010 | config.current = undefined; 1011 | }, 1012 | 1013 | queue: function() { 1014 | var priority, 1015 | test = this; 1016 | 1017 | if ( !this.valid() ) { 1018 | return; 1019 | } 1020 | 1021 | function run() { 1022 | 1023 | // each of these can by async 1024 | synchronize([ 1025 | function() { 1026 | test.before(); 1027 | }, 1028 | 1029 | test.hooks( "beforeEach" ), 1030 | function() { 1031 | test.run(); 1032 | }, 1033 | 1034 | test.hooks( "afterEach" ).reverse(), 1035 | 1036 | function() { 1037 | test.after(); 1038 | }, 1039 | function() { 1040 | test.finish(); 1041 | } 1042 | ]); 1043 | } 1044 | 1045 | // Prioritize previously failed tests, detected from sessionStorage 1046 | priority = QUnit.config.reorder && defined.sessionStorage && 1047 | +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); 1048 | 1049 | return synchronize( run, priority ); 1050 | }, 1051 | 1052 | push: function( result, actual, expected, message, negative ) { 1053 | var source, 1054 | details = { 1055 | module: this.module.name, 1056 | name: this.testName, 1057 | result: result, 1058 | message: message, 1059 | actual: actual, 1060 | expected: expected, 1061 | testId: this.testId, 1062 | negative: negative || false, 1063 | runtime: now() - this.started 1064 | }; 1065 | 1066 | if ( !result ) { 1067 | source = sourceFromStacktrace(); 1068 | 1069 | if ( source ) { 1070 | details.source = source; 1071 | } 1072 | } 1073 | 1074 | runLoggingCallbacks( "log", details ); 1075 | 1076 | this.assertions.push({ 1077 | result: !!result, 1078 | message: message 1079 | }); 1080 | }, 1081 | 1082 | pushFailure: function( message, source, actual ) { 1083 | if ( !( this instanceof Test ) ) { 1084 | throw new Error( "pushFailure() assertion outside test context, was " + 1085 | sourceFromStacktrace( 2 ) ); 1086 | } 1087 | 1088 | var details = { 1089 | module: this.module.name, 1090 | name: this.testName, 1091 | result: false, 1092 | message: message || "error", 1093 | actual: actual || null, 1094 | testId: this.testId, 1095 | runtime: now() - this.started 1096 | }; 1097 | 1098 | if ( source ) { 1099 | details.source = source; 1100 | } 1101 | 1102 | runLoggingCallbacks( "log", details ); 1103 | 1104 | this.assertions.push({ 1105 | result: false, 1106 | message: message 1107 | }); 1108 | }, 1109 | 1110 | resolvePromise: function( promise, phase ) { 1111 | var then, message, 1112 | test = this; 1113 | if ( promise != null ) { 1114 | then = promise.then; 1115 | if ( QUnit.objectType( then ) === "function" ) { 1116 | QUnit.stop(); 1117 | then.call( 1118 | promise, 1119 | function() { QUnit.start(); }, 1120 | function( error ) { 1121 | message = "Promise rejected " + 1122 | ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + 1123 | " " + test.testName + ": " + ( error.message || error ); 1124 | test.pushFailure( message, extractStacktrace( error, 0 ) ); 1125 | 1126 | // else next test will carry the responsibility 1127 | saveGlobal(); 1128 | 1129 | // Unblock 1130 | QUnit.start(); 1131 | } 1132 | ); 1133 | } 1134 | } 1135 | }, 1136 | 1137 | valid: function() { 1138 | var filter = config.filter, 1139 | regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ), 1140 | module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), 1141 | fullName = ( this.module.name + ": " + this.testName ); 1142 | 1143 | function testInModuleChain( testModule ) { 1144 | var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; 1145 | if ( testModuleName === module ) { 1146 | return true; 1147 | } else if ( testModule.parentModule ) { 1148 | return testInModuleChain( testModule.parentModule ); 1149 | } else { 1150 | return false; 1151 | } 1152 | } 1153 | 1154 | // Internally-generated tests are always valid 1155 | if ( this.callback && this.callback.validTest ) { 1156 | return true; 1157 | } 1158 | 1159 | if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { 1160 | return false; 1161 | } 1162 | 1163 | if ( module && !testInModuleChain( this.module ) ) { 1164 | return false; 1165 | } 1166 | 1167 | if ( !filter ) { 1168 | return true; 1169 | } 1170 | 1171 | return regexFilter ? 1172 | this.regexFilter( !!regexFilter[1], regexFilter[2], regexFilter[3], fullName ) : 1173 | this.stringFilter( filter, fullName ); 1174 | }, 1175 | 1176 | regexFilter: function( exclude, pattern, flags, fullName ) { 1177 | var regex = new RegExp( pattern, flags ); 1178 | var match = regex.test( fullName ); 1179 | 1180 | return match !== exclude; 1181 | }, 1182 | 1183 | stringFilter: function( filter, fullName ) { 1184 | filter = filter.toLowerCase(); 1185 | fullName = fullName.toLowerCase(); 1186 | 1187 | var include = filter.charAt( 0 ) !== "!"; 1188 | if ( !include ) { 1189 | filter = filter.slice( 1 ); 1190 | } 1191 | 1192 | // If the filter matches, we need to honour include 1193 | if ( fullName.indexOf( filter ) !== -1 ) { 1194 | return include; 1195 | } 1196 | 1197 | // Otherwise, do the opposite 1198 | return !include; 1199 | } 1200 | }; 1201 | 1202 | // Resets the test setup. Useful for tests that modify the DOM. 1203 | /* 1204 | DEPRECATED: Use multiple tests instead of resetting inside a test. 1205 | Use testStart or testDone for custom cleanup. 1206 | This method will throw an error in 2.0, and will be removed in 2.1 1207 | */ 1208 | QUnit.reset = function() { 1209 | 1210 | // Return on non-browser environments 1211 | // This is necessary to not break on node tests 1212 | if ( !defined.document ) { 1213 | return; 1214 | } 1215 | 1216 | var fixture = defined.document && document.getElementById && 1217 | document.getElementById( "qunit-fixture" ); 1218 | 1219 | if ( fixture ) { 1220 | fixture.innerHTML = config.fixture; 1221 | } 1222 | }; 1223 | 1224 | QUnit.pushFailure = function() { 1225 | if ( !QUnit.config.current ) { 1226 | throw new Error( "pushFailure() assertion outside test context, in " + 1227 | sourceFromStacktrace( 2 ) ); 1228 | } 1229 | 1230 | // Gets current test obj 1231 | var currentTest = QUnit.config.current; 1232 | 1233 | return currentTest.pushFailure.apply( currentTest, arguments ); 1234 | }; 1235 | 1236 | // Based on Java's String.hashCode, a simple but not 1237 | // rigorously collision resistant hashing function 1238 | function generateHash( module, testName ) { 1239 | var hex, 1240 | i = 0, 1241 | hash = 0, 1242 | str = module + "\x1C" + testName, 1243 | len = str.length; 1244 | 1245 | for ( ; i < len; i++ ) { 1246 | hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); 1247 | hash |= 0; 1248 | } 1249 | 1250 | // Convert the possibly negative integer hash code into an 8 character hex string, which isn't 1251 | // strictly necessary but increases user understanding that the id is a SHA-like hash 1252 | hex = ( 0x100000000 + hash ).toString( 16 ); 1253 | if ( hex.length < 8 ) { 1254 | hex = "0000000" + hex; 1255 | } 1256 | 1257 | return hex.slice( -8 ); 1258 | } 1259 | 1260 | function synchronize( callback, priority ) { 1261 | var last = !priority; 1262 | 1263 | if ( QUnit.objectType( callback ) === "array" ) { 1264 | while ( callback.length ) { 1265 | synchronize( callback.shift() ); 1266 | } 1267 | return; 1268 | } 1269 | 1270 | if ( priority ) { 1271 | config.queue.splice( priorityCount++, 0, callback ); 1272 | } else { 1273 | config.queue.push( callback ); 1274 | } 1275 | 1276 | if ( config.autorun && !config.blocking ) { 1277 | process( last ); 1278 | } 1279 | } 1280 | 1281 | function saveGlobal() { 1282 | config.pollution = []; 1283 | 1284 | if ( config.noglobals ) { 1285 | for ( var key in global ) { 1286 | if ( hasOwn.call( global, key ) ) { 1287 | 1288 | // in Opera sometimes DOM element ids show up here, ignore them 1289 | if ( /^qunit-test-output/.test( key ) ) { 1290 | continue; 1291 | } 1292 | config.pollution.push( key ); 1293 | } 1294 | } 1295 | } 1296 | } 1297 | 1298 | function checkPollution() { 1299 | var newGlobals, 1300 | deletedGlobals, 1301 | old = config.pollution; 1302 | 1303 | saveGlobal(); 1304 | 1305 | newGlobals = diff( config.pollution, old ); 1306 | if ( newGlobals.length > 0 ) { 1307 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 1308 | } 1309 | 1310 | deletedGlobals = diff( old, config.pollution ); 1311 | if ( deletedGlobals.length > 0 ) { 1312 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 1313 | } 1314 | } 1315 | 1316 | // Will be exposed as QUnit.asyncTest 1317 | function asyncTest( testName, expected, callback ) { 1318 | if ( arguments.length === 2 ) { 1319 | callback = expected; 1320 | expected = null; 1321 | } 1322 | 1323 | QUnit.test( testName, expected, callback, true ); 1324 | } 1325 | 1326 | // Will be exposed as QUnit.test 1327 | function test( testName, expected, callback, async ) { 1328 | if ( focused ) { return; } 1329 | 1330 | var newTest; 1331 | 1332 | if ( arguments.length === 2 ) { 1333 | callback = expected; 1334 | expected = null; 1335 | } 1336 | 1337 | newTest = new Test({ 1338 | testName: testName, 1339 | expected: expected, 1340 | async: async, 1341 | callback: callback 1342 | }); 1343 | 1344 | newTest.queue(); 1345 | } 1346 | 1347 | // Will be exposed as QUnit.skip 1348 | function skip( testName ) { 1349 | if ( focused ) { return; } 1350 | 1351 | var test = new Test({ 1352 | testName: testName, 1353 | skip: true 1354 | }); 1355 | 1356 | test.queue(); 1357 | } 1358 | 1359 | // Will be exposed as QUnit.only 1360 | function only( testName, expected, callback, async ) { 1361 | var newTest; 1362 | 1363 | if ( focused ) { return; } 1364 | 1365 | QUnit.config.queue.length = 0; 1366 | focused = true; 1367 | 1368 | if ( arguments.length === 2 ) { 1369 | callback = expected; 1370 | expected = null; 1371 | } 1372 | 1373 | newTest = new Test({ 1374 | testName: testName, 1375 | expected: expected, 1376 | async: async, 1377 | callback: callback 1378 | }); 1379 | 1380 | newTest.queue(); 1381 | } 1382 | 1383 | function Assert( testContext ) { 1384 | this.test = testContext; 1385 | } 1386 | 1387 | // Assert helpers 1388 | QUnit.assert = Assert.prototype = { 1389 | 1390 | // Specify the number of expected assertions to guarantee that failed test 1391 | // (no assertions are run at all) don't slip through. 1392 | expect: function( asserts ) { 1393 | if ( arguments.length === 1 ) { 1394 | this.test.expected = asserts; 1395 | } else { 1396 | return this.test.expected; 1397 | } 1398 | }, 1399 | 1400 | // Increment this Test's semaphore counter, then return a function that 1401 | // decrements that counter a maximum of once. 1402 | async: function( count ) { 1403 | var test = this.test, 1404 | popped = false, 1405 | acceptCallCount = count; 1406 | 1407 | if ( typeof acceptCallCount === "undefined" ) { 1408 | acceptCallCount = 1; 1409 | } 1410 | 1411 | test.semaphore += 1; 1412 | test.usedAsync = true; 1413 | pauseProcessing(); 1414 | 1415 | return function done() { 1416 | 1417 | if ( popped ) { 1418 | test.pushFailure( "Too many calls to the `assert.async` callback", 1419 | sourceFromStacktrace( 2 ) ); 1420 | return; 1421 | } 1422 | acceptCallCount -= 1; 1423 | if ( acceptCallCount > 0 ) { 1424 | return; 1425 | } 1426 | 1427 | test.semaphore -= 1; 1428 | popped = true; 1429 | resumeProcessing(); 1430 | }; 1431 | }, 1432 | 1433 | // Exports test.push() to the user API 1434 | push: function( /* result, actual, expected, message, negative */ ) { 1435 | var assert = this, 1436 | currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; 1437 | 1438 | // Backwards compatibility fix. 1439 | // Allows the direct use of global exported assertions and QUnit.assert.* 1440 | // Although, it's use is not recommended as it can leak assertions 1441 | // to other tests from async tests, because we only get a reference to the current test, 1442 | // not exactly the test where assertion were intended to be called. 1443 | if ( !currentTest ) { 1444 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 1445 | } 1446 | 1447 | if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { 1448 | currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", 1449 | sourceFromStacktrace( 2 ) ); 1450 | 1451 | // Allow this assertion to continue running anyway... 1452 | } 1453 | 1454 | if ( !( assert instanceof Assert ) ) { 1455 | assert = currentTest.assert; 1456 | } 1457 | return assert.test.push.apply( assert.test, arguments ); 1458 | }, 1459 | 1460 | ok: function( result, message ) { 1461 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1462 | QUnit.dump.parse( result ) ); 1463 | this.push( !!result, result, true, message ); 1464 | }, 1465 | 1466 | notOk: function( result, message ) { 1467 | message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + 1468 | QUnit.dump.parse( result ) ); 1469 | this.push( !result, result, false, message ); 1470 | }, 1471 | 1472 | equal: function( actual, expected, message ) { 1473 | /*jshint eqeqeq:false */ 1474 | this.push( expected == actual, actual, expected, message ); 1475 | }, 1476 | 1477 | notEqual: function( actual, expected, message ) { 1478 | /*jshint eqeqeq:false */ 1479 | this.push( expected != actual, actual, expected, message, true ); 1480 | }, 1481 | 1482 | propEqual: function( actual, expected, message ) { 1483 | actual = objectValues( actual ); 1484 | expected = objectValues( expected ); 1485 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1486 | }, 1487 | 1488 | notPropEqual: function( actual, expected, message ) { 1489 | actual = objectValues( actual ); 1490 | expected = objectValues( expected ); 1491 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); 1492 | }, 1493 | 1494 | deepEqual: function( actual, expected, message ) { 1495 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1496 | }, 1497 | 1498 | notDeepEqual: function( actual, expected, message ) { 1499 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); 1500 | }, 1501 | 1502 | strictEqual: function( actual, expected, message ) { 1503 | this.push( expected === actual, actual, expected, message ); 1504 | }, 1505 | 1506 | notStrictEqual: function( actual, expected, message ) { 1507 | this.push( expected !== actual, actual, expected, message, true ); 1508 | }, 1509 | 1510 | "throws": function( block, expected, message ) { 1511 | var actual, expectedType, 1512 | expectedOutput = expected, 1513 | ok = false, 1514 | currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; 1515 | 1516 | // 'expected' is optional unless doing string comparison 1517 | if ( message == null && typeof expected === "string" ) { 1518 | message = expected; 1519 | expected = null; 1520 | } 1521 | 1522 | currentTest.ignoreGlobalErrors = true; 1523 | try { 1524 | block.call( currentTest.testEnvironment ); 1525 | } catch (e) { 1526 | actual = e; 1527 | } 1528 | currentTest.ignoreGlobalErrors = false; 1529 | 1530 | if ( actual ) { 1531 | expectedType = QUnit.objectType( expected ); 1532 | 1533 | // we don't want to validate thrown error 1534 | if ( !expected ) { 1535 | ok = true; 1536 | expectedOutput = null; 1537 | 1538 | // expected is a regexp 1539 | } else if ( expectedType === "regexp" ) { 1540 | ok = expected.test( errorString( actual ) ); 1541 | 1542 | // expected is a string 1543 | } else if ( expectedType === "string" ) { 1544 | ok = expected === errorString( actual ); 1545 | 1546 | // expected is a constructor, maybe an Error constructor 1547 | } else if ( expectedType === "function" && actual instanceof expected ) { 1548 | ok = true; 1549 | 1550 | // expected is an Error object 1551 | } else if ( expectedType === "object" ) { 1552 | ok = actual instanceof expected.constructor && 1553 | actual.name === expected.name && 1554 | actual.message === expected.message; 1555 | 1556 | // expected is a validation function which returns true if validation passed 1557 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1558 | expectedOutput = null; 1559 | ok = true; 1560 | } 1561 | } 1562 | 1563 | currentTest.assert.push( ok, actual, expectedOutput, message ); 1564 | } 1565 | }; 1566 | 1567 | // Provide an alternative to assert.throws(), for environments that consider throws a reserved word 1568 | // Known to us are: Closure Compiler, Narwhal 1569 | (function() { 1570 | /*jshint sub:true */ 1571 | Assert.prototype.raises = Assert.prototype[ "throws" ]; 1572 | }()); 1573 | 1574 | function errorString( error ) { 1575 | var name, message, 1576 | resultErrorString = error.toString(); 1577 | if ( resultErrorString.substring( 0, 7 ) === "[object" ) { 1578 | name = error.name ? error.name.toString() : "Error"; 1579 | message = error.message ? error.message.toString() : ""; 1580 | if ( name && message ) { 1581 | return name + ": " + message; 1582 | } else if ( name ) { 1583 | return name; 1584 | } else if ( message ) { 1585 | return message; 1586 | } else { 1587 | return "Error"; 1588 | } 1589 | } else { 1590 | return resultErrorString; 1591 | } 1592 | } 1593 | 1594 | // Test for equality any JavaScript type. 1595 | // Author: Philippe Rathé 1596 | QUnit.equiv = (function() { 1597 | 1598 | // Stack to decide between skip/abort functions 1599 | var callers = []; 1600 | 1601 | // Stack to avoiding loops from circular referencing 1602 | var parents = []; 1603 | var parentsB = []; 1604 | 1605 | var getProto = Object.getPrototypeOf || function( obj ) { 1606 | 1607 | /*jshint proto: true */ 1608 | return obj.__proto__; 1609 | }; 1610 | 1611 | function useStrictEquality( b, a ) { 1612 | 1613 | // To catch short annotation VS 'new' annotation of a declaration. e.g.: 1614 | // `var i = 1;` 1615 | // `var j = new Number(1);` 1616 | if ( typeof a === "object" ) { 1617 | a = a.valueOf(); 1618 | } 1619 | if ( typeof b === "object" ) { 1620 | b = b.valueOf(); 1621 | } 1622 | 1623 | return a === b; 1624 | } 1625 | 1626 | function compareConstructors( a, b ) { 1627 | var protoA = getProto( a ); 1628 | var protoB = getProto( b ); 1629 | 1630 | // Comparing constructors is more strict than using `instanceof` 1631 | if ( a.constructor === b.constructor ) { 1632 | return true; 1633 | } 1634 | 1635 | // Ref #851 1636 | // If the obj prototype descends from a null constructor, treat it 1637 | // as a null prototype. 1638 | if ( protoA && protoA.constructor === null ) { 1639 | protoA = null; 1640 | } 1641 | if ( protoB && protoB.constructor === null ) { 1642 | protoB = null; 1643 | } 1644 | 1645 | // Allow objects with no prototype to be equivalent to 1646 | // objects with Object as their constructor. 1647 | if ( ( protoA === null && protoB === Object.prototype ) || 1648 | ( protoB === null && protoA === Object.prototype ) ) { 1649 | return true; 1650 | } 1651 | 1652 | return false; 1653 | } 1654 | 1655 | function getRegExpFlags( regexp ) { 1656 | return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ]; 1657 | } 1658 | 1659 | var callbacks = { 1660 | "string": useStrictEquality, 1661 | "boolean": useStrictEquality, 1662 | "number": useStrictEquality, 1663 | "null": useStrictEquality, 1664 | "undefined": useStrictEquality, 1665 | "symbol": useStrictEquality, 1666 | "date": useStrictEquality, 1667 | 1668 | "nan": function() { 1669 | return true; 1670 | }, 1671 | 1672 | "regexp": function( b, a ) { 1673 | return a.source === b.source && 1674 | 1675 | // Include flags in the comparison 1676 | getRegExpFlags( a ) === getRegExpFlags( b ); 1677 | }, 1678 | 1679 | // - skip when the property is a method of an instance (OOP) 1680 | // - abort otherwise, 1681 | // initial === would have catch identical references anyway 1682 | "function": function() { 1683 | var caller = callers[ callers.length - 1 ]; 1684 | return caller !== Object && typeof caller !== "undefined"; 1685 | }, 1686 | 1687 | "array": function( b, a ) { 1688 | var i, j, len, loop, aCircular, bCircular; 1689 | 1690 | len = a.length; 1691 | if ( len !== b.length ) { 1692 | // safe and faster 1693 | return false; 1694 | } 1695 | 1696 | // Track reference to avoid circular references 1697 | parents.push( a ); 1698 | parentsB.push( b ); 1699 | for ( i = 0; i < len; i++ ) { 1700 | loop = false; 1701 | for ( j = 0; j < parents.length; j++ ) { 1702 | aCircular = parents[ j ] === a[ i ]; 1703 | bCircular = parentsB[ j ] === b[ i ]; 1704 | if ( aCircular || bCircular ) { 1705 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1706 | loop = true; 1707 | } else { 1708 | parents.pop(); 1709 | parentsB.pop(); 1710 | return false; 1711 | } 1712 | } 1713 | } 1714 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1715 | parents.pop(); 1716 | parentsB.pop(); 1717 | return false; 1718 | } 1719 | } 1720 | parents.pop(); 1721 | parentsB.pop(); 1722 | return true; 1723 | }, 1724 | 1725 | "set": function( b, a ) { 1726 | var aArray, bArray; 1727 | 1728 | aArray = []; 1729 | a.forEach( function( v ) { 1730 | aArray.push( v ); 1731 | }); 1732 | bArray = []; 1733 | b.forEach( function( v ) { 1734 | bArray.push( v ); 1735 | }); 1736 | 1737 | return innerEquiv( bArray, aArray ); 1738 | }, 1739 | 1740 | "map": function( b, a ) { 1741 | var aArray, bArray; 1742 | 1743 | aArray = []; 1744 | a.forEach( function( v, k ) { 1745 | aArray.push( [ k, v ] ); 1746 | }); 1747 | bArray = []; 1748 | b.forEach( function( v, k ) { 1749 | bArray.push( [ k, v ] ); 1750 | }); 1751 | 1752 | return innerEquiv( bArray, aArray ); 1753 | }, 1754 | 1755 | "object": function( b, a ) { 1756 | var i, j, loop, aCircular, bCircular; 1757 | 1758 | // Default to true 1759 | var eq = true; 1760 | var aProperties = []; 1761 | var bProperties = []; 1762 | 1763 | if ( compareConstructors( a, b ) === false ) { 1764 | return false; 1765 | } 1766 | 1767 | // Stack constructor before traversing properties 1768 | callers.push( a.constructor ); 1769 | 1770 | // Track reference to avoid circular references 1771 | parents.push( a ); 1772 | parentsB.push( b ); 1773 | 1774 | // Be strict: don't ensure hasOwnProperty and go deep 1775 | for ( i in a ) { 1776 | loop = false; 1777 | for ( j = 0; j < parents.length; j++ ) { 1778 | aCircular = parents[ j ] === a[ i ]; 1779 | bCircular = parentsB[ j ] === b[ i ]; 1780 | if ( aCircular || bCircular ) { 1781 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1782 | loop = true; 1783 | } else { 1784 | eq = false; 1785 | break; 1786 | } 1787 | } 1788 | } 1789 | aProperties.push( i ); 1790 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1791 | eq = false; 1792 | break; 1793 | } 1794 | } 1795 | 1796 | parents.pop(); 1797 | parentsB.pop(); 1798 | 1799 | // Unstack, we are done 1800 | callers.pop(); 1801 | 1802 | for ( i in b ) { 1803 | 1804 | // Collect b's properties 1805 | bProperties.push( i ); 1806 | } 1807 | 1808 | // Ensures identical properties name 1809 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1810 | } 1811 | }; 1812 | 1813 | function typeEquiv( a, b ) { 1814 | var type = QUnit.objectType( a ); 1815 | return QUnit.objectType( b ) === type && callbacks[ type ]( b, a ); 1816 | } 1817 | 1818 | // The real equiv function 1819 | function innerEquiv( a, b ) { 1820 | 1821 | // We're done when there's nothing more to compare 1822 | if ( arguments.length < 2 ) { 1823 | return true; 1824 | } 1825 | 1826 | // Require type-specific equality 1827 | return ( a === b || typeEquiv( a, b ) ) && 1828 | 1829 | // ...across all consecutive argument pairs 1830 | ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) ); 1831 | } 1832 | 1833 | return innerEquiv; 1834 | }()); 1835 | 1836 | // Based on jsDump by Ariel Flesler 1837 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1838 | QUnit.dump = (function() { 1839 | function quote( str ) { 1840 | return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; 1841 | } 1842 | function literal( o ) { 1843 | return o + ""; 1844 | } 1845 | function join( pre, arr, post ) { 1846 | var s = dump.separator(), 1847 | base = dump.indent(), 1848 | inner = dump.indent( 1 ); 1849 | if ( arr.join ) { 1850 | arr = arr.join( "," + s + inner ); 1851 | } 1852 | if ( !arr ) { 1853 | return pre + post; 1854 | } 1855 | return [ pre, inner + arr, base + post ].join( s ); 1856 | } 1857 | function array( arr, stack ) { 1858 | var i = arr.length, 1859 | ret = new Array( i ); 1860 | 1861 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1862 | return "[object Array]"; 1863 | } 1864 | 1865 | this.up(); 1866 | while ( i-- ) { 1867 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1868 | } 1869 | this.down(); 1870 | return join( "[", ret, "]" ); 1871 | } 1872 | 1873 | var reName = /^function (\w+)/, 1874 | dump = { 1875 | 1876 | // objType is used mostly internally, you can fix a (custom) type in advance 1877 | parse: function( obj, objType, stack ) { 1878 | stack = stack || []; 1879 | var res, parser, parserType, 1880 | inStack = inArray( obj, stack ); 1881 | 1882 | if ( inStack !== -1 ) { 1883 | return "recursion(" + ( inStack - stack.length ) + ")"; 1884 | } 1885 | 1886 | objType = objType || this.typeOf( obj ); 1887 | parser = this.parsers[ objType ]; 1888 | parserType = typeof parser; 1889 | 1890 | if ( parserType === "function" ) { 1891 | stack.push( obj ); 1892 | res = parser.call( this, obj, stack ); 1893 | stack.pop(); 1894 | return res; 1895 | } 1896 | return ( parserType === "string" ) ? parser : this.parsers.error; 1897 | }, 1898 | typeOf: function( obj ) { 1899 | var type; 1900 | if ( obj === null ) { 1901 | type = "null"; 1902 | } else if ( typeof obj === "undefined" ) { 1903 | type = "undefined"; 1904 | } else if ( QUnit.is( "regexp", obj ) ) { 1905 | type = "regexp"; 1906 | } else if ( QUnit.is( "date", obj ) ) { 1907 | type = "date"; 1908 | } else if ( QUnit.is( "function", obj ) ) { 1909 | type = "function"; 1910 | } else if ( obj.setInterval !== undefined && 1911 | obj.document !== undefined && 1912 | obj.nodeType === undefined ) { 1913 | type = "window"; 1914 | } else if ( obj.nodeType === 9 ) { 1915 | type = "document"; 1916 | } else if ( obj.nodeType ) { 1917 | type = "node"; 1918 | } else if ( 1919 | 1920 | // native arrays 1921 | toString.call( obj ) === "[object Array]" || 1922 | 1923 | // NodeList objects 1924 | ( typeof obj.length === "number" && obj.item !== undefined && 1925 | ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && 1926 | obj[ 0 ] === undefined ) ) ) 1927 | ) { 1928 | type = "array"; 1929 | } else if ( obj.constructor === Error.prototype.constructor ) { 1930 | type = "error"; 1931 | } else { 1932 | type = typeof obj; 1933 | } 1934 | return type; 1935 | }, 1936 | separator: function() { 1937 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; 1938 | }, 1939 | // extra can be a number, shortcut for increasing-calling-decreasing 1940 | indent: function( extra ) { 1941 | if ( !this.multiline ) { 1942 | return ""; 1943 | } 1944 | var chr = this.indentChar; 1945 | if ( this.HTML ) { 1946 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1947 | } 1948 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 1949 | }, 1950 | up: function( a ) { 1951 | this.depth += a || 1; 1952 | }, 1953 | down: function( a ) { 1954 | this.depth -= a || 1; 1955 | }, 1956 | setParser: function( name, parser ) { 1957 | this.parsers[ name ] = parser; 1958 | }, 1959 | // The next 3 are exposed so you can use them 1960 | quote: quote, 1961 | literal: literal, 1962 | join: join, 1963 | // 1964 | depth: 1, 1965 | maxDepth: QUnit.config.maxDepth, 1966 | 1967 | // This is the list of parsers, to modify them, use dump.setParser 1968 | parsers: { 1969 | window: "[Window]", 1970 | document: "[Document]", 1971 | error: function( error ) { 1972 | return "Error(\"" + error.message + "\")"; 1973 | }, 1974 | unknown: "[Unknown]", 1975 | "null": "null", 1976 | "undefined": "undefined", 1977 | "function": function( fn ) { 1978 | var ret = "function", 1979 | 1980 | // functions never have name in IE 1981 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 1982 | 1983 | if ( name ) { 1984 | ret += " " + name; 1985 | } 1986 | ret += "( "; 1987 | 1988 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1989 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 1990 | }, 1991 | array: array, 1992 | nodelist: array, 1993 | "arguments": array, 1994 | object: function( map, stack ) { 1995 | var keys, key, val, i, nonEnumerableProperties, 1996 | ret = []; 1997 | 1998 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1999 | return "[object Object]"; 2000 | } 2001 | 2002 | dump.up(); 2003 | keys = []; 2004 | for ( key in map ) { 2005 | keys.push( key ); 2006 | } 2007 | 2008 | // Some properties are not always enumerable on Error objects. 2009 | nonEnumerableProperties = [ "message", "name" ]; 2010 | for ( i in nonEnumerableProperties ) { 2011 | key = nonEnumerableProperties[ i ]; 2012 | if ( key in map && inArray( key, keys ) < 0 ) { 2013 | keys.push( key ); 2014 | } 2015 | } 2016 | keys.sort(); 2017 | for ( i = 0; i < keys.length; i++ ) { 2018 | key = keys[ i ]; 2019 | val = map[ key ]; 2020 | ret.push( dump.parse( key, "key" ) + ": " + 2021 | dump.parse( val, undefined, stack ) ); 2022 | } 2023 | dump.down(); 2024 | return join( "{", ret, "}" ); 2025 | }, 2026 | node: function( node ) { 2027 | var len, i, val, 2028 | open = dump.HTML ? "<" : "<", 2029 | close = dump.HTML ? ">" : ">", 2030 | tag = node.nodeName.toLowerCase(), 2031 | ret = open + tag, 2032 | attrs = node.attributes; 2033 | 2034 | if ( attrs ) { 2035 | for ( i = 0, len = attrs.length; i < len; i++ ) { 2036 | val = attrs[ i ].nodeValue; 2037 | 2038 | // IE6 includes all attributes in .attributes, even ones not explicitly 2039 | // set. Those have values like undefined, null, 0, false, "" or 2040 | // "inherit". 2041 | if ( val && val !== "inherit" ) { 2042 | ret += " " + attrs[ i ].nodeName + "=" + 2043 | dump.parse( val, "attribute" ); 2044 | } 2045 | } 2046 | } 2047 | ret += close; 2048 | 2049 | // Show content of TextNode or CDATASection 2050 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 2051 | ret += node.nodeValue; 2052 | } 2053 | 2054 | return ret + open + "/" + tag + close; 2055 | }, 2056 | 2057 | // function calls it internally, it's the arguments part of the function 2058 | functionArgs: function( fn ) { 2059 | var args, 2060 | l = fn.length; 2061 | 2062 | if ( !l ) { 2063 | return ""; 2064 | } 2065 | 2066 | args = new Array( l ); 2067 | while ( l-- ) { 2068 | 2069 | // 97 is 'a' 2070 | args[ l ] = String.fromCharCode( 97 + l ); 2071 | } 2072 | return " " + args.join( ", " ) + " "; 2073 | }, 2074 | // object calls it internally, the key part of an item in a map 2075 | key: quote, 2076 | // function calls it internally, it's the content of the function 2077 | functionCode: "[code]", 2078 | // node calls it internally, it's a html attribute value 2079 | attribute: quote, 2080 | string: quote, 2081 | date: quote, 2082 | regexp: literal, 2083 | number: literal, 2084 | "boolean": literal 2085 | }, 2086 | // if true, entities are escaped ( <, >, \t, space and \n ) 2087 | HTML: false, 2088 | // indentation unit 2089 | indentChar: " ", 2090 | // if true, items in a collection, are separated by a \n, else just a space. 2091 | multiline: true 2092 | }; 2093 | 2094 | return dump; 2095 | }()); 2096 | 2097 | // back compat 2098 | QUnit.jsDump = QUnit.dump; 2099 | 2100 | // For browser, export only select globals 2101 | if ( defined.document ) { 2102 | 2103 | // Deprecated 2104 | // Extend assert methods to QUnit and Global scope through Backwards compatibility 2105 | (function() { 2106 | var i, 2107 | assertions = Assert.prototype; 2108 | 2109 | function applyCurrent( current ) { 2110 | return function() { 2111 | var assert = new Assert( QUnit.config.current ); 2112 | current.apply( assert, arguments ); 2113 | }; 2114 | } 2115 | 2116 | for ( i in assertions ) { 2117 | QUnit[ i ] = applyCurrent( assertions[ i ] ); 2118 | } 2119 | })(); 2120 | 2121 | (function() { 2122 | var i, l, 2123 | keys = [ 2124 | "test", 2125 | "module", 2126 | "expect", 2127 | "asyncTest", 2128 | "start", 2129 | "stop", 2130 | "ok", 2131 | "notOk", 2132 | "equal", 2133 | "notEqual", 2134 | "propEqual", 2135 | "notPropEqual", 2136 | "deepEqual", 2137 | "notDeepEqual", 2138 | "strictEqual", 2139 | "notStrictEqual", 2140 | "throws", 2141 | "raises" 2142 | ]; 2143 | 2144 | for ( i = 0, l = keys.length; i < l; i++ ) { 2145 | window[ keys[ i ] ] = QUnit[ keys[ i ] ]; 2146 | } 2147 | })(); 2148 | 2149 | window.QUnit = QUnit; 2150 | } 2151 | 2152 | // For nodejs 2153 | if ( typeof module !== "undefined" && module && module.exports ) { 2154 | module.exports = QUnit; 2155 | 2156 | // For consistency with CommonJS environments' exports 2157 | module.exports.QUnit = QUnit; 2158 | } 2159 | 2160 | // For CommonJS with exports, but without module.exports, like Rhino 2161 | if ( typeof exports !== "undefined" && exports ) { 2162 | exports.QUnit = QUnit; 2163 | } 2164 | 2165 | if ( typeof define === "function" && define.amd ) { 2166 | define( function() { 2167 | return QUnit; 2168 | } ); 2169 | QUnit.config.autostart = false; 2170 | } 2171 | 2172 | /* 2173 | * This file is a modified version of google-diff-match-patch's JavaScript implementation 2174 | * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), 2175 | * modifications are licensed as more fully set forth in LICENSE.txt. 2176 | * 2177 | * The original source of google-diff-match-patch is attributable and licensed as follows: 2178 | * 2179 | * Copyright 2006 Google Inc. 2180 | * https://code.google.com/p/google-diff-match-patch/ 2181 | * 2182 | * Licensed under the Apache License, Version 2.0 (the "License"); 2183 | * you may not use this file except in compliance with the License. 2184 | * You may obtain a copy of the License at 2185 | * 2186 | * https://www.apache.org/licenses/LICENSE-2.0 2187 | * 2188 | * Unless required by applicable law or agreed to in writing, software 2189 | * distributed under the License is distributed on an "AS IS" BASIS, 2190 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 2191 | * See the License for the specific language governing permissions and 2192 | * limitations under the License. 2193 | * 2194 | * More Info: 2195 | * https://code.google.com/p/google-diff-match-patch/ 2196 | * 2197 | * Usage: QUnit.diff(expected, actual) 2198 | * 2199 | */ 2200 | QUnit.diff = ( function() { 2201 | function DiffMatchPatch() { 2202 | } 2203 | 2204 | // DIFF FUNCTIONS 2205 | 2206 | /** 2207 | * The data structure representing a diff is an array of tuples: 2208 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 2209 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 2210 | */ 2211 | var DIFF_DELETE = -1, 2212 | DIFF_INSERT = 1, 2213 | DIFF_EQUAL = 0; 2214 | 2215 | /** 2216 | * Find the differences between two texts. Simplifies the problem by stripping 2217 | * any common prefix or suffix off the texts before diffing. 2218 | * @param {string} text1 Old string to be diffed. 2219 | * @param {string} text2 New string to be diffed. 2220 | * @param {boolean=} optChecklines Optional speedup flag. If present and false, 2221 | * then don't run a line-level diff first to identify the changed areas. 2222 | * Defaults to true, which does a faster, slightly less optimal diff. 2223 | * @return {!Array.} Array of diff tuples. 2224 | */ 2225 | DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { 2226 | var deadline, checklines, commonlength, 2227 | commonprefix, commonsuffix, diffs; 2228 | 2229 | // The diff must be complete in up to 1 second. 2230 | deadline = ( new Date() ).getTime() + 1000; 2231 | 2232 | // Check for null inputs. 2233 | if ( text1 === null || text2 === null ) { 2234 | throw new Error( "Null input. (DiffMain)" ); 2235 | } 2236 | 2237 | // Check for equality (speedup). 2238 | if ( text1 === text2 ) { 2239 | if ( text1 ) { 2240 | return [ 2241 | [ DIFF_EQUAL, text1 ] 2242 | ]; 2243 | } 2244 | return []; 2245 | } 2246 | 2247 | if ( typeof optChecklines === "undefined" ) { 2248 | optChecklines = true; 2249 | } 2250 | 2251 | checklines = optChecklines; 2252 | 2253 | // Trim off common prefix (speedup). 2254 | commonlength = this.diffCommonPrefix( text1, text2 ); 2255 | commonprefix = text1.substring( 0, commonlength ); 2256 | text1 = text1.substring( commonlength ); 2257 | text2 = text2.substring( commonlength ); 2258 | 2259 | // Trim off common suffix (speedup). 2260 | commonlength = this.diffCommonSuffix( text1, text2 ); 2261 | commonsuffix = text1.substring( text1.length - commonlength ); 2262 | text1 = text1.substring( 0, text1.length - commonlength ); 2263 | text2 = text2.substring( 0, text2.length - commonlength ); 2264 | 2265 | // Compute the diff on the middle block. 2266 | diffs = this.diffCompute( text1, text2, checklines, deadline ); 2267 | 2268 | // Restore the prefix and suffix. 2269 | if ( commonprefix ) { 2270 | diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); 2271 | } 2272 | if ( commonsuffix ) { 2273 | diffs.push( [ DIFF_EQUAL, commonsuffix ] ); 2274 | } 2275 | this.diffCleanupMerge( diffs ); 2276 | return diffs; 2277 | }; 2278 | 2279 | /** 2280 | * Reduce the number of edits by eliminating operationally trivial equalities. 2281 | * @param {!Array.} diffs Array of diff tuples. 2282 | */ 2283 | DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { 2284 | var changes, equalities, equalitiesLength, lastequality, 2285 | pointer, preIns, preDel, postIns, postDel; 2286 | changes = false; 2287 | equalities = []; // Stack of indices where equalities are found. 2288 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 2289 | /** @type {?string} */ 2290 | lastequality = null; 2291 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 2292 | pointer = 0; // Index of current position. 2293 | // Is there an insertion operation before the last equality. 2294 | preIns = false; 2295 | // Is there a deletion operation before the last equality. 2296 | preDel = false; 2297 | // Is there an insertion operation after the last equality. 2298 | postIns = false; 2299 | // Is there a deletion operation after the last equality. 2300 | postDel = false; 2301 | while ( pointer < diffs.length ) { 2302 | 2303 | // Equality found. 2304 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { 2305 | if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { 2306 | 2307 | // Candidate found. 2308 | equalities[ equalitiesLength++ ] = pointer; 2309 | preIns = postIns; 2310 | preDel = postDel; 2311 | lastequality = diffs[ pointer ][ 1 ]; 2312 | } else { 2313 | 2314 | // Not a candidate, and can never become one. 2315 | equalitiesLength = 0; 2316 | lastequality = null; 2317 | } 2318 | postIns = postDel = false; 2319 | 2320 | // An insertion or deletion. 2321 | } else { 2322 | 2323 | if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { 2324 | postDel = true; 2325 | } else { 2326 | postIns = true; 2327 | } 2328 | 2329 | /* 2330 | * Five types to be split: 2331 | * ABXYCD 2332 | * AXCD 2333 | * ABXC 2334 | * AXCD 2335 | * ABXC 2336 | */ 2337 | if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || 2338 | ( ( lastequality.length < 2 ) && 2339 | ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { 2340 | 2341 | // Duplicate record. 2342 | diffs.splice( 2343 | equalities[ equalitiesLength - 1 ], 2344 | 0, 2345 | [ DIFF_DELETE, lastequality ] 2346 | ); 2347 | 2348 | // Change second copy to insert. 2349 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 2350 | equalitiesLength--; // Throw away the equality we just deleted; 2351 | lastequality = null; 2352 | if ( preIns && preDel ) { 2353 | // No changes made which could affect previous entry, keep going. 2354 | postIns = postDel = true; 2355 | equalitiesLength = 0; 2356 | } else { 2357 | equalitiesLength--; // Throw away the previous equality. 2358 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 2359 | postIns = postDel = false; 2360 | } 2361 | changes = true; 2362 | } 2363 | } 2364 | pointer++; 2365 | } 2366 | 2367 | if ( changes ) { 2368 | this.diffCleanupMerge( diffs ); 2369 | } 2370 | }; 2371 | 2372 | /** 2373 | * Convert a diff array into a pretty HTML report. 2374 | * @param {!Array.} diffs Array of diff tuples. 2375 | * @param {integer} string to be beautified. 2376 | * @return {string} HTML representation. 2377 | */ 2378 | DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { 2379 | var op, data, x, 2380 | html = []; 2381 | for ( x = 0; x < diffs.length; x++ ) { 2382 | op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) 2383 | data = diffs[ x ][ 1 ]; // Text of change. 2384 | switch ( op ) { 2385 | case DIFF_INSERT: 2386 | html[ x ] = "" + data + ""; 2387 | break; 2388 | case DIFF_DELETE: 2389 | html[ x ] = "" + data + ""; 2390 | break; 2391 | case DIFF_EQUAL: 2392 | html[ x ] = "" + data + ""; 2393 | break; 2394 | } 2395 | } 2396 | return html.join( "" ); 2397 | }; 2398 | 2399 | /** 2400 | * Determine the common prefix of two strings. 2401 | * @param {string} text1 First string. 2402 | * @param {string} text2 Second string. 2403 | * @return {number} The number of characters common to the start of each 2404 | * string. 2405 | */ 2406 | DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { 2407 | var pointermid, pointermax, pointermin, pointerstart; 2408 | // Quick check for common null cases. 2409 | if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { 2410 | return 0; 2411 | } 2412 | // Binary search. 2413 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 2414 | pointermin = 0; 2415 | pointermax = Math.min( text1.length, text2.length ); 2416 | pointermid = pointermax; 2417 | pointerstart = 0; 2418 | while ( pointermin < pointermid ) { 2419 | if ( text1.substring( pointerstart, pointermid ) === 2420 | text2.substring( pointerstart, pointermid ) ) { 2421 | pointermin = pointermid; 2422 | pointerstart = pointermin; 2423 | } else { 2424 | pointermax = pointermid; 2425 | } 2426 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 2427 | } 2428 | return pointermid; 2429 | }; 2430 | 2431 | /** 2432 | * Determine the common suffix of two strings. 2433 | * @param {string} text1 First string. 2434 | * @param {string} text2 Second string. 2435 | * @return {number} The number of characters common to the end of each string. 2436 | */ 2437 | DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { 2438 | var pointermid, pointermax, pointermin, pointerend; 2439 | // Quick check for common null cases. 2440 | if ( !text1 || 2441 | !text2 || 2442 | text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { 2443 | return 0; 2444 | } 2445 | // Binary search. 2446 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 2447 | pointermin = 0; 2448 | pointermax = Math.min( text1.length, text2.length ); 2449 | pointermid = pointermax; 2450 | pointerend = 0; 2451 | while ( pointermin < pointermid ) { 2452 | if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === 2453 | text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { 2454 | pointermin = pointermid; 2455 | pointerend = pointermin; 2456 | } else { 2457 | pointermax = pointermid; 2458 | } 2459 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 2460 | } 2461 | return pointermid; 2462 | }; 2463 | 2464 | /** 2465 | * Find the differences between two texts. Assumes that the texts do not 2466 | * have any common prefix or suffix. 2467 | * @param {string} text1 Old string to be diffed. 2468 | * @param {string} text2 New string to be diffed. 2469 | * @param {boolean} checklines Speedup flag. If false, then don't run a 2470 | * line-level diff first to identify the changed areas. 2471 | * If true, then run a faster, slightly less optimal diff. 2472 | * @param {number} deadline Time when the diff should be complete by. 2473 | * @return {!Array.} Array of diff tuples. 2474 | * @private 2475 | */ 2476 | DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { 2477 | var diffs, longtext, shorttext, i, hm, 2478 | text1A, text2A, text1B, text2B, 2479 | midCommon, diffsA, diffsB; 2480 | 2481 | if ( !text1 ) { 2482 | // Just add some text (speedup). 2483 | return [ 2484 | [ DIFF_INSERT, text2 ] 2485 | ]; 2486 | } 2487 | 2488 | if ( !text2 ) { 2489 | // Just delete some text (speedup). 2490 | return [ 2491 | [ DIFF_DELETE, text1 ] 2492 | ]; 2493 | } 2494 | 2495 | longtext = text1.length > text2.length ? text1 : text2; 2496 | shorttext = text1.length > text2.length ? text2 : text1; 2497 | i = longtext.indexOf( shorttext ); 2498 | if ( i !== -1 ) { 2499 | // Shorter text is inside the longer text (speedup). 2500 | diffs = [ 2501 | [ DIFF_INSERT, longtext.substring( 0, i ) ], 2502 | [ DIFF_EQUAL, shorttext ], 2503 | [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] 2504 | ]; 2505 | // Swap insertions for deletions if diff is reversed. 2506 | if ( text1.length > text2.length ) { 2507 | diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; 2508 | } 2509 | return diffs; 2510 | } 2511 | 2512 | if ( shorttext.length === 1 ) { 2513 | // Single character string. 2514 | // After the previous speedup, the character can't be an equality. 2515 | return [ 2516 | [ DIFF_DELETE, text1 ], 2517 | [ DIFF_INSERT, text2 ] 2518 | ]; 2519 | } 2520 | 2521 | // Check to see if the problem can be split in two. 2522 | hm = this.diffHalfMatch( text1, text2 ); 2523 | if ( hm ) { 2524 | // A half-match was found, sort out the return data. 2525 | text1A = hm[ 0 ]; 2526 | text1B = hm[ 1 ]; 2527 | text2A = hm[ 2 ]; 2528 | text2B = hm[ 3 ]; 2529 | midCommon = hm[ 4 ]; 2530 | // Send both pairs off for separate processing. 2531 | diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); 2532 | diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); 2533 | // Merge the results. 2534 | return diffsA.concat( [ 2535 | [ DIFF_EQUAL, midCommon ] 2536 | ], diffsB ); 2537 | } 2538 | 2539 | if ( checklines && text1.length > 100 && text2.length > 100 ) { 2540 | return this.diffLineMode( text1, text2, deadline ); 2541 | } 2542 | 2543 | return this.diffBisect( text1, text2, deadline ); 2544 | }; 2545 | 2546 | /** 2547 | * Do the two texts share a substring which is at least half the length of the 2548 | * longer text? 2549 | * This speedup can produce non-minimal diffs. 2550 | * @param {string} text1 First string. 2551 | * @param {string} text2 Second string. 2552 | * @return {Array.} Five element Array, containing the prefix of 2553 | * text1, the suffix of text1, the prefix of text2, the suffix of 2554 | * text2 and the common middle. Or null if there was no match. 2555 | * @private 2556 | */ 2557 | DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { 2558 | var longtext, shorttext, dmp, 2559 | text1A, text2B, text2A, text1B, midCommon, 2560 | hm1, hm2, hm; 2561 | 2562 | longtext = text1.length > text2.length ? text1 : text2; 2563 | shorttext = text1.length > text2.length ? text2 : text1; 2564 | if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { 2565 | return null; // Pointless. 2566 | } 2567 | dmp = this; // 'this' becomes 'window' in a closure. 2568 | 2569 | /** 2570 | * Does a substring of shorttext exist within longtext such that the substring 2571 | * is at least half the length of longtext? 2572 | * Closure, but does not reference any external variables. 2573 | * @param {string} longtext Longer string. 2574 | * @param {string} shorttext Shorter string. 2575 | * @param {number} i Start index of quarter length substring within longtext. 2576 | * @return {Array.} Five element Array, containing the prefix of 2577 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 2578 | * of shorttext and the common middle. Or null if there was no match. 2579 | * @private 2580 | */ 2581 | function diffHalfMatchI( longtext, shorttext, i ) { 2582 | var seed, j, bestCommon, prefixLength, suffixLength, 2583 | bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; 2584 | // Start with a 1/4 length substring at position i as a seed. 2585 | seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); 2586 | j = -1; 2587 | bestCommon = ""; 2588 | while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { 2589 | prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), 2590 | shorttext.substring( j ) ); 2591 | suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), 2592 | shorttext.substring( 0, j ) ); 2593 | if ( bestCommon.length < suffixLength + prefixLength ) { 2594 | bestCommon = shorttext.substring( j - suffixLength, j ) + 2595 | shorttext.substring( j, j + prefixLength ); 2596 | bestLongtextA = longtext.substring( 0, i - suffixLength ); 2597 | bestLongtextB = longtext.substring( i + prefixLength ); 2598 | bestShorttextA = shorttext.substring( 0, j - suffixLength ); 2599 | bestShorttextB = shorttext.substring( j + prefixLength ); 2600 | } 2601 | } 2602 | if ( bestCommon.length * 2 >= longtext.length ) { 2603 | return [ bestLongtextA, bestLongtextB, 2604 | bestShorttextA, bestShorttextB, bestCommon 2605 | ]; 2606 | } else { 2607 | return null; 2608 | } 2609 | } 2610 | 2611 | // First check if the second quarter is the seed for a half-match. 2612 | hm1 = diffHalfMatchI( longtext, shorttext, 2613 | Math.ceil( longtext.length / 4 ) ); 2614 | // Check again based on the third quarter. 2615 | hm2 = diffHalfMatchI( longtext, shorttext, 2616 | Math.ceil( longtext.length / 2 ) ); 2617 | if ( !hm1 && !hm2 ) { 2618 | return null; 2619 | } else if ( !hm2 ) { 2620 | hm = hm1; 2621 | } else if ( !hm1 ) { 2622 | hm = hm2; 2623 | } else { 2624 | // Both matched. Select the longest. 2625 | hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; 2626 | } 2627 | 2628 | // A half-match was found, sort out the return data. 2629 | text1A, text1B, text2A, text2B; 2630 | if ( text1.length > text2.length ) { 2631 | text1A = hm[ 0 ]; 2632 | text1B = hm[ 1 ]; 2633 | text2A = hm[ 2 ]; 2634 | text2B = hm[ 3 ]; 2635 | } else { 2636 | text2A = hm[ 0 ]; 2637 | text2B = hm[ 1 ]; 2638 | text1A = hm[ 2 ]; 2639 | text1B = hm[ 3 ]; 2640 | } 2641 | midCommon = hm[ 4 ]; 2642 | return [ text1A, text1B, text2A, text2B, midCommon ]; 2643 | }; 2644 | 2645 | /** 2646 | * Do a quick line-level diff on both strings, then rediff the parts for 2647 | * greater accuracy. 2648 | * This speedup can produce non-minimal diffs. 2649 | * @param {string} text1 Old string to be diffed. 2650 | * @param {string} text2 New string to be diffed. 2651 | * @param {number} deadline Time when the diff should be complete by. 2652 | * @return {!Array.} Array of diff tuples. 2653 | * @private 2654 | */ 2655 | DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { 2656 | var a, diffs, linearray, pointer, countInsert, 2657 | countDelete, textInsert, textDelete, j; 2658 | // Scan the text on a line-by-line basis first. 2659 | a = this.diffLinesToChars( text1, text2 ); 2660 | text1 = a.chars1; 2661 | text2 = a.chars2; 2662 | linearray = a.lineArray; 2663 | 2664 | diffs = this.DiffMain( text1, text2, false, deadline ); 2665 | 2666 | // Convert the diff back to original text. 2667 | this.diffCharsToLines( diffs, linearray ); 2668 | // Eliminate freak matches (e.g. blank lines) 2669 | this.diffCleanupSemantic( diffs ); 2670 | 2671 | // Rediff any replacement blocks, this time character-by-character. 2672 | // Add a dummy entry at the end. 2673 | diffs.push( [ DIFF_EQUAL, "" ] ); 2674 | pointer = 0; 2675 | countDelete = 0; 2676 | countInsert = 0; 2677 | textDelete = ""; 2678 | textInsert = ""; 2679 | while ( pointer < diffs.length ) { 2680 | switch ( diffs[ pointer ][ 0 ] ) { 2681 | case DIFF_INSERT: 2682 | countInsert++; 2683 | textInsert += diffs[ pointer ][ 1 ]; 2684 | break; 2685 | case DIFF_DELETE: 2686 | countDelete++; 2687 | textDelete += diffs[ pointer ][ 1 ]; 2688 | break; 2689 | case DIFF_EQUAL: 2690 | // Upon reaching an equality, check for prior redundancies. 2691 | if ( countDelete >= 1 && countInsert >= 1 ) { 2692 | // Delete the offending records and add the merged ones. 2693 | diffs.splice( pointer - countDelete - countInsert, 2694 | countDelete + countInsert ); 2695 | pointer = pointer - countDelete - countInsert; 2696 | a = this.DiffMain( textDelete, textInsert, false, deadline ); 2697 | for ( j = a.length - 1; j >= 0; j-- ) { 2698 | diffs.splice( pointer, 0, a[ j ] ); 2699 | } 2700 | pointer = pointer + a.length; 2701 | } 2702 | countInsert = 0; 2703 | countDelete = 0; 2704 | textDelete = ""; 2705 | textInsert = ""; 2706 | break; 2707 | } 2708 | pointer++; 2709 | } 2710 | diffs.pop(); // Remove the dummy entry at the end. 2711 | 2712 | return diffs; 2713 | }; 2714 | 2715 | /** 2716 | * Find the 'middle snake' of a diff, split the problem in two 2717 | * and return the recursively constructed diff. 2718 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 2719 | * @param {string} text1 Old string to be diffed. 2720 | * @param {string} text2 New string to be diffed. 2721 | * @param {number} deadline Time at which to bail if not yet complete. 2722 | * @return {!Array.} Array of diff tuples. 2723 | * @private 2724 | */ 2725 | DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { 2726 | var text1Length, text2Length, maxD, vOffset, vLength, 2727 | v1, v2, x, delta, front, k1start, k1end, k2start, 2728 | k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; 2729 | // Cache the text lengths to prevent multiple calls. 2730 | text1Length = text1.length; 2731 | text2Length = text2.length; 2732 | maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); 2733 | vOffset = maxD; 2734 | vLength = 2 * maxD; 2735 | v1 = new Array( vLength ); 2736 | v2 = new Array( vLength ); 2737 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing 2738 | // integers and undefined. 2739 | for ( x = 0; x < vLength; x++ ) { 2740 | v1[ x ] = -1; 2741 | v2[ x ] = -1; 2742 | } 2743 | v1[ vOffset + 1 ] = 0; 2744 | v2[ vOffset + 1 ] = 0; 2745 | delta = text1Length - text2Length; 2746 | // If the total number of characters is odd, then the front path will collide 2747 | // with the reverse path. 2748 | front = ( delta % 2 !== 0 ); 2749 | // Offsets for start and end of k loop. 2750 | // Prevents mapping of space beyond the grid. 2751 | k1start = 0; 2752 | k1end = 0; 2753 | k2start = 0; 2754 | k2end = 0; 2755 | for ( d = 0; d < maxD; d++ ) { 2756 | // Bail out if deadline is reached. 2757 | if ( ( new Date() ).getTime() > deadline ) { 2758 | break; 2759 | } 2760 | 2761 | // Walk the front path one step. 2762 | for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { 2763 | k1Offset = vOffset + k1; 2764 | if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { 2765 | x1 = v1[ k1Offset + 1 ]; 2766 | } else { 2767 | x1 = v1[ k1Offset - 1 ] + 1; 2768 | } 2769 | y1 = x1 - k1; 2770 | while ( x1 < text1Length && y1 < text2Length && 2771 | text1.charAt( x1 ) === text2.charAt( y1 ) ) { 2772 | x1++; 2773 | y1++; 2774 | } 2775 | v1[ k1Offset ] = x1; 2776 | if ( x1 > text1Length ) { 2777 | // Ran off the right of the graph. 2778 | k1end += 2; 2779 | } else if ( y1 > text2Length ) { 2780 | // Ran off the bottom of the graph. 2781 | k1start += 2; 2782 | } else if ( front ) { 2783 | k2Offset = vOffset + delta - k1; 2784 | if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { 2785 | // Mirror x2 onto top-left coordinate system. 2786 | x2 = text1Length - v2[ k2Offset ]; 2787 | if ( x1 >= x2 ) { 2788 | // Overlap detected. 2789 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 2790 | } 2791 | } 2792 | } 2793 | } 2794 | 2795 | // Walk the reverse path one step. 2796 | for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { 2797 | k2Offset = vOffset + k2; 2798 | if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { 2799 | x2 = v2[ k2Offset + 1 ]; 2800 | } else { 2801 | x2 = v2[ k2Offset - 1 ] + 1; 2802 | } 2803 | y2 = x2 - k2; 2804 | while ( x2 < text1Length && y2 < text2Length && 2805 | text1.charAt( text1Length - x2 - 1 ) === 2806 | text2.charAt( text2Length - y2 - 1 ) ) { 2807 | x2++; 2808 | y2++; 2809 | } 2810 | v2[ k2Offset ] = x2; 2811 | if ( x2 > text1Length ) { 2812 | // Ran off the left of the graph. 2813 | k2end += 2; 2814 | } else if ( y2 > text2Length ) { 2815 | // Ran off the top of the graph. 2816 | k2start += 2; 2817 | } else if ( !front ) { 2818 | k1Offset = vOffset + delta - k2; 2819 | if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { 2820 | x1 = v1[ k1Offset ]; 2821 | y1 = vOffset + x1 - k1Offset; 2822 | // Mirror x2 onto top-left coordinate system. 2823 | x2 = text1Length - x2; 2824 | if ( x1 >= x2 ) { 2825 | // Overlap detected. 2826 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 2827 | } 2828 | } 2829 | } 2830 | } 2831 | } 2832 | // Diff took too long and hit the deadline or 2833 | // number of diffs equals number of characters, no commonality at all. 2834 | return [ 2835 | [ DIFF_DELETE, text1 ], 2836 | [ DIFF_INSERT, text2 ] 2837 | ]; 2838 | }; 2839 | 2840 | /** 2841 | * Given the location of the 'middle snake', split the diff in two parts 2842 | * and recurse. 2843 | * @param {string} text1 Old string to be diffed. 2844 | * @param {string} text2 New string to be diffed. 2845 | * @param {number} x Index of split point in text1. 2846 | * @param {number} y Index of split point in text2. 2847 | * @param {number} deadline Time at which to bail if not yet complete. 2848 | * @return {!Array.} Array of diff tuples. 2849 | * @private 2850 | */ 2851 | DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { 2852 | var text1a, text1b, text2a, text2b, diffs, diffsb; 2853 | text1a = text1.substring( 0, x ); 2854 | text2a = text2.substring( 0, y ); 2855 | text1b = text1.substring( x ); 2856 | text2b = text2.substring( y ); 2857 | 2858 | // Compute both diffs serially. 2859 | diffs = this.DiffMain( text1a, text2a, false, deadline ); 2860 | diffsb = this.DiffMain( text1b, text2b, false, deadline ); 2861 | 2862 | return diffs.concat( diffsb ); 2863 | }; 2864 | 2865 | /** 2866 | * Reduce the number of edits by eliminating semantically trivial equalities. 2867 | * @param {!Array.} diffs Array of diff tuples. 2868 | */ 2869 | DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { 2870 | var changes, equalities, equalitiesLength, lastequality, 2871 | pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, 2872 | lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; 2873 | changes = false; 2874 | equalities = []; // Stack of indices where equalities are found. 2875 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 2876 | /** @type {?string} */ 2877 | lastequality = null; 2878 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 2879 | pointer = 0; // Index of current position. 2880 | // Number of characters that changed prior to the equality. 2881 | lengthInsertions1 = 0; 2882 | lengthDeletions1 = 0; 2883 | // Number of characters that changed after the equality. 2884 | lengthInsertions2 = 0; 2885 | lengthDeletions2 = 0; 2886 | while ( pointer < diffs.length ) { 2887 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. 2888 | equalities[ equalitiesLength++ ] = pointer; 2889 | lengthInsertions1 = lengthInsertions2; 2890 | lengthDeletions1 = lengthDeletions2; 2891 | lengthInsertions2 = 0; 2892 | lengthDeletions2 = 0; 2893 | lastequality = diffs[ pointer ][ 1 ]; 2894 | } else { // An insertion or deletion. 2895 | if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 2896 | lengthInsertions2 += diffs[ pointer ][ 1 ].length; 2897 | } else { 2898 | lengthDeletions2 += diffs[ pointer ][ 1 ].length; 2899 | } 2900 | // Eliminate an equality that is smaller or equal to the edits on both 2901 | // sides of it. 2902 | if ( lastequality && ( lastequality.length <= 2903 | Math.max( lengthInsertions1, lengthDeletions1 ) ) && 2904 | ( lastequality.length <= Math.max( lengthInsertions2, 2905 | lengthDeletions2 ) ) ) { 2906 | 2907 | // Duplicate record. 2908 | diffs.splice( 2909 | equalities[ equalitiesLength - 1 ], 2910 | 0, 2911 | [ DIFF_DELETE, lastequality ] 2912 | ); 2913 | 2914 | // Change second copy to insert. 2915 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 2916 | 2917 | // Throw away the equality we just deleted. 2918 | equalitiesLength--; 2919 | 2920 | // Throw away the previous equality (it needs to be reevaluated). 2921 | equalitiesLength--; 2922 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 2923 | 2924 | // Reset the counters. 2925 | lengthInsertions1 = 0; 2926 | lengthDeletions1 = 0; 2927 | lengthInsertions2 = 0; 2928 | lengthDeletions2 = 0; 2929 | lastequality = null; 2930 | changes = true; 2931 | } 2932 | } 2933 | pointer++; 2934 | } 2935 | 2936 | // Normalize the diff. 2937 | if ( changes ) { 2938 | this.diffCleanupMerge( diffs ); 2939 | } 2940 | 2941 | // Find any overlaps between deletions and insertions. 2942 | // e.g: abcxxxxxxdef 2943 | // -> abcxxxdef 2944 | // e.g: xxxabcdefxxx 2945 | // -> defxxxabc 2946 | // Only extract an overlap if it is as big as the edit ahead or behind it. 2947 | pointer = 1; 2948 | while ( pointer < diffs.length ) { 2949 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && 2950 | diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 2951 | deletion = diffs[ pointer - 1 ][ 1 ]; 2952 | insertion = diffs[ pointer ][ 1 ]; 2953 | overlapLength1 = this.diffCommonOverlap( deletion, insertion ); 2954 | overlapLength2 = this.diffCommonOverlap( insertion, deletion ); 2955 | if ( overlapLength1 >= overlapLength2 ) { 2956 | if ( overlapLength1 >= deletion.length / 2 || 2957 | overlapLength1 >= insertion.length / 2 ) { 2958 | // Overlap found. Insert an equality and trim the surrounding edits. 2959 | diffs.splice( 2960 | pointer, 2961 | 0, 2962 | [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] 2963 | ); 2964 | diffs[ pointer - 1 ][ 1 ] = 2965 | deletion.substring( 0, deletion.length - overlapLength1 ); 2966 | diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); 2967 | pointer++; 2968 | } 2969 | } else { 2970 | if ( overlapLength2 >= deletion.length / 2 || 2971 | overlapLength2 >= insertion.length / 2 ) { 2972 | 2973 | // Reverse overlap found. 2974 | // Insert an equality and swap and trim the surrounding edits. 2975 | diffs.splice( 2976 | pointer, 2977 | 0, 2978 | [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] 2979 | ); 2980 | 2981 | diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; 2982 | diffs[ pointer - 1 ][ 1 ] = 2983 | insertion.substring( 0, insertion.length - overlapLength2 ); 2984 | diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; 2985 | diffs[ pointer + 1 ][ 1 ] = 2986 | deletion.substring( overlapLength2 ); 2987 | pointer++; 2988 | } 2989 | } 2990 | pointer++; 2991 | } 2992 | pointer++; 2993 | } 2994 | }; 2995 | 2996 | /** 2997 | * Determine if the suffix of one string is the prefix of another. 2998 | * @param {string} text1 First string. 2999 | * @param {string} text2 Second string. 3000 | * @return {number} The number of characters common to the end of the first 3001 | * string and the start of the second string. 3002 | * @private 3003 | */ 3004 | DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { 3005 | var text1Length, text2Length, textLength, 3006 | best, length, pattern, found; 3007 | // Cache the text lengths to prevent multiple calls. 3008 | text1Length = text1.length; 3009 | text2Length = text2.length; 3010 | // Eliminate the null case. 3011 | if ( text1Length === 0 || text2Length === 0 ) { 3012 | return 0; 3013 | } 3014 | // Truncate the longer string. 3015 | if ( text1Length > text2Length ) { 3016 | text1 = text1.substring( text1Length - text2Length ); 3017 | } else if ( text1Length < text2Length ) { 3018 | text2 = text2.substring( 0, text1Length ); 3019 | } 3020 | textLength = Math.min( text1Length, text2Length ); 3021 | // Quick check for the worst case. 3022 | if ( text1 === text2 ) { 3023 | return textLength; 3024 | } 3025 | 3026 | // Start by looking for a single character match 3027 | // and increase length until no match is found. 3028 | // Performance analysis: https://neil.fraser.name/news/2010/11/04/ 3029 | best = 0; 3030 | length = 1; 3031 | while ( true ) { 3032 | pattern = text1.substring( textLength - length ); 3033 | found = text2.indexOf( pattern ); 3034 | if ( found === -1 ) { 3035 | return best; 3036 | } 3037 | length += found; 3038 | if ( found === 0 || text1.substring( textLength - length ) === 3039 | text2.substring( 0, length ) ) { 3040 | best = length; 3041 | length++; 3042 | } 3043 | } 3044 | }; 3045 | 3046 | /** 3047 | * Split two texts into an array of strings. Reduce the texts to a string of 3048 | * hashes where each Unicode character represents one line. 3049 | * @param {string} text1 First string. 3050 | * @param {string} text2 Second string. 3051 | * @return {{chars1: string, chars2: string, lineArray: !Array.}} 3052 | * An object containing the encoded text1, the encoded text2 and 3053 | * the array of unique strings. 3054 | * The zeroth element of the array of unique strings is intentionally blank. 3055 | * @private 3056 | */ 3057 | DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { 3058 | var lineArray, lineHash, chars1, chars2; 3059 | lineArray = []; // e.g. lineArray[4] === 'Hello\n' 3060 | lineHash = {}; // e.g. lineHash['Hello\n'] === 4 3061 | 3062 | // '\x00' is a valid character, but various debuggers don't like it. 3063 | // So we'll insert a junk entry to avoid generating a null character. 3064 | lineArray[ 0 ] = ""; 3065 | 3066 | /** 3067 | * Split a text into an array of strings. Reduce the texts to a string of 3068 | * hashes where each Unicode character represents one line. 3069 | * Modifies linearray and linehash through being a closure. 3070 | * @param {string} text String to encode. 3071 | * @return {string} Encoded string. 3072 | * @private 3073 | */ 3074 | function diffLinesToCharsMunge( text ) { 3075 | var chars, lineStart, lineEnd, lineArrayLength, line; 3076 | chars = ""; 3077 | // Walk the text, pulling out a substring for each line. 3078 | // text.split('\n') would would temporarily double our memory footprint. 3079 | // Modifying text would create many large strings to garbage collect. 3080 | lineStart = 0; 3081 | lineEnd = -1; 3082 | // Keeping our own length variable is faster than looking it up. 3083 | lineArrayLength = lineArray.length; 3084 | while ( lineEnd < text.length - 1 ) { 3085 | lineEnd = text.indexOf( "\n", lineStart ); 3086 | if ( lineEnd === -1 ) { 3087 | lineEnd = text.length - 1; 3088 | } 3089 | line = text.substring( lineStart, lineEnd + 1 ); 3090 | lineStart = lineEnd + 1; 3091 | 3092 | if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : 3093 | ( lineHash[ line ] !== undefined ) ) { 3094 | chars += String.fromCharCode( lineHash[ line ] ); 3095 | } else { 3096 | chars += String.fromCharCode( lineArrayLength ); 3097 | lineHash[ line ] = lineArrayLength; 3098 | lineArray[ lineArrayLength++ ] = line; 3099 | } 3100 | } 3101 | return chars; 3102 | } 3103 | 3104 | chars1 = diffLinesToCharsMunge( text1 ); 3105 | chars2 = diffLinesToCharsMunge( text2 ); 3106 | return { 3107 | chars1: chars1, 3108 | chars2: chars2, 3109 | lineArray: lineArray 3110 | }; 3111 | }; 3112 | 3113 | /** 3114 | * Rehydrate the text in a diff from a string of line hashes to real lines of 3115 | * text. 3116 | * @param {!Array.} diffs Array of diff tuples. 3117 | * @param {!Array.} lineArray Array of unique strings. 3118 | * @private 3119 | */ 3120 | DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { 3121 | var x, chars, text, y; 3122 | for ( x = 0; x < diffs.length; x++ ) { 3123 | chars = diffs[ x ][ 1 ]; 3124 | text = []; 3125 | for ( y = 0; y < chars.length; y++ ) { 3126 | text[ y ] = lineArray[ chars.charCodeAt( y ) ]; 3127 | } 3128 | diffs[ x ][ 1 ] = text.join( "" ); 3129 | } 3130 | }; 3131 | 3132 | /** 3133 | * Reorder and merge like edit sections. Merge equalities. 3134 | * Any edit section can move as long as it doesn't cross an equality. 3135 | * @param {!Array.} diffs Array of diff tuples. 3136 | */ 3137 | DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { 3138 | var pointer, countDelete, countInsert, textInsert, textDelete, 3139 | commonlength, changes, diffPointer, position; 3140 | diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. 3141 | pointer = 0; 3142 | countDelete = 0; 3143 | countInsert = 0; 3144 | textDelete = ""; 3145 | textInsert = ""; 3146 | commonlength; 3147 | while ( pointer < diffs.length ) { 3148 | switch ( diffs[ pointer ][ 0 ] ) { 3149 | case DIFF_INSERT: 3150 | countInsert++; 3151 | textInsert += diffs[ pointer ][ 1 ]; 3152 | pointer++; 3153 | break; 3154 | case DIFF_DELETE: 3155 | countDelete++; 3156 | textDelete += diffs[ pointer ][ 1 ]; 3157 | pointer++; 3158 | break; 3159 | case DIFF_EQUAL: 3160 | // Upon reaching an equality, check for prior redundancies. 3161 | if ( countDelete + countInsert > 1 ) { 3162 | if ( countDelete !== 0 && countInsert !== 0 ) { 3163 | // Factor out any common prefixes. 3164 | commonlength = this.diffCommonPrefix( textInsert, textDelete ); 3165 | if ( commonlength !== 0 ) { 3166 | if ( ( pointer - countDelete - countInsert ) > 0 && 3167 | diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === 3168 | DIFF_EQUAL ) { 3169 | diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += 3170 | textInsert.substring( 0, commonlength ); 3171 | } else { 3172 | diffs.splice( 0, 0, [ DIFF_EQUAL, 3173 | textInsert.substring( 0, commonlength ) 3174 | ] ); 3175 | pointer++; 3176 | } 3177 | textInsert = textInsert.substring( commonlength ); 3178 | textDelete = textDelete.substring( commonlength ); 3179 | } 3180 | // Factor out any common suffixies. 3181 | commonlength = this.diffCommonSuffix( textInsert, textDelete ); 3182 | if ( commonlength !== 0 ) { 3183 | diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - 3184 | commonlength ) + diffs[ pointer ][ 1 ]; 3185 | textInsert = textInsert.substring( 0, textInsert.length - 3186 | commonlength ); 3187 | textDelete = textDelete.substring( 0, textDelete.length - 3188 | commonlength ); 3189 | } 3190 | } 3191 | // Delete the offending records and add the merged ones. 3192 | if ( countDelete === 0 ) { 3193 | diffs.splice( pointer - countInsert, 3194 | countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); 3195 | } else if ( countInsert === 0 ) { 3196 | diffs.splice( pointer - countDelete, 3197 | countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); 3198 | } else { 3199 | diffs.splice( 3200 | pointer - countDelete - countInsert, 3201 | countDelete + countInsert, 3202 | [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] 3203 | ); 3204 | } 3205 | pointer = pointer - countDelete - countInsert + 3206 | ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; 3207 | } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { 3208 | 3209 | // Merge this equality with the previous one. 3210 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; 3211 | diffs.splice( pointer, 1 ); 3212 | } else { 3213 | pointer++; 3214 | } 3215 | countInsert = 0; 3216 | countDelete = 0; 3217 | textDelete = ""; 3218 | textInsert = ""; 3219 | break; 3220 | } 3221 | } 3222 | if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { 3223 | diffs.pop(); // Remove the dummy entry at the end. 3224 | } 3225 | 3226 | // Second pass: look for single edits surrounded on both sides by equalities 3227 | // which can be shifted sideways to eliminate an equality. 3228 | // e.g: ABAC -> ABAC 3229 | changes = false; 3230 | pointer = 1; 3231 | 3232 | // Intentionally ignore the first and last element (don't need checking). 3233 | while ( pointer < diffs.length - 1 ) { 3234 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && 3235 | diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { 3236 | 3237 | diffPointer = diffs[ pointer ][ 1 ]; 3238 | position = diffPointer.substring( 3239 | diffPointer.length - diffs[ pointer - 1 ][ 1 ].length 3240 | ); 3241 | 3242 | // This is a single edit surrounded by equalities. 3243 | if ( position === diffs[ pointer - 1 ][ 1 ] ) { 3244 | 3245 | // Shift the edit over the previous equality. 3246 | diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + 3247 | diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - 3248 | diffs[ pointer - 1 ][ 1 ].length ); 3249 | diffs[ pointer + 1 ][ 1 ] = 3250 | diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; 3251 | diffs.splice( pointer - 1, 1 ); 3252 | changes = true; 3253 | } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === 3254 | diffs[ pointer + 1 ][ 1 ] ) { 3255 | 3256 | // Shift the edit over the next equality. 3257 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; 3258 | diffs[ pointer ][ 1 ] = 3259 | diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + 3260 | diffs[ pointer + 1 ][ 1 ]; 3261 | diffs.splice( pointer + 1, 1 ); 3262 | changes = true; 3263 | } 3264 | } 3265 | pointer++; 3266 | } 3267 | // If shifts were made, the diff needs reordering and another shift sweep. 3268 | if ( changes ) { 3269 | this.diffCleanupMerge( diffs ); 3270 | } 3271 | }; 3272 | 3273 | return function( o, n ) { 3274 | var diff, output, text; 3275 | diff = new DiffMatchPatch(); 3276 | output = diff.DiffMain( o, n ); 3277 | diff.diffCleanupEfficiency( output ); 3278 | text = diff.diffPrettyHtml( output ); 3279 | 3280 | return text; 3281 | }; 3282 | }() ); 3283 | 3284 | // Get a reference to the global object, like window in browsers 3285 | }( (function() { 3286 | return this; 3287 | })() )); 3288 | 3289 | (function() { 3290 | 3291 | // Don't load the HTML Reporter on non-Browser environments 3292 | if ( typeof window === "undefined" || !window.document ) { 3293 | return; 3294 | } 3295 | 3296 | // Deprecated QUnit.init - Ref #530 3297 | // Re-initialize the configuration options 3298 | QUnit.init = function() { 3299 | var tests, banner, result, qunit, 3300 | config = QUnit.config; 3301 | 3302 | config.stats = { all: 0, bad: 0 }; 3303 | config.moduleStats = { all: 0, bad: 0 }; 3304 | config.started = 0; 3305 | config.updateRate = 1000; 3306 | config.blocking = false; 3307 | config.autostart = true; 3308 | config.autorun = false; 3309 | config.filter = ""; 3310 | config.queue = []; 3311 | 3312 | // Return on non-browser environments 3313 | // This is necessary to not break on node tests 3314 | if ( typeof window === "undefined" ) { 3315 | return; 3316 | } 3317 | 3318 | qunit = id( "qunit" ); 3319 | if ( qunit ) { 3320 | qunit.innerHTML = 3321 | "

" + escapeText( document.title ) + "

" + 3322 | "

" + 3323 | "
" + 3324 | "

" + 3325 | "
    "; 3326 | } 3327 | 3328 | tests = id( "qunit-tests" ); 3329 | banner = id( "qunit-banner" ); 3330 | result = id( "qunit-testresult" ); 3331 | 3332 | if ( tests ) { 3333 | tests.innerHTML = ""; 3334 | } 3335 | 3336 | if ( banner ) { 3337 | banner.className = ""; 3338 | } 3339 | 3340 | if ( result ) { 3341 | result.parentNode.removeChild( result ); 3342 | } 3343 | 3344 | if ( tests ) { 3345 | result = document.createElement( "p" ); 3346 | result.id = "qunit-testresult"; 3347 | result.className = "result"; 3348 | tests.parentNode.insertBefore( result, tests ); 3349 | result.innerHTML = "Running...
     "; 3350 | } 3351 | }; 3352 | 3353 | var config = QUnit.config, 3354 | collapseNext = false, 3355 | hasOwn = Object.prototype.hasOwnProperty, 3356 | defined = { 3357 | document: window.document !== undefined, 3358 | sessionStorage: (function() { 3359 | var x = "qunit-test-string"; 3360 | try { 3361 | sessionStorage.setItem( x, x ); 3362 | sessionStorage.removeItem( x ); 3363 | return true; 3364 | } catch ( e ) { 3365 | return false; 3366 | } 3367 | }()) 3368 | }, 3369 | modulesList = []; 3370 | 3371 | /** 3372 | * Escape text for attribute or text content. 3373 | */ 3374 | function escapeText( s ) { 3375 | if ( !s ) { 3376 | return ""; 3377 | } 3378 | s = s + ""; 3379 | 3380 | // Both single quotes and double quotes (for attributes) 3381 | return s.replace( /['"<>&]/g, function( s ) { 3382 | switch ( s ) { 3383 | case "'": 3384 | return "'"; 3385 | case "\"": 3386 | return """; 3387 | case "<": 3388 | return "<"; 3389 | case ">": 3390 | return ">"; 3391 | case "&": 3392 | return "&"; 3393 | } 3394 | }); 3395 | } 3396 | 3397 | /** 3398 | * @param {HTMLElement} elem 3399 | * @param {string} type 3400 | * @param {Function} fn 3401 | */ 3402 | function addEvent( elem, type, fn ) { 3403 | if ( elem.addEventListener ) { 3404 | 3405 | // Standards-based browsers 3406 | elem.addEventListener( type, fn, false ); 3407 | } else if ( elem.attachEvent ) { 3408 | 3409 | // support: IE <9 3410 | elem.attachEvent( "on" + type, function() { 3411 | var event = window.event; 3412 | if ( !event.target ) { 3413 | event.target = event.srcElement || document; 3414 | } 3415 | 3416 | fn.call( elem, event ); 3417 | }); 3418 | } 3419 | } 3420 | 3421 | /** 3422 | * @param {Array|NodeList} elems 3423 | * @param {string} type 3424 | * @param {Function} fn 3425 | */ 3426 | function addEvents( elems, type, fn ) { 3427 | var i = elems.length; 3428 | while ( i-- ) { 3429 | addEvent( elems[ i ], type, fn ); 3430 | } 3431 | } 3432 | 3433 | function hasClass( elem, name ) { 3434 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; 3435 | } 3436 | 3437 | function addClass( elem, name ) { 3438 | if ( !hasClass( elem, name ) ) { 3439 | elem.className += ( elem.className ? " " : "" ) + name; 3440 | } 3441 | } 3442 | 3443 | function toggleClass( elem, name ) { 3444 | if ( hasClass( elem, name ) ) { 3445 | removeClass( elem, name ); 3446 | } else { 3447 | addClass( elem, name ); 3448 | } 3449 | } 3450 | 3451 | function removeClass( elem, name ) { 3452 | var set = " " + elem.className + " "; 3453 | 3454 | // Class name may appear multiple times 3455 | while ( set.indexOf( " " + name + " " ) >= 0 ) { 3456 | set = set.replace( " " + name + " ", " " ); 3457 | } 3458 | 3459 | // trim for prettiness 3460 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); 3461 | } 3462 | 3463 | function id( name ) { 3464 | return defined.document && document.getElementById && document.getElementById( name ); 3465 | } 3466 | 3467 | function getUrlConfigHtml() { 3468 | var i, j, val, 3469 | escaped, escapedTooltip, 3470 | selection = false, 3471 | len = config.urlConfig.length, 3472 | urlConfigHtml = ""; 3473 | 3474 | for ( i = 0; i < len; i++ ) { 3475 | val = config.urlConfig[ i ]; 3476 | if ( typeof val === "string" ) { 3477 | val = { 3478 | id: val, 3479 | label: val 3480 | }; 3481 | } 3482 | 3483 | escaped = escapeText( val.id ); 3484 | escapedTooltip = escapeText( val.tooltip ); 3485 | 3486 | if ( config[ val.id ] === undefined ) { 3487 | config[ val.id ] = QUnit.urlParams[ val.id ]; 3488 | } 3489 | 3490 | if ( !val.value || typeof val.value === "string" ) { 3491 | urlConfigHtml += ""; 3497 | } else { 3498 | urlConfigHtml += ""; 3527 | } 3528 | } 3529 | 3530 | return urlConfigHtml; 3531 | } 3532 | 3533 | // Handle "click" events on toolbar checkboxes and "change" for select menus. 3534 | // Updates the URL with the new state of `config.urlConfig` values. 3535 | function toolbarChanged() { 3536 | var updatedUrl, value, 3537 | field = this, 3538 | params = {}; 3539 | 3540 | // Detect if field is a select menu or a checkbox 3541 | if ( "selectedIndex" in field ) { 3542 | value = field.options[ field.selectedIndex ].value || undefined; 3543 | } else { 3544 | value = field.checked ? ( field.defaultValue || true ) : undefined; 3545 | } 3546 | 3547 | params[ field.name ] = value; 3548 | updatedUrl = setUrl( params ); 3549 | 3550 | if ( "hidepassed" === field.name && "replaceState" in window.history ) { 3551 | config[ field.name ] = value || false; 3552 | if ( value ) { 3553 | addClass( id( "qunit-tests" ), "hidepass" ); 3554 | } else { 3555 | removeClass( id( "qunit-tests" ), "hidepass" ); 3556 | } 3557 | 3558 | // It is not necessary to refresh the whole page 3559 | window.history.replaceState( null, "", updatedUrl ); 3560 | } else { 3561 | window.location = updatedUrl; 3562 | } 3563 | } 3564 | 3565 | function setUrl( params ) { 3566 | var key, 3567 | querystring = "?"; 3568 | 3569 | params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); 3570 | 3571 | for ( key in params ) { 3572 | if ( hasOwn.call( params, key ) ) { 3573 | if ( params[ key ] === undefined ) { 3574 | continue; 3575 | } 3576 | querystring += encodeURIComponent( key ); 3577 | if ( params[ key ] !== true ) { 3578 | querystring += "=" + encodeURIComponent( params[ key ] ); 3579 | } 3580 | querystring += "&"; 3581 | } 3582 | } 3583 | return location.protocol + "//" + location.host + 3584 | location.pathname + querystring.slice( 0, -1 ); 3585 | } 3586 | 3587 | function applyUrlParams() { 3588 | var selectedModule, 3589 | modulesList = id( "qunit-modulefilter" ), 3590 | filter = id( "qunit-filter-input" ).value; 3591 | 3592 | selectedModule = modulesList ? 3593 | decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : 3594 | undefined; 3595 | 3596 | window.location = setUrl({ 3597 | module: ( selectedModule === "" ) ? undefined : selectedModule, 3598 | filter: ( filter === "" ) ? undefined : filter, 3599 | 3600 | // Remove testId filter 3601 | testId: undefined 3602 | }); 3603 | } 3604 | 3605 | function toolbarUrlConfigContainer() { 3606 | var urlConfigContainer = document.createElement( "span" ); 3607 | 3608 | urlConfigContainer.innerHTML = getUrlConfigHtml(); 3609 | addClass( urlConfigContainer, "qunit-url-config" ); 3610 | 3611 | // For oldIE support: 3612 | // * Add handlers to the individual elements instead of the container 3613 | // * Use "click" instead of "change" for checkboxes 3614 | addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); 3615 | addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); 3616 | 3617 | return urlConfigContainer; 3618 | } 3619 | 3620 | function toolbarLooseFilter() { 3621 | var filter = document.createElement( "form" ), 3622 | label = document.createElement( "label" ), 3623 | input = document.createElement( "input" ), 3624 | button = document.createElement( "button" ); 3625 | 3626 | addClass( filter, "qunit-filter" ); 3627 | 3628 | label.innerHTML = "Filter: "; 3629 | 3630 | input.type = "text"; 3631 | input.value = config.filter || ""; 3632 | input.name = "filter"; 3633 | input.id = "qunit-filter-input"; 3634 | 3635 | button.innerHTML = "Go"; 3636 | 3637 | label.appendChild( input ); 3638 | 3639 | filter.appendChild( label ); 3640 | filter.appendChild( button ); 3641 | addEvent( filter, "submit", function( ev ) { 3642 | applyUrlParams(); 3643 | 3644 | if ( ev && ev.preventDefault ) { 3645 | ev.preventDefault(); 3646 | } 3647 | 3648 | return false; 3649 | }); 3650 | 3651 | return filter; 3652 | } 3653 | 3654 | function toolbarModuleFilterHtml() { 3655 | var i, 3656 | moduleFilterHtml = ""; 3657 | 3658 | if ( !modulesList.length ) { 3659 | return false; 3660 | } 3661 | 3662 | modulesList.sort(function( a, b ) { 3663 | return a.localeCompare( b ); 3664 | }); 3665 | 3666 | moduleFilterHtml += "" + 3667 | ""; 3678 | 3679 | return moduleFilterHtml; 3680 | } 3681 | 3682 | function toolbarModuleFilter() { 3683 | var toolbar = id( "qunit-testrunner-toolbar" ), 3684 | moduleFilter = document.createElement( "span" ), 3685 | moduleFilterHtml = toolbarModuleFilterHtml(); 3686 | 3687 | if ( !toolbar || !moduleFilterHtml ) { 3688 | return false; 3689 | } 3690 | 3691 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 3692 | moduleFilter.innerHTML = moduleFilterHtml; 3693 | 3694 | addEvent( moduleFilter.lastChild, "change", applyUrlParams ); 3695 | 3696 | toolbar.appendChild( moduleFilter ); 3697 | } 3698 | 3699 | function appendToolbar() { 3700 | var toolbar = id( "qunit-testrunner-toolbar" ); 3701 | 3702 | if ( toolbar ) { 3703 | toolbar.appendChild( toolbarUrlConfigContainer() ); 3704 | toolbar.appendChild( toolbarLooseFilter() ); 3705 | } 3706 | } 3707 | 3708 | function appendHeader() { 3709 | var header = id( "qunit-header" ); 3710 | 3711 | if ( header ) { 3712 | header.innerHTML = "" + header.innerHTML + " "; 3715 | } 3716 | } 3717 | 3718 | function appendBanner() { 3719 | var banner = id( "qunit-banner" ); 3720 | 3721 | if ( banner ) { 3722 | banner.className = ""; 3723 | } 3724 | } 3725 | 3726 | function appendTestResults() { 3727 | var tests = id( "qunit-tests" ), 3728 | result = id( "qunit-testresult" ); 3729 | 3730 | if ( result ) { 3731 | result.parentNode.removeChild( result ); 3732 | } 3733 | 3734 | if ( tests ) { 3735 | tests.innerHTML = ""; 3736 | result = document.createElement( "p" ); 3737 | result.id = "qunit-testresult"; 3738 | result.className = "result"; 3739 | tests.parentNode.insertBefore( result, tests ); 3740 | result.innerHTML = "Running...
     "; 3741 | } 3742 | } 3743 | 3744 | function storeFixture() { 3745 | var fixture = id( "qunit-fixture" ); 3746 | if ( fixture ) { 3747 | config.fixture = fixture.innerHTML; 3748 | } 3749 | } 3750 | 3751 | function appendFilteredTest() { 3752 | var testId = QUnit.config.testId; 3753 | if ( !testId || testId.length <= 0 ) { 3754 | return ""; 3755 | } 3756 | return "
    Rerunning selected tests: " + testId.join(", ") + 3757 | " " + "Run all tests" + "
    "; 3760 | } 3761 | 3762 | function appendUserAgent() { 3763 | var userAgent = id( "qunit-userAgent" ); 3764 | 3765 | if ( userAgent ) { 3766 | userAgent.innerHTML = ""; 3767 | userAgent.appendChild( 3768 | document.createTextNode( 3769 | "QUnit " + QUnit.version + "; " + navigator.userAgent 3770 | ) 3771 | ); 3772 | } 3773 | } 3774 | 3775 | function appendTestsList( modules ) { 3776 | var i, l, x, z, test, moduleObj; 3777 | 3778 | for ( i = 0, l = modules.length; i < l; i++ ) { 3779 | moduleObj = modules[ i ]; 3780 | 3781 | if ( moduleObj.name ) { 3782 | modulesList.push( moduleObj.name ); 3783 | } 3784 | 3785 | for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { 3786 | test = moduleObj.tests[ x ]; 3787 | 3788 | appendTest( test.name, test.testId, moduleObj.name ); 3789 | } 3790 | } 3791 | } 3792 | 3793 | function appendTest( name, testId, moduleName ) { 3794 | var title, rerunTrigger, testBlock, assertList, 3795 | tests = id( "qunit-tests" ); 3796 | 3797 | if ( !tests ) { 3798 | return; 3799 | } 3800 | 3801 | title = document.createElement( "strong" ); 3802 | title.innerHTML = getNameHtml( name, moduleName ); 3803 | 3804 | rerunTrigger = document.createElement( "a" ); 3805 | rerunTrigger.innerHTML = "Rerun"; 3806 | rerunTrigger.href = setUrl({ testId: testId }); 3807 | 3808 | testBlock = document.createElement( "li" ); 3809 | testBlock.appendChild( title ); 3810 | testBlock.appendChild( rerunTrigger ); 3811 | testBlock.id = "qunit-test-output-" + testId; 3812 | 3813 | assertList = document.createElement( "ol" ); 3814 | assertList.className = "qunit-assert-list"; 3815 | 3816 | testBlock.appendChild( assertList ); 3817 | 3818 | tests.appendChild( testBlock ); 3819 | } 3820 | 3821 | // HTML Reporter initialization and load 3822 | QUnit.begin(function( details ) { 3823 | var qunit = id( "qunit" ); 3824 | 3825 | // Fixture is the only one necessary to run without the #qunit element 3826 | storeFixture(); 3827 | 3828 | if ( qunit ) { 3829 | qunit.innerHTML = 3830 | "

    " + escapeText( document.title ) + "

    " + 3831 | "

    " + 3832 | "
    " + 3833 | appendFilteredTest() + 3834 | "

    " + 3835 | "
      "; 3836 | } 3837 | 3838 | appendHeader(); 3839 | appendBanner(); 3840 | appendTestResults(); 3841 | appendUserAgent(); 3842 | appendToolbar(); 3843 | appendTestsList( details.modules ); 3844 | toolbarModuleFilter(); 3845 | 3846 | if ( qunit && config.hidepassed ) { 3847 | addClass( qunit.lastChild, "hidepass" ); 3848 | } 3849 | }); 3850 | 3851 | QUnit.done(function( details ) { 3852 | var i, key, 3853 | banner = id( "qunit-banner" ), 3854 | tests = id( "qunit-tests" ), 3855 | html = [ 3856 | "Tests completed in ", 3857 | details.runtime, 3858 | " milliseconds.
      ", 3859 | "", 3860 | details.passed, 3861 | " assertions of ", 3862 | details.total, 3863 | " passed, ", 3864 | details.failed, 3865 | " failed." 3866 | ].join( "" ); 3867 | 3868 | if ( banner ) { 3869 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 3870 | } 3871 | 3872 | if ( tests ) { 3873 | id( "qunit-testresult" ).innerHTML = html; 3874 | } 3875 | 3876 | if ( config.altertitle && defined.document && document.title ) { 3877 | 3878 | // show ✖ for good, ✔ for bad suite result in title 3879 | // use escape sequences in case file gets loaded with non-utf-8-charset 3880 | document.title = [ 3881 | ( details.failed ? "\u2716" : "\u2714" ), 3882 | document.title.replace( /^[\u2714\u2716] /i, "" ) 3883 | ].join( " " ); 3884 | } 3885 | 3886 | // clear own sessionStorage items if all tests passed 3887 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 3888 | for ( i = 0; i < sessionStorage.length; i++ ) { 3889 | key = sessionStorage.key( i++ ); 3890 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 3891 | sessionStorage.removeItem( key ); 3892 | } 3893 | } 3894 | } 3895 | 3896 | // scroll back to top to show results 3897 | if ( config.scrolltop && window.scrollTo ) { 3898 | window.scrollTo( 0, 0 ); 3899 | } 3900 | }); 3901 | 3902 | function getNameHtml( name, module ) { 3903 | var nameHtml = ""; 3904 | 3905 | if ( module ) { 3906 | nameHtml = "" + escapeText( module ) + ": "; 3907 | } 3908 | 3909 | nameHtml += "" + escapeText( name ) + ""; 3910 | 3911 | return nameHtml; 3912 | } 3913 | 3914 | QUnit.testStart(function( details ) { 3915 | var running, testBlock, bad; 3916 | 3917 | testBlock = id( "qunit-test-output-" + details.testId ); 3918 | if ( testBlock ) { 3919 | testBlock.className = "running"; 3920 | } else { 3921 | 3922 | // Report later registered tests 3923 | appendTest( details.name, details.testId, details.module ); 3924 | } 3925 | 3926 | running = id( "qunit-testresult" ); 3927 | if ( running ) { 3928 | bad = QUnit.config.reorder && defined.sessionStorage && 3929 | +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); 3930 | 3931 | running.innerHTML = ( bad ? 3932 | "Rerunning previously failed test:
      " : 3933 | "Running:
      " ) + 3934 | getNameHtml( details.name, details.module ); 3935 | } 3936 | 3937 | }); 3938 | 3939 | function stripHtml( string ) { 3940 | // strip tags, html entity and whitespaces 3941 | return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); 3942 | } 3943 | 3944 | QUnit.log(function( details ) { 3945 | var assertList, assertLi, 3946 | message, expected, actual, diff, 3947 | showDiff = false, 3948 | testItem = id( "qunit-test-output-" + details.testId ); 3949 | 3950 | if ( !testItem ) { 3951 | return; 3952 | } 3953 | 3954 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 3955 | message = "" + message + ""; 3956 | message += "@ " + details.runtime + " ms"; 3957 | 3958 | // pushFailure doesn't provide details.expected 3959 | // when it calls, it's implicit to also not show expected and diff stuff 3960 | // Also, we need to check details.expected existence, as it can exist and be undefined 3961 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 3962 | if ( details.negative ) { 3963 | expected = escapeText( "NOT " + QUnit.dump.parse( details.expected ) ); 3964 | } else { 3965 | expected = escapeText( QUnit.dump.parse( details.expected ) ); 3966 | } 3967 | 3968 | actual = escapeText( QUnit.dump.parse( details.actual ) ); 3969 | message += ""; 3972 | 3973 | if ( actual !== expected ) { 3974 | 3975 | message += ""; 3977 | 3978 | // Don't show diff if actual or expected are booleans 3979 | if ( !( /^(true|false)$/.test( actual ) ) && 3980 | !( /^(true|false)$/.test( expected ) ) ) { 3981 | diff = QUnit.diff( expected, actual ); 3982 | showDiff = stripHtml( diff ).length !== 3983 | stripHtml( expected ).length + 3984 | stripHtml( actual ).length; 3985 | } 3986 | 3987 | // Don't show diff if expected and actual are totally different 3988 | if ( showDiff ) { 3989 | message += ""; 3991 | } 3992 | } else if ( expected.indexOf( "[object Array]" ) !== -1 || 3993 | expected.indexOf( "[object Object]" ) !== -1 ) { 3994 | message += ""; 3999 | } 4000 | 4001 | if ( details.source ) { 4002 | message += ""; 4004 | } 4005 | 4006 | message += "
      Expected:
      " +
      3970 | 			expected +
      3971 | 			"
      Result:
      " +
      3976 | 				actual + "
      Diff:
      " +
      3990 | 					diff + "
      Message: " + 3995 | "Diff suppressed as the depth of object is more than current max depth (" + 3996 | QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + 3997 | " run with a higher max depth or " + 3998 | "Rerun without max depth.

      Source:
      " +
      4003 | 				escapeText( details.source ) + "
      "; 4007 | 4008 | // this occurs when pushFailure is set and we have an extracted stack trace 4009 | } else if ( !details.result && details.source ) { 4010 | message += "" + 4011 | "" + 4013 | "
      Source:
      " +
      4012 | 			escapeText( details.source ) + "
      "; 4014 | } 4015 | 4016 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 4017 | 4018 | assertLi = document.createElement( "li" ); 4019 | assertLi.className = details.result ? "pass" : "fail"; 4020 | assertLi.innerHTML = message; 4021 | assertList.appendChild( assertLi ); 4022 | }); 4023 | 4024 | QUnit.testDone(function( details ) { 4025 | var testTitle, time, testItem, assertList, 4026 | good, bad, testCounts, skipped, sourceName, 4027 | tests = id( "qunit-tests" ); 4028 | 4029 | if ( !tests ) { 4030 | return; 4031 | } 4032 | 4033 | testItem = id( "qunit-test-output-" + details.testId ); 4034 | 4035 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 4036 | 4037 | good = details.passed; 4038 | bad = details.failed; 4039 | 4040 | // store result when possible 4041 | if ( config.reorder && defined.sessionStorage ) { 4042 | if ( bad ) { 4043 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 4044 | } else { 4045 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 4046 | } 4047 | } 4048 | 4049 | if ( bad === 0 ) { 4050 | 4051 | // Collapse the passing tests 4052 | addClass( assertList, "qunit-collapsed" ); 4053 | } else if ( bad && config.collapse && !collapseNext ) { 4054 | 4055 | // Skip collapsing the first failing test 4056 | collapseNext = true; 4057 | } else { 4058 | 4059 | // Collapse remaining tests 4060 | addClass( assertList, "qunit-collapsed" ); 4061 | } 4062 | 4063 | // testItem.firstChild is the test name 4064 | testTitle = testItem.firstChild; 4065 | 4066 | testCounts = bad ? 4067 | "" + bad + ", " + "" + good + ", " : 4068 | ""; 4069 | 4070 | testTitle.innerHTML += " (" + testCounts + 4071 | details.assertions.length + ")"; 4072 | 4073 | if ( details.skipped ) { 4074 | testItem.className = "skipped"; 4075 | skipped = document.createElement( "em" ); 4076 | skipped.className = "qunit-skipped-label"; 4077 | skipped.innerHTML = "skipped"; 4078 | testItem.insertBefore( skipped, testTitle ); 4079 | } else { 4080 | addEvent( testTitle, "click", function() { 4081 | toggleClass( assertList, "qunit-collapsed" ); 4082 | }); 4083 | 4084 | testItem.className = bad ? "fail" : "pass"; 4085 | 4086 | time = document.createElement( "span" ); 4087 | time.className = "runtime"; 4088 | time.innerHTML = details.runtime + " ms"; 4089 | testItem.insertBefore( time, assertList ); 4090 | } 4091 | 4092 | // Show the source of the test when showing assertions 4093 | if ( details.source ) { 4094 | sourceName = document.createElement( "p" ); 4095 | sourceName.innerHTML = "Source: " + details.source; 4096 | addClass( sourceName, "qunit-source" ); 4097 | if ( bad === 0 ) { 4098 | addClass( sourceName, "qunit-collapsed" ); 4099 | } 4100 | addEvent( testTitle, "click", function() { 4101 | toggleClass( sourceName, "qunit-collapsed" ); 4102 | }); 4103 | testItem.appendChild( sourceName ); 4104 | } 4105 | }); 4106 | 4107 | if ( defined.document ) { 4108 | 4109 | // Avoid readyState issue with phantomjs 4110 | // Ref: #818 4111 | var notPhantom = ( function( p ) { 4112 | return !( p && p.version && p.version.major > 0 ); 4113 | } )( window.phantom ); 4114 | 4115 | if ( notPhantom && document.readyState === "complete" ) { 4116 | QUnit.load(); 4117 | } else { 4118 | addEvent( window, "load", QUnit.load ); 4119 | } 4120 | } else { 4121 | config.pageLoaded = true; 4122 | config.autorun = true; 4123 | } 4124 | 4125 | })(); 4126 | -------------------------------------------------------------------------------- /test/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/styles.css: -------------------------------------------------------------------------------- 1 | .frames { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /test/test-suite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XSSBuster Testsuite 6 | 19 | 20 | 21 | 22 | 23 | 24 |
      25 |
      26 | 27 | 28 | 29 | 30 |
      31 | 32 |
      33 | 34 | 35 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var hash = location.hash.slice(1); 4 | 5 | var rawSanPayload = 'evilVar=1//svg/alert(1)'; 6 | 7 | var sanPayload = encodeURIComponent('evilVar=1//svg/alert(1)'); 8 | 9 | var testDiv = document.getElementById('test'); 10 | 11 | /** 12 | * Register a new cross-browser event listener. 13 | * 14 | * @param evName {string}, the name of the event to register. 15 | * @param callback {function}, a callback function for the event listener. 16 | * @return void. 17 | */ 18 | var addListener = (function() { 19 | return (window.addEventListener) ? window.addEventListener : 20 | // for IE8 and earlier versions support 21 | function (evName, callback) { 22 | this.attachEvent('on' + evName, callback); 23 | }; 24 | })(); 25 | 26 | /** 27 | * Create a script element. 28 | * 29 | * @return {object}, a script node. 30 | */ 31 | var createScriptEl = function () { 32 | var scriptEl = document.createElement('script'); 33 | scriptEl.text = hash; 34 | return scriptEl; 35 | }; 36 | 37 | /** 38 | * Test any given sink function. 39 | * 40 | * @param fn {function}, a sink function. 41 | * @param name {string}, a sink function's name. 42 | * @return void. 43 | */ 44 | var sinkTest = function(fn, name) { 45 | QUnit.test(name + ' test', function(assert) { 46 | fn(hash); 47 | fn('var goodVar = 1;'); 48 | assert.ok(evilVar === 0, name + ' sanitized'); 49 | assert.ok(evilVar === 0, name + ' functional'); 50 | }); 51 | }; 52 | 53 | QUnit.test('window.name test', function(assert) { 54 | assert.equal(window.name, sanPayload, 'window.name sanitized'); 55 | }); 56 | 57 | QUnit.test('location.hash test', function(assert) { 58 | assert.equal(location.hash.slice(1), decodeURIComponent(sanPayload), 59 | 'location.hash sanitized'); 60 | }); 61 | 62 | QUnit.test('document.title test', function(assert) { 63 | assert.equal(document.title, sanPayload, 'document.title sanitized'); 64 | }); 65 | 66 | QUnit.test('location.search test', function(assert) { 67 | assert.equal(location.search.slice(5), 68 | sanPayload, 'location.search sanitized'); 69 | }); 70 | 71 | QUnit.test('Function constructor test', function(assert) { 72 | var fn = new Function(''); 73 | assert.ok(fn instanceof Function, 'fn is an instance of Function'); 74 | }); 75 | 76 | QUnit.test('document.cookie test', function(assert) { 77 | document.cookie = rawSanPayload; 78 | assert.notEqual(document.cookie, rawSanPayload, 'cookie sanitized'); 79 | }); 80 | 81 | QUnit.test('localStorage test', function(assert) { 82 | window.localStorage.setItem('test', rawSanPayload); 83 | assert.notEqual(localStorage.getItem('test'), rawSanPayload, 84 | 'localStorage sanitized'); 85 | }); 86 | 87 | QUnit.test('sessionStorage test', function(assert) { 88 | window.sessionStorage.setItem('test', rawSanPayload); 89 | assert.notEqual(sessionStorage.getItem('test'), rawSanPayload, 90 | 'sessionStorage sanitized'); 91 | }); 92 | 93 | QUnit.test('appendChild test', function(assert) { 94 | var scriptEl = createScriptEl(); 95 | document.body.appendChild(scriptEl); 96 | assert.ok(evilVar === 0, 'appendChild sanitized'); 97 | }); 98 | 99 | QUnit.test('insertAdjacentElement test', function(assert) { 100 | var scriptEl = createScriptEl(); 101 | testDiv.insertAdjacentElement('afterend', scriptEl); 102 | assert.ok(evilVar === 0, 'insertAdjacentElement sanitized'); 103 | }); 104 | 105 | QUnit.test('replaceChild test', function(assert) { 106 | var scriptEl = createScriptEl(); 107 | document.body.replaceChild(scriptEl, testDiv); 108 | assert.ok(evilVar === 0, 'replaceChild sanitized'); 109 | }); 110 | 111 | QUnit.test('insertBefore test', function(assert) { 112 | var scriptEl = createScriptEl(); 113 | document.body.insertBefore(scriptEl, document.body.childNodes[0]); 114 | assert.ok(evilVar === 0, 'insertBefore sanitized'); 115 | }); 116 | 117 | QUnit.test('embed element test', function(assert) { 118 | var embedEl = document.createElement('embed'); 119 | embedEl.src = hash; 120 | embedEl.style.display = 'none'; 121 | document.body.appendChild(embedEl); 122 | assert.ok(!embedEl.hasAttribute('src'), 'embed src sanitized'); 123 | }); 124 | 125 | QUnit.test('iframe element test', function(assert) { 126 | var iframeEl = document.createElement('iframe'); 127 | iframeEl.src = hash; 128 | iframeEl.srcdoc = hash; 129 | iframeEl.style.display = 'none'; 130 | document.body.appendChild(iframeEl); 131 | assert.ok(!iframeEl.hasAttribute('src'), 'iframe src sanitized'); 132 | assert.ok(!iframeEl.hasAttribute('srcdoc'), 'iframe srcdoc sanitized'); 133 | }); 134 | 135 | QUnit.test('applet element test', function(assert) { 136 | var appletEl = document.createElement('applet'); 137 | appletEl.code = hash; 138 | appletEl.codebase = hash; 139 | appletEl.archive = hash; 140 | appletEl.object = hash; 141 | appletEl.style.display = 'none'; 142 | document.body.appendChild(appletEl); 143 | assert.ok(!appletEl.hasAttribute('code'), 'applet code sanitized'); 144 | assert.ok(!appletEl.hasAttribute('codebase'), 145 | 'applet codebase sanitized'); 146 | assert.ok(!appletEl.hasAttribute('archive'), 147 | 'applet archive sanitized'); 148 | assert.ok(!appletEl.hasAttribute('object'), 'applet object sanitized'); 149 | }); 150 | 151 | QUnit.test('createContextualFragment test', function(assert) { 152 | var evilTagString = ''; 153 | var goodTagString = ''; 154 | var createDocumentFragement = function (tagString) { 155 | var range = document.createRange(); 156 | var documentFragment = range.createContextualFragment(tagString); 157 | document.body.appendChild(documentFragment); 158 | }; 159 | createDocumentFragement(evilTagString); 160 | createDocumentFragement(goodTagString); 161 | assert.ok(evilVar === 0, 'createContextualFragment sanitized'); 162 | assert.ok(goodVar === 1, 'createContextualFragment sanitized'); 163 | }); 164 | 165 | sinkTest(eval, 'eval'); 166 | sinkTest(setTimeout, 'setTimeout'); 167 | sinkTest(setInterval, 'setInterval'); 168 | 169 | addListener.call(window, 'message', function(ev) { 170 | QUnit.test('window.onmessage test', function(assert) { 171 | var _origin = ev.origin || ev.originalEvent.origin; 172 | if (_origin !== window.location.origin) { 173 | assert.equal(ev.data, sanPayload, 'message sanitized'); 174 | } else { 175 | assert.notEqual(ev.data, sanPayload, 176 | 'message not sanitized (same origin)'); 177 | } 178 | }); 179 | }); 180 | 181 | addListener.call(window, 'load', function() { 182 | var frames = document.getElementsByTagName('iframe'); 183 | var fIndex = frames.length; 184 | while (fIndex--) { 185 | (function (currentFrame) { 186 | var frameTest = function () { 187 | QUnit.test('iframe window.name test', function(assert) { 188 | assert.equal(currentFrame.name, 189 | sanPayload, 'iframe window.name sanitized'); 190 | }); 191 | QUnit.test('iframe location.hash test', function(assert) { 192 | assert.equal(currentFrame.location.hash.slice(1), 193 | sanPayload, 'iframe location.hash sanitized'); 194 | }); 195 | QUnit.test('iframe document.title test', function(assert) { 196 | assert.equal(currentFrame.document.title, 197 | sanPayload, 'iframe document.title sanitized'); 198 | }); 199 | QUnit.test('iframe location.search test', function(assert) { 200 | assert.equal(currentFrame.location.search.slice(5), 201 | sanPayload, 'iframe location.search sanitized'); 202 | }); 203 | nativeSetTimeout(function() { 204 | QUnit.test('deferred frame window.name test', function(assert) { 205 | assert.equal(currentFrame.name, 206 | sanPayload, 'iframe window.name sanitized (deferred)'); 207 | }); 208 | QUnit.test('deferred frame location.hash test', function(assert) { 209 | assert.equal(currentFrame.location.hash.slice(1), 210 | sanPayload, 'iframe location.hash sanitized (deferred)'); 211 | }); 212 | }, 2000); 213 | }; 214 | nativeSetTimeout(frameTest, 2000); 215 | }(frames[fIndex].contentWindow)); 216 | } 217 | }); 218 | })(); 219 | --------------------------------------------------------------------------------