| OLD | NEW |
| (Empty) | |
| 1 <html> |
| 2 <link rel="import" href="../resources/chai.sky" /> |
| 3 <link rel="import" href="../resources/mocha.sky" /> |
| 4 <link rel="import" href="/sky/framework/sky-element/observe.sky" as="observe" /> |
| 5 |
| 6 <script> |
| 7 |
| 8 var Path = observe.Path; |
| 9 var PathObserver = observe.PathObserver; |
| 10 var ArrayObserver = observe.ArrayObserver; |
| 11 var ObjectObserver = observe.ObjectObserver; |
| 12 var CompoundObserver = observe.CompoundObserver; |
| 13 var ObserverTransform = observe.ObserverTransform; |
| 14 |
| 15 var observer; |
| 16 var callbackArgs = undefined; |
| 17 var callbackInvoked = false; |
| 18 |
| 19 function then(fn) { |
| 20 setTimeout(function() { |
| 21 fn(); |
| 22 }, 0); |
| 23 |
| 24 return { |
| 25 then: function(next) { |
| 26 return then(next); |
| 27 } |
| 28 }; |
| 29 } |
| 30 |
| 31 function noop() {} |
| 32 |
| 33 function callback() { |
| 34 callbackArgs = Array.prototype.slice.apply(arguments); |
| 35 callbackInvoked = true; |
| 36 } |
| 37 |
| 38 function doSetup() {} |
| 39 function doTeardown() { |
| 40 callbackInvoked = false; |
| 41 callbackArgs = undefined; |
| 42 } |
| 43 |
| 44 function assertNoChanges() { |
| 45 if (observer) |
| 46 observer.deliver(); |
| 47 assert.isFalse(callbackInvoked); |
| 48 assert.isUndefined(callbackArgs); |
| 49 } |
| 50 |
| 51 function assertPathChanges(expectNewValue, expectOldValue, dontDeliver) { |
| 52 if (!dontDeliver) |
| 53 observer.deliver(); |
| 54 |
| 55 assert.isTrue(callbackInvoked); |
| 56 |
| 57 var newValue = callbackArgs[0]; |
| 58 var oldValue = callbackArgs[1]; |
| 59 assert.deepEqual(expectNewValue, newValue); |
| 60 assert.deepEqual(expectOldValue, oldValue); |
| 61 |
| 62 if (!dontDeliver) { |
| 63 assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| 64 window.dirtyCheckCycleCount === 1); |
| 65 } |
| 66 |
| 67 callbackArgs = undefined; |
| 68 callbackInvoked = false; |
| 69 } |
| 70 |
| 71 function assertCompoundPathChanges(expectNewValues, expectOldValues, |
| 72 expectObserved, dontDeliver) { |
| 73 if (!dontDeliver) |
| 74 observer.deliver(); |
| 75 |
| 76 assert.isTrue(callbackInvoked); |
| 77 |
| 78 var newValues = callbackArgs[0]; |
| 79 var oldValues = callbackArgs[1]; |
| 80 var observed = callbackArgs[2]; |
| 81 assert.deepEqual(expectNewValues, newValues); |
| 82 assert.deepEqual(expectOldValues, oldValues); |
| 83 assert.deepEqual(expectObserved, observed); |
| 84 |
| 85 if (!dontDeliver) { |
| 86 assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| 87 window.dirtyCheckCycleCount === 1); |
| 88 } |
| 89 |
| 90 callbackArgs = undefined; |
| 91 callbackInvoked = false; |
| 92 } |
| 93 |
| 94 var createObject = ('__proto__' in {}) ? |
| 95 function(obj) { return obj; } : |
| 96 function(obj) { |
| 97 var proto = obj.__proto__; |
| 98 if (!proto) |
| 99 return obj; |
| 100 var newObject = Object.create(proto); |
| 101 Object.getOwnPropertyNames(obj).forEach(function(name) { |
| 102 Object.defineProperty(newObject, name, |
| 103 Object.getOwnPropertyDescriptor(obj, name)); |
| 104 }); |
| 105 return newObject; |
| 106 }; |
| 107 |
| 108 function assertPath(pathString, expectKeys, expectSerialized) { |
| 109 var path = Path.get(pathString); |
| 110 if (!expectKeys) { |
| 111 assert.isFalse(path.valid); |
| 112 return; |
| 113 } |
| 114 |
| 115 assert.deepEqual(Array.prototype.slice.apply(path), expectKeys); |
| 116 assert.strictEqual(path.toString(), expectSerialized); |
| 117 } |
| 118 |
| 119 function assertInvalidPath(pathString) { |
| 120 assertPath(pathString); |
| 121 } |
| 122 |
| 123 describe('Path', function() { |
| 124 it('constructor throws', function() { |
| 125 assert.throws(function() { |
| 126 new Path('foo') |
| 127 }); |
| 128 }); |
| 129 |
| 130 it('path validity', function() { |
| 131 // invalid path get value is always undefined |
| 132 var p = Path.get('a b'); |
| 133 assert.isFalse(p.valid); |
| 134 assert.isUndefined(p.getValueFrom({ a: { b: 2 }})); |
| 135 |
| 136 assertPath('', [], ''); |
| 137 assertPath(' ', [], ''); |
| 138 assertPath(null, [], ''); |
| 139 assertPath(undefined, [], ''); |
| 140 assertPath('a', ['a'], 'a'); |
| 141 assertPath('a.b', ['a', 'b'], 'a.b'); |
| 142 assertPath('a. b', ['a', 'b'], 'a.b'); |
| 143 assertPath('a .b', ['a', 'b'], 'a.b'); |
| 144 assertPath('a . b', ['a', 'b'], 'a.b'); |
| 145 assertPath(' a . b ', ['a', 'b'], 'a.b'); |
| 146 assertPath('a[0]', ['a', '0'], 'a[0]'); |
| 147 assertPath('a [0]', ['a', '0'], 'a[0]'); |
| 148 assertPath('a[0][1]', ['a', '0', '1'], 'a[0][1]'); |
| 149 assertPath('a [ 0 ] [ 1 ] ', ['a', '0', '1'], 'a[0][1]'); |
| 150 assertPath('[1234567890] ', ['1234567890'], '[1234567890]'); |
| 151 assertPath(' [1234567890] ', ['1234567890'], '[1234567890]'); |
| 152 assertPath('opt0', ['opt0'], 'opt0'); |
| 153 assertPath('$foo.$bar._baz', ['$foo', '$bar', '_baz'], '$foo.$bar._baz'); |
| 154 assertPath('foo["baz"]', ['foo', 'baz'], 'foo.baz'); |
| 155 assertPath('foo["b\\"az"]', ['foo', 'b"az'], 'foo["b\\"az"]'); |
| 156 assertPath("foo['b\\'az']", ['foo', "b'az"], 'foo["b\'az"]'); |
| 157 assertPath(['a', 'b'], ['a', 'b'], 'a.b'); |
| 158 assertPath([''], [''], '[""]'); |
| 159 |
| 160 function Foo(val) { this.val = val; } |
| 161 Foo.prototype.toString = function() { return 'Foo' + this.val; }; |
| 162 assertPath([new Foo('a'), new Foo('b')], ['Fooa', 'Foob'], 'Fooa.Foob'); |
| 163 |
| 164 assertInvalidPath('.'); |
| 165 assertInvalidPath(' . '); |
| 166 assertInvalidPath('..'); |
| 167 assertInvalidPath('a[4'); |
| 168 assertInvalidPath('a.b.'); |
| 169 assertInvalidPath('a,b'); |
| 170 assertInvalidPath('a["foo]'); |
| 171 assertInvalidPath('[0x04]'); |
| 172 assertInvalidPath('[0foo]'); |
| 173 assertInvalidPath('[foo-bar]'); |
| 174 assertInvalidPath('foo-bar'); |
| 175 assertInvalidPath('42'); |
| 176 assertInvalidPath('a[04]'); |
| 177 assertInvalidPath(' a [ 04 ]'); |
| 178 assertInvalidPath(' 42 '); |
| 179 assertInvalidPath('foo["bar]'); |
| 180 assertInvalidPath("foo['bar]"); |
| 181 }); |
| 182 |
| 183 it('Paths are interned', function() { |
| 184 var p = Path.get('foo.bar'); |
| 185 var p2 = Path.get('foo.bar'); |
| 186 assert.strictEqual(p, p2); |
| 187 |
| 188 var p3 = Path.get(''); |
| 189 var p4 = Path.get(''); |
| 190 assert.strictEqual(p3, p4); |
| 191 }); |
| 192 |
| 193 it('null is empty path', function() { |
| 194 assert.strictEqual(Path.get(''), Path.get(null)); |
| 195 }); |
| 196 |
| 197 it('undefined is empty path', function() { |
| 198 assert.strictEqual(Path.get(undefined), Path.get(null)); |
| 199 }); |
| 200 |
| 201 it('Path.getValueFrom', function() { |
| 202 var obj = { |
| 203 a: { |
| 204 b: { |
| 205 c: 1 |
| 206 } |
| 207 } |
| 208 }; |
| 209 |
| 210 var p1 = Path.get('a'); |
| 211 var p2 = Path.get('a.b'); |
| 212 var p3 = Path.get('a.b.c'); |
| 213 |
| 214 assert.strictEqual(obj.a, p1.getValueFrom(obj)); |
| 215 assert.strictEqual(obj.a.b, p2.getValueFrom(obj)); |
| 216 assert.strictEqual(1, p3.getValueFrom(obj)); |
| 217 |
| 218 obj.a.b.c = 2; |
| 219 assert.strictEqual(2, p3.getValueFrom(obj)); |
| 220 |
| 221 obj.a.b = { |
| 222 c: 3 |
| 223 }; |
| 224 assert.strictEqual(3, p3.getValueFrom(obj)); |
| 225 |
| 226 obj.a = { |
| 227 b: 4 |
| 228 }; |
| 229 assert.strictEqual(undefined, p3.getValueFrom(obj)); |
| 230 assert.strictEqual(4, p2.getValueFrom(obj)); |
| 231 }); |
| 232 |
| 233 it('Path.setValueFrom', function() { |
| 234 var obj = {}; |
| 235 var p2 = Path.get('bar'); |
| 236 |
| 237 Path.get('foo').setValueFrom(obj, 3); |
| 238 assert.equal(3, obj.foo); |
| 239 |
| 240 var bar = { baz: 3 }; |
| 241 |
| 242 Path.get('bar').setValueFrom(obj, bar); |
| 243 assert.equal(bar, obj.bar); |
| 244 |
| 245 var p = Path.get('bar.baz.bat'); |
| 246 p.setValueFrom(obj, 'not here'); |
| 247 assert.equal(undefined, p.getValueFrom(obj)); |
| 248 }); |
| 249 |
| 250 it('Degenerate Values', function() { |
| 251 var emptyPath = Path.get(); |
| 252 var foo = {}; |
| 253 |
| 254 assert.equal(null, emptyPath.getValueFrom(null)); |
| 255 assert.equal(foo, emptyPath.getValueFrom(foo)); |
| 256 assert.equal(3, emptyPath.getValueFrom(3)); |
| 257 assert.equal(undefined, Path.get('a').getValueFrom(undefined)); |
| 258 }); |
| 259 }); |
| 260 |
| 261 describe('Basic Tests', function() { |
| 262 |
| 263 it('Exception Doesnt Stop Notification', function() { |
| 264 var model = [1]; |
| 265 var count = 0; |
| 266 |
| 267 var observer2 = new PathObserver(model, '[0]'); |
| 268 observer2.open(function() { |
| 269 count++; |
| 270 throw 'ouch'; |
| 271 }); |
| 272 |
| 273 var observer3 = new ArrayObserver(model); |
| 274 observer3.open(function() { |
| 275 count++; |
| 276 throw 'ouch'; |
| 277 }); |
| 278 |
| 279 model[0] = 2; |
| 280 model[1] = 2; |
| 281 |
| 282 observer2.deliver(); |
| 283 observer3.deliver(); |
| 284 |
| 285 assert.equal(2, count); |
| 286 |
| 287 observer2.close(); |
| 288 observer3.close(); |
| 289 }); |
| 290 |
| 291 it('Can only open once', function() { |
| 292 observer = new PathObserver({ id: 1 }, 'id'); |
| 293 observer.open(callback); |
| 294 assert.throws(function() { |
| 295 observer.open(callback); |
| 296 }); |
| 297 observer.close(); |
| 298 |
| 299 observer = new CompoundObserver(); |
| 300 observer.open(callback); |
| 301 assert.throws(function() { |
| 302 observer.open(callback); |
| 303 }); |
| 304 observer.close(); |
| 305 |
| 306 observer = new ArrayObserver([], 'id'); |
| 307 observer.open(callback); |
| 308 assert.throws(function() { |
| 309 observer.open(callback); |
| 310 }); |
| 311 observer.close(); |
| 312 |
| 313 }); |
| 314 |
| 315 }); |
| 316 |
| 317 describe('ObserverTransform', function() { |
| 318 |
| 319 it('Close Invokes Close', function() { |
| 320 var count = 0; |
| 321 var observer = { |
| 322 open: function() {}, |
| 323 close: function() { count++; } |
| 324 }; |
| 325 |
| 326 var observer = new ObserverTransform(observer); |
| 327 observer.open(); |
| 328 observer.close(); |
| 329 assert.strictEqual(1, count); |
| 330 }); |
| 331 |
| 332 it('valueFn/setValueFn', function() { |
| 333 var obj = { foo: 1 }; |
| 334 |
| 335 function valueFn(value) { return value * 2; } |
| 336 |
| 337 function setValueFn(value) { return value / 2; } |
| 338 |
| 339 observer = new ObserverTransform(new PathObserver(obj, 'foo'), |
| 340 valueFn, |
| 341 setValueFn); |
| 342 observer.open(callback); |
| 343 |
| 344 obj.foo = 2; |
| 345 |
| 346 assert.strictEqual(4, observer.discardChanges()); |
| 347 assertNoChanges(); |
| 348 |
| 349 observer.setValue(2); |
| 350 assert.strictEqual(obj.foo, 1); |
| 351 assertPathChanges(2, 4); |
| 352 |
| 353 obj.foo = 10; |
| 354 assertPathChanges(20, 2); |
| 355 |
| 356 observer.close(); |
| 357 }); |
| 358 |
| 359 it('valueFn - object literal', function() { |
| 360 var model = {}; |
| 361 |
| 362 function valueFn(value) { |
| 363 return [ value ]; |
| 364 } |
| 365 |
| 366 observer = new ObserverTransform(new PathObserver(model, 'foo'), valueFn); |
| 367 observer.open(callback); |
| 368 |
| 369 model.foo = 1; |
| 370 assertPathChanges([1], [undefined]); |
| 371 |
| 372 model.foo = 3; |
| 373 assertPathChanges([3], [1]); |
| 374 |
| 375 observer.close(); |
| 376 }); |
| 377 |
| 378 it('CompoundObserver - valueFn reduction', function() { |
| 379 var model = { a: 1, b: 2, c: 3 }; |
| 380 |
| 381 function valueFn(values) { |
| 382 return values.reduce(function(last, cur) { |
| 383 return typeof cur === 'number' ? last + cur : undefined; |
| 384 }, 0); |
| 385 } |
| 386 |
| 387 var compound = new CompoundObserver(); |
| 388 compound.addPath(model, 'a'); |
| 389 compound.addPath(model, 'b'); |
| 390 compound.addPath(model, Path.get('c')); |
| 391 |
| 392 observer = new ObserverTransform(compound, valueFn); |
| 393 assert.strictEqual(6, observer.open(callback)); |
| 394 |
| 395 model.a = -10; |
| 396 model.b = 20; |
| 397 model.c = 30; |
| 398 assertPathChanges(40, 6); |
| 399 |
| 400 observer.close(); |
| 401 }); |
| 402 }) |
| 403 |
| 404 describe('PathObserver Tests', function() { |
| 405 |
| 406 beforeEach(doSetup); |
| 407 |
| 408 afterEach(doTeardown); |
| 409 |
| 410 it('Callback args', function() { |
| 411 var obj = { |
| 412 foo: 'bar' |
| 413 }; |
| 414 |
| 415 var path = Path.get('foo'); |
| 416 var observer = new PathObserver(obj, path); |
| 417 |
| 418 var args; |
| 419 observer.open(function() { |
| 420 args = Array.prototype.slice.apply(arguments); |
| 421 }); |
| 422 |
| 423 obj.foo = 'baz'; |
| 424 observer.deliver(); |
| 425 assert.strictEqual(args.length, 3); |
| 426 assert.strictEqual(args[0], 'baz'); |
| 427 assert.strictEqual(args[1], 'bar'); |
| 428 assert.strictEqual(args[2], observer); |
| 429 assert.strictEqual(args[2].path, path); |
| 430 observer.close(); |
| 431 }); |
| 432 |
| 433 it('PathObserver.path', function() { |
| 434 var obj = { |
| 435 foo: 'bar' |
| 436 }; |
| 437 |
| 438 var path = Path.get('foo'); |
| 439 var observer = new PathObserver(obj, 'foo'); |
| 440 assert.strictEqual(observer.path, Path.get('foo')); |
| 441 }); |
| 442 |
| 443 |
| 444 it('invalid', function() { |
| 445 var observer = new PathObserver({ a: { b: 1 }}Â , 'a b'); |
| 446 observer.open(callback); |
| 447 assert.strictEqual(undefined, observer.value); |
| 448 observer.deliver(); |
| 449 assert.isFalse(callbackInvoked); |
| 450 }); |
| 451 |
| 452 it('Optional target for callback', function() { |
| 453 var target = { |
| 454 changed: function(value, oldValue) { |
| 455 this.called = true; |
| 456 } |
| 457 }; |
| 458 var obj = { foo: 1 }; |
| 459 var observer = new PathObserver(obj, 'foo'); |
| 460 observer.open(target.changed, target); |
| 461 obj.foo = 2; |
| 462 observer.deliver(); |
| 463 assert.isTrue(target.called); |
| 464 |
| 465 observer.close(); |
| 466 }); |
| 467 |
| 468 it('Delivery Until No Changes', function() { |
| 469 var obj = { foo: { bar: 5 }}; |
| 470 var callbackCount = 0; |
| 471 var observer = new PathObserver(obj, 'foo . bar'); |
| 472 observer.open(function() { |
| 473 callbackCount++; |
| 474 if (!obj.foo.bar) |
| 475 return; |
| 476 |
| 477 obj.foo.bar--; |
| 478 }); |
| 479 |
| 480 obj.foo.bar--; |
| 481 observer.deliver(); |
| 482 |
| 483 assert.equal(5, callbackCount); |
| 484 |
| 485 observer.close(); |
| 486 }); |
| 487 |
| 488 it('Path disconnect', function() { |
| 489 var arr = {}; |
| 490 |
| 491 arr.foo = 'bar'; |
| 492 observer = new PathObserver(arr, 'foo'); |
| 493 observer.open(callback); |
| 494 arr.foo = 'baz'; |
| 495 |
| 496 assertPathChanges('baz', 'bar'); |
| 497 arr.foo = 'bar'; |
| 498 |
| 499 observer.close(); |
| 500 |
| 501 arr.foo = 'boo'; |
| 502 assertNoChanges(); |
| 503 }); |
| 504 |
| 505 it('Path discardChanges', function() { |
| 506 var arr = {}; |
| 507 |
| 508 arr.foo = 'bar'; |
| 509 observer = new PathObserver(arr, 'foo'); |
| 510 observer.open(callback); |
| 511 arr.foo = 'baz'; |
| 512 |
| 513 assertPathChanges('baz', 'bar'); |
| 514 |
| 515 arr.foo = 'bat'; |
| 516 observer.discardChanges(); |
| 517 assertNoChanges(); |
| 518 |
| 519 arr.foo = 'bag'; |
| 520 assertPathChanges('bag', 'bat'); |
| 521 observer.close(); |
| 522 }); |
| 523 |
| 524 it('Path setValue', function() { |
| 525 var obj = {}; |
| 526 |
| 527 obj.foo = 'bar'; |
| 528 observer = new PathObserver(obj, 'foo'); |
| 529 observer.open(callback); |
| 530 obj.foo = 'baz'; |
| 531 |
| 532 observer.setValue('bat'); |
| 533 assert.strictEqual(obj.foo, 'bat'); |
| 534 assertPathChanges('bat', 'bar'); |
| 535 |
| 536 observer.setValue('bot'); |
| 537 observer.discardChanges(); |
| 538 assertNoChanges(); |
| 539 |
| 540 observer.close(); |
| 541 }); |
| 542 |
| 543 it('Degenerate Values', function() { |
| 544 var emptyPath = Path.get(); |
| 545 observer = new PathObserver(null, ''); |
| 546 observer.open(callback); |
| 547 assert.equal(null, observer.value); |
| 548 observer.close(); |
| 549 |
| 550 var foo = {}; |
| 551 observer = new PathObserver(foo, ''); |
| 552 assert.equal(foo, observer.open(callback)); |
| 553 observer.close(); |
| 554 |
| 555 observer = new PathObserver(3, ''); |
| 556 assert.equal(3, observer.open(callback)); |
| 557 observer.close(); |
| 558 |
| 559 observer = new PathObserver(undefined, 'a'); |
| 560 assert.equal(undefined, observer.open(callback)); |
| 561 observer.close(); |
| 562 |
| 563 var bar = { id: 23 }; |
| 564 observer = new PathObserver(undefined, 'a/3!'); |
| 565 assert.equal(undefined, observer.open(callback)); |
| 566 observer.close(); |
| 567 }); |
| 568 |
| 569 it('Path NaN', function() { |
| 570 var foo = { val: 1 }; |
| 571 observer = new PathObserver(foo, 'val'); |
| 572 observer.open(callback); |
| 573 foo.val = 0/0; |
| 574 |
| 575 // Can't use assertSummary because deepEqual() will fail with NaN |
| 576 observer.deliver(); |
| 577 assert.isTrue(callbackInvoked); |
| 578 assert.isTrue(isNaN(callbackArgs[0])); |
| 579 assert.strictEqual(1, callbackArgs[1]); |
| 580 observer.close(); |
| 581 }); |
| 582 |
| 583 it('Path Set Value Back To Same', function() { |
| 584 var obj = {}; |
| 585 var path = Path.get('foo'); |
| 586 |
| 587 path.setValueFrom(obj, 3); |
| 588 assert.equal(3, obj.foo); |
| 589 |
| 590 observer = new PathObserver(obj, 'foo'); |
| 591 assert.equal(3, observer.open(callback)); |
| 592 |
| 593 path.setValueFrom(obj, 2); |
| 594 assert.equal(2, observer.discardChanges()); |
| 595 |
| 596 path.setValueFrom(obj, 3); |
| 597 assert.equal(3, observer.discardChanges()); |
| 598 |
| 599 assertNoChanges(); |
| 600 |
| 601 observer.close(); |
| 602 }); |
| 603 |
| 604 it('Path Triple Equals', function() { |
| 605 var model = { }; |
| 606 |
| 607 observer = new PathObserver(model, 'foo'); |
| 608 observer.open(callback); |
| 609 |
| 610 model.foo = null; |
| 611 assertPathChanges(null, undefined); |
| 612 |
| 613 model.foo = undefined; |
| 614 assertPathChanges(undefined, null); |
| 615 |
| 616 observer.close(); |
| 617 }); |
| 618 |
| 619 it('Path Simple', function() { |
| 620 var model = { }; |
| 621 |
| 622 observer = new PathObserver(model, 'foo'); |
| 623 observer.open(callback); |
| 624 |
| 625 model.foo = 1; |
| 626 assertPathChanges(1, undefined); |
| 627 |
| 628 model.foo = 2; |
| 629 assertPathChanges(2, 1); |
| 630 |
| 631 delete model.foo; |
| 632 assertPathChanges(undefined, 2); |
| 633 |
| 634 observer.close(); |
| 635 }); |
| 636 |
| 637 it('Path Simple - path object', function() { |
| 638 var model = { }; |
| 639 |
| 640 var path = Path.get('foo'); |
| 641 observer = new PathObserver(model, path); |
| 642 observer.open(callback); |
| 643 |
| 644 model.foo = 1; |
| 645 assertPathChanges(1, undefined); |
| 646 |
| 647 model.foo = 2; |
| 648 assertPathChanges(2, 1); |
| 649 |
| 650 delete model.foo; |
| 651 assertPathChanges(undefined, 2); |
| 652 |
| 653 observer.close(); |
| 654 }); |
| 655 |
| 656 it('Path - root is initially null', function(done) { |
| 657 var model = { }; |
| 658 |
| 659 var path = Path.get('foo'); |
| 660 observer = new PathObserver(model, 'foo.bar'); |
| 661 observer.open(callback); |
| 662 |
| 663 model.foo = { }; |
| 664 then(function() { |
| 665 model.foo.bar = 1; |
| 666 |
| 667 }).then(function() { |
| 668 assertPathChanges(1, undefined, true); |
| 669 |
| 670 observer.close(); |
| 671 done(); |
| 672 }); |
| 673 }); |
| 674 |
| 675 it('Path With Indices', function() { |
| 676 var model = []; |
| 677 |
| 678 observer = new PathObserver(model, '[0]'); |
| 679 observer.open(callback); |
| 680 |
| 681 model.push(1); |
| 682 assertPathChanges(1, undefined); |
| 683 |
| 684 observer.close(); |
| 685 }); |
| 686 |
| 687 it('Path Observation', function() { |
| 688 var model = { |
| 689 a: { |
| 690 b: { |
| 691 c: 'hello, world' |
| 692 } |
| 693 } |
| 694 }; |
| 695 |
| 696 observer = new PathObserver(model, 'a.b.c'); |
| 697 observer.open(callback); |
| 698 |
| 699 model.a.b.c = 'hello, mom'; |
| 700 assertPathChanges('hello, mom', 'hello, world'); |
| 701 |
| 702 model.a.b = { |
| 703 c: 'hello, dad' |
| 704 }; |
| 705 assertPathChanges('hello, dad', 'hello, mom'); |
| 706 |
| 707 model.a = { |
| 708 b: { |
| 709 c: 'hello, you' |
| 710 } |
| 711 }; |
| 712 assertPathChanges('hello, you', 'hello, dad'); |
| 713 |
| 714 model.a.b = 1; |
| 715 assertPathChanges(undefined, 'hello, you'); |
| 716 |
| 717 // Stop observing |
| 718 observer.close(); |
| 719 |
| 720 model.a.b = {c: 'hello, back again -- but not observing'}; |
| 721 assertNoChanges(); |
| 722 |
| 723 // Resume observing |
| 724 observer = new PathObserver(model, 'a.b.c'); |
| 725 observer.open(callback); |
| 726 |
| 727 model.a.b.c = 'hello. Back for reals'; |
| 728 assertPathChanges('hello. Back for reals', |
| 729 'hello, back again -- but not observing'); |
| 730 |
| 731 observer.close(); |
| 732 }); |
| 733 |
| 734 it('Path Set To Same As Prototype', function() { |
| 735 var model = createObject({ |
| 736 __proto__: { |
| 737 id: 1 |
| 738 } |
| 739 }); |
| 740 |
| 741 observer = new PathObserver(model, 'id'); |
| 742 observer.open(callback); |
| 743 model.id = 1; |
| 744 |
| 745 assertNoChanges(); |
| 746 observer.close(); |
| 747 }); |
| 748 |
| 749 it('Path Set Read Only', function() { |
| 750 var model = {}; |
| 751 Object.defineProperty(model, 'x', { |
| 752 configurable: true, |
| 753 writable: false, |
| 754 value: 1 |
| 755 }); |
| 756 observer = new PathObserver(model, 'x'); |
| 757 observer.open(callback); |
| 758 |
| 759 assert.throws(function() { |
| 760 model.x = 2; |
| 761 }); |
| 762 |
| 763 assertNoChanges(); |
| 764 observer.close(); |
| 765 }); |
| 766 |
| 767 it('Path Set Shadows', function() { |
| 768 var model = createObject({ |
| 769 __proto__: { |
| 770 x: 1 |
| 771 } |
| 772 }); |
| 773 |
| 774 observer = new PathObserver(model, 'x'); |
| 775 observer.open(callback); |
| 776 model.x = 2; |
| 777 assertPathChanges(2, 1); |
| 778 observer.close(); |
| 779 }); |
| 780 |
| 781 it('Delete With Same Value On Prototype', function() { |
| 782 var model = createObject({ |
| 783 __proto__: { |
| 784 x: 1, |
| 785 }, |
| 786 x: 1 |
| 787 }); |
| 788 |
| 789 observer = new PathObserver(model, 'x'); |
| 790 observer.open(callback); |
| 791 delete model.x; |
| 792 assertNoChanges(); |
| 793 observer.close(); |
| 794 }); |
| 795 |
| 796 it('Delete With Different Value On Prototype', function() { |
| 797 var model = createObject({ |
| 798 __proto__: { |
| 799 x: 1, |
| 800 }, |
| 801 x: 2 |
| 802 }); |
| 803 |
| 804 observer = new PathObserver(model, 'x'); |
| 805 observer.open(callback); |
| 806 delete model.x; |
| 807 assertPathChanges(1, 2); |
| 808 observer.close(); |
| 809 }); |
| 810 |
| 811 it('Value Change On Prototype', function() { |
| 812 var proto = { |
| 813 x: 1 |
| 814 } |
| 815 var model = createObject({ |
| 816 __proto__: proto |
| 817 }); |
| 818 |
| 819 observer = new PathObserver(model, 'x'); |
| 820 observer.open(callback); |
| 821 model.x = 2; |
| 822 assertPathChanges(2, 1); |
| 823 |
| 824 delete model.x; |
| 825 assertPathChanges(1, 2); |
| 826 |
| 827 proto.x = 3; |
| 828 assertPathChanges(3, 1); |
| 829 observer.close(); |
| 830 }); |
| 831 |
| 832 // FIXME: Need test of observing change on proto. |
| 833 |
| 834 it('Delete Of Non Configurable', function() { |
| 835 var model = {}; |
| 836 Object.defineProperty(model, 'x', { |
| 837 configurable: false, |
| 838 value: 1 |
| 839 }); |
| 840 |
| 841 observer = new PathObserver(model, 'x'); |
| 842 observer.open(callback); |
| 843 |
| 844 assert.throws(function() { |
| 845 delete model.x; |
| 846 }); |
| 847 |
| 848 assertNoChanges(); |
| 849 observer.close(); |
| 850 }); |
| 851 |
| 852 it('Notify', function() { |
| 853 if (typeof Object.getNotifier !== 'function') |
| 854 return; |
| 855 |
| 856 var model = { |
| 857 a: {} |
| 858 } |
| 859 |
| 860 var _b = 2; |
| 861 |
| 862 Object.defineProperty(model.a, 'b', { |
| 863 get: function() { return _b; }, |
| 864 set: function(b) { |
| 865 Object.getNotifier(this).notify({ |
| 866 type: 'update', |
| 867 name: 'b', |
| 868 oldValue: _b |
| 869 }); |
| 870 |
| 871 _b = b; |
| 872 } |
| 873 }); |
| 874 |
| 875 observer = new PathObserver(model, 'a.b'); |
| 876 observer.open(callback); |
| 877 _b = 3; |
| 878 assertPathChanges(3, 2); |
| 879 |
| 880 model.a.b = 4; // will be observed. |
| 881 assertPathChanges(4, 3); |
| 882 |
| 883 observer.close(); |
| 884 }); |
| 885 |
| 886 it('issue-161', function(done) { |
| 887 var model = { model: 'model' }; |
| 888 var ob1 = new PathObserver(model, 'obj.bar'); |
| 889 var called = false |
| 890 ob1.open(function() { |
| 891 called = true; |
| 892 }); |
| 893 |
| 894 var obj2 = new PathObserver(model, 'obj'); |
| 895 obj2.open(function() { |
| 896 model.obj.bar = true; |
| 897 }); |
| 898 |
| 899 model.obj = { 'obj': 'obj' }; |
| 900 model.obj.foo = true; |
| 901 |
| 902 then(function() { |
| 903 assert.strictEqual(called, true); |
| 904 done(); |
| 905 }); |
| 906 }); |
| 907 |
| 908 it('object cycle', function(done) { |
| 909 var model = { a: {}, c: 1 }; |
| 910 model.a.b = model; |
| 911 |
| 912 var called = 0; |
| 913 new PathObserver(model, 'a.b.c').open(function() { |
| 914 called++; |
| 915 }); |
| 916 |
| 917 // This change should be detected, even though it's a change to the root |
| 918 // object and isn't a change to `a`. |
| 919 model.c = 42; |
| 920 |
| 921 then(function() { |
| 922 assert.equal(called, 1); |
| 923 done(); |
| 924 }); |
| 925 }); |
| 926 |
| 927 }); |
| 928 |
| 929 |
| 930 describe('CompoundObserver Tests', function() { |
| 931 |
| 932 beforeEach(doSetup); |
| 933 |
| 934 afterEach(doTeardown); |
| 935 |
| 936 it('Simple', function() { |
| 937 var model = { a: 1, b: 2, c: 3 }; |
| 938 |
| 939 observer = new CompoundObserver(); |
| 940 observer.addPath(model, 'a'); |
| 941 observer.addPath(model, 'b'); |
| 942 observer.addPath(model, Path.get('c')); |
| 943 observer.open(callback); |
| 944 assertNoChanges(); |
| 945 |
| 946 var observerCallbackArg = [model, Path.get('a'), |
| 947 model, Path.get('b'), |
| 948 model, Path.get('c')]; |
| 949 model.a = -10; |
| 950 model.b = 20; |
| 951 model.c = 30; |
| 952 assertCompoundPathChanges([-10, 20, 30], [1, 2, 3], |
| 953 observerCallbackArg); |
| 954 |
| 955 model.a = 'a'; |
| 956 model.c = 'c'; |
| 957 assertCompoundPathChanges(['a', 20, 'c'], [-10,, 30], |
| 958 observerCallbackArg); |
| 959 |
| 960 model.a = 2; |
| 961 model.b = 3; |
| 962 model.c = 4; |
| 963 |
| 964 assertCompoundPathChanges([2, 3, 4], ['a', 20, 'c'], |
| 965 observerCallbackArg); |
| 966 |
| 967 model.a = 'z'; |
| 968 model.b = 'y'; |
| 969 model.c = 'x'; |
| 970 assert.deepEqual(['z', 'y', 'x'], observer.discardChanges()); |
| 971 assertNoChanges(); |
| 972 |
| 973 assert.strictEqual('z', model.a); |
| 974 assert.strictEqual('y', model.b); |
| 975 assert.strictEqual('x', model.c); |
| 976 assertNoChanges(); |
| 977 |
| 978 observer.close(); |
| 979 }); |
| 980 |
| 981 it('reportChangesOnOpen', function() { |
| 982 var model = { a: 1, b: 2, c: 3 }; |
| 983 |
| 984 observer = new CompoundObserver(true); |
| 985 observer.addPath(model, 'a'); |
| 986 observer.addPath(model, 'b'); |
| 987 observer.addPath(model, Path.get('c')); |
| 988 |
| 989 model.a = -10; |
| 990 model.b = 20; |
| 991 observer.open(callback); |
| 992 var observerCallbackArg = [model, Path.get('a'), |
| 993 model, Path.get('b'), |
| 994 model, Path.get('c')]; |
| 995 assertCompoundPathChanges([-10, 20, 3], [1, 2, ], |
| 996 observerCallbackArg, true); |
| 997 observer.close(); |
| 998 }); |
| 999 |
| 1000 it('Degenerate Values', function() { |
| 1001 var model = {}; |
| 1002 observer = new CompoundObserver(); |
| 1003 observer.addPath({}, '.'); // invalid path |
| 1004 observer.addPath('obj-value', ''); // empty path |
| 1005 observer.addPath({}, 'foo'); // unreachable |
| 1006 observer.addPath(3, 'bar'); // non-object with non-empty path |
| 1007 var values = observer.open(callback); |
| 1008 assert.strictEqual(4, values.length); |
| 1009 assert.strictEqual(undefined, values[0]); |
| 1010 assert.strictEqual('obj-value', values[1]); |
| 1011 assert.strictEqual(undefined, values[2]); |
| 1012 assert.strictEqual(undefined, values[3]); |
| 1013 observer.close(); |
| 1014 }); |
| 1015 |
| 1016 it('valueFn - return object literal', function() { |
| 1017 var model = { a: 1}; |
| 1018 |
| 1019 function valueFn(values) { |
| 1020 return {}; |
| 1021 } |
| 1022 |
| 1023 observer = new CompoundObserver(valueFn); |
| 1024 |
| 1025 observer.addPath(model, 'a'); |
| 1026 observer.open(callback); |
| 1027 model.a = 2; |
| 1028 |
| 1029 observer.deliver(); |
| 1030 assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| 1031 window.dirtyCheckCycleCount === 1); |
| 1032 observer.close(); |
| 1033 }); |
| 1034 |
| 1035 it('reset', function() { |
| 1036 var model = { a: 1, b: 2, c: 3 }; |
| 1037 var callCount = 0; |
| 1038 function callback() { |
| 1039 callCount++; |
| 1040 } |
| 1041 |
| 1042 observer = new CompoundObserver(); |
| 1043 |
| 1044 observer.addPath(model, 'a'); |
| 1045 observer.addPath(model, 'b'); |
| 1046 assert.deepEqual([1, 2], observer.open(callback)); |
| 1047 |
| 1048 model.a = 2; |
| 1049 observer.deliver(); |
| 1050 assert.strictEqual(1, callCount); |
| 1051 |
| 1052 model.b = 3; |
| 1053 observer.deliver(); |
| 1054 assert.strictEqual(2, callCount); |
| 1055 |
| 1056 model.c = 4; |
| 1057 observer.deliver(); |
| 1058 assert.strictEqual(2, callCount); |
| 1059 |
| 1060 observer.startReset(); |
| 1061 observer.addPath(model, 'b'); |
| 1062 observer.addPath(model, 'c'); |
| 1063 assert.deepEqual([3, 4], observer.finishReset()) |
| 1064 |
| 1065 model.a = 3; |
| 1066 observer.deliver(); |
| 1067 assert.strictEqual(2, callCount); |
| 1068 |
| 1069 model.b = 4; |
| 1070 observer.deliver(); |
| 1071 assert.strictEqual(3, callCount); |
| 1072 |
| 1073 model.c = 5; |
| 1074 observer.deliver(); |
| 1075 assert.strictEqual(4, callCount); |
| 1076 |
| 1077 observer.close(); |
| 1078 }); |
| 1079 |
| 1080 it('Heterogeneous', function() { |
| 1081 var model = { a: 1, b: 2 }; |
| 1082 var otherModel = { c: 3 }; |
| 1083 |
| 1084 function valueFn(value) { return value * 2; } |
| 1085 function setValueFn(value) { return value / 2; } |
| 1086 |
| 1087 var compound = new CompoundObserver; |
| 1088 compound.addPath(model, 'a'); |
| 1089 compound.addObserver(new ObserverTransform(new PathObserver(model, 'b'), |
| 1090 valueFn, setValueFn)); |
| 1091 compound.addObserver(new PathObserver(otherModel, 'c')); |
| 1092 |
| 1093 function combine(values) { |
| 1094 return values[0] + values[1] + values[2]; |
| 1095 }; |
| 1096 observer = new ObserverTransform(compound, combine); |
| 1097 assert.strictEqual(8, observer.open(callback)); |
| 1098 |
| 1099 model.a = 2; |
| 1100 model.b = 4; |
| 1101 assertPathChanges(13, 8); |
| 1102 |
| 1103 model.b = 10; |
| 1104 otherModel.c = 5; |
| 1105 assertPathChanges(27, 13); |
| 1106 |
| 1107 model.a = 20; |
| 1108 model.b = 1; |
| 1109 otherModel.c = 5; |
| 1110 assertNoChanges(); |
| 1111 |
| 1112 observer.close(); |
| 1113 }) |
| 1114 }); |
| 1115 |
| 1116 describe('ArrayObserver Tests', function() { |
| 1117 |
| 1118 beforeEach(doSetup); |
| 1119 |
| 1120 afterEach(doTeardown); |
| 1121 |
| 1122 function ensureNonSparse(arr) { |
| 1123 for (var i = 0; i < arr.length; i++) { |
| 1124 if (i in arr) |
| 1125 continue; |
| 1126 arr[i] = undefined; |
| 1127 } |
| 1128 } |
| 1129 |
| 1130 function assertArrayChanges(expectSplices) { |
| 1131 observer.deliver(); |
| 1132 var splices = callbackArgs[0]; |
| 1133 |
| 1134 assert.isTrue(callbackInvoked); |
| 1135 |
| 1136 splices.forEach(function(splice) { |
| 1137 ensureNonSparse(splice.removed); |
| 1138 }); |
| 1139 |
| 1140 expectSplices.forEach(function(splice) { |
| 1141 ensureNonSparse(splice.removed); |
| 1142 }); |
| 1143 |
| 1144 assert.deepEqual(expectSplices, splices); |
| 1145 callbackArgs = undefined; |
| 1146 callbackInvoked = false; |
| 1147 } |
| 1148 |
| 1149 function applySplicesAndAssertDeepEqual(orig, copy) { |
| 1150 observer.deliver(); |
| 1151 if (callbackInvoked) { |
| 1152 var splices = callbackArgs[0]; |
| 1153 ArrayObserver.applySplices(copy, orig, splices); |
| 1154 } |
| 1155 |
| 1156 ensureNonSparse(orig); |
| 1157 ensureNonSparse(copy); |
| 1158 assert.deepEqual(orig, copy); |
| 1159 callbackArgs = undefined; |
| 1160 callbackInvoked = false; |
| 1161 } |
| 1162 |
| 1163 function assertEditDistance(orig, expectDistance) { |
| 1164 observer.deliver(); |
| 1165 var splices = callbackArgs[0]; |
| 1166 var actualDistance = 0; |
| 1167 |
| 1168 if (callbackInvoked) { |
| 1169 splices.forEach(function(splice) { |
| 1170 actualDistance += splice.addedCount + splice.removed.length; |
| 1171 }); |
| 1172 } |
| 1173 |
| 1174 assert.deepEqual(expectDistance, actualDistance); |
| 1175 callbackArgs = undefined; |
| 1176 callbackInvoked = false; |
| 1177 } |
| 1178 |
| 1179 function arrayMutationTest(arr, operations) { |
| 1180 var copy = arr.slice(); |
| 1181 observer = new ArrayObserver(arr); |
| 1182 observer.open(callback); |
| 1183 operations.forEach(function(op) { |
| 1184 switch(op.name) { |
| 1185 case 'delete': |
| 1186 delete arr[op.index]; |
| 1187 break; |
| 1188 |
| 1189 case 'update': |
| 1190 arr[op.index] = op.value; |
| 1191 break; |
| 1192 |
| 1193 default: |
| 1194 arr[op.name].apply(arr, op.args); |
| 1195 break; |
| 1196 } |
| 1197 }); |
| 1198 |
| 1199 applySplicesAndAssertDeepEqual(arr, copy); |
| 1200 observer.close(); |
| 1201 } |
| 1202 |
| 1203 it('Optional target for callback', function() { |
| 1204 var target = { |
| 1205 changed: function(splices) { |
| 1206 this.called = true; |
| 1207 } |
| 1208 }; |
| 1209 var obj = []; |
| 1210 var observer = new ArrayObserver(obj); |
| 1211 observer.open(target.changed, target); |
| 1212 obj.length = 1; |
| 1213 observer.deliver(); |
| 1214 assert.isTrue(target.called); |
| 1215 observer.close(); |
| 1216 }); |
| 1217 |
| 1218 it('Delivery Until No Changes', function() { |
| 1219 var arr = [0, 1, 2, 3, 4]; |
| 1220 var callbackCount = 0; |
| 1221 var observer = new ArrayObserver(arr); |
| 1222 observer.open(function() { |
| 1223 callbackCount++; |
| 1224 arr.shift(); |
| 1225 }); |
| 1226 |
| 1227 arr.shift(); |
| 1228 observer.deliver(); |
| 1229 |
| 1230 assert.equal(5, callbackCount); |
| 1231 |
| 1232 observer.close(); |
| 1233 }); |
| 1234 |
| 1235 it('Array disconnect', function() { |
| 1236 var arr = [ 0 ]; |
| 1237 |
| 1238 observer = new ArrayObserver(arr); |
| 1239 observer.open(callback); |
| 1240 |
| 1241 arr[0] = 1; |
| 1242 |
| 1243 assertArrayChanges([{ |
| 1244 index: 0, |
| 1245 removed: [0], |
| 1246 addedCount: 1 |
| 1247 }]); |
| 1248 |
| 1249 observer.close(); |
| 1250 arr[1] = 2; |
| 1251 assertNoChanges(); |
| 1252 }); |
| 1253 |
| 1254 it('Array discardChanges', function() { |
| 1255 var arr = []; |
| 1256 |
| 1257 arr.push(1); |
| 1258 observer = new ArrayObserver(arr); |
| 1259 observer.open(callback); |
| 1260 arr.push(2); |
| 1261 |
| 1262 assertArrayChanges([{ |
| 1263 index: 1, |
| 1264 removed: [], |
| 1265 addedCount: 1 |
| 1266 }]); |
| 1267 |
| 1268 arr.push(3); |
| 1269 observer.discardChanges(); |
| 1270 assertNoChanges(); |
| 1271 |
| 1272 arr.pop(); |
| 1273 assertArrayChanges([{ |
| 1274 index: 2, |
| 1275 removed: [3], |
| 1276 addedCount: 0 |
| 1277 }]); |
| 1278 observer.close(); |
| 1279 }); |
| 1280 |
| 1281 it('Array', function() { |
| 1282 var model = [0, 1]; |
| 1283 |
| 1284 observer = new ArrayObserver(model); |
| 1285 observer.open(callback); |
| 1286 |
| 1287 model[0] = 2; |
| 1288 |
| 1289 assertArrayChanges([{ |
| 1290 index: 0, |
| 1291 removed: [0], |
| 1292 addedCount: 1 |
| 1293 }]); |
| 1294 |
| 1295 model[1] = 3; |
| 1296 assertArrayChanges([{ |
| 1297 index: 1, |
| 1298 removed: [1], |
| 1299 addedCount: 1 |
| 1300 }]); |
| 1301 |
| 1302 observer.close(); |
| 1303 }); |
| 1304 |
| 1305 it('Array observe non-array throws', function() { |
| 1306 assert.throws(function () { |
| 1307 observer = new ArrayObserver({}); |
| 1308 }); |
| 1309 }); |
| 1310 |
| 1311 it('Array Set Same', function() { |
| 1312 var model = [1]; |
| 1313 |
| 1314 observer = new ArrayObserver(model); |
| 1315 observer.open(callback); |
| 1316 |
| 1317 model[0] = 1; |
| 1318 observer.deliver(); |
| 1319 assert.isFalse(callbackInvoked); |
| 1320 observer.close(); |
| 1321 }); |
| 1322 |
| 1323 it('Array Splice', function() { |
| 1324 var model = [0, 1] |
| 1325 |
| 1326 observer = new ArrayObserver(model); |
| 1327 observer.open(callback); |
| 1328 |
| 1329 model.splice(1, 1, 2, 3); // [0, 2, 3] |
| 1330 assertArrayChanges([{ |
| 1331 index: 1, |
| 1332 removed: [1], |
| 1333 addedCount: 2 |
| 1334 }]); |
| 1335 |
| 1336 model.splice(0, 1); // [2, 3] |
| 1337 assertArrayChanges([{ |
| 1338 index: 0, |
| 1339 removed: [0], |
| 1340 addedCount: 0 |
| 1341 }]); |
| 1342 |
| 1343 model.splice(); |
| 1344 assertNoChanges(); |
| 1345 |
| 1346 model.splice(0, 0); |
| 1347 assertNoChanges(); |
| 1348 |
| 1349 model.splice(0, -1); |
| 1350 assertNoChanges(); |
| 1351 |
| 1352 model.splice(-1, 0, 1.5); // [2, 1.5, 3] |
| 1353 assertArrayChanges([{ |
| 1354 index: 1, |
| 1355 removed: [], |
| 1356 addedCount: 1 |
| 1357 }]); |
| 1358 |
| 1359 model.splice(3, 0, 0); // [2, 1.5, 3, 0] |
| 1360 assertArrayChanges([{ |
| 1361 index: 3, |
| 1362 removed: [], |
| 1363 addedCount: 1 |
| 1364 }]); |
| 1365 |
| 1366 model.splice(0); // [] |
| 1367 assertArrayChanges([{ |
| 1368 index: 0, |
| 1369 removed: [2, 1.5, 3, 0], |
| 1370 addedCount: 0 |
| 1371 }]); |
| 1372 |
| 1373 observer.close(); |
| 1374 }); |
| 1375 |
| 1376 it('Array Splice Truncate And Expand With Length', function() { |
| 1377 var model = ['a', 'b', 'c', 'd', 'e']; |
| 1378 |
| 1379 observer = new ArrayObserver(model); |
| 1380 observer.open(callback); |
| 1381 |
| 1382 model.length = 2; |
| 1383 |
| 1384 assertArrayChanges([{ |
| 1385 index: 2, |
| 1386 removed: ['c', 'd', 'e'], |
| 1387 addedCount: 0 |
| 1388 }]); |
| 1389 |
| 1390 model.length = 5; |
| 1391 |
| 1392 assertArrayChanges([{ |
| 1393 index: 2, |
| 1394 removed: [], |
| 1395 addedCount: 3 |
| 1396 }]); |
| 1397 |
| 1398 observer.close(); |
| 1399 }); |
| 1400 |
| 1401 it('Array Splice Delete Too Many', function() { |
| 1402 var model = ['a', 'b', 'c']; |
| 1403 |
| 1404 observer = new ArrayObserver(model); |
| 1405 observer.open(callback); |
| 1406 |
| 1407 model.splice(2, 3); // ['a', 'b'] |
| 1408 assertArrayChanges([{ |
| 1409 index: 2, |
| 1410 removed: ['c'], |
| 1411 addedCount: 0 |
| 1412 }]); |
| 1413 |
| 1414 observer.close(); |
| 1415 }); |
| 1416 |
| 1417 it('Array Length', function() { |
| 1418 var model = [0, 1]; |
| 1419 |
| 1420 observer = new ArrayObserver(model); |
| 1421 observer.open(callback); |
| 1422 |
| 1423 model.length = 5; // [0, 1, , , ,]; |
| 1424 assertArrayChanges([{ |
| 1425 index: 2, |
| 1426 removed: [], |
| 1427 addedCount: 3 |
| 1428 }]); |
| 1429 |
| 1430 model.length = 1; |
| 1431 assertArrayChanges([{ |
| 1432 index: 1, |
| 1433 removed: [1, , , ,], |
| 1434 addedCount: 0 |
| 1435 }]); |
| 1436 |
| 1437 model.length = 1; |
| 1438 assertNoChanges(); |
| 1439 |
| 1440 observer.close(); |
| 1441 }); |
| 1442 |
| 1443 it('Array Push', function() { |
| 1444 var model = [0, 1]; |
| 1445 |
| 1446 observer = new ArrayObserver(model); |
| 1447 observer.open(callback); |
| 1448 |
| 1449 model.push(2, 3); // [0, 1, 2, 3] |
| 1450 assertArrayChanges([{ |
| 1451 index: 2, |
| 1452 removed: [], |
| 1453 addedCount: 2 |
| 1454 }]); |
| 1455 |
| 1456 model.push(); |
| 1457 assertNoChanges(); |
| 1458 |
| 1459 observer.close(); |
| 1460 }); |
| 1461 |
| 1462 it('Array Pop', function() { |
| 1463 var model = [0, 1]; |
| 1464 |
| 1465 observer = new ArrayObserver(model); |
| 1466 observer.open(callback); |
| 1467 |
| 1468 model.pop(); // [0] |
| 1469 assertArrayChanges([{ |
| 1470 index: 1, |
| 1471 removed: [1], |
| 1472 addedCount: 0 |
| 1473 }]); |
| 1474 |
| 1475 model.pop(); // [] |
| 1476 assertArrayChanges([{ |
| 1477 index: 0, |
| 1478 removed: [0], |
| 1479 addedCount: 0 |
| 1480 }]); |
| 1481 |
| 1482 model.pop(); |
| 1483 assertNoChanges(); |
| 1484 |
| 1485 observer.close(); |
| 1486 }); |
| 1487 |
| 1488 it('Array Shift', function() { |
| 1489 var model = [0, 1]; |
| 1490 |
| 1491 observer = new ArrayObserver(model); |
| 1492 observer.open(callback); |
| 1493 |
| 1494 model.shift(); // [1] |
| 1495 assertArrayChanges([{ |
| 1496 index: 0, |
| 1497 removed: [0], |
| 1498 addedCount: 0 |
| 1499 }]); |
| 1500 |
| 1501 model.shift(); // [] |
| 1502 assertArrayChanges([{ |
| 1503 index: 0, |
| 1504 removed: [1], |
| 1505 addedCount: 0 |
| 1506 }]); |
| 1507 |
| 1508 model.shift(); |
| 1509 assertNoChanges(); |
| 1510 |
| 1511 observer.close(); |
| 1512 }); |
| 1513 |
| 1514 it('Array Unshift', function() { |
| 1515 var model = [0, 1]; |
| 1516 |
| 1517 observer = new ArrayObserver(model); |
| 1518 observer.open(callback); |
| 1519 |
| 1520 model.unshift(-1); // [-1, 0, 1] |
| 1521 assertArrayChanges([{ |
| 1522 index: 0, |
| 1523 removed: [], |
| 1524 addedCount: 1 |
| 1525 }]); |
| 1526 |
| 1527 model.unshift(-3, -2); // [] |
| 1528 assertArrayChanges([{ |
| 1529 index: 0, |
| 1530 removed: [], |
| 1531 addedCount: 2 |
| 1532 }]); |
| 1533 |
| 1534 model.unshift(); |
| 1535 assertNoChanges(); |
| 1536 |
| 1537 observer.close(); |
| 1538 }); |
| 1539 |
| 1540 it('Array Tracker Contained', function() { |
| 1541 arrayMutationTest( |
| 1542 ['a', 'b'], |
| 1543 [ |
| 1544 { name: 'splice', args: [1, 1] }, |
| 1545 { name: 'unshift', args: ['c', 'd', 'e'] }, |
| 1546 { name: 'splice', args: [1, 2, 'f'] } |
| 1547 ] |
| 1548 ); |
| 1549 }); |
| 1550 |
| 1551 it('Array Tracker Delete Empty', function() { |
| 1552 arrayMutationTest( |
| 1553 [], |
| 1554 [ |
| 1555 { name: 'delete', index: 0 }, |
| 1556 { name: 'splice', args: [0, 0, 'a', 'b', 'c'] } |
| 1557 ] |
| 1558 ); |
| 1559 }); |
| 1560 |
| 1561 it('Array Tracker Right Non Overlap', function() { |
| 1562 arrayMutationTest( |
| 1563 ['a', 'b', 'c', 'd'], |
| 1564 [ |
| 1565 { name: 'splice', args: [0, 1, 'e'] }, |
| 1566 { name: 'splice', args: [2, 1, 'f', 'g'] } |
| 1567 ] |
| 1568 ); |
| 1569 }); |
| 1570 |
| 1571 it('Array Tracker Left Non Overlap', function() { |
| 1572 arrayMutationTest( |
| 1573 ['a', 'b', 'c', 'd'], |
| 1574 [ |
| 1575 { name: 'splice', args: [3, 1, 'f', 'g'] }, |
| 1576 { name: 'splice', args: [0, 1, 'e'] } |
| 1577 ] |
| 1578 ); |
| 1579 }); |
| 1580 |
| 1581 it('Array Tracker Right Adjacent', function() { |
| 1582 arrayMutationTest( |
| 1583 ['a', 'b', 'c', 'd'], |
| 1584 [ |
| 1585 { name: 'splice', args: [1, 1, 'e'] }, |
| 1586 { name: 'splice', args: [2, 1, 'f', 'g'] } |
| 1587 ] |
| 1588 ); |
| 1589 }); |
| 1590 |
| 1591 it('Array Tracker Left Adjacent', function() { |
| 1592 arrayMutationTest( |
| 1593 ['a', 'b', 'c', 'd'], |
| 1594 [ |
| 1595 { name: 'splice', args: [2, 2, 'e'] }, |
| 1596 { name: 'splice', args: [1, 1, 'f', 'g'] } |
| 1597 ] |
| 1598 ); |
| 1599 }); |
| 1600 |
| 1601 it('Array Tracker Right Overlap', function() { |
| 1602 arrayMutationTest( |
| 1603 ['a', 'b', 'c', 'd'], |
| 1604 [ |
| 1605 { name: 'splice', args: [1, 1, 'e'] }, |
| 1606 { name: 'splice', args: [1, 1, 'f', 'g'] } |
| 1607 ] |
| 1608 ); |
| 1609 }); |
| 1610 |
| 1611 it('Array Tracker Left Overlap', function() { |
| 1612 arrayMutationTest( |
| 1613 ['a', 'b', 'c', 'd'], |
| 1614 [ |
| 1615 // a b [e f g] d |
| 1616 { name: 'splice', args: [2, 1, 'e', 'f', 'g'] }, |
| 1617 // a [h i j] f g d |
| 1618 { name: 'splice', args: [1, 2, 'h', 'i', 'j'] } |
| 1619 ] |
| 1620 ); |
| 1621 }); |
| 1622 |
| 1623 it('Array Tracker Prefix And Suffix One In', function() { |
| 1624 arrayMutationTest( |
| 1625 ['a', 'b', 'c', 'd'], |
| 1626 [ |
| 1627 { name: 'unshift', args: ['z'] }, |
| 1628 { name: 'push', arg: ['z'] } |
| 1629 ] |
| 1630 ); |
| 1631 }); |
| 1632 |
| 1633 it('Array Tracker Shift One', function() { |
| 1634 arrayMutationTest( |
| 1635 [16, 15, 15], |
| 1636 [ |
| 1637 { name: 'shift', args: ['z'] } |
| 1638 ] |
| 1639 ); |
| 1640 }); |
| 1641 |
| 1642 it('Array Tracker Update Delete', function() { |
| 1643 arrayMutationTest( |
| 1644 ['a', 'b', 'c', 'd'], |
| 1645 [ |
| 1646 { name: 'splice', args: [2, 1, 'e', 'f', 'g'] }, |
| 1647 { name: 'update', index: 0, value: 'h' }, |
| 1648 { name: 'delete', index: 1 } |
| 1649 ] |
| 1650 ); |
| 1651 }); |
| 1652 |
| 1653 it('Array Tracker Update After Delete', function() { |
| 1654 arrayMutationTest( |
| 1655 ['a', 'b', undefined, 'd'], |
| 1656 [ |
| 1657 { name: 'update', index: 2, value: 'e' } |
| 1658 ] |
| 1659 ); |
| 1660 }); |
| 1661 |
| 1662 it('Array Tracker Delete Mid Array', function() { |
| 1663 arrayMutationTest( |
| 1664 ['a', 'b', 'c', 'd'], |
| 1665 [ |
| 1666 { name: 'delete', index: 2 } |
| 1667 ] |
| 1668 ); |
| 1669 }); |
| 1670 |
| 1671 it('Array Random Case 1', function() { |
| 1672 var model = ['a','b']; |
| 1673 var copy = model.slice(); |
| 1674 |
| 1675 observer = new ArrayObserver(model); |
| 1676 observer.open(callback); |
| 1677 |
| 1678 model.splice(0, 1, 'c', 'd', 'e'); |
| 1679 model.splice(4,0,'f'); |
| 1680 model.splice(3,2); |
| 1681 |
| 1682 applySplicesAndAssertDeepEqual(model, copy); |
| 1683 }); |
| 1684 |
| 1685 it('Array Random Case 2', function() { |
| 1686 var model = [3,4]; |
| 1687 var copy = model.slice(); |
| 1688 |
| 1689 observer = new ArrayObserver(model); |
| 1690 observer.open(callback); |
| 1691 |
| 1692 model.splice(2,0,8); |
| 1693 model.splice(0,1,0,5); |
| 1694 model.splice(2,2); |
| 1695 |
| 1696 applySplicesAndAssertDeepEqual(model, copy); |
| 1697 }); |
| 1698 |
| 1699 it('Array Random Case 3', function() { |
| 1700 var model = [1,3,6]; |
| 1701 var copy = model.slice(); |
| 1702 |
| 1703 observer = new ArrayObserver(model); |
| 1704 observer.open(callback); |
| 1705 |
| 1706 model.splice(1,1); |
| 1707 model.splice(0,2,1,7); |
| 1708 model.splice(1,0,3,7); |
| 1709 |
| 1710 applySplicesAndAssertDeepEqual(model, copy); |
| 1711 }); |
| 1712 |
| 1713 function ArrayFuzzer() {} |
| 1714 |
| 1715 ArrayFuzzer.valMax = 16; |
| 1716 ArrayFuzzer.arrayLengthMax = 128; |
| 1717 ArrayFuzzer.operationCount = 64; |
| 1718 |
| 1719 function randDouble(start, end) { |
| 1720 return Math.random()*(end-start) + start; |
| 1721 } |
| 1722 |
| 1723 function randInt(start, end) { |
| 1724 return Math.round(randDouble(start, end)); |
| 1725 } |
| 1726 |
| 1727 function randASCIIChar() { |
| 1728 return String.fromCharCode(randInt(32, 126)); |
| 1729 } |
| 1730 |
| 1731 function randValue() { |
| 1732 switch(randInt(0, 5)) { |
| 1733 case 0: |
| 1734 return {}; |
| 1735 case 1: |
| 1736 return undefined; |
| 1737 case 2: |
| 1738 return null; |
| 1739 case 3: |
| 1740 return randInt(0, ArrayFuzzer.valMax); |
| 1741 case 4: |
| 1742 return randDouble(0, ArrayFuzzer.valMax); |
| 1743 case 5: |
| 1744 return randASCIIChar(); |
| 1745 } |
| 1746 } |
| 1747 |
| 1748 function randArray() { |
| 1749 var args = []; |
| 1750 var count = randInt(0, ArrayFuzzer.arrayLengthMax); |
| 1751 while(count-- > 0) |
| 1752 args.push(randValue()); |
| 1753 |
| 1754 return args; |
| 1755 } |
| 1756 |
| 1757 function randomArrayOperation(arr) { |
| 1758 function empty() { |
| 1759 return []; |
| 1760 } |
| 1761 |
| 1762 var operations = { |
| 1763 push: randArray, |
| 1764 unshift: randArray, |
| 1765 pop: empty, |
| 1766 shift: empty, |
| 1767 splice: function() { |
| 1768 var args = []; |
| 1769 args.push(randInt(-arr.length*2, arr.length*2), randInt(0, arr.length*2)
); |
| 1770 args = args.concat(randArray()); |
| 1771 return args; |
| 1772 } |
| 1773 }; |
| 1774 |
| 1775 // Do a splice once for each of the other operations. |
| 1776 var operationList = ['splice', 'update', |
| 1777 'splice', 'delete', |
| 1778 'splice', 'push', |
| 1779 'splice', 'pop', |
| 1780 'splice', 'shift', |
| 1781 'splice', 'unshift']; |
| 1782 |
| 1783 var op = { |
| 1784 name: operationList[randInt(0, operationList.length - 1)] |
| 1785 }; |
| 1786 |
| 1787 switch(op.name) { |
| 1788 case 'delete': |
| 1789 op.index = randInt(0, arr.length - 1); |
| 1790 delete arr[op.index]; |
| 1791 break; |
| 1792 |
| 1793 case 'update': |
| 1794 op.index = randInt(0, arr.length); |
| 1795 op.value = randValue(); |
| 1796 arr[op.index] = op.value; |
| 1797 break; |
| 1798 |
| 1799 default: |
| 1800 op.args = operations[op.name](); |
| 1801 arr[op.name].apply(arr, op.args); |
| 1802 break; |
| 1803 } |
| 1804 |
| 1805 return op; |
| 1806 } |
| 1807 |
| 1808 function randomArrayOperations(arr, count) { |
| 1809 var ops = [] |
| 1810 for (var i = 0; i < count; i++) { |
| 1811 ops.push(randomArrayOperation(arr)); |
| 1812 } |
| 1813 |
| 1814 return ops; |
| 1815 } |
| 1816 |
| 1817 ArrayFuzzer.prototype.go = function() { |
| 1818 var orig = this.arr = randArray(); |
| 1819 randomArrayOperations(this.arr, ArrayFuzzer.operationCount); |
| 1820 var copy = this.copy = this.arr.slice(); |
| 1821 this.origCopy = this.copy.slice(); |
| 1822 |
| 1823 var observer = new ArrayObserver(this.arr); |
| 1824 observer.open(function(splices) { |
| 1825 ArrayObserver.applySplices(copy, orig, splices); |
| 1826 }); |
| 1827 |
| 1828 this.ops = randomArrayOperations(this.arr, ArrayFuzzer.operationCount); |
| 1829 observer.deliver(); |
| 1830 observer.close(); |
| 1831 } |
| 1832 |
| 1833 |
| 1834 it('Array Tracker Fuzzer', function() { |
| 1835 var testCount = 64; |
| 1836 |
| 1837 for (var i = 0; i < testCount; i++) { |
| 1838 var fuzzer = new ArrayFuzzer(); |
| 1839 fuzzer.go(); |
| 1840 ensureNonSparse(fuzzer.arr); |
| 1841 ensureNonSparse(fuzzer.copy); |
| 1842 assert.deepEqual(fuzzer.arr, fuzzer.copy); |
| 1843 } |
| 1844 }); |
| 1845 |
| 1846 it('Array Tracker No Proxies Edits', function() { |
| 1847 var model = []; |
| 1848 observer = new ArrayObserver(model); |
| 1849 observer.open(callback); |
| 1850 model.length = 0; |
| 1851 model.push(1, 2, 3); |
| 1852 assertEditDistance(model, 3); |
| 1853 observer.close(); |
| 1854 |
| 1855 model = ['x', 'x', 'x', 'x', '1', '2', '3']; |
| 1856 observer = new ArrayObserver(model); |
| 1857 observer.open(callback); |
| 1858 model.length = 0; |
| 1859 model.push('1', '2', '3', 'y', 'y', 'y', 'y'); |
| 1860 assertEditDistance(model, 8); |
| 1861 observer.close(); |
| 1862 |
| 1863 model = ['1', '2', '3', '4', '5']; |
| 1864 observer = new ArrayObserver(model); |
| 1865 observer.open(callback); |
| 1866 model.length = 0; |
| 1867 model.push('a', '2', 'y', 'y', '4', '5', 'z', 'z'); |
| 1868 assertEditDistance(model, 7); |
| 1869 observer.close(); |
| 1870 }); |
| 1871 }); |
| 1872 </script> |
| OLD | NEW |