Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,618 @@
/* global define */
'use strict';
// The views are very tightly coupled to models, rather than reimplement model
// behavior in a mock, rely on tested interfaces on the model instead
suite('ListView', function() {
// Define a shim for ObservableArray that is to be loaded in ListView.
// Then we are able to creat a stub on it.
suiteSetup(function(done) {
// Use the shim in ListView.
var ObservableArrayShimName = 'observable_array_shim' + Date.now();
var map = {
'modules/mvvm/list_view': {
'modules/mvvm/observable_array': ObservableArrayShimName
}
};
// XXX: Create a new require context.
// We can only define the mock module after retrieving the context.
var requireCtx = testRequire([], map, function() {});
// Define the mock module.
define(ObservableArrayShimName, function() {
var ctor = function observable_array_shim() {
return ctor.mInnerFunction.apply(this, arguments);
};
ctor.mInnerFunction = function() {};
ctor.mTeardown = function observable_array_shim_teardown() {
ctor.mInnerFunction = function() {};
};
return ctor;
});
requireCtx([
'modules/mvvm/observable',
'modules/mvvm/observable_array',
'modules/mvvm/list_view',
ObservableArrayShimName
], (function(Observable, ObservableArray, ListView, ObservableArrayShim) {
this.Observable = Observable;
this.ListView = ListView;
this.ObservableArrayShim = ObservableArrayShim;
// stub the real ObservableArray to create spies on observe method
// Redirect the shim to the ObservableArray stub.
this.ObservableArray = sinon.stub(ObservableArrayShim,
'mInnerFunction', function() {
var result = ObservableArray.apply(this, arguments);
sinon.spy(result, 'observe');
sinon.spy(result, 'unobserve');
return result;
});
done();
}).bind(this));
});
teardown(function() {
this.ObservableArray.reset();
});
suiteTeardown(function() {
this.ObservableArray.restore();
});
setup(function() {
this.array = [];
this.observableArray = this.ObservableArray(this.array);
});
suite('ListView(<ul>, ObservableArray, function)', function() {
setup(function() {
this.container = document.createElement('ul');
this.template = (function(item, recycle) {
if (recycle && this.template.recycle) {
return recycle;
}
return document.createElement('li');
}).bind(this);
this.sinon.spy(this, 'template');
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('.element', function() {
assert.equal(this.listView.element, this.container);
});
test('Listens to ObservableArray', function() {
var observe = this.observableArray.observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
suite('Add Multiple Elements', function() {
setup(function() {
// add 3 items
for (var count = 0; count < 3; count++) {
this.observableArray.push({ count: count });
}
});
// use forEach instead of for to keep 'count' in a closure
[0, 1, 2].forEach(function(count) {
suite('item ' + count, function() {
setup(function() {
this.item = this.array[count];
});
test('kept order', function() {
assert.equal(this.item.count, count);
});
test('called template', function() {
assert.ok(this.template.calledWith(this.item));
});
test('template returned element is in correct position', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[count], li);
});
});
});
suite('Replace Element', function() {
setup(function() {
this.template.reset();
this.item = { replacement: true };
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[1], li);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Replace Element (w/recycle)', function() {
setup(function() {
this.template.recycle = true;
this.template.reset();
this.item = { replacement: true };
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('same element still in place', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(li, this.originalElements[1], 'returned original');
assert.equal(this.container.children[1], this.originalElements[1]);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Reset With Shorter Array', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }];
this.observableArray.reset(this.array);
});
// use forEach instead of for to store 'index' in closure
[0, 1].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
test('removed extra elements', function() {
assert.equal(this.container.children.length, 2);
assert.equal(this.originalElements[2].parentNode, null);
});
});
suite('Reset With Longer Array', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }, { reset: 3 }, { reset: 4 }];
this.observableArray.reset(this.array);
});
// use forEach instead of for to store 'index' in closure
[0, 1, 2].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
suite('item 3', function() {
setup(function() {
this.item = this.array[3];
});
test('called template without recycled element', function() {
assert.ok(this.template.calledWith(this.item));
assert.equal(
this.template.withArgs(this.item).args[0][1], undefined
);
});
test('added extra element', function() {
assert.equal(this.container.children.length, 4);
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[3], li);
});
});
});
suite('reset with .set([{},{}])', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }];
this.listView.set(this.array);
});
test('creates and watches an ObservableArray', function() {
var withNew = this.ObservableArray.withArgs(this.array);
assert.ok(withNew.called);
var observe = withNew.returnValues[0].observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
// use forEach instead of for to store 'index' in closure
[0, 1].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
test('removed extra elements', function() {
assert.equal(this.container.children.length, 2);
assert.equal(this.originalElements[2].parentNode, null);
});
});
suite('.enabled = false', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.listView.enabled = false;
});
// test a bunch of things that modify the array
[function pop() {
this.observableArray.pop();
}, function push() {
this.observableArray.push({});
}, function replace() {
this.observableArray.set(1, {});
}, function reset() {
this.array = [{}];
this.observableArray.reset(this.array);
}, function set() {
this.array = [{}];
this.listView.set(this.array);
}].forEach(function(method) {
suite(method.name, function() {
setup(method);
test('does not call template', function() {
assert.equal(this.template.called, 0);
});
test('does not change elements', function() {
for (var i = 0; i < this.container.children.length; i++) {
assert.equal(this.container.children[i],
this.originalElements[i]);
}
});
suite('.enabled = true', function() {
setup(function() {
this.listView.enabled = true;
});
test('calls template for each item in array', function() {
this.array.forEach(function(item, index) {
assert.ok(this.template.calledWith(
item, this.originalElements[index]
), 'array[' + index + ']');
}, this);
});
test('elements are put in the right place', function() {
this.array.forEach(function(item, index) {
var template = this.template.withArgs(item);
assert.equal(
this.container.children[index], template.returnValues[0],
'element[' + index + ']'
);
}, this);
});
});
});
});
});
});
});
suite('ListView(<div>, ObservableArray, function)', function() {
setup(function() {
this.container = document.createElement('div');
// ensure content inside the container at creation doesn't interfere
this.container.appendChild(document.createElement('span'));
this.template = (function(item, recycle) {
if (recycle && this.template.recycle) {
return recycle;
}
return document.createElement('div');
}).bind(this);
this.sinon.spy(this, 'template');
this.observableArray.push({ count: 0 });
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('.element', function() {
assert.equal(this.listView.element, this.container);
});
test('Listens to ObservableArray', function() {
var observe = this.observableArray.observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
test('Initial Element doesnt recycle', function() {
var item = this.array[0];
var filtered = this.template.withArgs(item);
// ensures that if there happens to be an element in the creation
// of the ListView, it is not used as a "recycled" element
assert.ok(filtered.callCount, 1);
assert.equal(filtered.args[0][1], undefined);
});
suite('Add Multiple Elements', function() {
setup(function() {
// add 2 more items
for (var count = 1; count < 3; count++) {
this.observableArray.push({ count: count });
}
});
// use forEach instead of for to keep 'count' in a closure
[1, 2].forEach(function(count) {
suite('item ' + count, function() {
setup(function() {
this.item = this.array[count];
});
test('kept order', function() {
assert.equal(this.item.count, count);
});
test('called template', function() {
assert.ok(this.template.calledWith(this.item));
});
test('template returned element is in correct position', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[count], div);
});
});
});
suite('Replace Element', function() {
setup(function() {
this.template.reset();
this.item = { replacement: true };
this.originalElements = [].slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('replaced element', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[1], div);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Remove Element', function() {
setup(function() {
this.template.reset();
this.originalElements = [].slice.call(this.container.children);
this.observableArray.pop();
});
test('removed last element', function() {
assert.equal(this.originalElements[2].parentNode, null);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[1], this.originalElements[1]);
});
});
suite('Insert Element', function() {
setup(function() {
this.originalElements = [].slice.call(this.container.children);
this.item = {};
this.template.reset();
this.observableArray.splice(0, 0, this.item);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('inserted element', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[0], div);
});
test('shifts other elements', function() {
assert.equal(this.container.children[1], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[1]);
assert.equal(this.container.children[3], this.originalElements[2]);
});
});
});
});
suite('observable hooks', function() {
setup(function() {
this.observable = this.Observable({ test: 1 });
this.observableArray = this.ObservableArray([this.observable]);
this.hook = this.sinon.spy();
this.template = (function(data, recycle, helper) {
helper.observeAndCall(data, {
test: this.hook
});
return recycle || document.createElement('li');
}).bind(this);
this.container = document.createElement('ul');
this.sinon.spy(this, 'template');
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('hook called at creation', function() {
assert.equal(this.hook.callCount, 1);
});
test('hook called when observable changes', function() {
this.hook.reset();
this.observable.test = false;
assert.equal(this.hook.callCount, 1);
});
test('unobserves when removing', function() {
this.sinon.spy(this.observable, 'unobserve');
this.observableArray.splice(0, 1);
assert.ok(this.observable.unobserve.calledWith(this.hook));
});
test('unobserves when replacing', function() {
this.sinon.spy(this.observable, 'unobserve');
this.sinon.spy(this.observable, 'observe');
this.observableArray.set(0, this.observable);
// should unobserve
assert.ok(this.observable.unobserve.calledWith(this.hook));
// and also observe
assert.ok(this.observable.observe.calledWith('test', this.hook));
// but should unobserve first
assert.ok(
this.observable.unobserve.calledBefore(this.observable.observe)
);
});
});
suite('destroy', function() {
setup(function() {
this.observableArray = this.ObservableArray([1, 2, 3]);
this.container = document.createElement('div');
this.listView = this.ListView(
this.container, this.observableArray, function() {
return document.createElement('div');
}
);
});
test('removes children', function() {
this.listView.destroy();
assert.equal(this.container.children.length, 0);
});
test('sanity check', function() {
assert.equal(this.observableArray.unobserve.callCount, 0);
assert.equal(this.container.children.length, 3);
});
test('.element', function() {
this.listView.destroy();
assert.equal(this.listView.element, null);
});
test('unbinds from observeable', function() {
this.listView.destroy();
// called unobserve with a method
assert.equal(this.observableArray.unobserve.callCount, 1);
var arg = this.observableArray.unobserve.args[0][0];
assert.isFunction(arg);
});
test('creating another ListView destroys old', function() {
this.observableArray.observe.reset();
this.sinon.spy(this.listView, 'destroy');
// create a new one, we don't care about what happens to it.
this.ListView(
this.container, this.observableArray, function() {
return document.createElement('div');
}
);
// destroys old view
assert.ok(this.listView.destroy.called);
// unbinds from observable
assert.ok(this.observableArray.unobserve.called);
// re-observes
assert.ok(this.observableArray.observe.called);
// and make sure its done in the right order
assert.ok(
this.observableArray.unobserve.calledBefore(
this.observableArray.observe
)
);
});
});
});

View file

@ -0,0 +1,275 @@
'use strict';
suite('ObservableArray', function() {
var methods = [
'forEach', 'push', 'pop', 'splice', 'set', 'get', 'reset', 'observe'
];
suiteSetup(function(done) {
testRequire(['modules/mvvm/observable_array'], (function(ObservableArray) {
this.ObservableArray = ObservableArray;
done();
}).bind(this));
});
setup(function() {
this.array = methods.slice();
this.observable = this.ObservableArray(this.array);
});
suite('shape: ObservableArray(array)', function() {
methods.forEach(function(method) {
test('.' + method + ' is a function', function() {
assert.equal(typeof this.observable[method], 'function');
});
});
test('.length is a number', function() {
assert.equal(typeof this.observable.length, 'number');
});
test('.array is the original array', function() {
assert.equal(this.observable.array, this.array);
});
});
test('get()', function() {
assert.ok(this.array.length);
for (var index = 0; index < this.array.length; index++) {
assert.equal(this.observable.get(index), this.array[index],
'index: ' + index);
}
});
suite('observable actions', function() {
var events = ['insert', 'remove', 'replace', 'reset'];
function checkSpies(expected) {
events.forEach(function(event) {
var expectedCount = expected[event] || 0;
test(event + ' called ' + expectedCount + ' times', function() {
assert.equal(this.spies[event].args.length, expectedCount);
});
});
}
function checkArgs(event) {
var args = [].slice.call(arguments, 1);
test(event + ' called with correct arguments', function() {
var calls = this.spies[event].args;
assert.deepEqual(calls[calls.length - 1], args);
});
}
function resetSpies(environment) {
events.forEach(function(event) {
this.spies[event].reset();
}, environment);
}
setup(function() {
this.spies = {};
events.forEach(function(event) {
this.spies[event] = this.sinon.spy();
this.observable.observe(event, this.spies[event]);
}, this);
});
suite('pop()', function() {
setup(function() {
this.observable.pop();
});
checkSpies({ remove: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: methods.length - 1,
count: 1
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.pop();
assert.deepEqual(this.array, expected);
});
});
suite('push()', function() {
setup(function() {
this.observable.push('test');
});
checkSpies({ insert: 1 });
checkArgs('insert', {
type: 'insert',
data: {
index: methods.length,
count: 1,
items: ['test']
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.push('test');
assert.deepEqual(this.array, expected);
});
});
suite('splice(1, 2)', function() {
setup(function() {
this.observable.splice(1, 2);
});
checkSpies({ remove: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: 1,
count: 2
}
});
test('modifies array', function() {
var expect = methods.slice();
expect.splice(1, 2);
assert.deepEqual(this.array, expect);
});
});
suite('splice(1, 2, 3, 4)', function() {
setup(function() {
this.observable.splice(1, 2, 3, 4);
});
checkSpies({ remove: 1, insert: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: 1,
count: 2
}
});
checkArgs('insert', {
type: 'insert',
data: {
index: 1,
count: 2,
items: [3, 4]
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.splice(1, 2, 3, 4);
assert.deepEqual(this.array, expected);
});
});
suite('splice(2, 0, 2)', function() {
setup(function() {
this.observable.splice(2, 0, 2);
});
checkSpies({ insert: 1 });
checkArgs('insert', {
type: 'insert',
data: {
index: 2,
count: 1,
items: [2]
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.splice(2, 0, 2);
assert.deepEqual(this.array, expected);
});
});
suite('set(7, true)', function() {
setup(function() {
this.observable.set(7, true);
});
checkSpies({ replace: 1 });
checkArgs('replace', {
type: 'replace',
data: {
index: 7,
oldValue: methods[7],
newValue: true
}
});
test('modifies array', function() {
var expected = methods.slice();
expected[7] = true;
assert.deepEqual(this.array, expected);
});
});
suite('reset([])', function() {
var testArray = [true, false];
setup(function() {
this.observable.reset(testArray);
});
checkSpies({ reset: 1 });
checkArgs('reset', {
type: 'reset',
data: {
items: testArray
}
});
test('changes array', function() {
assert.equal(this.observable.array, testArray);
});
});
suite('pop() on empty', function() {
setup(function() {
this.observable.reset([]);
resetSpies(this);
this.observable.pop();
});
// no events expected
checkSpies({});
});
suite('unobserve', function() {
setup(function() {
// bind some listeners
this.spies = [this.sinon.spy(), this.sinon.spy()];
events.forEach(function(event) {
this.observable.observe(event, this.spies[0]);
this.observable.observe(event, this.spies[1]);
}, this);
});
test('(property, handler)', function() {
// remove handler for property
this.observable.unobserve('insert', this.spies[0]);
// insert, only other handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// remove, both handlers should be called
this.observable.pop();
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 2);
});
test('(handler)', function() {
// remove handler for all properties
this.observable.unobserve(this.spies[0]);
// insert, only other handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// remove, only other handler should be called
this.observable.pop();
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 2);
});
test('(property)', function() {
// remove handler for all properties
this.observable.unobserve('insert');
// insert, neither handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 0);
// remove, both handlers should be called
this.observable.pop({});
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 1);
});
});
});
});

View file

@ -0,0 +1,110 @@
'use strict';
suite('Observable', function() {
suiteSetup(function(done) {
testRequire(['modules/mvvm/observable'], (function(Observable) {
this.Observable = Observable;
done();
}).bind(this));
});
test('shape: Observable({}).observe is a function', function() {
var observable = this.Observable({});
assert.equal(typeof observable.observe, 'function');
});
test('shape: Observable({}).unobserve is a function', function() {
var observable = this.Observable({});
assert.equal(typeof observable.observe, 'function');
});
suite('Creates observable properties', function() {
var template = {
testBoolean: true,
testNumber: 10,
testString: 'test',
'test object': {}
// currently disabled - we don't handle functions
// testFunction: function() {}
};
var props = Object.keys(template);
setup(function() {
this.observable = this.Observable(template);
});
// some tests for each property
props.forEach(function(prop) {
test('prop: "' + prop + '" copied value', function() {
assert.equal(this.observable[prop], template[prop]);
});
// test some stuff for observing
suite('prop: "' + prop + '" observe', function() {
setup(function() {
this.spy = this.sinon.spy();
this.observable.observe(prop, this.spy);
});
test('does not generate any callbacks when attaching', function() {
assert.isFalse(this.spy.called);
});
test('does not notify when set to same value', function() {
this.observable[prop] = template[prop];
assert.isFalse(this.spy.called);
});
test('notifies when set to new value', function() {
this.newValue = {};
this.observable[prop] = this.newValue;
assert.isTrue(this.spy.calledWith(this.newValue, template[prop]));
});
});
});
suite('unobserve', function() {
setup(function() {
// bind some listeners
this.spies = [this.sinon.spy(), this.sinon.spy()];
props.forEach(function(prop) {
this.observable.observe(prop, this.spies[0]);
this.observable.observe(prop, this.spies[1]);
}, this);
});
test('(property, handler)', function() {
// remove handler for property
this.observable.unobserve('testNumber', this.spies[0]);
// change property - only other handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// change a different property, both handlers should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 2);
});
test('(handler)', function() {
// remove handler for all properties
this.observable.unobserve(this.spies[0]);
// change property - only other handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// change a different property, only other handler should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 2);
});
test('(property)', function() {
// remove handler for all properties
this.observable.unobserve('testNumber');
// change property - neither handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 0);
// change a different property, both handlers should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 1);
});
});
});
});