blob: 4f8349be82de258b30f090b8d7d385e8dbe0508b [file] [log] [blame] [edit]
<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>