├── .gitignore ├── .npmignore ├── .travis.yml ├── .zuul.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── benchmark.js ├── bower.json ├── package.json ├── seamless-immutable.development.js ├── seamless-immutable.development.min.js ├── seamless-immutable.production.min.js ├── src └── seamless-immutable.js └── test ├── Immutable.isImmutable.spec.js ├── Immutable.spec.js ├── ImmutableArray.spec.js ├── ImmutableArray ├── test-asMutable.js ├── test-asObject.js ├── test-compat.js ├── test-flatMap.js ├── test-getIn.js ├── test-set.js └── test-update.js ├── ImmutableDate.spec.js ├── ImmutableDate ├── test-asMutable.js └── test-compat.js ├── ImmutableError.spec.js ├── ImmutableError └── test-compat.js ├── ImmutableObject.spec.js ├── ImmutableObject ├── test-asMutable.js ├── test-compat.js ├── test-getIn.js ├── test-merge.js ├── test-replace.js ├── test-set.js ├── test-update.js └── test-without.js └── TestUtils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | tmp 31 | html-report 32 | lcov.info 33 | lcov-report 34 | .idea 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | tmp 31 | html-report 32 | lcov.info 33 | lcov-report 34 | .idea 35 | 36 | Gruntfile.js 37 | .travis.yml 38 | bower.json 39 | .zuul.yml 40 | benchmark.js 41 | test 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - '8.11.1' 7 | 8 | before_script: 9 | - npm install 10 | 11 | script: npm run travis-test 12 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | browsers: 3 | - name: chrome 4 | version: ["45", "latest"] 5 | - name: ie 6 | version: ["9", "10", "latest"] 7 | - name: firefox 8 | version: ["36", "latest"] 9 | - name: android 10 | version: ["4.0", "latest"] 11 | - name: safari 12 | version: ["6", "latest"] 13 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var envify = require("envify/custom"); 3 | 4 | module.exports = function(grunt) { 5 | grunt.initConfig({ 6 | mochaTest: { 7 | test: { 8 | src: ["test/*.spec.js"] 9 | } 10 | }, 11 | envify: { 12 | dev: { 13 | env: { 14 | NODE_ENV: "development" 15 | }, 16 | input: "src/seamless-immutable.js", 17 | output: "seamless-immutable.development.js" 18 | }, 19 | prod: { 20 | env: { 21 | NODE_ENV: "production" 22 | }, 23 | input: "src/seamless-immutable.js", 24 | output: "seamless-immutable.production.min.js" 25 | } 26 | }, 27 | uglify: { 28 | options: { 29 | banner: '/* (c) 2017, Richard Feldman, github.com/rtfeldman/seamless-immutable/blob/master/LICENSE */' 30 | }, 31 | min: { 32 | files: { 33 | "seamless-immutable.development.min.js": ["seamless-immutable.development.js"], 34 | "seamless-immutable.production.min.js": ["seamless-immutable.production.min.js"] 35 | } 36 | } 37 | } 38 | }); 39 | 40 | grunt.loadNpmTasks("grunt-mocha-test"); 41 | grunt.loadNpmTasks("grunt-contrib-uglify"); 42 | 43 | grunt.registerMultiTask("envify", "Envifies a source file to a target file", function() { 44 | var inputStream = fs.createReadStream(this.data.input); 45 | var outputStream = fs.createWriteStream(this.data.output); 46 | 47 | var done = this.async(); 48 | 49 | inputStream 50 | .pipe(envify(this.data.env)()) 51 | .pipe(outputStream) 52 | .on("finish", done); 53 | }); 54 | 55 | grunt.registerTask("test", "mochaTest"); 56 | grunt.registerTask("build", ["envify", "uglify"]); 57 | grunt.registerTask("default", ["build", "test"]); 58 | }; 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Richard Feldman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of seamless-immutable nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | seamless-immutable 2 | ================== 3 | 4 | Immutable JS data structures which are backwards-compatible with normal Arrays and Objects. 5 | 6 | Use them in `for` loops, pass them to functions expecting vanilla JavaScript data structures, etc. 7 | 8 | ```javascript 9 | var array = Immutable(["totally", "immutable", {hammer: "Can’t Touch This"}]); 10 | 11 | array[1] = "I'm going to mutate you!" 12 | array[1] // "immutable" 13 | 14 | array[2].hammer = "hm, surely I can mutate this nested object..." 15 | array[2].hammer // "Can’t Touch This" 16 | 17 | for (var index in array) { console.log(array[index]); } 18 | // "totally" 19 | // "immutable" 20 | // { hammer: 'Can’t Touch This' } 21 | 22 | JSON.stringify(array) // '["totally","immutable",{"hammer":"Can’t Touch This"}]' 23 | ``` 24 | 25 | This level of backwards compatibility requires [ECMAScript 5](http://kangax.github.io/compat-table/es5/) features like [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) and [Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) to exist and work correctly, which limits the browsers that can use this library to the ones shown in the test results below. (tl;dr [IE9+](https://saucelabs.com/u/seamless-immutable)) 26 | 27 | [![build status][1]][2] [![NPM version][3]][4] [![coverage status][5]][6] 28 | 29 | ## Performance 30 | 31 | Whenever you deeply clone large nested objects, it should typically go much faster with `Immutable` data structures. This is because the library reuses the existing nested objects rather than instantiating new ones. 32 | 33 | In the development build, objects are [frozen](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze). (Note that [Safari is relatively slow to iterate over frozen objects](http://jsperf.com/performance-frozen-object/20).) The development build also overrides unsupported methods (methods that ordinarily mutate the underlying data structure) to throw helpful exceptions. 34 | 35 | The production (minified) build does neither of these, which significantly improves performance. 36 | 37 | We generally recommend to use the "development" build that enforces immutability (and this is the default in Node.js). Only switch to the production build when you encounter performance problems. (See #50 for how to do that in Node or using a build tool - essentially do explicitely refer to the production build.) 38 | 39 | ## Intentional Abstraction Leaks 40 | 41 | By popular demand, functions, errors, dates, and [React](https://facebook.github.io/react/) 42 | components are treated as immutable even though technically they can be mutated. 43 | (It turns out that trying to make these immutable leads to more bad things 44 | than good.) If you call `Immutable()` on any of these, be forewarned: they will 45 | not actually be immutable! 46 | 47 | ## Add-ons 48 | 49 | seamless-immutable is tightly focused on the mechanics of turning existing JavaScript data structures into immutable variants. 50 | Additional packages are available to build on this capability and enable additional programming models: 51 | 52 | |Library|Description| 53 | |--------|------------| 54 | |[Cursor](https://github.com/MartinSnyder/seamless-immutable-cursor)|Compact Cursor Library built on top of the excellent seamless-immutable. Cursors can be used to manage transitions and manipulations of immutable structures in an application.| 55 | |[Mergers](https://github.com/crudh/seamless-immutable-mergers)|A collection of mergers for use with seamless-immutable. Also includes documentation about custom mergers, with examples, for writing your own.| 56 | 57 | ## API Overview 58 | 59 | `Immutable()` returns a backwards-compatible immutable representation of whatever you pass it, so feel free to pass it absolutely anything that can be serialized as JSON. (As is the case with JSON, objects containing circular references are not allowed. Functions are allowed, unlike in JSON, but they will not be touched.) 60 | 61 | Since numbers, strings, `undefined`, and `null` are all immutable to begin with, the only unusual things it returns are Immutable Arrays and Immutable Objects. These have the same [ES5 methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) you’re used to seeing on them, but with these important differences: 62 | 63 | 1. All the methods that would normally mutate the data structures instead throw `ImmutableError`. 64 | 2. All the methods that return a relevant value now return an immutable equivalent of that value. 65 | 3. Attempting to reassign values to their elements (e.g. `foo[5] = bar`) will not work. Browsers other than Internet Explorer will throw a `TypeError` if [use strict](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) is enabled, and in all other cases it will fail silently. 66 | 4. A few additional methods have been added for convenience. 67 | 68 | For example: 69 | 70 | ```javascript 71 | Immutable([3, 1, 4]).sort() 72 | // This will throw an ImmutableError, because sort() is a mutating method. 73 | 74 | Immutable([1, 2, 3]).concat([10, 9, 8]).sort() 75 | // This will also throw ImmutableError, because an Immutable Array's methods 76 | // (including concat()) are guaranteed to return other immutable values. 77 | 78 | [1, 2, 3].concat(Immutable([6, 5, 4])).sort() 79 | // This will succeed, and will yield a sorted mutable array containing 80 | // [1, 2, 3, 4, 5, 6], because a vanilla array's concat() method has 81 | // no knowledge of Immutable. 82 | 83 | var obj = Immutable({all: "your base", are: {belong: "to them"}}); 84 | Immutable.merge(obj, {are: {belong: "to us"}}) 85 | // This will return the following: 86 | // Immutable({all: "your base", are: {belong: "to us"}}) 87 | ``` 88 | 89 | ## Static or instance syntax 90 | 91 | Seamless-immutable supports both static and instance syntaxes: 92 | 93 | ```javascript 94 | var Immutable = require("seamless-immutable").static; 95 | var obj = {}; 96 | 97 | Immutable.setIn(obj, ['key'], data) 98 | ``` 99 | 100 | ```javascript 101 | var Immutable = require("seamless-immutable"); 102 | var obj = {}; 103 | 104 | obj.setIn(['key'], data) 105 | ``` 106 | 107 | Although the later is shorter and is the current default, it can lead to 108 | collisions and some users may dislike polluting object properties when it comes 109 | to debugging. As such the first syntax is recommended, but both are supported. 110 | 111 | ## Immutable.from 112 | 113 | If your linter cringes with the use of `Immutable` without a preceding `new` 114 | (e.g. ESLint's [new-cap](http://eslint.org/docs/rules/new-cap) rule), 115 | use `Immutable.from`: 116 | 117 | ```javascript 118 | Immutable.from([1, 2, 3]); 119 | // is functionally the same as calling: 120 | Immutable([1, 2, 3]) 121 | ``` 122 | 123 | ## Immutable Array 124 | 125 | Like a regular Array, but immutable! You can construct these by passing 126 | an array to `Immutable()`: 127 | 128 | ```javascript 129 | Immutable([1, 2, 3]) 130 | // An immutable array containing 1, 2, and 3. 131 | ``` 132 | 133 | Beyond [the usual Array fare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Accessor_methods), the following methods have been added. 134 | 135 | ### flatMap 136 | 137 | ```javascript 138 | var array = Immutable(["here", "we", "go"]); 139 | Immutable.flatMap(array, function(str) { 140 | return [str, str, str]; 141 | }); 142 | // returns Immutable(["here", "here", "here", "we", "we", "we", "go", "go", "go"]) 143 | 144 | var array = Immutable(["drop the numbers!", 3, 2, 1, 0, null, undefined]); 145 | Immutable.flatMap(array, function(value) { 146 | if (typeof value === "number") { 147 | return []; 148 | } else { 149 | return value; 150 | } 151 | }); 152 | // returns Immutable(["drop the numbers!", null, undefined]) 153 | ``` 154 | 155 | Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, except that whenever the provided 156 | iterator function returns an Array, that Array's elements are each added to the final result. 157 | 158 | ```javascript 159 | var array = Immutable([1,2,3]); 160 | array.map(value => [value+2, value+4]); 161 | // returns Immutable([ [ 3, 5 ], [ 4, 6 ], [ 5, 7 ] ]) 162 | 163 | Immutable.flatMap(array, value => [value+2, value+4]); 164 | // returns Immutable([ 3, 5, 4, 6, 5, 7 ]) 165 | ``` 166 | 167 | ### asObject 168 | 169 | ```javascript 170 | var array = Immutable(["hey", "you"]); 171 | Immutable.asObject(array, function(str) { 172 | return [str, str.toUpperCase()]; 173 | }); 174 | // returns Immutable({hey: "HEY", you: "YOU"}) 175 | ``` 176 | 177 | Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function 178 | will return an array of two elements - the first representing a key, the other 179 | a value. Then returns an Immutable Object constructed of those keys and values. 180 | 181 | You can also call `.asObject` without passing an iterator, in which case it will proceed assuming the Array 182 | is already organized as desired. 183 | 184 | ### asMutable 185 | 186 | ```javascript 187 | var array = Immutable(["hello", "world"]); 188 | var mutableArray = Immutable.asMutable(array); 189 | 190 | mutableArray.push("!!!"); 191 | 192 | mutableArray // ["hello", "world", "!!!"] 193 | ``` 194 | 195 | Returns a mutable copy of the array. For a deeply mutable copy, in which any instances of `Immutable` contained in nested data structures within the array have been converted back to mutable data structures, call `Immutable.asMutable(obj, {deep: true})` instead. 196 | 197 | ### isImmutable 198 | ```javascript 199 | var array = Immutable(["hello", "world"]); 200 | var mutableArray = ["hello", "world"]; 201 | 202 | Immutable.isImmutable(array) 203 | // returns true 204 | 205 | Immutable.isImmutable(mutableArray) 206 | // returns false 207 | ``` 208 | 209 | Returns whether an object is immutable or not. 210 | 211 | ### Additional Methods 212 | In addition, Immutable Arrays also provide member functions for the `set`, `setIn`, `update`, `updateIn`, and `getIn` functions (described below). 213 | ```javascript 214 | var array = Immutable(["hello", "world"]); 215 | 216 | // Equivalent to Immutable.set(array, 1, "you"); 217 | var mutatedArray = array.set(1, "you"); 218 | 219 | mutatedArray // ["hello", "you"] 220 | ``` 221 | 222 | ## Immutable Object 223 | 224 | Like a regular Object, but immutable! You can construct these by passing an 225 | object to `Immutable()`. 226 | 227 | ```javascript 228 | Immutable({foo: "bar"}) 229 | // An immutable object containing the key "foo" and the value "bar". 230 | ``` 231 | 232 | To construct an Immutable Object with a custom prototype, simply specify the 233 | prototype in `options` (while useful for preserving prototypes, please note 234 | that custom mutator methods will not work as the object will be immutable): 235 | 236 | ```javascript 237 | function Square(length) { this.length = length }; 238 | Square.prototype.area = function() { return Math.pow(this.length, 2) }; 239 | 240 | Immutable(new Square(2), {prototype: Square.prototype}).area(); 241 | // An immutable object, with prototype Square, 242 | // containing the key "length" and method `area()` returning 4 243 | ``` 244 | 245 | Beyond [the usual Object fare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Methods_of_Object_instances), the following methods have been added. 246 | 247 | ### Stack overflow protection 248 | 249 | Currently you can't construct Immutable from an object with circular references. To protect from ugly stack overflows, we provide a simple protection during development. We stop at a suspiciously deep stack level and [show an error message][deep]. 250 | 251 | If your objects are deep, but not circular, you can increase this level from default `64`. For example: 252 | 253 | ```javascript 254 | Immutable(deepObject, null, 256); 255 | ``` 256 | 257 | This check is not performed in the production build. 258 | 259 | [deep]: https://github.com/rtfeldman/seamless-immutable/wiki/Deeply-nested-object-was-detected 260 | 261 | ### merge 262 | 263 | ```javascript 264 | var obj = Immutable({status: "good", hypothesis: "plausible", errors: 0}); 265 | Immutable.merge(obj, {status: "funky", hypothesis: "confirmed"}); 266 | // returns Immutable({status: "funky", hypothesis: "confirmed", errors: 0}) 267 | 268 | var obj = Immutable({status: "bad", errors: 37}); 269 | Immutable.merge(obj, [ 270 | {status: "funky", errors: 1}, {status: "groovy", errors: 2}, {status: "sweet"}]); 271 | // returns Immutable({status: "sweet", errors: 2}) 272 | // because passing an Array is shorthand for 273 | // invoking a separate merge for each object in turn. 274 | ``` 275 | Returns an Immutable Object containing the properties and values of both 276 | this object and the provided object, prioritizing the provided object's 277 | values whenever the same key is present in both objects. 278 | 279 | Multiple objects can be provided in an Array in which case more `merge` 280 | invocations will be performed using each provided object in turn. 281 | 282 | A third argument can be provided to configure the merge. It should be an object with any of the following fields: 283 | 284 | ```javascript 285 | { 286 | deep: true, // perform a deep merge 287 | merger: yourCustomMerger // supply a custom merger 288 | } 289 | ``` 290 | 291 | You can find examples and documentation about custom mergers [here](https://github.com/crudh/seamless-immutable-mergers). 292 | 293 | ### replace 294 | 295 | ```javascript 296 | var obj1 = Immutable({a: {b: 'test'}, c: 'test'}); 297 | var obj2 = Immutable.replace(obj1, {a: {b: 'test'}}, {deep: true}); 298 | // returns Immutable({a: {b: 'test'}}); 299 | obj1 === obj2 300 | // returns false 301 | obj1.a === obj2.a 302 | // returns true because child .a objects were identical 303 | ``` 304 | 305 | Returns an Immutable Object containing the properties and values of the 306 | second object only. With deep merge, all child objects are checked for 307 | equality and the original immutable object is returned when possible. 308 | 309 | A second argument can be provided to perform a deep merge: `{deep: true}`. 310 | 311 | ### set 312 | 313 | ```javascript 314 | var obj = Immutable({type: "parrot", subtype: "Norwegian Blue", status: "alive"}); 315 | Immutable.set(obj, "status", "dead"); 316 | // returns Immutable({type: "parrot", subtype: "Norwegian Blue", status: "dead"}) 317 | ``` 318 | 319 | Returns an Immutable Object with a single property set to the provided value. 320 | Basically a more straightforward way of saying 321 | ```javascript 322 | var obj = Immutable({type: "parrot", subtype: "Norwegian Blue", status: "alive"}); 323 | Immutable.merge(obj, {status: "dead"}); 324 | ``` 325 | (and more convenient with non-literal keys unless you have ES6 ```[computed_property_names]```). 326 | 327 | An additional argument can be provided to perform a deep compare: `{deep: true}`. 328 | 329 | When called with an Immutable Array, the property parameter is the index to be changed: 330 | ```javascript 331 | var array = Immutable(["hello", "world"]); 332 | var mutatedArray = Immutable.set(array, 1, "you"); 333 | 334 | mutatedArray // ["hello", "you"] 335 | ``` 336 | If the `{deep: true}` parameter is provided when using an Immutable Array, the object at the provided index will be merged with the provided value using `Immutable.merge()`. 337 | 338 | ### setIn 339 | 340 | Like [set](#set), but accepts a nested path to the property. 341 | 342 | ```javascript 343 | var obj = Immutable({type: {main: "parrot", sub: "Norwegian Blue"}, status: "alive"}); 344 | Immutable.setIn(obj, ["type", "sub"], "Norwegian Ridgeback"); 345 | // returns Immutable({type: {main: "parrot", sub: "Norwegian Ridgeback"}, status: "alive"}) 346 | ``` 347 | 348 | An additional argument can be provided to perform a deep compare: `{deep: true}`. 349 | 350 | When called with an Immutable Array, at least the first value in the path should be an index. This also works with nested arrays: 351 | ```javascript 352 | var array = Immutable([["one fish", "two fish"], ["red fish", "blue fish"]]); 353 | var mutatedArray = Immutable.setIn(array, [1, 1], "green fish"); 354 | 355 | mutatedArray // [["one fish", "two fish"], ["red fish", "green fish"]] 356 | ``` 357 | 358 | ### getIn 359 | 360 | Returns the value at the given path. A default value can be provided as a second argument. 361 | 362 | ```javascript 363 | var obj = Immutable({type: {main: "parrot", subtype: "Norwegian Blue"}, status: "alive"}); 364 | Immutable.getIn(obj, ["type", "subtype"]); 365 | // returns "Norwegian Blue" 366 | Immutable.getIn(obj, ["type", "class"], "Aves"); 367 | // returns "Aves" 368 | ``` 369 | 370 | ### update 371 | 372 | Returns an Immutable Object with a single property updated using the provided updater function. 373 | 374 | ```javascript 375 | function inc (x) { return x + 1 } 376 | var obj = Immutable({foo: 1}); 377 | Immutable.update(obj, "foo", inc); 378 | // returns Immutable({foo: 2}) 379 | ``` 380 | 381 | All additional arguments will be passed to the updater function. 382 | 383 | ```javascript 384 | function add (x, y) { return x + y } 385 | var obj = Immutable({foo: 1}); 386 | Immutable.update(obj, "foo", add, 10); 387 | // returns Immutable({foo: 11}) 388 | ``` 389 | 390 | ### updateIn 391 | 392 | Like [update](#update), but accepts a nested path to the property. 393 | 394 | ```javascript 395 | function add (x, y) { return x + y } 396 | var obj = Immutable({foo: {bar: 1}}); 397 | Immutable.updateIn(obj, ["foo", "bar"], add, 10); 398 | // returns Immutable({foo: {bar: 11}}) 399 | ``` 400 | 401 | ### without 402 | 403 | ```javascript 404 | var obj = Immutable({the: "forests", will: "echo", with: "laughter"}); 405 | Immutable.without(obj, "with"); 406 | // returns Immutable({the: "forests", will: "echo"}) 407 | 408 | var obj = Immutable({the: "forests", will: "echo", with: "laughter"}); 409 | Immutable.without(obj, ["will", "with"]); 410 | // returns Immutable({the: "forests"}) 411 | 412 | var obj = Immutable({the: "forests", will: "echo", with: "laughter"}); 413 | Immutable.without(obj, "will", "with"); 414 | // returns Immutable({the: "forests"}) 415 | 416 | var obj = Immutable({the: "forests", will: "echo", with: "laughter"}); 417 | Immutable.without(obj, (value, key) => key === "the" || value === "echo"); 418 | // returns Immutable({with: "laughter"}) 419 | ``` 420 | 421 | Returns an Immutable Object excluding the given keys or keys/values satisfying 422 | the given predicate from the existing object. 423 | 424 | Multiple keys can be provided, either in an Array or as extra arguments. 425 | 426 | ### asMutable 427 | 428 | ```javascript 429 | var obj = Immutable({when: "the", levee: "breaks"}); 430 | var mutableObject = Immutable.asMutable(obj); 431 | 432 | mutableObject.have = "no place to go"; 433 | 434 | mutableObject // {when: "the", levee: "breaks", have: "no place to go"} 435 | ``` 436 | 437 | Returns a mutable copy of the object. For a deeply mutable copy, in which any instances of `Immutable` contained in nested data structures within the object have been converted back to mutable data structures, call `Immutable.asMutable(obj, {deep: true})` instead. 438 | 439 | ### Releases 440 | 441 | #### 7.1.4 442 | 443 | Fixed bug with custom mergers treating all non-truthy values as undefined ([#244](https://github.com/rtfeldman/seamless-immutable/issues/244)). 444 | 445 | #### 7.1.3 446 | 447 | Treat `Blob` instances as immutable. Use `Array.isArray` over `instanceof`. 448 | 449 | #### 7.1.2 450 | 451 | Treat `Error` instances as immutable. 452 | 453 | #### 7.1.1 454 | 455 | Fix .npmignore 456 | 457 | #### 7.1.0 458 | 459 | Add `getIn` and assumption that Promises are immutable. 460 | 461 | #### 7.0.0 462 | 463 | Add `Immutable.static` as the preferred API. Default to development build in webpack. 464 | 465 | #### 6.3.0 466 | 467 | Adds optional deep compare for `.set`, `.setIn` and `.replace` 468 | 469 | #### 6.2.0 470 | 471 | Adds static alternatives to methods, e.g. `Immutable.setIn` 472 | 473 | #### 6.1.4 474 | 475 | Fixes [bug with deep merge() on an array argument](https://github.com/rtfeldman/seamless-immutable/pull/140). 476 | 477 | #### 6.1.3 478 | 479 | Fixes bug with setting a new object on an existing leaf array. 480 | 481 | #### 6.1.2 482 | 483 | Fixes bug where on some systems arrays are treated as plain objects. 484 | 485 | #### 6.1.1 486 | 487 | `without` now handles numeric keys the same way as string keys. 488 | 489 | #### 6.1.0 490 | 491 | Alias `Immutable.from()` to `Immutable()` for linters. 492 | 493 | #### 6.0.1 494 | 495 | React components are now considered immutable. 496 | 497 | #### 6.0.0 498 | 499 | Add cycle detection. 500 | 501 | #### 5.2.0 502 | 503 | Add `update` and `updateIn`. 504 | 505 | #### 5.1.1 506 | 507 | `Immutable(Object.create(null))` now works as expected. 508 | 509 | #### 5.1.0 510 | 511 | Add predicate support to `without()` 512 | 513 | #### 5.0.1 514 | 515 | Fix missing dev/prod builds for 5.0.0 516 | 517 | #### 5.0.0 518 | 519 | In development build, freeze Dates and ban mutating methods. (Note: dev and prod builds were mistakenly 520 | not generated for this, so to get this functionality in those builds, use 5.0.1) 521 | 522 | #### 4.1.1 523 | 524 | Make `setIn` more null safe. 525 | 526 | #### 4.1.0 527 | 528 | Adds `set` and `setIn` 529 | 530 | #### 4.0.1 531 | 532 | Now when you `require("seamless-immutable")`, you get the development build by default. 533 | 534 | #### 4.0.0 535 | 536 | `main` now points to `src/seamless-immutable.js` so you can more easily build with `envify` yourself. 537 | 538 | #### 3.0.0 539 | 540 | Add support for optional prototyping. 541 | 542 | #### 2.4.2 543 | 544 | Calling .asMutable({deep: true}) on an Immutable data structure with a nested Date no longer throws an exception. 545 | 546 | #### 2.4.1 547 | 548 | Arrays with nonstandard prototypes no longer throw exceptions when passed to `Immutable`. 549 | 550 | #### 2.4.0 551 | 552 | Custom mergers now check for reference equality and abort early if there is no more work needed, allowing improved performance. 553 | 554 | #### 2.3.2 555 | 556 | Fixes a bug where indices passed into iterators for flatMap and asObject were strings instead of numbers. 557 | 558 | #### 2.3.1 559 | 560 | Fixes an IE and Firefox bug related to cloning Dates while preserving their prototypes. 561 | 562 | #### 2.3.0 563 | 564 | Dates now retain their prototypes, the same way Arrays do. 565 | 566 | #### 2.2.0 567 | 568 | Adds a minified production build with no freezing or defensive unsupported methods, for a ~2x performance boost. 569 | 570 | #### 2.1.0 571 | 572 | Adds optional `merger` function to `#merge`. 573 | 574 | #### 2.0.2 575 | 576 | Bugfix: `#merge` with `{deep: true}` no longer attempts (unsuccessfully) to deeply merge arrays as though they were regular objects. 577 | 578 | #### 2.0.1 579 | 580 | Minor documentation typo fix. 581 | 582 | #### 2.0.0 583 | 584 | Breaking API change: `#merge` now takes exactly one or exactly two arguments. The second is optional and allows specifying `deep: true`. 585 | 586 | #### 1.3.0 587 | 588 | Don't bother returning a new value from `#merge` if no changes would result. 589 | 590 | #### 1.2.0 591 | 592 | Make error message for invalid `#asObject` less fancy, resulting in a performance improvement. 593 | 594 | #### 1.1.0 595 | 596 | Adds `#asMutable` 597 | 598 | #### 1.0.0 599 | 600 | Initial stable release 601 | 602 | ## Development 603 | 604 | Run `npm install -g grunt-cli`, `npm install` and then `grunt` to build and test it. 605 | 606 | [1]: https://travis-ci.org/rtfeldman/seamless-immutable.svg?branch=master 607 | [2]: https://travis-ci.org/rtfeldman/seamless-immutable 608 | [3]: https://badge.fury.io/js/seamless-immutable.svg 609 | [4]: https://badge.fury.io/js/seamless-immutable 610 | [5]: http://img.shields.io/coveralls/rtfeldman/seamless-immutable.svg 611 | [6]: https://coveralls.io/r/rtfeldman/seamless-immutable 612 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/seamless-immutable/2d870b14a01e222493c686a7644181185f859558/benchmark.js -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seamless-immutable", 3 | "main": "src/seamless-immutable.js", 4 | "version": "7.1.4", 5 | "homepage": "https://github.com/rtfeldman/seamless-immutable", 6 | "authors": [ 7 | "Richard Feldman " 8 | ], 9 | "description": "Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objects.", 10 | "keywords": [ 11 | "immutable" 12 | ], 13 | "license": "BSD-3-Clause", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seamless-immutable", 3 | "version": "7.1.4", 4 | "description": "Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objects.", 5 | "main": "src/seamless-immutable.js", 6 | "browser": "seamless-immutable.development.js", 7 | "react-native": "src/seamless-immutable.js", 8 | "devDependencies": { 9 | "chai": "3.5.0", 10 | "coveralls": "2.11.8", 11 | "deep-equal": "1.0.1", 12 | "envify": "3.4.0", 13 | "grunt": "1.0.2", 14 | "grunt-contrib-uglify": "0.11.1", 15 | "grunt-mocha-test": "0.12.7", 16 | "istanbul": "0.4.2", 17 | "jscheck": "0.2.0", 18 | "jshint": "2.9.5", 19 | "lodash": "3.10.1", 20 | "mocha": "2.4.5", 21 | "mocha-istanbul": "0.2.0", 22 | "mocha-lcov-reporter": "1.2.0", 23 | "react": "^15.0.1", 24 | "zuul": "3.11.1" 25 | }, 26 | "scripts": { 27 | "test": "grunt", 28 | "test-watch": "mocha --watch test/*.spec.js", 29 | "jshint": "jshint seamless-immutable.development.js", 30 | "coverage": "export ISTANBUL_REPORTERS=text-summary,html,lcov && rm -rf tmp/ && rm -rf html-report/ && istanbul instrument test/ -o tmp/ && mocha --reporter mocha-istanbul tmp/*.spec.js && echo Open html-report/index.html to view results as HTML.", 31 | "zuul-local": "zuul --local -- test/*.spec.js", 32 | "travis-test": "npm run jshint && npm test && npm run coveralls", 33 | "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/rtfeldman/seamless-immutable.git" 38 | }, 39 | "keywords": [ 40 | "immutable" 41 | ], 42 | "author": "Richard Feldman", 43 | "license": "BSD-3-Clause", 44 | "bugs": { 45 | "url": "https://github.com/rtfeldman/seamless-immutable/issues" 46 | }, 47 | "jshintConfig": { 48 | "newcap": false, 49 | "validthis": true, 50 | "proto": true 51 | }, 52 | "homepage": "https://github.com/rtfeldman/seamless-immutable" 53 | } 54 | -------------------------------------------------------------------------------- /seamless-immutable.development.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | function immutableInit(config) { 5 | 6 | // https://github.com/facebook/react/blob/v15.0.1/src/isomorphic/classic/element/ReactElement.js#L21 7 | var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element'); 8 | var REACT_ELEMENT_TYPE_FALLBACK = 0xeac7; 9 | 10 | var globalConfig = { 11 | use_static: false 12 | }; 13 | if (isObject(config)) { 14 | if (config.use_static !== undefined) { 15 | globalConfig.use_static = Boolean(config.use_static); 16 | } 17 | } 18 | 19 | function isObject(data) { 20 | return ( 21 | typeof data === 'object' && 22 | !Array.isArray(data) && 23 | data !== null 24 | ); 25 | } 26 | 27 | function instantiateEmptyObject(obj) { 28 | var prototype = Object.getPrototypeOf(obj); 29 | if (!prototype) { 30 | return {}; 31 | } else { 32 | return Object.create(prototype); 33 | } 34 | } 35 | 36 | function addPropertyTo(target, methodName, value) { 37 | Object.defineProperty(target, methodName, { 38 | enumerable: false, 39 | configurable: false, 40 | writable: false, 41 | value: value 42 | }); 43 | } 44 | 45 | function banProperty(target, methodName) { 46 | addPropertyTo(target, methodName, function() { 47 | throw new ImmutableError("The " + methodName + 48 | " method cannot be invoked on an Immutable data structure."); 49 | }); 50 | } 51 | 52 | var immutabilityTag = "__immutable_invariants_hold"; 53 | 54 | function addImmutabilityTag(target) { 55 | addPropertyTo(target, immutabilityTag, true); 56 | } 57 | 58 | function isImmutable(target) { 59 | if (typeof target === "object") { 60 | return target === null || Boolean( 61 | Object.getOwnPropertyDescriptor(target, immutabilityTag) 62 | ); 63 | } else { 64 | // In JavaScript, only objects are even potentially mutable. 65 | // strings, numbers, null, and undefined are all naturally immutable. 66 | return true; 67 | } 68 | } 69 | 70 | function isEqual(a, b) { 71 | // Avoid false positives due to (NaN !== NaN) evaluating to true 72 | return (a === b || (a !== a && b !== b)); 73 | } 74 | 75 | function isMergableObject(target) { 76 | return target !== null && typeof target === "object" && !(Array.isArray(target)) && !(target instanceof Date); 77 | } 78 | 79 | var mutatingObjectMethods = [ 80 | "setPrototypeOf" 81 | ]; 82 | 83 | var nonMutatingObjectMethods = [ 84 | "keys" 85 | ]; 86 | 87 | var mutatingArrayMethods = mutatingObjectMethods.concat([ 88 | "push", "pop", "sort", "splice", "shift", "unshift", "reverse" 89 | ]); 90 | 91 | var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([ 92 | "map", "filter", "slice", "concat", "reduce", "reduceRight" 93 | ]); 94 | 95 | var mutatingDateMethods = mutatingObjectMethods.concat([ 96 | "setDate", "setFullYear", "setHours", "setMilliseconds", "setMinutes", "setMonth", "setSeconds", 97 | "setTime", "setUTCDate", "setUTCFullYear", "setUTCHours", "setUTCMilliseconds", "setUTCMinutes", 98 | "setUTCMonth", "setUTCSeconds", "setYear" 99 | ]); 100 | 101 | function ImmutableError(message) { 102 | this.name = 'MyError'; 103 | this.message = message; 104 | this.stack = (new Error()).stack; 105 | } 106 | ImmutableError.prototype = new Error(); 107 | ImmutableError.prototype.constructor = Error; 108 | 109 | function makeImmutable(obj, bannedMethods) { 110 | // Tag it so we can quickly tell it's immutable later. 111 | addImmutabilityTag(obj); 112 | 113 | if ("development" !== "production") { 114 | // Make all mutating methods throw exceptions. 115 | for (var index in bannedMethods) { 116 | if (bannedMethods.hasOwnProperty(index)) { 117 | banProperty(obj, bannedMethods[index]); 118 | } 119 | } 120 | 121 | // Freeze it and return it. 122 | Object.freeze(obj); 123 | } 124 | 125 | return obj; 126 | } 127 | 128 | function makeMethodReturnImmutable(obj, methodName) { 129 | var currentMethod = obj[methodName]; 130 | 131 | addPropertyTo(obj, methodName, function() { 132 | return Immutable(currentMethod.apply(obj, arguments)); 133 | }); 134 | } 135 | 136 | function arraySet(idx, value, config) { 137 | var deep = config && config.deep; 138 | 139 | if (idx in this) { 140 | if (deep && this[idx] !== value && isMergableObject(value) && isMergableObject(this[idx])) { 141 | value = Immutable.merge(this[idx], value, {deep: true, mode: 'replace'}); 142 | } 143 | if (isEqual(this[idx], value)) { 144 | return this; 145 | } 146 | } 147 | 148 | var mutable = asMutableArray.call(this); 149 | mutable[idx] = Immutable(value); 150 | return makeImmutableArray(mutable); 151 | } 152 | 153 | var immutableEmptyArray = Immutable([]); 154 | 155 | function arraySetIn(pth, value, config) { 156 | var head = pth[0]; 157 | 158 | if (pth.length === 1) { 159 | return arraySet.call(this, head, value, config); 160 | } else { 161 | var tail = pth.slice(1); 162 | var thisHead = this[head]; 163 | var newValue; 164 | 165 | if (typeof(thisHead) === "object" && thisHead !== null) { 166 | // Might (validly) be object or array 167 | newValue = Immutable.setIn(thisHead, tail, value); 168 | } else { 169 | var nextHead = tail[0]; 170 | // If the next path part is a number, then we are setting into an array, else an object. 171 | if (nextHead !== '' && isFinite(nextHead)) { 172 | newValue = arraySetIn.call(immutableEmptyArray, tail, value); 173 | } else { 174 | newValue = objectSetIn.call(immutableEmptyObject, tail, value); 175 | } 176 | } 177 | 178 | if (head in this && thisHead === newValue) { 179 | return this; 180 | } 181 | 182 | var mutable = asMutableArray.call(this); 183 | mutable[head] = newValue; 184 | return makeImmutableArray(mutable); 185 | } 186 | } 187 | 188 | function makeImmutableArray(array) { 189 | // Don't change their implementations, but wrap these functions to make sure 190 | // they always return an immutable value. 191 | for (var index in nonMutatingArrayMethods) { 192 | if (nonMutatingArrayMethods.hasOwnProperty(index)) { 193 | var methodName = nonMutatingArrayMethods[index]; 194 | makeMethodReturnImmutable(array, methodName); 195 | } 196 | } 197 | 198 | if (!globalConfig.use_static) { 199 | addPropertyTo(array, "flatMap", flatMap); 200 | addPropertyTo(array, "asObject", asObject); 201 | addPropertyTo(array, "asMutable", asMutableArray); 202 | addPropertyTo(array, "set", arraySet); 203 | addPropertyTo(array, "setIn", arraySetIn); 204 | addPropertyTo(array, "update", update); 205 | addPropertyTo(array, "updateIn", updateIn); 206 | addPropertyTo(array, "getIn", getIn); 207 | } 208 | 209 | for(var i = 0, length = array.length; i < length; i++) { 210 | array[i] = Immutable(array[i]); 211 | } 212 | 213 | return makeImmutable(array, mutatingArrayMethods); 214 | } 215 | 216 | function makeImmutableDate(date) { 217 | if (!globalConfig.use_static) { 218 | addPropertyTo(date, "asMutable", asMutableDate); 219 | } 220 | 221 | return makeImmutable(date, mutatingDateMethods); 222 | } 223 | 224 | function asMutableDate() { 225 | return new Date(this.getTime()); 226 | } 227 | 228 | /** 229 | * Effectively performs a map() over the elements in the array, using the 230 | * provided iterator, except that whenever the iterator returns an array, that 231 | * array's elements are added to the final result instead of the array itself. 232 | * 233 | * @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object. 234 | */ 235 | function flatMap(iterator) { 236 | // Calling .flatMap() with no arguments is a no-op. Don't bother cloning. 237 | if (arguments.length === 0) { 238 | return this; 239 | } 240 | 241 | var result = [], 242 | length = this.length, 243 | index; 244 | 245 | for (index = 0; index < length; index++) { 246 | var iteratorResult = iterator(this[index], index, this); 247 | 248 | if (Array.isArray(iteratorResult)) { 249 | // Concatenate Array results into the return value we're building up. 250 | result.push.apply(result, iteratorResult); 251 | } else { 252 | // Handle non-Array results the same way map() does. 253 | result.push(iteratorResult); 254 | } 255 | } 256 | 257 | return makeImmutableArray(result); 258 | } 259 | 260 | /** 261 | * Returns an Immutable copy of the object without the given keys included. 262 | * 263 | * @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments. 264 | */ 265 | function without(remove) { 266 | // Calling .without() with no arguments is a no-op. Don't bother cloning. 267 | if (typeof remove === "undefined" && arguments.length === 0) { 268 | return this; 269 | } 270 | 271 | if (typeof remove !== "function") { 272 | // If we weren't given an array, use the arguments list. 273 | var keysToRemoveArray = (Array.isArray(remove)) ? 274 | remove.slice() : Array.prototype.slice.call(arguments); 275 | 276 | // Convert numeric keys to strings since that's how they'll 277 | // come from the enumeration of the object. 278 | keysToRemoveArray.forEach(function(el, idx, arr) { 279 | if(typeof(el) === "number") { 280 | arr[idx] = el.toString(); 281 | } 282 | }); 283 | 284 | remove = function(val, key) { 285 | return keysToRemoveArray.indexOf(key) !== -1; 286 | }; 287 | } 288 | 289 | var result = instantiateEmptyObject(this); 290 | 291 | for (var key in this) { 292 | if (this.hasOwnProperty(key) && remove(this[key], key) === false) { 293 | result[key] = this[key]; 294 | } 295 | } 296 | 297 | return makeImmutableObject(result); 298 | } 299 | 300 | function asMutableArray(opts) { 301 | var result = [], i, length; 302 | 303 | if(opts && opts.deep) { 304 | for(i = 0, length = this.length; i < length; i++) { 305 | result.push(asDeepMutable(this[i])); 306 | } 307 | } else { 308 | for(i = 0, length = this.length; i < length; i++) { 309 | result.push(this[i]); 310 | } 311 | } 312 | 313 | return result; 314 | } 315 | 316 | /** 317 | * Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function 318 | * will return an array of two elements - the first representing a key, the other 319 | * a value. Then returns an Immutable Object constructed of those keys and values. 320 | * 321 | * @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value. 322 | */ 323 | function asObject(iterator) { 324 | // If no iterator was provided, assume the identity function 325 | // (suggesting this array is already a list of key/value pairs.) 326 | if (typeof iterator !== "function") { 327 | iterator = function(value) { return value; }; 328 | } 329 | 330 | var result = {}, 331 | length = this.length, 332 | index; 333 | 334 | for (index = 0; index < length; index++) { 335 | var pair = iterator(this[index], index, this), 336 | key = pair[0], 337 | value = pair[1]; 338 | 339 | result[key] = value; 340 | } 341 | 342 | return makeImmutableObject(result); 343 | } 344 | 345 | function asDeepMutable(obj) { 346 | if ( 347 | (!obj) || 348 | (typeof obj !== 'object') || 349 | (!Object.getOwnPropertyDescriptor(obj, immutabilityTag)) || 350 | (obj instanceof Date) 351 | ) { return obj; } 352 | return Immutable.asMutable(obj, {deep: true}); 353 | } 354 | 355 | function quickCopy(src, dest) { 356 | for (var key in src) { 357 | if (Object.getOwnPropertyDescriptor(src, key)) { 358 | dest[key] = src[key]; 359 | } 360 | } 361 | 362 | return dest; 363 | } 364 | 365 | /** 366 | * Returns an Immutable Object containing the properties and values of both 367 | * this object and the provided object, prioritizing the provided object's 368 | * values whenever the same key is present in both objects. 369 | * 370 | * @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority. 371 | * @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function 372 | * that takes a property from both objects. If anything is returned it overrides the normal merge behaviour. 373 | */ 374 | function merge(other, config) { 375 | // Calling .merge() with no arguments is a no-op. Don't bother cloning. 376 | if (arguments.length === 0) { 377 | return this; 378 | } 379 | 380 | if (other === null || (typeof other !== "object")) { 381 | throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other)); 382 | } 383 | 384 | var receivedArray = (Array.isArray(other)), 385 | deep = config && config.deep, 386 | mode = config && config.mode || 'merge', 387 | merger = config && config.merger, 388 | result; 389 | 390 | // Use the given key to extract a value from the given object, then place 391 | // that value in the result object under the same key. If that resulted 392 | // in a change from this object's value at that key, set anyChanges = true. 393 | function addToResult(currentObj, otherObj, key) { 394 | var immutableValue = Immutable(otherObj[key]); 395 | var mergerResult = merger && merger(currentObj[key], immutableValue, config); 396 | var currentValue = currentObj[key]; 397 | 398 | if ((result !== undefined) || 399 | (mergerResult !== undefined) || 400 | (!currentObj.hasOwnProperty(key)) || 401 | !isEqual(immutableValue, currentValue)) { 402 | 403 | var newValue; 404 | 405 | if (mergerResult !== undefined) { 406 | newValue = mergerResult; 407 | } else if (deep && isMergableObject(currentValue) && isMergableObject(immutableValue)) { 408 | newValue = Immutable.merge(currentValue, immutableValue, config); 409 | } else { 410 | newValue = immutableValue; 411 | } 412 | 413 | if (!isEqual(currentValue, newValue) || !currentObj.hasOwnProperty(key)) { 414 | if (result === undefined) { 415 | // Make a shallow clone of the current object. 416 | result = quickCopy(currentObj, instantiateEmptyObject(currentObj)); 417 | } 418 | 419 | result[key] = newValue; 420 | } 421 | } 422 | } 423 | 424 | function clearDroppedKeys(currentObj, otherObj) { 425 | for (var key in currentObj) { 426 | if (!otherObj.hasOwnProperty(key)) { 427 | if (result === undefined) { 428 | // Make a shallow clone of the current object. 429 | result = quickCopy(currentObj, instantiateEmptyObject(currentObj)); 430 | } 431 | delete result[key]; 432 | } 433 | } 434 | } 435 | 436 | var key; 437 | 438 | // Achieve prioritization by overriding previous values that get in the way. 439 | if (!receivedArray) { 440 | // The most common use case: just merge one object into the existing one. 441 | for (key in other) { 442 | if (Object.getOwnPropertyDescriptor(other, key)) { 443 | addToResult(this, other, key); 444 | } 445 | } 446 | if (mode === 'replace') { 447 | clearDroppedKeys(this, other); 448 | } 449 | } else { 450 | // We also accept an Array 451 | for (var index = 0, length = other.length; index < length; index++) { 452 | var otherFromArray = other[index]; 453 | 454 | for (key in otherFromArray) { 455 | if (otherFromArray.hasOwnProperty(key)) { 456 | addToResult(result !== undefined ? result : this, otherFromArray, key); 457 | } 458 | } 459 | } 460 | } 461 | 462 | if (result === undefined) { 463 | return this; 464 | } else { 465 | return makeImmutableObject(result); 466 | } 467 | } 468 | 469 | function objectReplace(value, config) { 470 | var deep = config && config.deep; 471 | 472 | // Calling .replace() with no arguments is a no-op. Don't bother cloning. 473 | if (arguments.length === 0) { 474 | return this; 475 | } 476 | 477 | if (value === null || typeof value !== "object") { 478 | throw new TypeError("Immutable#replace can only be invoked with objects or arrays, not " + JSON.stringify(value)); 479 | } 480 | 481 | return Immutable.merge(this, value, {deep: deep, mode: 'replace'}); 482 | } 483 | 484 | var immutableEmptyObject = Immutable({}); 485 | 486 | function objectSetIn(path, value, config) { 487 | if (!(Array.isArray(path)) || path.length === 0) { 488 | throw new TypeError("The first argument to Immutable#setIn must be an array containing at least one \"key\" string."); 489 | } 490 | 491 | var head = path[0]; 492 | if (path.length === 1) { 493 | return objectSet.call(this, head, value, config); 494 | } 495 | 496 | var tail = path.slice(1); 497 | var newValue; 498 | var thisHead = this[head]; 499 | 500 | if (this.hasOwnProperty(head) && typeof(thisHead) === "object" && thisHead !== null) { 501 | // Might (validly) be object or array 502 | newValue = Immutable.setIn(thisHead, tail, value); 503 | } else { 504 | newValue = objectSetIn.call(immutableEmptyObject, tail, value); 505 | } 506 | 507 | if (this.hasOwnProperty(head) && thisHead === newValue) { 508 | return this; 509 | } 510 | 511 | var mutable = quickCopy(this, instantiateEmptyObject(this)); 512 | mutable[head] = newValue; 513 | return makeImmutableObject(mutable); 514 | } 515 | 516 | function objectSet(property, value, config) { 517 | var deep = config && config.deep; 518 | 519 | if (this.hasOwnProperty(property)) { 520 | if (deep && this[property] !== value && isMergableObject(value) && isMergableObject(this[property])) { 521 | value = Immutable.merge(this[property], value, {deep: true, mode: 'replace'}); 522 | } 523 | if (isEqual(this[property], value)) { 524 | return this; 525 | } 526 | } 527 | 528 | var mutable = quickCopy(this, instantiateEmptyObject(this)); 529 | mutable[property] = Immutable(value); 530 | return makeImmutableObject(mutable); 531 | } 532 | 533 | function update(property, updater) { 534 | var restArgs = Array.prototype.slice.call(arguments, 2); 535 | var initialVal = this[property]; 536 | return Immutable.set(this, property, updater.apply(initialVal, [initialVal].concat(restArgs))); 537 | } 538 | 539 | function getInPath(obj, path) { 540 | /*jshint eqnull:true */ 541 | for (var i = 0, l = path.length; obj != null && i < l; i++) { 542 | obj = obj[path[i]]; 543 | } 544 | 545 | return (i && i == l) ? obj : undefined; 546 | } 547 | 548 | function updateIn(path, updater) { 549 | var restArgs = Array.prototype.slice.call(arguments, 2); 550 | var initialVal = getInPath(this, path); 551 | 552 | return Immutable.setIn(this, path, updater.apply(initialVal, [initialVal].concat(restArgs))); 553 | } 554 | 555 | function getIn(path, defaultValue) { 556 | var value = getInPath(this, path); 557 | return value === undefined ? defaultValue : value; 558 | } 559 | 560 | function asMutableObject(opts) { 561 | var result = instantiateEmptyObject(this), key; 562 | 563 | if(opts && opts.deep) { 564 | for (key in this) { 565 | if (this.hasOwnProperty(key)) { 566 | result[key] = asDeepMutable(this[key]); 567 | } 568 | } 569 | } else { 570 | for (key in this) { 571 | if (this.hasOwnProperty(key)) { 572 | result[key] = this[key]; 573 | } 574 | } 575 | } 576 | 577 | return result; 578 | } 579 | 580 | // Creates plain object to be used for cloning 581 | function instantiatePlainObject() { 582 | return {}; 583 | } 584 | 585 | // Finalizes an object with immutable methods, freezes it, and returns it. 586 | function makeImmutableObject(obj) { 587 | if (!globalConfig.use_static) { 588 | addPropertyTo(obj, "merge", merge); 589 | addPropertyTo(obj, "replace", objectReplace); 590 | addPropertyTo(obj, "without", without); 591 | addPropertyTo(obj, "asMutable", asMutableObject); 592 | addPropertyTo(obj, "set", objectSet); 593 | addPropertyTo(obj, "setIn", objectSetIn); 594 | addPropertyTo(obj, "update", update); 595 | addPropertyTo(obj, "updateIn", updateIn); 596 | addPropertyTo(obj, "getIn", getIn); 597 | } 598 | 599 | return makeImmutable(obj, mutatingObjectMethods); 600 | } 601 | 602 | // Returns true if object is a valid react element 603 | // https://github.com/facebook/react/blob/v15.0.1/src/isomorphic/classic/element/ReactElement.js#L326 604 | function isReactElement(obj) { 605 | return typeof obj === 'object' && 606 | obj !== null && 607 | (obj.$$typeof === REACT_ELEMENT_TYPE_FALLBACK || obj.$$typeof === REACT_ELEMENT_TYPE); 608 | } 609 | 610 | function isFileObject(obj) { 611 | return typeof File !== 'undefined' && 612 | obj instanceof File; 613 | } 614 | 615 | function isBlobObject(obj) { 616 | return typeof Blob !== 'undefined' && 617 | obj instanceof Blob; 618 | } 619 | 620 | function isPromise(obj) { 621 | return typeof obj === 'object' && 622 | typeof obj.then === 'function'; 623 | } 624 | 625 | function isError(obj) { 626 | return obj instanceof Error; 627 | } 628 | 629 | function Immutable(obj, options, stackRemaining) { 630 | if (isImmutable(obj) || isReactElement(obj) || isFileObject(obj) || isBlobObject(obj) || isError(obj)) { 631 | return obj; 632 | } else if (isPromise(obj)) { 633 | return obj.then(Immutable); 634 | } else if (Array.isArray(obj)) { 635 | return makeImmutableArray(obj.slice()); 636 | } else if (obj instanceof Date) { 637 | return makeImmutableDate(new Date(obj.getTime())); 638 | } else { 639 | // Don't freeze the object we were given; make a clone and use that. 640 | var prototype = options && options.prototype; 641 | var instantiateEmptyObject = 642 | (!prototype || prototype === Object.prototype) ? 643 | instantiatePlainObject : (function() { return Object.create(prototype); }); 644 | var clone = instantiateEmptyObject(); 645 | 646 | if ("development" !== "production") { 647 | /*jshint eqnull:true */ 648 | if (stackRemaining == null) { 649 | stackRemaining = 64; 650 | } 651 | if (stackRemaining <= 0) { 652 | throw new ImmutableError("Attempt to construct Immutable from a deeply nested object was detected." + 653 | " Have you tried to wrap an object with circular references (e.g. React element)?" + 654 | " See https://github.com/rtfeldman/seamless-immutable/wiki/Deeply-nested-object-was-detected for details."); 655 | } 656 | stackRemaining -= 1; 657 | } 658 | 659 | for (var key in obj) { 660 | if (Object.getOwnPropertyDescriptor(obj, key)) { 661 | clone[key] = Immutable(obj[key], undefined, stackRemaining); 662 | } 663 | } 664 | 665 | return makeImmutableObject(clone); 666 | } 667 | } 668 | 669 | // Wrapper to allow the use of object methods as static methods of Immutable. 670 | function toStatic(fn) { 671 | function staticWrapper() { 672 | var args = [].slice.call(arguments); 673 | var self = args.shift(); 674 | return fn.apply(self, args); 675 | } 676 | 677 | return staticWrapper; 678 | } 679 | 680 | // Wrapper to allow the use of object methods as static methods of Immutable. 681 | // with the additional condition of choosing which function to call depending 682 | // if argument is an array or an object. 683 | function toStaticObjectOrArray(fnObject, fnArray) { 684 | function staticWrapper() { 685 | var args = [].slice.call(arguments); 686 | var self = args.shift(); 687 | if (Array.isArray(self)) { 688 | return fnArray.apply(self, args); 689 | } else { 690 | return fnObject.apply(self, args); 691 | } 692 | } 693 | 694 | return staticWrapper; 695 | } 696 | 697 | // Wrapper to allow the use of object methods as static methods of Immutable. 698 | // with the additional condition of choosing which function to call depending 699 | // if argument is an array or an object or a date. 700 | function toStaticObjectOrDateOrArray(fnObject, fnArray, fnDate) { 701 | function staticWrapper() { 702 | var args = [].slice.call(arguments); 703 | var self = args.shift(); 704 | if (Array.isArray(self)) { 705 | return fnArray.apply(self, args); 706 | } else if (self instanceof Date) { 707 | return fnDate.apply(self, args); 708 | } else { 709 | return fnObject.apply(self, args); 710 | } 711 | } 712 | 713 | return staticWrapper; 714 | } 715 | 716 | // Export the library 717 | Immutable.from = Immutable; 718 | Immutable.isImmutable = isImmutable; 719 | Immutable.ImmutableError = ImmutableError; 720 | Immutable.merge = toStatic(merge); 721 | Immutable.replace = toStatic(objectReplace); 722 | Immutable.without = toStatic(without); 723 | Immutable.asMutable = toStaticObjectOrDateOrArray(asMutableObject, asMutableArray, asMutableDate); 724 | Immutable.set = toStaticObjectOrArray(objectSet, arraySet); 725 | Immutable.setIn = toStaticObjectOrArray(objectSetIn, arraySetIn); 726 | Immutable.update = toStatic(update); 727 | Immutable.updateIn = toStatic(updateIn); 728 | Immutable.getIn = toStatic(getIn); 729 | Immutable.flatMap = toStatic(flatMap); 730 | Immutable.asObject = toStatic(asObject); 731 | if (!globalConfig.use_static) { 732 | Immutable.static = immutableInit({ 733 | use_static: true 734 | }); 735 | } 736 | 737 | Object.freeze(Immutable); 738 | 739 | return Immutable; 740 | } 741 | 742 | var Immutable = immutableInit(); 743 | /* istanbul ignore if */ 744 | if (typeof define === 'function' && define.amd) { 745 | define(function() { 746 | return Immutable; 747 | }); 748 | } else if (typeof module === "object") { 749 | module.exports = Immutable; 750 | } else if (typeof exports === "object") { 751 | exports.Immutable = Immutable; 752 | } else if (typeof window === "object") { 753 | window.Immutable = Immutable; 754 | } else if (typeof global === "object") { 755 | global.Immutable = Immutable; 756 | } 757 | })(); 758 | -------------------------------------------------------------------------------- /seamless-immutable.development.min.js: -------------------------------------------------------------------------------- 1 | /* (c) 2017, Richard Feldman, github.com/rtfeldman/seamless-immutable/blob/master/LICENSE */!function(){"use strict";function a(b){function c(a){return"object"==typeof a&&!Array.isArray(a)&&null!==a}function d(a){var b=Object.getPrototypeOf(a);return b?Object.create(b):{}}function e(a,b,c){Object.defineProperty(a,b,{enumerable:!1,configurable:!1,writable:!1,value:c})}function f(a,b){e(a,b,function(){throw new k("The "+b+" method cannot be invoked on an Immutable data structure.")})}function g(a){e(a,V,!0)}function h(a){return"object"!=typeof a||(null===a||Boolean(Object.getOwnPropertyDescriptor(a,V)))}function i(a,b){return a===b||a!==a&&b!==b}function j(a){return!(null===a||"object"!=typeof a||Array.isArray(a)||a instanceof Date)}function k(a){this.name="MyError",this.message=a,this.stack=(new Error).stack}function l(a,b){g(a);for(var c in b)b.hasOwnProperty(c)&&f(a,b[c]);return Object.freeze(a),a}function m(a,b){var c=a[b];e(a,b,function(){return O(c.apply(a,arguments))})}function n(a,b,c){var d=c&&c.deep;if(a in this&&(d&&this[a]!==b&&j(b)&&j(this[a])&&(b=O.merge(this[a],b,{deep:!0,mode:"replace"})),i(this[a],b)))return this;var e=u.call(this);return e[a]=O(b),p(e)}function o(a,b,c){var d=a[0];if(1===a.length)return n.call(this,d,b,c);var e,f=a.slice(1),g=this[d];if("object"==typeof g&&null!==g)e=O.setIn(g,f,b);else{var h=f[0];e=""!==h&&isFinite(h)?o.call(_,f,b):A.call(aa,f,b)}if(d in this&&g===e)return this;var i=u.call(this);return i[d]=e,p(i)}function p(a){for(var b in Z)if(Z.hasOwnProperty(b)){var c=Z[b];m(a,c)}U.use_static||(e(a,"flatMap",s),e(a,"asObject",v),e(a,"asMutable",u),e(a,"set",n),e(a,"setIn",o),e(a,"update",C),e(a,"updateIn",E),e(a,"getIn",F));for(var d=0,f=a.length;d { 80 | assert.isTrue(Immutable.isImmutable(result), 'The promise fulfillment value should be immutable'); 81 | assert.isFalse(Immutable.isImmutable(wrappedPromise), 'The promise itself should not be immutable'); 82 | }); 83 | }); 84 | 85 | it("doesn't wrap the promise rejection reason", function() { 86 | var reason = new Error('foo'); 87 | var promise = Promise.reject(reason); 88 | var wrappedPromise = Immutable(promise); 89 | 90 | wrappedPromise.catch(catchedReason => { 91 | assert.strictEqual(reason, catchedReason, 'The promise rejection reason should be left untouched'); 92 | }); 93 | }); 94 | 95 | it("doesn't fail when a function is defined on Array.prototype", function() { 96 | Array.prototype.veryEvilFunction = function() {}; 97 | Immutable([]); 98 | delete Array.prototype.veryEvilFunction; 99 | }); 100 | 101 | it("returns an object with the given optional prototype", function() { 102 | function TestClass(o) { _.extend(this, o); }; 103 | var data = new TestClass({a: 1, b: 2}); 104 | 105 | var immutable = Immutable(data, {prototype: TestClass.prototype}); 106 | 107 | TestUtils.assertJsonEqual(immutable, data); 108 | TestUtils.assertHasPrototype(immutable, TestClass.prototype); 109 | }); 110 | 111 | // These are already immutable, and should pass through Immutable() untouched. 112 | _.each({ 113 | "string": JSC.string(), 114 | "number": JSC.number(), 115 | "boolean": JSC.boolean(), 116 | "null": JSC.literal(null), 117 | "undefined": JSC.literal(undefined) 118 | }, function (specifier, type) { 119 | it("simply returns its argument when passed a value of type " + type, function () { 120 | TestUtils.check(100, [specifier], function (value) { 121 | assert.strictEqual(Immutable(value), value); 122 | 123 | // should still pass through with a faulty prototype option 124 | assert.strictEqual(Immutable(value, {prototype: Object.prototype}), value); 125 | }); 126 | }); 127 | }); 128 | 129 | it("doesn't modify React classes", function() { 130 | var reactClass = React.createClass({ 131 | render: function() {} 132 | }); 133 | var factory = React.createFactory(reactClass); 134 | 135 | var component = factory(); 136 | var immutableComponent = Immutable(component); 137 | 138 | assert.typeOf(immutableComponent, 'object'); 139 | assert.isTrue(React.isValidElement(immutableComponent), 'Immutable component was not a valid react element'); 140 | assert.isFalse(Immutable.isImmutable(immutableComponent), 'React element should not be immutable'); 141 | TestUtils.assertJsonEqual(immutableComponent, component); 142 | }); 143 | 144 | it("doesn't modify React elements", function() { 145 | var reactElement = React.createElement('div'); 146 | var immutableElement = Immutable(reactElement); 147 | 148 | assert.typeOf(immutableElement, 'object'); 149 | assert.isTrue(React.isValidElement(immutableElement), 'Immutable element was not a valid react element'); 150 | assert.isFalse(Immutable.isImmutable(immutableElement), 'React element should not be immutable'); 151 | TestUtils.assertJsonEqual(immutableElement, reactElement); 152 | }); 153 | 154 | it("doesn't modify File objects", function() { 155 | global.File = TestUtils.FileMock; 156 | var file = new File(['part'], 'filename.jpg'); 157 | var immutableFile = Immutable(file); 158 | 159 | assert.isTrue(file instanceof File, "file instanceof File"); 160 | assert.isTrue(immutableFile instanceof File, "immutableFile instanceof File"); 161 | assert.equal(immutableFile, file); 162 | 163 | delete global.File; 164 | }); 165 | 166 | it("doesnt modify Blob objects", function() { 167 | global.Blob = TestUtils.BlobMock; 168 | var blob = new Blob(); 169 | var immutableBlob = Immutable(blob); 170 | 171 | assert.isTrue(blob instanceof Blob, "blob instanceof Blob"); 172 | assert.isTrue(immutableBlob instanceof Blob, "immutableBlob instanceof Blob"); 173 | assert.equal(immutableBlob, blob); 174 | 175 | delete global.Blob; 176 | }); 177 | 178 | it("doesn't modify Error objects", function () { 179 | var error = new Error('Oh no something bad happened!'); 180 | var immutableError = Immutable(error); 181 | assert.strictEqual(error, immutableError, 'Immutable should pass the error directly through') 182 | }); 183 | 184 | it("detects cycles", function() { 185 | var obj = {}; 186 | obj.prop = obj; 187 | var expectedError; 188 | 189 | if (config.id === 'prod') { 190 | if (typeof navigator === "undefined") { 191 | // Node.js 192 | expectedError = RangeError; 193 | } else if (navigator.userAgent.indexOf("MSIE") !== -1) { 194 | // IE9-10 195 | expectedError = /Out of stack space/; 196 | } else if (navigator.userAgent.indexOf("Trident") !== -1) { 197 | // IE11 198 | expectedError = /Out of stack space/; 199 | } else if (navigator.userAgent.indexOf("Firefox") !== -1) { 200 | // Firefox 201 | expectedError = InternalError; 202 | } else { 203 | // Chrome/Safari/Opera 204 | expectedError = RangeError; 205 | } 206 | } else { 207 | expectedError = /deeply nested/; 208 | } 209 | 210 | assert.throws(function() { Immutable(obj); }, expectedError); 211 | }); 212 | 213 | it("can configure stackRemaining", function() { 214 | var mutable = {bottom: true}; 215 | _.range(65).forEach(function() { 216 | mutable = {prop: mutable}; 217 | }); 218 | 219 | if (config.id === 'prod') { 220 | TestUtils.assertJsonEqual(mutable, Immutable(mutable)); 221 | } else { 222 | assert.throws(function() { Immutable(mutable); }, /deeply nested/); 223 | TestUtils.assertJsonEqual(mutable, Immutable(mutable, null, 66)); 224 | } 225 | }); 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/ImmutableArray.spec.js: -------------------------------------------------------------------------------- 1 | var testCompat = require("./ImmutableArray/test-compat.js"); 2 | var testFlatMap = require("./ImmutableArray/test-flatMap.js"); 3 | var testAsObject = require("./ImmutableArray/test-asObject.js"); 4 | var testAsMutable = require("./ImmutableArray/test-asMutable.js"); 5 | var testSet = require("./ImmutableArray/test-set.js"); 6 | var testUpdate = require("./ImmutableArray/test-update.js"); 7 | var testGetIn = require("./ImmutableArray/test-getIn.js"); 8 | var devBuild = require("../seamless-immutable.development.js"); 9 | var prodBuild = require("../seamless-immutable.production.min.js"); 10 | var getTestUtils = require("./TestUtils.js"); 11 | 12 | [ 13 | {id: "dev", name: "Development build", implementation: devBuild}, 14 | {id: "prod", name: "Production build", implementation: prodBuild} 15 | ].forEach(function(config) { 16 | var Immutable = config.implementation; 17 | var TestUtils = getTestUtils(Immutable); 18 | 19 | describe(config.name, function () { 20 | describe("ImmutableArray", function () { 21 | testCompat(config); 22 | testFlatMap(config); 23 | testAsObject(config); 24 | testAsMutable(config); 25 | testSet(config); 26 | testUpdate(config); 27 | testGetIn(config); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-asMutable.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | var check = TestUtils.check; 10 | 11 | describe("#asMutable", function() { 12 | it("returns an empty mutable array from an empty immutable array", function() { 13 | var immutable = Immutable([]); 14 | var mutable = Immutable.asMutable(immutable); 15 | 16 | assertIsArray(mutable); 17 | assertCanBeMutated(mutable); 18 | assert.isFalse( Immutable.isImmutable(mutable)); 19 | TestUtils.assertJsonEqual(immutable,mutable); 20 | }); 21 | 22 | it("returns a shallow mutable copy if not provided the deep flag", function() { 23 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier, JSC.any()]) ], function(array) { 24 | var immutable = Immutable(array); 25 | var mutable = Immutable.asMutable(immutable); 26 | 27 | assertIsArray(mutable); 28 | assertCanBeMutated(mutable); 29 | assert.isFalse( Immutable.isImmutable(mutable)); 30 | TestUtils.assertIsDeeplyImmutable(mutable[0]); 31 | TestUtils.assertIsDeeplyImmutable(mutable[0].deep); 32 | TestUtils.assertJsonEqual(immutable,mutable); 33 | }); 34 | 35 | }); 36 | 37 | it("returns a deep mutable copy if provided the deep flag", function() { 38 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier, JSC.any()]) ], function(array) { 39 | var immutable = Immutable(array); 40 | var mutable = Immutable.asMutable(immutable, { deep: true }); 41 | 42 | assertIsArray(mutable); 43 | assertCanBeMutated(mutable); 44 | assert.isFalse( Immutable.isImmutable(mutable)); 45 | assert.isFalse( Immutable.isImmutable(mutable[0])); 46 | assert.isFalse( Immutable.isImmutable(mutable[0]['deep'])); 47 | TestUtils.assertJsonEqual(immutable,mutable); 48 | }); 49 | }); 50 | 51 | it("supports non-static syntax", function() { 52 | var obj = Immutable(['test']); 53 | obj = obj.asMutable(); 54 | TestUtils.assertJsonEqual(obj, ['test']); 55 | assertCanBeMutated(obj); 56 | }); 57 | }); 58 | 59 | function assertCanBeMutated(array) { 60 | try { 61 | var newElement = { foo: "bar" }; 62 | var originalLength = array.length; 63 | 64 | array.push(newElement); 65 | 66 | assert.equal(array[array.length - 1], newElement); 67 | assert.equal(array.length, originalLength + 1); 68 | 69 | array.pop(newElement); 70 | } catch(error) { 71 | assert.fail("Exception when trying to verify that this array was mutable: " + JSON.stringify(array)); 72 | } 73 | } 74 | 75 | function assertIsArray(array) { 76 | assert(array instanceof Array, "Expected an Array, but did not get one. Got: " + JSON.stringify(array)) 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-asObject.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | var check = TestUtils.check; 10 | 11 | describe("#asObject", function() { 12 | it("works on arrays of various lengths", function() { 13 | check(100, [TestUtils.ComplexObjectSpecifier()], function(obj) { 14 | var keys = _.keys(obj); 15 | var values = _.values(obj); 16 | var array = Immutable(_.map(obj, function(value, key) { 17 | return [key, value]; 18 | })); 19 | 20 | var result = Immutable.asObject(array, function(value, index) { 21 | assert.strictEqual((typeof index), "number"); 22 | 23 | // Check that the index argument we receive works as expected. 24 | TestUtils.assertJsonEqual(value, array[index], "Expected array[" + index + "] to be " + value); 25 | 26 | return [keys[index], values[index]] 27 | }); 28 | 29 | TestUtils.assertJsonEqual(result, obj); 30 | }); 31 | }); 32 | 33 | it("works without an iterator on arrays that are already organized properly", function() { 34 | check(100, [TestUtils.ComplexObjectSpecifier()], function(obj) { 35 | var array = Immutable(_.map(obj, function(value, key) { 36 | return [key, value]; 37 | })); 38 | 39 | var result = Immutable.asObject(array); 40 | 41 | TestUtils.assertJsonEqual(result, obj); 42 | }); 43 | }); 44 | 45 | // Sanity check to make sure our QuickCheck logic isn't off the rails. 46 | it("passes a basic sanity check on canned input", function() { 47 | var expected = Immutable({all: "your base", are: {belong: "to us"}}); 48 | var actual = Immutable([{key: "all", value: "your base"}, {key: "are", value: {belong: "to us"}}]); 49 | actual = Immutable.asObject(actual, function(elem) { 50 | return [elem.key, elem.value]; 51 | }); 52 | 53 | TestUtils.assertJsonEqual(actual, expected); 54 | }); 55 | 56 | it("supports non-static syntax", function() { 57 | function dummyUpdater(data) { 58 | return [data, data + '_updated']; 59 | } 60 | var obj = Immutable(['test']); 61 | obj = obj.asObject(dummyUpdater); 62 | TestUtils.assertJsonEqual(obj, {test: 'test_updated'}); 63 | }); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-compat.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | var identityFunction = function(arg) { return arg; }; 7 | 8 | module.exports = function(config) { 9 | var Immutable = config.implementation; 10 | var TestUtils = getTestUtils(Immutable); 11 | 12 | var checkImmutableMutable = TestUtils.checkImmutableMutable(100, [JSC.array([TestUtils.ComplexObjectSpecifier()])]); 13 | 14 | describe("ImmutableArray", function() { 15 | describe("which is compatible with vanilla mutable arrays", function() { 16 | it("is an instance of Array", function() { 17 | checkImmutableMutable(function(immutable, mutable) { 18 | assert.instanceOf(immutable, Array); 19 | }); 20 | }); 21 | 22 | it("has the same length as its mutable equivalent", function() { 23 | checkImmutableMutable(function(immutable, mutable) { 24 | assert.strictEqual(immutable.length, mutable.length); 25 | }); 26 | }); 27 | 28 | it("supports accessing elements by index via []", function() { 29 | checkImmutableMutable(function(immutable, mutable, index) { 30 | assert.typeOf(index, "number"); 31 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 32 | }, [JSC.integer()]); 33 | }); 34 | 35 | it("works with for loops", function() { 36 | checkImmutableMutable(function(immutable, mutable) { 37 | for (var index=0; index < immutable.length; index++) { 38 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 39 | } 40 | }); 41 | }); 42 | 43 | it("works with for..in loops", function() { 44 | checkImmutableMutable(function(immutable, mutable) { 45 | for (var index in immutable) { 46 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 47 | } 48 | }); 49 | }); 50 | 51 | it("supports concat", function() { 52 | checkImmutableMutable(function(immutable, mutable, otherArray) { 53 | TestUtils.assertJsonEqual(immutable.concat(otherArray), mutable.concat(otherArray)); 54 | }, [JSC.array()]); 55 | }); 56 | 57 | it("supports being an argument to a normal immutable's concat", function() { 58 | checkImmutableMutable(function(immutable, mutable, otherArray) { 59 | TestUtils.assertJsonEqual(otherArray.concat(immutable), otherArray.concat(mutable)); 60 | }, [JSC.array()]); 61 | }); 62 | 63 | it("can be concatted to itself", function() { 64 | checkImmutableMutable(function(immutable, mutable) { 65 | TestUtils.assertJsonEqual(immutable.concat(immutable), mutable.concat(mutable)); 66 | }); 67 | }); 68 | 69 | it("has a toString() method that works like a regular immutable's toString()", function() { 70 | checkImmutableMutable(function(immutable, mutable) { 71 | assert.strictEqual(immutable.toString(), mutable.toString()); 72 | }); 73 | }); 74 | 75 | it("supports being passed to JSON.stringify", function() { 76 | checkImmutableMutable(function(immutable, mutable) { 77 | TestUtils.assertJsonEqual(JSON.stringify(immutable), JSON.stringify(mutable)); 78 | }); 79 | }); 80 | 81 | if (config.id === "dev") { 82 | it("is frozen", function() { 83 | checkImmutableMutable(function(immutable, mutable) { 84 | assert.isTrue(Object.isFrozen(immutable)); 85 | }); 86 | }); 87 | } 88 | 89 | if (config.id === "prod") { 90 | it("is not frozen", function() { 91 | checkImmutableMutable(function(immutable, mutable) { 92 | assert.isFalse(Object.isFrozen(immutable)); 93 | }); 94 | }); 95 | } 96 | 97 | it("is tagged as immutable", function() { 98 | checkImmutableMutable(function(immutable, mutable) { 99 | TestUtils.assertIsDeeplyImmutable(immutable); 100 | }) 101 | }); 102 | 103 | if (config.id === "dev") { 104 | it("cannot have its elements directly mutated", function () { 105 | checkImmutableMutable(function (immutable, mutable, randomIndex, randomData) { 106 | immutable[randomIndex] = randomData; 107 | 108 | assert.typeOf(randomIndex, "number"); 109 | assert.strictEqual(immutable.length, mutable.length); 110 | TestUtils.assertJsonEqual(immutable[randomIndex], mutable[randomIndex]); 111 | }, [JSC.integer(), JSC.any()]); 112 | }); 113 | } 114 | 115 | if (config.id === "prod") { 116 | it("can have its elements directly mutated", function () { 117 | var immutableArr = Immutable([1, 2, 3]); 118 | immutableArr[0] = 4; 119 | assert.equal(immutableArr[0], 4); 120 | 121 | immutableArr.sort(); 122 | TestUtils.assertJsonEqual(immutableArr, [2, 3, 4]); 123 | }); 124 | } 125 | 126 | it("makes nested content immutable as well", function() { 127 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 128 | mutable.push(innerArray); // Make a nested immutable 129 | mutable.push(obj); // Get an object in there too 130 | 131 | immutable = Immutable(mutable); 132 | 133 | assert.strictEqual(immutable.length, mutable.length); 134 | 135 | for (var index in mutable) { 136 | TestUtils.assertIsDeeplyImmutable(immutable[index]); 137 | } 138 | 139 | TestUtils.assertIsDeeplyImmutable(immutable); 140 | }); 141 | }); 142 | 143 | it("adds instance methods only when config tells it to", function() { 144 | var I = Immutable.static; 145 | var immutable = I({}); 146 | assert.equal(typeof immutable.merge, 'undefined'); 147 | assert.equal(typeof immutable.replace, 'undefined'); 148 | assert.equal(typeof immutable.without, 'undefined'); 149 | assert.equal(typeof immutable.asMutable, 'undefined'); 150 | assert.equal(typeof immutable.set, 'undefined'); 151 | assert.equal(typeof immutable.setIn, 'undefined'); 152 | assert.equal(typeof immutable.update, 'undefined'); 153 | assert.equal(typeof immutable.updateIn, 'undefined'); 154 | var immutable = Immutable({}); 155 | assert.equal(typeof immutable.merge, 'function'); 156 | assert.equal(typeof immutable.replace, 'function'); 157 | assert.equal(typeof immutable.without, 'function'); 158 | assert.equal(typeof immutable.asMutable, 'function'); 159 | assert.equal(typeof immutable.set, 'function'); 160 | assert.equal(typeof immutable.setIn, 'function'); 161 | assert.equal(typeof immutable.update, 'function'); 162 | assert.equal(typeof immutable.updateIn, 'function'); 163 | }); 164 | 165 | // TODO this never fails under Node, even after removing Immutable.Array's 166 | // call to toImmutable(). Need to verify that it can fail in browsers. 167 | it("reuses existing immutables during construction", function() { 168 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 169 | mutable.push(innerArray); // Make a nested immutable 170 | mutable.push(obj); // Get an object in there too 171 | 172 | immutable = Immutable(mutable); 173 | 174 | var copiedArray = Immutable(immutable); 175 | 176 | assert.strictEqual(copiedArray.length, immutable.length); 177 | 178 | for (var index in copiedArray) { 179 | TestUtils.assertJsonEqual(immutable[index], copiedArray[index]); 180 | } 181 | }, [JSC.array(), JSC.object()]); 182 | }); 183 | }); 184 | }); 185 | 186 | // Add a "returns immutable" claim for each non-mutating method on Array. 187 | nonMutatingArrayMethods = { 188 | map: [JSC.literal(identityFunction)], 189 | filter: [JSC.literal(identityFunction)], 190 | reduce: [JSC.literal(identityFunction), JSC.any()], 191 | reduceRight: [JSC.literal(identityFunction), JSC.any()], 192 | concat: [JSC.array()], 193 | slice: [JSC.integer(), JSC.integer()] 194 | } 195 | 196 | _.each(nonMutatingArrayMethods, function(specifiers, methodName) { 197 | it("returns only immutables when you call its " + 198 | methodName + "() method", function() { 199 | checkImmutableMutable(function(immutable, mutable) { 200 | var methodArgs = specifiers.map(function(generator) { return generator() }); 201 | TestUtils.assertImmutable(methodName, immutable, mutable, methodArgs); 202 | }); 203 | }); 204 | }); 205 | 206 | [ // Add a "reports banned" claim for each mutating method on Array. 207 | "setPrototypeOf", "push", "pop", "sort", "splice", "shift", "unshift", "reverse" 208 | ].forEach(function(methodName) { 209 | var description = "it throws an ImmutableError when you try to call its " + 210 | methodName + "() method"; 211 | 212 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 213 | assert.throw(function() { 214 | array[methodName].apply(array, methodArgs); 215 | }); 216 | }, function() { 217 | return new Immutable.ImmutableError(); 218 | }); 219 | }); 220 | }; 221 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-flatMap.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | var check = TestUtils.check; 10 | 11 | describe("#flatMap", function() { 12 | // Sanity check to make sure our QuickCheck logic isn't off the rails. 13 | it("passes a basic sanity check on canned input", function() { 14 | var expected = Immutable(["foo", "foo2", "bar", "bar2", "baz", "baz2"]); 15 | var actual = Immutable.flatMap(Immutable(["foo", "bar", "baz"]), function(elem) { 16 | return [elem, elem + "2"]; 17 | }); 18 | 19 | TestUtils.assertJsonEqual(actual, expected); 20 | }); 21 | 22 | it("works the same way as map when the iterator function returns non-arrays", function() { 23 | check(100, [JSC.array()], function(array) { 24 | var returnValues = array.map(function() { return JSC.any()(); }); 25 | var iterator = function(value, index) { return returnValues[index]; }; 26 | var expected = _.map(array, iterator); 27 | var actual = Immutable.flatMap(Immutable(array), iterator); 28 | 29 | TestUtils.assertJsonEqual(actual, expected); 30 | }); 31 | }); 32 | 33 | it("passes the expected index value", function() { 34 | check(100, [JSC.array()], function(array) { 35 | var iterator = function(value, index) { return index; }; 36 | var expected = _.map(array, iterator); 37 | var actual = Immutable.flatMap(Immutable(array), iterator); 38 | 39 | TestUtils.assertJsonEqual(actual, expected); 40 | }); 41 | }); 42 | 43 | it("works the same way as map followed by flatten when the iterator function returns arrays", function() { 44 | check(100, [JSC.array()], function(array) { 45 | var returnValues = array.map(function() { return [JSC.any()()]; }); 46 | var iterator = function(value, index) { return returnValues[index]; }; 47 | var expected = _.flatten(_.map(array, iterator)); 48 | var actual = Immutable.flatMap(Immutable(array), iterator); 49 | 50 | TestUtils.assertJsonEqual(actual, expected); 51 | }); 52 | }); 53 | 54 | it("works the same way as flatten when called with no arguments", function() { 55 | check(100, [JSC.array()], function(array) { 56 | var expected = _.flatten(array); 57 | var actual = Immutable.flatMap(Immutable(array)); 58 | 59 | TestUtils.assertJsonEqual(actual, expected); 60 | }); 61 | }); 62 | 63 | it("results in an empty array if the iterator function returns empty array", function() { 64 | var expected = Immutable([]); 65 | var iterator = function() { return [] }; 66 | 67 | check(100, [JSC.array()], function(array) { 68 | var actual = Immutable.flatMap(array, iterator); 69 | 70 | TestUtils.assertJsonEqual(actual, expected); 71 | }); 72 | }); 73 | 74 | it("supports non-static syntax", function() { 75 | function dummyUpdater(data) { 76 | return data + '_updated'; 77 | } 78 | var obj = Immutable(['test']); 79 | obj = obj.flatMap(dummyUpdater); 80 | TestUtils.assertJsonEqual(obj, ['test_updated']); 81 | }); 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-getIn.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var _ = require("lodash"); 3 | var getTestUtils = require("../TestUtils.js"); 4 | 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | var check = TestUtils.check; 10 | 11 | describe("#getIn", function() { 12 | it("gets a property by path", function () { 13 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) { 14 | var immutable = Immutable(array); 15 | var idx = JSC.integer(0, array.length-1); 16 | var key = JSC.one_of(_.keys(immutable[idx]))(); 17 | var value = immutable[idx][key]; 18 | 19 | TestUtils.assertJsonEqual( 20 | Immutable.getIn(immutable, [idx, key]), 21 | value 22 | ); 23 | }); 24 | }); 25 | 26 | it("returns the default value if the resolved value is undefined", function () { 27 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 28 | var immutable = Immutable([0,1,2]); 29 | 30 | TestUtils.assertJsonEqual( 31 | Immutable.getIn(immutable, [3], 'default'), 32 | 'default' 33 | ); 34 | }); 35 | }); 36 | 37 | it("supports non-static syntax", function() { 38 | var obj = Immutable(['test']); 39 | obj = obj.getIn([0]); 40 | TestUtils.assertJsonEqual(obj, 'test'); 41 | }); 42 | }); 43 | }; 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-set.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | 7 | module.exports = function(config) { 8 | var Immutable = config.implementation; 9 | var TestUtils = getTestUtils(Immutable); 10 | var check = TestUtils.check; 11 | 12 | function getPathComponent() { 13 | // It's very convenient to use lodash.set, but it has funky behaviour 14 | // with numeric keys. 15 | var s = JSC.string()().replace(/[^\w]/g, '_'); 16 | return /^\d+$/.test(s) ? s + 'a' : s; 17 | } 18 | 19 | describe("#set", function() { 20 | it("sets an array element", function () { 21 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier, JSC.any()]) ], function(array) { 22 | var immutable = Immutable(array); 23 | var mutable = array.slice(); 24 | var index = JSC.integer(0, array.length); 25 | var newValue = JSC.any(); 26 | 27 | var resultImmutable = Immutable.set(immutable, index, newValue); 28 | var resultMutable = mutable.slice(); 29 | resultMutable[index] = newValue; 30 | 31 | TestUtils.assertJsonEqual(resultImmutable, resultMutable); 32 | }); 33 | }); 34 | 35 | it("sets an array element with deep compare if provided the deep flag", function () { 36 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) { 37 | var immutable = Immutable(array); 38 | var mutable = array.slice(); 39 | var index = JSC.integer(0, array.length); 40 | var value; 41 | 42 | var newValue; 43 | do { 44 | value = JSC.any()(); 45 | } while (TestUtils.isDeepEqual(value, array[index])); 46 | 47 | var resultImmutable = Immutable.set(immutable, index, newValue, {deep: true}); 48 | var resultMutable = mutable.slice(); 49 | resultMutable[index] = newValue; 50 | 51 | TestUtils.assertJsonEqual( 52 | resultImmutable, 53 | resultMutable 54 | ); 55 | assert.notEqual( 56 | immutable, 57 | resultImmutable 58 | ); 59 | assert.equal( 60 | Immutable.set(resultImmutable, index, newValue, {deep: true}), 61 | resultImmutable 62 | ); 63 | }); 64 | }); 65 | 66 | it("supports non-static syntax", function() { 67 | var obj = Immutable(['test']); 68 | obj = obj.set('0', 'new_data'); 69 | TestUtils.assertJsonEqual(obj, ['new_data']); 70 | }); 71 | }); 72 | 73 | describe("#setIn", function() { 74 | it("sets a property by path", function () { 75 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) { 76 | var immutable = Immutable(array); 77 | var mutable = array.slice(); 78 | var value = JSC.any()(); 79 | 80 | var idx = JSC.integer(0, array.length-1); 81 | var key = JSC.one_of(_.keys(immutable[idx]))(); 82 | 83 | var util = require('util'); 84 | function printArr(arr) { 85 | return '[\n\t>'+_.map(arr, util.inspect).join('\n\t>')+'\n]'; 86 | } 87 | 88 | TestUtils.assertJsonEqual(immutable, mutable); 89 | var resultMutable = mutable.slice(); 90 | resultMutable[idx][key] = value; 91 | 92 | TestUtils.assertJsonEqual( 93 | Immutable.setIn(immutable, [idx, key], value), 94 | resultMutable 95 | ); 96 | }); 97 | }); 98 | 99 | it("sets a property by path with deep compare if provided the deep flag", function () { 100 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) { 101 | var immutable = Immutable(array); 102 | var mutable = array.slice(); 103 | 104 | var idx = JSC.integer(0, array.length-1); 105 | var key = JSC.one_of(_.keys(immutable[idx]))(); 106 | var value; 107 | do { 108 | value = JSC.any()(); 109 | } while (TestUtils.isDeepEqual(value, immutable[idx][key])); 110 | 111 | TestUtils.assertJsonEqual(immutable, mutable); 112 | 113 | var resultImmutable = Immutable.setIn(immutable, [idx, key], value, {deep: true}); 114 | var resultMutable = mutable.slice(); 115 | resultMutable[idx][key] = value; 116 | 117 | TestUtils.assertJsonEqual( 118 | resultImmutable, 119 | resultMutable 120 | ); 121 | assert.notEqual( 122 | immutable, 123 | resultImmutable 124 | ); 125 | assert.equal( 126 | Immutable.setIn(resultImmutable, [idx, key], value, {deep: true}), 127 | resultImmutable 128 | ); 129 | }); 130 | }); 131 | 132 | it("supports non-static syntax", function() { 133 | var obj = Immutable(['test']); 134 | obj = obj.setIn(['0'], 'new_data'); 135 | TestUtils.assertJsonEqual(obj, ['new_data']); 136 | }); 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /test/ImmutableArray/test-update.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | 7 | module.exports = function(config) { 8 | var Immutable = config.implementation; 9 | var TestUtils = getTestUtils(Immutable); 10 | var check = TestUtils.check; 11 | 12 | function getPathComponent() { 13 | // It's very convenient to use lodash.set, but it has funky behaviour 14 | // with numeric keys. 15 | var s = JSC.string()().replace(/[^\w]/g, '_'); 16 | return /^\d+$/.test(s) ? s + 'a' : s; 17 | } 18 | 19 | function dummyUpdater (x) { 20 | return x + "updated"; 21 | } 22 | 23 | describe("#update", function() { 24 | it("updates an array element using updater function", function () { 25 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier, JSC.any()]) ], function(array) { 26 | var immutable = Immutable(array); 27 | var mutable = Immutable.asMutable(immutable); 28 | var index = JSC.integer(0, array.length); 29 | 30 | immutable = Immutable.update(immutable, index, dummyUpdater); 31 | mutable[index] = dummyUpdater(mutable[index]); 32 | 33 | TestUtils.assertJsonEqual(immutable, mutable); 34 | }); 35 | }); 36 | 37 | it("supports non-static syntax", function() { 38 | function dummyUpdater(data) { 39 | return data + '_updated'; 40 | } 41 | var obj = Immutable(['test']); 42 | obj = obj.update('0', dummyUpdater); 43 | TestUtils.assertJsonEqual(obj, ['test_updated']); 44 | }); 45 | }); 46 | 47 | describe("#updateIn", function() { 48 | it("updates a property by path using updater function", function () { 49 | check(100, [ JSC.array([TestUtils.TraversableObjectSpecifier]) ], function(array) { 50 | var immutable = Immutable(array); 51 | // var value = JSC.any()(); 52 | 53 | var idx = JSC.integer(0, array.length-1); 54 | var key = JSC.one_of(_.keys(immutable[idx]))(); 55 | 56 | var util = require('util'); 57 | function printArr(arr) { 58 | return '[\n\t>'+_.map(arr, util.inspect).join('\n\t>')+'\n]'; 59 | } 60 | 61 | var mutable = Immutable.asMutable(immutable); 62 | TestUtils.assertJsonEqual(immutable, mutable); 63 | if (Immutable.isImmutable(mutable[idx])) { 64 | mutable[idx] = Immutable.asMutable(mutable[idx]); 65 | } 66 | mutable[idx][key] = dummyUpdater(mutable[idx][key]); 67 | 68 | TestUtils.assertJsonEqual( 69 | Immutable.updateIn(immutable, [idx, key], dummyUpdater), 70 | mutable 71 | ); 72 | }); 73 | }); 74 | 75 | it("supports non-static syntax", function() { 76 | function dummyUpdater(data) { 77 | return data + '_updated'; 78 | } 79 | var obj = Immutable(['test']); 80 | obj = obj.updateIn(['0'], dummyUpdater); 81 | TestUtils.assertJsonEqual(obj, ['test_updated']); 82 | }); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /test/ImmutableDate.spec.js: -------------------------------------------------------------------------------- 1 | var testAsMutable = require("./ImmutableDate/test-asMutable.js"); 2 | var testCompat = require("./ImmutableDate/test-compat.js"); 3 | var devBuild = require("../seamless-immutable.development.js"); 4 | var prodBuild = require("../seamless-immutable.production.min.js"); 5 | var getTestUtils = require("./TestUtils.js"); 6 | 7 | [ 8 | {id: "dev", name: "Development build", implementation: devBuild}, 9 | {id: "prod", name: "Production build", implementation: prodBuild} 10 | ].forEach(function(config) { 11 | var Immutable = config.implementation; 12 | var TestUtils = getTestUtils(Immutable); 13 | 14 | describe(config.name, function () { 15 | describe("ImmutableDate", function () { 16 | testCompat(config); 17 | testAsMutable(config); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/ImmutableDate/test-asMutable.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | 10 | describe("#asMutable", function() { 11 | it("returns mutable Date from an immutable Date", function() { 12 | var immutable = Immutable(new Date()); 13 | var mutable = Immutable.asMutable(immutable); 14 | 15 | assertNotArray(mutable); 16 | assertCanBeMutated(mutable); 17 | assert.isFalse(Immutable.isImmutable(mutable)); 18 | assert.deepEqual(immutable, mutable); 19 | assert.equal(Object.keys(mutable).length, 0); 20 | }); 21 | 22 | it("returns mutable Date from an immutable Date when using the deep flag", function() { 23 | var immutable = Immutable(new Date()); 24 | var mutable = Immutable.asMutable(immutable, {deep: true}); 25 | 26 | assertNotArray(mutable); 27 | assertCanBeMutated(mutable); 28 | assert.isFalse(Immutable.isImmutable(mutable)); 29 | assert.deepEqual(immutable, mutable); 30 | assert.equal(Object.keys(mutable).length, 0); 31 | }); 32 | 33 | it("preserves prototypes after call to asMutable", function() { 34 | var data = new Date(); 35 | 36 | var immutable = Immutable(data, {prototype: Date.prototype}); 37 | var result = Immutable.asMutable(immutable); 38 | 39 | assert.deepEqual(result, data); 40 | TestUtils.assertHasPrototype(result, Date.prototype); 41 | }); 42 | 43 | 44 | it("supports non-static syntax", function() { 45 | var mutable = new Date(); 46 | var obj = Immutable(mutable); 47 | obj = obj.asMutable(); 48 | TestUtils.assertJsonEqual(obj.getTime(), mutable.getTime()); 49 | assertCanBeMutated(obj); 50 | }); 51 | }); 52 | 53 | function assertCanBeMutated(obj) { 54 | try { 55 | var newElement = { foo: "bar" }; 56 | var originalKeyCount = Object.keys(obj).length; 57 | var key = "__test__field__"; 58 | 59 | assert.equal(false, obj.hasOwnProperty(key)); 60 | 61 | obj[key] = newElement; 62 | 63 | assert.equal(true, obj.hasOwnProperty(key)); 64 | 65 | assert.equal(obj[key], newElement); 66 | assert.equal(Object.keys(obj).length, originalKeyCount + 1); 67 | 68 | delete obj[key]; 69 | } catch(error) { 70 | assert.fail("Exception when trying to verify that this object was mutable: " + JSON.stringify(obj)); 71 | } 72 | } 73 | 74 | function assertNotArray(obj) { 75 | assert(!(obj instanceof Array), "Did not expect an Array, but got one. Got: " + JSON.stringify(obj)) 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /test/ImmutableDate/test-compat.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | function notParseableAsInt(str) { 7 | return parseInt(str).toString() !== str; 8 | } 9 | 10 | module.exports = function(config) { 11 | var Immutable = config.implementation; 12 | var TestUtils = getTestUtils(Immutable); 13 | 14 | var checkImmutableMutable = TestUtils.checkImmutableMutable(1, [JSC.literal(new Date())]); 15 | 16 | describe("which is compatible with vanilla mutable Dates", function() { 17 | it("is an instance of Date", function () { 18 | checkImmutableMutable(function(immutable, mutable) { 19 | assert.instanceOf(immutable, Date); 20 | }); 21 | }); 22 | 23 | it("has the same keys as its mutable equivalent", function() { 24 | checkImmutableMutable(function(immutable, mutable) { 25 | // Exclude properties that can be parsed as 32-bit unsigned integers, 26 | // as they have no sort order guarantees. 27 | var immutableKeys = Object.keys(immutable).filter(notParseableAsInt); 28 | var mutableKeys = Object.keys(mutable).filter(notParseableAsInt); 29 | 30 | assert.deepEqual(immutableKeys, mutableKeys); 31 | }); 32 | }); 33 | 34 | it("has a toString() method that works like a regular immutable's toString()", function() { 35 | checkImmutableMutable(function(immutable, mutable) { 36 | assert.strictEqual(immutable.toString(), mutable.toString()); 37 | }); 38 | }); 39 | 40 | it("supports being passed to JSON.stringify", function() { 41 | TestUtils.check(1, [JSC.literal(new Date())], function(mutable) { 42 | // Delete all the keys that could be parsed as 32-bit unsigned integers, 43 | // as there is no guaranteed sort order for them. 44 | for (var key in mutable) { 45 | if (!notParseableAsInt(mutable[key])) { 46 | delete mutable[key]; 47 | } 48 | } 49 | 50 | var immutable = Immutable(mutable); 51 | assert.deepEqual(JSON.stringify(immutable), JSON.stringify(mutable)); 52 | }); 53 | }); 54 | 55 | if (config.id === "dev") { 56 | it("is frozen", function () { 57 | checkImmutableMutable(function(immutable, mutable) { 58 | assert.isTrue(Object.isFrozen(immutable)); 59 | }); 60 | }); 61 | } 62 | 63 | if (config.id === "prod") { 64 | it("is frozen", function () { 65 | checkImmutableMutable(function(immutable, mutable) { 66 | assert.isFalse(Object.isFrozen(immutable)); 67 | }); 68 | }); 69 | } 70 | 71 | it("is tagged as immutable", function() { 72 | checkImmutableMutable(function(immutable, mutable) { 73 | TestUtils.assertIsDeeplyImmutable(immutable); 74 | }); 75 | }); 76 | 77 | [ // Add a "reports banned" claim for each mutating method on Date. 78 | "setDate", "setFullYear", "setHours", "setMilliseconds", "setMinutes", "setMonth", "setSeconds", 79 | "setTime", "setUTCDate", "setUTCFullYear", "setUTCHours", "setUTCMilliseconds", "setUTCMinutes", 80 | "setUTCMonth", "setUTCSeconds", "setYear" 81 | ].forEach(function(methodName) { 82 | var description = "it throws an ImmutableError when you try to call its " + 83 | methodName + "() method"; 84 | 85 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 86 | assert.throw(function() { 87 | date[methodName].apply(date, methodArgs); 88 | }); 89 | }, function() { 90 | return new Immutable.ImmutableError(); 91 | }); 92 | }); 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /test/ImmutableError.spec.js: -------------------------------------------------------------------------------- 1 | var testCompat = require("./ImmutableError/test-compat.js"); 2 | var devBuild = require("../seamless-immutable.development.js"); 3 | var prodBuild = require("../seamless-immutable.production.min.js"); 4 | var getTestUtils = require("./TestUtils.js"); 5 | 6 | [ 7 | {id: "dev", name: "Development build", implementation: devBuild}, 8 | {id: "prod", name: "Production build", implementation: prodBuild} 9 | ].forEach(function(config) { 10 | var Immutable = config.implementation; 11 | var TestUtils = getTestUtils(Immutable); 12 | 13 | describe(config.name, function () { 14 | describe("ImmutableError", function () { 15 | testCompat(config); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/ImmutableError/test-compat.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | function notParseableAsInt(str) { 7 | return parseInt(str).toString() !== str; 8 | } 9 | 10 | module.exports = function(config) { 11 | var Immutable = config.implementation; 12 | var TestUtils = getTestUtils(Immutable); 13 | 14 | it("is an instance of ImmutableError", function () { 15 | var e = new Immutable.ImmutableError(); 16 | assert.instanceOf(e, Immutable.ImmutableError); 17 | }); 18 | 19 | it("is an instance of Error", function () { 20 | var e = new Immutable.ImmutableError(); 21 | assert.instanceOf(e, Error); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /test/ImmutableObject.spec.js: -------------------------------------------------------------------------------- 1 | var testMerge = require("./ImmutableObject/test-merge.js"); 2 | var testReplace = require("./ImmutableObject/test-replace.js"); 3 | var testCompat = require("./ImmutableObject/test-compat.js"); 4 | var testWithout = require("./ImmutableObject/test-without.js"); 5 | var testAsMutable = require("./ImmutableObject/test-asMutable.js"); 6 | var testSet = require("./ImmutableObject/test-set.js"); 7 | var testUpdate = require("./ImmutableObject/test-update.js"); 8 | var testGetIn = require("./ImmutableObject/test-getIn.js"); 9 | var devBuild = require("../seamless-immutable.development.js"); 10 | var prodBuild = require("../seamless-immutable.production.min.js"); 11 | var getTestUtils = require("./TestUtils.js"); 12 | 13 | [ 14 | {id: "dev", name: "Development build", implementation: devBuild}, 15 | {id: "prod", name: "Production build", implementation: prodBuild} 16 | ].forEach(function(config) { 17 | var Immutable = config.implementation; 18 | var TestUtils = getTestUtils(Immutable); 19 | 20 | describe(config.name, function () { 21 | describe("ImmutableObject", function () { 22 | testCompat(config); 23 | testMerge(config); 24 | testReplace(config); 25 | testWithout(config); 26 | testAsMutable(config); 27 | testSet(config); 28 | testUpdate(config); 29 | testGetIn(config); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-asMutable.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | module.exports = function(config) { 7 | var Immutable = config.implementation; 8 | var TestUtils = getTestUtils(Immutable); 9 | var check = TestUtils.check; 10 | 11 | describe("#asMutable", function() { 12 | it("returns an empty mutable oject from an empty immutable array", function() { 13 | var immutable = Immutable({}); 14 | var mutable = Immutable.asMutable(immutable); 15 | 16 | assertNotArray(mutable); 17 | assertCanBeMutated(mutable); 18 | assert.isFalse( Immutable.isImmutable(mutable)); 19 | TestUtils.assertJsonEqual(immutable,mutable); 20 | assert.equal(Object.keys(mutable).length, 0); 21 | }); 22 | 23 | it("returns a shallow mutable copy if not provided the deep flag", function() { 24 | check(100, [ TestUtils.TraversableObjectSpecifier ], function(obj) { 25 | var immutable = Immutable(obj); 26 | var mutable = Immutable.asMutable(immutable); 27 | 28 | assertNotArray(mutable); 29 | assertCanBeMutated(mutable); 30 | assert.isFalse(Immutable.isImmutable(mutable)); 31 | TestUtils.assertIsDeeplyImmutable(mutable.complex); 32 | TestUtils.assertIsDeeplyImmutable(mutable.deep.complex); 33 | TestUtils.assertJsonEqual(immutable,mutable); 34 | }) 35 | }) 36 | 37 | it("returns a deep mutable copy if provided the deep flag", function() { 38 | check(100, [ TestUtils.TraversableObjectSpecifier ], function(obj) { 39 | var immutable = Immutable(obj); 40 | var mutable = Immutable.asMutable(immutable, { deep: true }); 41 | 42 | assertNotArray(mutable); 43 | assertCanBeMutated(mutable); 44 | assert.isFalse(Immutable.isImmutable(mutable)); 45 | assert.isFalse(Immutable.isImmutable(mutable['complex'])); 46 | assert.isFalse(Immutable.isImmutable(mutable['deep']['complex'])); 47 | TestUtils.assertJsonEqual(immutable,mutable); 48 | }); 49 | }); 50 | 51 | it("does not throw an error when asMutable deep = true is called on an Immutable with a nested date", function() { 52 | check(100, [ TestUtils.TraversableObjectSpecifier ], function(obj) { 53 | var test = Immutable({ testDate: new Date()}); 54 | Immutable.asMutable(test, {deep: true}); 55 | }); 56 | }); 57 | 58 | it("preserves prototypes after call to asMutable", function() { 59 | function TestClass(o) { _.extend(this, o); }; 60 | var data = new TestClass({a: 1, b: 2}); 61 | 62 | var immutable = Immutable(data, {prototype: TestClass.prototype}); 63 | var result = Immutable.asMutable(immutable); 64 | 65 | TestUtils.assertJsonEqual(result, data); 66 | TestUtils.assertHasPrototype(result, TestClass.prototype); 67 | }); 68 | 69 | it("static method continues to work after overriding the instance method", function() { 70 | var I = Immutable.static; 71 | 72 | var immutable; 73 | 74 | immutable = I({asMutable: 'string'}); 75 | TestUtils.assertJsonEqual(immutable, {asMutable: 'string'}); 76 | 77 | immutable = I({}); 78 | immutable = I.setIn(immutable, ['asMutable'], 'string'); 79 | TestUtils.assertJsonEqual(immutable, {asMutable: 'string'}); 80 | 81 | immutable = I({asMutable: 'string'}); 82 | var mutable = I.asMutable(immutable); 83 | TestUtils.assertJsonEqual(immutable, mutable); 84 | assertCanBeMutated(mutable); 85 | }); 86 | 87 | it("supports non-static syntax", function() { 88 | var obj = Immutable({test: 'test'}); 89 | obj = obj.asMutable(); 90 | TestUtils.assertJsonEqual(obj, {test: 'test'}); 91 | assertCanBeMutated(obj); 92 | }); 93 | }); 94 | 95 | function assertCanBeMutated(obj) { 96 | try { 97 | var newElement = { foo: "bar" }; 98 | var originalKeyCount = Object.keys(obj).length; 99 | var key = "__test__field__"; 100 | 101 | assert.equal(false, obj.hasOwnProperty(key)); 102 | 103 | obj[key] = newElement; 104 | 105 | assert.equal(true, obj.hasOwnProperty(key)); 106 | 107 | assert.equal(obj[key], newElement); 108 | assert.equal(Object.keys(obj).length, originalKeyCount + 1); 109 | 110 | delete obj[key]; 111 | } catch(error) { 112 | assert.fail("Exception when trying to verify that this object was mutable: " + JSON.stringify(obj)); 113 | } 114 | } 115 | 116 | function assertNotArray(obj) { 117 | assert(!(obj instanceof Array), "Did not expect an Array, but got one. Got: " + JSON.stringify(obj)) 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-compat.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | var identityFunction = function(arg) { return arg; }; 7 | 8 | function notParseableAsInt(str) { 9 | return parseInt(str).toString() !== str; 10 | } 11 | 12 | module.exports = function(config) { 13 | var Immutable = config.implementation; 14 | var TestUtils = getTestUtils(Immutable); 15 | 16 | var checkImmutableMutable = TestUtils.checkImmutableMutable(100, [TestUtils.ComplexObjectSpecifier()]); 17 | 18 | describe("which is compatible with vanilla mutable objects", function() { 19 | it("is an instance of Object", function() { 20 | checkImmutableMutable(function(immutable, mutable) { 21 | assert.instanceOf(immutable, Object); 22 | }); 23 | }); 24 | 25 | it("has the same keys as its mutable equivalent", function() { 26 | checkImmutableMutable(function(immutable, mutable) { 27 | // Exclude properties that can be parsed as 32-bit unsigned integers, 28 | // as they have no sort order guarantees. 29 | var immutableKeys = Object.keys(immutable).filter(notParseableAsInt); 30 | var mutableKeys = Object.keys(mutable).filter(notParseableAsInt); 31 | 32 | TestUtils.assertJsonEqual(immutableKeys, mutableKeys); 33 | }); 34 | }); 35 | 36 | it("supports accessing elements by index via []", function() { 37 | checkImmutableMutable(function(immutable, mutable, index) { 38 | assert.typeOf(index, "number"); 39 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 40 | }, [JSC.integer()]); 41 | }); 42 | 43 | it("works with for loops", function() { 44 | checkImmutableMutable(function(immutable, mutable) { 45 | for (var index=0; index < immutable.length; index++) { 46 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 47 | } 48 | }); 49 | }); 50 | 51 | it("works with for..in loops", function() { 52 | checkImmutableMutable(function(immutable, mutable) { 53 | for (var index in immutable) { 54 | TestUtils.assertJsonEqual(immutable[index], mutable[index]); 55 | } 56 | }); 57 | }); 58 | 59 | it("has a toString() method that works like a regular immutable's toString()", function() { 60 | checkImmutableMutable(function(immutable, mutable) { 61 | assert.strictEqual(immutable.toString(), mutable.toString()); 62 | }); 63 | }); 64 | 65 | it("supports being passed to JSON.stringify", function() { 66 | TestUtils.check(100, [JSC.array()], function(mutable) { 67 | // Delete all the keys that could be parsed as 32-bit unsigned integers, 68 | // as there is no guaranteed sort order for them. 69 | for (var key in mutable) { 70 | if (!notParseableAsInt(mutable[key])) { 71 | delete mutable[key]; 72 | } 73 | } 74 | 75 | var immutable = Immutable(mutable); 76 | TestUtils.assertJsonEqual(JSON.stringify(immutable), JSON.stringify(mutable)); 77 | }); 78 | }); 79 | 80 | if (config.id === "dev") { 81 | it("is frozen", function () { 82 | checkImmutableMutable(function (immutable, mutable) { 83 | assert.isTrue(Object.isFrozen(immutable)); 84 | }); 85 | }); 86 | } 87 | 88 | if (config.id === "prod") { 89 | it("is frozen", function () { 90 | checkImmutableMutable(function (immutable, mutable) { 91 | assert.isFalse(Object.isFrozen(immutable)); 92 | }); 93 | }); 94 | } 95 | 96 | it("is tagged as immutable", function() { 97 | checkImmutableMutable(function(immutable, mutable) { 98 | TestUtils.assertIsDeeplyImmutable(immutable); 99 | }) 100 | }); 101 | 102 | if (config.id === "dev") { 103 | it("cannot have its elements directly mutated", function () { 104 | checkImmutableMutable(function (immutable, mutable, randomIndex, randomData) { 105 | immutable[randomIndex] = randomData; 106 | 107 | assert.typeOf(randomIndex, "number"); 108 | assert.strictEqual(immutable.length, mutable.length); 109 | TestUtils.assertJsonEqual(immutable[randomIndex], mutable[randomIndex]); 110 | }, [JSC.integer(), JSC.any()]); 111 | }); 112 | } 113 | 114 | if (config.id === "prod") { 115 | it("can have its elements directly mutated", function () { 116 | var immutable = Immutable({id: 3, name: "base"}); 117 | 118 | immutable.id = 4; 119 | immutable.name = "belong"; 120 | 121 | assert.equal(immutable.id, 4); 122 | assert.equal(immutable.name, "belong"); 123 | }); 124 | } 125 | 126 | it("supports handling dates without converting them to regular objects", function() { 127 | var date = new Date(); 128 | var immutableDate = Immutable(date); 129 | 130 | TestUtils.assertIsDeeplyImmutable(immutableDate); 131 | assert.isTrue(immutableDate instanceof Date); 132 | 133 | assert.notEqual(date, immutableDate); 134 | assert.equal(date.getTime(), immutableDate.getTime()); 135 | 136 | var objectWithDate = {date: new Date()}; 137 | var immutableObjectWithDate = Immutable(objectWithDate); 138 | 139 | TestUtils.assertIsDeeplyImmutable(immutableObjectWithDate); 140 | assert.isTrue(immutableObjectWithDate.date instanceof Date); 141 | 142 | assert.notEqual(objectWithDate, immutableObjectWithDate); 143 | assert.equal(objectWithDate.date.getTime(), immutableObjectWithDate.date.getTime()); 144 | }); 145 | 146 | it("supports handling dates without using prototype option", function() { 147 | var date = new Date(); 148 | var immutableDate = Immutable(date, {prototype: Object.prototype}); 149 | 150 | assert.isTrue(immutableDate instanceof Date); 151 | }); 152 | 153 | it("makes nested content immutable as well", function() { 154 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 155 | mutable.foo = innerArray; // Make a nested immutable array 156 | mutable.bar = obj; // Get an object in there too 157 | 158 | immutable = Immutable(mutable); 159 | 160 | assert.strictEqual(immutable.length, mutable.length); 161 | 162 | for (var index in mutable) { 163 | TestUtils.assertIsDeeplyImmutable(immutable[index]); 164 | } 165 | 166 | TestUtils.assertIsDeeplyImmutable(immutable); 167 | }); 168 | }); 169 | 170 | it("adds instance methods only when config tells it to", function() { 171 | var I = Immutable.static; 172 | var immutable = I([]); 173 | assert.equal(typeof immutable.flatMap, 'undefined'); 174 | assert.equal(typeof immutable.asObject, 'undefined'); 175 | assert.equal(typeof immutable.asMutable, 'undefined'); 176 | assert.equal(typeof immutable.set, 'undefined'); 177 | assert.equal(typeof immutable.setIn, 'undefined'); 178 | assert.equal(typeof immutable.update, 'undefined'); 179 | assert.equal(typeof immutable.updateIn, 'undefined'); 180 | var immutable = Immutable([]); 181 | assert.equal(typeof immutable.flatMap, 'function'); 182 | assert.equal(typeof immutable.asObject, 'function'); 183 | assert.equal(typeof immutable.asMutable, 'function'); 184 | assert.equal(typeof immutable.set, 'function'); 185 | assert.equal(typeof immutable.setIn, 'function'); 186 | assert.equal(typeof immutable.update, 'function'); 187 | assert.equal(typeof immutable.updateIn, 'function'); 188 | }); 189 | 190 | // TODO this never fails under Node, even after removing Immutable.Array's 191 | // call to toImmutable(). Need to verify that it can fail in browsers. 192 | it("reuses existing immutables during construction", function() { 193 | checkImmutableMutable(function(immutable, mutable, innerArray, obj) { 194 | mutable.foo = innerArray; // Make a nested immutable array 195 | mutable.bar = obj; // Get an object in there too 196 | 197 | immutable = Immutable(mutable); 198 | 199 | var copiedArray = Immutable(immutable); 200 | 201 | assert.strictEqual(copiedArray.length, immutable.length); 202 | 203 | for (var index in copiedArray) { 204 | TestUtils.assertJsonEqual(immutable[index], copiedArray[index]); 205 | } 206 | }, [JSC.array(), JSC.object()]); 207 | }); 208 | }); 209 | }; 210 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-getIn.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | 7 | module.exports = function(config) { 8 | var Immutable = config.implementation; 9 | var TestUtils = getTestUtils(Immutable); 10 | var check = TestUtils.check; 11 | 12 | function getPathComponent() { 13 | // It's very convenient to use lodash.set, but it has funky behaviour 14 | // with numeric keys. 15 | var s = JSC.string()().replace(/[^\w]/g, '_'); 16 | return /^\d+$/.test(s) ? s + 'a' : s; 17 | } 18 | 19 | describe("#getIn", function() { 20 | it("gets a property by path", function () { 21 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 22 | var immutable = Immutable(ob); 23 | 24 | var path = [], depth = JSC.integer(1, 5)(); 25 | for (var j = 0; j < depth; j++) { 26 | path.push(getPathComponent()); 27 | } 28 | 29 | TestUtils.assertJsonEqual( 30 | Immutable.getIn(immutable, path), 31 | _.get(immutable, path) 32 | ); 33 | }); 34 | }); 35 | 36 | it("returns the default value if the resolved value is undefined", function () { 37 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 38 | var immutable = Immutable({ test: 'test'}); 39 | 40 | TestUtils.assertJsonEqual( 41 | Immutable.getIn(immutable, ['notFound'], 'default'), 42 | 'default' 43 | ); 44 | }); 45 | }); 46 | 47 | it("static method continues to work after overriding the instance method", function() { 48 | var I = Immutable.static; 49 | 50 | var immutable; 51 | 52 | immutable = I({getIn: 'string'}); 53 | TestUtils.assertJsonEqual(immutable, {getIn: 'string'}); 54 | var value = I.getIn(immutable, ['getIn']); 55 | TestUtils.assertJsonEqual(value, 'string'); 56 | }); 57 | 58 | it("supports non-static syntax", function() { 59 | var obj = Immutable({ test: 'test' }); 60 | var value = obj.getIn(['test']); 61 | TestUtils.assertJsonEqual(value, 'test'); 62 | }); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-merge.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | // Anything but an object, array, or undefined. 7 | function invalidMergeArgumentSpecifier() { 8 | return JSC.one_of([ 9 | (function() { return function() {}; }), 10 | JSC.integer(), JSC.number(), JSC.string(), 11 | true, Infinity, -Infinity 12 | ]); 13 | } 14 | 15 | module.exports = function(config) { 16 | var Immutable = config.implementation; 17 | var TestUtils = getTestUtils(Immutable); 18 | var check = TestUtils.check; 19 | 20 | function generateArrayOfObjects() { 21 | return JSC.array()().map(function() { return TestUtils.ComplexObjectSpecifier()(); }); 22 | } 23 | 24 | describe("#merge", function() { 25 | function generateMergeTestsFor(specifiers, config) { 26 | var runs = 100; 27 | 28 | function checkMultiple(callback) { 29 | check(runs, specifiers, function(list) { 30 | list = list instanceof Array ? list : [list]; 31 | 32 | assert.notStrictEqual(list.length, 0, "Can't usefully check merge() with no objects"); 33 | 34 | var immutable = Immutable(list[0]); 35 | 36 | function runMerge(others) { 37 | others = others || list; 38 | 39 | return Immutable.merge(immutable, others, config); 40 | } 41 | 42 | callback(immutable, list, runMerge); 43 | }) 44 | } 45 | 46 | it("prioritizes the arguments' properties", function() { 47 | checkMultiple(function(immutable, mutables, runMerge) { 48 | var expectedChanges = {}; 49 | 50 | _.each(mutables, function(mutable) { 51 | var keys = _.keys(immutable); 52 | 53 | assert.notStrictEqual(keys.length, 0, "Can't usefully check merge() with an empty object"); 54 | 55 | // Randomly change some values that share keys with the immutable. 56 | _.each(keys, function(key) { 57 | if (Math.random() > 0.5) { 58 | var value = immutable[key], 59 | suffix = JSC.string()(), 60 | newValue = value + suffix; 61 | 62 | assert.notStrictEqual(value, newValue, "Failed to change value (" + value + ") by appending \"" + suffix + "\""); 63 | 64 | // Record that we expect this to end up in the final result. 65 | expectedChanges[key] = newValue; 66 | assert(Immutable.isImmutable(expectedChanges[key])); 67 | 68 | mutable[key] = newValue; 69 | } else if (mutable.hasOwnProperty(key) && mutable[key] !== immutable[key]) { 70 | // NaN will break tests, though not the actual function. 71 | if (isNaN(mutable[key])) { 72 | mutable[key] = 0; 73 | } 74 | 75 | expectedChanges[key] = mutable[key]; 76 | } 77 | }); 78 | }); 79 | 80 | var result = runMerge(mutables); 81 | 82 | _.each(expectedChanges, function(expectedValue, key) { 83 | assert.notStrictEqual(expectedValue, immutable[key], 84 | "Expected to change key (" + key + "), but expected change was the same as the old value (" + expectedValue + ")"); 85 | 86 | assert.strictEqual(result[key], expectedValue, 87 | "The merged object did not keep the key/newValue pair as expected."); 88 | }); 89 | }); 90 | }); 91 | 92 | it("contains all the arguments' properties", function() { 93 | checkMultiple(function(immutable, mutables, runMerge) { 94 | var result = runMerge(); 95 | 96 | _.each(mutables, function(mutable, index) { 97 | _.each(mutable, function (value, key) { 98 | assert(result.hasOwnProperty(key), "Result " + JSON.stringify(result) + " did not have property " + key + " with value " + value + " from " + JSON.stringify(mutable)); 99 | }); 100 | }); 101 | }); 102 | }); 103 | 104 | it("contains all the original's properties", function() { 105 | checkMultiple(function(immutable, mutables, runMerge) { 106 | var result = runMerge(); 107 | 108 | _.each(immutable, function (value, key) { 109 | assert(result.hasOwnProperty(key)); 110 | }); 111 | }); 112 | }); 113 | 114 | it("does not reproduce except when required #70", function() { 115 | var immutable = Immutable({a: {b: 1, c: 1}}); 116 | 117 | // Non-deep merge is never equal for deep objects. 118 | assert.notStrictEqual(immutable, Immutable.merge(immutable, {a: {b: 1}})); 119 | 120 | // Deep merge for only some of the keys is equal, except in replace mode. 121 | assert.strictEqual(immutable, Immutable.merge(immutable, {a: {b: 1}}, {deep: true})); 122 | assert.notStrictEqual(immutable, Immutable.merge(immutable, {a: {b: 1}}, {deep: true, mode: 'replace'})); 123 | 124 | // Identical child objects in deep merge remain equal. 125 | assert.strictEqual(immutable.a, Immutable.merge(immutable, {d: {e: 2}}, {deep: true}).a); 126 | 127 | // Deep merge for all of the keys is always equal. 128 | assert.strictEqual(immutable, Immutable.merge(immutable, {a: {b: 1, c: 1}}, {deep: true})); 129 | assert.strictEqual(immutable, Immutable.merge(immutable, {a: {b: 1, c: 1}}, {deep: true, mode: 'replace'})); 130 | 131 | // Deep merge with updated data is never equal. 132 | assert.notStrictEqual(immutable, Immutable.merge(immutable, {a: {b: 1, c: 2}}, {deep: true})); 133 | assert.notStrictEqual(immutable, Immutable.merge(immutable, {a: {b: 1, c: 2}}, {deep: true, mode: 'replace'})); 134 | }); 135 | 136 | it("does nothing when merging an identical object", function() { 137 | checkMultiple(function(immutable, mutables, runMerge) { 138 | _.each(mutables, function(mutable, index) { 139 | var identicalImmutable = Immutable(mutable); 140 | 141 | assert.strictEqual(identicalImmutable, 142 | Immutable.merge(identicalImmutable, mutable, {deep: true})); 143 | assert.strictEqual(identicalImmutable, 144 | Immutable.merge(identicalImmutable, mutable, {deep: true, mode: 'replace'})); 145 | }); 146 | }); 147 | }); 148 | 149 | it("does nothing when passed a merge that will result in no changes", function() { 150 | checkMultiple(function(immutable, mutables, runMerge) { 151 | // Make sure all the changes will be no-ops. 152 | _.each(mutables, function(mutable, index) { 153 | _.each(mutable, function (value, key) { 154 | // If the immutable one has this key, use the same value. 155 | // Otherwise, delete the key. 156 | if (immutable.hasOwnProperty(key)) { 157 | mutable[key] = immutable[key]; 158 | } else { 159 | delete mutable[key]; 160 | } 161 | }); 162 | }); 163 | 164 | var result = runMerge(); 165 | 166 | assert.strictEqual(result, immutable); 167 | }); 168 | }); 169 | 170 | it("returns a deeply Immutable Object", function() { 171 | checkMultiple(function(immutable, mutables, runMerge) { 172 | var result = runMerge(); 173 | 174 | assert.instanceOf(result, Object); 175 | TestUtils.assertIsDeeplyImmutable(result); 176 | }); 177 | }); 178 | } 179 | 180 | // Sanity check to make sure our QuickCheck logic isn't off the rails. 181 | it("passes a basic sanity check on canned input", function() { 182 | var expected = Immutable({all: "your base", are: {belong: "to us"}}); 183 | var actual = Immutable({all: "your base", are: {belong: "to them"}}) 184 | actual = Immutable.merge(actual, {are: {belong: "to us"}}) 185 | 186 | TestUtils.assertJsonEqual(actual, expected); 187 | }); 188 | 189 | it("does nothing when passed a canned merge that will result in no changes", function() { 190 | var expected = Immutable({all: "your base", are: {belong: "to us"}}); 191 | var actual = Immutable.merge(expected, {all: "your base"}); // Should result in a no-op. 192 | 193 | assert.strictEqual(expected, actual, JSON.stringify(expected) + " did not equal " + JSON.stringify(actual)); 194 | }); 195 | 196 | it("is a no-op when passed nothing", function() { 197 | check(100, [TestUtils.ComplexObjectSpecifier()], function(obj) { 198 | var expected = Immutable(obj); 199 | var actual = Immutable.merge(expected); 200 | 201 | TestUtils.assertJsonEqual(actual, expected); 202 | }); 203 | }); 204 | 205 | it("Throws an exception if you pass it a non-object", function() { 206 | check(100, [TestUtils.ComplexObjectSpecifier(), invalidMergeArgumentSpecifier()], function(obj, nonObj) { 207 | assert.isObject(obj, 0, "Test error: this specifier should always generate an object, which " + JSON.stringify(obj) + " was not."); 208 | assert.isNotObject(nonObj, 0, "Test error: this specifier should always generate a non-object, which" + JSON.stringify(nonObj) + " was not."); 209 | 210 | var immutable = Immutable(obj); 211 | 212 | assert.throws(function() { 213 | Immutable.merge(immutable, nonObj); 214 | }, TypeError) 215 | }); 216 | }); 217 | 218 | it("merges deep when the config tells it to", function() { 219 | var original = Immutable({all: "your base", are: {belong: "to us", you: {have: "x", make: "your time"}}}); 220 | var toMerge = {are: {you: {have: "no chance to survive"}}}; 221 | 222 | var expectedShallow = Immutable({all: "your base", are: {you: {have: "no chance to survive"}}}); 223 | var actualShallow = Immutable.merge(original, toMerge); 224 | 225 | TestUtils.assertJsonEqual(actualShallow, expectedShallow); 226 | 227 | var expectedDeep = Immutable({all: "your base", are: {belong: "to us", you: {have: "no chance to survive", make: "your time"}}}); 228 | var actualDeep = Immutable.merge(original, toMerge, {deep: true}); 229 | 230 | TestUtils.assertJsonEqual(actualDeep, expectedDeep); 231 | }); 232 | 233 | it("merges deep when the config tells it to with an array as the first argument", function() { 234 | var original = Immutable({test: {a: true}}); 235 | var toMerge = [{test: {b: true}}, {test: {c: true}}]; 236 | 237 | var expectedShallow = Immutable({test: {c: true}}); 238 | var actualShallow = Immutable.merge(original, toMerge); 239 | 240 | TestUtils.assertJsonEqual(actualShallow, expectedShallow); 241 | 242 | var expectedDeep = Immutable({test: {a: true, b: true, c: true}}); 243 | var actualDeep = Immutable.merge(original, toMerge, {deep: true}); 244 | 245 | TestUtils.assertJsonEqual(actualDeep, expectedDeep); 246 | }); 247 | 248 | it("merges deep on only objects", function() { 249 | var original = Immutable({id: 3, name: "three", valid: true, a: {id: 2}, b: [50], x: [1, 2], sub: {z: [100]}}); 250 | var toMerge = {id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [3, 4], sub: {y: [10, 11], z: [101, 102]}}; 251 | 252 | var expected = Immutable({id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [3, 4], sub: {z: [101, 102], y: [10, 11]}}); 253 | var actual = Immutable.merge(original, toMerge, {deep: true}); 254 | 255 | TestUtils.assertJsonEqual(actual, expected); 256 | }); 257 | 258 | function arrayMerger(thisValue, providedValue) { 259 | if (thisValue instanceof Array && providedValue instanceof Array) { 260 | return thisValue.concat(providedValue); 261 | } 262 | } 263 | 264 | it("merges with a custom merger when the config tells it to", function() { 265 | var expected = Immutable({all: "your base", are: {belong: "to us"}, you: ['have', 'no', 'chance', 'to', 'survive']}); 266 | var actual = Immutable({all: "your base", are: {belong: "to us"}, you: ['have', 'no']}); 267 | var actual = Immutable.merge(actual, {you: ['chance', 'to', 'survive']}, {merger: arrayMerger}); 268 | 269 | TestUtils.assertJsonEqual(actual, expected); 270 | }); 271 | 272 | it("merges deep with a custom merger when the config tells it to", function() { 273 | var original = Immutable({id: 3, name: "three", valid: true, a: {id: 2}, b: [50], x: [1, 2], sub: {z: [100]}}); 274 | var toMerge = {id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [3, 4], sub: {y: [10, 11], z: [101, 102]}}; 275 | 276 | var expected = Immutable({id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [1, 2, 3, 4], sub: {z: [100, 101, 102], y: [10, 11]}}); 277 | var actual = Immutable.merge(original, toMerge, {deep: true, merger: arrayMerger}); 278 | 279 | TestUtils.assertJsonEqual(actual, expected); 280 | }); 281 | 282 | it("merges with a custom merger that returns the current object the result is the same as the original", function() { 283 | var data = {id: 3, name: "three", valid: true, a: {id: 2}, b: [50], x: [1, 2], sub: {z: [100]}}; 284 | var original = Immutable(data); 285 | var actualWithoutMerger = Immutable.merge(original, data); 286 | assert.notEqual(original, actualWithoutMerger); 287 | 288 | var config = { 289 | merger: function(current, other) { 290 | return current; 291 | } 292 | }; 293 | var actualWithMerger = Immutable.merge(original, data, config); 294 | assert.equal(original, actualWithMerger); 295 | }); 296 | 297 | it("merges with a custom merger that returns null and makes sure it isn't treated as undefined", function() { 298 | var original = Immutable({a: null}) 299 | var toMerge = {a: undefined} 300 | var expected = Immutable({a: null}) 301 | 302 | var config = { 303 | merger: function(current, other) { 304 | return null 305 | } 306 | } 307 | var actual = Immutable.merge(original, toMerge, config) 308 | assert.equal(original, actual) 309 | }) 310 | 311 | it("preserves prototypes across merges", function() { 312 | function TestClass(o) { _.extend(this, o); }; 313 | var data = new TestClass({a: 1, b: 2}); 314 | var mergeData = {b: 3, c: 4}; 315 | 316 | var immutable = Immutable(data, {prototype: TestClass.prototype}); 317 | var result = Immutable.merge(immutable, mergeData); 318 | 319 | TestUtils.assertJsonEqual(result, _.extend({}, data, mergeData)); 320 | TestUtils.assertHasPrototype(result, TestClass.prototype); 321 | }); 322 | 323 | it("static method continues to work after overriding the instance method", function() { 324 | var I = Immutable.static; 325 | 326 | var immutable; 327 | 328 | immutable = I({merge: 'string'}); 329 | TestUtils.assertJsonEqual(immutable, {merge: 'string'}); 330 | 331 | immutable = I({}); 332 | immutable = I.setIn(immutable, ['merge'], 'string'); 333 | TestUtils.assertJsonEqual(immutable, {merge: 'string'}); 334 | 335 | immutable = I({}); 336 | immutable = I.merge(immutable, {merge: 'string'}); 337 | immutable = I.merge(immutable, {new_key: 'new_data'}); 338 | TestUtils.assertJsonEqual(immutable, {merge: 'string', new_key: 'new_data'}); 339 | }); 340 | 341 | describe("when passed a single object", function() { 342 | generateMergeTestsFor([TestUtils.ComplexObjectSpecifier()]); 343 | }); 344 | 345 | describe("when passed a single object with deep set to true", function() { 346 | generateMergeTestsFor([TestUtils.ComplexObjectSpecifier()], {deep: true}); 347 | }); 348 | 349 | describe("when passed a single object with deep set to true and mode set to replace", function() { 350 | generateMergeTestsFor([TestUtils.ComplexObjectSpecifier()], {deep: true, mode: 'replace'}); 351 | }); 352 | 353 | describe("when passed a single object with a custom merger", function() { 354 | generateMergeTestsFor([TestUtils.ComplexObjectSpecifier()], {merger: arrayMerger()}); 355 | }); 356 | 357 | describe("when passed an array of objects", function() { 358 | generateMergeTestsFor([generateArrayOfObjects]); 359 | }); 360 | 361 | describe("when passed an array of objects with deep set to true", function() { 362 | generateMergeTestsFor([generateArrayOfObjects], {deep: true}); 363 | }); 364 | 365 | describe("when passed an array of objects with deep set to true and mode set to replace", function() { 366 | generateMergeTestsFor([generateArrayOfObjects], {deep: true, mode: 'replace'}); 367 | }); 368 | 369 | describe("when passed an array of objects with a custom merger", function() { 370 | generateMergeTestsFor([generateArrayOfObjects], {merger: arrayMerger()}); 371 | }); 372 | 373 | it("supports non-static syntax", function() { 374 | var obj = Immutable({}); 375 | obj = obj.merge({test: 'test'}); 376 | TestUtils.assertJsonEqual(obj, {test: 'test'}); 377 | }); 378 | }); 379 | }; 380 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-replace.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | // Anything but an object, array, or undefined. 7 | function invalidMergeArgumentSpecifier() { 8 | return JSC.one_of([ 9 | (function() { return function() {}; }), 10 | JSC.integer(), JSC.number(), JSC.string(), 11 | true, Infinity, -Infinity 12 | ]); 13 | } 14 | 15 | module.exports = function(config) { 16 | var Immutable = config.implementation; 17 | var TestUtils = getTestUtils(Immutable); 18 | var check = TestUtils.check; 19 | 20 | function generateArrayOfObjects() { 21 | return JSC.array()().map(function() { return TestUtils.ComplexObjectSpecifier()(); }); 22 | } 23 | 24 | describe("#replace", function() { 25 | function generateReplaceTestsFor(specifiers, config) { 26 | var runs = 100; 27 | 28 | function checkSingle(callback) { 29 | check(runs, specifiers, function(mutable) { 30 | var immutable = Immutable(mutable); 31 | 32 | function runReplace(other) { 33 | other = other || mutable; 34 | return Immutable.replace(immutable, other, config); 35 | } 36 | 37 | callback(immutable, mutable, runReplace); 38 | }) 39 | } 40 | 41 | it("prioritizes the arguments' properties", function() { 42 | checkSingle(function(immutable, mutable, runReplace) { 43 | var expectedChanges = {}; 44 | 45 | var keys = _.keys(immutable); 46 | 47 | assert.notStrictEqual(keys.length, 0, "Can't usefully check replace() with an empty object"); 48 | 49 | // Randomly change some values that share keys with the immutable. 50 | _.each(keys, function(key) { 51 | if (Math.random() > 0.5) { 52 | var value = immutable[key], 53 | suffix = JSC.string()(), 54 | newValue = value + suffix; 55 | 56 | assert.notStrictEqual(value, newValue, "Failed to change value (" + value + ") by appending \"" + suffix + "\""); 57 | 58 | // Record that we expect this to end up in the final result. 59 | expectedChanges[key] = newValue; 60 | assert(Immutable.isImmutable(expectedChanges[key])); 61 | 62 | mutable[key] = newValue; 63 | } else if (mutable.hasOwnProperty(key) && mutable[key] !== immutable[key]) { 64 | // NaN will break tests, though not the actual function. 65 | if (isNaN(mutable[key])) { 66 | mutable[key] = 0; 67 | } 68 | 69 | expectedChanges[key] = mutable[key]; 70 | } 71 | }); 72 | 73 | var result = runReplace(expectedChanges); 74 | 75 | _.each(expectedChanges, function(value, key) { 76 | assert.notStrictEqual(value, immutable[key], 77 | "Expected to change key (" + key + "), but expected change was the same as the old value (" + value + ")"); 78 | }); 79 | 80 | _.each(result, function(value, key) { 81 | assert(expectedChanges.hasOwnProperty(key), 82 | "Result has key (" + key + "), but this key was not included in object set to .replace() call"); 83 | }); 84 | }); 85 | }); 86 | 87 | it("contains all the arguments' properties", function() { 88 | checkSingle(function(immutable, mutable, runReplace) { 89 | var result = runReplace(); 90 | 91 | _.each(mutable, function (value, key) { 92 | assert(result.hasOwnProperty(key), "Result " + JSON.stringify(result) + " did not have property " + key + " with value " + value + " from " + JSON.stringify(mutable)); 93 | }); 94 | }); 95 | }); 96 | 97 | it("does not contain the original's properties", function() { 98 | checkSingle(function(immutable, mutable, runReplace) { 99 | var result = runReplace(); 100 | 101 | _.each(immutable, function (value, key) { 102 | assert(!result.hasOwnProperty(key) || mutable.hasOwnProperty(key)); 103 | }); 104 | }); 105 | }); 106 | 107 | it("does not reproduce except when required #70", function() { 108 | var immutable = Immutable({a: {b: 1, c: 1}, d: {e: 1}}); 109 | 110 | // Non-deep replace is never equal for deep objects. 111 | assert.notStrictEqual(immutable, Immutable.replace(immutable, {a: {b: 1, c: 1}, d: {e: 1}})); 112 | 113 | // Deep merge for only some of the keys is not equal 114 | assert.notStrictEqual(immutable, Immutable.replace(immutable, {a: {b: 1, c: 1}}, {deep: true})); 115 | 116 | // Identical child objects in deep merge remain equal. 117 | assert.strictEqual(immutable.a, Immutable.replace(immutable, {a: {b: 1, c: 1}}, {deep: true}).a); 118 | 119 | // Deep merge for all of the keys is always equal. 120 | assert.strictEqual(immutable, Immutable.replace(immutable, {a: {b: 1, c: 1}, d: {e: 1}}, {deep: true})); 121 | 122 | // Deep merge with updated data is never equal. 123 | assert.notStrictEqual(immutable, Immutable.replace(immutable, {a: {b: 1, c: 1}, d: {e: 2}}, {deep: true})); 124 | }); 125 | 126 | it("does nothing when replacing an identical object", function() { 127 | checkSingle(function(immutable, mutable, runReplace) { 128 | var identicalImmutable = Immutable(mutable); 129 | 130 | assert.strictEqual(identicalImmutable, 131 | Immutable.replace(identicalImmutable, mutable, {deep: true})); 132 | }); 133 | }); 134 | 135 | it("returns a deeply Immutable Object", function() { 136 | checkSingle(function(immutable, mutable, runReplace) { 137 | var result = runReplace(); 138 | 139 | assert.instanceOf(result, Object); 140 | TestUtils.assertIsDeeplyImmutable(result); 141 | }); 142 | }); 143 | } 144 | 145 | // Sanity check to make sure our QuickCheck logic isn't off the rails. 146 | it("passes a basic sanity check on canned input", function() { 147 | var expected = Immutable({are: {belong: "to us"}}); 148 | var actual = Immutable({all: "your base", are: {belong: "to them"}}); 149 | actual = Immutable.replace(actual, {are: {belong: "to us"}}) 150 | 151 | TestUtils.assertJsonEqual(actual, expected); 152 | }); 153 | 154 | it("does nothing when passed a canned merge that will result in no changes", function() { 155 | var expected = Immutable({all: "your base"}); 156 | var actual = Immutable.replace(expected, {all: "your base"}); // Should result in a no-op. 157 | 158 | assert.strictEqual(expected, actual, JSON.stringify(expected) + " did not equal " + JSON.stringify(actual)); 159 | }); 160 | 161 | it("is a no-op when passed nothing", function() { 162 | check(100, [TestUtils.ComplexObjectSpecifier()], function(obj) { 163 | var expected = Immutable(obj); 164 | var actual = Immutable.replace(expected); 165 | 166 | assert.strictEqual(actual, expected); 167 | }); 168 | }); 169 | 170 | it("Throws an exception if you pass it a non-object", function() { 171 | check(100, [TestUtils.ComplexObjectSpecifier(), invalidMergeArgumentSpecifier()], function(obj, nonObj) { 172 | assert.isObject(obj, 0, "Test error: this specifier should always generate an object, which " + JSON.stringify(obj) + " was not."); 173 | assert.isNotObject(nonObj, 0, "Test error: this specifier should always generate a non-object, which" + JSON.stringify(nonObj) + " was not."); 174 | 175 | var immutable = Immutable(obj); 176 | 177 | assert.throws(function() { 178 | Immutable.replace(immutable, nonObj); 179 | }, TypeError) 180 | }); 181 | }); 182 | 183 | it("merges deep when the config tells it to", function() { 184 | var original = Immutable({all: "your base", are: {belong: "to us", you: {have: "x", make: "your time"}}}); 185 | var toMerge = {are: {you: {have: "no chance to survive"}}}; 186 | 187 | var expectedShallow = Immutable({are: {you: {have: "no chance to survive"}}}); 188 | var actualShallow = Immutable.replace(original, toMerge); 189 | 190 | TestUtils.assertJsonEqual(actualShallow, expectedShallow); 191 | 192 | var expectedDeep = Immutable({are: {you: {have: "no chance to survive"}}}); 193 | var actualDeep = Immutable.replace(original, toMerge, {deep: true}); 194 | 195 | TestUtils.assertJsonEqual(actualDeep, expectedDeep); 196 | }); 197 | 198 | it("merges deep on only objects", function() { 199 | var original = Immutable({id: 3, name: "three", valid: true, a: {id: 2}, b: [50], x: [1, 2], sub: {z: [100]}}); 200 | var toMerge = {id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [3, 4], sub: {y: [10, 11], z: [101, 102]}}; 201 | 202 | var expected = Immutable({id: 3, name: "three", valid: false, a: [1000], b: {id: 4}, x: [3, 4], sub: {y: [10, 11], z: [101, 102]}}); 203 | var actual = Immutable.replace(original, toMerge, {deep: true}); 204 | 205 | TestUtils.assertJsonEqual(actual, expected); 206 | }); 207 | 208 | it("preserves prototypes across merges", function() { 209 | function TestClass(o) { _.extend(this, o); }; 210 | var data = new TestClass({a: 1, b: 2}); 211 | var mergeData = {b: 3, c: 4}; 212 | 213 | var immutable = Immutable(data, {prototype: TestClass.prototype}); 214 | var result = Immutable.replace(immutable, mergeData); 215 | 216 | TestUtils.assertJsonEqual(result, _.extend(mergeData)); 217 | TestUtils.assertHasPrototype(result, TestClass.prototype); 218 | }); 219 | 220 | it("static method continues to work after overriding the instance method", function() { 221 | var I = Immutable.static; 222 | 223 | var immutable; 224 | 225 | immutable = I({replace: 'string'}); 226 | TestUtils.assertJsonEqual(immutable, {replace: 'string'}); 227 | 228 | immutable = I({}); 229 | immutable = I.setIn(immutable, ['replace'], 'string'); 230 | TestUtils.assertJsonEqual(immutable, {replace: 'string'}); 231 | 232 | immutable = I({}); 233 | immutable = I.replace(immutable, {replace: 'string'}); 234 | TestUtils.assertJsonEqual(immutable, {replace: 'string'}); 235 | immutable = I.replace(immutable, {new_key: 'new_data'}); 236 | TestUtils.assertJsonEqual(immutable, {new_key: 'new_data'}); 237 | }); 238 | 239 | describe("when passed a single object", function() { 240 | generateReplaceTestsFor([TestUtils.ComplexObjectSpecifier()]); 241 | }); 242 | 243 | describe("when passed a single object with deep set to true", function() { 244 | generateReplaceTestsFor([TestUtils.ComplexObjectSpecifier()], {deep: true}); 245 | }); 246 | 247 | it("supports non-static syntax", function() { 248 | var obj = Immutable({}); 249 | obj = obj.replace({test: 'test'}); 250 | TestUtils.assertJsonEqual(obj, {test: 'test'}); 251 | }); 252 | }); 253 | }; 254 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-set.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | 7 | module.exports = function(config) { 8 | var Immutable = config.implementation; 9 | var TestUtils = getTestUtils(Immutable); 10 | var check = TestUtils.check; 11 | 12 | function getPathComponent() { 13 | // It's very convenient to use lodash.set, but it has funky behaviour 14 | // with numeric keys. 15 | var s = JSC.string()().replace(/[^\w]/g, '_'); 16 | return /^\d+$/.test(s) ? s + 'a' : s; 17 | } 18 | 19 | describe("#set", function() { 20 | xit("sets a property by name", function () { 21 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 22 | var immutable = Immutable(ob); 23 | var mutable = _.assign({}, ob); 24 | var prop = getPathComponent(); 25 | var value = JSC.any()(); 26 | 27 | TestUtils.assertJsonEqual( 28 | Immutable.set(immutable, prop, value), 29 | _.set(mutable, prop, value) 30 | ); 31 | }); 32 | }); 33 | 34 | xit("sets a property by name with deep compare if provided the deep flag", function () { 35 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 36 | var immutable = Immutable(ob); 37 | var mutable = _.assign({}, ob); 38 | var prop = getPathComponent(); 39 | var value; 40 | do { 41 | value = JSC.any()(); 42 | } while (TestUtils.isDeepEqual(value, immutable[prop])); 43 | 44 | var resultImmutable = Immutable.set(immutable, prop, value, {deep: true}); 45 | var resultMutable = _.set(mutable, prop, value); 46 | TestUtils.assertJsonEqual( 47 | resultImmutable, 48 | resultMutable 49 | ); 50 | assert.notEqual( 51 | immutable, 52 | resultImmutable 53 | ); 54 | assert.equal( 55 | Immutable.set(resultImmutable, prop, value, {deep: true}), 56 | resultImmutable 57 | ); 58 | }); 59 | }); 60 | 61 | it("static method continues to work after overriding the instance method", function() { 62 | var I = Immutable.static; 63 | 64 | var immutable; 65 | 66 | immutable = I({set: 'string'}); 67 | TestUtils.assertJsonEqual(immutable, {set: 'string'}); 68 | 69 | immutable = I({}); 70 | immutable = I.set(immutable, 'set', 'string'); 71 | TestUtils.assertJsonEqual(immutable, {set: 'string'}); 72 | immutable = I.set(immutable, 'new_key', 'new_data'); 73 | TestUtils.assertJsonEqual(immutable, {set: 'string', new_key: 'new_data'}); 74 | }); 75 | 76 | it("supports non-static syntax", function() { 77 | var obj = Immutable({}); 78 | obj = obj.set('test', 'test'); 79 | TestUtils.assertJsonEqual(obj, {test: 'test'}); 80 | }); 81 | }); 82 | 83 | 84 | describe("#setIn", function() { 85 | xit("sets a property by path", function () { 86 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 87 | var immutable = Immutable(ob); 88 | var mutable = _.assign({}, ob); 89 | 90 | TestUtils.assertJsonEqual(immutable, mutable); 91 | 92 | var path = [], depth = JSC.integer(1, 5)(); 93 | for (var j = 0; j < depth; j++) { 94 | path.push(getPathComponent()); 95 | } 96 | 97 | var value; 98 | do { 99 | value = JSC.any()(); 100 | } while (TestUtils.isDeepEqual(value, _.get(immutable, path))); 101 | 102 | TestUtils.assertJsonEqual( 103 | Immutable.setIn(immutable, path, value), 104 | _.set(mutable, path, value) 105 | ); 106 | }); 107 | }); 108 | 109 | 110 | it("handles setting a new object on existing leaf array correctly", function () { 111 | var ob = {foo: []}; 112 | var path = ['foo', 0, 'bar']; 113 | var val = 'val'; 114 | 115 | var immutable = Immutable(ob); 116 | var final = Immutable.setIn(immutable, path, val); 117 | 118 | assert.deepEqual(final, {foo: [{bar: 'val'}]}); 119 | }); 120 | 121 | 122 | xit("sets a property by path with deep compare if provided the deep flag", function () { 123 | check(100, [TestUtils.ComplexObjectSpecifier()], function(ob) { 124 | var immutable = Immutable(ob); 125 | var mutable = _.assign({}, ob); 126 | var value = JSC.any()(); 127 | 128 | TestUtils.assertJsonEqual(immutable, mutable); 129 | 130 | var path = [], depth = JSC.integer(1, 5)(); 131 | for (var j = 0; j < depth; j++) { 132 | path.push(getPathComponent()); 133 | } 134 | 135 | var resultImmutable = Immutable.setIn(immutable, path, value, {deep: true}); 136 | var resultMutable = _.set(mutable, path, value); 137 | TestUtils.assertJsonEqual( 138 | resultImmutable, 139 | resultMutable 140 | ); 141 | assert.notEqual( 142 | immutable, 143 | resultImmutable 144 | ); 145 | assert.equal( 146 | Immutable.setIn(resultImmutable, path, value, {deep: true}), 147 | resultImmutable 148 | ); 149 | }); 150 | }); 151 | 152 | it("static method continues to work after overriding the instance method", function() { 153 | var I = Immutable.static; 154 | 155 | var immutable; 156 | 157 | immutable = I({setIn: 'string'}); 158 | TestUtils.assertJsonEqual(immutable, {setIn: 'string'}); 159 | 160 | immutable = I({}); 161 | immutable = I.setIn(immutable, ['setIn'], 'string'); 162 | TestUtils.assertJsonEqual(immutable, {setIn: 'string'}); 163 | immutable = I.setIn(immutable, ['new_key'], 'new_data'); 164 | TestUtils.assertJsonEqual(immutable, {setIn: 'string', new_key: 'new_data'}); 165 | }); 166 | 167 | it("supports non-static syntax", function() { 168 | var obj = Immutable({}); 169 | obj = obj.setIn(['test'], 'test'); 170 | TestUtils.assertJsonEqual(obj, {test: 'test'}); 171 | }); 172 | }); 173 | }; 174 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-update.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | 7 | module.exports = function(config) { 8 | var Immutable = config.implementation; 9 | var TestUtils = getTestUtils(Immutable); 10 | var check = TestUtils.check; 11 | 12 | function getPathComponent() { 13 | // It's very convenient to use lodash.set, but it has funky behaviour 14 | // with numeric keys. 15 | var s = JSC.string()().replace(/[^\w]/g, '_'); 16 | return /^\d+$/.test(s) ? s + 'a' : s; 17 | } 18 | 19 | function dummyUpdater (x) { 20 | return JSON.stringify(x) + "_updated"; 21 | } 22 | 23 | describe("#update", function() { 24 | xit("updates a property using updater function", function () { 25 | check(100, [TestUtils.TraversableObjectSpecifier], function(ob) { 26 | var immutable = Immutable(ob); 27 | var mutable = Immutable.asMutable(immutable, {deep: true}); 28 | var prop = 'complex'; 29 | 30 | TestUtils.assertJsonEqual( 31 | Immutable.update(immutable, prop, dummyUpdater), 32 | _.set(mutable, prop, dummyUpdater(_.get(mutable, prop))) 33 | ); 34 | }); 35 | }); 36 | 37 | xit("allows passing additional parameters to updater function", function () { 38 | check(100, [TestUtils.TraversableObjectSpecifier], function(ob) { 39 | var immutable = Immutable(ob); 40 | var mutable = Immutable.asMutable(immutable, {deep: true}); 41 | var prop = 'complex'; 42 | 43 | TestUtils.assertJsonEqual( 44 | Immutable.update(immutable, prop, dummyUpdater, "agr1", 42), 45 | _.set(mutable, prop, dummyUpdater(_.get(mutable, prop), "agr1", 42)) 46 | ); 47 | }); 48 | }); 49 | 50 | it("static method continues to work after overriding the instance method", function() { 51 | function dummyUpdater(data) { 52 | return data + '_updated'; 53 | } 54 | 55 | var I = Immutable.static; 56 | 57 | var immutable; 58 | 59 | immutable = I({update: 'string'}); 60 | TestUtils.assertJsonEqual(immutable, {update: 'string'}); 61 | 62 | immutable = I({}); 63 | immutable = I.set(immutable, 'update', 'string'); 64 | TestUtils.assertJsonEqual(immutable, {update: 'string'}); 65 | immutable = I.update(immutable, 'update', dummyUpdater); 66 | TestUtils.assertJsonEqual(immutable, {update: 'string_updated'}); 67 | 68 | }); 69 | 70 | it("supports non-static syntax", function() { 71 | function dummyUpdater(data) { 72 | return data + '_updated'; 73 | } 74 | var obj = Immutable({test: 'test'}); 75 | obj = obj.update('test', dummyUpdater); 76 | TestUtils.assertJsonEqual(obj, {test: 'test_updated'}); 77 | }); 78 | }); 79 | 80 | describe("#updateIn", function() { 81 | xit("updates a property in path using updater function", function () { 82 | check(100, [TestUtils.TraversableObjectSpecifier], function(ob) { 83 | var immutable = Immutable(ob); 84 | var mutable = Immutable.asMutable(immutable, {deep: true}); 85 | 86 | TestUtils.assertJsonEqual(immutable, mutable); 87 | 88 | var path = ['deep', 'complex']; 89 | 90 | TestUtils.assertJsonEqual( 91 | Immutable.updateIn(immutable, path, dummyUpdater), 92 | _.set(mutable, path, dummyUpdater(_.get(mutable, path))) 93 | ); 94 | }); 95 | }); 96 | 97 | xit("allows passing additional parameters to updater function", function () { 98 | check(100, [TestUtils.TraversableObjectSpecifier], function(ob) { 99 | var immutable = Immutable(ob); 100 | var mutable = Immutable.asMutable(immutable, {deep: true}); 101 | 102 | TestUtils.assertJsonEqual(immutable, mutable); 103 | 104 | var path = ['deep', 'complex']; 105 | 106 | TestUtils.assertJsonEqual( 107 | Immutable.updateIn(immutable, path, dummyUpdater, "agr1", 42), 108 | _.set(mutable, path, dummyUpdater(_.get(mutable, path), "agr1", 42)) 109 | ); 110 | }); 111 | }); 112 | 113 | it("static method continues to work after overriding the instance method", function() { 114 | function dummyUpdater(data) { 115 | return data + '_updated'; 116 | } 117 | 118 | var I = Immutable.static; 119 | 120 | var immutable; 121 | 122 | immutable = I({updateIn: 'string'}); 123 | TestUtils.assertJsonEqual(immutable, {updateIn: 'string'}); 124 | 125 | immutable = I({}); 126 | immutable = I.setIn(immutable, ['updateIn'], 'string'); 127 | TestUtils.assertJsonEqual(immutable, {updateIn: 'string'}); 128 | immutable = I.updateIn(immutable, ['updateIn'], dummyUpdater); 129 | TestUtils.assertJsonEqual(immutable, {updateIn: 'string_updated'}); 130 | }); 131 | 132 | it("supports non-static syntax", function() { 133 | function dummyUpdater(data) { 134 | return data + '_updated'; 135 | } 136 | var obj = Immutable({test: 'test'}); 137 | obj = obj.updateIn(['test'], dummyUpdater); 138 | TestUtils.assertJsonEqual(obj, {test: 'test_updated'}); 139 | }); 140 | }); 141 | }; 142 | -------------------------------------------------------------------------------- /test/ImmutableObject/test-without.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var getTestUtils = require("../TestUtils.js"); 5 | 6 | function generateArrayOfStrings() { 7 | return JSC.array()().map(function() { return JSC.string()(); }); 8 | } 9 | 10 | function argumentsToArray(theArguments) { 11 | return Array.prototype.slice.call(theArguments); 12 | } 13 | 14 | function dropKeysPredicate(keys) { 15 | return function (value, key) { 16 | return _.includes(keys, key); 17 | } 18 | } 19 | 20 | module.exports = function(config) { 21 | var Immutable = config.implementation; 22 | var TestUtils = getTestUtils(Immutable); 23 | var check = TestUtils.check; 24 | 25 | describe("#without", function() { 26 | 27 | function checkImmutableWithKeys(keysSpecifier, callback/*function(immutable, keys)*/) { 28 | var runs = 100; 29 | 30 | function checkMultiple(callback) { 31 | // keysSpecifier = array of generators 32 | check(runs, keysSpecifier, function(singleArrayOrFirstKey) { 33 | 34 | var useVarArgs = !(singleArrayOrFirstKey instanceof Array); 35 | 36 | var keys; 37 | if (arguments.length > 1) { 38 | keys = Array.prototype.slice.call(arguments); 39 | } else if (singleArrayOrFirstKey instanceof Array) { 40 | keys = singleArrayOrFirstKey; 41 | } else { 42 | keys = [singleArrayOrFirstKey]; 43 | } 44 | 45 | assert.notStrictEqual(keys.length, 0, "Can't usefully check without() with no objects"); 46 | 47 | // Make an object that at LEAST contains the specified keys. 48 | var immutable = Immutable.asObject(Immutable(keys), function(key) { 49 | return [key, JSC.any()()]; 50 | }); 51 | immutable = Immutable.merge(immutable, TestUtils.ComplexObjectSpecifier()()); 52 | 53 | callback(immutable, keys, useVarArgs); 54 | }) 55 | } 56 | 57 | checkMultiple(callback); 58 | } 59 | 60 | function generateWithoutTestsFor(firstSpecifier) { 61 | 62 | var keysSpecifier = (firstSpecifier === generateArrayOfStrings) ? 63 | [firstSpecifier] : argumentsToArray(arguments); 64 | 65 | checkImmutableWithKeys(keysSpecifier, function(immutable, keys, useVarArgs) { 66 | 67 | it("returns the same result as a corresponding without(predicate)", function() { 68 | var expected = Immutable.without(immutable, dropKeysPredicate(keys)); 69 | var actual = useVarArgs ? 70 | Immutable.without.apply(Immutable, [immutable].concat(keys)) : 71 | Immutable.without(immutable, keys); 72 | TestUtils.assertJsonEqual(actual, expected); 73 | }); 74 | 75 | it("drops the keys", function() { 76 | var expectedKeys = _.difference(_.keys(immutable), keys); 77 | var result = Immutable.without(immutable, keys); 78 | 79 | TestUtils.assertJsonEqual(_.keys(result), expectedKeys); 80 | }); 81 | }); 82 | } 83 | 84 | // Sanity check to make sure our QuickCheck logic isn't off the rails. 85 | it("passes a basic sanity check on canned input", function() { 86 | var expected = Immutable({cat: "meow", dog: "woof"}); 87 | var actual = Immutable({cat: "meow", dog: "woof", fox: "???"}); 88 | actual = Immutable.without(actual, "fox"); 89 | 90 | TestUtils.assertJsonEqual(actual, expected); 91 | }); 92 | 93 | // Check that numeric keys are removed too. 94 | it("passes a basic sanity check with numeric keys", function() { 95 | var expected = Immutable({cat: "meow", dog: "woof"}); 96 | var actual = Immutable({cat: "meow", dog: "woof", 42: "???"}); 97 | actual = Immutable.without(actual, 42); 98 | TestUtils.assertJsonEqual(actual, expected); 99 | 100 | actual = Immutable({cat: "meow", dog: "woof", 42: "???", 0.5: "xxx"}); 101 | actual = Immutable.without(actual, [42, 0.5]); 102 | TestUtils.assertJsonEqual(actual, expected); 103 | }); 104 | 105 | it("is a no-op when passed nothing", function() { 106 | check(100, [TestUtils.ComplexObjectSpecifier()], function(obj) { 107 | var expected = Immutable(obj); 108 | var actual = Immutable.without(expected); 109 | 110 | TestUtils.assertJsonEqual(actual, expected); 111 | }); 112 | }); 113 | 114 | it("preserves prototypes after call to without", function() { 115 | function TestClass(o) { _.extend(this, o); }; 116 | var data = new TestClass({a: 1, b: 2}); 117 | 118 | var immutable = Immutable(data, {prototype: TestClass.prototype}); 119 | var result = Immutable.without(immutable, 'b'); 120 | 121 | TestUtils.assertJsonEqual(result, _.omit(data, 'b')); 122 | TestUtils.assertHasPrototype(result, TestClass.prototype); 123 | }); 124 | 125 | it("static method continues to work after overriding the instance method", function() { 126 | var I = Immutable.static; 127 | 128 | var immutable; 129 | 130 | immutable = I({without: 'string'}); 131 | TestUtils.assertJsonEqual(immutable, {without: 'string'}); 132 | 133 | immutable = I({}); 134 | immutable = I.set(immutable, 'without', 'string'); 135 | TestUtils.assertJsonEqual(immutable, {without: 'string'}); 136 | immutable = I.set(immutable, 'some_key', 'string'); 137 | immutable = I.without(immutable, 'some_key'); 138 | TestUtils.assertJsonEqual(immutable, {without: 'string'}); 139 | immutable = I.without(immutable, 'without'); 140 | TestUtils.assertJsonEqual(immutable, {}); 141 | }); 142 | 143 | describe("when passed a single key", function() { 144 | generateWithoutTestsFor(JSC.string()); 145 | }); 146 | 147 | describe("when passed multiple keys", function() { 148 | generateWithoutTestsFor(JSC.string(), JSC.string(), JSC.string()); 149 | }); 150 | 151 | describe("when passed an array of keys", function() { 152 | generateWithoutTestsFor(generateArrayOfStrings); 153 | }); 154 | 155 | describe("when passed a predicate", function() { 156 | checkImmutableWithKeys([generateArrayOfStrings], function(immutable, keys) { 157 | 158 | it("drops the keys satisfying the predicate", function() { 159 | var expectedKeys = _.difference(_.keys(immutable), keys); 160 | var result = Immutable.without(immutable, dropKeysPredicate(keys)); 161 | 162 | TestUtils.assertJsonEqual(_.keys(result), expectedKeys); 163 | 164 | // Make sure the remaining keys still point to the same values 165 | _.each(_.keys(result), function(key) { 166 | TestUtils.assertJsonEqual(immutable[key], result[key]);; 167 | }); 168 | }); 169 | 170 | it("returns an Immutable Object", function() { 171 | var result = Immutable.without(immutable, dropKeysPredicate(keys)); 172 | assert.instanceOf(result, Object); 173 | TestUtils.assertIsDeeplyImmutable(result); 174 | }); 175 | 176 | it("works the same way as _.omit", function() { 177 | var expected = _.omit(immutable, function (value, key) { 178 | return _.includes(keys, key); 179 | }); 180 | 181 | var actual = Immutable.without(immutable, function (value, key) { 182 | return _.includes(keys, key); 183 | }); 184 | 185 | TestUtils.assertJsonEqual(expected, actual); 186 | }); 187 | 188 | }); 189 | 190 | }); 191 | 192 | it("supports non-static syntax", function() { 193 | var obj = Immutable({test: 'test'}); 194 | obj = obj.without('test'); 195 | TestUtils.assertJsonEqual(obj, {}); 196 | }); 197 | }); 198 | }; 199 | -------------------------------------------------------------------------------- /test/TestUtils.js: -------------------------------------------------------------------------------- 1 | var JSC = require("jscheck"); 2 | var assert = require("chai").assert; 3 | var _ = require("lodash"); 4 | var deepEqual = require("deep-equal"); 5 | 6 | function assertJsonEqual(first, second) { 7 | if (typeof first === "object" && typeof second === "object" && first !== null && second !== null) { 8 | var keys = _.keys(first).sort(); 9 | assert.deepEqual(keys, _.keys(second).sort()); 10 | _.each(keys, function(key) { 11 | assertJsonEqual(first[key], second[key]); 12 | }); 13 | } else { 14 | assert.strictEqual(JSON.stringify(first), JSON.stringify(second)); 15 | } 16 | } 17 | 18 | function wrapAssertImmutable(Immutable) { 19 | return function assertImmutable(methodName, immutableArray, mutableArray, args) { 20 | var mutableResult = mutableArray[methodName].apply(mutableArray, args); 21 | var immutableResult = Immutable(immutableArray[methodName].apply(immutableArray, args)); 22 | 23 | assertJsonEqual(immutableResult, mutableResult); 24 | } 25 | } 26 | 27 | // Immutable.isImmutable only checks (for performance reasons) that objects 28 | // are shallowly immutable. For tests, though, we want to be thorough! 29 | function wrapAssertIsDeeplyImmutable(Immutable) { 30 | return function assertIsDeeplyImmutable(obj) { 31 | assert(Immutable.isImmutable(obj)); 32 | 33 | if (typeof obj === "object") { 34 | _.each(obj, assertIsDeeplyImmutable); 35 | } 36 | } 37 | } 38 | 39 | function assertHasPrototype(obj, expectedPrototype) { 40 | assert.strictEqual(Object.getPrototypeOf(obj), expectedPrototype); 41 | } 42 | 43 | function withoutIntegerKeys(obj) { 44 | return _.object(_.map(obj, function(value, key) { 45 | // Don't choose keys that can be parsed as 32-bit unsigned integers, 46 | // as browsers make no guarantee on key ordering for those, 47 | // and we rely on ordered keys to simplify several tests. 48 | if (JSON.stringify(parseInt(key)) === key && key !== Infinity && key !== -Infinity && !isNaN(key)) { 49 | return [key + "n", value]; 50 | } 51 | 52 | return [key, value]; 53 | })); 54 | } 55 | 56 | // Returns an object which may or may not contain nested objects and arrays. 57 | function ComplexObjectSpecifier() { 58 | return function() { 59 | var obj = _.object(_.map(JSC.array()(), function() { 60 | var key = JSC.string()(); 61 | var value = JSC.one_of([JSC.array(), JSC.object(), 62 | JSC.falsy(), JSC.integer(), JSC.number(), JSC.string(), 63 | true, Infinity, -Infinity])(); 64 | 65 | if (typeof value === "object") { 66 | return [key, withoutIntegerKeys(value)]; 67 | } 68 | 69 | return [key, value]; 70 | })); 71 | 72 | return withoutIntegerKeys(obj); 73 | } 74 | } 75 | 76 | function TraversableObjectSpecifier() { 77 | var complexFactory = JSC.one_of([ComplexObjectSpecifier(), JSC.array()]); 78 | var obj = JSC.object({complex: complexFactory, 79 | deep: JSC.object({complex: complexFactory}) 80 | })(); 81 | 82 | obj[JSC.string()()] = JSC.any()(); 83 | return withoutIntegerKeys(obj); 84 | } 85 | 86 | function wrapImmutableArraySpecifier(Immutable) { 87 | return function ImmutableArraySpecifier(JSC) { 88 | var args = Array.prototype.slice.call(arguments); 89 | 90 | return function generator() { 91 | var mutable = JSC.array.apply(JSC.array, args)(); 92 | 93 | return Immutable(mutable); 94 | } 95 | } 96 | } 97 | 98 | function check(runs, generators, runTest) { 99 | var completed; 100 | 101 | for (completed=0; completed < runs; completed++) { 102 | var generated = generators.map(function(generator) { return generator() }); 103 | 104 | runTest.apply(runTest, generated); 105 | } 106 | 107 | assert.strictEqual(completed, runs, 108 | "The expected " + runs + " runs were not completed."); 109 | 110 | return completed; 111 | } 112 | 113 | function wrapCheckImmutableMutable(Immutable) { 114 | return function checkImmutableMutable(runs, specifiers) { 115 | return function (callback, extraSpecifiers) { 116 | extraSpecifiers = extraSpecifiers || []; 117 | 118 | check(runs, specifiers.concat(extraSpecifiers), function (mutable) { 119 | var immutable = Immutable(mutable); 120 | var args = Array.prototype.slice.call(arguments); 121 | 122 | callback.apply(callback, [immutable].concat(args)); 123 | }); 124 | }; 125 | } 126 | } 127 | 128 | function isDeepEqual(a, b) { 129 | // Avoid false positives due to (NaN !== NaN) evaluating to true 130 | return (deepEqual(a, b) || (a !== a && b !== b)); 131 | } 132 | 133 | // window.File mock 134 | function File(parts, filename) { 135 | this.name = 'ok'; 136 | this.size = 4; 137 | this.lastModifiedDate = new Date(); 138 | this.lastModified = this.lastModifiedDate.getTime(); 139 | } 140 | 141 | // window.Blob mock 142 | function Blob() { 143 | this.size = 0; 144 | this.type = ''; 145 | } 146 | 147 | module.exports = function(Immutable) { 148 | return { 149 | assertJsonEqual: assertJsonEqual, 150 | assertImmutable: wrapAssertImmutable(Immutable), 151 | assertIsDeeplyImmutable: wrapAssertIsDeeplyImmutable(Immutable), 152 | assertHasPrototype: assertHasPrototype, 153 | ImmutableArraySpecifier: wrapImmutableArraySpecifier(Immutable), 154 | ComplexObjectSpecifier: ComplexObjectSpecifier, 155 | TraversableObjectSpecifier: TraversableObjectSpecifier, 156 | check: check, 157 | checkImmutableMutable: wrapCheckImmutableMutable(Immutable), 158 | isDeepEqual: isDeepEqual, 159 | FileMock: File, 160 | BlobMock: Blob 161 | } 162 | }; 163 | --------------------------------------------------------------------------------