mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
373 lines
13 KiB
JavaScript
373 lines
13 KiB
JavaScript
|
/* global describe, it */
|
||
|
|
||
|
var _ = require('underscore');
|
||
|
var assert = require('assert');
|
||
|
var ko = require('knockout');
|
||
|
var sinon = require('sinon');
|
||
|
|
||
|
var clientUtil = require('../clientUtil');
|
||
|
var koArray = require('app/client/lib/koArray');
|
||
|
|
||
|
describe('koArray', function() {
|
||
|
clientUtil.setTmpMochaGlobals();
|
||
|
|
||
|
it("should emit spliceChange events", function() {
|
||
|
var arr = koArray([1, 2, 3]);
|
||
|
|
||
|
var events = [];
|
||
|
|
||
|
// Whenever we get an event, push it to events.
|
||
|
['change', 'spliceChange'].forEach(function(type) {
|
||
|
arr.subscribe(function(data) {
|
||
|
events.push({ type: type, data: data });
|
||
|
}, null, type);
|
||
|
});
|
||
|
|
||
|
function expectSplice(start, num, deleted, options) {
|
||
|
assert.equal(events.length, 2);
|
||
|
var e = events.shift();
|
||
|
assert.equal(e.type, 'spliceChange');
|
||
|
assert.equal(e.data.start, start);
|
||
|
assert.equal(e.data.added, num);
|
||
|
assert.deepEqual(e.data.deleted, deleted);
|
||
|
|
||
|
e = events.shift();
|
||
|
assert.equal(e.type, 'change');
|
||
|
}
|
||
|
|
||
|
assert.deepEqual(arr.all(), [1, 2, 3]);
|
||
|
|
||
|
// push should work fine.
|
||
|
arr.push("foo");
|
||
|
expectSplice(3, 1, []);
|
||
|
|
||
|
arr.push("bar");
|
||
|
expectSplice(4, 1, []);
|
||
|
assert.deepEqual(arr.all(), [1, 2, 3, "foo", "bar"]);
|
||
|
assert.deepEqual(arr.peek(), [1, 2, 3, "foo", "bar"]);
|
||
|
assert.equal(arr.peekLength, 5);
|
||
|
|
||
|
// insertions via splice should work.
|
||
|
arr.splice(1, 0, "hello", "world");
|
||
|
expectSplice(1, 2, []);
|
||
|
assert.deepEqual(arr.all(), [1, "hello", "world", 2, 3, "foo", "bar"]);
|
||
|
|
||
|
// including using negative indices.
|
||
|
arr.splice(-6, 2, "blah");
|
||
|
expectSplice(1, 1, ["hello", "world"]);
|
||
|
assert.deepEqual(arr.all(), [1, "blah", 2, 3, "foo", "bar"]);
|
||
|
|
||
|
// slice should work but not emit anything.
|
||
|
assert.deepEqual(arr.slice(3, 5), [3, "foo"]);
|
||
|
assert.equal(events.length, 0);
|
||
|
|
||
|
// deletions using splice should work
|
||
|
arr.splice(-2, 1);
|
||
|
expectSplice(4, 0, ["foo"]);
|
||
|
assert.deepEqual(arr.all(), [1, "blah", 2, 3, "bar"]);
|
||
|
|
||
|
// including deletions to the end
|
||
|
arr.splice(1);
|
||
|
expectSplice(1, 0, ["blah", 2, 3, "bar"]);
|
||
|
assert.deepEqual(arr.all(), [1]);
|
||
|
|
||
|
// setting a new array should also produce a splice event.
|
||
|
var newValues = [4, 5, 6];
|
||
|
arr.assign(newValues);
|
||
|
expectSplice(0, 3, [1]);
|
||
|
|
||
|
// Check that koArray does not affect the array passed-in on assignment.
|
||
|
arr.push(7);
|
||
|
expectSplice(3, 1, []);
|
||
|
assert.deepEqual(newValues, [4, 5, 6]);
|
||
|
assert.deepEqual(arr.peek(), [4, 5, 6, 7]);
|
||
|
|
||
|
// We don't support various observableArray() methods. If we do start supporting them, we
|
||
|
// need to make sure they emit correct events.
|
||
|
assert.throws(function() { arr.pop(); }, Error);
|
||
|
assert.throws(function() { arr.remove("b"); }, Error);
|
||
|
});
|
||
|
|
||
|
it("should create dependencies when needed", function() {
|
||
|
var arr = koArray([1, 2, 3]);
|
||
|
var sum = ko.computed(function() {
|
||
|
return arr.all().reduce(function(sum, item) { return sum + item; }, 0);
|
||
|
});
|
||
|
var peekSum = ko.computed(function() {
|
||
|
return arr.peek().reduce(function(sum, item) { return sum + item; }, 0);
|
||
|
});
|
||
|
|
||
|
assert.equal(sum(), 6);
|
||
|
assert.equal(peekSum(), 6);
|
||
|
arr.push(10);
|
||
|
assert.equal(sum(), 16);
|
||
|
assert.equal(peekSum(), 6);
|
||
|
arr.splice(1, 1);
|
||
|
assert.equal(sum(), 14);
|
||
|
assert.equal(peekSum(), 6);
|
||
|
arr.splice(0);
|
||
|
assert.equal(sum(), 0);
|
||
|
assert.equal(peekSum(), 6);
|
||
|
});
|
||
|
|
||
|
describe("#arraySplice", function() {
|
||
|
it("should work similarly to splice", function() {
|
||
|
var arr = koArray([1, 2, 3]);
|
||
|
arr.arraySplice(1, 2, []);
|
||
|
assert.deepEqual(arr.peek(), [1]);
|
||
|
arr.arraySplice(1, 0, [10, 11]);
|
||
|
assert.deepEqual(arr.peek(), [1, 10, 11]);
|
||
|
arr.arraySplice(0, 0, [4, 5]);
|
||
|
assert.deepEqual(arr.peek(), [4, 5, 1, 10, 11]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("#makeLiveIndex", function() {
|
||
|
it("should be kept valid", function() {
|
||
|
var arr = koArray([1, 2, 3]);
|
||
|
var index = arr.makeLiveIndex();
|
||
|
assert.equal(index(), 0);
|
||
|
|
||
|
index(-1);
|
||
|
assert.equal(index(), 0);
|
||
|
|
||
|
index(null);
|
||
|
assert.equal(index(), 0);
|
||
|
|
||
|
index(100);
|
||
|
assert.equal(index(), 2);
|
||
|
|
||
|
arr.splice(1, 1);
|
||
|
assert.deepEqual(arr.peek(), [1, 3]);
|
||
|
assert.equal(index(), 1);
|
||
|
|
||
|
arr.splice(0, 1, 5, 6, 7);
|
||
|
assert.deepEqual(arr.peek(), [5, 6, 7, 3]);
|
||
|
assert.equal(index(), 3);
|
||
|
|
||
|
arr.push(10);
|
||
|
arr.splice(2, 2);
|
||
|
assert.deepEqual(arr.peek(), [5, 6, 10]);
|
||
|
assert.equal(index(), 2);
|
||
|
|
||
|
arr.splice(2, 1);
|
||
|
assert.deepEqual(arr.peek(), [5, 6]);
|
||
|
assert.equal(index(), 1);
|
||
|
|
||
|
arr.splice(0, 2);
|
||
|
assert.deepEqual(arr.peek(), []);
|
||
|
assert.equal(index(), null);
|
||
|
|
||
|
arr.splice(0, 0, 1, 2, 3);
|
||
|
assert.deepEqual(arr.peek(), [1, 2, 3]);
|
||
|
assert.equal(index(), 0);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("#map", function() {
|
||
|
it("should map immediately and continuously", function() {
|
||
|
var arr = koArray([1, 2, 3]);
|
||
|
var mapped = arr.map(function(orig) { return orig * 10; });
|
||
|
assert.deepEqual(mapped.peek(), [10, 20, 30]);
|
||
|
arr.push(4);
|
||
|
assert.deepEqual(mapped.peek(), [10, 20, 30, 40]);
|
||
|
arr.splice(1, 1);
|
||
|
assert.deepEqual(mapped.peek(), [10, 30, 40]);
|
||
|
arr.splice(0, 1, 5, 6, 7);
|
||
|
assert.deepEqual(mapped.peek(), [50, 60, 70, 30, 40]);
|
||
|
arr.splice(2, 0, 2);
|
||
|
assert.deepEqual(mapped.peek(), [50, 60, 20, 70, 30, 40]);
|
||
|
arr.splice(1, 3);
|
||
|
assert.deepEqual(mapped.peek(), [50, 30, 40]);
|
||
|
arr.splice(0, 0, 1, 2, 3);
|
||
|
assert.deepEqual(mapped.peek(), [10, 20, 30, 50, 30, 40]);
|
||
|
arr.splice(3, 3);
|
||
|
assert.deepEqual(mapped.peek(), [10, 20, 30]);
|
||
|
|
||
|
// Check that `this` argument works correctly.
|
||
|
var foo = { test: function(orig) { return orig * 100; } };
|
||
|
var mapped2 = arr.map(function(orig) { return this.test(orig); }, foo);
|
||
|
assert.deepEqual(mapped2.peek(), [100, 200, 300]);
|
||
|
arr.splice(1, 0, 4, 5);
|
||
|
assert.deepEqual(mapped2.peek(), [100, 400, 500, 200, 300]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("#syncMap", function() {
|
||
|
it("should keep two arrays in sync", function() {
|
||
|
var arr1 = koArray([1, 2, 3]);
|
||
|
var arr2 = koArray([4, 5, 6]);
|
||
|
var mapped = koArray();
|
||
|
|
||
|
mapped.syncMap(arr1);
|
||
|
assert.deepEqual(mapped.peek(), [1, 2, 3]);
|
||
|
arr1.splice(1, 1, 8, 9);
|
||
|
assert.deepEqual(mapped.peek(), [1, 8, 9, 3]);
|
||
|
|
||
|
mapped.syncMap(arr2, function(x) { return x * 10; });
|
||
|
assert.deepEqual(mapped.peek(), [40, 50, 60]);
|
||
|
arr1.splice(1, 1, 8, 9);
|
||
|
assert.deepEqual(mapped.peek(), [40, 50, 60]);
|
||
|
arr2.push(8, 9);
|
||
|
assert.deepEqual(mapped.peek(), [40, 50, 60, 80, 90]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('#subscribeForEach', function() {
|
||
|
it('should call onAdd and onRemove callbacks', function() {
|
||
|
var arr1 = koArray([1, 2, 3]);
|
||
|
var seen = [];
|
||
|
function onAdd(x) { seen.push(["add", x]); }
|
||
|
function onRm(x) { seen.push(["rm", x]); }
|
||
|
var sub = arr1.subscribeForEach({ add: onAdd, remove: onRm });
|
||
|
assert.deepEqual(seen, [["add", 1], ["add", 2], ["add", 3]]);
|
||
|
|
||
|
seen = [];
|
||
|
arr1.push(4);
|
||
|
assert.deepEqual(seen, [["add", 4]]);
|
||
|
|
||
|
seen = [];
|
||
|
arr1.splice(1, 2);
|
||
|
assert.deepEqual(seen, [["rm", 2], ["rm", 3]]);
|
||
|
|
||
|
seen = [];
|
||
|
arr1.splice(0, 1, 5, 6);
|
||
|
assert.deepEqual(seen, [["rm", 1], ["add", 5], ["add", 6]]);
|
||
|
|
||
|
// If subscription is disposed, callbacks should no longer get called.
|
||
|
sub.dispose();
|
||
|
seen = [];
|
||
|
arr1.push(10);
|
||
|
assert.deepEqual(seen, []);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('#setAutoDisposeValues', function() {
|
||
|
it('should dispose elements when asked', function() {
|
||
|
var objects = _.range(5).map(function(n) { return { value: n, dispose: sinon.spy() }; });
|
||
|
var arr = koArray(objects.slice(0, 3)).setAutoDisposeValues();
|
||
|
|
||
|
// Just to check what's in the array to start with.
|
||
|
assert.equal(arr.all().length, 3);
|
||
|
assert.strictEqual(arr.at(0), objects[0]);
|
||
|
|
||
|
// Delete two elements: they should get disposed, but the remaining one should not.
|
||
|
var x = arr.splice(0, 2);
|
||
|
assert.equal(arr.all().length, 1);
|
||
|
assert.strictEqual(arr.at(0), objects[2]);
|
||
|
assert.equal(x.length, 2);
|
||
|
sinon.assert.calledOnce(x[0].dispose);
|
||
|
sinon.assert.calledOnce(x[1].dispose);
|
||
|
sinon.assert.notCalled(objects[2].dispose);
|
||
|
|
||
|
// Reassign: the remaining element should now also get disposed.
|
||
|
arr.assign(objects.slice(3, 5));
|
||
|
assert.equal(arr.all().length, 2);
|
||
|
assert.strictEqual(arr.at(0), objects[3]);
|
||
|
sinon.assert.calledOnce(objects[2].dispose);
|
||
|
sinon.assert.notCalled(objects[3].dispose);
|
||
|
sinon.assert.notCalled(objects[4].dispose);
|
||
|
|
||
|
// Dispose the entire array: previously assigned elements should be disposed.
|
||
|
arr.dispose();
|
||
|
sinon.assert.calledOnce(objects[3].dispose);
|
||
|
sinon.assert.calledOnce(objects[4].dispose);
|
||
|
|
||
|
// Check that elements disposed earlier haven't been disposed more than once.
|
||
|
sinon.assert.calledOnce(objects[0].dispose);
|
||
|
sinon.assert.calledOnce(objects[1].dispose);
|
||
|
sinon.assert.calledOnce(objects[2].dispose);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('syncedKoArray', function() {
|
||
|
it("should return array synced to the value of the observable", function() {
|
||
|
var arr1 = koArray(["1", "2", "3"]);
|
||
|
var arr2 = koArray(["foo", "bar"]);
|
||
|
var arr3 = ["hello", "world"];
|
||
|
var obs = ko.observable(arr1);
|
||
|
|
||
|
var combined = koArray.syncedKoArray(obs);
|
||
|
|
||
|
// The values match the array returned by the observable, but mapped using wrap().
|
||
|
assert.deepEqual(combined.all(), ["1", "2", "3"]);
|
||
|
|
||
|
// Changes to the array changes the synced array.
|
||
|
arr1.push("4");
|
||
|
assert.deepEqual(combined.all(), ["1", "2", "3", "4"]);
|
||
|
|
||
|
// Changing the observable changes the synced array; the value may be a plain array.
|
||
|
obs(arr3);
|
||
|
assert.deepEqual(combined.all(), ["hello", "world"]);
|
||
|
|
||
|
// Previously mapped observable array no longer affects the combined one. And of course
|
||
|
// modifying the non-observable array makes no difference either.
|
||
|
arr1.push("4");
|
||
|
arr3.splice(0, 1);
|
||
|
arr3.push("qwer");
|
||
|
assert.deepEqual(combined.all(), ["hello", "world"]);
|
||
|
|
||
|
// Test assigning again to a koArray.
|
||
|
obs(arr2);
|
||
|
assert.deepEqual(combined.all(), ["foo", "bar"]);
|
||
|
arr2.splice(0, 1);
|
||
|
assert.deepEqual(combined.all(), ["bar"]);
|
||
|
arr2.splice(0, 0, "this", "is", "a", "test");
|
||
|
assert.deepEqual(combined.all(), ["this", "is", "a", "test", "bar"]);
|
||
|
arr2.assign(["10", "20"]);
|
||
|
assert.deepEqual(combined.all(), ["10", "20"]);
|
||
|
|
||
|
// Check that only arr2 has a subscriber (not arr1), and that disposing unsubscribes from
|
||
|
// both the observable and the currently active array.
|
||
|
assert.equal(arr1.getObservable().getSubscriptionsCount(), 1);
|
||
|
assert.equal(arr2.getObservable().getSubscriptionsCount(), 2);
|
||
|
assert.equal(obs.getSubscriptionsCount(), 1);
|
||
|
combined.dispose();
|
||
|
assert.equal(obs.getSubscriptionsCount(), 0);
|
||
|
assert.equal(arr2.getObservable().getSubscriptionsCount(), 1);
|
||
|
});
|
||
|
|
||
|
it("should work with a mapper callback", function() {
|
||
|
var arr1 = koArray(["1", "2", "3"]);
|
||
|
var obs = ko.observable();
|
||
|
|
||
|
function wrap(value) { return "x" + value; }
|
||
|
var combined = koArray.syncedKoArray(obs, wrap);
|
||
|
assert.deepEqual(combined.all(), []);
|
||
|
obs(arr1);
|
||
|
assert.deepEqual(combined.all(), ["x1", "x2", "x3"]);
|
||
|
arr1.push("4");
|
||
|
assert.deepEqual(combined.all(), ["x1", "x2", "x3", "x4"]);
|
||
|
obs(["foo", "bar"]);
|
||
|
assert.deepEqual(combined.all(), ["xfoo", "xbar"]);
|
||
|
arr1.splice(1, 1);
|
||
|
obs(arr1);
|
||
|
arr1.splice(1, 1);
|
||
|
assert.deepEqual(combined.all(), ["x1", "x4"]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("syncedMap", function() {
|
||
|
it("should associate state with each item and dispose it", function() {
|
||
|
var arr = koArray(["1", "2", "3"]);
|
||
|
var constructSpy = sinon.spy(), disposeSpy = sinon.spy();
|
||
|
var map = koArray.syncedMap(arr, (state, val) => {
|
||
|
constructSpy(val);
|
||
|
state.autoDisposeCallback(() => disposeSpy(val));
|
||
|
});
|
||
|
assert.deepEqual(constructSpy.args, [["1"], ["2"], ["3"]]);
|
||
|
assert.deepEqual(disposeSpy.args, []);
|
||
|
arr.splice(1, 0, "4", "5");
|
||
|
assert.deepEqual(arr.peek(), ["1", "4", "5", "2", "3"]);
|
||
|
assert.deepEqual(constructSpy.args, [["1"], ["2"], ["3"], ["4"], ["5"]]);
|
||
|
assert.deepEqual(disposeSpy.args, []);
|
||
|
arr.splice(0, 2);
|
||
|
assert.deepEqual(constructSpy.args, [["1"], ["2"], ["3"], ["4"], ["5"]]);
|
||
|
assert.deepEqual(disposeSpy.args, [["1"], ["4"]]);
|
||
|
map.dispose();
|
||
|
assert.deepEqual(constructSpy.args, [["1"], ["2"], ["3"], ["4"], ["5"]]);
|
||
|
assert.deepEqual(disposeSpy.args, [["1"], ["4"], ["2"], ["3"], ["5"]]);
|
||
|
});
|
||
|
});
|
||
|
});
|