├── .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 |
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 | "" +
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 += "Expected: " + expected + " ";
805 |
806 | if ( actual != expected ) {
807 | output += "Result: " + actual + " ";
808 | output += "Diff: " + QUnit.diff( expected, actual ) + " ";
809 | }
810 |
811 | source = sourceFromStacktrace();
812 |
813 | if ( source ) {
814 | details.source = source;
815 | output += "Source: " + escapeInnerText( source ) + " ";
816 | }
817 |
818 | output += "
";
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 += "Result: " + escapeInnerText( actual ) + " ";
850 | }
851 |
852 | if ( source ) {
853 | details.source = source;
854 | output += "Source: " + escapeInnerText( source ) + " ";
855 | }
856 |
857 | output += "
";
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 += "" + val.label + " ";
952 | }
953 |
954 | moduleFilterHtml += "Module: < All Modules > ";
955 | for ( i in config.modules ) {
956 | if ( config.modules.hasOwnProperty( i ) ) {
957 | numModules += 1;
958 | moduleFilterHtml += "" + i + " ";
959 | }
960 | }
961 | 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 |
--------------------------------------------------------------------------------