├── .gitignore ├── README.md ├── Package.nuspec ├── LICENSE ├── spec ├── runner.html ├── lib │ ├── qunit-1.10.0.css │ ├── json2.js │ └── qunit-1.10.0.js ├── issues.js ├── proxyDependentObservableBehaviors.js └── mappingBehaviors.js ├── HISTORY.md └── knockout.mapping.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.csproj.user 3 | bin 4 | obj 5 | *.pdb 6 | _ReSharper* 7 | *.ReSharper.user 8 | *.ReSharper 9 | desktop.ini 10 | .eprj -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Object mapping plugin for [Knockout](http://knockoutjs.com/) - Find the documentation [here](http://knockoutjs.com/documentation/plugins-mapping.html). 2 | READ THIS 3 | --- 4 | Due to lack of time this project is currently not actively maintained. Feel free to be a hero-- step up and [fork this repo](https://github.com/SteveSanderson/knockout.mapping/fork)! 5 | -------------------------------------------------------------------------------- /Package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Knockout.Mapping 5 | 2.4.1 6 | Roy Jacobs 7 | http://www.opensource.org/licenses/mit-license.php 8 | http://knockoutjs.com/documentation/plugins-mapping.html 9 | false 10 | The mapping plugin gives you a straightforward way to map plain JavaScript objects into a view model with the appropriate Knockout observables. This is an alternative to manually writing your own JavaScript code that constructs a view model based on some data you've fetched from the server. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) - http://www.opensource.org/licenses/mit-license.php 2 | 3 | Copyright (c) Steven Sanderson, Roy Jacobs 4 | http://knockoutjs.com/documentation/plugins-mapping.html 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 33 | 34 | 35 | 36 | 37 | 38 |

QUnit Test Suite

39 |

40 |
41 |

42 |
    43 |
    test markup
    44 | 45 | 46 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Release 2.4.1 - February 8th, 2013 2 | 3 | * Added mappedGet for observable arrays 4 | * Issue #134: Throttle issue using mapping 5 | * Issue #135: Why is custom update for observableArray firing twice when using mapping plugin? 6 | 7 | Release 2.4.0 - February 4th, 2013 8 | 9 | * Removed asynchronous processing that was used to reset mapping nesting 10 | * Improved getType performance 11 | 12 | Release 2.3.5 - December 10th, 2012 13 | 14 | * Issue #121: Added functionality so that explicit declared none observable members on a ViewModel will remain none observable after mapping 15 | 16 | Release 2.3.4 - November 22nd, 2012 17 | 18 | * Issue #114: Added new "observe" array to options 19 | 20 | Release 2.3.3 - October 30th, 2012 21 | 22 | * Fixed issue #105, #111: Update callback is not being called 23 | * Fixed issue #107: String values in mapping cause infinite recursion in extendObject 24 | 25 | Release 2.3.2 - August 20th, 2012 26 | 27 | * Fixed issue #86: Don't update properties on object with update callback 28 | 29 | Release 2.3.1 - August 6th, 2012 30 | 31 | * Fixed issue #33: Create method in mappings receive meaningless options.parent for observableArray properties 32 | * Fixed issue #99: Updating throttled observable 33 | * Fixed issue #100: private variable leaks onto window object 34 | 35 | Release 2.3.0 - July 31st, 2012 36 | 37 | * Added support for not mapping certain array elements (return "options.skip" from your create callback) 38 | * Fixed issue #91: "wrap" function makes computed writable 39 | * Fixed issue #94: Bug/problem with ignore argument in mapping.fromJS 40 | 41 | Release 2.2.4 -------------------------------------------------------------------------------- /spec/lib/qunit-1.10.0.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 | -------------------------------------------------------------------------------- /spec/issues.js: -------------------------------------------------------------------------------- 1 | module('Integration tests'); 2 | 3 | test('Store', function() { 4 | function Product(data) { 5 | 6 | var viewModel = { 7 | guid: ko.observable(), 8 | name : ko.observable() 9 | }; 10 | 11 | ko.mapping.fromJS(data, {}, viewModel); 12 | 13 | return viewModel; 14 | } 15 | 16 | Store = function(data) { 17 | data = data || {}; 18 | var mapping = { 19 | Products: { 20 | key: function(data) { 21 | return ko.utils.unwrapObservable(data.guid); 22 | }, 23 | create: function(options) { 24 | return new Product(options.data); 25 | } 26 | }, 27 | 28 | Selected: { 29 | update: function(options) { 30 | return ko.utils.arrayFirst(viewModel.Products(), function(p) { 31 | return p.guid() == options.data.guid; 32 | }); 33 | } 34 | } 35 | }; 36 | 37 | var viewModel = { 38 | Products: ko.observableArray(), 39 | Selected : ko.observable() 40 | }; 41 | 42 | ko.mapping.fromJS(data, mapping, viewModel); 43 | 44 | return viewModel; 45 | }; 46 | 47 | var jsData = { 48 | "Products": [ 49 | { "guid": "01", "name": "Product1" }, 50 | { "guid": "02", "name": "Product2" }, 51 | { "guid": "03", "name": "Product3" } 52 | ], 53 | "Selected": { "guid": "02" } 54 | }; 55 | var viewModel = new Store(jsData); 56 | equal(viewModel.Selected().name(), "Product2"); 57 | }); 58 | 59 | //https://github.com/SteveSanderson/knockout.mapping/issues/34 60 | test('Issue #34', function() { 61 | var importData = function(dataArray, target) { 62 | var mapping = { 63 | "create": function( options ) { 64 | return options.data; 65 | } 66 | }; 67 | 68 | return ko.mapping.fromJS(dataArray, mapping, target); 69 | }; 70 | 71 | var viewModel = { 72 | things: ko.observableArray( [] ), 73 | load: function() { 74 | var rows = [ 75 | { id: 1 } 76 | ]; 77 | 78 | importData(rows, viewModel.things); 79 | } 80 | }; 81 | 82 | viewModel.load(); 83 | viewModel.load(); 84 | viewModel.load(); 85 | 86 | deepEqual(viewModel.things(), [{"id":1}]); 87 | }); 88 | 89 | test('Adding large amounts of items to array is slow', function() { 90 | expect(0); 91 | 92 | var numItems = 5000; 93 | var data = []; 94 | for (var t=0;t tag: use the global `ko` object, attaching a `mapping` property 12 | factory(ko, ko.mapping = {}); 13 | } 14 | }(function (ko, exports) { 15 | var DEBUG=true; 16 | var mappingProperty = "__ko_mapping__"; 17 | var realKoDependentObservable = ko.dependentObservable; 18 | var mappingNesting = 0; 19 | var dependentObservables; 20 | var visitedObjects; 21 | var recognizedRootProperties = ["create", "update", "key", "arrayChanged"]; 22 | var emptyReturn = {}; 23 | 24 | var _defaultOptions = { 25 | include: ["_destroy"], 26 | ignore: [], 27 | copy: [], 28 | observe: [] 29 | }; 30 | var defaultOptions = _defaultOptions; 31 | 32 | function unionArrays() { 33 | var args = arguments, 34 | l = args.length, 35 | obj = {}, 36 | res = [], 37 | i, j, k; 38 | 39 | while (l--) { 40 | k = args[l]; 41 | i = k.length; 42 | 43 | while (i--) { 44 | j = k[i]; 45 | if (!obj[j]) { 46 | obj[j] = 1; 47 | res.push(j); 48 | } 49 | } 50 | } 51 | 52 | return res; 53 | } 54 | 55 | function extendObject(destination, source) { 56 | var destType; 57 | 58 | for (var key in source) { 59 | if (source.hasOwnProperty(key) && source[key]) { 60 | destType = exports.getType(destination[key]); 61 | if (key && destination[key] && destType !== "array" && destType !== "string") { 62 | extendObject(destination[key], source[key]); 63 | } else { 64 | var bothArrays = exports.getType(destination[key]) === "array" && exports.getType(source[key]) === "array"; 65 | if (bothArrays) { 66 | destination[key] = unionArrays(destination[key], source[key]); 67 | } else { 68 | destination[key] = source[key]; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | function merge(obj1, obj2) { 76 | var merged = {}; 77 | extendObject(merged, obj1); 78 | extendObject(merged, obj2); 79 | 80 | return merged; 81 | } 82 | 83 | exports.isMapped = function (viewModel) { 84 | var unwrapped = ko.utils.unwrapObservable(viewModel); 85 | return unwrapped && unwrapped[mappingProperty]; 86 | } 87 | 88 | exports.fromJS = function (jsObject /*, inputOptions, target*/ ) { 89 | if (arguments.length == 0) throw new Error("When calling ko.fromJS, pass the object you want to convert."); 90 | 91 | try { 92 | if (!mappingNesting++) { 93 | dependentObservables = []; 94 | visitedObjects = new objectLookup(); 95 | } 96 | 97 | var options; 98 | var target; 99 | 100 | if (arguments.length == 2) { 101 | if (arguments[1][mappingProperty]) { 102 | target = arguments[1]; 103 | } else { 104 | options = arguments[1]; 105 | } 106 | } 107 | if (arguments.length == 3) { 108 | options = arguments[1]; 109 | target = arguments[2]; 110 | } 111 | 112 | if (target) { 113 | options = merge(options, target[mappingProperty]); 114 | } 115 | options = fillOptions(options); 116 | 117 | var result = updateViewModel(target, jsObject, options); 118 | if (target) { 119 | result = target; 120 | } 121 | 122 | // Evaluate any dependent observables that were proxied. 123 | // Do this after the model's observables have been created 124 | if (!--mappingNesting) { 125 | while (dependentObservables.length) { 126 | var DO = dependentObservables.pop(); 127 | if (DO) { 128 | DO(); 129 | 130 | // Move this magic property to the underlying dependent observable 131 | DO.__DO["throttleEvaluation"] = DO["throttleEvaluation"]; 132 | } 133 | } 134 | } 135 | 136 | // Save any new mapping options in the view model, so that updateFromJS can use them later. 137 | result[mappingProperty] = merge(result[mappingProperty], options); 138 | 139 | return result; 140 | } catch(e) { 141 | mappingNesting = 0; 142 | throw e; 143 | } 144 | }; 145 | 146 | exports.fromJSON = function (jsonString /*, options, target*/ ) { 147 | var parsed = ko.utils.parseJson(jsonString); 148 | arguments[0] = parsed; 149 | return exports.fromJS.apply(this, arguments); 150 | }; 151 | 152 | exports.updateFromJS = function (viewModel) { 153 | throw new Error("ko.mapping.updateFromJS, use ko.mapping.fromJS instead. Please note that the order of parameters is different!"); 154 | }; 155 | 156 | exports.updateFromJSON = function (viewModel) { 157 | throw new Error("ko.mapping.updateFromJSON, use ko.mapping.fromJSON instead. Please note that the order of parameters is different!"); 158 | }; 159 | 160 | exports.toJS = function (rootObject, options) { 161 | if (!defaultOptions) exports.resetDefaultOptions(); 162 | 163 | if (arguments.length == 0) throw new Error("When calling ko.mapping.toJS, pass the object you want to convert."); 164 | if (exports.getType(defaultOptions.ignore) !== "array") throw new Error("ko.mapping.defaultOptions().ignore should be an array."); 165 | if (exports.getType(defaultOptions.include) !== "array") throw new Error("ko.mapping.defaultOptions().include should be an array."); 166 | if (exports.getType(defaultOptions.copy) !== "array") throw new Error("ko.mapping.defaultOptions().copy should be an array."); 167 | 168 | // Merge in the options used in fromJS 169 | options = fillOptions(options, rootObject[mappingProperty]); 170 | 171 | // We just unwrap everything at every level in the object graph 172 | return exports.visitModel(rootObject, function (x) { 173 | return ko.utils.unwrapObservable(x) 174 | }, options); 175 | }; 176 | 177 | exports.toJSON = function (rootObject, options) { 178 | var plainJavaScriptObject = exports.toJS(rootObject, options); 179 | return ko.utils.stringifyJson(plainJavaScriptObject); 180 | }; 181 | 182 | exports.defaultOptions = function () { 183 | if (arguments.length > 0) { 184 | defaultOptions = arguments[0]; 185 | } else { 186 | return defaultOptions; 187 | } 188 | }; 189 | 190 | exports.resetDefaultOptions = function () { 191 | defaultOptions = { 192 | include: _defaultOptions.include.slice(0), 193 | ignore: _defaultOptions.ignore.slice(0), 194 | copy: _defaultOptions.copy.slice(0), 195 | observe: _defaultOptions.observe.slice(0) 196 | }; 197 | }; 198 | 199 | exports.getType = function(x) { 200 | if ((x) && (typeof (x) === "object")) { 201 | if (x.constructor === Date) return "date"; 202 | if (x.constructor === Array) return "array"; 203 | } 204 | return typeof x; 205 | } 206 | 207 | function fillOptions(rawOptions, otherOptions) { 208 | var options = merge({}, rawOptions); 209 | 210 | // Move recognized root-level properties into a root namespace 211 | for (var i = recognizedRootProperties.length - 1; i >= 0; i--) { 212 | var property = recognizedRootProperties[i]; 213 | 214 | // Carry on, unless this property is present 215 | if (!options[property]) continue; 216 | 217 | // Move the property into the root namespace 218 | if (!(options[""] instanceof Object)) options[""] = {}; 219 | options[""][property] = options[property]; 220 | delete options[property]; 221 | } 222 | 223 | if (otherOptions) { 224 | options.ignore = mergeArrays(otherOptions.ignore, options.ignore); 225 | options.include = mergeArrays(otherOptions.include, options.include); 226 | options.copy = mergeArrays(otherOptions.copy, options.copy); 227 | options.observe = mergeArrays(otherOptions.observe, options.observe); 228 | } 229 | options.ignore = mergeArrays(options.ignore, defaultOptions.ignore); 230 | options.include = mergeArrays(options.include, defaultOptions.include); 231 | options.copy = mergeArrays(options.copy, defaultOptions.copy); 232 | options.observe = mergeArrays(options.observe, defaultOptions.observe); 233 | 234 | options.mappedProperties = options.mappedProperties || {}; 235 | options.copiedProperties = options.copiedProperties || {}; 236 | return options; 237 | } 238 | 239 | function mergeArrays(a, b) { 240 | if (exports.getType(a) !== "array") { 241 | if (exports.getType(a) === "undefined") a = []; 242 | else a = [a]; 243 | } 244 | if (exports.getType(b) !== "array") { 245 | if (exports.getType(b) === "undefined") b = []; 246 | else b = [b]; 247 | } 248 | 249 | return ko.utils.arrayGetDistinctValues(a.concat(b)); 250 | } 251 | 252 | // When using a 'create' callback, we proxy the dependent observable so that it doesn't immediately evaluate on creation. 253 | // The reason is that the dependent observables in the user-specified callback may contain references to properties that have not been mapped yet. 254 | function withProxyDependentObservable(dependentObservables, callback) { 255 | var localDO = ko.dependentObservable; 256 | ko.dependentObservable = function (read, owner, options) { 257 | options = options || {}; 258 | 259 | if (read && typeof read == "object") { // mirrors condition in knockout implementation of DO's 260 | options = read; 261 | } 262 | 263 | var realDeferEvaluation = options.deferEvaluation; 264 | 265 | var isRemoved = false; 266 | 267 | // We wrap the original dependent observable so that we can remove it from the 'dependentObservables' list we need to evaluate after mapping has 268 | // completed if the user already evaluated the DO themselves in the meantime. 269 | var wrap = function (DO) { 270 | // Temporarily revert ko.dependentObservable, since it is used in ko.isWriteableObservable 271 | var tmp = ko.dependentObservable; 272 | ko.dependentObservable = realKoDependentObservable; 273 | var isWriteable = ko.isWriteableObservable(DO); 274 | ko.dependentObservable = tmp; 275 | 276 | var wrapped = realKoDependentObservable({ 277 | read: function () { 278 | if (!isRemoved) { 279 | ko.utils.arrayRemoveItem(dependentObservables, DO); 280 | isRemoved = true; 281 | } 282 | return DO.apply(DO, arguments); 283 | }, 284 | write: isWriteable && function (val) { 285 | return DO(val); 286 | }, 287 | deferEvaluation: true 288 | }); 289 | if (DEBUG) wrapped._wrapper = true; 290 | wrapped.__DO = DO; 291 | return wrapped; 292 | }; 293 | 294 | options.deferEvaluation = true; // will either set for just options, or both read/options. 295 | var realDependentObservable = new realKoDependentObservable(read, owner, options); 296 | 297 | if (!realDeferEvaluation) { 298 | realDependentObservable = wrap(realDependentObservable); 299 | dependentObservables.push(realDependentObservable); 300 | } 301 | 302 | return realDependentObservable; 303 | } 304 | ko.dependentObservable.fn = realKoDependentObservable.fn; 305 | ko.computed = ko.dependentObservable; 306 | var result = callback(); 307 | ko.dependentObservable = localDO; 308 | ko.computed = ko.dependentObservable; 309 | return result; 310 | } 311 | 312 | function updateViewModel(mappedRootObject, rootObject, options, parentName, parent, parentPropertyName, mappedParent) { 313 | var isArray = exports.getType(ko.utils.unwrapObservable(rootObject)) === "array"; 314 | 315 | parentPropertyName = parentPropertyName || ""; 316 | 317 | // If this object was already mapped previously, take the options from there and merge them with our existing ones. 318 | if (exports.isMapped(mappedRootObject)) { 319 | var previousMapping = ko.utils.unwrapObservable(mappedRootObject)[mappingProperty]; 320 | options = merge(previousMapping, options); 321 | } 322 | 323 | var callbackParams = { 324 | data: rootObject, 325 | parent: mappedParent || parent 326 | }; 327 | 328 | var hasCreateCallback = function () { 329 | return options[parentName] && options[parentName].create instanceof Function; 330 | }; 331 | 332 | var createCallback = function (data) { 333 | return withProxyDependentObservable(dependentObservables, function () { 334 | 335 | if (ko.utils.unwrapObservable(parent) instanceof Array) { 336 | return options[parentName].create({ 337 | data: data || callbackParams.data, 338 | parent: callbackParams.parent, 339 | skip: emptyReturn 340 | }); 341 | } else { 342 | return options[parentName].create({ 343 | data: data || callbackParams.data, 344 | parent: callbackParams.parent 345 | }); 346 | } 347 | }); 348 | }; 349 | 350 | var hasUpdateCallback = function () { 351 | return options[parentName] && options[parentName].update instanceof Function; 352 | }; 353 | 354 | var updateCallback = function (obj, data) { 355 | var params = { 356 | data: data || callbackParams.data, 357 | parent: callbackParams.parent, 358 | target: ko.utils.unwrapObservable(obj) 359 | }; 360 | 361 | if (ko.isWriteableObservable(obj)) { 362 | params.observable = obj; 363 | } 364 | 365 | return options[parentName].update(params); 366 | } 367 | 368 | var alreadyMapped = visitedObjects.get(rootObject); 369 | if (alreadyMapped) { 370 | return alreadyMapped; 371 | } 372 | 373 | parentName = parentName || ""; 374 | 375 | if (!isArray) { 376 | // For atomic types, do a direct update on the observable 377 | if (!canHaveProperties(rootObject)) { 378 | switch (exports.getType(rootObject)) { 379 | case "function": 380 | if (hasUpdateCallback()) { 381 | if (ko.isWriteableObservable(rootObject)) { 382 | rootObject(updateCallback(rootObject)); 383 | mappedRootObject = rootObject; 384 | } else { 385 | mappedRootObject = updateCallback(rootObject); 386 | } 387 | } else { 388 | mappedRootObject = rootObject; 389 | } 390 | break; 391 | default: 392 | if (ko.isWriteableObservable(mappedRootObject)) { 393 | if (hasUpdateCallback()) { 394 | var valueToWrite = updateCallback(mappedRootObject); 395 | mappedRootObject(valueToWrite); 396 | return valueToWrite; 397 | } else { 398 | var valueToWrite = ko.utils.unwrapObservable(rootObject); 399 | mappedRootObject(valueToWrite); 400 | return valueToWrite; 401 | } 402 | } else { 403 | var hasCreateOrUpdateCallback = hasCreateCallback() || hasUpdateCallback(); 404 | 405 | if (hasCreateCallback()) { 406 | mappedRootObject = createCallback(); 407 | } else { 408 | mappedRootObject = ko.observable(ko.utils.unwrapObservable(rootObject)); 409 | } 410 | 411 | if (hasUpdateCallback()) { 412 | mappedRootObject(updateCallback(mappedRootObject)); 413 | } 414 | 415 | if (hasCreateOrUpdateCallback) return mappedRootObject; 416 | } 417 | } 418 | 419 | } else { 420 | mappedRootObject = ko.utils.unwrapObservable(mappedRootObject); 421 | if (!mappedRootObject) { 422 | if (hasCreateCallback()) { 423 | var result = createCallback(); 424 | 425 | if (hasUpdateCallback()) { 426 | result = updateCallback(result); 427 | } 428 | 429 | return result; 430 | } else { 431 | if (hasUpdateCallback()) { 432 | return updateCallback(result); 433 | } 434 | 435 | mappedRootObject = {}; 436 | } 437 | } 438 | 439 | if (hasUpdateCallback()) { 440 | mappedRootObject = updateCallback(mappedRootObject); 441 | } 442 | 443 | visitedObjects.save(rootObject, mappedRootObject); 444 | if (hasUpdateCallback()) return mappedRootObject; 445 | 446 | // For non-atomic types, visit all properties and update recursively 447 | visitPropertiesOrArrayEntries(rootObject, function (indexer) { 448 | var fullPropertyName = parentPropertyName.length ? parentPropertyName + "." + indexer : indexer; 449 | 450 | if (ko.utils.arrayIndexOf(options.ignore, fullPropertyName) != -1) { 451 | return; 452 | } 453 | 454 | if (ko.utils.arrayIndexOf(options.copy, fullPropertyName) != -1) { 455 | mappedRootObject[indexer] = rootObject[indexer]; 456 | return; 457 | } 458 | 459 | if(typeof rootObject[indexer] != "object" && typeof rootObject[indexer] != "array" && options.observe.length > 0 && ko.utils.arrayIndexOf(options.observe, fullPropertyName) == -1) 460 | { 461 | mappedRootObject[indexer] = rootObject[indexer]; 462 | options.copiedProperties[fullPropertyName] = true; 463 | return; 464 | } 465 | 466 | // In case we are adding an already mapped property, fill it with the previously mapped property value to prevent recursion. 467 | // If this is a property that was generated by fromJS, we should use the options specified there 468 | var prevMappedProperty = visitedObjects.get(rootObject[indexer]); 469 | var retval = updateViewModel(mappedRootObject[indexer], rootObject[indexer], options, indexer, mappedRootObject, fullPropertyName, mappedRootObject); 470 | var value = prevMappedProperty || retval; 471 | 472 | if(options.observe.length > 0 && ko.utils.arrayIndexOf(options.observe, fullPropertyName) == -1) 473 | { 474 | mappedRootObject[indexer] = ko.utils.unwrapObservable(value); 475 | options.copiedProperties[fullPropertyName] = true; 476 | return; 477 | } 478 | 479 | if (ko.isWriteableObservable(mappedRootObject[indexer])) { 480 | value = ko.utils.unwrapObservable(value); 481 | if (mappedRootObject[indexer]() !== value) { 482 | mappedRootObject[indexer](value); 483 | } 484 | } else { 485 | value = mappedRootObject[indexer] === undefined ? value : ko.utils.unwrapObservable(value); 486 | mappedRootObject[indexer] = value; 487 | } 488 | 489 | options.mappedProperties[fullPropertyName] = true; 490 | }); 491 | } 492 | } else { //mappedRootObject is an array 493 | var changes = []; 494 | 495 | var hasKeyCallback = false; 496 | var keyCallback = function (x) { 497 | return x; 498 | } 499 | if (options[parentName] && options[parentName].key) { 500 | keyCallback = options[parentName].key; 501 | hasKeyCallback = true; 502 | } 503 | 504 | if (!ko.isObservable(mappedRootObject)) { 505 | // When creating the new observable array, also add a bunch of utility functions that take the 'key' of the array items into account. 506 | mappedRootObject = ko.observableArray([]); 507 | 508 | mappedRootObject.mappedRemove = function (valueOrPredicate) { 509 | var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { 510 | return value === keyCallback(valueOrPredicate); 511 | }; 512 | return mappedRootObject.remove(function (item) { 513 | return predicate(keyCallback(item)); 514 | }); 515 | } 516 | 517 | mappedRootObject.mappedRemoveAll = function (arrayOfValues) { 518 | var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback); 519 | return mappedRootObject.remove(function (item) { 520 | return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1; 521 | }); 522 | } 523 | 524 | mappedRootObject.mappedDestroy = function (valueOrPredicate) { 525 | var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { 526 | return value === keyCallback(valueOrPredicate); 527 | }; 528 | return mappedRootObject.destroy(function (item) { 529 | return predicate(keyCallback(item)); 530 | }); 531 | } 532 | 533 | mappedRootObject.mappedDestroyAll = function (arrayOfValues) { 534 | var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback); 535 | return mappedRootObject.destroy(function (item) { 536 | return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1; 537 | }); 538 | } 539 | 540 | mappedRootObject.mappedIndexOf = function (item) { 541 | var keys = filterArrayByKey(mappedRootObject(), keyCallback); 542 | var key = keyCallback(item); 543 | return ko.utils.arrayIndexOf(keys, key); 544 | } 545 | 546 | mappedRootObject.mappedGet = function (item) { 547 | return mappedRootObject()[mappedRootObject.mappedIndexOf(item)]; 548 | } 549 | 550 | mappedRootObject.mappedCreate = function (value) { 551 | if (mappedRootObject.mappedIndexOf(value) !== -1) { 552 | throw new Error("There already is an object with the key that you specified."); 553 | } 554 | 555 | var item = hasCreateCallback() ? createCallback(value) : value; 556 | if (hasUpdateCallback()) { 557 | var newValue = updateCallback(item, value); 558 | if (ko.isWriteableObservable(item)) { 559 | item(newValue); 560 | } else { 561 | item = newValue; 562 | } 563 | } 564 | mappedRootObject.push(item); 565 | return item; 566 | } 567 | } 568 | 569 | var currentArrayKeys = filterArrayByKey(ko.utils.unwrapObservable(mappedRootObject), keyCallback).sort(); 570 | var newArrayKeys = filterArrayByKey(rootObject, keyCallback); 571 | if (hasKeyCallback) newArrayKeys.sort(); 572 | var editScript = ko.utils.compareArrays(currentArrayKeys, newArrayKeys); 573 | 574 | var ignoreIndexOf = {}; 575 | 576 | var i, j; 577 | 578 | var unwrappedRootObject = ko.utils.unwrapObservable(rootObject); 579 | var itemsByKey = {}; 580 | var optimizedKeys = true; 581 | for (i = 0, j = unwrappedRootObject.length; i < j; i++) { 582 | var key = keyCallback(unwrappedRootObject[i]); 583 | if (key === undefined || key instanceof Object) { 584 | optimizedKeys = false; 585 | break; 586 | } 587 | itemsByKey[key] = unwrappedRootObject[i]; 588 | } 589 | 590 | var newContents = []; 591 | var passedOver = 0; 592 | for (i = 0, j = editScript.length; i < j; i++) { 593 | var key = editScript[i]; 594 | var mappedItem; 595 | var fullPropertyName = parentPropertyName + "[" + i + "]"; 596 | switch (key.status) { 597 | case "added": 598 | var item = optimizedKeys ? itemsByKey[key.value] : getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback); 599 | mappedItem = updateViewModel(undefined, item, options, parentName, mappedRootObject, fullPropertyName, parent); 600 | if(!hasCreateCallback()) { 601 | mappedItem = ko.utils.unwrapObservable(mappedItem); 602 | } 603 | 604 | var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf); 605 | 606 | if (mappedItem === emptyReturn) { 607 | passedOver++; 608 | } else { 609 | newContents[index - passedOver] = mappedItem; 610 | } 611 | 612 | ignoreIndexOf[index] = true; 613 | break; 614 | case "retained": 615 | var item = optimizedKeys ? itemsByKey[key.value] : getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback); 616 | mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback); 617 | updateViewModel(mappedItem, item, options, parentName, mappedRootObject, fullPropertyName, parent); 618 | 619 | var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf); 620 | newContents[index] = mappedItem; 621 | ignoreIndexOf[index] = true; 622 | break; 623 | case "deleted": 624 | mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback); 625 | break; 626 | } 627 | 628 | changes.push({ 629 | event: key.status, 630 | item: mappedItem 631 | }); 632 | } 633 | 634 | mappedRootObject(newContents); 635 | 636 | if (options[parentName] && options[parentName].arrayChanged) { 637 | ko.utils.arrayForEach(changes, function (change) { 638 | options[parentName].arrayChanged(change.event, change.item); 639 | }); 640 | } 641 | } 642 | 643 | return mappedRootObject; 644 | } 645 | 646 | function ignorableIndexOf(array, item, ignoreIndices) { 647 | for (var i = 0, j = array.length; i < j; i++) { 648 | if (ignoreIndices[i] === true) continue; 649 | if (array[i] === item) return i; 650 | } 651 | return null; 652 | } 653 | 654 | function mapKey(item, callback) { 655 | var mappedItem; 656 | if (callback) mappedItem = callback(item); 657 | if (exports.getType(mappedItem) === "undefined") mappedItem = item; 658 | 659 | return ko.utils.unwrapObservable(mappedItem); 660 | } 661 | 662 | function getItemByKey(array, key, callback) { 663 | array = ko.utils.unwrapObservable(array); 664 | for (var i = 0, j = array.length; i < j; i++) { 665 | var item = array[i]; 666 | if (mapKey(item, callback) === key) return item; 667 | } 668 | 669 | throw new Error("When calling ko.update*, the key '" + key + "' was not found!"); 670 | } 671 | 672 | function filterArrayByKey(array, callback) { 673 | return ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (item) { 674 | if (callback) { 675 | return mapKey(item, callback); 676 | } else { 677 | return item; 678 | } 679 | }); 680 | } 681 | 682 | function visitPropertiesOrArrayEntries(rootObject, visitorCallback) { 683 | if (exports.getType(rootObject) === "array") { 684 | for (var i = 0; i < rootObject.length; i++) 685 | visitorCallback(i); 686 | } else { 687 | for (var propertyName in rootObject) 688 | visitorCallback(propertyName); 689 | } 690 | }; 691 | 692 | function canHaveProperties(object) { 693 | var type = exports.getType(object); 694 | return ((type === "object") || (type === "array")) && (object !== null); 695 | } 696 | 697 | // Based on the parentName, this creates a fully classified name of a property 698 | 699 | function getPropertyName(parentName, parent, indexer) { 700 | var propertyName = parentName || ""; 701 | if (exports.getType(parent) === "array") { 702 | if (parentName) { 703 | propertyName += "[" + indexer + "]"; 704 | } 705 | } else { 706 | if (parentName) { 707 | propertyName += "."; 708 | } 709 | propertyName += indexer; 710 | } 711 | return propertyName; 712 | } 713 | 714 | exports.visitModel = function (rootObject, callback, options) { 715 | options = options || {}; 716 | options.visitedObjects = options.visitedObjects || new objectLookup(); 717 | 718 | var mappedRootObject; 719 | var unwrappedRootObject = ko.utils.unwrapObservable(rootObject); 720 | 721 | if (!canHaveProperties(unwrappedRootObject)) { 722 | return callback(rootObject, options.parentName); 723 | } else { 724 | options = fillOptions(options, unwrappedRootObject[mappingProperty]); 725 | 726 | // Only do a callback, but ignore the results 727 | callback(rootObject, options.parentName); 728 | mappedRootObject = exports.getType(unwrappedRootObject) === "array" ? [] : {}; 729 | } 730 | 731 | options.visitedObjects.save(rootObject, mappedRootObject); 732 | 733 | var parentName = options.parentName; 734 | visitPropertiesOrArrayEntries(unwrappedRootObject, function (indexer) { 735 | if (options.ignore && ko.utils.arrayIndexOf(options.ignore, indexer) != -1) return; 736 | 737 | var propertyValue = unwrappedRootObject[indexer]; 738 | options.parentName = getPropertyName(parentName, unwrappedRootObject, indexer); 739 | 740 | // If we don't want to explicitly copy the unmapped property... 741 | if (ko.utils.arrayIndexOf(options.copy, indexer) === -1) { 742 | // ...find out if it's a property we want to explicitly include 743 | if (ko.utils.arrayIndexOf(options.include, indexer) === -1) { 744 | // The mapped properties object contains all the properties that were part of the original object. 745 | // If a property does not exist, and it is not because it is part of an array (e.g. "myProp[3]"), then it should not be unmapped. 746 | if (unwrappedRootObject[mappingProperty] 747 | && unwrappedRootObject[mappingProperty].mappedProperties && !unwrappedRootObject[mappingProperty].mappedProperties[indexer] 748 | && unwrappedRootObject[mappingProperty].copiedProperties && !unwrappedRootObject[mappingProperty].copiedProperties[indexer] 749 | && !(exports.getType(unwrappedRootObject) === "array")) { 750 | return; 751 | } 752 | } 753 | } 754 | 755 | var outputProperty; 756 | switch (exports.getType(ko.utils.unwrapObservable(propertyValue))) { 757 | case "object": 758 | case "array": 759 | case "undefined": 760 | var previouslyMappedValue = options.visitedObjects.get(propertyValue); 761 | mappedRootObject[indexer] = (exports.getType(previouslyMappedValue) !== "undefined") ? previouslyMappedValue : exports.visitModel(propertyValue, callback, options); 762 | break; 763 | default: 764 | mappedRootObject[indexer] = callback(propertyValue, options.parentName); 765 | } 766 | }); 767 | 768 | return mappedRootObject; 769 | } 770 | 771 | function simpleObjectLookup() { 772 | var keys = []; 773 | var values = []; 774 | this.save = function (key, value) { 775 | var existingIndex = ko.utils.arrayIndexOf(keys, key); 776 | if (existingIndex >= 0) values[existingIndex] = value; 777 | else { 778 | keys.push(key); 779 | values.push(value); 780 | } 781 | }; 782 | this.get = function (key) { 783 | var existingIndex = ko.utils.arrayIndexOf(keys, key); 784 | var value = (existingIndex >= 0) ? values[existingIndex] : undefined; 785 | return value; 786 | }; 787 | }; 788 | 789 | function objectLookup() { 790 | var buckets = {}; 791 | 792 | var findBucket = function(key) { 793 | var bucketKey; 794 | try { 795 | bucketKey = key;//JSON.stringify(key); 796 | } 797 | catch (e) { 798 | bucketKey = "$$$"; 799 | } 800 | 801 | var bucket = buckets[bucketKey]; 802 | if (bucket === undefined) { 803 | bucket = new simpleObjectLookup(); 804 | buckets[bucketKey] = bucket; 805 | } 806 | return bucket; 807 | }; 808 | 809 | this.save = function (key, value) { 810 | findBucket(key).save(key, value); 811 | }; 812 | this.get = function (key) { 813 | return findBucket(key).get(key); 814 | }; 815 | }; 816 | })); 817 | -------------------------------------------------------------------------------- /spec/mappingBehaviors.js: -------------------------------------------------------------------------------- 1 | module('Mapping'); 2 | 3 | test('ko.mapping.toJS should unwrap observable values', function () { 4 | var atomicValues = ["hello", 123, true, null, undefined, 5 | { 6 | a: 1 7 | }]; 8 | for (var i = 0; i < atomicValues.length; i++) { 9 | var data = ko.observable(atomicValues[i]); 10 | var result = ko.mapping.toJS(data); 11 | equal(ko.isObservable(result), false); 12 | deepEqual(result, atomicValues[i]); 13 | } 14 | }); 15 | 16 | test('ko.mapping.toJS should unwrap observable properties, including nested ones', function () { 17 | var data = { 18 | a: ko.observable(123), 19 | b: { 20 | b1: ko.observable(456), 21 | b2: 789 22 | } 23 | }; 24 | var result = ko.mapping.toJS(data); 25 | equal(result.a, 123); 26 | equal(result.b.b1, 456); 27 | equal(result.b.b2, 789); 28 | }); 29 | 30 | test('ko.mapping.toJS should unwrap observable arrays and things inside them', function () { 31 | var data = ko.observableArray(['a', 1, 32 | { 33 | someProp: ko.observable('Hey') 34 | }]); 35 | var result = ko.mapping.toJS(data); 36 | equal(result.length, 3); 37 | equal(result[0], 'a'); 38 | equal(result[1], 1); 39 | equal(result[2].someProp, 'Hey'); 40 | }); 41 | 42 | test('ko.mapping.toJS should ignore specified single property', function() { 43 | var data = { 44 | a: "a", 45 | b: "b" 46 | }; 47 | 48 | var result = ko.mapping.toJS(data, { ignore: "b" }); 49 | equal(result.a, "a"); 50 | equal(result.b, undefined); 51 | }); 52 | 53 | test('ko.mapping.toJS should ignore specified single property on update', function() { 54 | var data = { 55 | a: "a", 56 | b: "b", 57 | c: "c" 58 | }; 59 | 60 | var result = ko.mapping.fromJS(data); 61 | equal(result.a(), "a"); 62 | equal(result.b(), "b"); 63 | equal(result.c(), "c"); 64 | ko.mapping.fromJS({ a: "a2", b: "b2", c: "c2" }, { ignore: ["b", "c"] }, result); 65 | equal(result.a(), "a2"); 66 | equal(result.b(), "b"); 67 | equal(result.c(), "c"); 68 | }); 69 | 70 | test('ko.mapping.toJS should ignore specified multiple properties', function() { 71 | var data = { 72 | a: { a1: "a1", a2: "a2" }, 73 | b: "b" 74 | }; 75 | 76 | var result = ko.mapping.fromJS(data, { ignore: ["a.a1", "b"] }); 77 | equal(result.a.a1, undefined); 78 | equal(result.a.a2(), "a2"); 79 | equal(result.b, undefined); 80 | 81 | data.a.a1 = "a11"; 82 | data.a.a2 = "a22"; 83 | ko.mapping.fromJS(data, {}, result); 84 | equal(result.a.a1, undefined); 85 | equal(result.a.a2(), "a22"); 86 | equal(result.b, undefined); 87 | }); 88 | 89 | test('ko.mapping.fromJS should ignore specified single property', function() { 90 | var data = { 91 | a: "a", 92 | b: "b" 93 | }; 94 | 95 | var result = ko.mapping.fromJS(data, { ignore: "b" }); 96 | equal(result.a(), "a"); 97 | equal(result.b, undefined); 98 | }); 99 | 100 | test('ko.mapping.fromJS should ignore specified array item', function() { 101 | var data = { 102 | a: "a", 103 | b: [{ b1: "v1" }, { b2: "v2" }] 104 | }; 105 | 106 | var result = ko.mapping.fromJS(data, { ignore: "b[1].b2" }); 107 | equal(result.a(), "a"); 108 | equal(result.b()[0].b1(), "v1"); 109 | equal(result.b()[1].b2, undefined); 110 | }); 111 | 112 | test('ko.mapping.fromJS should ignore specified single property, also when going back .toJS', function() { 113 | var data = { 114 | a: "a", 115 | b: "b" 116 | }; 117 | 118 | var result = ko.mapping.fromJS(data, { ignore: "b" }); 119 | var js = ko.mapping.toJS(result); 120 | equal(js.a, "a"); 121 | equal(js.b, undefined); 122 | }); 123 | 124 | test('ko.mapping.fromJS should copy specified single property', function() { 125 | var data = { 126 | a: "a", 127 | b: "b" 128 | }; 129 | 130 | var result = ko.mapping.fromJS(data, { copy: "b" }); 131 | equal(result.a(), "a"); 132 | equal(result.b, "b"); 133 | }); 134 | 135 | test('ko.mapping.fromJS should copy specified array', function() { 136 | var data = { 137 | a: "a", 138 | b: ["b1", "b2"] 139 | }; 140 | 141 | var result = ko.mapping.fromJS(data, { copy: "b" }); 142 | equal(result.a(), "a"); 143 | deepEqual(result.b, ["b1", "b2"]); 144 | }); 145 | 146 | test('ko.mapping.fromJS should copy specified array item', function() { 147 | var data = { 148 | a: "a", 149 | b: [{ b1: "v1" }, { b2: "v2" }] 150 | }; 151 | 152 | var result = ko.mapping.fromJS(data, { copy: "b[0].b1" }); 153 | equal(result.a(), "a"); 154 | equal(result.b()[0].b1, "v1"); 155 | equal(result.b()[1].b2(), "v2"); 156 | }); 157 | 158 | test('ko.mapping.fromJS should copy specified single property, also when going back .toJS', function() { 159 | var data = { 160 | a: "a", 161 | b: "b" 162 | }; 163 | 164 | var result = ko.mapping.fromJS(data, { copy: "b" }); 165 | var js = ko.mapping.toJS(result); 166 | equal(js.a, "a"); 167 | equal(js.b, "b"); 168 | }); 169 | 170 | test('ko.mapping.fromJS should copy specified single property, also when going back .toJS, except when overridden', function() { 171 | var data = { 172 | a: "a", 173 | b: "b" 174 | }; 175 | 176 | var result = ko.mapping.fromJS(data, { copy: "b" }); 177 | var js = ko.mapping.toJS(result, { ignore: "b" }); 178 | equal(js.a, "a"); 179 | equal(js.b, undefined); 180 | }); 181 | 182 | test('ko.mapping.toJS should include specified single property', function() { 183 | var data = { 184 | a: "a" 185 | }; 186 | 187 | var mapped = ko.mapping.fromJS(data); 188 | mapped.c = 1; 189 | mapped.d = 2; 190 | var result = ko.mapping.toJS(mapped, { include: "c" }); 191 | equal(result.a, "a"); 192 | equal(result.c, 1); 193 | equal(result.d, undefined); 194 | }); 195 | 196 | test('ko.mapping.toJS should by default ignore the mapping property', function() { 197 | var data = { 198 | a: "a", 199 | b: "b" 200 | }; 201 | 202 | var fromJS = ko.mapping.fromJS(data); 203 | var result = ko.mapping.toJS(fromJS); 204 | equal(result.a, "a"); 205 | equal(result.b, "b"); 206 | equal(result.__ko_mapping__, undefined); 207 | }); 208 | 209 | test('ko.mapping.toJS should by default include the _destroy property', function() { 210 | var data = { 211 | a: "a" 212 | }; 213 | 214 | var fromJS = ko.mapping.fromJS(data); 215 | fromJS._destroy = true; 216 | var result = ko.mapping.toJS(fromJS); 217 | equal(result.a, "a"); 218 | equal(result._destroy, true); 219 | }); 220 | 221 | test('ko.mapping.toJS should merge the default includes', function() { 222 | var data = { 223 | a: "a" 224 | }; 225 | 226 | var fromJS = ko.mapping.fromJS(data); 227 | fromJS.b = "b"; 228 | fromJS._destroy = true; 229 | var result = ko.mapping.toJS(fromJS, { include: "b" }); 230 | equal(result.a, "a"); 231 | equal(result.b, "b"); 232 | equal(result._destroy, true); 233 | }); 234 | 235 | test('ko.mapping.toJS should merge the default ignores', function() { 236 | var data = { 237 | a: "a", 238 | b: "b", 239 | c: "c" 240 | }; 241 | 242 | ko.mapping.defaultOptions().ignore = ["a"]; 243 | var fromJS = ko.mapping.fromJS(data); 244 | var result = ko.mapping.toJS(fromJS, { ignore: "b" }); 245 | equal(result.a, undefined); 246 | equal(result.b, undefined); 247 | equal(result.c, "c"); 248 | }); 249 | 250 | test('ko.mapping.defaultOptions should by default include the _destroy property', function() { 251 | notEqual(ko.utils.arrayIndexOf(ko.mapping.defaultOptions().include, "_destroy"), -1); 252 | }); 253 | 254 | test('ko.mapping.defaultOptions.include should be an array', function() { 255 | var didThrow = false; 256 | try { 257 | ko.mapping.defaultOptions().include = {}; 258 | ko.mapping.toJS({}); 259 | } 260 | catch (ex) { 261 | didThrow = true 262 | } 263 | equal(didThrow, true); 264 | }); 265 | 266 | test('ko.mapping.defaultOptions.ignore should be an array', function() { 267 | var didThrow = false; 268 | try { 269 | ko.mapping.defaultOptions().ignore = {}; 270 | ko.mapping.toJS({}); 271 | } 272 | catch (ex) { 273 | didThrow = true 274 | } 275 | equal(didThrow, true); 276 | }); 277 | 278 | test('ko.mapping.defaultOptions can be set', function() { 279 | var oldOptions = ko.mapping.defaultOptions(); 280 | ko.mapping.defaultOptions({ a: "a" }); 281 | var newOptions = ko.mapping.defaultOptions(); 282 | ko.mapping.defaultOptions(oldOptions); 283 | equal(newOptions.a, "a"); 284 | }); 285 | 286 | test('recognized root-level options should be moved into a root namespace, leaving other options in place', function() { 287 | var recognizedRootProperties = ['create', 'update', 'key', 'arrayChanged']; 288 | 289 | // Zero out the default options so they don't interfere with this test 290 | ko.mapping.defaultOptions({}); 291 | 292 | // Set up a mapping with root and child mappings 293 | var mapping = { 294 | ignore: ['a'], 295 | copy: ['b'], 296 | include: ['c'], 297 | create: function(opts) { return opts.data; }, 298 | update: function(opts) { return opts.data; }, 299 | key: function(item) { return ko.utils.unwrapObservable(item.id); }, 300 | arrayChanged: function(event, item) { }, 301 | children: { 302 | ignore: ['a1'], 303 | copy: ['b1'], 304 | include: ['c1'], 305 | create: function(opts) { return opts.data; }, 306 | update: function(opts) { return opts.data; }, 307 | key: function(item) { return ko.utils.unwrapObservable(item.id); }, 308 | arrayChanged: function(event, item) { } 309 | } 310 | }; 311 | 312 | // Run the mapping through ko.mapping.fromJS 313 | var resultantMapping = ko.mapping.fromJS({}, mapping).__ko_mapping__; 314 | 315 | // Test that the recognized root-level mappings were moved into a root-level namespace 316 | for(var i=recognizedRootProperties.length-1; i>=0; i--) { 317 | notDeepEqual(resultantMapping[recognizedRootProperties[i]], mapping[[recognizedRootProperties[i]]]); 318 | deepEqual(resultantMapping[''][recognizedRootProperties[i]], mapping[[recognizedRootProperties[i]]]); 319 | }; 320 | 321 | // Test that the non-recognized root-level and descendant mappings were left in place 322 | for(property in mapping) { 323 | window[recognizedRootProperties.indexOf(property) == -1 ? 'deepEqual' : 'notDeepEqual'](resultantMapping[property], mapping[property]); 324 | }; 325 | }); 326 | 327 | test('ko.mapping.toJS should ignore properties that were not part of the original model', function () { 328 | var data = { 329 | a: 123, 330 | b: { 331 | b1: 456, 332 | b2: [ 333 | "b21", "b22" 334 | ], 335 | } 336 | }; 337 | 338 | var mapped = ko.mapping.fromJS(data); 339 | mapped.extraProperty = ko.observable(333); 340 | mapped.extraFunction = function() {}; 341 | 342 | var unmapped = ko.mapping.toJS(mapped); 343 | equal(unmapped.a, 123); 344 | equal(unmapped.b.b1, 456); 345 | equal(unmapped.b.b2[0], "b21"); 346 | equal(unmapped.b.b2[1], "b22"); 347 | equal(unmapped.extraProperty, undefined); 348 | equal(unmapped.extraFunction, undefined); 349 | equal(unmapped.__ko_mapping__, undefined); 350 | }); 351 | 352 | test('ko.mapping.toJS should ignore properties that were not part of the original model when there are no nested create callbacks', function () { 353 | var data = [ 354 | { 355 | a: [{ id: "a1.1" }, { id: "a1.2" }] 356 | } 357 | ]; 358 | 359 | var mapped = ko.mapping.fromJS(data, { 360 | create: function(options) { 361 | return ko.mapping.fromJS(options.data); 362 | } 363 | }); 364 | mapped.extraProperty = ko.observable(333); 365 | mapped.extraFunction = function() {}; 366 | 367 | var unmapped = ko.mapping.toJS(mapped); 368 | equal(unmapped[0].a[0].id, "a1.1"); 369 | equal(unmapped[0].a[1].id, "a1.2"); 370 | equal(unmapped.extraProperty, undefined); 371 | equal(unmapped.extraFunction, undefined); 372 | equal(unmapped.__ko_mapping__, undefined); 373 | }); 374 | 375 | test('ko.mapping.toJS should ignore properties that were not part of the original model when there are nested create callbacks', function () { 376 | var data = [ 377 | { 378 | a: [{ id: "a1.1" }, { id: "a1.2" }] 379 | } 380 | ]; 381 | 382 | var nestedMappingOptions = { 383 | a: { 384 | create: function(options) { 385 | return ko.mapping.fromJS(options.data); 386 | } 387 | } 388 | }; 389 | 390 | var mapped = ko.mapping.fromJS(data, { 391 | create: function(options) { 392 | return ko.mapping.fromJS(options.data, nestedMappingOptions); 393 | } 394 | }); 395 | mapped.extraProperty = ko.observable(333); 396 | mapped.extraFunction = function() {}; 397 | 398 | var unmapped = ko.mapping.toJS(mapped); 399 | equal(unmapped[0].a[0].id, "a1.1"); 400 | equal(unmapped[0].a[1].id, "a1.2"); 401 | equal(unmapped.extraProperty, undefined); 402 | equal(unmapped.extraFunction, undefined); 403 | equal(unmapped.__ko_mapping__, undefined); 404 | }); 405 | 406 | test('ko.mapping.toJS should ignore specified properties', function() { 407 | var data = { 408 | a: "a", 409 | b: "b", 410 | c: "c" 411 | }; 412 | 413 | var result = ko.mapping.toJS(data, { ignore: ["b", "c"] }); 414 | equal(result.a, "a"); 415 | equal(result.b, undefined); 416 | equal(result.c, undefined); 417 | }); 418 | 419 | test('ko.mapping.toJSON should ignore specified properties', function() { 420 | var data = { 421 | a: "a", 422 | b: "b", 423 | c: "c" 424 | }; 425 | 426 | var result = ko.mapping.toJSON(data, { ignore: ["b", "c"] }); 427 | equal(result, "{\"a\":\"a\"}"); 428 | }); 429 | 430 | test('ko.mapping.toJSON should unwrap everything and then stringify', function () { 431 | var data = ko.observableArray(['a', 1, 432 | { 433 | someProp: ko.observable('Hey') 434 | }]); 435 | var result = ko.mapping.toJSON(data); 436 | 437 | // Check via parsing so the specs are independent of browser-specific JSON string formatting 438 | equal(typeof result, 'string'); 439 | var parsedResult = ko.utils.parseJson(result); 440 | equal(parsedResult.length, 3); 441 | equal(parsedResult[0], 'a'); 442 | equal(parsedResult[1], 1); 443 | equal(parsedResult[2].someProp, 'Hey'); 444 | }); 445 | 446 | test('ko.mapping.fromJS should require a parameter', function () { 447 | var didThrow = false; 448 | try { 449 | ko.mapping.fromJS() 450 | } 451 | catch (ex) { 452 | didThrow = true 453 | } 454 | equal(didThrow, true); 455 | }); 456 | 457 | test('ko.mapping.fromJS should return an observable if you supply an atomic value', function () { 458 | var atomicValues = ["hello", 123, true, null, undefined]; 459 | for (var i = 0; i < atomicValues.length; i++) { 460 | var result = ko.mapping.fromJS(atomicValues[i]); 461 | equal(ko.isObservable(result), true); 462 | equal(result(), atomicValues[i]); 463 | } 464 | }); 465 | 466 | test('ko.mapping.fromJS should be able to map into an existing object', function () { 467 | var existingObj = { 468 | a: "a" 469 | }; 470 | 471 | var obj = { 472 | b: "b" 473 | }; 474 | 475 | ko.mapping.fromJS(obj, {}, existingObj); 476 | 477 | equal(ko.isObservable(existingObj.a), false); 478 | equal(ko.isObservable(existingObj.b), true); 479 | equal(existingObj.a, "a"); 480 | equal(existingObj.b(), "b"); 481 | }); 482 | 483 | test('ko.mapping.fromJS should return an observableArray if you supply an array, but should not wrap its entries in further observables', function () { 484 | var sampleArray = ["a", "b"]; 485 | var result = ko.mapping.fromJS(sampleArray); 486 | equal(typeof result.destroyAll, 'function'); // Just an example of a function on ko.observableArray but not on Array 487 | equal(result().length, 2); 488 | equal(result()[0], "a"); 489 | equal(result()[1], "b"); 490 | }); 491 | 492 | test('ko.mapping.fromJS should return an observableArray if you supply an array, and leave entries as observables if there is a create mapping that does that', function () { 493 | var sampleArray = {array: ["a", "b"]}; 494 | var result = ko.mapping.fromJS(sampleArray, { 495 | array: { 496 | create: function(options) { 497 | return new ko.observable(options.data); 498 | } 499 | } 500 | }); 501 | equal(result.array().length, 2); 502 | equal(ko.isObservable(result.array()[0]),true); 503 | equal(ko.isObservable(result.array()[1]),true); 504 | equal(result.array()[0](), "a"); 505 | equal(result.array()[1](), "b"); 506 | }); 507 | 508 | test('ko.mapping.fromJS should not return an observable if you supply an object that could have properties', function () { 509 | equal(ko.isObservable(ko.mapping.fromJS({})), false); 510 | }); 511 | 512 | test('ko.mapping.fromJS should not wrap functions in an observable', function () { 513 | var result = ko.mapping.fromJS({}, { 514 | create: function(model) { 515 | return { 516 | myFunc: function() { 517 | return 123; 518 | } 519 | } 520 | } 521 | }); 522 | equal(result.myFunc(), 123); 523 | }); 524 | 525 | test('ko.mapping.fromJS update callbacks should pass in a non-observable', function () { 526 | var result = ko.mapping.fromJS({ 527 | obj: { a: "a" } 528 | }, { 529 | obj: { 530 | update: function(options) { 531 | equal(options.observable, undefined); 532 | return { b: "b" }; 533 | } 534 | } 535 | }); 536 | equal(result.obj.b, "b"); 537 | }); 538 | 539 | test('ko.mapping.fromJS update callbacks should pass in an observable, when original is also observable', function () { 540 | var result = ko.mapping.fromJS({ 541 | obj: ko.observable("a") 542 | }, { 543 | obj: { 544 | update: function(options) { 545 | return options.observable() + "ab"; 546 | } 547 | } 548 | }); 549 | equal(result.obj(), "aab"); 550 | }); 551 | 552 | test('ko.mapping.fromJS update callbacks should pass in an observable, when original is not observable', function () { 553 | var result = ko.mapping.fromJS({ 554 | obj: "a" 555 | }, { 556 | obj: { 557 | update: function(options) { 558 | return options.observable() + "ab"; 559 | } 560 | } 561 | }); 562 | equal(result.obj(), "aab"); 563 | }); 564 | 565 | test('ko.mapping.fromJS should map the top-level atomic properties on the supplied object as observables', function () { 566 | var result = ko.mapping.fromJS({ 567 | a: 123, 568 | b: 'Hello', 569 | c: true 570 | }); 571 | equal(ko.isObservable(result.a), true); 572 | equal(ko.isObservable(result.b), true); 573 | equal(ko.isObservable(result.c), true); 574 | equal(result.a(), 123); 575 | equal(result.b(), 'Hello'); 576 | equal(result.c(), true); 577 | }); 578 | 579 | test('ko.mapping.fromJS should not map the top-level non-atomic properties on the supplied object as observables', function () { 580 | var result = ko.mapping.fromJS({ 581 | a: { 582 | a1: "Hello" 583 | } 584 | }); 585 | equal(ko.isObservable(result.a), false); 586 | equal(ko.isObservable(result.a.a1), true); 587 | equal(result.a.a1(), 'Hello'); 588 | }); 589 | 590 | test('ko.mapping.fromJS should not map the top-level non-atomic properties on the supplied overriden model as observables', function () { 591 | var result = ko.mapping.fromJS({ 592 | a: { 593 | a2: "a2" 594 | } 595 | }, { 596 | create: function(model) { 597 | return { 598 | a: { 599 | a1: "a1" 600 | } 601 | }; 602 | } 603 | }); 604 | equal(ko.isObservable(result.a), false); 605 | equal(ko.isObservable(result.a.a1), false); 606 | equal(result.a.a2, undefined); 607 | equal(result.a.a1, 'a1'); 608 | }); 609 | 610 | test('ko.mapping.fromJS should not map top-level objects on the supplied overriden model as observables', function () { 611 | var dummyObject = function (options) { 612 | this.a1 = options.a1; 613 | return this; 614 | } 615 | 616 | var result = ko.mapping.fromJS({}, { 617 | create: function(model) { 618 | return { 619 | a: new dummyObject({ 620 | a1: "Hello" 621 | }) 622 | }; 623 | } 624 | }); 625 | equal(ko.isObservable(result.a), false); 626 | equal(ko.isObservable(result.a.a1), false); 627 | equal(result.a.a1, 'Hello'); 628 | }); 629 | 630 | test('ko.mapping.fromJS should allow non-unique atomic properties', function () { 631 | var vm = ko.mapping.fromJS({ 632 | a: [1, 2, 1] 633 | }); 634 | 635 | deepEqual(vm.a(), [1, 2, 1]); 636 | }); 637 | /* speed optimizations don't allow this anymore... 638 | test('ko.mapping.fromJS should not allow non-unique non-atomic properties', function () { 639 | var options = { 640 | key: function(item) { return ko.utils.unwrapObservable(item.id); } 641 | }; 642 | 643 | var didThrow = false; 644 | try { 645 | ko.mapping.fromJS([ 646 | { id: "a1" }, 647 | { id: "a2" }, 648 | { id: "a1" } 649 | ], options); 650 | } 651 | catch (ex) { 652 | didThrow = true 653 | } 654 | equal(didThrow, true); 655 | }); 656 | */ 657 | test('ko.mapping.fromJS should map descendant properties on the supplied object as observables', function () { 658 | var result = ko.mapping.fromJS({ 659 | a: { 660 | a1: 'a1value', 661 | a2: { 662 | a21: 'a21value', 663 | a22: 'a22value' 664 | } 665 | }, 666 | b: { 667 | b1: null, 668 | b2: undefined 669 | } 670 | }); 671 | equal(result.a.a1(), 'a1value'); 672 | equal(result.a.a2.a21(), 'a21value'); 673 | equal(result.a.a2.a22(), 'a22value'); 674 | equal(result.b.b1(), null); 675 | equal(result.b.b2(), undefined); 676 | }); 677 | 678 | test('ko.mapping.fromJS should map observable properties, but without adding a further observable wrapper', function () { 679 | var result = ko.mapping.fromJS({ 680 | a: ko.observable('Hey') 681 | }); 682 | equal(result.a(), 'Hey'); 683 | }); 684 | 685 | test('ko.mapping.fromJS should escape from reference cycles', function () { 686 | var obj = {}; 687 | obj.someProp = { 688 | owner: obj 689 | }; 690 | var result = ko.mapping.fromJS(obj); 691 | equal(result.someProp.owner === result, true); 692 | }); 693 | 694 | test('ko.mapping.fromJS should send relevant create callbacks', function () { 695 | var items = []; 696 | var index = 0; 697 | var result = ko.mapping.fromJS({ 698 | a: "hello" 699 | }, { 700 | create: function (model) { 701 | index++; 702 | return model; 703 | } 704 | }); 705 | equal(index, 1); 706 | }); 707 | 708 | test('ko.mapping.fromJS should send relevant create callbacks when mapping arrays', function () { 709 | var items = []; 710 | var index = 0; 711 | var result = ko.mapping.fromJS([ 712 | "hello" 713 | ], { 714 | create: function (model) { 715 | index++; 716 | return model; 717 | } 718 | }); 719 | equal(index, 1); 720 | }); 721 | 722 | test('ko.mapping.fromJS should send parent along to create callback when creating an object', function() { 723 | var obj = { 724 | a: "a", 725 | b: { 726 | b1: "b1" 727 | } 728 | }; 729 | 730 | var result = ko.mapping.fromJS(obj, { 731 | "b": { 732 | create: function(options) { 733 | equal(ko.isObservable(options.parent.a), true); 734 | equal(options.parent.a(), "a"); 735 | } 736 | } 737 | }); 738 | }); 739 | 740 | test('ko.mapping.fromJS should send parent along to create callback when creating an array item inside an object', function() { 741 | var obj = { 742 | a: "a", 743 | b: [ 744 | { id: 1 }, 745 | { id: 2 } 746 | ] 747 | }; 748 | 749 | var target = {}; 750 | var numCreated = 0; 751 | var result = ko.mapping.fromJS(obj, { 752 | "b": { 753 | create: function(options) { 754 | equal(ko.isObservable(options.parent), false); 755 | equal(options.parent, target); 756 | numCreated++; 757 | } 758 | } 759 | }, target); 760 | 761 | equal(numCreated, 2); 762 | }); 763 | 764 | test('ko.mapping.fromJS should send parent along to create callback when creating an array item inside an array', function() { 765 | // parent is the array 766 | 767 | var obj = [ 768 | { id: 1 }, 769 | { id: 2 } 770 | ]; 771 | 772 | var target = []; 773 | var numCreated = 0; 774 | var result = ko.mapping.fromJS(obj, { 775 | create: function(options) { 776 | equal(ko.isObservable(options.parent), true); 777 | numCreated++; 778 | } 779 | }, target); 780 | 781 | equal(numCreated, 2); 782 | }); 783 | 784 | test('ko.mapping.fromJS should update objects in arrays that were specified in the overriden model in the create callback', function () { 785 | var options = { 786 | create: function(options) { 787 | return ko.mapping.fromJS(options.data); 788 | } 789 | } 790 | 791 | var result = ko.mapping.fromJS([], options); 792 | ko.mapping.fromJS([{ 793 | a: "a", 794 | b: "b" 795 | }], {}, result); 796 | 797 | equal(ko.isObservable(result), true); 798 | equal(ko.isObservable(result()[0].a), true); 799 | equal(result()[0].a(), "a"); 800 | equal(ko.isObservable(result()[0].b), true); 801 | equal(result()[0].b(), "b"); 802 | }); 803 | 804 | test('ko.mapping.fromJS should use the create callback to update objects in arrays', function () { 805 | var created = []; 806 | var arrayEvents = 0; 807 | 808 | var options = { 809 | key: function(item) { return ko.utils.unwrapObservable(item.id); }, 810 | create: function(options) { 811 | created.push(options.data.id); 812 | return ko.mapping.fromJS(options.data); 813 | }, 814 | arrayChanged: function(event, item) { 815 | arrayEvents++; 816 | } 817 | } 818 | 819 | var result = ko.mapping.fromJS([ 820 | { id: "a" } 821 | ], options); 822 | 823 | ko.mapping.fromJS([ 824 | { id: "a" }, 825 | { id: "b" } 826 | ], {}, result); 827 | 828 | equal(created[0], "a"); 829 | equal(created[1], "b"); 830 | equal(result()[0].id(), "a"); 831 | equal(result()[1].id(), "b"); 832 | equal(arrayEvents, 3); // added, retained, added 833 | }); 834 | 835 | test('ko.mapping.fromJS should not call the create callback for existing objects', function () { 836 | var numCreate = 0; 837 | var options = { 838 | create: function (model) { 839 | numCreate++; 840 | var overridenModel = {}; 841 | return overridenModel; 842 | } 843 | }; 844 | 845 | var items = []; 846 | var index = 0; 847 | var result = ko.mapping.fromJS({ 848 | a: "hello" 849 | }, options); 850 | 851 | ko.mapping.fromJS({ 852 | a: "bye" 853 | }, {}, result); 854 | 855 | equal(numCreate, 1); 856 | }); 857 | 858 | test('ko.mapping.fromJS should not overwrite the existing observable array', function () { 859 | var result = ko.mapping.fromJS({ 860 | a: [1] 861 | }); 862 | 863 | var resultA = result.a; 864 | 865 | ko.mapping.fromJS({ 866 | a: [1] 867 | }, result); 868 | 869 | equal(resultA, result.a); 870 | }); 871 | 872 | test('ko.mapping.fromJS should send an added callback for every array item that is added to a previously non-existent array', function () { 873 | var added = []; 874 | 875 | var options = { 876 | "a" : { 877 | arrayChanged: function (event, newValue) { 878 | if (event === "added") added.push(newValue); 879 | } 880 | } 881 | }; 882 | var result = ko.mapping.fromJS({}, options); 883 | ko.mapping.fromJS({ 884 | a: [1, 2] 885 | }, {}, result); 886 | equal(added.length, 2); 887 | equal(added[0], 1); 888 | equal(added[1], 2); 889 | }); 890 | 891 | test('ko.mapping.fromJS should send an added callback for every array item that is added to a previously empty array', function () { 892 | var added = []; 893 | 894 | var options = { 895 | "a": { 896 | arrayChanged: function (event, newValue) { 897 | if (event === "added") added.push(newValue); 898 | } 899 | } 900 | }; 901 | var result = ko.mapping.fromJS({ a: [] }, options); 902 | ko.mapping.fromJS({ 903 | a: [1, 2] 904 | }, {}, result); 905 | equal(added.length, 2); 906 | equal(added[0], 1); 907 | equal(added[1], 2); 908 | }); 909 | 910 | test('ko.mapping.fromJS should not make observable anything that is not in the js object', function () { 911 | var result = ko.mapping.fromJS({}); 912 | result.a = "a"; 913 | equal(ko.isObservable(result.a), false); 914 | 915 | ko.mapping.fromJS({ 916 | b: "b" 917 | }, {}, result); 918 | 919 | equal(ko.isObservable(result.a), false); 920 | equal(ko.isObservable(result.b), true); 921 | equal(result.a, "a"); 922 | equal(result.b(), "b"); 923 | }); 924 | 925 | test('ko.mapping.fromJS should not make observable anything that is not in the js object when overriding the model', function () { 926 | var options = { 927 | create: function(model) { 928 | return { 929 | a: "a" 930 | } 931 | } 932 | }; 933 | 934 | var result = ko.mapping.fromJS({}, options); 935 | ko.mapping.fromJS({ 936 | b: "b" 937 | }, {}, result); 938 | 939 | equal(ko.isObservable(result.a), false); 940 | equal(ko.isObservable(result.b), true); 941 | equal(result.a, "a"); 942 | equal(result.b(), "b"); 943 | }); 944 | 945 | test('ko.mapping.fromJS should send an added callback for every array item that is added', function () { 946 | var added = []; 947 | 948 | var options = { 949 | "a": { 950 | arrayChanged: function (event, newValue) { 951 | if (event === "added") added.push(newValue); 952 | } 953 | } 954 | }; 955 | var result = ko.mapping.fromJS({ 956 | a: [] 957 | }, options); 958 | ko.mapping.fromJS({ 959 | a: [1, 2] 960 | }, {}, result); 961 | equal(added.length, 2); 962 | equal(added[0], 1); 963 | equal(added[1], 2); 964 | }); 965 | 966 | test('ko.mapping.fromJS should send an added callback for every array item that is added', function () { 967 | var added = []; 968 | 969 | var result = ko.mapping.fromJS({ 970 | a: [1, 2] 971 | }, { 972 | "a": { 973 | arrayChanged: function (event, newValue) { 974 | if (event === "added") added.push(newValue); 975 | } 976 | } 977 | }); 978 | equal(added.length, 2); 979 | equal(added[0], 1); 980 | equal(added[1], 2); 981 | }); 982 | 983 | test('ko.mapping.fromJSON should parse and then map in the same way', function () { 984 | var jsonString = ko.utils.stringifyJson({ // Note that "undefined" property values are omitted by the stringifier, so not testing those 985 | a: { 986 | a1: 'a1value', 987 | a2: { 988 | a21: 'a21value', 989 | a22: 'a22value' 990 | } 991 | }, 992 | b: { 993 | b1: null 994 | } 995 | }); 996 | var result = ko.mapping.fromJSON(jsonString); 997 | equal(result.a.a1(), 'a1value'); 998 | equal(result.a.a2.a21(), 'a21value'); 999 | equal(result.a.a2.a22(), 'a22value'); 1000 | equal(result.b.b1(), null); 1001 | }); 1002 | 1003 | test('ko.mapping.fromJS should be able to map empty object structures', function () { 1004 | var obj = { 1005 | someProp: undefined, 1006 | a: [] 1007 | }; 1008 | var result = ko.mapping.fromJS(obj); 1009 | equal(ko.isObservable(result.someProp), true); 1010 | equal(ko.isObservable(result.a), true); 1011 | equal(ko.isObservable(result.unknownProperty), false); 1012 | }); 1013 | 1014 | test('ko.mapping.fromJS should send create callbacks when atomic items are constructed', function () { 1015 | var atomicValues = ["hello", 123, true, null, undefined]; 1016 | var callbacksReceived = 0; 1017 | for (var i = 0; i < atomicValues.length; i++) { 1018 | var result = ko.mapping.fromJS(atomicValues[i], { 1019 | create: function (item) { 1020 | callbacksReceived++; 1021 | return item; 1022 | } 1023 | }); 1024 | } 1025 | equal(callbacksReceived, 5); 1026 | }); 1027 | 1028 | test('ko.mapping.fromJS should send callbacks when atomic array elements are constructed', function () { 1029 | var oldItems = { 1030 | array: [] 1031 | }; 1032 | var newItems = { 1033 | array: [{ 1034 | id: 1 1035 | }, 1036 | { 1037 | id: 2 1038 | }] 1039 | }; 1040 | 1041 | var items = []; 1042 | var result = ko.mapping.fromJS(oldItems, { 1043 | "array": { 1044 | arrayChanged: function (event, item) { 1045 | if (event == "added") 1046 | items.push(item); 1047 | } 1048 | } 1049 | }); 1050 | ko.mapping.fromJS(newItems, {}, result); 1051 | equal(items.length, 2); 1052 | }); 1053 | 1054 | test('ko.mapping.fromJS should not send callbacks containing parent names when descendant objects are constructed', function () { 1055 | var obj = { 1056 | a: { 1057 | a1: "hello", 1058 | a2: 234, 1059 | a3: { 1060 | a31: null 1061 | } 1062 | } 1063 | }; 1064 | var parents = []; 1065 | var pushParent = function (item, parent) { 1066 | parents.push(parent); 1067 | return item; 1068 | }; 1069 | var result = ko.mapping.fromJS(obj, { 1070 | create: pushParent 1071 | }); 1072 | equal(parents.length, 1); 1073 | equal(parents[0], undefined); 1074 | }); 1075 | 1076 | test('ko.mapping.fromJS should create instead of update, on empty objects', function () { 1077 | var obj = { 1078 | a: ["a1", "a2"] 1079 | }; 1080 | 1081 | var result; 1082 | result = ko.mapping.fromJS({}); 1083 | ko.mapping.fromJS(obj, {}, result); 1084 | equal(result.a().length, 2); 1085 | equal(result.a()[0], "a1"); 1086 | equal(result.a()[1], "a2"); 1087 | }); 1088 | 1089 | test('ko.mapping.fromJS should update atomic observables', function () { 1090 | var atomicValues = ["hello", 123, true, null, undefined]; 1091 | var atomicValues2 = ["hello2", 124, false, "not null", "defined"]; 1092 | 1093 | for (var i = 0; i < atomicValues.length; i++) { 1094 | var result = ko.mapping.fromJS(atomicValues[i]); 1095 | ko.mapping.fromJS(atomicValues2[i], {}, result); 1096 | equal(ko.isObservable(result), true); 1097 | equal(result(), atomicValues2[i]); 1098 | } 1099 | }); 1100 | 1101 | test('ko.mapping.fromJS should update objects', function () { 1102 | var obj = { 1103 | a: "prop", 1104 | b: { 1105 | b1: null, 1106 | b2: "b2" 1107 | } 1108 | } 1109 | 1110 | var obj2 = { 1111 | a: "prop2", 1112 | b: { 1113 | b1: 124, 1114 | b2: "b22" 1115 | } 1116 | } 1117 | 1118 | var result = ko.mapping.fromJS(obj); 1119 | ko.mapping.fromJS(obj2, {}, result); 1120 | equal(result.a(), "prop2"); 1121 | equal(result.b.b1(), 124); 1122 | equal(result.b.b2(), "b22"); 1123 | }); 1124 | 1125 | test('ko.mapping.fromJS should update initially empty objects', function () { 1126 | var obj = { 1127 | a: undefined, 1128 | b: [] 1129 | } 1130 | 1131 | var obj2 = { 1132 | a: "prop2", 1133 | b: ["b1", "b2"] 1134 | } 1135 | 1136 | var result = ko.mapping.fromJS(obj); 1137 | ko.mapping.fromJS(obj2, {}, result); 1138 | equal(result.a(), "prop2"); 1139 | equal(result.b()[0], "b1"); 1140 | equal(result.b()[1], "b2"); 1141 | }); 1142 | 1143 | test('ko.mapping.fromJS should update arrays containing atomic types', function () { 1144 | var obj = ["a1", "a2", 6]; 1145 | var obj2 = ["a3", "a4", 7]; 1146 | 1147 | var result = ko.mapping.fromJS(obj); 1148 | 1149 | ko.mapping.fromJS(obj2, {}, result); 1150 | equal(result().length, 3); 1151 | equal(result()[0], "a3"); 1152 | equal(result()[1], "a4"); 1153 | equal(result()[2], 7); 1154 | }); 1155 | 1156 | test('ko.mapping.fromJS should update arrays containing objects', function () { 1157 | var obj = { 1158 | a: [{ 1159 | id: 1, 1160 | value: "a1" 1161 | }, 1162 | { 1163 | id: 2, 1164 | value: "a2" 1165 | }] 1166 | } 1167 | 1168 | var obj2 = { 1169 | a: [{ 1170 | id: 1, 1171 | value: "a1" 1172 | }, 1173 | { 1174 | id: 3, 1175 | value: "a3" 1176 | }] 1177 | } 1178 | 1179 | var options = { 1180 | "a": { 1181 | key: function (item) { 1182 | return item.id; 1183 | } 1184 | } 1185 | }; 1186 | var result = ko.mapping.fromJS(obj, options); 1187 | 1188 | ko.mapping.fromJS(obj2, {}, result); 1189 | equal(result.a().length, 2); 1190 | equal(result.a()[0].value(), "a1"); 1191 | equal(result.a()[1].value(), "a3"); 1192 | }); 1193 | 1194 | test('ko.mapping.fromJS should send a callback when adding new objects to an array', function () { 1195 | var obj = [{ 1196 | id: 1 1197 | }]; 1198 | var obj2 = [{ 1199 | id: 1 1200 | }, 1201 | { 1202 | id: 2 1203 | }]; 1204 | 1205 | var mappedItems = []; 1206 | 1207 | var options = { 1208 | key: function(item) { 1209 | return item.id; 1210 | }, 1211 | arrayChanged: function (event, item) { 1212 | if (event == "added") mappedItems.push(item); 1213 | } 1214 | }; 1215 | var result = ko.mapping.fromJS(obj, options); 1216 | ko.mapping.fromJS(obj2, {}, result); 1217 | equal(mappedItems.length, 2); 1218 | equal(mappedItems[0].id(), 1); 1219 | equal(mappedItems[1].id(), 2); 1220 | }); 1221 | 1222 | test('ko.mapping.fromJS should be able to update from an observable source', function () { 1223 | var obj = [{ 1224 | id: 1 1225 | }]; 1226 | var obj2 = ko.mapping.fromJS([{ 1227 | id: 1 1228 | }, 1229 | { 1230 | id: 2 1231 | }]); 1232 | 1233 | var result = ko.mapping.fromJS(obj); 1234 | ko.mapping.fromJS(obj2, {}, result); 1235 | equal(result().length, 2); 1236 | equal(result()[0].id(), 1); 1237 | equal(result()[1].id(), 2); 1238 | }); 1239 | 1240 | test('ko.mapping.fromJS should send a deleted callback when an item was deleted from an array', function () { 1241 | var obj = [1, 2]; 1242 | var obj2 = [1]; 1243 | 1244 | var items = []; 1245 | 1246 | var options = { 1247 | arrayChanged: function (event, item) { 1248 | if (event == "deleted") items.push(item); 1249 | } 1250 | }; 1251 | var result = ko.mapping.fromJS(obj, options); 1252 | ko.mapping.fromJS(obj2, {}, result); 1253 | equal(items.length, 1); 1254 | equal(items[0], 2); 1255 | }); 1256 | 1257 | test('ko.mapping.fromJS should reuse options that were added in ko.mapping.fromJS', function() { 1258 | var viewModelMapping = { 1259 | key: function(data) { 1260 | return ko.utils.unwrapObservable(data.id); 1261 | }, 1262 | create: function(options) { 1263 | return new viewModel(options); 1264 | } 1265 | }; 1266 | 1267 | var viewModel = function(options) { 1268 | var mapping = { 1269 | entries: viewModelMapping 1270 | }; 1271 | 1272 | ko.mapping.fromJS(options.data, mapping, this); 1273 | 1274 | this.func = function() { return true; }; 1275 | }; 1276 | 1277 | var model = ko.mapping.fromJS([], viewModelMapping); 1278 | 1279 | var data = [{ 1280 | "id": 1, 1281 | "entries": [{ 1282 | "id": 2, 1283 | "entries": [{ 1284 | "id": 3, 1285 | "entries": [] 1286 | }] 1287 | }] 1288 | }]; 1289 | 1290 | ko.mapping.fromJS(data, {}, model); 1291 | ko.mapping.fromJS(data, {}, model); 1292 | 1293 | equal(model()[0].func(), true); 1294 | equal(model()[0].entries()[0].func(), true); 1295 | equal(model()[0].entries()[0].entries()[0].func(), true); 1296 | }); 1297 | 1298 | test('ko.mapping.toJS should not change the mapped object', function() { 1299 | var obj = { 1300 | a: "a" 1301 | } 1302 | 1303 | var result = ko.mapping.fromJS(obj); 1304 | result.b = ko.observable(123); 1305 | var toJS = ko.mapping.toJS(result); 1306 | 1307 | equal(ko.isObservable(result.b), true); 1308 | equal(result.b(), 123); 1309 | equal(toJS.b, undefined); 1310 | }); 1311 | 1312 | test('ko.mapping.toJS should not change the mapped array', function() { 1313 | var obj = [{ 1314 | a: 50 1315 | }] 1316 | 1317 | var result = ko.mapping.fromJS(obj); 1318 | result()[0].b = ko.observable(123); 1319 | var toJS = ko.mapping.toJS(result); 1320 | 1321 | equal(ko.isObservable(result()[0].b), true); 1322 | equal(result()[0].b(), 123); 1323 | }); 1324 | 1325 | test('observableArray.mappedRemove should use key callback if available', function() { 1326 | var obj = [ 1327 | { id : 1 }, 1328 | { id : 2 } 1329 | ] 1330 | 1331 | var result = ko.mapping.fromJS(obj, { 1332 | key: function(item) { 1333 | return ko.utils.unwrapObservable(item.id); 1334 | } 1335 | }); 1336 | result.mappedRemove({ id : 2 }); 1337 | equal(result().length, 1); 1338 | }); 1339 | 1340 | test('observableArray.mappedRemove with predicate should use key callback if available', function() { 1341 | var obj = [ 1342 | { id : 1 }, 1343 | { id : 2 } 1344 | ] 1345 | 1346 | var result = ko.mapping.fromJS(obj, { 1347 | key: function(item) { 1348 | return ko.utils.unwrapObservable(item.id); 1349 | } 1350 | }); 1351 | result.mappedRemove(function(key) { 1352 | return key == 2; 1353 | }); 1354 | equal(result().length, 1); 1355 | }); 1356 | 1357 | test('observableArray.mappedRemoveAll should use key callback if available', function() { 1358 | var obj = [ 1359 | { id : 1 }, 1360 | { id : 2 } 1361 | ] 1362 | 1363 | var result = ko.mapping.fromJS(obj, { 1364 | key: function(item) { 1365 | return ko.utils.unwrapObservable(item.id); 1366 | } 1367 | }); 1368 | result.mappedRemoveAll([{ id : 2 }]); 1369 | equal(result().length, 1); 1370 | }); 1371 | 1372 | test('observableArray.mappedDestroy should use key callback if available', function() { 1373 | var obj = [ 1374 | { id : 1 }, 1375 | { id : 2 } 1376 | ] 1377 | 1378 | var result = ko.mapping.fromJS(obj, { 1379 | key: function(item) { 1380 | return ko.utils.unwrapObservable(item.id); 1381 | } 1382 | }); 1383 | result.mappedDestroy({ id : 2 }); 1384 | equal(result()[0]._destroy, undefined); 1385 | equal(result()[1]._destroy, true); 1386 | }); 1387 | 1388 | test('observableArray.mappedDestroy with predicate should use key callback if available', function() { 1389 | var obj = [ 1390 | { id : 1 }, 1391 | { id : 2 } 1392 | ] 1393 | 1394 | var result = ko.mapping.fromJS(obj, { 1395 | key: function(item) { 1396 | return ko.utils.unwrapObservable(item.id); 1397 | } 1398 | }); 1399 | result.mappedDestroy(function(key) { 1400 | return key == 2; 1401 | }); 1402 | equal(result()[0]._destroy, undefined); 1403 | equal(result()[1]._destroy, true); 1404 | }); 1405 | 1406 | test('observableArray.mappedDestroyAll should use key callback if available', function() { 1407 | var obj = [ 1408 | { id : 1 }, 1409 | { id : 2 } 1410 | ] 1411 | 1412 | var result = ko.mapping.fromJS(obj, { 1413 | key: function(item) { 1414 | return ko.utils.unwrapObservable(item.id); 1415 | } 1416 | }); 1417 | result.mappedDestroyAll([{ id : 2 }]); 1418 | equal(result()[0]._destroy, undefined); 1419 | equal(result()[1]._destroy, true); 1420 | }); 1421 | 1422 | test('observableArray.mappedIndexOf should use key callback if available', function() { 1423 | var obj = [ 1424 | { id : 1 }, 1425 | { id : 2 } 1426 | ] 1427 | 1428 | var result = ko.mapping.fromJS(obj, { 1429 | key: function(item) { 1430 | return ko.utils.unwrapObservable(item.id); 1431 | } 1432 | }); 1433 | equal(result.mappedIndexOf({ id : 1 }), 0); 1434 | equal(result.mappedIndexOf({ id : 2 }), 1); 1435 | equal(result.mappedIndexOf({ id : 3 }), -1); 1436 | }); 1437 | 1438 | test('observableArray.mappedCreate should use key callback if available and not allow duplicates', function() { 1439 | var obj = [ 1440 | { id : 1 }, 1441 | { id : 2 } 1442 | ] 1443 | 1444 | var result = ko.mapping.fromJS(obj, { 1445 | key: function(item) { 1446 | return ko.utils.unwrapObservable(item.id); 1447 | } 1448 | }); 1449 | 1450 | var caught = false; 1451 | try { 1452 | result.mappedCreate({ id : 1 }); 1453 | } 1454 | catch(e) { 1455 | caught = true; 1456 | } 1457 | 1458 | equal(caught, true); 1459 | equal(result().length, 2); 1460 | }); 1461 | 1462 | test('observableArray.mappedCreate should use create callback if available', function() { 1463 | var obj = [ 1464 | { id : 1 }, 1465 | { id : 2 } 1466 | ] 1467 | 1468 | var childModel = function(data){ 1469 | ko.mapping.fromJS(data, {}, this); 1470 | this.Hello = ko.observable("hello"); 1471 | } 1472 | 1473 | var result = ko.mapping.fromJS(obj, { 1474 | key: function(item) { 1475 | return ko.utils.unwrapObservable(item.id); 1476 | }, 1477 | create: function(options){ 1478 | return new childModel(options.data); 1479 | } 1480 | }); 1481 | 1482 | result.mappedCreate({ id: 3 }); 1483 | var index = result.mappedIndexOf({ id : 3 }); 1484 | equal(index, 2); 1485 | equal(result()[index].Hello(), "hello"); 1486 | }); 1487 | 1488 | test('observableArray.mappedCreate should use update callback if available', function() { 1489 | var obj = [ 1490 | { id : 1 }, 1491 | { id : 2 } 1492 | ] 1493 | 1494 | var childModel = function(data){ 1495 | ko.mapping.fromJS(data, {}, this); 1496 | } 1497 | 1498 | var result = ko.mapping.fromJS(obj, { 1499 | key: function(item) { 1500 | return ko.utils.unwrapObservable(item.id); 1501 | }, 1502 | create: function(options){ 1503 | return new childModel(options.data); 1504 | }, 1505 | update: function(options){ 1506 | return { 1507 | bla: options.data.id * 10 1508 | }; 1509 | } 1510 | }); 1511 | 1512 | result.mappedCreate({ id: 3 }); 1513 | equal(result()[0].bla, 10); 1514 | equal(result()[2].bla, 30); 1515 | }); 1516 | 1517 | test('ko.mapping.fromJS should merge options from subsequent calls', function() { 1518 | var obj = ['a']; 1519 | 1520 | var result = ko.mapping.fromJS(obj, { dummyOption1: 1 }); 1521 | ko.mapping.fromJS({}, { dummyOption2: 2 }, result); 1522 | 1523 | equal(result.__ko_mapping__.dummyOption1, 1); 1524 | equal(result.__ko_mapping__.dummyOption2, 2); 1525 | }); 1526 | 1527 | test('ko.mapping.fromJS should correctly handle falsey values', function () { 1528 | var obj = [false, ""]; 1529 | 1530 | var result = ko.mapping.fromJS(obj); 1531 | 1532 | equal(result()[0] === false, true); 1533 | equal(result()[1] === "", true); 1534 | }); 1535 | 1536 | test('ko.mapping.fromJS should correctly handle falsey values in keys', function () { 1537 | var created = []; 1538 | var gotDeletedEvent = false; 1539 | 1540 | var options = { 1541 | key: function(item) { return ko.utils.unwrapObservable(item.id); }, 1542 | arrayChanged: function(event, item) { 1543 | if (event === "deleted") gotDeletedEvent = true; 1544 | } 1545 | } 1546 | 1547 | var result = ko.mapping.fromJS([ 1548 | { id: 0 } 1549 | ], options); 1550 | 1551 | ko.mapping.fromJS([ 1552 | { id: 0 }, 1553 | { id: 1 } 1554 | ], {}, result); 1555 | 1556 | equal(gotDeletedEvent, false); 1557 | }); 1558 | 1559 | test('ko.mapping.fromJS should allow duplicate atomic items in arrays', function () { 1560 | var result = ko.mapping.fromJS([ 1561 | "1", "1", "2" 1562 | ]); 1563 | 1564 | equal(result().length, 3); 1565 | equal(result()[0], "1"); 1566 | equal(result()[1], "1"); 1567 | equal(result()[2], "2"); 1568 | 1569 | ko.mapping.fromJS([ 1570 | "1", "1", "1", "2" 1571 | ], {}, result); 1572 | 1573 | equal(result().length, 4); 1574 | equal(result()[0], "1"); 1575 | equal(result()[1], "1"); 1576 | equal(result()[2], "1"); 1577 | equal(result()[3], "2"); 1578 | }); 1579 | 1580 | test('when doing ko.mapping.fromJS on an already mapped object, the new options should combine with the old', function() { 1581 | var dataA = { 1582 | a: "a" 1583 | }; 1584 | var dataB = { 1585 | b: "b" 1586 | }; 1587 | 1588 | var mapped = {}; 1589 | ko.mapping.fromJS(dataA, {}, mapped); 1590 | ko.mapping.fromJS(dataB, {}, mapped); 1591 | equal(mapped.__ko_mapping__.mappedProperties.a, true); 1592 | equal(mapped.__ko_mapping__.mappedProperties.b, true); 1593 | }); 1594 | 1595 | test('ko.mapping.fromJS should merge options from subsequent calls', function() { 1596 | var obj = ['a']; 1597 | 1598 | var result = ko.mapping.fromJS(obj, { dummyOption1: 1 }); 1599 | ko.mapping.fromJS(['b'], { dummyOption2: 2 }, result); 1600 | 1601 | equal(result.__ko_mapping__.dummyOption1, 1); 1602 | equal(result.__ko_mapping__.dummyOption2, 2); 1603 | }); 1604 | 1605 | test('ko.mapping.fromJS should work on unmapped objects', function() { 1606 | var obj = ko.observableArray(['a']); 1607 | 1608 | ko.mapping.fromJS(['b'], {}, obj); 1609 | 1610 | equal(obj()[0], 'b'); 1611 | }); 1612 | 1613 | test('ko.mapping.fromJS should update an array only once', function() { 1614 | var obj = { 1615 | a: ko.observableArray() 1616 | }; 1617 | 1618 | var updateCount = 0; 1619 | obj.a.subscribe(function() { 1620 | updateCount++; 1621 | }); 1622 | 1623 | ko.mapping.fromJS({ a: [1, 2, 3] }, {}, obj); 1624 | 1625 | equal(updateCount, 1); 1626 | }); 1627 | 1628 | test('ko.mapping.fromJSON should merge options from subsequent calls', function() { 1629 | var obj = ['a']; 1630 | 1631 | var result = ko.mapping.fromJS(obj, { dummyOption1: 1 }); 1632 | ko.mapping.fromJSON('["b"]', { dummyOption2: 2 }, result); 1633 | 1634 | equal(result.__ko_mapping__.dummyOption1, 1); 1635 | equal(result.__ko_mapping__.dummyOption2, 2); 1636 | }); 1637 | 1638 | test('ko.mapping.fromJS should be able to update observables not created by fromJS', function() { 1639 | var existing = { 1640 | a: ko.observable(), 1641 | d: ko.observableArray() 1642 | }; 1643 | 1644 | ko.mapping.fromJS({ 1645 | a: { 1646 | b: "b!" 1647 | }, 1648 | d: [2] 1649 | }, {}, existing); 1650 | 1651 | equal(existing.a().b(), "b!"); 1652 | equal(existing.d().length, 1); 1653 | equal(existing.d()[0], 2); 1654 | }); 1655 | 1656 | test('ko.mapping.fromJS should accept an already mapped object as the second parameter', function() { 1657 | var mapped = ko.mapping.fromJS({ a: "a" }); 1658 | ko.mapping.fromJS({ a: "b" }, mapped); 1659 | equal(mapped.a(), "b"); 1660 | }); 1661 | 1662 | test('ko.mapping.fromJS should properly map objects that appear in multiple places', function() { 1663 | var obj = { title: "Lorem ipsum" }, obj2 = { title: "Lorem ipsum 2" }; 1664 | var x = [obj,obj2]; 1665 | var y = { o: obj, x: x }; 1666 | 1667 | var z = ko.mapping.fromJS(y); 1668 | 1669 | equal(y.x[0].title, "Lorem ipsum"); 1670 | equal(z.x()[0].title(), "Lorem ipsum"); 1671 | }); 1672 | 1673 | test('ko.mapping.fromJS should properly update arrays containing a NULL key', function() { 1674 | var data = [1,2,3,null]; 1675 | var model=ko.mapping.fromJS(data); 1676 | 1677 | deepEqual(model(), [1,2,3,null]); 1678 | 1679 | data = [null,1,2,3]; 1680 | ko.mapping.fromJS(data, {}, model); 1681 | 1682 | deepEqual(model(), [null,1,2,3]); 1683 | }); 1684 | 1685 | test('ko.mapping.visitModel will pass in correct parent names', function() { 1686 | var data = { a: { a2: "a2value" } }; 1687 | var parents = []; 1688 | ko.mapping.visitModel(data, function(obj, parent) { 1689 | parents.push(parent); 1690 | }); 1691 | equal(parents.length, 3); 1692 | equal(parents[0], undefined); 1693 | equal(parents[1], "a"); 1694 | equal(parents[2], "a.a2"); 1695 | }); 1696 | 1697 | test('ko.mapping.toJS should merge the default observe', function() { 1698 | var data = { 1699 | a: "a", 1700 | b: "b", 1701 | c: "c" 1702 | }; 1703 | 1704 | ko.mapping.defaultOptions().observe = ["a"]; 1705 | var result = ko.mapping.fromJS(data, { observe: "b" }); 1706 | equal(ko.isObservable(result.a), true); 1707 | equal(ko.isObservable(result.b), true); 1708 | equal(ko.isObservable(result.c), false); 1709 | }); 1710 | 1711 | test('ko.mapping.fromJS should observe specified single property', function() { 1712 | var data = { 1713 | a: "a", 1714 | b: "b" 1715 | }; 1716 | 1717 | var result = ko.mapping.fromJS(data, { observe: "a" }); 1718 | equal(result.a(), "a"); 1719 | equal(result.b, "b"); 1720 | }); 1721 | 1722 | test('ko.mapping.fromJS should observe specified array', function() { 1723 | var data = { 1724 | a: "a", 1725 | b: ["b1", "b2"] 1726 | }; 1727 | 1728 | var result = ko.mapping.fromJS(data, { observe: "b" }); 1729 | equal(result.a, "a"); 1730 | equal(ko.isObservable(result.b), true); 1731 | }); 1732 | 1733 | test('ko.mapping.fromJS should observe specified array item', function() { 1734 | var data = { 1735 | a: "a", 1736 | b: [{ b1: "v1" }, { b2: "v2" }] 1737 | }; 1738 | 1739 | var result = ko.mapping.fromJS(data, { observe: "b[0].b1" }); 1740 | equal(result.a, "a"); 1741 | equal(result.b[0].b1(), "v1"); 1742 | equal(result.b[1].b2, "v2"); 1743 | }); 1744 | 1745 | test('ko.mapping.fromJS should observe specified array but not the children', function() { 1746 | var data = { 1747 | a: "a", 1748 | b: [{ b1: "v1" }, { b2: "v2" }] 1749 | }; 1750 | 1751 | var result = ko.mapping.fromJS(data, { observe: "b" }); 1752 | equal(result.a, "a"); 1753 | equal(result.b()[0].b1, "v1"); 1754 | equal(result.b()[1].b2, "v2"); 1755 | }); 1756 | 1757 | test('ko.mapping.fromJS should observe specified single property, also when going back .toJS', function() { 1758 | var data = { 1759 | a: "a", 1760 | b: "b" 1761 | }; 1762 | 1763 | var result = ko.mapping.fromJS(data, { observe: "b" }); 1764 | var js = ko.mapping.toJS(result); 1765 | equal(js.a, "a"); 1766 | equal(js.b, "b"); 1767 | }); 1768 | 1769 | test('ko.mapping.fromJS should copy specified single property, also when going back .toJS, except when overridden', function() { 1770 | var data = { 1771 | a: "a", 1772 | b: "b" 1773 | }; 1774 | 1775 | var result = ko.mapping.fromJS(data, { observe: "b" }); 1776 | var js = ko.mapping.toJS(result, { ignore: "b" }); 1777 | equal(js.a, "a"); 1778 | equal(js.b, undefined); 1779 | }); 1780 | 1781 | test('ko.mapping.fromJS with observe option should not fail when map data with sub-object', function() { 1782 | var data = { 1783 | a: "a", 1784 | b: { 1785 | c: "c" 1786 | } 1787 | }; 1788 | 1789 | var result = ko.mapping.fromJS(data, { observe: "a" }); 1790 | equal(ko.isObservable(result.a), true); 1791 | equal(ko.isObservable(result.b), false); 1792 | equal(ko.isObservable(result.b.c), false); 1793 | }); 1794 | 1795 | test('ko.mapping.fromJS should observe property in sub-object', function() { 1796 | var data = { 1797 | a: "a", 1798 | b: { 1799 | c: "c" 1800 | } 1801 | }; 1802 | 1803 | var result = ko.mapping.fromJS(data, { observe: "b.c" }); 1804 | equal(ko.isObservable(result.a), false); 1805 | equal(ko.isObservable(result.b), false); 1806 | equal(ko.isObservable(result.b.c), true); 1807 | }); 1808 | 1809 | test('ko.mapping.fromJS explicit declared none observable members should not be mapped to an observable', function() { 1810 | var data = { 1811 | a: "a", 1812 | b: "b", 1813 | c: "c" 1814 | }; 1815 | 1816 | var ViewModel = function() { 1817 | this.a = ko.observable(); 1818 | this.b = null; 1819 | }; 1820 | 1821 | var result = ko.mapping.fromJS(data, {}, new ViewModel()); 1822 | equal(ko.isObservable(result.a), true); 1823 | equal(ko.isObservable(result.b), false); 1824 | equal(ko.isObservable(result.c), true); 1825 | equal(result.b, data.b); 1826 | }); 1827 | 1828 | test('ko.mapping.toJS explicit declared none observable members should be mapped toJS correctly', function() { 1829 | var data = { 1830 | a: "a", 1831 | }; 1832 | 1833 | var ViewModel = function() { 1834 | this.a = null; 1835 | }; 1836 | 1837 | var result = ko.mapping.fromJS(data, {}, new ViewModel()); 1838 | var js = ko.mapping.toJS(result); 1839 | 1840 | equal(js.b, data.b); 1841 | }); 1842 | 1843 | -------------------------------------------------------------------------------- /spec/lib/qunit-1.10.0.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 | --------------------------------------------------------------------------------