| <html> |
| <import src="../resources/chai.sky" /> |
| <import src="../resources/mocha.sky" /> |
| <import src="/sky/framework/elements/sky-element/observe.sky" as="observe" /> |
| |
| <script> |
| |
| var Path = observe.Path; |
| var PathObserver = observe.PathObserver; |
| var ArrayObserver = observe.ArrayObserver; |
| var ObjectObserver = observe.ObjectObserver; |
| var CompoundObserver = observe.CompoundObserver; |
| var ObserverTransform = observe.ObserverTransform; |
| |
| var observer; |
| var callbackArgs = undefined; |
| var callbackInvoked = false; |
| |
| function then(fn) { |
| setTimeout(function() { |
| fn(); |
| }, 0); |
| |
| return { |
| then: function(next) { |
| return then(next); |
| } |
| }; |
| } |
| |
| function noop() {} |
| |
| function callback() { |
| callbackArgs = Array.prototype.slice.apply(arguments); |
| callbackInvoked = true; |
| } |
| |
| function doSetup() {} |
| function doTeardown() { |
| callbackInvoked = false; |
| callbackArgs = undefined; |
| } |
| |
| function assertNoChanges() { |
| if (observer) |
| observer.deliver(); |
| assert.isFalse(callbackInvoked); |
| assert.isUndefined(callbackArgs); |
| } |
| |
| function assertPathChanges(expectNewValue, expectOldValue, dontDeliver) { |
| if (!dontDeliver) |
| observer.deliver(); |
| |
| assert.isTrue(callbackInvoked); |
| |
| var newValue = callbackArgs[0]; |
| var oldValue = callbackArgs[1]; |
| assert.deepEqual(expectNewValue, newValue); |
| assert.deepEqual(expectOldValue, oldValue); |
| |
| if (!dontDeliver) { |
| assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| window.dirtyCheckCycleCount === 1); |
| } |
| |
| callbackArgs = undefined; |
| callbackInvoked = false; |
| } |
| |
| function assertCompoundPathChanges(expectNewValues, expectOldValues, |
| expectObserved, dontDeliver) { |
| if (!dontDeliver) |
| observer.deliver(); |
| |
| assert.isTrue(callbackInvoked); |
| |
| var newValues = callbackArgs[0]; |
| var oldValues = callbackArgs[1]; |
| var observed = callbackArgs[2]; |
| assert.deepEqual(expectNewValues, newValues); |
| assert.deepEqual(expectOldValues, oldValues); |
| assert.deepEqual(expectObserved, observed); |
| |
| if (!dontDeliver) { |
| assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| window.dirtyCheckCycleCount === 1); |
| } |
| |
| callbackArgs = undefined; |
| callbackInvoked = false; |
| } |
| |
| var createObject = ('__proto__' in {}) ? |
| function(obj) { return obj; } : |
| function(obj) { |
| var proto = obj.__proto__; |
| if (!proto) |
| return obj; |
| var newObject = Object.create(proto); |
| Object.getOwnPropertyNames(obj).forEach(function(name) { |
| Object.defineProperty(newObject, name, |
| Object.getOwnPropertyDescriptor(obj, name)); |
| }); |
| return newObject; |
| }; |
| |
| function assertPath(pathString, expectKeys, expectSerialized) { |
| var path = Path.get(pathString); |
| if (!expectKeys) { |
| assert.isFalse(path.valid); |
| return; |
| } |
| |
| assert.deepEqual(Array.prototype.slice.apply(path), expectKeys); |
| assert.strictEqual(path.toString(), expectSerialized); |
| } |
| |
| function assertInvalidPath(pathString) { |
| assertPath(pathString); |
| } |
| |
| describe('Path', function() { |
| it('constructor throws', function() { |
| assert.throws(function() { |
| new Path('foo') |
| }); |
| }); |
| |
| it('path validity', function() { |
| // invalid path get value is always undefined |
| var p = Path.get('a b'); |
| assert.isFalse(p.valid); |
| assert.isUndefined(p.getValueFrom({ a: { b: 2 }})); |
| |
| assertPath('', [], ''); |
| assertPath(' ', [], ''); |
| assertPath(null, [], ''); |
| assertPath(undefined, [], ''); |
| assertPath('a', ['a'], 'a'); |
| assertPath('a.b', ['a', 'b'], 'a.b'); |
| assertPath('a. b', ['a', 'b'], 'a.b'); |
| assertPath('a .b', ['a', 'b'], 'a.b'); |
| assertPath('a . b', ['a', 'b'], 'a.b'); |
| assertPath(' a . b ', ['a', 'b'], 'a.b'); |
| assertPath('a[0]', ['a', '0'], 'a[0]'); |
| assertPath('a [0]', ['a', '0'], 'a[0]'); |
| assertPath('a[0][1]', ['a', '0', '1'], 'a[0][1]'); |
| assertPath('a [ 0 ] [ 1 ] ', ['a', '0', '1'], 'a[0][1]'); |
| assertPath('[1234567890] ', ['1234567890'], '[1234567890]'); |
| assertPath(' [1234567890] ', ['1234567890'], '[1234567890]'); |
| assertPath('opt0', ['opt0'], 'opt0'); |
| assertPath('$foo.$bar._baz', ['$foo', '$bar', '_baz'], '$foo.$bar._baz'); |
| assertPath('foo["baz"]', ['foo', 'baz'], 'foo.baz'); |
| assertPath('foo["b\\"az"]', ['foo', 'b"az'], 'foo["b\\"az"]'); |
| assertPath("foo['b\\'az']", ['foo', "b'az"], 'foo["b\'az"]'); |
| assertPath(['a', 'b'], ['a', 'b'], 'a.b'); |
| assertPath([''], [''], '[""]'); |
| |
| function Foo(val) { this.val = val; } |
| Foo.prototype.toString = function() { return 'Foo' + this.val; }; |
| assertPath([new Foo('a'), new Foo('b')], ['Fooa', 'Foob'], 'Fooa.Foob'); |
| |
| assertInvalidPath('.'); |
| assertInvalidPath(' . '); |
| assertInvalidPath('..'); |
| assertInvalidPath('a[4'); |
| assertInvalidPath('a.b.'); |
| assertInvalidPath('a,b'); |
| assertInvalidPath('a["foo]'); |
| assertInvalidPath('[0x04]'); |
| assertInvalidPath('[0foo]'); |
| assertInvalidPath('[foo-bar]'); |
| assertInvalidPath('foo-bar'); |
| assertInvalidPath('42'); |
| assertInvalidPath('a[04]'); |
| assertInvalidPath(' a [ 04 ]'); |
| assertInvalidPath(' 42 '); |
| assertInvalidPath('foo["bar]'); |
| assertInvalidPath("foo['bar]"); |
| }); |
| |
| it('Paths are interned', function() { |
| var p = Path.get('foo.bar'); |
| var p2 = Path.get('foo.bar'); |
| assert.strictEqual(p, p2); |
| |
| var p3 = Path.get(''); |
| var p4 = Path.get(''); |
| assert.strictEqual(p3, p4); |
| }); |
| |
| it('null is empty path', function() { |
| assert.strictEqual(Path.get(''), Path.get(null)); |
| }); |
| |
| it('undefined is empty path', function() { |
| assert.strictEqual(Path.get(undefined), Path.get(null)); |
| }); |
| |
| it('Path.getValueFrom', function() { |
| var obj = { |
| a: { |
| b: { |
| c: 1 |
| } |
| } |
| }; |
| |
| var p1 = Path.get('a'); |
| var p2 = Path.get('a.b'); |
| var p3 = Path.get('a.b.c'); |
| |
| assert.strictEqual(obj.a, p1.getValueFrom(obj)); |
| assert.strictEqual(obj.a.b, p2.getValueFrom(obj)); |
| assert.strictEqual(1, p3.getValueFrom(obj)); |
| |
| obj.a.b.c = 2; |
| assert.strictEqual(2, p3.getValueFrom(obj)); |
| |
| obj.a.b = { |
| c: 3 |
| }; |
| assert.strictEqual(3, p3.getValueFrom(obj)); |
| |
| obj.a = { |
| b: 4 |
| }; |
| assert.strictEqual(undefined, p3.getValueFrom(obj)); |
| assert.strictEqual(4, p2.getValueFrom(obj)); |
| }); |
| |
| it('Path.setValueFrom', function() { |
| var obj = {}; |
| var p2 = Path.get('bar'); |
| |
| Path.get('foo').setValueFrom(obj, 3); |
| assert.equal(3, obj.foo); |
| |
| var bar = { baz: 3 }; |
| |
| Path.get('bar').setValueFrom(obj, bar); |
| assert.equal(bar, obj.bar); |
| |
| var p = Path.get('bar.baz.bat'); |
| p.setValueFrom(obj, 'not here'); |
| assert.equal(undefined, p.getValueFrom(obj)); |
| }); |
| |
| it('Degenerate Values', function() { |
| var emptyPath = Path.get(); |
| var foo = {}; |
| |
| assert.equal(null, emptyPath.getValueFrom(null)); |
| assert.equal(foo, emptyPath.getValueFrom(foo)); |
| assert.equal(3, emptyPath.getValueFrom(3)); |
| assert.equal(undefined, Path.get('a').getValueFrom(undefined)); |
| }); |
| }); |
| |
| describe('Basic Tests', function() { |
| |
| it('Exception Doesnt Stop Notification', function() { |
| var model = [1]; |
| var count = 0; |
| |
| var observer2 = new PathObserver(model, '[0]'); |
| observer2.open(function() { |
| count++; |
| throw 'ouch'; |
| }); |
| |
| var observer3 = new ArrayObserver(model); |
| observer3.open(function() { |
| count++; |
| throw 'ouch'; |
| }); |
| |
| model[0] = 2; |
| model[1] = 2; |
| |
| observer2.deliver(); |
| observer3.deliver(); |
| |
| assert.equal(2, count); |
| |
| observer2.close(); |
| observer3.close(); |
| }); |
| |
| it('Can only open once', function() { |
| observer = new PathObserver({ id: 1 }, 'id'); |
| observer.open(callback); |
| assert.throws(function() { |
| observer.open(callback); |
| }); |
| observer.close(); |
| |
| observer = new CompoundObserver(); |
| observer.open(callback); |
| assert.throws(function() { |
| observer.open(callback); |
| }); |
| observer.close(); |
| |
| observer = new ArrayObserver([], 'id'); |
| observer.open(callback); |
| assert.throws(function() { |
| observer.open(callback); |
| }); |
| observer.close(); |
| |
| }); |
| |
| }); |
| |
| describe('ObserverTransform', function() { |
| |
| it('Close Invokes Close', function() { |
| var count = 0; |
| var observer = { |
| open: function() {}, |
| close: function() { count++; } |
| }; |
| |
| var observer = new ObserverTransform(observer); |
| observer.open(); |
| observer.close(); |
| assert.strictEqual(1, count); |
| }); |
| |
| it('valueFn/setValueFn', function() { |
| var obj = { foo: 1 }; |
| |
| function valueFn(value) { return value * 2; } |
| |
| observer = new ObserverTransform(new PathObserver(obj, 'foo'), |
| valueFn); |
| observer.open(callback); |
| |
| obj.foo = 2; |
| |
| assert.strictEqual(4, observer.discardChanges()); |
| assertNoChanges(); |
| |
| observer.setValue(2); |
| assert.strictEqual(obj.foo, 2); |
| |
| obj.foo = 10; |
| assertPathChanges(20, 4); |
| |
| observer.close(); |
| }); |
| |
| it('valueFn - object literal', function() { |
| var model = {}; |
| |
| function valueFn(value) { |
| return [ value ]; |
| } |
| |
| observer = new ObserverTransform(new PathObserver(model, 'foo'), valueFn); |
| observer.open(callback); |
| |
| model.foo = 1; |
| assertPathChanges([1], [undefined]); |
| |
| model.foo = 3; |
| assertPathChanges([3], [1]); |
| |
| observer.close(); |
| }); |
| |
| it('CompoundObserver - valueFn reduction', function() { |
| var model = { a: 1, b: 2, c: 3 }; |
| |
| function valueFn(values) { |
| return values.reduce(function(last, cur) { |
| return typeof cur === 'number' ? last + cur : undefined; |
| }, 0); |
| } |
| |
| var compound = new CompoundObserver(); |
| compound.addPath(model, 'a'); |
| compound.addPath(model, 'b'); |
| compound.addPath(model, Path.get('c')); |
| |
| observer = new ObserverTransform(compound, valueFn); |
| assert.strictEqual(6, observer.open(callback)); |
| |
| model.a = -10; |
| model.b = 20; |
| model.c = 30; |
| assertPathChanges(40, 6); |
| |
| observer.close(); |
| }); |
| }) |
| |
| describe('PathObserver Tests', function() { |
| |
| beforeEach(doSetup); |
| |
| afterEach(doTeardown); |
| |
| it('Callback args', function() { |
| var obj = { |
| foo: 'bar' |
| }; |
| |
| var path = Path.get('foo'); |
| var observer = new PathObserver(obj, path); |
| |
| var args; |
| observer.open(function() { |
| args = Array.prototype.slice.apply(arguments); |
| }); |
| |
| obj.foo = 'baz'; |
| observer.deliver(); |
| assert.strictEqual(args.length, 3); |
| assert.strictEqual(args[0], 'baz'); |
| assert.strictEqual(args[1], 'bar'); |
| assert.strictEqual(args[2], observer); |
| assert.strictEqual(args[2].path, path); |
| observer.close(); |
| }); |
| |
| it('PathObserver.path', function() { |
| var obj = { |
| foo: 'bar' |
| }; |
| |
| var path = Path.get('foo'); |
| var observer = new PathObserver(obj, 'foo'); |
| assert.strictEqual(observer.path, Path.get('foo')); |
| }); |
| |
| |
| it('invalid', function() { |
| var observer = new PathObserver({ a: { b: 1 }}Â , 'a b'); |
| observer.open(callback); |
| assert.strictEqual(undefined, observer.value); |
| observer.deliver(); |
| assert.isFalse(callbackInvoked); |
| }); |
| |
| it('Optional target for callback', function() { |
| var target = { |
| changed: function(value, oldValue) { |
| this.called = true; |
| } |
| }; |
| var obj = { foo: 1 }; |
| var observer = new PathObserver(obj, 'foo'); |
| observer.open(target.changed, target); |
| obj.foo = 2; |
| observer.deliver(); |
| assert.isTrue(target.called); |
| |
| observer.close(); |
| }); |
| |
| it('Delivery Until No Changes', function() { |
| var obj = { foo: { bar: 5 }}; |
| var callbackCount = 0; |
| var observer = new PathObserver(obj, 'foo . bar'); |
| observer.open(function() { |
| callbackCount++; |
| if (!obj.foo.bar) |
| return; |
| |
| obj.foo.bar--; |
| }); |
| |
| obj.foo.bar--; |
| observer.deliver(); |
| |
| assert.equal(5, callbackCount); |
| |
| observer.close(); |
| }); |
| |
| it('Path disconnect', function() { |
| var arr = {}; |
| |
| arr.foo = 'bar'; |
| observer = new PathObserver(arr, 'foo'); |
| observer.open(callback); |
| arr.foo = 'baz'; |
| |
| assertPathChanges('baz', 'bar'); |
| arr.foo = 'bar'; |
| |
| observer.close(); |
| |
| arr.foo = 'boo'; |
| assertNoChanges(); |
| }); |
| |
| it('Path discardChanges', function() { |
| var arr = {}; |
| |
| arr.foo = 'bar'; |
| observer = new PathObserver(arr, 'foo'); |
| observer.open(callback); |
| arr.foo = 'baz'; |
| |
| assertPathChanges('baz', 'bar'); |
| |
| arr.foo = 'bat'; |
| observer.discardChanges(); |
| assertNoChanges(); |
| |
| arr.foo = 'bag'; |
| assertPathChanges('bag', 'bat'); |
| observer.close(); |
| }); |
| |
| it('Path setValue', function() { |
| var obj = {}; |
| |
| obj.foo = 'bar'; |
| observer = new PathObserver(obj, 'foo'); |
| observer.open(callback); |
| obj.foo = 'baz'; |
| |
| observer.setValue('bat'); |
| assert.strictEqual(obj.foo, 'bat'); |
| assertPathChanges('bat', 'bar'); |
| |
| observer.setValue('bot'); |
| observer.discardChanges(); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Degenerate Values', function() { |
| var emptyPath = Path.get(); |
| observer = new PathObserver(null, ''); |
| observer.open(callback); |
| assert.equal(null, observer.value); |
| observer.close(); |
| |
| var foo = {}; |
| observer = new PathObserver(foo, ''); |
| assert.equal(foo, observer.open(callback)); |
| observer.close(); |
| |
| observer = new PathObserver(3, ''); |
| assert.equal(3, observer.open(callback)); |
| observer.close(); |
| |
| observer = new PathObserver(undefined, 'a'); |
| assert.equal(undefined, observer.open(callback)); |
| observer.close(); |
| |
| var bar = { id: 23 }; |
| observer = new PathObserver(undefined, 'a/3!'); |
| assert.equal(undefined, observer.open(callback)); |
| observer.close(); |
| }); |
| |
| it('Path NaN', function() { |
| var foo = { val: 1 }; |
| observer = new PathObserver(foo, 'val'); |
| observer.open(callback); |
| foo.val = 0/0; |
| |
| // Can't use assertSummary because deepEqual() will fail with NaN |
| observer.deliver(); |
| assert.isTrue(callbackInvoked); |
| assert.isTrue(isNaN(callbackArgs[0])); |
| assert.strictEqual(1, callbackArgs[1]); |
| observer.close(); |
| }); |
| |
| it('Path Set Value Back To Same', function() { |
| var obj = {}; |
| var path = Path.get('foo'); |
| |
| path.setValueFrom(obj, 3); |
| assert.equal(3, obj.foo); |
| |
| observer = new PathObserver(obj, 'foo'); |
| assert.equal(3, observer.open(callback)); |
| |
| path.setValueFrom(obj, 2); |
| assert.equal(2, observer.discardChanges()); |
| |
| path.setValueFrom(obj, 3); |
| assert.equal(3, observer.discardChanges()); |
| |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Path Triple Equals', function() { |
| var model = { }; |
| |
| observer = new PathObserver(model, 'foo'); |
| observer.open(callback); |
| |
| model.foo = null; |
| assertPathChanges(null, undefined); |
| |
| model.foo = undefined; |
| assertPathChanges(undefined, null); |
| |
| observer.close(); |
| }); |
| |
| it('Path Simple', function() { |
| var model = { }; |
| |
| observer = new PathObserver(model, 'foo'); |
| observer.open(callback); |
| |
| model.foo = 1; |
| assertPathChanges(1, undefined); |
| |
| model.foo = 2; |
| assertPathChanges(2, 1); |
| |
| delete model.foo; |
| assertPathChanges(undefined, 2); |
| |
| observer.close(); |
| }); |
| |
| it('Path Simple - path object', function() { |
| var model = { }; |
| |
| var path = Path.get('foo'); |
| observer = new PathObserver(model, path); |
| observer.open(callback); |
| |
| model.foo = 1; |
| assertPathChanges(1, undefined); |
| |
| model.foo = 2; |
| assertPathChanges(2, 1); |
| |
| delete model.foo; |
| assertPathChanges(undefined, 2); |
| |
| observer.close(); |
| }); |
| |
| it('Path - root is initially null', function(done) { |
| var model = { }; |
| |
| var path = Path.get('foo'); |
| observer = new PathObserver(model, 'foo.bar'); |
| observer.open(callback); |
| |
| model.foo = { }; |
| then(function() { |
| model.foo.bar = 1; |
| |
| }).then(function() { |
| assertPathChanges(1, undefined, true); |
| |
| observer.close(); |
| done(); |
| }); |
| }); |
| |
| it('Path With Indices', function() { |
| var model = []; |
| |
| observer = new PathObserver(model, '[0]'); |
| observer.open(callback); |
| |
| model.push(1); |
| assertPathChanges(1, undefined); |
| |
| observer.close(); |
| }); |
| |
| it('Path Observation', function() { |
| var model = { |
| a: { |
| b: { |
| c: 'hello, world' |
| } |
| } |
| }; |
| |
| observer = new PathObserver(model, 'a.b.c'); |
| observer.open(callback); |
| |
| model.a.b.c = 'hello, mom'; |
| assertPathChanges('hello, mom', 'hello, world'); |
| |
| model.a.b = { |
| c: 'hello, dad' |
| }; |
| assertPathChanges('hello, dad', 'hello, mom'); |
| |
| model.a = { |
| b: { |
| c: 'hello, you' |
| } |
| }; |
| assertPathChanges('hello, you', 'hello, dad'); |
| |
| model.a.b = 1; |
| assertPathChanges(undefined, 'hello, you'); |
| |
| // Stop observing |
| observer.close(); |
| |
| model.a.b = {c: 'hello, back again -- but not observing'}; |
| assertNoChanges(); |
| |
| // Resume observing |
| observer = new PathObserver(model, 'a.b.c'); |
| observer.open(callback); |
| |
| model.a.b.c = 'hello. Back for reals'; |
| assertPathChanges('hello. Back for reals', |
| 'hello, back again -- but not observing'); |
| |
| observer.close(); |
| }); |
| |
| it('Path Set To Same As Prototype', function() { |
| var model = createObject({ |
| __proto__: { |
| id: 1 |
| } |
| }); |
| |
| observer = new PathObserver(model, 'id'); |
| observer.open(callback); |
| model.id = 1; |
| |
| assertNoChanges(); |
| observer.close(); |
| }); |
| |
| it('Path Set Read Only', function() { |
| var model = {}; |
| Object.defineProperty(model, 'x', { |
| configurable: true, |
| writable: false, |
| value: 1 |
| }); |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| |
| assert.throws(function() { |
| model.x = 2; |
| }); |
| |
| assertNoChanges(); |
| observer.close(); |
| }); |
| |
| it('Path Set Shadows', function() { |
| var model = createObject({ |
| __proto__: { |
| x: 1 |
| } |
| }); |
| |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| model.x = 2; |
| assertPathChanges(2, 1); |
| observer.close(); |
| }); |
| |
| it('Delete With Same Value On Prototype', function() { |
| var model = createObject({ |
| __proto__: { |
| x: 1, |
| }, |
| x: 1 |
| }); |
| |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| delete model.x; |
| assertNoChanges(); |
| observer.close(); |
| }); |
| |
| it('Delete With Different Value On Prototype', function() { |
| var model = createObject({ |
| __proto__: { |
| x: 1, |
| }, |
| x: 2 |
| }); |
| |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| delete model.x; |
| assertPathChanges(1, 2); |
| observer.close(); |
| }); |
| |
| it('Value Change On Prototype', function() { |
| var proto = { |
| x: 1 |
| } |
| var model = createObject({ |
| __proto__: proto |
| }); |
| |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| model.x = 2; |
| assertPathChanges(2, 1); |
| |
| delete model.x; |
| assertPathChanges(1, 2); |
| |
| proto.x = 3; |
| assertPathChanges(3, 1); |
| observer.close(); |
| }); |
| |
| // FIXME: Need test of observing change on proto. |
| |
| it('Delete Of Non Configurable', function() { |
| var model = {}; |
| Object.defineProperty(model, 'x', { |
| configurable: false, |
| value: 1 |
| }); |
| |
| observer = new PathObserver(model, 'x'); |
| observer.open(callback); |
| |
| assert.throws(function() { |
| delete model.x; |
| }); |
| |
| assertNoChanges(); |
| observer.close(); |
| }); |
| |
| it('Notify', function() { |
| if (typeof Object.getNotifier !== 'function') |
| return; |
| |
| var model = { |
| a: {} |
| } |
| |
| var _b = 2; |
| |
| Object.defineProperty(model.a, 'b', { |
| get: function() { return _b; }, |
| set: function(b) { |
| Object.getNotifier(this).notify({ |
| type: 'update', |
| name: 'b', |
| oldValue: _b |
| }); |
| |
| _b = b; |
| } |
| }); |
| |
| observer = new PathObserver(model, 'a.b'); |
| observer.open(callback); |
| _b = 3; |
| assertPathChanges(3, 2); |
| |
| model.a.b = 4; // will be observed. |
| assertPathChanges(4, 3); |
| |
| observer.close(); |
| }); |
| |
| it('issue-161', function(done) { |
| var model = { model: 'model' }; |
| var ob1 = new PathObserver(model, 'obj.bar'); |
| var called = false |
| ob1.open(function() { |
| called = true; |
| }); |
| |
| var obj2 = new PathObserver(model, 'obj'); |
| obj2.open(function() { |
| model.obj.bar = true; |
| }); |
| |
| model.obj = { 'obj': 'obj' }; |
| model.obj.foo = true; |
| |
| then(function() { |
| assert.strictEqual(called, true); |
| done(); |
| }); |
| }); |
| |
| it('object cycle', function(done) { |
| var model = { a: {}, c: 1 }; |
| model.a.b = model; |
| |
| var called = 0; |
| new PathObserver(model, 'a.b.c').open(function() { |
| called++; |
| }); |
| |
| // This change should be detected, even though it's a change to the root |
| // object and isn't a change to `a`. |
| model.c = 42; |
| |
| then(function() { |
| assert.equal(called, 1); |
| done(); |
| }); |
| }); |
| |
| }); |
| |
| |
| describe('CompoundObserver Tests', function() { |
| |
| beforeEach(doSetup); |
| |
| afterEach(doTeardown); |
| |
| it('Simple', function() { |
| var model = { a: 1, b: 2, c: 3 }; |
| |
| observer = new CompoundObserver(); |
| observer.addPath(model, 'a'); |
| observer.addPath(model, 'b'); |
| observer.addPath(model, Path.get('c')); |
| observer.open(callback); |
| assertNoChanges(); |
| |
| var observerCallbackArg = [model, Path.get('a'), |
| model, Path.get('b'), |
| model, Path.get('c')]; |
| model.a = -10; |
| model.b = 20; |
| model.c = 30; |
| assertCompoundPathChanges([-10, 20, 30], [1, 2, 3], |
| observerCallbackArg); |
| |
| model.a = 'a'; |
| model.c = 'c'; |
| assertCompoundPathChanges(['a', 20, 'c'], [-10,, 30], |
| observerCallbackArg); |
| |
| model.a = 2; |
| model.b = 3; |
| model.c = 4; |
| |
| assertCompoundPathChanges([2, 3, 4], ['a', 20, 'c'], |
| observerCallbackArg); |
| |
| model.a = 'z'; |
| model.b = 'y'; |
| model.c = 'x'; |
| assert.deepEqual(['z', 'y', 'x'], observer.discardChanges()); |
| assertNoChanges(); |
| |
| assert.strictEqual('z', model.a); |
| assert.strictEqual('y', model.b); |
| assert.strictEqual('x', model.c); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('reportChangesOnOpen', function() { |
| var model = { a: 1, b: 2, c: 3 }; |
| |
| observer = new CompoundObserver(true); |
| observer.addPath(model, 'a'); |
| observer.addPath(model, 'b'); |
| observer.addPath(model, Path.get('c')); |
| |
| model.a = -10; |
| model.b = 20; |
| observer.open(callback); |
| var observerCallbackArg = [model, Path.get('a'), |
| model, Path.get('b'), |
| model, Path.get('c')]; |
| assertCompoundPathChanges([-10, 20, 3], [1, 2, ], |
| observerCallbackArg, true); |
| observer.close(); |
| }); |
| |
| it('Degenerate Values', function() { |
| var model = {}; |
| observer = new CompoundObserver(); |
| observer.addPath({}, '.'); // invalid path |
| observer.addPath('obj-value', ''); // empty path |
| observer.addPath({}, 'foo'); // unreachable |
| observer.addPath(3, 'bar'); // non-object with non-empty path |
| var values = observer.open(callback); |
| assert.strictEqual(4, values.length); |
| assert.strictEqual(undefined, values[0]); |
| assert.strictEqual('obj-value', values[1]); |
| assert.strictEqual(undefined, values[2]); |
| assert.strictEqual(undefined, values[3]); |
| observer.close(); |
| }); |
| |
| it('valueFn - return object literal', function() { |
| var model = { a: 1}; |
| |
| function valueFn(values) { |
| return {}; |
| } |
| |
| observer = new CompoundObserver(valueFn); |
| |
| observer.addPath(model, 'a'); |
| observer.open(callback); |
| model.a = 2; |
| |
| observer.deliver(); |
| assert.isTrue(window.dirtyCheckCycleCount === undefined || |
| window.dirtyCheckCycleCount === 1); |
| observer.close(); |
| }); |
| |
| it('reset', function() { |
| var model = { a: 1, b: 2, c: 3 }; |
| var callCount = 0; |
| function callback() { |
| callCount++; |
| } |
| |
| observer = new CompoundObserver(); |
| |
| observer.addPath(model, 'a'); |
| observer.addPath(model, 'b'); |
| assert.deepEqual([1, 2], observer.open(callback)); |
| |
| model.a = 2; |
| observer.deliver(); |
| assert.strictEqual(1, callCount); |
| |
| model.b = 3; |
| observer.deliver(); |
| assert.strictEqual(2, callCount); |
| |
| model.c = 4; |
| observer.deliver(); |
| assert.strictEqual(2, callCount); |
| |
| observer.startReset(); |
| observer.addPath(model, 'b'); |
| observer.addPath(model, 'c'); |
| assert.deepEqual([3, 4], observer.finishReset()) |
| |
| model.a = 3; |
| observer.deliver(); |
| assert.strictEqual(2, callCount); |
| |
| model.b = 4; |
| observer.deliver(); |
| assert.strictEqual(3, callCount); |
| |
| model.c = 5; |
| observer.deliver(); |
| assert.strictEqual(4, callCount); |
| |
| observer.close(); |
| }); |
| |
| it('Heterogeneous', function() { |
| var model = { a: 1, b: 2 }; |
| var otherModel = { c: 3 }; |
| |
| function valueFn(value) { return value * 2; } |
| function setValueFn(value) { return value / 2; } |
| |
| var compound = new CompoundObserver; |
| compound.addPath(model, 'a'); |
| compound.addObserver(new ObserverTransform(new PathObserver(model, 'b'), |
| valueFn, setValueFn)); |
| compound.addObserver(new PathObserver(otherModel, 'c')); |
| |
| function combine(values) { |
| return values[0] + values[1] + values[2]; |
| }; |
| observer = new ObserverTransform(compound, combine); |
| assert.strictEqual(8, observer.open(callback)); |
| |
| model.a = 2; |
| model.b = 4; |
| assertPathChanges(13, 8); |
| |
| model.b = 10; |
| otherModel.c = 5; |
| assertPathChanges(27, 13); |
| |
| model.a = 20; |
| model.b = 1; |
| otherModel.c = 5; |
| assertNoChanges(); |
| |
| observer.close(); |
| }) |
| }); |
| |
| describe('ArrayObserver Tests', function() { |
| |
| beforeEach(doSetup); |
| |
| afterEach(doTeardown); |
| |
| function ensureNonSparse(arr) { |
| for (var i = 0; i < arr.length; i++) { |
| if (i in arr) |
| continue; |
| arr[i] = undefined; |
| } |
| } |
| |
| function assertArrayChanges(expectSplices) { |
| observer.deliver(); |
| var splices = callbackArgs[0]; |
| |
| assert.isTrue(callbackInvoked); |
| |
| splices.forEach(function(splice) { |
| ensureNonSparse(splice.removed); |
| }); |
| |
| expectSplices.forEach(function(splice) { |
| ensureNonSparse(splice.removed); |
| }); |
| |
| assert.deepEqual(expectSplices, splices); |
| callbackArgs = undefined; |
| callbackInvoked = false; |
| } |
| |
| function applySplicesAndAssertDeepEqual(orig, copy) { |
| observer.deliver(); |
| if (callbackInvoked) { |
| var splices = callbackArgs[0]; |
| ArrayObserver.applySplices(copy, orig, splices); |
| } |
| |
| ensureNonSparse(orig); |
| ensureNonSparse(copy); |
| assert.deepEqual(orig, copy); |
| callbackArgs = undefined; |
| callbackInvoked = false; |
| } |
| |
| function assertEditDistance(orig, expectDistance) { |
| observer.deliver(); |
| var splices = callbackArgs[0]; |
| var actualDistance = 0; |
| |
| if (callbackInvoked) { |
| splices.forEach(function(splice) { |
| actualDistance += splice.addedCount + splice.removed.length; |
| }); |
| } |
| |
| assert.deepEqual(expectDistance, actualDistance); |
| callbackArgs = undefined; |
| callbackInvoked = false; |
| } |
| |
| function arrayMutationTest(arr, operations) { |
| var copy = arr.slice(); |
| observer = new ArrayObserver(arr); |
| observer.open(callback); |
| operations.forEach(function(op) { |
| switch(op.name) { |
| case 'delete': |
| delete arr[op.index]; |
| break; |
| |
| case 'update': |
| arr[op.index] = op.value; |
| break; |
| |
| default: |
| arr[op.name].apply(arr, op.args); |
| break; |
| } |
| }); |
| |
| applySplicesAndAssertDeepEqual(arr, copy); |
| observer.close(); |
| } |
| |
| it('Optional target for callback', function() { |
| var target = { |
| changed: function(splices) { |
| this.called = true; |
| } |
| }; |
| var obj = []; |
| var observer = new ArrayObserver(obj); |
| observer.open(target.changed, target); |
| obj.length = 1; |
| observer.deliver(); |
| assert.isTrue(target.called); |
| observer.close(); |
| }); |
| |
| it('Delivery Until No Changes', function() { |
| var arr = [0, 1, 2, 3, 4]; |
| var callbackCount = 0; |
| var observer = new ArrayObserver(arr); |
| observer.open(function() { |
| callbackCount++; |
| arr.shift(); |
| }); |
| |
| arr.shift(); |
| observer.deliver(); |
| |
| assert.equal(5, callbackCount); |
| |
| observer.close(); |
| }); |
| |
| it('Array disconnect', function() { |
| var arr = [ 0 ]; |
| |
| observer = new ArrayObserver(arr); |
| observer.open(callback); |
| |
| arr[0] = 1; |
| |
| assertArrayChanges([{ |
| index: 0, |
| removed: [0], |
| addedCount: 1 |
| }]); |
| |
| observer.close(); |
| arr[1] = 2; |
| assertNoChanges(); |
| }); |
| |
| it('Array discardChanges', function() { |
| var arr = []; |
| |
| arr.push(1); |
| observer = new ArrayObserver(arr); |
| observer.open(callback); |
| arr.push(2); |
| |
| assertArrayChanges([{ |
| index: 1, |
| removed: [], |
| addedCount: 1 |
| }]); |
| |
| arr.push(3); |
| observer.discardChanges(); |
| assertNoChanges(); |
| |
| arr.pop(); |
| assertArrayChanges([{ |
| index: 2, |
| removed: [3], |
| addedCount: 0 |
| }]); |
| observer.close(); |
| }); |
| |
| it('Array', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model[0] = 2; |
| |
| assertArrayChanges([{ |
| index: 0, |
| removed: [0], |
| addedCount: 1 |
| }]); |
| |
| model[1] = 3; |
| assertArrayChanges([{ |
| index: 1, |
| removed: [1], |
| addedCount: 1 |
| }]); |
| |
| observer.close(); |
| }); |
| |
| it('Array observe non-array throws', function() { |
| assert.throws(function () { |
| observer = new ArrayObserver({}); |
| }); |
| }); |
| |
| it('Array Set Same', function() { |
| var model = [1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model[0] = 1; |
| observer.deliver(); |
| assert.isFalse(callbackInvoked); |
| observer.close(); |
| }); |
| |
| it('Array Splice', function() { |
| var model = [0, 1] |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.splice(1, 1, 2, 3); // [0, 2, 3] |
| assertArrayChanges([{ |
| index: 1, |
| removed: [1], |
| addedCount: 2 |
| }]); |
| |
| model.splice(0, 1); // [2, 3] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [0], |
| addedCount: 0 |
| }]); |
| |
| model.splice(); |
| assertNoChanges(); |
| |
| model.splice(0, 0); |
| assertNoChanges(); |
| |
| model.splice(0, -1); |
| assertNoChanges(); |
| |
| model.splice(-1, 0, 1.5); // [2, 1.5, 3] |
| assertArrayChanges([{ |
| index: 1, |
| removed: [], |
| addedCount: 1 |
| }]); |
| |
| model.splice(3, 0, 0); // [2, 1.5, 3, 0] |
| assertArrayChanges([{ |
| index: 3, |
| removed: [], |
| addedCount: 1 |
| }]); |
| |
| model.splice(0); // [] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [2, 1.5, 3, 0], |
| addedCount: 0 |
| }]); |
| |
| observer.close(); |
| }); |
| |
| it('Array Splice Truncate And Expand With Length', function() { |
| var model = ['a', 'b', 'c', 'd', 'e']; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.length = 2; |
| |
| assertArrayChanges([{ |
| index: 2, |
| removed: ['c', 'd', 'e'], |
| addedCount: 0 |
| }]); |
| |
| model.length = 5; |
| |
| assertArrayChanges([{ |
| index: 2, |
| removed: [], |
| addedCount: 3 |
| }]); |
| |
| observer.close(); |
| }); |
| |
| it('Array Splice Delete Too Many', function() { |
| var model = ['a', 'b', 'c']; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.splice(2, 3); // ['a', 'b'] |
| assertArrayChanges([{ |
| index: 2, |
| removed: ['c'], |
| addedCount: 0 |
| }]); |
| |
| observer.close(); |
| }); |
| |
| it('Array Length', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.length = 5; // [0, 1, , , ,]; |
| assertArrayChanges([{ |
| index: 2, |
| removed: [], |
| addedCount: 3 |
| }]); |
| |
| model.length = 1; |
| assertArrayChanges([{ |
| index: 1, |
| removed: [1, , , ,], |
| addedCount: 0 |
| }]); |
| |
| model.length = 1; |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Array Push', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.push(2, 3); // [0, 1, 2, 3] |
| assertArrayChanges([{ |
| index: 2, |
| removed: [], |
| addedCount: 2 |
| }]); |
| |
| model.push(); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Array Pop', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.pop(); // [0] |
| assertArrayChanges([{ |
| index: 1, |
| removed: [1], |
| addedCount: 0 |
| }]); |
| |
| model.pop(); // [] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [0], |
| addedCount: 0 |
| }]); |
| |
| model.pop(); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Array Shift', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.shift(); // [1] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [0], |
| addedCount: 0 |
| }]); |
| |
| model.shift(); // [] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [1], |
| addedCount: 0 |
| }]); |
| |
| model.shift(); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Array Unshift', function() { |
| var model = [0, 1]; |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.unshift(-1); // [-1, 0, 1] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [], |
| addedCount: 1 |
| }]); |
| |
| model.unshift(-3, -2); // [] |
| assertArrayChanges([{ |
| index: 0, |
| removed: [], |
| addedCount: 2 |
| }]); |
| |
| model.unshift(); |
| assertNoChanges(); |
| |
| observer.close(); |
| }); |
| |
| it('Array Tracker Contained', function() { |
| arrayMutationTest( |
| ['a', 'b'], |
| [ |
| { name: 'splice', args: [1, 1] }, |
| { name: 'unshift', args: ['c', 'd', 'e'] }, |
| { name: 'splice', args: [1, 2, 'f'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Delete Empty', function() { |
| arrayMutationTest( |
| [], |
| [ |
| { name: 'delete', index: 0 }, |
| { name: 'splice', args: [0, 0, 'a', 'b', 'c'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Right Non Overlap', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [0, 1, 'e'] }, |
| { name: 'splice', args: [2, 1, 'f', 'g'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Left Non Overlap', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [3, 1, 'f', 'g'] }, |
| { name: 'splice', args: [0, 1, 'e'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Right Adjacent', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [1, 1, 'e'] }, |
| { name: 'splice', args: [2, 1, 'f', 'g'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Left Adjacent', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [2, 2, 'e'] }, |
| { name: 'splice', args: [1, 1, 'f', 'g'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Right Overlap', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [1, 1, 'e'] }, |
| { name: 'splice', args: [1, 1, 'f', 'g'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Left Overlap', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| // a b [e f g] d |
| { name: 'splice', args: [2, 1, 'e', 'f', 'g'] }, |
| // a [h i j] f g d |
| { name: 'splice', args: [1, 2, 'h', 'i', 'j'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Prefix And Suffix One In', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'unshift', args: ['z'] }, |
| { name: 'push', arg: ['z'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Shift One', function() { |
| arrayMutationTest( |
| [16, 15, 15], |
| [ |
| { name: 'shift', args: ['z'] } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Update Delete', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'splice', args: [2, 1, 'e', 'f', 'g'] }, |
| { name: 'update', index: 0, value: 'h' }, |
| { name: 'delete', index: 1 } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Update After Delete', function() { |
| arrayMutationTest( |
| ['a', 'b', undefined, 'd'], |
| [ |
| { name: 'update', index: 2, value: 'e' } |
| ] |
| ); |
| }); |
| |
| it('Array Tracker Delete Mid Array', function() { |
| arrayMutationTest( |
| ['a', 'b', 'c', 'd'], |
| [ |
| { name: 'delete', index: 2 } |
| ] |
| ); |
| }); |
| |
| it('Array Random Case 1', function() { |
| var model = ['a','b']; |
| var copy = model.slice(); |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.splice(0, 1, 'c', 'd', 'e'); |
| model.splice(4,0,'f'); |
| model.splice(3,2); |
| |
| applySplicesAndAssertDeepEqual(model, copy); |
| }); |
| |
| it('Array Random Case 2', function() { |
| var model = [3,4]; |
| var copy = model.slice(); |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.splice(2,0,8); |
| model.splice(0,1,0,5); |
| model.splice(2,2); |
| |
| applySplicesAndAssertDeepEqual(model, copy); |
| }); |
| |
| it('Array Random Case 3', function() { |
| var model = [1,3,6]; |
| var copy = model.slice(); |
| |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| |
| model.splice(1,1); |
| model.splice(0,2,1,7); |
| model.splice(1,0,3,7); |
| |
| applySplicesAndAssertDeepEqual(model, copy); |
| }); |
| |
| it('Array Tracker No Proxies Edits', function() { |
| var model = []; |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| model.length = 0; |
| model.push(1, 2, 3); |
| assertEditDistance(model, 3); |
| observer.close(); |
| |
| model = ['x', 'x', 'x', 'x', '1', '2', '3']; |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| model.length = 0; |
| model.push('1', '2', '3', 'y', 'y', 'y', 'y'); |
| assertEditDistance(model, 8); |
| observer.close(); |
| |
| model = ['1', '2', '3', '4', '5']; |
| observer = new ArrayObserver(model); |
| observer.open(callback); |
| model.length = 0; |
| model.push('a', '2', 'y', 'y', '4', '5', 'z', 'z'); |
| assertEditDistance(model, 7); |
| observer.close(); |
| }); |
| }); |
| </script> |