Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1442)

Unified Diff: sky/tests/framework/observe.sky

Issue 694323003: Added tests for observe.sky (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | sky/tests/framework/observe-expected.txt » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sky/tests/framework/observe.sky
diff --git a/sky/tests/framework/observe.sky b/sky/tests/framework/observe.sky
new file mode 100644
index 0000000000000000000000000000000000000000..4ba41e7dae3a5f65e72524bc156ed4c5e65f90fc
--- /dev/null
+++ b/sky/tests/framework/observe.sky
@@ -0,0 +1,1872 @@
+<html>
+<link rel="import" href="../resources/chai.sky" />
+<link rel="import" href="../resources/mocha.sky" />
+<link rel="import" href="/sky/framework/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; }
+
+ function setValueFn(value) { return value / 2; }
+
+ observer = new ObserverTransform(new PathObserver(obj, 'foo'),
+ valueFn,
+ setValueFn);
+ observer.open(callback);
+
+ obj.foo = 2;
+
+ assert.strictEqual(4, observer.discardChanges());
+ assertNoChanges();
+
+ observer.setValue(2);
+ assert.strictEqual(obj.foo, 1);
+ assertPathChanges(2, 4);
+
+ obj.foo = 10;
+ assertPathChanges(20, 2);
+
+ 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);
+ });
+
+ function ArrayFuzzer() {}
+
+ ArrayFuzzer.valMax = 16;
+ ArrayFuzzer.arrayLengthMax = 128;
+ ArrayFuzzer.operationCount = 64;
+
+ function randDouble(start, end) {
+ return Math.random()*(end-start) + start;
+ }
+
+ function randInt(start, end) {
+ return Math.round(randDouble(start, end));
+ }
+
+ function randASCIIChar() {
+ return String.fromCharCode(randInt(32, 126));
+ }
+
+ function randValue() {
+ switch(randInt(0, 5)) {
+ case 0:
+ return {};
+ case 1:
+ return undefined;
+ case 2:
+ return null;
+ case 3:
+ return randInt(0, ArrayFuzzer.valMax);
+ case 4:
+ return randDouble(0, ArrayFuzzer.valMax);
+ case 5:
+ return randASCIIChar();
+ }
+ }
+
+ function randArray() {
+ var args = [];
+ var count = randInt(0, ArrayFuzzer.arrayLengthMax);
+ while(count-- > 0)
+ args.push(randValue());
+
+ return args;
+ }
+
+ function randomArrayOperation(arr) {
+ function empty() {
+ return [];
+ }
+
+ var operations = {
+ push: randArray,
+ unshift: randArray,
+ pop: empty,
+ shift: empty,
+ splice: function() {
+ var args = [];
+ args.push(randInt(-arr.length*2, arr.length*2), randInt(0, arr.length*2));
+ args = args.concat(randArray());
+ return args;
+ }
+ };
+
+ // Do a splice once for each of the other operations.
+ var operationList = ['splice', 'update',
+ 'splice', 'delete',
+ 'splice', 'push',
+ 'splice', 'pop',
+ 'splice', 'shift',
+ 'splice', 'unshift'];
+
+ var op = {
+ name: operationList[randInt(0, operationList.length - 1)]
+ };
+
+ switch(op.name) {
+ case 'delete':
+ op.index = randInt(0, arr.length - 1);
+ delete arr[op.index];
+ break;
+
+ case 'update':
+ op.index = randInt(0, arr.length);
+ op.value = randValue();
+ arr[op.index] = op.value;
+ break;
+
+ default:
+ op.args = operations[op.name]();
+ arr[op.name].apply(arr, op.args);
+ break;
+ }
+
+ return op;
+ }
+
+ function randomArrayOperations(arr, count) {
+ var ops = []
+ for (var i = 0; i < count; i++) {
+ ops.push(randomArrayOperation(arr));
+ }
+
+ return ops;
+ }
+
+ ArrayFuzzer.prototype.go = function() {
+ var orig = this.arr = randArray();
+ randomArrayOperations(this.arr, ArrayFuzzer.operationCount);
+ var copy = this.copy = this.arr.slice();
+ this.origCopy = this.copy.slice();
+
+ var observer = new ArrayObserver(this.arr);
+ observer.open(function(splices) {
+ ArrayObserver.applySplices(copy, orig, splices);
+ });
+
+ this.ops = randomArrayOperations(this.arr, ArrayFuzzer.operationCount);
+ observer.deliver();
+ observer.close();
+ }
+
+
+ it('Array Tracker Fuzzer', function() {
+ var testCount = 64;
+
+ for (var i = 0; i < testCount; i++) {
+ var fuzzer = new ArrayFuzzer();
+ fuzzer.go();
+ ensureNonSparse(fuzzer.arr);
+ ensureNonSparse(fuzzer.copy);
+ assert.deepEqual(fuzzer.arr, fuzzer.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>
« no previous file with comments | « no previous file | sky/tests/framework/observe-expected.txt » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698