├── .gitignore ├── .npmignore ├── package.json ├── index.html ├── LICENSE ├── README.md ├── qunit.css ├── test.js ├── test2.js ├── jsonpatch.coffee ├── jsonpatch.js ├── jslitmus.js └── qunit.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | qunit.css 4 | qunit.js 5 | index.html 6 | test.js 7 | jslitmus.js 8 | jsonpatch.coffee 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-patch", 3 | "author": "Byron Ruth ", 4 | "version": "0.7.0", 5 | "description": "A JavaScript implementation of the JSON Object Notation (JSON) Patch http://tools.ietf.org/html/rfc6902 and JSON Pointer http://tools.ietf.org/html/rfc6901 specifications.", 6 | "license": "BSD", 7 | "keywords": [ 8 | "diff", 9 | "patch", 10 | "json", 11 | "jsonpatch", 12 | "jsonpointer" 13 | ], 14 | "main": "jsonpatch.js", 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/bruth/jsonpatch-js.git" 21 | }, 22 | "devDependencies": { 23 | "coffee-script": "^1.12.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

QUnit Test Suite

15 |

16 |
17 |

18 |
    19 |
    20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014 Byron Ruth 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonpatch-js 2 | 3 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/bruth/jsonpatch-js/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 4 | 5 | Library to apply JSON Patches in JavaScript 6 | 7 | - JSON Patch - http://tools.ietf.org/html/rfc6902 8 | - JSON Pointer - http://tools.ietf.org/html/rfc6901 9 | 10 | jsonpatch-js works as in the browser as a script, as a Node module and as an 11 | AMD module. 12 | 13 | ## Install 14 | 15 | **Bower** 16 | 17 | ``` 18 | bower install json-patch 19 | ``` 20 | 21 | **NPM** 22 | 23 | ``` 24 | npm install json-patch 25 | ``` 26 | 27 | **Note: at this time, all operations are applied in-place.** 28 | 29 | ## Methods 30 | 31 | **`jsonpatch.apply(document, patch)`** 32 | 33 | Applies a patch to the document 34 | 35 | **`jsonpatch.compile(patch)`** 36 | 37 | Compiles a patch and returns a function that takes a document to apply the patch to. 38 | 39 | ## Patch Operations 40 | 41 | ### Add 42 | 43 | Patch syntax: `{op: 'add', path: , value: }` 44 | 45 | ```javascript 46 | // Add property, result: {foo: 'bar'} 47 | jsonpatch.apply({}, [{op: 'add', path: '/foo', value: 'bar'}]); 48 | 49 | // Add array element, result: {foo: [1, 2, 3]} 50 | jsonpatch.apply({foo: [1, 3]}, [{op: 'add', path: '/foo/1', value: 2}]); 51 | 52 | // Complex, result: {foo: [{bar: 'baz'}]} 53 | jsonpatch.apply({foo: [{}]}, [{op: 'add', path: '/foo/0/bar', value: 'baz'}]); 54 | ``` 55 | 56 | ### Remove 57 | 58 | Patch syntax: `{op: 'remove', path: }` 59 | 60 | ```javascript 61 | // Remove property, result: {} 62 | jsonpatch.apply({foo: 'bar'}, [{op: 'remove', path: '/foo'}]); 63 | 64 | // Remove array element, result: {foo: [1, 3]} 65 | jsonpatch.apply({foo: [1, 2, 3]}, [{op: 'remove', path: '/foo/1'}]); 66 | 67 | // Complex, result: {foo: [{}]} 68 | jsonpatch.apply({foo: [{bar: 'baz'}]}, [{op: 'remove', path: '/foo/0/bar'}]); 69 | ``` 70 | 71 | ### Replace 72 | 73 | Patch syntax: `{op: 'replace', path: , value: }` 74 | 75 | ```javascript 76 | // Replace property, result: {foo: 1} 77 | jsonpatch.apply({foo: 'bar'}, [{op: 'replace', path: '/foo', value: 1}]); 78 | 79 | // Replace array element, result: {foo: [1, 4, 3]} 80 | jsonpatch.apply({foo: [1, 2, 3]}, [{op: 'replace', path: '/foo/1', value: 4}]); 81 | 82 | // Complex, result: {foo: [{bar: 1}]} 83 | jsonpatch.apply({foo: [{bar: 'baz'}]}, [{op: 'replace', path: '/foo/0/bar', value: 1}]); 84 | ``` 85 | 86 | ### Move 87 | 88 | Patch syntax: `{op: 'move', from: , path: }` 89 | 90 | ```javascript 91 | // Move property, result {bar: [1, 2, 3]} 92 | jsonpatch.apply({foo: [1, 2, 3]}, [{op: 'move', from: '/foo', path: '/bar'}]); 93 | ``` 94 | 95 | ### Copy 96 | 97 | Patch syntax: `{op: 'copy', from: , path: }` 98 | 99 | ```javascript 100 | // Copy property, result {foo: [1, 2, 3], bar: 2} 101 | jsonpatch.apply({foo: [1, 2, 3]}, [{op: 'copy', from: '/foo/1', path: '/bar'}]); 102 | ``` 103 | 104 | ### Test 105 | 106 | Patch syntax: `{op: 'test', path: , value: }` 107 | 108 | ```javascript 109 | // Test equality of property to value, result: true 110 | jsonpatch.apply({foo: 'bar'}, [{op: 'test', path: '/foo', value: 'bar'}] 111 | ``` 112 | 113 | *Changed in 0.5.0* 114 | 115 | The return value is no longer a boolean, but now the the document itself which adheres correctly to the specification. It the test fails, a `PatchTestFailed` error will be thrown. 116 | 117 | ## Error Types 118 | 119 | **`JSONPatchError`** 120 | 121 | Base error type which all patch errors extend from. 122 | 123 | **`InvalidPointerError`** 124 | 125 | Thrown when the pointer is invalid. 126 | 127 | **`InvalidPatchError`** 128 | 129 | Thrown when the patch itself has an invalid syntax. 130 | 131 | **`PatchConflictError`** 132 | 133 | Thrown when there is a conflic with applying the patch to the document. 134 | 135 | **`PatchTestFailed`** 136 | 137 | Thrown when a test operation is applied and fails. 138 | -------------------------------------------------------------------------------- /qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests ol { 115 | margin-top: 0.5em; 116 | padding: 0.5em; 117 | 118 | background-color: #fff; 119 | 120 | border-radius: 5px; 121 | -moz-border-radius: 5px; 122 | -webkit-border-radius: 5px; 123 | } 124 | 125 | #qunit-tests table { 126 | border-collapse: collapse; 127 | margin-top: .2em; 128 | } 129 | 130 | #qunit-tests th { 131 | text-align: right; 132 | vertical-align: top; 133 | padding: 0 .5em 0 0; 134 | } 135 | 136 | #qunit-tests td { 137 | vertical-align: top; 138 | } 139 | 140 | #qunit-tests pre { 141 | margin: 0; 142 | white-space: pre-wrap; 143 | word-wrap: break-word; 144 | } 145 | 146 | #qunit-tests del { 147 | background-color: #e0f2be; 148 | color: #374e0c; 149 | text-decoration: none; 150 | } 151 | 152 | #qunit-tests ins { 153 | background-color: #ffcaca; 154 | color: #500; 155 | text-decoration: none; 156 | } 157 | 158 | /*** Test Counts */ 159 | 160 | #qunit-tests b.counts { color: black; } 161 | #qunit-tests b.passed { color: #5E740B; } 162 | #qunit-tests b.failed { color: #710909; } 163 | 164 | #qunit-tests li li { 165 | padding: 5px; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #3c510c; 175 | background-color: #fff; 176 | border-left: 10px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 10px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 5px 5px; 198 | -moz-border-radius: 0 0 5px 5px; 199 | -webkit-border-bottom-right-radius: 5px; 200 | -webkit-border-bottom-left-radius: 5px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | #qunit-testresult .module-name { 224 | font-weight: bold; 225 | } 226 | 227 | /** Fixture */ 228 | 229 | #qunit-fixture { 230 | position: absolute; 231 | top: -10000px; 232 | left: -10000px; 233 | width: 1000px; 234 | height: 1000px; 235 | } 236 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var obj, compiled; 2 | 3 | module('Original Tests'); 4 | 5 | // QUnit 6 | test('invalid patches', function() { 7 | throws(function() { 8 | jsonpatch.apply({}, [{foo: '/bar'}]); 9 | }, jsonpatch.InvalidPatchError, 'Bad operation'); 10 | 11 | throws(function() { 12 | jsonpatch.apply({}, [{op: 'add', path: ''}]); 13 | }, jsonpatch.InvalidPatchError, 'Path must start with a /'); 14 | }); 15 | 16 | test('add', function() { 17 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 18 | 19 | jsonpatch.apply(obj, [{op: 'add', path: '/bar', value: [1, 2, 3, 4]}]); 20 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}); 21 | 22 | jsonpatch.apply(obj, [{op: 'add', path: '/baz/0/foo', value: 'world'}]); 23 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello', foo: 'world'}], bar: [1, 2, 3, 4]}); 24 | 25 | raises(function() { 26 | jsonpatch.apply(obj, [{op: 'add', path: '/bar/8', value: '5'}]); 27 | }, jsonpatch.PatchConflictError, 'Out of bounds (upper)'); 28 | 29 | raises(function() { 30 | jsonpatch.apply(obj, [{op: 'add', path: '/bar/-1', value: '5'}]); 31 | }, jsonpatch.PatchConflictError, 'Out of bounds (lower)'); 32 | 33 | raises(function() { 34 | jsonpatch.apply(obj, [{op: 'add', path: '/bar/8'}]); 35 | }, jsonpatch.InvalidPatchError, 'Patch member value not defined'); 36 | 37 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 38 | jsonpatch.apply(obj, [{op: 'add', path: '/bar', value: true}]); 39 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}], bar: true}); 40 | 41 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 42 | jsonpatch.apply(obj, [{op: 'add', path: '/bar', value: false}]); 43 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}], bar: false}); 44 | 45 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 46 | jsonpatch.apply(obj, [{op: 'add', path: '/bar', value: null}]); 47 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}], bar: null}); 48 | }); 49 | 50 | 51 | test('remove', function() { 52 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 53 | jsonpatch.apply(obj, [{op: 'remove', path: '/bar'}]); 54 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}]}); 55 | 56 | jsonpatch.apply(obj, [{op: 'remove', path: '/baz/0/qux'}]); 57 | deepEqual(obj, {foo: 1, baz: [{}]}); 58 | }); 59 | 60 | 61 | test('replace', function() { 62 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 63 | 64 | jsonpatch.apply(obj, [{op: 'replace', path: '/foo', value: [1, 2, 3, 4]}]); 65 | deepEqual(obj, {foo: [1, 2, 3, 4], baz: [{qux: 'hello'}]}); 66 | 67 | jsonpatch.apply(obj, [{op: 'replace', path: '/baz/0/qux', value: 'world'}]); 68 | deepEqual(obj, {foo: [1, 2, 3, 4], baz: [{qux: 'world'}]}); 69 | }); 70 | 71 | 72 | test('test', function() { 73 | obj = {foo: {bar: [1, 2, 5, 4]}}; 74 | deepEqual(obj, jsonpatch.apply(obj, [{op: 'test', path: '/foo', value: {bar: [1, 2, 5, 4]}}])); 75 | raises(function() { 76 | jsonpatch.apply(obj, [{op: 'test', path: '/foo', value: [1, 2]}]); 77 | }, jsonpatch.PatchTestFailed); 78 | }); 79 | 80 | 81 | test('move', function() { 82 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 83 | 84 | jsonpatch.apply(obj, [{op: 'move', from: '/foo', path: '/bar'}]); 85 | deepEqual(obj, {baz: [{qux: 'hello'}], bar: 1}); 86 | 87 | jsonpatch.apply(obj, [{op: 'move', from: '/baz/0/qux', path: '/baz/1'}]); 88 | deepEqual(obj, {baz: [{}, 'hello'], bar: 1}); 89 | }); 90 | 91 | 92 | test('copy', function() { 93 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 94 | 95 | jsonpatch.apply(obj, [{op: 'copy', from: '/foo', path: '/bar'}]); 96 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}], bar: 1}); 97 | 98 | jsonpatch.apply(obj, [{op: 'copy', from: '/baz/0/qux', path: '/baz/1'}]); 99 | deepEqual(obj, {foo: 1, baz: [{qux: 'hello'}, 'hello'], bar: 1}); 100 | }); 101 | 102 | 103 | 104 | // JSLitmus 105 | JSLitmus.test('Add Operation', function() { 106 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 107 | jsonpatch.apply(obj, [{op: 'add', path: '/bar', value: [1, 2, 3, 4]}]); 108 | }); 109 | 110 | JSLitmus.test('Remove Operation', function() { 111 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 112 | jsonpatch.apply(obj, [{op: 'remove', path: '/bar'}]); 113 | }); 114 | 115 | JSLitmus.test('Replace Operation', function() { 116 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 117 | jsonpatch.apply(obj, [{op: 'replace', path: '/foo', value: [1, 2, 3, 4]}]); 118 | }); 119 | 120 | JSLitmus.test('Move Operation', function() { 121 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 122 | jsonpatch.apply(obj, [{op: 'move', from: '/baz/0', path: '/bar/0'}]); 123 | }); 124 | 125 | JSLitmus.test('Copy Operation', function() { 126 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 127 | jsonpatch.apply(obj, [{op: 'copy', from: '/baz/0', path: '/bar/0'}]); 128 | }); 129 | 130 | 131 | JSLitmus.test('Test Operation', function() { 132 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 133 | jsonpatch.apply(obj, [{op: 'test', path: '/baz', value: [{qux: 'hello'}]}]); 134 | }); 135 | 136 | var addCompiled = jsonpatch.compile([{op: 'add', path: '/bar', value: [1, 2, 3, 4]}]); 137 | JSLitmus.test('Compiled Add Operation', function() { 138 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 139 | addCompiled(obj); 140 | }); 141 | 142 | var removeCompiled = jsonpatch.compile([{op: 'remove', path: '/bar'}]); 143 | JSLitmus.test('Compiled Remove Operation', function() { 144 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 145 | removeCompiled(obj); 146 | }); 147 | 148 | var replaceCompiled = jsonpatch.compile([{op: 'replace', path: '/foo', value: [1, 2, 3, 4]}]); 149 | JSLitmus.test('Compiled Replace Operation', function() { 150 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 151 | replaceCompiled(obj); 152 | }); 153 | 154 | var moveCompiled = jsonpatch.compile([{op: 'move', from: '/baz/0', path: '/bar/0'}]); 155 | JSLitmus.test('Compiled Move Operation', function() { 156 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 157 | moveCompiled(obj); 158 | }); 159 | 160 | var copyCompiled = jsonpatch.compile([{op: 'copy', from: '/baz/0', path: '/bar/0'}]); 161 | JSLitmus.test('Compiled Copy Operation', function() { 162 | obj = {foo: 1, baz: [{qux: 'hello'}], bar: [1, 2, 3, 4]}; 163 | copyCompiled(obj); 164 | }); 165 | 166 | 167 | var testCompiled = jsonpatch.compile([{op: 'test', path: '/baz', value: [{qux: 'hello'}]}]); 168 | JSLitmus.test('Compiled Test Operation', function() { 169 | obj = {foo: 1, baz: [{qux: 'hello'}]}; 170 | testCompiled(obj); 171 | }); 172 | -------------------------------------------------------------------------------- /test2.js: -------------------------------------------------------------------------------- 1 | // json-patch-tests 2 | var tests = [ 3 | 4 | { "comment": "empty list, empty docs", 5 | "doc": {}, 6 | "patch": [], 7 | "expected": {} }, 8 | 9 | { "comment": "empty patch list", 10 | "doc": {"foo": 1}, 11 | "patch": [], 12 | "expected": {"foo": 1} }, 13 | 14 | { "comment": "rearrangements OK?", 15 | "doc": {"foo": 1, "bar": 2}, 16 | "patch": [], 17 | "expected": {"bar":2, "foo": 1} }, 18 | 19 | { "comment": "rearrangements OK? How about one level down ... array", 20 | "doc": [{"foo": 1, "bar": 2}], 21 | "patch": [], 22 | "expected": [{"bar":2, "foo": 1}] }, 23 | 24 | { "comment": "rearrangements OK? How about one level down...", 25 | "doc": {"foo":{"foo": 1, "bar": 2}}, 26 | "patch": [], 27 | "expected": {"foo":{"bar":2, "foo": 1}} }, 28 | 29 | { "comment": "add replaces any existing field", 30 | "doc": {"foo": null}, 31 | "patch": [{"op": "add", "path": "/foo", "value":1}], 32 | "expected": {"foo": 1} }, 33 | 34 | { "comment": "toplevel array", 35 | "doc": [], 36 | "patch": [{"op": "add", "path": "/0", "value": "foo"}], 37 | "expected": ["foo"] }, 38 | 39 | { "comment": "toplevel array, no change", 40 | "doc": ["foo"], 41 | "patch": [], 42 | "expected": ["foo"] }, 43 | 44 | { "comment": "toplevel object, numeric string", 45 | "doc": {}, 46 | "patch": [{"op": "add", "path": "/foo", "value": "1"}], 47 | "expected": {"foo":"1"} }, 48 | 49 | { "comment": "toplevel object, integer", 50 | "doc": {}, 51 | "patch": [{"op": "add", "path": "/foo", "value": 1}], 52 | "expected": {"foo":1} }, 53 | 54 | { "comment": "Toplevel scalar values OK?", 55 | "doc": "foo", 56 | "patch": [{"op": "replace", "path": "", "value": "bar"}], 57 | "expected": "bar", 58 | "disabled": true }, 59 | 60 | { "comment": "Add, / target", 61 | "doc": {}, 62 | "patch": [ {"op": "add", "path": "/", "value":1 } ], 63 | "expected": {"":1} }, 64 | 65 | { "comment": "Add composite value at top level", 66 | "doc": {"foo": 1}, 67 | "patch": [{"op": "add", "path": "/bar", "value": [1, 2]}], 68 | "expected": {"foo": 1, "bar": [1, 2]} }, 69 | 70 | { "comment": "Add into composite value", 71 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 72 | "patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}], 73 | "expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} }, 74 | 75 | { "doc": {"bar": [1, 2]}, 76 | "patch": [{"op": "add", "path": "/bar/8", "value": "5"}], 77 | "error": "Out of bounds (upper)" }, 78 | 79 | { "doc": {"bar": [1, 2]}, 80 | "patch": [{"op": "add", "path": "/bar/-1", "value": "5"}], 81 | "error": "Out of bounds (lower)" }, 82 | 83 | { "doc": {"foo": 1}, 84 | "patch": [{"op": "add", "path": "/bar", "value": true}], 85 | "expected": {"foo": 1, "bar": true} }, 86 | 87 | { "doc": {"foo": 1}, 88 | "patch": [{"op": "add", "path": "/bar", "value": false}], 89 | "expected": {"foo": 1, "bar": false} }, 90 | 91 | { "doc": {"foo": 1}, 92 | "patch": [{"op": "add", "path": "/bar", "value": null}], 93 | "expected": {"foo": 1, "bar": null} }, 94 | 95 | { "comment": "0 can be an array index or object element name", 96 | "doc": {"foo": 1}, 97 | "patch": [{"op": "add", "path": "/0", "value": "bar"}], 98 | "expected": {"foo": 1, "0": "bar" } }, 99 | 100 | { "doc": ["foo"], 101 | "patch": [{"op": "add", "path": "/1", "value": "bar"}], 102 | "expected": ["foo", "bar"] }, 103 | 104 | { "doc": ["foo", "sil"], 105 | "patch": [{"op": "add", "path": "/1", "value": "bar"}], 106 | "expected": ["foo", "bar", "sil"] }, 107 | 108 | { "doc": ["foo", "sil"], 109 | "patch": [{"op": "add", "path": "/0", "value": "bar"}], 110 | "expected": ["bar", "foo", "sil"] }, 111 | 112 | { "doc": ["foo", "sil"], 113 | "patch": [{"op":"add", "path": "/2", "value": "bar"}], 114 | "expected": ["foo", "sil", "bar"] }, 115 | 116 | { "comment": "test against implementation-specific numeric parsing", 117 | "doc": {"1e0": "foo"}, 118 | "patch": [{"op": "test", "path": "/1e0", "value": "foo"}], 119 | "test": true }, 120 | 121 | { "comment": "test with bad number should fail", 122 | "doc": ["foo", "bar"], 123 | "patch": [{"op": "test", "path": "/1e0", "value": "bar"}], 124 | "error": "test op shouldn't get array element 1" }, 125 | 126 | { "doc": ["foo", "sil"], 127 | "patch": [{"op": "add", "path": "/bar", "value": 42}], 128 | "error": "Object operation on array target" }, 129 | 130 | { "doc": ["foo", "sil"], 131 | "patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}], 132 | "expected": ["foo", ["bar", "baz"], "sil"], 133 | "comment": "value in array add not flattened" }, 134 | 135 | { "doc": {"foo": 1, "bar": [1, 2, 3, 4]}, 136 | "patch": [{"op": "remove", "path": "/bar"}], 137 | "expected": {"foo": 1} }, 138 | 139 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 140 | "patch": [{"op": "remove", "path": "/baz/0/qux"}], 141 | "expected": {"foo": 1, "baz": [{}]} }, 142 | 143 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 144 | "patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}], 145 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} }, 146 | 147 | { "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]}, 148 | "patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}], 149 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} }, 150 | 151 | { "doc": ["foo"], 152 | "patch": [{"op": "replace", "path": "/0", "value": "bar"}], 153 | "expected": ["bar"] }, 154 | 155 | { "doc": [""], 156 | "patch": [{"op": "replace", "path": "/0", "value": 0}], 157 | "expected": [0] }, 158 | 159 | { "doc": [""], 160 | "patch": [{"op": "replace", "path": "/0", "value": true}], 161 | "expected": [true] }, 162 | 163 | { "doc": [""], 164 | "patch": [{"op": "replace", "path": "/0", "value": false}], 165 | "expected": [false] }, 166 | 167 | { "doc": [""], 168 | "patch": [{"op": "replace", "path": "/0", "value": null}], 169 | "expected": [null] }, 170 | 171 | { "doc": ["foo", "sil"], 172 | "patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}], 173 | "expected": ["foo", ["bar", "baz"]], 174 | "comment": "value in array replace not flattened" }, 175 | 176 | { "comment": "replace whole document", 177 | "doc": {"foo": "bar"}, 178 | "patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}], 179 | "expected": {"baz": "qux"} }, 180 | 181 | { "comment": "spurious patch properties", 182 | "doc": {"foo": 1}, 183 | "patch": [{"op": "test", "path": "/foo", "value": 1, "spurious": 1}], 184 | "test": true }, 185 | 186 | { "doc": {"foo": null}, 187 | "patch": [{"op": "test", "path": "/foo", "value": null}], 188 | "comment": "null value should still be valid obj property", 189 | "test": true }, 190 | 191 | { "doc": {"foo": {"foo": 1, "bar": 2}}, 192 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": 2, "foo": 1}}], 193 | "comment": "test should pass despite rearrangement", 194 | "test": true }, 195 | 196 | { "doc": {"foo": [{"foo": 1, "bar": 2}]}, 197 | "patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}], 198 | "comment": "test should pass despite (nested) rearrangement", 199 | "test": true }, 200 | 201 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, 202 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": [1, 2, 5, 4]}}], 203 | "comment": "test should pass - no error", 204 | "test": true }, 205 | 206 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, 207 | "patch": [{"op": "test", "path": "/foo", "value": [1, 2]}], 208 | "comment": "test op should fail", 209 | "error": "patch test fail" }, 210 | 211 | { "comment": "Whole document", 212 | "doc": { "foo": 1 }, 213 | "patch": [{"op": "test", "path": "", "value": {"foo": 1}}], 214 | "expected": true, 215 | "disabled": true }, 216 | 217 | { "comment": "Empty-string element", 218 | "doc": { "": 1 }, 219 | "patch": [{"op": "test", "path": "/", "value": 1}], 220 | "test": true }, 221 | 222 | { "doc": { 223 | "foo": ["bar", "baz"], 224 | "": 0, 225 | "a/b": 1, 226 | "c%d": 2, 227 | "e^f": 3, 228 | "g|h": 4, 229 | "i\\j": 5, 230 | "k\"l": 6, 231 | " ": 7, 232 | "m~n": 8 233 | }, 234 | "patch": [{"op": "test", "path": "/foo", "value": ["bar", "baz"]}, 235 | {"op": "test", "path": "/foo/0", "value": "bar"}, 236 | {"op": "test", "path": "/", "value": 0}, 237 | {"op": "test", "path": "/a~1b", "value": 1}, 238 | {"op": "test", "path": "/c%d", "value": 2}, 239 | {"op": "test", "path": "/e^f", "value": 3}, 240 | {"op": "test", "path": "/g|h", "value": 4}, 241 | {"op": "test", "path": "/i\\j", "value": 5}, 242 | {"op": "test", "path": "/k\"l", "value": 6}, 243 | {"op": "test", "path": "/ ", "value": 7}, 244 | {"op": "test", "path": "/m~0n", "value": 8}], 245 | "test": true }, 246 | 247 | { "comment": "Move to same location has no effect", 248 | "doc": {"foo": 1}, 249 | "patch": [{"op": "move", "from": "/foo", "path": "/foo"}], 250 | "expected": {"foo": 1} }, 251 | 252 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 253 | "patch": [{"op": "move", "from": "/foo", "path": "/bar"}], 254 | "expected": {"baz": [{"qux": "hello"}], "bar": 1} }, 255 | 256 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, 257 | "patch": [{"op": "move", "from": "/baz/0/qux", "path": "/baz/1"}], 258 | "expected": {"baz": [{}, "hello"], "bar": 1} }, 259 | 260 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, 261 | "patch": [{"op": "copy", "from": "/baz/0", "path": "/boo"}], 262 | "expected": {"baz":[{"qux":"hello"}],"bar":1,"boo":{"qux":"hello"}} }, 263 | 264 | { "comment": "replacing the root of the document is possible with add", 265 | "doc": {"foo": "bar"}, 266 | "patch": [{"op": "add", "path": "", "value": {"baz": "qux"}}], 267 | "expected": {"baz":"qux"}}, 268 | 269 | { "comment": "Adding to \"/-\" adds to the end of the array", 270 | "doc": [ 1, 2 ], 271 | "patch": [ { "op": "add", "path": "/-", "value": { "foo": [ "bar", "baz" ] } } ], 272 | "expected": [ 1, 2, { "foo": [ "bar", "baz" ] } ]}, 273 | 274 | { "comment": "Adding to \"/-\" adds to the end of the array, even n levels down", 275 | "doc": [ 1, 2, [ 3, [ 4, 5 ] ] ], 276 | "patch": [ { "op": "add", "path": "/2/1/-", "value": { "foo": [ "bar", "baz" ] } } ], 277 | "expected": [ 1, 2, [ 3, [ 4, 5, { "foo": [ "bar", "baz" ] } ] ] ]}, 278 | 279 | { "comment": "test remove with bad number should fail", 280 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 281 | "patch": [{"op": "remove", "path": "/baz/1e0/qux"}], 282 | "error": "remove op shouldn't remove from array with bad number" }, 283 | 284 | { "comment": "test remove on array", 285 | "doc": [1, 2, 3, 4], 286 | "patch": [{"op": "remove", "path": "/0"}], 287 | "expected": [2, 3, 4] }, 288 | 289 | { "comment": "test remove with bad index should fail", 290 | "doc": [1, 2, 3, 4], 291 | "patch": [{"op": "remove", "path": "/1e0"}], 292 | "error": "remove op shouldn't remove from array with bad number" }, 293 | 294 | { "comment": "test replace with bad number should fail", 295 | "doc": [""], 296 | "patch": [{"op": "replace", "path": "/1e0", "value": false}], 297 | "error": "replace op shouldn't replace in array with bad number" }, 298 | 299 | { "comment": "test copy with bad number should fail", 300 | "doc": {"baz": [1,2,3], "bar": 1}, 301 | "patch": [{"op": "copy", "from": "/baz/1e0", "path": "/boo"}], 302 | "error": "copy op shouldn't work with bad number" }, 303 | 304 | { "comment": "test move with bad number should fail", 305 | "doc": {"foo": 1, "baz": [1,2,3,4]}, 306 | "patch": [{"op": "move", "from": "/baz/1e0", "path": "/foo"}], 307 | "error": "move op shouldn't work with bad number" }, 308 | 309 | { "comment": "test add with bad number should fail", 310 | "doc": ["foo", "sil"], 311 | "patch": [{"op": "add", "path": "/1e0", "value": "bar"}], 312 | "error": "add op shouldn't add to array with bad number" } 313 | ]; 314 | 315 | 316 | module('JSON Patch Tests'); 317 | 318 | function runTest(config) { 319 | test(config.comment, function() { 320 | if (config.doc == null || config.disabled) { 321 | expect(0); 322 | return; 323 | } 324 | 325 | if (config.error) { 326 | raises(function() { 327 | jsonpatch.apply(config.doc, config.patch); 328 | }); 329 | } else if (config.test) { 330 | output = jsonpatch.apply(config.doc, config.patch); 331 | deepEqual(output, config.doc); 332 | } else { 333 | output = jsonpatch.apply(config.doc, config.patch); 334 | deepEqual(output, config.expected); 335 | } 336 | expect(1); 337 | }); 338 | } 339 | 340 | for (var i = 0; i < tests.length; i++) { 341 | runTest(tests[i]); 342 | } 343 | -------------------------------------------------------------------------------- /jsonpatch.coffee: -------------------------------------------------------------------------------- 1 | # jsonpatch.js 0.6.1 2 | # (c) 2011-2017 Byron Ruth 3 | # jsonpatch may be freely distributed under the BSD license 4 | 5 | ((factory) -> 6 | # Detect global object for browser, node, and worker 7 | root = if window? then window else if global? then global else @; 8 | if typeof exports isnt 'undefined' 9 | # Node/CommonJS 10 | factory(exports) 11 | else if typeof define is 'function' and define.amd 12 | # AMD 13 | define ['exports'], (exports) -> 14 | root.jsonpatch = factory(exports) 15 | else 16 | # Browser globals 17 | root.jsonpatch = factory({}) 18 | ) (exports) -> 19 | 20 | # Utilities 21 | toString = Object.prototype.toString 22 | hasOwnProperty = Object.prototype.hasOwnProperty 23 | 24 | # Define a few helper functions taken from the awesome underscore library 25 | isArray = (obj) -> toString.call(obj) is '[object Array]' 26 | isObject = (obj) -> toString.call(obj) is '[object Object]' 27 | isString = (obj) -> toString.call(obj) is '[object String]' 28 | 29 | # Limited Underscore.js implementation, internal recursive comparison function. 30 | _isEqual = (a, b, stack) -> 31 | # Identical objects are equal. `0 === -0`, but they aren't identical. 32 | # See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 33 | if a is b then return a isnt 0 or 1 / a == 1 / b 34 | # A strict comparison is necessary because `null == undefined`. 35 | if a == null or b == null then return a is b 36 | # Compare `[[Class]]` names. 37 | className = toString.call(a) 38 | if className isnt toString.call(b) then return false 39 | switch className 40 | # Strings, numbers, and booleans are compared by value. 41 | when '[object String]' 42 | # Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 43 | # equivalent to `new String("5")`. 44 | String(a) is String(b) 45 | when '[object Number]' 46 | a = +a 47 | b = +b 48 | # `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 49 | # other numeric values. 50 | if a isnt a 51 | b isnt b 52 | else 53 | if a is 0 54 | 1 / a is 1 / b 55 | else 56 | a is b 57 | when '[object Boolean]' 58 | # Coerce dates and booleans to numeric primitive values. Dates are compared by their 59 | # millisecond representations. Note that invalid dates with millisecond representations 60 | # of `NaN` are not equivalent. 61 | +a is +b 62 | 63 | if typeof a isnt 'object' or typeof b isnt 'object' then return false 64 | # Assume equality for cyclic structures. The algorithm for detecting cyclic 65 | # structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 66 | length = stack.length 67 | while length-- 68 | # Linear search. Performance is inversely proportional to the number of 69 | # unique nested structures. 70 | if stack[length] is a then return true 71 | 72 | # Add the first object to the stack of traversed objects. 73 | stack.push(a) 74 | size = 0 75 | result = true 76 | # Recursively compare objects and arrays. 77 | if className is '[object Array]' 78 | # Compare array lengths to determine if a deep comparison is necessary. 79 | size = a.length 80 | result = size is b.length 81 | if result 82 | # Deep compare the contents, ignoring non-numeric properties. 83 | while size-- 84 | # Ensure commutative equality for sparse arrays. 85 | if not (result = size in a is size in b and _isEqual(a[size], b[size], stack)) then break 86 | else 87 | # Objects with different constructors are not equivalent. 88 | if "constructor" in a isnt "constructor" in b or a.constructor isnt b.constructor then return false 89 | # Deep compare objects. 90 | for key of a 91 | if hasOwnProperty.call(a, key) 92 | # Count the expected number of properties. 93 | size++ 94 | # Deep compare each member. 95 | if not (result = hasOwnProperty.call(b, key) and _isEqual(a[key], b[key], stack)) then break 96 | 97 | # Ensure that both objects contain the same number of properties. 98 | if result 99 | for key of b 100 | if hasOwnProperty.call(b, key) and not size-- then break 101 | result = not size 102 | # Remove the first object from the stack of traversed objects. 103 | stack.pop() 104 | return result 105 | 106 | # Perform a deep comparison to check if two objects are equal. 107 | isEqual = (a, b) -> _isEqual(a, b, []) 108 | 109 | 110 | # Various error constructors 111 | class JSONPatchError extends Error 112 | constructor: (@message='JSON patch error') -> 113 | @name = 'JSONPatchError' 114 | 115 | class InvalidPointerError extends Error 116 | constructor: (@message='Invalid pointer') -> 117 | @name = 'InvalidPointer' 118 | 119 | class InvalidPatchError extends JSONPatchError 120 | constructor: (@message='Invalid patch') -> 121 | @name = 'InvalidPatch' 122 | 123 | class PatchConflictError extends JSONPatchError 124 | constructor: (@message='Patch conflict') -> 125 | @name = 'PatchConflictError' 126 | 127 | class PatchTestFailed extends Error 128 | constructor: (@message='Patch test failed') -> 129 | @name = 'PatchTestFailed' 130 | 131 | 132 | escapedSlash = /~1/g 133 | escapedTilde = /~0/g 134 | accessorMatch = /^[-+]?\d+$/ 135 | 136 | # Spec: http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-05 137 | class JSONPointer 138 | constructor: (path) -> 139 | steps = [] 140 | # If a path is specified, it must start with a / 141 | if path and (steps = path.split '/').shift() isnt '' 142 | throw new InvalidPointerError() 143 | 144 | # Decode each component, decode JSON Pointer specific syntax ~0 and ~1 145 | for step, i in steps 146 | steps[i] = step.replace(escapedSlash, '/') 147 | .replace(escapedTilde, '~') 148 | 149 | # The final segment is the accessor (property/index) of the object 150 | # the pointer ultimately references 151 | @accessor = steps.pop() 152 | @steps = steps 153 | @path = path 154 | 155 | # Returns an object with the object reference and the accessor 156 | getReference: (parent) -> 157 | for step in @steps 158 | if isArray parent then step = parseInt(step, 10) 159 | if step not of parent 160 | throw new PatchConflictError('Array location out of 161 | bounds or not an instance property') 162 | parent = parent[step] 163 | return parent 164 | 165 | # Checks and coerces the accessor relative to the reference 166 | # object it will be applied to. 167 | coerce: (reference, accessor) -> 168 | if isArray(reference) 169 | if isString(accessor) 170 | if accessor is '-' 171 | accessor = reference.length 172 | else if accessorMatch.test(accessor) 173 | accessor = parseInt(accessor, 10) 174 | else 175 | throw new InvalidPointerError('Invalid array index number') 176 | 177 | return accessor 178 | 179 | 180 | # Interface for patch operation classes 181 | class JSONPatch 182 | constructor: (patch) -> 183 | # All patches required a 'path' member 184 | if 'path' not of patch 185 | throw new InvalidPatchError() 186 | 187 | # Validates the patch based on the requirements of this operation 188 | @validate(patch) 189 | @patch = patch 190 | # Create the primary pointer for this operation 191 | @path = new JSONPointer(patch.path) 192 | # Call for operation-specific setup 193 | @initialize(patch) 194 | 195 | initialize: -> 196 | 197 | validate: (patch) -> 198 | 199 | apply: (document) -> throw new Error('Method not implemented') 200 | 201 | 202 | class AddPatch extends JSONPatch 203 | validate: (patch) -> 204 | if 'value' not of patch then throw new InvalidPatchError() 205 | 206 | apply: (document) -> 207 | reference = @path.getReference(document) 208 | accessor = @path.accessor 209 | value = @patch.value 210 | 211 | if isArray(reference) 212 | accessor = @path.coerce(reference, accessor) 213 | if accessor < 0 or accessor > reference.length 214 | throw new PatchConflictError("Index #{accessor} out of bounds") 215 | reference.splice(accessor, 0, value) 216 | else if not accessor? 217 | document = value 218 | else 219 | reference[accessor] = value 220 | return document 221 | 222 | 223 | class RemovePatch extends JSONPatch 224 | apply: (document) -> 225 | reference = @path.getReference(document) 226 | accessor = @path.accessor 227 | 228 | if isArray(reference) 229 | accessor = @path.coerce(reference, accessor) 230 | if accessor >= reference.length 231 | throw new PatchConflictError("Value at #{accessor} does not exist") 232 | reference.splice(accessor, 1) 233 | else 234 | if accessor not of reference 235 | throw new PatchConflictError("Value at #{accessor} does not exist") 236 | delete reference[accessor] 237 | return document 238 | 239 | 240 | class ReplacePatch extends JSONPatch 241 | validate: (patch) -> 242 | if 'value' not of patch then throw new InvalidPatchError() 243 | 244 | apply: (document) -> 245 | reference = @path.getReference(document) 246 | accessor = @path.accessor 247 | value = @patch.value 248 | 249 | # Replace whole document 250 | if not accessor? 251 | return value 252 | 253 | if isArray(reference) 254 | accessor = @path.coerce(reference, accessor) 255 | if accessor >= reference.length 256 | throw new PatchConflictError("Value at #{accessor} does not exist") 257 | reference.splice(accessor, 1, value) 258 | else 259 | if accessor not of reference 260 | throw new PatchConflictError("Value at #{accessor} does not exist") 261 | reference[accessor] = value 262 | 263 | return document 264 | 265 | 266 | class TestPatch extends JSONPatch 267 | validate: (patch) -> 268 | if 'value' not of patch 269 | throw new InvalidPatchError("'value' member is required") 270 | 271 | apply: (document) -> 272 | reference = @path.getReference(document) 273 | accessor = @path.accessor 274 | value = @patch.value 275 | 276 | if isArray(reference) 277 | accessor = @path.coerce(reference, accessor) 278 | 279 | if not isEqual(reference[accessor], value) 280 | throw new PatchTestFailed() 281 | 282 | return document 283 | 284 | 285 | class MovePatch extends JSONPatch 286 | initialize: (patch) -> 287 | @from = new JSONPointer(patch.from) 288 | len = @from.steps.length 289 | 290 | within = true 291 | for i in [0..len] 292 | if @from.steps[i] isnt @path.steps[i] 293 | within = false 294 | break 295 | 296 | if within 297 | if @path.steps.length isnt len 298 | throw new InvalidPatchError("'to' member cannot be a descendent of 'path'") 299 | if @from.accessor is @path.accessor 300 | # The path and to pointers reference the same location, 301 | # therefore apply can be a no-op 302 | @apply = (document) -> document 303 | 304 | validate: (patch) -> 305 | if 'from' not of patch 306 | throw new InvalidPatchError("'from' member is required") 307 | 308 | apply: (document) -> 309 | reference = @from.getReference(document) 310 | accessor = @from.accessor 311 | 312 | if isArray(reference) 313 | accessor = @from.coerce(reference, accessor) 314 | if accessor >= reference.length 315 | throw new PatchConflictError("Value at #{accessor} does not exist") 316 | value = reference.splice(accessor, 1)[0] 317 | else 318 | if accessor not of reference 319 | throw new PatchConflictError("Value at #{accessor} does not exist") 320 | value = reference[accessor] 321 | delete reference[accessor] 322 | 323 | reference = @path.getReference(document) 324 | accessor = @path.accessor 325 | 326 | # Add to object 327 | if isArray(reference) 328 | accessor = @path.coerce(reference, accessor) 329 | if accessor < 0 or accessor > reference.length 330 | throw new PatchConflictError("Index #{accessor} out of bounds") 331 | reference.splice(accessor, 0, value) 332 | else 333 | if accessor of reference 334 | throw new PatchConflictError("Value at #{accessor} exists") 335 | reference[accessor] = value 336 | return document 337 | 338 | 339 | class CopyPatch extends MovePatch 340 | apply: (document) -> 341 | reference = @from.getReference(document) 342 | accessor = @from.accessor 343 | if isArray(reference) 344 | accessor = @from.coerce(reference, accessor) 345 | if accessor >= reference.length 346 | throw new PatchConflictError("Value at #{accessor} does not exist") 347 | value = reference.slice(accessor, accessor + 1)[0] 348 | else 349 | if accessor not of reference 350 | throw new PatchConflictError("Value at #{accessor} does not exist") 351 | value = reference[accessor] 352 | 353 | reference = @path.getReference(document) 354 | accessor = @path.accessor 355 | 356 | # Add to object 357 | if isArray(reference) 358 | accessor = @path.coerce(reference, accessor) 359 | if accessor < 0 or accessor > reference.length 360 | throw new PatchConflictError("Index #{accessor} out of bounds") 361 | reference.splice(accessor, 0, value) 362 | else 363 | if accessor of reference 364 | throw new PatchConflictError("Value at #{accessor} exists") 365 | reference[accessor] = value 366 | return document 367 | 368 | 369 | # Map of operation classes 370 | operationMap = 371 | add: AddPatch 372 | remove: RemovePatch 373 | replace: ReplacePatch 374 | move: MovePatch 375 | copy: CopyPatch 376 | test: TestPatch 377 | 378 | 379 | # Validates and compiles a patch document and returns a function to apply 380 | # to multiple documents 381 | compile = (patch) -> 382 | if not isArray(patch) 383 | if isObject(patch) 384 | patch = [patch] 385 | else 386 | throw new InvalidPatchError('patch must be an object or array') 387 | 388 | ops = [] 389 | 390 | for p in patch 391 | # Not a valid operation 392 | if not (klass = operationMap[p.op]) 393 | throw new InvalidPatchError('invalid operation: ' + p.op) 394 | 395 | ops.push new klass(p) 396 | 397 | return (document) -> 398 | result = document 399 | 400 | for op in ops 401 | result = op.apply(result) 402 | 403 | return result 404 | 405 | 406 | # Applies a patch to a document 407 | apply = (document, patch) -> 408 | compile(patch)(document) 409 | 410 | 411 | # Export to exports 412 | exports.version = '0.7.0' 413 | exports.apply = apply 414 | exports.compile = compile 415 | exports.JSONPointer = JSONPointer 416 | exports.JSONPatch = JSONPatch 417 | exports.JSONPatchError = JSONPatchError 418 | exports.InvalidPointerError = InvalidPointerError 419 | exports.InvalidPatchError = InvalidPatchError 420 | exports.PatchConflictError = PatchConflictError 421 | exports.PatchTestFailed = PatchTestFailed 422 | 423 | return exports 424 | -------------------------------------------------------------------------------- /jsonpatch.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.7 2 | (function() { 3 | var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, 4 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 5 | hasProp = {}.hasOwnProperty; 6 | 7 | (function(factory) { 8 | var root; 9 | root = typeof window !== "undefined" && window !== null ? window : typeof global !== "undefined" && global !== null ? global : this; 10 | if (typeof exports !== 'undefined') { 11 | return factory(exports); 12 | } else if (typeof define === 'function' && define.amd) { 13 | return define(['exports'], function(exports) { 14 | return root.jsonpatch = factory(exports); 15 | }); 16 | } else { 17 | return root.jsonpatch = factory({}); 18 | } 19 | })(function(exports) { 20 | var AddPatch, CopyPatch, InvalidPatchError, InvalidPointerError, JSONPatch, JSONPatchError, JSONPointer, MovePatch, PatchConflictError, PatchTestFailed, RemovePatch, ReplacePatch, TestPatch, _isEqual, accessorMatch, apply, compile, escapedSlash, escapedTilde, hasOwnProperty, isArray, isEqual, isObject, isString, operationMap, toString; 21 | toString = Object.prototype.toString; 22 | hasOwnProperty = Object.prototype.hasOwnProperty; 23 | isArray = function(obj) { 24 | return toString.call(obj) === '[object Array]'; 25 | }; 26 | isObject = function(obj) { 27 | return toString.call(obj) === '[object Object]'; 28 | }; 29 | isString = function(obj) { 30 | return toString.call(obj) === '[object String]'; 31 | }; 32 | _isEqual = function(a, b, stack) { 33 | var className, key, length, result, size; 34 | if (a === b) { 35 | return a !== 0 || 1 / a === 1 / b; 36 | } 37 | if (a === null || b === null) { 38 | return a === b; 39 | } 40 | className = toString.call(a); 41 | if (className !== toString.call(b)) { 42 | return false; 43 | } 44 | switch (className) { 45 | case '[object String]': 46 | String(a) === String(b); 47 | break; 48 | case '[object Number]': 49 | a = +a; 50 | b = +b; 51 | if (a !== a) { 52 | b !== b; 53 | } else { 54 | if (a === 0) { 55 | 1 / a === 1 / b; 56 | } else { 57 | a === b; 58 | } 59 | } 60 | break; 61 | case '[object Boolean]': 62 | +a === +b; 63 | } 64 | if (typeof a !== 'object' || typeof b !== 'object') { 65 | return false; 66 | } 67 | length = stack.length; 68 | while (length--) { 69 | if (stack[length] === a) { 70 | return true; 71 | } 72 | } 73 | stack.push(a); 74 | size = 0; 75 | result = true; 76 | if (className === '[object Array]') { 77 | size = a.length; 78 | result = size === b.length; 79 | if (result) { 80 | while (size--) { 81 | if (!(result = indexOf.call(a, size) >= 0 === indexOf.call(b, size) >= 0 && _isEqual(a[size], b[size], stack))) { 82 | break; 83 | } 84 | } 85 | } 86 | } else { 87 | if (indexOf.call(a, "constructor") >= 0 !== indexOf.call(b, "constructor") >= 0 || a.constructor !== b.constructor) { 88 | return false; 89 | } 90 | for (key in a) { 91 | if (hasOwnProperty.call(a, key)) { 92 | size++; 93 | if (!(result = hasOwnProperty.call(b, key) && _isEqual(a[key], b[key], stack))) { 94 | break; 95 | } 96 | } 97 | } 98 | if (result) { 99 | for (key in b) { 100 | if (hasOwnProperty.call(b, key) && !size--) { 101 | break; 102 | } 103 | } 104 | result = !size; 105 | } 106 | } 107 | stack.pop(); 108 | return result; 109 | }; 110 | isEqual = function(a, b) { 111 | return _isEqual(a, b, []); 112 | }; 113 | JSONPatchError = (function(superClass) { 114 | extend(JSONPatchError, superClass); 115 | 116 | function JSONPatchError(message) { 117 | this.message = message != null ? message : 'JSON patch error'; 118 | this.name = 'JSONPatchError'; 119 | } 120 | 121 | return JSONPatchError; 122 | 123 | })(Error); 124 | InvalidPointerError = (function(superClass) { 125 | extend(InvalidPointerError, superClass); 126 | 127 | function InvalidPointerError(message) { 128 | this.message = message != null ? message : 'Invalid pointer'; 129 | this.name = 'InvalidPointer'; 130 | } 131 | 132 | return InvalidPointerError; 133 | 134 | })(Error); 135 | InvalidPatchError = (function(superClass) { 136 | extend(InvalidPatchError, superClass); 137 | 138 | function InvalidPatchError(message) { 139 | this.message = message != null ? message : 'Invalid patch'; 140 | this.name = 'InvalidPatch'; 141 | } 142 | 143 | return InvalidPatchError; 144 | 145 | })(JSONPatchError); 146 | PatchConflictError = (function(superClass) { 147 | extend(PatchConflictError, superClass); 148 | 149 | function PatchConflictError(message) { 150 | this.message = message != null ? message : 'Patch conflict'; 151 | this.name = 'PatchConflictError'; 152 | } 153 | 154 | return PatchConflictError; 155 | 156 | })(JSONPatchError); 157 | PatchTestFailed = (function(superClass) { 158 | extend(PatchTestFailed, superClass); 159 | 160 | function PatchTestFailed(message) { 161 | this.message = message != null ? message : 'Patch test failed'; 162 | this.name = 'PatchTestFailed'; 163 | } 164 | 165 | return PatchTestFailed; 166 | 167 | })(Error); 168 | escapedSlash = /~1/g; 169 | escapedTilde = /~0/g; 170 | accessorMatch = /^[-+]?\d+$/; 171 | JSONPointer = (function() { 172 | function JSONPointer(path) { 173 | var i, j, len1, step, steps; 174 | steps = []; 175 | if (path && (steps = path.split('/')).shift() !== '') { 176 | throw new InvalidPointerError(); 177 | } 178 | for (i = j = 0, len1 = steps.length; j < len1; i = ++j) { 179 | step = steps[i]; 180 | steps[i] = step.replace(escapedSlash, '/').replace(escapedTilde, '~'); 181 | } 182 | this.accessor = steps.pop(); 183 | this.steps = steps; 184 | this.path = path; 185 | } 186 | 187 | JSONPointer.prototype.getReference = function(parent) { 188 | var j, len1, ref, step; 189 | ref = this.steps; 190 | for (j = 0, len1 = ref.length; j < len1; j++) { 191 | step = ref[j]; 192 | if (isArray(parent)) { 193 | step = parseInt(step, 10); 194 | } 195 | if (!(step in parent)) { 196 | throw new PatchConflictError('Array location out of bounds or not an instance property'); 197 | } 198 | parent = parent[step]; 199 | } 200 | return parent; 201 | }; 202 | 203 | JSONPointer.prototype.coerce = function(reference, accessor) { 204 | if (isArray(reference)) { 205 | if (isString(accessor)) { 206 | if (accessor === '-') { 207 | accessor = reference.length; 208 | } else if (accessorMatch.test(accessor)) { 209 | accessor = parseInt(accessor, 10); 210 | } else { 211 | throw new InvalidPointerError('Invalid array index number'); 212 | } 213 | } 214 | } 215 | return accessor; 216 | }; 217 | 218 | return JSONPointer; 219 | 220 | })(); 221 | JSONPatch = (function() { 222 | function JSONPatch(patch) { 223 | if (!('path' in patch)) { 224 | throw new InvalidPatchError(); 225 | } 226 | this.validate(patch); 227 | this.patch = patch; 228 | this.path = new JSONPointer(patch.path); 229 | this.initialize(patch); 230 | } 231 | 232 | JSONPatch.prototype.initialize = function() {}; 233 | 234 | JSONPatch.prototype.validate = function(patch) {}; 235 | 236 | JSONPatch.prototype.apply = function(document) { 237 | throw new Error('Method not implemented'); 238 | }; 239 | 240 | return JSONPatch; 241 | 242 | })(); 243 | AddPatch = (function(superClass) { 244 | extend(AddPatch, superClass); 245 | 246 | function AddPatch() { 247 | return AddPatch.__super__.constructor.apply(this, arguments); 248 | } 249 | 250 | AddPatch.prototype.validate = function(patch) { 251 | if (!('value' in patch)) { 252 | throw new InvalidPatchError(); 253 | } 254 | }; 255 | 256 | AddPatch.prototype.apply = function(document) { 257 | var accessor, reference, value; 258 | reference = this.path.getReference(document); 259 | accessor = this.path.accessor; 260 | value = this.patch.value; 261 | if (isArray(reference)) { 262 | accessor = this.path.coerce(reference, accessor); 263 | if (accessor < 0 || accessor > reference.length) { 264 | throw new PatchConflictError("Index " + accessor + " out of bounds"); 265 | } 266 | reference.splice(accessor, 0, value); 267 | } else if (accessor == null) { 268 | document = value; 269 | } else { 270 | reference[accessor] = value; 271 | } 272 | return document; 273 | }; 274 | 275 | return AddPatch; 276 | 277 | })(JSONPatch); 278 | RemovePatch = (function(superClass) { 279 | extend(RemovePatch, superClass); 280 | 281 | function RemovePatch() { 282 | return RemovePatch.__super__.constructor.apply(this, arguments); 283 | } 284 | 285 | RemovePatch.prototype.apply = function(document) { 286 | var accessor, reference; 287 | reference = this.path.getReference(document); 288 | accessor = this.path.accessor; 289 | if (isArray(reference)) { 290 | accessor = this.path.coerce(reference, accessor); 291 | if (accessor >= reference.length) { 292 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 293 | } 294 | reference.splice(accessor, 1); 295 | } else { 296 | if (!(accessor in reference)) { 297 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 298 | } 299 | delete reference[accessor]; 300 | } 301 | return document; 302 | }; 303 | 304 | return RemovePatch; 305 | 306 | })(JSONPatch); 307 | ReplacePatch = (function(superClass) { 308 | extend(ReplacePatch, superClass); 309 | 310 | function ReplacePatch() { 311 | return ReplacePatch.__super__.constructor.apply(this, arguments); 312 | } 313 | 314 | ReplacePatch.prototype.validate = function(patch) { 315 | if (!('value' in patch)) { 316 | throw new InvalidPatchError(); 317 | } 318 | }; 319 | 320 | ReplacePatch.prototype.apply = function(document) { 321 | var accessor, reference, value; 322 | reference = this.path.getReference(document); 323 | accessor = this.path.accessor; 324 | value = this.patch.value; 325 | if (accessor == null) { 326 | return value; 327 | } 328 | if (isArray(reference)) { 329 | accessor = this.path.coerce(reference, accessor); 330 | if (accessor >= reference.length) { 331 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 332 | } 333 | reference.splice(accessor, 1, value); 334 | } else { 335 | if (!(accessor in reference)) { 336 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 337 | } 338 | reference[accessor] = value; 339 | } 340 | return document; 341 | }; 342 | 343 | return ReplacePatch; 344 | 345 | })(JSONPatch); 346 | TestPatch = (function(superClass) { 347 | extend(TestPatch, superClass); 348 | 349 | function TestPatch() { 350 | return TestPatch.__super__.constructor.apply(this, arguments); 351 | } 352 | 353 | TestPatch.prototype.validate = function(patch) { 354 | if (!('value' in patch)) { 355 | throw new InvalidPatchError("'value' member is required"); 356 | } 357 | }; 358 | 359 | TestPatch.prototype.apply = function(document) { 360 | var accessor, reference, value; 361 | reference = this.path.getReference(document); 362 | accessor = this.path.accessor; 363 | value = this.patch.value; 364 | if (isArray(reference)) { 365 | accessor = this.path.coerce(reference, accessor); 366 | } 367 | if (!isEqual(reference[accessor], value)) { 368 | throw new PatchTestFailed(); 369 | } 370 | return document; 371 | }; 372 | 373 | return TestPatch; 374 | 375 | })(JSONPatch); 376 | MovePatch = (function(superClass) { 377 | extend(MovePatch, superClass); 378 | 379 | function MovePatch() { 380 | return MovePatch.__super__.constructor.apply(this, arguments); 381 | } 382 | 383 | MovePatch.prototype.initialize = function(patch) { 384 | var i, j, len, ref, within; 385 | this.from = new JSONPointer(patch.from); 386 | len = this.from.steps.length; 387 | within = true; 388 | for (i = j = 0, ref = len; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { 389 | if (this.from.steps[i] !== this.path.steps[i]) { 390 | within = false; 391 | break; 392 | } 393 | } 394 | if (within) { 395 | if (this.path.steps.length !== len) { 396 | throw new InvalidPatchError("'to' member cannot be a descendent of 'path'"); 397 | } 398 | if (this.from.accessor === this.path.accessor) { 399 | return this.apply = function(document) { 400 | return document; 401 | }; 402 | } 403 | } 404 | }; 405 | 406 | MovePatch.prototype.validate = function(patch) { 407 | if (!('from' in patch)) { 408 | throw new InvalidPatchError("'from' member is required"); 409 | } 410 | }; 411 | 412 | MovePatch.prototype.apply = function(document) { 413 | var accessor, reference, value; 414 | reference = this.from.getReference(document); 415 | accessor = this.from.accessor; 416 | if (isArray(reference)) { 417 | accessor = this.from.coerce(reference, accessor); 418 | if (accessor >= reference.length) { 419 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 420 | } 421 | value = reference.splice(accessor, 1)[0]; 422 | } else { 423 | if (!(accessor in reference)) { 424 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 425 | } 426 | value = reference[accessor]; 427 | delete reference[accessor]; 428 | } 429 | reference = this.path.getReference(document); 430 | accessor = this.path.accessor; 431 | if (isArray(reference)) { 432 | accessor = this.path.coerce(reference, accessor); 433 | if (accessor < 0 || accessor > reference.length) { 434 | throw new PatchConflictError("Index " + accessor + " out of bounds"); 435 | } 436 | reference.splice(accessor, 0, value); 437 | } else { 438 | if (accessor in reference) { 439 | throw new PatchConflictError("Value at " + accessor + " exists"); 440 | } 441 | reference[accessor] = value; 442 | } 443 | return document; 444 | }; 445 | 446 | return MovePatch; 447 | 448 | })(JSONPatch); 449 | CopyPatch = (function(superClass) { 450 | extend(CopyPatch, superClass); 451 | 452 | function CopyPatch() { 453 | return CopyPatch.__super__.constructor.apply(this, arguments); 454 | } 455 | 456 | CopyPatch.prototype.apply = function(document) { 457 | var accessor, reference, value; 458 | reference = this.from.getReference(document); 459 | accessor = this.from.accessor; 460 | if (isArray(reference)) { 461 | accessor = this.from.coerce(reference, accessor); 462 | if (accessor >= reference.length) { 463 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 464 | } 465 | value = reference.slice(accessor, accessor + 1)[0]; 466 | } else { 467 | if (!(accessor in reference)) { 468 | throw new PatchConflictError("Value at " + accessor + " does not exist"); 469 | } 470 | value = reference[accessor]; 471 | } 472 | reference = this.path.getReference(document); 473 | accessor = this.path.accessor; 474 | if (isArray(reference)) { 475 | accessor = this.path.coerce(reference, accessor); 476 | if (accessor < 0 || accessor > reference.length) { 477 | throw new PatchConflictError("Index " + accessor + " out of bounds"); 478 | } 479 | reference.splice(accessor, 0, value); 480 | } else { 481 | if (accessor in reference) { 482 | throw new PatchConflictError("Value at " + accessor + " exists"); 483 | } 484 | reference[accessor] = value; 485 | } 486 | return document; 487 | }; 488 | 489 | return CopyPatch; 490 | 491 | })(MovePatch); 492 | operationMap = { 493 | add: AddPatch, 494 | remove: RemovePatch, 495 | replace: ReplacePatch, 496 | move: MovePatch, 497 | copy: CopyPatch, 498 | test: TestPatch 499 | }; 500 | compile = function(patch) { 501 | var j, klass, len1, ops, p; 502 | if (!isArray(patch)) { 503 | if (isObject(patch)) { 504 | patch = [patch]; 505 | } else { 506 | throw new InvalidPatchError('patch must be an object or array'); 507 | } 508 | } 509 | ops = []; 510 | for (j = 0, len1 = patch.length; j < len1; j++) { 511 | p = patch[j]; 512 | if (!(klass = operationMap[p.op])) { 513 | throw new InvalidPatchError('invalid operation: ' + p.op); 514 | } 515 | ops.push(new klass(p)); 516 | } 517 | return function(document) { 518 | var k, len2, op, result; 519 | result = document; 520 | for (k = 0, len2 = ops.length; k < len2; k++) { 521 | op = ops[k]; 522 | result = op.apply(result); 523 | } 524 | return result; 525 | }; 526 | }; 527 | apply = function(document, patch) { 528 | return compile(patch)(document); 529 | }; 530 | exports.version = '0.7.0'; 531 | exports.apply = apply; 532 | exports.compile = compile; 533 | exports.JSONPointer = JSONPointer; 534 | exports.JSONPatch = JSONPatch; 535 | exports.JSONPatchError = JSONPatchError; 536 | exports.InvalidPointerError = InvalidPointerError; 537 | exports.InvalidPatchError = InvalidPatchError; 538 | exports.PatchConflictError = PatchConflictError; 539 | exports.PatchTestFailed = PatchTestFailed; 540 | return exports; 541 | }); 542 | 543 | }).call(this); 544 | -------------------------------------------------------------------------------- /jslitmus.js: -------------------------------------------------------------------------------- 1 | // JSLitmus.js 2 | // 3 | // Copyright (c) 2010, Robert Kieffer, http://broofa.com 4 | // Available under MIT license (http://en.wikipedia.org/wiki/MIT_License) 5 | 6 | (function() { 7 | // Private methods and state 8 | 9 | // Get platform info but don't go crazy trying to recognize everything 10 | // that's out there. This is just for the major platforms and OSes. 11 | var platform = 'unknown platform', ua = navigator.userAgent; 12 | 13 | // Detect OS 14 | var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|'); 15 | var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null; 16 | if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null; 17 | 18 | // Detect browser 19 | var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null; 20 | 21 | // Detect version 22 | var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)'); 23 | var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null; 24 | var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform'; 25 | 26 | /** 27 | * A smattering of methods that are needed to implement the JSLitmus testbed. 28 | */ 29 | var jsl = { 30 | /** 31 | * Enhanced version of escape() 32 | */ 33 | escape: function(s) { 34 | s = s.replace(/,/g, '\\,'); 35 | s = escape(s); 36 | s = s.replace(/\+/g, '%2b'); 37 | s = s.replace(/ /g, '+'); 38 | return s; 39 | }, 40 | 41 | /** 42 | * Get an element by ID. 43 | */ 44 | $: function(id) { 45 | return document.getElementById(id); 46 | }, 47 | 48 | /** 49 | * Null function 50 | */ 51 | F: function() {}, 52 | 53 | /** 54 | * Set the status shown in the UI 55 | */ 56 | status: function(msg) { 57 | var el = jsl.$('jsl_status'); 58 | if (el) el.innerHTML = msg || ''; 59 | }, 60 | 61 | /** 62 | * Convert a number to an abbreviated string like, "15K" or "10M" 63 | */ 64 | toLabel: function(n) { 65 | if (n == Infinity) { 66 | return 'Infinity'; 67 | } else if (n > 1e9) { 68 | n = Math.round(n/1e8); 69 | return n/10 + 'B'; 70 | } else if (n > 1e6) { 71 | n = Math.round(n/1e5); 72 | return n/10 + 'M'; 73 | } else if (n > 1e3) { 74 | n = Math.round(n/1e2); 75 | return n/10 + 'K'; 76 | } 77 | return n; 78 | }, 79 | 80 | /** 81 | * Copy properties from src to dst 82 | */ 83 | extend: function(dst, src) { 84 | for (var k in src) dst[k] = src[k]; return dst; 85 | }, 86 | 87 | /** 88 | * Like Array.join(), but for the key-value pairs in an object 89 | */ 90 | join: function(o, delimit1, delimit2) { 91 | if (o.join) return o.join(delimit1); // If it's an array 92 | var pairs = []; 93 | for (var k in o) pairs.push(k + delimit1 + o[k]); 94 | return pairs.join(delimit2); 95 | }, 96 | 97 | /** 98 | * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution 99 | */ 100 | indexOf: function(arr, o) { 101 | if (arr.indexOf) return arr.indexOf(o); 102 | for (var i = 0; i < this.length; i++) if (arr[i] === o) return i; 103 | return -1; 104 | } 105 | }; 106 | 107 | /** 108 | * Test manages a single test (created with 109 | * JSLitmus.test()) 110 | * 111 | * @private 112 | */ 113 | var Test = function (name, f) { 114 | if (!f) throw new Error('Undefined test function'); 115 | if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) { 116 | throw new Error('"' + name + '" test: Test is not a valid Function object'); 117 | } 118 | this.loopArg = RegExp.$1; 119 | this.name = name; 120 | this.f = f; 121 | }; 122 | 123 | jsl.extend(Test, /** @lends Test */ { 124 | /** Calibration tests for establishing iteration loop overhead */ 125 | CALIBRATIONS: [ 126 | new Test('calibrating loop', function(count) {while (count--);}), 127 | new Test('calibrating function', jsl.F) 128 | ], 129 | 130 | /** 131 | * Run calibration tests. Returns true if calibrations are not yet 132 | * complete (in which case calling code should run the tests yet again). 133 | * onCalibrated - Callback to invoke when calibrations have finished 134 | */ 135 | calibrate: function(onCalibrated) { 136 | for (var i = 0; i < Test.CALIBRATIONS.length; i++) { 137 | var cal = Test.CALIBRATIONS[i]; 138 | if (cal.running) return true; 139 | if (!cal.count) { 140 | cal.isCalibration = true; 141 | cal.onStop = onCalibrated; 142 | //cal.MIN_TIME = .1; // Do calibrations quickly 143 | cal.run(2e4); 144 | return true; 145 | } 146 | } 147 | return false; 148 | } 149 | }); 150 | 151 | jsl.extend(Test.prototype, {/** @lends Test.prototype */ 152 | /** Initial number of iterations */ 153 | INIT_COUNT: 10, 154 | /** Max iterations allowed (i.e. used to detect bad looping functions) */ 155 | MAX_COUNT: 1e9, 156 | /** Minimum time a test should take to get valid results (secs) */ 157 | MIN_TIME: .5, 158 | 159 | /** Callback invoked when test state changes */ 160 | onChange: jsl.F, 161 | 162 | /** Callback invoked when test is finished */ 163 | onStop: jsl.F, 164 | 165 | /** 166 | * Reset test state 167 | */ 168 | reset: function() { 169 | delete this.count; 170 | delete this.time; 171 | delete this.running; 172 | delete this.error; 173 | }, 174 | 175 | /** 176 | * Run the test (in a timeout). We use a timeout to make sure the browser 177 | * has a chance to finish rendering any UI changes we've made, like 178 | * updating the status message. 179 | */ 180 | run: function(count) { 181 | count = count || this.INIT_COUNT; 182 | jsl.status(this.name + ' x ' + count); 183 | this.running = true; 184 | var me = this; 185 | setTimeout(function() {me._run(count);}, 200); 186 | }, 187 | 188 | /** 189 | * The nuts and bolts code that actually runs a test 190 | */ 191 | _run: function(count) { 192 | var me = this; 193 | 194 | // Make sure calibration tests have run 195 | if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return; 196 | this.error = null; 197 | 198 | try { 199 | var start, f = this.f, now, i = count; 200 | 201 | // Start the timer 202 | start = new Date(); 203 | 204 | // Now for the money shot. If this is a looping function ... 205 | if (this.loopArg) { 206 | // ... let it do the iteration itself 207 | f(count); 208 | } else { 209 | // ... otherwise do the iteration for it 210 | while (i--) f(); 211 | } 212 | 213 | // Get time test took (in secs) 214 | this.time = Math.max(1,new Date() - start)/1000; 215 | 216 | // Store iteration count and per-operation time taken 217 | this.count = count; 218 | this.period = this.time/count; 219 | 220 | // Do we need to do another run? 221 | this.running = this.time <= this.MIN_TIME; 222 | 223 | // ... if so, compute how many times we should iterate 224 | if (this.running) { 225 | // Bump the count to the nearest power of 2 226 | var x = this.MIN_TIME/this.time; 227 | var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2)))); 228 | count *= pow; 229 | if (count > this.MAX_COUNT) { 230 | throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.'); 231 | } 232 | } 233 | } catch (e) { 234 | // Exceptions are caught and displayed in the test UI 235 | this.reset(); 236 | this.error = e; 237 | } 238 | 239 | // Figure out what to do next 240 | if (this.running) { 241 | me.run(count); 242 | } else { 243 | jsl.status(''); 244 | me.onStop(me); 245 | } 246 | 247 | // Finish up 248 | this.onChange(this); 249 | }, 250 | 251 | /** 252 | * Get the number of operations per second for this test. 253 | * 254 | * @param normalize if true, iteration loop overhead taken into account 255 | */ 256 | getHz: function(/**Boolean*/ normalize) { 257 | var p = this.period; 258 | 259 | // Adjust period based on the calibration test time 260 | if (normalize && !this.isCalibration) { 261 | var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1]; 262 | 263 | // If the period is within 20% of the calibration time, then zero the 264 | // it out 265 | p = p < cal.period*1.2 ? 0 : p - cal.period; 266 | } 267 | 268 | return Math.round(1/p); 269 | }, 270 | 271 | /** 272 | * Get a friendly string describing the test 273 | */ 274 | toString: function() { 275 | return this.name + ' - ' + this.time/this.count + ' secs'; 276 | } 277 | }); 278 | 279 | // CSS we need for the UI 280 | var STYLESHEET = ''; 360 | 361 | // HTML markup for the UI 362 | var MARKUP = '
    \ 363 | \ 364 | \ 365 |
    \ 366 |
    \ 367 | Normalize results \ 368 | \ 369 | \ 370 | \ 371 | \ 372 | \ 373 | \ 374 | \ 375 | \ 376 | \ 377 | \ 378 | \ 379 |
    ' + platform + '
    TestOps/sec
    \ 380 |
    \ 381 | \ 386 | Powered by JSLitmus \ 387 |
    '; 388 | 389 | /** 390 | * The public API for creating and running tests 391 | */ 392 | window.JSLitmus = { 393 | /** The list of all tests that have been registered with JSLitmus.test */ 394 | _tests: [], 395 | /** The queue of tests that need to be run */ 396 | _queue: [], 397 | 398 | /** 399 | * The parsed query parameters the current page URL. This is provided as a 400 | * convenience for test functions - it's not used by JSLitmus proper 401 | */ 402 | params: {}, 403 | 404 | /** 405 | * Initialize 406 | */ 407 | _init: function() { 408 | // Parse query params into JSLitmus.params[] hash 409 | var match = (location + '').match(/([^?#]*)(#.*)?$/); 410 | if (match) { 411 | var pairs = match[1].split('&'); 412 | for (var i = 0; i < pairs.length; i++) { 413 | var pair = pairs[i].split('='); 414 | if (pair.length > 1) { 415 | var key = pair.shift(); 416 | var value = pair.length > 1 ? pair.join('=') : pair[0]; 417 | this.params[key] = value; 418 | } 419 | } 420 | } 421 | 422 | // Write out the stylesheet. We have to do this here because IE 423 | // doesn't honor sheets written after the document has loaded. 424 | document.write(STYLESHEET); 425 | 426 | // Setup the rest of the UI once the document is loaded 427 | if (window.addEventListener) { 428 | window.addEventListener('load', this._setup, false); 429 | } else if (document.addEventListener) { 430 | document.addEventListener('load', this._setup, false); 431 | } else if (window.attachEvent) { 432 | window.attachEvent('onload', this._setup); 433 | } 434 | 435 | return this; 436 | }, 437 | 438 | /** 439 | * Set up the UI 440 | */ 441 | _setup: function() { 442 | var el = jsl.$('jslitmus_container'); 443 | if (!el) document.body.appendChild(el = document.createElement('div')); 444 | 445 | el.innerHTML = MARKUP; 446 | 447 | // Render the UI for all our tests 448 | for (var i=0; i < JSLitmus._tests.length; i++) 449 | JSLitmus.renderTest(JSLitmus._tests[i]); 450 | }, 451 | 452 | /** 453 | * (Re)render all the test results 454 | */ 455 | renderAll: function() { 456 | for (var i = 0; i < JSLitmus._tests.length; i++) 457 | JSLitmus.renderTest(JSLitmus._tests[i]); 458 | JSLitmus.renderChart(); 459 | }, 460 | 461 | /** 462 | * (Re)render the chart graphics 463 | */ 464 | renderChart: function() { 465 | var url = JSLitmus.chartUrl(); 466 | jsl.$('chart_link').href = url; 467 | jsl.$('chart_image').src = url; 468 | jsl.$('chart').style.display = ''; 469 | 470 | // Update the tiny URL 471 | jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url); 472 | }, 473 | 474 | /** 475 | * (Re)render the results for a specific test 476 | */ 477 | renderTest: function(test) { 478 | // Make a new row if needed 479 | if (!test._row) { 480 | var trow = jsl.$('test_row_template'); 481 | if (!trow) return; 482 | 483 | test._row = trow.cloneNode(true); 484 | test._row.style.display = ''; 485 | test._row.id = ''; 486 | test._row.onclick = function() {JSLitmus._queueTest(test);}; 487 | test._row.title = 'Run ' + test.name + ' test'; 488 | trow.parentNode.appendChild(test._row); 489 | test._row.cells[0].innerHTML = test.name; 490 | } 491 | 492 | var cell = test._row.cells[1]; 493 | var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping']; 494 | 495 | if (test.error) { 496 | cns.push('test_error'); 497 | cell.innerHTML = 498 | '
    ' + test.error + '
    ' + 499 | '
    • ' + 500 | jsl.join(test.error, ': ', '
    • ') + 501 | '
    '; 502 | } else { 503 | if (test.running) { 504 | cns.push('test_running'); 505 | cell.innerHTML = 'running'; 506 | } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) { 507 | cns.push('test_pending'); 508 | cell.innerHTML = 'pending'; 509 | } else if (test.count) { 510 | cns.push('test_done'); 511 | var hz = test.getHz(jsl.$('test_normalize').checked); 512 | cell.innerHTML = hz != Infinity ? hz : '∞'; 513 | cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds'; 514 | } else { 515 | cell.innerHTML = 'ready'; 516 | } 517 | } 518 | cell.className = cns.join(' '); 519 | }, 520 | 521 | /** 522 | * Create a new test 523 | */ 524 | test: function(name, f) { 525 | // Create the Test object 526 | var test = new Test(name, f); 527 | JSLitmus._tests.push(test); 528 | 529 | // Re-render if the test state changes 530 | test.onChange = JSLitmus.renderTest; 531 | 532 | // Run the next test if this one finished 533 | test.onStop = function(test) { 534 | if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test); 535 | JSLitmus.currentTest = null; 536 | JSLitmus._nextTest(); 537 | }; 538 | 539 | // Render the new test 540 | this.renderTest(test); 541 | }, 542 | 543 | /** 544 | * Add all tests to the run queue 545 | */ 546 | runAll: function(e) { 547 | e = e || window.event; 548 | var reverse = e && e.shiftKey, len = JSLitmus._tests.length; 549 | for (var i = 0; i < len; i++) { 550 | JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]); 551 | } 552 | }, 553 | 554 | /** 555 | * Remove all tests from the run queue. The current test has to finish on 556 | * it's own though 557 | */ 558 | stop: function() { 559 | while (JSLitmus._queue.length) { 560 | var test = JSLitmus._queue.shift(); 561 | JSLitmus.renderTest(test); 562 | } 563 | }, 564 | 565 | /** 566 | * Run the next test in the run queue 567 | */ 568 | _nextTest: function() { 569 | if (!JSLitmus.currentTest) { 570 | var test = JSLitmus._queue.shift(); 571 | if (test) { 572 | jsl.$('stop_button').disabled = false; 573 | JSLitmus.currentTest = test; 574 | test.run(); 575 | JSLitmus.renderTest(test); 576 | if (JSLitmus.onTestStart) JSLitmus.onTestStart(test); 577 | } else { 578 | jsl.$('stop_button').disabled = true; 579 | JSLitmus.renderChart(); 580 | } 581 | } 582 | }, 583 | 584 | /** 585 | * Add a test to the run queue 586 | */ 587 | _queueTest: function(test) { 588 | if (jsl.indexOf(JSLitmus._queue, test) >= 0) return; 589 | JSLitmus._queue.push(test); 590 | JSLitmus.renderTest(test); 591 | JSLitmus._nextTest(); 592 | }, 593 | 594 | /** 595 | * Generate a Google Chart URL that shows the data for all tests 596 | */ 597 | chartUrl: function() { 598 | var n = JSLitmus._tests.length, markers = [], data = []; 599 | var d, min = 0, max = -1e10; 600 | var normalize = jsl.$('test_normalize').checked; 601 | 602 | // Gather test data 603 | for (var i=0; i < JSLitmus._tests.length; i++) { 604 | var test = JSLitmus._tests[i]; 605 | if (test.count) { 606 | var hz = test.getHz(normalize); 607 | var v = hz != Infinity ? hz : 0; 608 | data.push(v); 609 | markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' + 610 | markers.length + ',10'); 611 | max = Math.max(v, max); 612 | } 613 | } 614 | if (markers.length <= 0) return null; 615 | 616 | // Build chart title 617 | var title = document.getElementsByTagName('title'); 618 | title = (title && title.length) ? title[0].innerHTML : null; 619 | var chart_title = []; 620 | if (title) chart_title.push(title); 621 | chart_title.push('Ops/sec (' + platform + ')'); 622 | 623 | // Build labels 624 | var labels = [jsl.toLabel(min), jsl.toLabel(max)]; 625 | 626 | var w = 250, bw = 15; 627 | var bs = 5; 628 | var h = markers.length*(bw + bs) + 30 + chart_title.length*20; 629 | 630 | var params = { 631 | chtt: escape(chart_title.join('|')), 632 | chts: '000000,10', 633 | cht: 'bhg', // chart type 634 | chd: 't:' + data.join(','), // data set 635 | chds: min + ',' + max, // max/min of data 636 | chxt: 'x', // label axes 637 | chxl: '0:|' + labels.join('|'), // labels 638 | chsp: '0,1', 639 | chm: markers.join('|'), // test names 640 | chbh: [bw, 0, bs].join(','), // bar widths 641 | // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient 642 | chs: w + 'x' + h 643 | }; 644 | return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&'); 645 | } 646 | }; 647 | 648 | JSLitmus._init(); 649 | })(); 650 | -------------------------------------------------------------------------------- /qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | config, 15 | onErrorFnPrev, 16 | testId = 0, 17 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 18 | toString = Object.prototype.toString, 19 | hasOwn = Object.prototype.hasOwnProperty, 20 | // Keep a local reference to Date (GH-283) 21 | Date = window.Date, 22 | defined = { 23 | setTimeout: typeof window.setTimeout !== "undefined", 24 | sessionStorage: (function() { 25 | var x = "qunit-test-string"; 26 | try { 27 | sessionStorage.setItem( x, x ); 28 | sessionStorage.removeItem( x ); 29 | return true; 30 | } catch( e ) { 31 | return false; 32 | } 33 | }()) 34 | }; 35 | 36 | function Test( settings ) { 37 | extend( this, settings ); 38 | this.assertions = []; 39 | this.testNumber = ++Test.count; 40 | } 41 | 42 | Test.count = 0; 43 | 44 | Test.prototype = { 45 | init: function() { 46 | var a, b, li, 47 | tests = id( "qunit-tests" ); 48 | 49 | if ( tests ) { 50 | b = document.createElement( "strong" ); 51 | b.innerHTML = this.name; 52 | 53 | // `a` initialized at top of scope 54 | a = document.createElement( "a" ); 55 | a.innerHTML = "Rerun"; 56 | a.href = QUnit.url({ testNumber: this.testNumber }); 57 | 58 | li = document.createElement( "li" ); 59 | li.appendChild( b ); 60 | li.appendChild( a ); 61 | li.className = "running"; 62 | li.id = this.id = "qunit-test-output" + testId++; 63 | 64 | tests.appendChild( li ); 65 | } 66 | }, 67 | setup: function() { 68 | if ( this.module !== config.previousModule ) { 69 | if ( config.previousModule ) { 70 | runLoggingCallbacks( "moduleDone", QUnit, { 71 | name: config.previousModule, 72 | failed: config.moduleStats.bad, 73 | passed: config.moduleStats.all - config.moduleStats.bad, 74 | total: config.moduleStats.all 75 | }); 76 | } 77 | config.previousModule = this.module; 78 | config.moduleStats = { all: 0, bad: 0 }; 79 | runLoggingCallbacks( "moduleStart", QUnit, { 80 | name: this.module 81 | }); 82 | } else if ( config.autorun ) { 83 | runLoggingCallbacks( "moduleStart", QUnit, { 84 | name: this.module 85 | }); 86 | } 87 | 88 | config.current = this; 89 | 90 | this.testEnvironment = extend({ 91 | setup: function() {}, 92 | teardown: function() {} 93 | }, this.moduleTestEnvironment ); 94 | 95 | runLoggingCallbacks( "testStart", QUnit, { 96 | name: this.testName, 97 | module: this.module 98 | }); 99 | 100 | // allow utility functions to access the current test environment 101 | // TODO why?? 102 | QUnit.current_testEnvironment = this.testEnvironment; 103 | 104 | if ( !config.pollution ) { 105 | saveGlobal(); 106 | } 107 | if ( config.notrycatch ) { 108 | this.testEnvironment.setup.call( this.testEnvironment ); 109 | return; 110 | } 111 | try { 112 | this.testEnvironment.setup.call( this.testEnvironment ); 113 | } catch( e ) { 114 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | } 116 | }, 117 | run: function() { 118 | config.current = this; 119 | 120 | var running = id( "qunit-testresult" ); 121 | 122 | if ( running ) { 123 | running.innerHTML = "Running:
    " + this.name; 124 | } 125 | 126 | if ( this.async ) { 127 | QUnit.stop(); 128 | } 129 | 130 | if ( config.notrycatch ) { 131 | this.callback.call( this.testEnvironment, QUnit.assert ); 132 | return; 133 | } 134 | 135 | try { 136 | this.callback.call( this.testEnvironment, QUnit.assert ); 137 | } catch( e ) { 138 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) ); 139 | // else next test will carry the responsibility 140 | saveGlobal(); 141 | 142 | // Restart the tests if they're blocking 143 | if ( config.blocking ) { 144 | QUnit.start(); 145 | } 146 | } 147 | }, 148 | teardown: function() { 149 | config.current = this; 150 | if ( config.notrycatch ) { 151 | this.testEnvironment.teardown.call( this.testEnvironment ); 152 | return; 153 | } else { 154 | try { 155 | this.testEnvironment.teardown.call( this.testEnvironment ); 156 | } catch( e ) { 157 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 158 | } 159 | } 160 | checkPollution(); 161 | }, 162 | finish: function() { 163 | config.current = this; 164 | if ( config.requireExpects && this.expected == null ) { 165 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 166 | } else if ( this.expected != null && this.expected != this.assertions.length ) { 167 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 168 | } else if ( this.expected == null && !this.assertions.length ) { 169 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 170 | } 171 | 172 | var assertion, a, b, i, li, ol, 173 | test = this, 174 | good = 0, 175 | bad = 0, 176 | tests = id( "qunit-tests" ); 177 | 178 | config.stats.all += this.assertions.length; 179 | config.moduleStats.all += this.assertions.length; 180 | 181 | if ( tests ) { 182 | ol = document.createElement( "ol" ); 183 | 184 | for ( i = 0; i < this.assertions.length; i++ ) { 185 | assertion = this.assertions[i]; 186 | 187 | li = document.createElement( "li" ); 188 | li.className = assertion.result ? "pass" : "fail"; 189 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 190 | ol.appendChild( li ); 191 | 192 | if ( assertion.result ) { 193 | good++; 194 | } else { 195 | bad++; 196 | config.stats.bad++; 197 | config.moduleStats.bad++; 198 | } 199 | } 200 | 201 | // store result when possible 202 | if ( QUnit.config.reorder && defined.sessionStorage ) { 203 | if ( bad ) { 204 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 205 | } else { 206 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 207 | } 208 | } 209 | 210 | if ( bad === 0 ) { 211 | ol.style.display = "none"; 212 | } 213 | 214 | // `b` initialized at top of scope 215 | b = document.createElement( "strong" ); 216 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 217 | 218 | addEvent(b, "click", function() { 219 | var next = b.nextSibling.nextSibling, 220 | display = next.style.display; 221 | next.style.display = display === "none" ? "block" : "none"; 222 | }); 223 | 224 | addEvent(b, "dblclick", function( e ) { 225 | var target = e && e.target ? e.target : window.event.srcElement; 226 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 227 | target = target.parentNode; 228 | } 229 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 230 | window.location = QUnit.url({ testNumber: test.testNumber }); 231 | } 232 | }); 233 | 234 | // `li` initialized at top of scope 235 | li = id( this.id ); 236 | li.className = bad ? "fail" : "pass"; 237 | li.removeChild( li.firstChild ); 238 | a = li.firstChild; 239 | li.appendChild( b ); 240 | li.appendChild ( a ); 241 | li.appendChild( ol ); 242 | 243 | } else { 244 | for ( i = 0; i < this.assertions.length; i++ ) { 245 | if ( !this.assertions[i].result ) { 246 | bad++; 247 | config.stats.bad++; 248 | config.moduleStats.bad++; 249 | } 250 | } 251 | } 252 | 253 | runLoggingCallbacks( "testDone", QUnit, { 254 | name: this.testName, 255 | module: this.module, 256 | failed: bad, 257 | passed: this.assertions.length - bad, 258 | total: this.assertions.length 259 | }); 260 | 261 | QUnit.reset(); 262 | 263 | config.current = undefined; 264 | }, 265 | 266 | queue: function() { 267 | var bad, 268 | test = this; 269 | 270 | synchronize(function() { 271 | test.init(); 272 | }); 273 | function run() { 274 | // each of these can by async 275 | synchronize(function() { 276 | test.setup(); 277 | }); 278 | synchronize(function() { 279 | test.run(); 280 | }); 281 | synchronize(function() { 282 | test.teardown(); 283 | }); 284 | synchronize(function() { 285 | test.finish(); 286 | }); 287 | } 288 | 289 | // `bad` initialized at top of scope 290 | // defer when previous test run passed, if storage is available 291 | bad = QUnit.config.reorder && defined.sessionStorage && 292 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 293 | 294 | if ( bad ) { 295 | run(); 296 | } else { 297 | synchronize( run, true ); 298 | } 299 | } 300 | }; 301 | 302 | // Root QUnit object. 303 | // `QUnit` initialized at top of scope 304 | QUnit = { 305 | 306 | // call on start of module test to prepend name to all tests 307 | module: function( name, testEnvironment ) { 308 | config.currentModule = name; 309 | config.currentModuleTestEnvironment = testEnvironment; 310 | config.modules[name] = true; 311 | }, 312 | 313 | asyncTest: function( testName, expected, callback ) { 314 | if ( arguments.length === 2 ) { 315 | callback = expected; 316 | expected = null; 317 | } 318 | 319 | QUnit.test( testName, expected, callback, true ); 320 | }, 321 | 322 | test: function( testName, expected, callback, async ) { 323 | var test, 324 | name = "" + escapeInnerText( testName ) + ""; 325 | 326 | if ( arguments.length === 2 ) { 327 | callback = expected; 328 | expected = null; 329 | } 330 | 331 | if ( config.currentModule ) { 332 | name = "" + config.currentModule + ": " + name; 333 | } 334 | 335 | test = new Test({ 336 | name: name, 337 | testName: testName, 338 | expected: expected, 339 | async: async, 340 | callback: callback, 341 | module: config.currentModule, 342 | moduleTestEnvironment: config.currentModuleTestEnvironment, 343 | stack: sourceFromStacktrace( 2 ) 344 | }); 345 | 346 | if ( !validTest( test ) ) { 347 | return; 348 | } 349 | 350 | test.queue(); 351 | }, 352 | 353 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 354 | expect: function( asserts ) { 355 | if (arguments.length === 1) { 356 | config.current.expected = asserts; 357 | } else { 358 | return config.current.expected; 359 | } 360 | }, 361 | 362 | start: function( count ) { 363 | config.semaphore -= count || 1; 364 | // don't start until equal number of stop-calls 365 | if ( config.semaphore > 0 ) { 366 | return; 367 | } 368 | // ignore if start is called more often then stop 369 | if ( config.semaphore < 0 ) { 370 | config.semaphore = 0; 371 | } 372 | // A slight delay, to avoid any current callbacks 373 | if ( defined.setTimeout ) { 374 | window.setTimeout(function() { 375 | if ( config.semaphore > 0 ) { 376 | return; 377 | } 378 | if ( config.timeout ) { 379 | clearTimeout( config.timeout ); 380 | } 381 | 382 | config.blocking = false; 383 | process( true ); 384 | }, 13); 385 | } else { 386 | config.blocking = false; 387 | process( true ); 388 | } 389 | }, 390 | 391 | stop: function( count ) { 392 | config.semaphore += count || 1; 393 | config.blocking = true; 394 | 395 | if ( config.testTimeout && defined.setTimeout ) { 396 | clearTimeout( config.timeout ); 397 | config.timeout = window.setTimeout(function() { 398 | QUnit.ok( false, "Test timed out" ); 399 | config.semaphore = 1; 400 | QUnit.start(); 401 | }, config.testTimeout ); 402 | } 403 | } 404 | }; 405 | 406 | // Asssert helpers 407 | // All of these must call either QUnit.push() or manually do: 408 | // - runLoggingCallbacks( "log", .. ); 409 | // - config.current.assertions.push({ .. }); 410 | QUnit.assert = { 411 | /** 412 | * Asserts rough true-ish result. 413 | * @name ok 414 | * @function 415 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 416 | */ 417 | ok: function( result, msg ) { 418 | if ( !config.current ) { 419 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 420 | } 421 | result = !!result; 422 | 423 | var source, 424 | details = { 425 | module: config.current.module, 426 | name: config.current.testName, 427 | result: result, 428 | message: msg 429 | }; 430 | 431 | msg = escapeInnerText( msg || (result ? "okay" : "failed" ) ); 432 | msg = "" + msg + ""; 433 | 434 | if ( !result ) { 435 | source = sourceFromStacktrace( 2 ); 436 | if ( source ) { 437 | details.source = source; 438 | msg += "
    Source:
    " + escapeInnerText( source ) + "
    "; 439 | } 440 | } 441 | runLoggingCallbacks( "log", QUnit, details ); 442 | config.current.assertions.push({ 443 | result: result, 444 | message: msg 445 | }); 446 | }, 447 | 448 | /** 449 | * Assert that the first two arguments are equal, with an optional message. 450 | * Prints out both actual and expected values. 451 | * @name equal 452 | * @function 453 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 454 | */ 455 | equal: function( actual, expected, message ) { 456 | QUnit.push( expected == actual, actual, expected, message ); 457 | }, 458 | 459 | /** 460 | * @name notEqual 461 | * @function 462 | */ 463 | notEqual: function( actual, expected, message ) { 464 | QUnit.push( expected != actual, actual, expected, message ); 465 | }, 466 | 467 | /** 468 | * @name deepEqual 469 | * @function 470 | */ 471 | deepEqual: function( actual, expected, message ) { 472 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 473 | }, 474 | 475 | /** 476 | * @name notDeepEqual 477 | * @function 478 | */ 479 | notDeepEqual: function( actual, expected, message ) { 480 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 481 | }, 482 | 483 | /** 484 | * @name strictEqual 485 | * @function 486 | */ 487 | strictEqual: function( actual, expected, message ) { 488 | QUnit.push( expected === actual, actual, expected, message ); 489 | }, 490 | 491 | /** 492 | * @name notStrictEqual 493 | * @function 494 | */ 495 | notStrictEqual: function( actual, expected, message ) { 496 | QUnit.push( expected !== actual, actual, expected, message ); 497 | }, 498 | 499 | throws: function( block, expected, message ) { 500 | var actual, 501 | ok = false; 502 | 503 | // 'expected' is optional 504 | if ( typeof expected === "string" ) { 505 | message = expected; 506 | expected = null; 507 | } 508 | 509 | config.current.ignoreGlobalErrors = true; 510 | try { 511 | block.call( config.current.testEnvironment ); 512 | } catch (e) { 513 | actual = e; 514 | } 515 | config.current.ignoreGlobalErrors = false; 516 | 517 | if ( actual ) { 518 | // we don't want to validate thrown error 519 | if ( !expected ) { 520 | ok = true; 521 | // expected is a regexp 522 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 523 | ok = expected.test( actual ); 524 | // expected is a constructor 525 | } else if ( actual instanceof expected ) { 526 | ok = true; 527 | // expected is a validation function which returns true is validation passed 528 | } else if ( expected.call( {}, actual ) === true ) { 529 | ok = true; 530 | } 531 | 532 | QUnit.push( ok, actual, null, message ); 533 | } else { 534 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 535 | } 536 | } 537 | }; 538 | 539 | /** 540 | * @deprecate since 1.8.0 541 | * Kept assertion helpers in root for backwards compatibility 542 | */ 543 | extend( QUnit, QUnit.assert ); 544 | 545 | /** 546 | * @deprecated since 1.9.0 547 | * Kept global "raises()" for backwards compatibility 548 | */ 549 | QUnit.raises = QUnit.assert.throws; 550 | 551 | /** 552 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 553 | * Kept to avoid TypeErrors for undefined methods. 554 | */ 555 | QUnit.equals = function() { 556 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 557 | }; 558 | QUnit.same = function() { 559 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 560 | }; 561 | 562 | // We want access to the constructor's prototype 563 | (function() { 564 | function F() {} 565 | F.prototype = QUnit; 566 | QUnit = new F(); 567 | // Make F QUnit's constructor so that we can add to the prototype later 568 | QUnit.constructor = F; 569 | }()); 570 | 571 | /** 572 | * Config object: Maintain internal state 573 | * Later exposed as QUnit.config 574 | * `config` initialized at top of scope 575 | */ 576 | config = { 577 | // The queue of tests to run 578 | queue: [], 579 | 580 | // block until document ready 581 | blocking: true, 582 | 583 | // when enabled, show only failing tests 584 | // gets persisted through sessionStorage and can be changed in UI via checkbox 585 | hidepassed: false, 586 | 587 | // by default, run previously failed tests first 588 | // very useful in combination with "Hide passed tests" checked 589 | reorder: true, 590 | 591 | // by default, modify document.title when suite is done 592 | altertitle: true, 593 | 594 | // when enabled, all tests must call expect() 595 | requireExpects: false, 596 | 597 | // add checkboxes that are persisted in the query-string 598 | // when enabled, the id is set to `true` as a `QUnit.config` property 599 | urlConfig: [ 600 | { 601 | id: "noglobals", 602 | label: "Check for Globals", 603 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 604 | }, 605 | { 606 | id: "notrycatch", 607 | label: "No try-catch", 608 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 609 | } 610 | ], 611 | 612 | // Set of all modules. 613 | modules: {}, 614 | 615 | // logging callback queues 616 | begin: [], 617 | done: [], 618 | log: [], 619 | testStart: [], 620 | testDone: [], 621 | moduleStart: [], 622 | moduleDone: [] 623 | }; 624 | 625 | // Initialize more QUnit.config and QUnit.urlParams 626 | (function() { 627 | var i, 628 | location = window.location || { search: "", protocol: "file:" }, 629 | params = location.search.slice( 1 ).split( "&" ), 630 | length = params.length, 631 | urlParams = {}, 632 | current; 633 | 634 | if ( params[ 0 ] ) { 635 | for ( i = 0; i < length; i++ ) { 636 | current = params[ i ].split( "=" ); 637 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 638 | // allow just a key to turn on a flag, e.g., test.html?noglobals 639 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 640 | urlParams[ current[ 0 ] ] = current[ 1 ]; 641 | } 642 | } 643 | 644 | QUnit.urlParams = urlParams; 645 | 646 | // String search anywhere in moduleName+testName 647 | config.filter = urlParams.filter; 648 | 649 | // Exact match of the module name 650 | config.module = urlParams.module; 651 | 652 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 653 | 654 | // Figure out if we're running the tests from a server or not 655 | QUnit.isLocal = location.protocol === "file:"; 656 | }()); 657 | 658 | // Export global variables, unless an 'exports' object exists, 659 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 660 | if ( typeof exports === "undefined" ) { 661 | extend( window, QUnit ); 662 | 663 | // Expose QUnit object 664 | window.QUnit = QUnit; 665 | } 666 | 667 | // Extend QUnit object, 668 | // these after set here because they should not be exposed as global functions 669 | extend( QUnit, { 670 | config: config, 671 | 672 | // Initialize the configuration options 673 | init: function() { 674 | extend( config, { 675 | stats: { all: 0, bad: 0 }, 676 | moduleStats: { all: 0, bad: 0 }, 677 | started: +new Date(), 678 | updateRate: 1000, 679 | blocking: false, 680 | autostart: true, 681 | autorun: false, 682 | filter: "", 683 | queue: [], 684 | semaphore: 0 685 | }); 686 | 687 | var tests, banner, result, 688 | qunit = id( "qunit" ); 689 | 690 | if ( qunit ) { 691 | qunit.innerHTML = 692 | "

    " + escapeInnerText( document.title ) + "

    " + 693 | "

    " + 694 | "
    " + 695 | "

    " + 696 | "
      "; 697 | } 698 | 699 | tests = id( "qunit-tests" ); 700 | banner = id( "qunit-banner" ); 701 | result = id( "qunit-testresult" ); 702 | 703 | if ( tests ) { 704 | tests.innerHTML = ""; 705 | } 706 | 707 | if ( banner ) { 708 | banner.className = ""; 709 | } 710 | 711 | if ( result ) { 712 | result.parentNode.removeChild( result ); 713 | } 714 | 715 | if ( tests ) { 716 | result = document.createElement( "p" ); 717 | result.id = "qunit-testresult"; 718 | result.className = "result"; 719 | tests.parentNode.insertBefore( result, tests ); 720 | result.innerHTML = "Running...
       "; 721 | } 722 | }, 723 | 724 | // Resets the test setup. Useful for tests that modify the DOM. 725 | reset: function() { 726 | var fixture = id( "qunit-fixture" ); 727 | if ( fixture ) { 728 | fixture.innerHTML = config.fixture; 729 | } 730 | }, 731 | 732 | // Trigger an event on an element. 733 | // @example triggerEvent( document.body, "click" ); 734 | triggerEvent: function( elem, type, event ) { 735 | if ( document.createEvent ) { 736 | event = document.createEvent( "MouseEvents" ); 737 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 738 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 739 | 740 | elem.dispatchEvent( event ); 741 | } else if ( elem.fireEvent ) { 742 | elem.fireEvent( "on" + type ); 743 | } 744 | }, 745 | 746 | // Safe object type checking 747 | is: function( type, obj ) { 748 | return QUnit.objectType( obj ) == type; 749 | }, 750 | 751 | objectType: function( obj ) { 752 | if ( typeof obj === "undefined" ) { 753 | return "undefined"; 754 | // consider: typeof null === object 755 | } 756 | if ( obj === null ) { 757 | return "null"; 758 | } 759 | 760 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ""; 761 | 762 | switch ( type ) { 763 | case "Number": 764 | if ( isNaN(obj) ) { 765 | return "nan"; 766 | } 767 | return "number"; 768 | case "String": 769 | case "Boolean": 770 | case "Array": 771 | case "Date": 772 | case "RegExp": 773 | case "Function": 774 | return type.toLowerCase(); 775 | } 776 | if ( typeof obj === "object" ) { 777 | return "object"; 778 | } 779 | return undefined; 780 | }, 781 | 782 | push: function( result, actual, expected, message ) { 783 | if ( !config.current ) { 784 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 785 | } 786 | 787 | var output, source, 788 | details = { 789 | module: config.current.module, 790 | name: config.current.testName, 791 | result: result, 792 | message: message, 793 | actual: actual, 794 | expected: expected 795 | }; 796 | 797 | message = escapeInnerText( message ) || ( result ? "okay" : "failed" ); 798 | message = "" + message + ""; 799 | output = message; 800 | 801 | if ( !result ) { 802 | expected = escapeInnerText( QUnit.jsDump.parse(expected) ); 803 | actual = escapeInnerText( QUnit.jsDump.parse(actual) ); 804 | output += ""; 805 | 806 | if ( actual != expected ) { 807 | output += ""; 808 | output += ""; 809 | } 810 | 811 | source = sourceFromStacktrace(); 812 | 813 | if ( source ) { 814 | details.source = source; 815 | output += ""; 816 | } 817 | 818 | output += "
      Expected:
      " + expected + "
      Result:
      " + actual + "
      Diff:
      " + QUnit.diff( expected, actual ) + "
      Source:
      " + escapeInnerText( source ) + "
      "; 819 | } 820 | 821 | runLoggingCallbacks( "log", QUnit, details ); 822 | 823 | config.current.assertions.push({ 824 | result: !!result, 825 | message: output 826 | }); 827 | }, 828 | 829 | pushFailure: function( message, source, actual ) { 830 | if ( !config.current ) { 831 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 832 | } 833 | 834 | var output, 835 | details = { 836 | module: config.current.module, 837 | name: config.current.testName, 838 | result: false, 839 | message: message 840 | }; 841 | 842 | message = escapeInnerText( message ) || "error"; 843 | message = "" + message + ""; 844 | output = message; 845 | 846 | output += ""; 847 | 848 | if ( actual ) { 849 | output += ""; 850 | } 851 | 852 | if ( source ) { 853 | details.source = source; 854 | output += ""; 855 | } 856 | 857 | output += "
      Result:
      " + escapeInnerText( actual ) + "
      Source:
      " + escapeInnerText( source ) + "
      "; 858 | 859 | runLoggingCallbacks( "log", QUnit, details ); 860 | 861 | config.current.assertions.push({ 862 | result: false, 863 | message: output 864 | }); 865 | }, 866 | 867 | url: function( params ) { 868 | params = extend( extend( {}, QUnit.urlParams ), params ); 869 | var key, 870 | querystring = "?"; 871 | 872 | for ( key in params ) { 873 | if ( !hasOwn.call( params, key ) ) { 874 | continue; 875 | } 876 | querystring += encodeURIComponent( key ) + "=" + 877 | encodeURIComponent( params[ key ] ) + "&"; 878 | } 879 | return window.location.pathname + querystring.slice( 0, -1 ); 880 | }, 881 | 882 | extend: extend, 883 | id: id, 884 | addEvent: addEvent 885 | // load, equiv, jsDump, diff: Attached later 886 | }); 887 | 888 | /** 889 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 890 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 891 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 892 | * Doing this allows us to tell if the following methods have been overwritten on the actual 893 | * QUnit object. 894 | */ 895 | extend( QUnit.constructor.prototype, { 896 | 897 | // Logging callbacks; all receive a single argument with the listed properties 898 | // run test/logs.html for any related changes 899 | begin: registerLoggingCallback( "begin" ), 900 | 901 | // done: { failed, passed, total, runtime } 902 | done: registerLoggingCallback( "done" ), 903 | 904 | // log: { result, actual, expected, message } 905 | log: registerLoggingCallback( "log" ), 906 | 907 | // testStart: { name } 908 | testStart: registerLoggingCallback( "testStart" ), 909 | 910 | // testDone: { name, failed, passed, total } 911 | testDone: registerLoggingCallback( "testDone" ), 912 | 913 | // moduleStart: { name } 914 | moduleStart: registerLoggingCallback( "moduleStart" ), 915 | 916 | // moduleDone: { name, failed, passed, total } 917 | moduleDone: registerLoggingCallback( "moduleDone" ) 918 | }); 919 | 920 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 921 | config.autorun = true; 922 | } 923 | 924 | QUnit.load = function() { 925 | runLoggingCallbacks( "begin", QUnit, {} ); 926 | 927 | // Initialize the config, saving the execution queue 928 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, moduleFilter, 929 | numModules = 0, 930 | moduleFilterHtml = "", 931 | urlConfigHtml = "", 932 | oldconfig = extend( {}, config ); 933 | 934 | QUnit.init(); 935 | extend(config, oldconfig); 936 | 937 | config.blocking = false; 938 | 939 | len = config.urlConfig.length; 940 | 941 | for ( i = 0; i < len; i++ ) { 942 | val = config.urlConfig[i]; 943 | if ( typeof val === "string" ) { 944 | val = { 945 | id: val, 946 | label: val, 947 | tooltip: "[no tooltip available]" 948 | }; 949 | } 950 | config[ val.id ] = QUnit.urlParams[ val.id ]; 951 | urlConfigHtml += ""; 952 | } 953 | 954 | moduleFilterHtml += ""; 962 | 963 | // `userAgent` initialized at top of scope 964 | userAgent = id( "qunit-userAgent" ); 965 | if ( userAgent ) { 966 | userAgent.innerHTML = navigator.userAgent; 967 | } 968 | 969 | // `banner` initialized at top of scope 970 | banner = id( "qunit-header" ); 971 | if ( banner ) { 972 | banner.innerHTML = "" + banner.innerHTML + " "; 973 | } 974 | 975 | // `toolbar` initialized at top of scope 976 | toolbar = id( "qunit-testrunner-toolbar" ); 977 | if ( toolbar ) { 978 | // `filter` initialized at top of scope 979 | filter = document.createElement( "input" ); 980 | filter.type = "checkbox"; 981 | filter.id = "qunit-filter-pass"; 982 | 983 | addEvent( filter, "click", function() { 984 | var tmp, 985 | ol = document.getElementById( "qunit-tests" ); 986 | 987 | if ( filter.checked ) { 988 | ol.className = ol.className + " hidepass"; 989 | } else { 990 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 991 | ol.className = tmp.replace( / hidepass /, " " ); 992 | } 993 | if ( defined.sessionStorage ) { 994 | if (filter.checked) { 995 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 996 | } else { 997 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 998 | } 999 | } 1000 | }); 1001 | 1002 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1003 | filter.checked = true; 1004 | // `ol` initialized at top of scope 1005 | ol = document.getElementById( "qunit-tests" ); 1006 | ol.className = ol.className + " hidepass"; 1007 | } 1008 | toolbar.appendChild( filter ); 1009 | 1010 | // `label` initialized at top of scope 1011 | label = document.createElement( "label" ); 1012 | label.setAttribute( "for", "qunit-filter-pass" ); 1013 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 1014 | label.innerHTML = "Hide passed tests"; 1015 | toolbar.appendChild( label ); 1016 | 1017 | urlConfigCheckboxes = document.createElement( 'span' ); 1018 | urlConfigCheckboxes.innerHTML = urlConfigHtml; 1019 | addEvent( urlConfigCheckboxes, "change", function( event ) { 1020 | var params = {}; 1021 | params[ event.target.name ] = event.target.checked ? true : undefined; 1022 | window.location = QUnit.url( params ); 1023 | }); 1024 | toolbar.appendChild( urlConfigCheckboxes ); 1025 | 1026 | if (numModules > 1) { 1027 | moduleFilter = document.createElement( 'span' ); 1028 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); 1029 | moduleFilter.innerHTML = moduleFilterHtml; 1030 | addEvent( moduleFilter, "change", function() { 1031 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1032 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1033 | 1034 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); 1035 | }); 1036 | toolbar.appendChild(moduleFilter); 1037 | } 1038 | } 1039 | 1040 | // `main` initialized at top of scope 1041 | main = id( "qunit-fixture" ); 1042 | if ( main ) { 1043 | config.fixture = main.innerHTML; 1044 | } 1045 | 1046 | if ( config.autostart ) { 1047 | QUnit.start(); 1048 | } 1049 | }; 1050 | 1051 | addEvent( window, "load", QUnit.load ); 1052 | 1053 | // `onErrorFnPrev` initialized at top of scope 1054 | // Preserve other handlers 1055 | onErrorFnPrev = window.onerror; 1056 | 1057 | // Cover uncaught exceptions 1058 | // Returning true will surpress the default browser handler, 1059 | // returning false will let it run. 1060 | window.onerror = function ( error, filePath, linerNr ) { 1061 | var ret = false; 1062 | if ( onErrorFnPrev ) { 1063 | ret = onErrorFnPrev( error, filePath, linerNr ); 1064 | } 1065 | 1066 | // Treat return value as window.onerror itself does, 1067 | // Only do our handling if not surpressed. 1068 | if ( ret !== true ) { 1069 | if ( QUnit.config.current ) { 1070 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1071 | return true; 1072 | } 1073 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1074 | } else { 1075 | QUnit.test( "global failure", extend( function() { 1076 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1077 | }, { validTest: validTest } ) ); 1078 | } 1079 | return false; 1080 | } 1081 | 1082 | return ret; 1083 | }; 1084 | 1085 | function done() { 1086 | config.autorun = true; 1087 | 1088 | // Log the last module results 1089 | if ( config.currentModule ) { 1090 | runLoggingCallbacks( "moduleDone", QUnit, { 1091 | name: config.currentModule, 1092 | failed: config.moduleStats.bad, 1093 | passed: config.moduleStats.all - config.moduleStats.bad, 1094 | total: config.moduleStats.all 1095 | }); 1096 | } 1097 | 1098 | var i, key, 1099 | banner = id( "qunit-banner" ), 1100 | tests = id( "qunit-tests" ), 1101 | runtime = +new Date() - config.started, 1102 | passed = config.stats.all - config.stats.bad, 1103 | html = [ 1104 | "Tests completed in ", 1105 | runtime, 1106 | " milliseconds.
      ", 1107 | "", 1108 | passed, 1109 | " tests of ", 1110 | config.stats.all, 1111 | " passed, ", 1112 | config.stats.bad, 1113 | " failed." 1114 | ].join( "" ); 1115 | 1116 | if ( banner ) { 1117 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1118 | } 1119 | 1120 | if ( tests ) { 1121 | id( "qunit-testresult" ).innerHTML = html; 1122 | } 1123 | 1124 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1125 | // show ✖ for good, ✔ for bad suite result in title 1126 | // use escape sequences in case file gets loaded with non-utf-8-charset 1127 | document.title = [ 1128 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1129 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1130 | ].join( " " ); 1131 | } 1132 | 1133 | // clear own sessionStorage items if all tests passed 1134 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1135 | // `key` & `i` initialized at top of scope 1136 | for ( i = 0; i < sessionStorage.length; i++ ) { 1137 | key = sessionStorage.key( i++ ); 1138 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1139 | sessionStorage.removeItem( key ); 1140 | } 1141 | } 1142 | } 1143 | 1144 | // scroll back to top to show results 1145 | if ( window.scrollTo ) { 1146 | window.scrollTo(0, 0); 1147 | } 1148 | 1149 | runLoggingCallbacks( "done", QUnit, { 1150 | failed: config.stats.bad, 1151 | passed: passed, 1152 | total: config.stats.all, 1153 | runtime: runtime 1154 | }); 1155 | } 1156 | 1157 | /** @return Boolean: true if this test should be ran */ 1158 | function validTest( test ) { 1159 | var include, 1160 | filter = config.filter && config.filter.toLowerCase(), 1161 | module = config.module && config.module.toLowerCase(), 1162 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1163 | 1164 | // Internally-generated tests are always valid 1165 | if ( test.callback && test.callback.validTest === validTest ) { 1166 | delete test.callback.validTest; 1167 | return true; 1168 | } 1169 | 1170 | if ( config.testNumber ) { 1171 | return test.testNumber === config.testNumber; 1172 | } 1173 | 1174 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1175 | return false; 1176 | } 1177 | 1178 | if ( !filter ) { 1179 | return true; 1180 | } 1181 | 1182 | include = filter.charAt( 0 ) !== "!"; 1183 | if ( !include ) { 1184 | filter = filter.slice( 1 ); 1185 | } 1186 | 1187 | // If the filter matches, we need to honour include 1188 | if ( fullName.indexOf( filter ) !== -1 ) { 1189 | return include; 1190 | } 1191 | 1192 | // Otherwise, do the opposite 1193 | return !include; 1194 | } 1195 | 1196 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1197 | // Later Safari and IE10 are supposed to support error.stack as well 1198 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1199 | function extractStacktrace( e, offset ) { 1200 | offset = offset === undefined ? 3 : offset; 1201 | 1202 | var stack, include, i, regex; 1203 | 1204 | if ( e.stacktrace ) { 1205 | // Opera 1206 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1207 | } else if ( e.stack ) { 1208 | // Firefox, Chrome 1209 | stack = e.stack.split( "\n" ); 1210 | if (/^error$/i.test( stack[0] ) ) { 1211 | stack.shift(); 1212 | } 1213 | if ( fileName ) { 1214 | include = []; 1215 | for ( i = offset; i < stack.length; i++ ) { 1216 | if ( stack[ i ].indexOf( fileName ) != -1 ) { 1217 | break; 1218 | } 1219 | include.push( stack[ i ] ); 1220 | } 1221 | if ( include.length ) { 1222 | return include.join( "\n" ); 1223 | } 1224 | } 1225 | return stack[ offset ]; 1226 | } else if ( e.sourceURL ) { 1227 | // Safari, PhantomJS 1228 | // hopefully one day Safari provides actual stacktraces 1229 | // exclude useless self-reference for generated Error objects 1230 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1231 | return; 1232 | } 1233 | // for actual exceptions, this is useful 1234 | return e.sourceURL + ":" + e.line; 1235 | } 1236 | } 1237 | function sourceFromStacktrace( offset ) { 1238 | try { 1239 | throw new Error(); 1240 | } catch ( e ) { 1241 | return extractStacktrace( e, offset ); 1242 | } 1243 | } 1244 | 1245 | function escapeInnerText( s ) { 1246 | if ( !s ) { 1247 | return ""; 1248 | } 1249 | s = s + ""; 1250 | return s.replace( /[\&<>]/g, function( s ) { 1251 | switch( s ) { 1252 | case "&": return "&"; 1253 | case "<": return "<"; 1254 | case ">": return ">"; 1255 | default: return s; 1256 | } 1257 | }); 1258 | } 1259 | 1260 | function synchronize( callback, last ) { 1261 | config.queue.push( callback ); 1262 | 1263 | if ( config.autorun && !config.blocking ) { 1264 | process( last ); 1265 | } 1266 | } 1267 | 1268 | function process( last ) { 1269 | function next() { 1270 | process( last ); 1271 | } 1272 | var start = new Date().getTime(); 1273 | config.depth = config.depth ? config.depth + 1 : 1; 1274 | 1275 | while ( config.queue.length && !config.blocking ) { 1276 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1277 | config.queue.shift()(); 1278 | } else { 1279 | window.setTimeout( next, 13 ); 1280 | break; 1281 | } 1282 | } 1283 | config.depth--; 1284 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1285 | done(); 1286 | } 1287 | } 1288 | 1289 | function saveGlobal() { 1290 | config.pollution = []; 1291 | 1292 | if ( config.noglobals ) { 1293 | for ( var key in window ) { 1294 | // in Opera sometimes DOM element ids show up here, ignore them 1295 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1296 | continue; 1297 | } 1298 | config.pollution.push( key ); 1299 | } 1300 | } 1301 | } 1302 | 1303 | function checkPollution( name ) { 1304 | var newGlobals, 1305 | deletedGlobals, 1306 | old = config.pollution; 1307 | 1308 | saveGlobal(); 1309 | 1310 | newGlobals = diff( config.pollution, old ); 1311 | if ( newGlobals.length > 0 ) { 1312 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1313 | } 1314 | 1315 | deletedGlobals = diff( old, config.pollution ); 1316 | if ( deletedGlobals.length > 0 ) { 1317 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1318 | } 1319 | } 1320 | 1321 | // returns a new Array with the elements that are in a but not in b 1322 | function diff( a, b ) { 1323 | var i, j, 1324 | result = a.slice(); 1325 | 1326 | for ( i = 0; i < result.length; i++ ) { 1327 | for ( j = 0; j < b.length; j++ ) { 1328 | if ( result[i] === b[j] ) { 1329 | result.splice( i, 1 ); 1330 | i--; 1331 | break; 1332 | } 1333 | } 1334 | } 1335 | return result; 1336 | } 1337 | 1338 | function extend( a, b ) { 1339 | for ( var prop in b ) { 1340 | if ( b[ prop ] === undefined ) { 1341 | delete a[ prop ]; 1342 | 1343 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1344 | } else if ( prop !== "constructor" || a !== window ) { 1345 | a[ prop ] = b[ prop ]; 1346 | } 1347 | } 1348 | 1349 | return a; 1350 | } 1351 | 1352 | function addEvent( elem, type, fn ) { 1353 | if ( elem.addEventListener ) { 1354 | elem.addEventListener( type, fn, false ); 1355 | } else if ( elem.attachEvent ) { 1356 | elem.attachEvent( "on" + type, fn ); 1357 | } else { 1358 | fn(); 1359 | } 1360 | } 1361 | 1362 | function id( name ) { 1363 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1364 | document.getElementById( name ); 1365 | } 1366 | 1367 | function registerLoggingCallback( key ) { 1368 | return function( callback ) { 1369 | config[key].push( callback ); 1370 | }; 1371 | } 1372 | 1373 | // Supports deprecated method of completely overwriting logging callbacks 1374 | function runLoggingCallbacks( key, scope, args ) { 1375 | //debugger; 1376 | var i, callbacks; 1377 | if ( QUnit.hasOwnProperty( key ) ) { 1378 | QUnit[ key ].call(scope, args ); 1379 | } else { 1380 | callbacks = config[ key ]; 1381 | for ( i = 0; i < callbacks.length; i++ ) { 1382 | callbacks[ i ].call( scope, args ); 1383 | } 1384 | } 1385 | } 1386 | 1387 | // Test for equality any JavaScript type. 1388 | // Author: Philippe Rathé 1389 | QUnit.equiv = (function() { 1390 | 1391 | // Call the o related callback with the given arguments. 1392 | function bindCallbacks( o, callbacks, args ) { 1393 | var prop = QUnit.objectType( o ); 1394 | if ( prop ) { 1395 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1396 | return callbacks[ prop ].apply( callbacks, args ); 1397 | } else { 1398 | return callbacks[ prop ]; // or undefined 1399 | } 1400 | } 1401 | } 1402 | 1403 | // the real equiv function 1404 | var innerEquiv, 1405 | // stack to decide between skip/abort functions 1406 | callers = [], 1407 | // stack to avoiding loops from circular referencing 1408 | parents = [], 1409 | 1410 | getProto = Object.getPrototypeOf || function ( obj ) { 1411 | return obj.__proto__; 1412 | }, 1413 | callbacks = (function () { 1414 | 1415 | // for string, boolean, number and null 1416 | function useStrictEquality( b, a ) { 1417 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1418 | // to catch short annotaion VS 'new' annotation of a 1419 | // declaration 1420 | // e.g. var i = 1; 1421 | // var j = new Number(1); 1422 | return a == b; 1423 | } else { 1424 | return a === b; 1425 | } 1426 | } 1427 | 1428 | return { 1429 | "string": useStrictEquality, 1430 | "boolean": useStrictEquality, 1431 | "number": useStrictEquality, 1432 | "null": useStrictEquality, 1433 | "undefined": useStrictEquality, 1434 | 1435 | "nan": function( b ) { 1436 | return isNaN( b ); 1437 | }, 1438 | 1439 | "date": function( b, a ) { 1440 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1441 | }, 1442 | 1443 | "regexp": function( b, a ) { 1444 | return QUnit.objectType( b ) === "regexp" && 1445 | // the regex itself 1446 | a.source === b.source && 1447 | // and its modifers 1448 | a.global === b.global && 1449 | // (gmi) ... 1450 | a.ignoreCase === b.ignoreCase && 1451 | a.multiline === b.multiline && 1452 | a.sticky === b.sticky; 1453 | }, 1454 | 1455 | // - skip when the property is a method of an instance (OOP) 1456 | // - abort otherwise, 1457 | // initial === would have catch identical references anyway 1458 | "function": function() { 1459 | var caller = callers[callers.length - 1]; 1460 | return caller !== Object && typeof caller !== "undefined"; 1461 | }, 1462 | 1463 | "array": function( b, a ) { 1464 | var i, j, len, loop; 1465 | 1466 | // b could be an object literal here 1467 | if ( QUnit.objectType( b ) !== "array" ) { 1468 | return false; 1469 | } 1470 | 1471 | len = a.length; 1472 | if ( len !== b.length ) { 1473 | // safe and faster 1474 | return false; 1475 | } 1476 | 1477 | // track reference to avoid circular references 1478 | parents.push( a ); 1479 | for ( i = 0; i < len; i++ ) { 1480 | loop = false; 1481 | for ( j = 0; j < parents.length; j++ ) { 1482 | if ( parents[j] === a[i] ) { 1483 | loop = true;// dont rewalk array 1484 | } 1485 | } 1486 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1487 | parents.pop(); 1488 | return false; 1489 | } 1490 | } 1491 | parents.pop(); 1492 | return true; 1493 | }, 1494 | 1495 | "object": function( b, a ) { 1496 | var i, j, loop, 1497 | // Default to true 1498 | eq = true, 1499 | aProperties = [], 1500 | bProperties = []; 1501 | 1502 | // comparing constructors is more strict than using 1503 | // instanceof 1504 | if ( a.constructor !== b.constructor ) { 1505 | // Allow objects with no prototype to be equivalent to 1506 | // objects with Object as their constructor. 1507 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1508 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1509 | return false; 1510 | } 1511 | } 1512 | 1513 | // stack constructor before traversing properties 1514 | callers.push( a.constructor ); 1515 | // track reference to avoid circular references 1516 | parents.push( a ); 1517 | 1518 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1519 | // and go deep 1520 | loop = false; 1521 | for ( j = 0; j < parents.length; j++ ) { 1522 | if ( parents[j] === a[i] ) { 1523 | // don't go down the same path twice 1524 | loop = true; 1525 | } 1526 | } 1527 | aProperties.push(i); // collect a's properties 1528 | 1529 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1530 | eq = false; 1531 | break; 1532 | } 1533 | } 1534 | 1535 | callers.pop(); // unstack, we are done 1536 | parents.pop(); 1537 | 1538 | for ( i in b ) { 1539 | bProperties.push( i ); // collect b's properties 1540 | } 1541 | 1542 | // Ensures identical properties name 1543 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1544 | } 1545 | }; 1546 | }()); 1547 | 1548 | innerEquiv = function() { // can take multiple arguments 1549 | var args = [].slice.apply( arguments ); 1550 | if ( args.length < 2 ) { 1551 | return true; // end transition 1552 | } 1553 | 1554 | return (function( a, b ) { 1555 | if ( a === b ) { 1556 | return true; // catch the most you can 1557 | } else if ( a === null || b === null || typeof a === "undefined" || 1558 | typeof b === "undefined" || 1559 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1560 | return false; // don't lose time with error prone cases 1561 | } else { 1562 | return bindCallbacks(a, callbacks, [ b, a ]); 1563 | } 1564 | 1565 | // apply transition with (1..n) arguments 1566 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1567 | }; 1568 | 1569 | return innerEquiv; 1570 | }()); 1571 | 1572 | /** 1573 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1574 | * http://flesler.blogspot.com Licensed under BSD 1575 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1576 | * 1577 | * @projectDescription Advanced and extensible data dumping for Javascript. 1578 | * @version 1.0.0 1579 | * @author Ariel Flesler 1580 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1581 | */ 1582 | QUnit.jsDump = (function() { 1583 | function quote( str ) { 1584 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1585 | } 1586 | function literal( o ) { 1587 | return o + ""; 1588 | } 1589 | function join( pre, arr, post ) { 1590 | var s = jsDump.separator(), 1591 | base = jsDump.indent(), 1592 | inner = jsDump.indent(1); 1593 | if ( arr.join ) { 1594 | arr = arr.join( "," + s + inner ); 1595 | } 1596 | if ( !arr ) { 1597 | return pre + post; 1598 | } 1599 | return [ pre, inner + arr, base + post ].join(s); 1600 | } 1601 | function array( arr, stack ) { 1602 | var i = arr.length, ret = new Array(i); 1603 | this.up(); 1604 | while ( i-- ) { 1605 | ret[i] = this.parse( arr[i] , undefined , stack); 1606 | } 1607 | this.down(); 1608 | return join( "[", ret, "]" ); 1609 | } 1610 | 1611 | var reName = /^function (\w+)/, 1612 | jsDump = { 1613 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1614 | stack = stack || [ ]; 1615 | var inStack, res, 1616 | parser = this.parsers[ type || this.typeOf(obj) ]; 1617 | 1618 | type = typeof parser; 1619 | inStack = inArray( obj, stack ); 1620 | 1621 | if ( inStack != -1 ) { 1622 | return "recursion(" + (inStack - stack.length) + ")"; 1623 | } 1624 | //else 1625 | if ( type == "function" ) { 1626 | stack.push( obj ); 1627 | res = parser.call( this, obj, stack ); 1628 | stack.pop(); 1629 | return res; 1630 | } 1631 | // else 1632 | return ( type == "string" ) ? parser : this.parsers.error; 1633 | }, 1634 | typeOf: function( obj ) { 1635 | var type; 1636 | if ( obj === null ) { 1637 | type = "null"; 1638 | } else if ( typeof obj === "undefined" ) { 1639 | type = "undefined"; 1640 | } else if ( QUnit.is( "regexp", obj) ) { 1641 | type = "regexp"; 1642 | } else if ( QUnit.is( "date", obj) ) { 1643 | type = "date"; 1644 | } else if ( QUnit.is( "function", obj) ) { 1645 | type = "function"; 1646 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1647 | type = "window"; 1648 | } else if ( obj.nodeType === 9 ) { 1649 | type = "document"; 1650 | } else if ( obj.nodeType ) { 1651 | type = "node"; 1652 | } else if ( 1653 | // native arrays 1654 | toString.call( obj ) === "[object Array]" || 1655 | // NodeList objects 1656 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1657 | ) { 1658 | type = "array"; 1659 | } else { 1660 | type = typeof obj; 1661 | } 1662 | return type; 1663 | }, 1664 | separator: function() { 1665 | return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; 1666 | }, 1667 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1668 | if ( !this.multiline ) { 1669 | return ""; 1670 | } 1671 | var chr = this.indentChar; 1672 | if ( this.HTML ) { 1673 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1674 | } 1675 | return new Array( this._depth_ + (extra||0) ).join(chr); 1676 | }, 1677 | up: function( a ) { 1678 | this._depth_ += a || 1; 1679 | }, 1680 | down: function( a ) { 1681 | this._depth_ -= a || 1; 1682 | }, 1683 | setParser: function( name, parser ) { 1684 | this.parsers[name] = parser; 1685 | }, 1686 | // The next 3 are exposed so you can use them 1687 | quote: quote, 1688 | literal: literal, 1689 | join: join, 1690 | // 1691 | _depth_: 1, 1692 | // This is the list of parsers, to modify them, use jsDump.setParser 1693 | parsers: { 1694 | window: "[Window]", 1695 | document: "[Document]", 1696 | error: "[ERROR]", //when no parser is found, shouldn"t happen 1697 | unknown: "[Unknown]", 1698 | "null": "null", 1699 | "undefined": "undefined", 1700 | "function": function( fn ) { 1701 | var ret = "function", 1702 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE 1703 | 1704 | if ( name ) { 1705 | ret += " " + name; 1706 | } 1707 | ret += "( "; 1708 | 1709 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1710 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1711 | }, 1712 | array: array, 1713 | nodelist: array, 1714 | "arguments": array, 1715 | object: function( map, stack ) { 1716 | var ret = [ ], keys, key, val, i; 1717 | QUnit.jsDump.up(); 1718 | if ( Object.keys ) { 1719 | keys = Object.keys( map ); 1720 | } else { 1721 | keys = []; 1722 | for ( key in map ) { 1723 | keys.push( key ); 1724 | } 1725 | } 1726 | keys.sort(); 1727 | for ( i = 0; i < keys.length; i++ ) { 1728 | key = keys[ i ]; 1729 | val = map[ key ]; 1730 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1731 | } 1732 | QUnit.jsDump.down(); 1733 | return join( "{", ret, "}" ); 1734 | }, 1735 | node: function( node ) { 1736 | var a, val, 1737 | open = QUnit.jsDump.HTML ? "<" : "<", 1738 | close = QUnit.jsDump.HTML ? ">" : ">", 1739 | tag = node.nodeName.toLowerCase(), 1740 | ret = open + tag; 1741 | 1742 | for ( a in QUnit.jsDump.DOMAttrs ) { 1743 | val = node[ QUnit.jsDump.DOMAttrs[a] ]; 1744 | if ( val ) { 1745 | ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" ); 1746 | } 1747 | } 1748 | return ret + close + open + "/" + tag + close; 1749 | }, 1750 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1751 | var args, 1752 | l = fn.length; 1753 | 1754 | if ( !l ) { 1755 | return ""; 1756 | } 1757 | 1758 | args = new Array(l); 1759 | while ( l-- ) { 1760 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1761 | } 1762 | return " " + args.join( ", " ) + " "; 1763 | }, 1764 | key: quote, //object calls it internally, the key part of an item in a map 1765 | functionCode: "[code]", //function calls it internally, it's the content of the function 1766 | attribute: quote, //node calls it internally, it's an html attribute value 1767 | string: quote, 1768 | date: quote, 1769 | regexp: literal, //regex 1770 | number: literal, 1771 | "boolean": literal 1772 | }, 1773 | DOMAttrs: { 1774 | //attributes to dump from nodes, name=>realName 1775 | id: "id", 1776 | name: "name", 1777 | "class": "className" 1778 | }, 1779 | HTML: false,//if true, entities are escaped ( <, >, \t, space and \n ) 1780 | indentChar: " ",//indentation unit 1781 | multiline: true //if true, items in a collection, are separated by a \n, else just a space. 1782 | }; 1783 | 1784 | return jsDump; 1785 | }()); 1786 | 1787 | // from Sizzle.js 1788 | function getText( elems ) { 1789 | var i, elem, 1790 | ret = ""; 1791 | 1792 | for ( i = 0; elems[i]; i++ ) { 1793 | elem = elems[i]; 1794 | 1795 | // Get the text from text nodes and CDATA nodes 1796 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1797 | ret += elem.nodeValue; 1798 | 1799 | // Traverse everything else, except comment nodes 1800 | } else if ( elem.nodeType !== 8 ) { 1801 | ret += getText( elem.childNodes ); 1802 | } 1803 | } 1804 | 1805 | return ret; 1806 | } 1807 | 1808 | // from jquery.js 1809 | function inArray( elem, array ) { 1810 | if ( array.indexOf ) { 1811 | return array.indexOf( elem ); 1812 | } 1813 | 1814 | for ( var i = 0, length = array.length; i < length; i++ ) { 1815 | if ( array[ i ] === elem ) { 1816 | return i; 1817 | } 1818 | } 1819 | 1820 | return -1; 1821 | } 1822 | 1823 | /* 1824 | * Javascript Diff Algorithm 1825 | * By John Resig (http://ejohn.org/) 1826 | * Modified by Chu Alan "sprite" 1827 | * 1828 | * Released under the MIT license. 1829 | * 1830 | * More Info: 1831 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1832 | * 1833 | * Usage: QUnit.diff(expected, actual) 1834 | * 1835 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 1836 | */ 1837 | QUnit.diff = (function() { 1838 | function diff( o, n ) { 1839 | var i, 1840 | ns = {}, 1841 | os = {}; 1842 | 1843 | for ( i = 0; i < n.length; i++ ) { 1844 | if ( ns[ n[i] ] == null ) { 1845 | ns[ n[i] ] = { 1846 | rows: [], 1847 | o: null 1848 | }; 1849 | } 1850 | ns[ n[i] ].rows.push( i ); 1851 | } 1852 | 1853 | for ( i = 0; i < o.length; i++ ) { 1854 | if ( os[ o[i] ] == null ) { 1855 | os[ o[i] ] = { 1856 | rows: [], 1857 | n: null 1858 | }; 1859 | } 1860 | os[ o[i] ].rows.push( i ); 1861 | } 1862 | 1863 | for ( i in ns ) { 1864 | if ( !hasOwn.call( ns, i ) ) { 1865 | continue; 1866 | } 1867 | if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) { 1868 | n[ ns[i].rows[0] ] = { 1869 | text: n[ ns[i].rows[0] ], 1870 | row: os[i].rows[0] 1871 | }; 1872 | o[ os[i].rows[0] ] = { 1873 | text: o[ os[i].rows[0] ], 1874 | row: ns[i].rows[0] 1875 | }; 1876 | } 1877 | } 1878 | 1879 | for ( i = 0; i < n.length - 1; i++ ) { 1880 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 1881 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 1882 | 1883 | n[ i + 1 ] = { 1884 | text: n[ i + 1 ], 1885 | row: n[i].row + 1 1886 | }; 1887 | o[ n[i].row + 1 ] = { 1888 | text: o[ n[i].row + 1 ], 1889 | row: i + 1 1890 | }; 1891 | } 1892 | } 1893 | 1894 | for ( i = n.length - 1; i > 0; i-- ) { 1895 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 1896 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 1897 | 1898 | n[ i - 1 ] = { 1899 | text: n[ i - 1 ], 1900 | row: n[i].row - 1 1901 | }; 1902 | o[ n[i].row - 1 ] = { 1903 | text: o[ n[i].row - 1 ], 1904 | row: i - 1 1905 | }; 1906 | } 1907 | } 1908 | 1909 | return { 1910 | o: o, 1911 | n: n 1912 | }; 1913 | } 1914 | 1915 | return function( o, n ) { 1916 | o = o.replace( /\s+$/, "" ); 1917 | n = n.replace( /\s+$/, "" ); 1918 | 1919 | var i, pre, 1920 | str = "", 1921 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 1922 | oSpace = o.match(/\s+/g), 1923 | nSpace = n.match(/\s+/g); 1924 | 1925 | if ( oSpace == null ) { 1926 | oSpace = [ " " ]; 1927 | } 1928 | else { 1929 | oSpace.push( " " ); 1930 | } 1931 | 1932 | if ( nSpace == null ) { 1933 | nSpace = [ " " ]; 1934 | } 1935 | else { 1936 | nSpace.push( " " ); 1937 | } 1938 | 1939 | if ( out.n.length === 0 ) { 1940 | for ( i = 0; i < out.o.length; i++ ) { 1941 | str += "" + out.o[i] + oSpace[i] + ""; 1942 | } 1943 | } 1944 | else { 1945 | if ( out.n[0].text == null ) { 1946 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 1947 | str += "" + out.o[n] + oSpace[n] + ""; 1948 | } 1949 | } 1950 | 1951 | for ( i = 0; i < out.n.length; i++ ) { 1952 | if (out.n[i].text == null) { 1953 | str += "" + out.n[i] + nSpace[i] + ""; 1954 | } 1955 | else { 1956 | // `pre` initialized at top of scope 1957 | pre = ""; 1958 | 1959 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 1960 | pre += "" + out.o[n] + oSpace[n] + ""; 1961 | } 1962 | str += " " + out.n[i].text + nSpace[i] + pre; 1963 | } 1964 | } 1965 | } 1966 | 1967 | return str; 1968 | }; 1969 | }()); 1970 | 1971 | // for CommonJS enviroments, export everything 1972 | if ( typeof exports !== "undefined" ) { 1973 | extend(exports, QUnit); 1974 | } 1975 | 1976 | // get at whatever the global object is, like window in browsers 1977 | }( (function() {return this;}.call()) )); 1978 | --------------------------------------------------------------------------------