You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/test/client/models/rowset.js

429 lines
17 KiB

var _ = require('underscore');
var assert = require('chai').assert;
var sinon = require('sinon');
var rowset = require('app/client/models/rowset');
describe('rowset', function() {
describe('RowListener', function() {
it('should translate events to callbacks', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return [1, 2, 3]; };
var lis = rowset.RowListener.create(null);
sinon.spy(lis, "onAddRows");
sinon.spy(lis, "onRemoveRows");
sinon.spy(lis, "onUpdateRows");
lis.subscribeTo(src);
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3]]]);
lis.onAddRows.resetHistory();
src.trigger('rowChange', 'add', [5, 6]);
src.trigger('rowChange', 'remove', [6, 1]);
src.trigger('rowChange', 'update', [3, 5]);
assert.deepEqual(lis.onAddRows.args, [[[5, 6]]]);
assert.deepEqual(lis.onRemoveRows.args, [[[6, 1]]]);
assert.deepEqual(lis.onUpdateRows.args, [[[3, 5]]]);
});
it('should support subscribing to multiple sources', function() {
var src1 = rowset.RowSource.create(null);
src1.getAllRows = function() { return [1, 2, 3]; };
var src2 = rowset.RowSource.create(null);
src2.getAllRows = function() { return ["a", "b", "c"]; };
var lis = rowset.RowListener.create(null);
sinon.spy(lis, "onAddRows");
sinon.spy(lis, "onRemoveRows");
sinon.spy(lis, "onUpdateRows");
lis.subscribeTo(src1);
lis.subscribeTo(src2);
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3]], [["a", "b", "c"]]]);
src1.trigger('rowChange', 'update', [2, 3]);
src2.trigger('rowChange', 'remove', ["b"]);
assert.deepEqual(lis.onUpdateRows.args, [[[2, 3]]]);
assert.deepEqual(lis.onRemoveRows.args, [[["b"]]]);
lis.onAddRows.resetHistory();
lis.unsubscribeFrom(src1);
src1.trigger('rowChange', 'add', [4]);
src2.trigger('rowChange', 'add', ["d"]);
assert.deepEqual(lis.onAddRows.args, [[["d"]]]);
});
});
describe('MappedRowSource', function() {
it('should map row identifiers', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return [1, 2, 3]; };
var mapped = rowset.MappedRowSource.create(null, src, r => "X" + r);
assert.deepEqual(mapped.getAllRows(), ["X1", "X2", "X3"]);
var changeSpy = sinon.spy(), notifySpy = sinon.spy();
mapped.on('rowChange', changeSpy);
mapped.on('rowNotify', notifySpy);
src.trigger('rowChange', 'add', [4, 5, 6]);
src.trigger('rowNotify', [2, 3, 4], 'hello');
src.trigger('rowNotify', rowset.ALL, 'world');
src.trigger('rowChange', 'remove', [1, 5]);
src.trigger('rowChange', 'update', [4, 2]);
assert.deepEqual(changeSpy.args[0], ['add', ['X4', 'X5', 'X6']]);
assert.deepEqual(changeSpy.args[1], ['remove', ['X1', 'X5']]);
assert.deepEqual(changeSpy.args[2], ['update', ['X4', 'X2']]);
assert.deepEqual(changeSpy.callCount, 3);
assert.deepEqual(notifySpy.args[0], [['X2', 'X3', 'X4'], 'hello']);
assert.deepEqual(notifySpy.args[1], [rowset.ALL, 'world']);
assert.deepEqual(notifySpy.callCount, 2);
});
});
function suiteFilteredRowSource(FilteredRowSourceClass) {
it('should only forward matching rows', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return [1, 2, 3]; };
// Filter for only rows that are even numbers.
var filtered = FilteredRowSourceClass.create(null, function(r) { return r % 2 === 0; });
filtered.subscribeTo(src);
assert.deepEqual(Array.from(filtered.getAllRows()), [2]);
var spy = sinon.spy(), notifySpy = sinon.spy();
filtered.on('rowChange', spy);
filtered.on('rowNotify', notifySpy);
src.trigger('rowChange', 'add', [4, 5, 6]);
src.trigger('rowChange', 'add', [7]);
src.trigger('rowNotify', [2, 3, 4], 'hello');
src.trigger('rowNotify', rowset.ALL, 'world');
src.trigger('rowChange', 'remove', [1, 5]);
src.trigger('rowChange', 'remove', [2, 3, 6]);
assert.deepEqual(spy.args[0], ['add', [4, 6]]);
// Nothing for the middle 'add' and 'remove'.
assert.deepEqual(spy.args[1], ['remove', [2, 6]]);
assert.equal(spy.callCount, 2);
assert.deepEqual(notifySpy.args[0], [[2, 4], 'hello']);
assert.deepEqual(notifySpy.args[1], [rowset.ALL, 'world']);
assert.equal(notifySpy.callCount, 2);
assert.deepEqual(Array.from(filtered.getAllRows()), [4]);
});
it('should translate updates to adds or removes if needed', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return [1, 2, 3]; };
var includeSet = new Set([2, 3, 6]);
// Filter for only rows that are in includeMap.
var filtered = FilteredRowSourceClass.create(null, function(r) { return includeSet.has(r); });
filtered.subscribeTo(src);
assert.deepEqual(Array.from(filtered.getAllRows()), [2, 3]);
var spy = sinon.spy();
filtered.on('rowChange', spy);
src.trigger('rowChange', 'add', [4, 5]);
assert.equal(spy.callCount, 0);
includeSet.add(4);
includeSet.delete(2);
src.trigger('rowChange', 'update', [3, 2, 4, 5]);
assert.equal(spy.callCount, 3);
assert.deepEqual(spy.args[0], ['remove', [2]]);
assert.deepEqual(spy.args[1], ['update', [3]]);
assert.deepEqual(spy.args[2], ['add', [4]]);
spy.resetHistory();
src.trigger('rowChange', 'update', [1]);
assert.equal(spy.callCount, 0);
});
}
describe('BaseFilteredRowSource', () => {
suiteFilteredRowSource(rowset.BaseFilteredRowSource);
});
describe('FilteredRowSource', () => {
suiteFilteredRowSource(rowset.FilteredRowSource);
// One extra test case for FilteredRowSource.
it('should support changing the filter function', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return [1, 2, 3, 4, 5]; };
var includeSet = new Set([2, 3, 6]);
// Filter for only rows that are in includeMap.
var filtered = rowset.FilteredRowSource.create(null, function(r) { return includeSet.has(r); });
filtered.subscribeTo(src);
assert.deepEqual(Array.from(filtered.getAllRows()), [2, 3]);
var spy = sinon.spy();
filtered.on('rowChange', spy);
includeSet.add(4);
includeSet.delete(2);
filtered.updateFilter(function(r) { return includeSet.has(r); });
assert.equal(spy.callCount, 2);
assert.deepEqual(spy.args[0], ['remove', [2]]);
assert.deepEqual(spy.args[1], ['add', [4]]);
assert.deepEqual(Array.from(filtered.getAllRows()), [3, 4]);
spy.resetHistory();
includeSet.add(5);
includeSet.add(17);
includeSet.delete(3);
filtered.refilterRows([2, 4, 5, 17]);
// 3 is still in because we didn't ask to refilter it. 17 is still out because it's not in
// any original source.
assert.deepEqual(Array.from(filtered.getAllRows()), [3, 4, 5]);
assert.equal(spy.callCount, 1);
assert.deepEqual(spy.args[0], ['add', [5]]);
});
});
describe('RowGrouping', function() {
it('should add/remove/notify rows in the correct group', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return ["a", "b", "c"]; };
var groups = {a: 1, b: 2, c: 2, d: 1, e: 3, f: 3};
var grouping = rowset.RowGrouping.create(null, function(r) { return groups[r]; });
grouping.subscribeTo(src);
var group1 = grouping.getGroup(1), group2 = grouping.getGroup(2);
assert.deepEqual(Array.from(group1.getAllRows()), ["a"]);
assert.deepEqual(Array.from(group2.getAllRows()), ["b", "c"]);
var lis1 = sinon.spy(), lis2 = sinon.spy(), nlis1 = sinon.spy(), nlis2 = sinon.spy();
group1.on('rowChange', lis1);
group2.on('rowChange', lis2);
group1.on('rowNotify', nlis1);
group2.on('rowNotify', nlis2);
src.trigger('rowChange', 'add', ["d", "e", "f"]);
assert.deepEqual(lis1.args, [['add', ["d"]]]);
assert.deepEqual(lis2.args, []);
src.trigger('rowNotify', ["a", "e"], "foo");
src.trigger('rowNotify', rowset.ALL, "bar");
assert.deepEqual(nlis1.args, [[["a"], "foo"], [rowset.ALL, "bar"]]);
assert.deepEqual(nlis2.args, [[rowset.ALL, "bar"]]);
lis1.resetHistory();
lis2.resetHistory();
src.trigger('rowChange', 'remove', ["a", "b", "d", "e"]);
assert.deepEqual(lis1.args, [['remove', ["a", "d"]]]);
assert.deepEqual(lis2.args, [['remove', ["b"]]]);
assert.deepEqual(Array.from(group1.getAllRows()), []);
assert.deepEqual(Array.from(group2.getAllRows()), ["c"]);
assert.deepEqual(Array.from(grouping.getGroup(3).getAllRows()), ["f"]);
});
it('should translate updates to adds or removes if needed', function() {
var src = rowset.RowSource.create(null);
src.getAllRows = function() { return ["a", "b", "c", "d", "e"]; };
var groups = {a: 1, b: 2, c: 2, d: 1, e: 3, f: 3};
var grouping = rowset.RowGrouping.create(null, function(r) { return groups[r]; });
var group1 = grouping.getGroup(1), group2 = grouping.getGroup(2);
grouping.subscribeTo(src);
assert.deepEqual(Array.from(group1.getAllRows()), ["a", "d"]);
assert.deepEqual(Array.from(group2.getAllRows()), ["b", "c"]);
var lis1 = sinon.spy(), lis2 = sinon.spy();
group1.on('rowChange', lis1);
group2.on('rowChange', lis2);
_.extend(groups, {a: 2, b: 3, e: 1});
src.trigger('rowChange', 'update', ["a", "b", "d", "e"]);
assert.deepEqual(lis1.args, [['remove', ['a']], ['update', ['d']], ['add', ['e']]]);
assert.deepEqual(lis2.args, [['remove', ['b']], ['add', ['a']]]);
lis1.resetHistory();
lis2.resetHistory();
src.trigger('rowChange', 'update', ["a", "b", "d", "e"]);
assert.deepEqual(lis1.args, [['update', ['d', 'e']]]);
assert.deepEqual(lis2.args, [['update', ['a']]]);
});
});
describe('SortedRowSet', function() {
var src, order, sortedSet, sortedArray;
beforeEach(function() {
src = rowset.RowSource.create(null);
src.getAllRows = function() { return ["a", "b", "c", "d", "e"]; };
order = {a: 4, b: 0, c: 1, d: 2, e: 3};
sortedSet = rowset.SortedRowSet.create(null, function(a, b) { return order[a] - order[b]; });
sortedArray = sortedSet.getKoArray();
});
it('should sort on first subscribe', function() {
assert.deepEqual(sortedArray.peek(), []);
sortedSet.subscribeTo(src);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
});
it('should maintain sort on adds and removes', function() {
sortedSet.subscribeTo(src);
var lis = sinon.spy();
sortedArray.subscribe(lis, null, 'spliceChange');
_.extend(order, {p: 2.5, q: 3.5});
// Small changes (currently < 2 elements) trigger individual splice events.
src.trigger('rowChange', 'add', ['p', 'q']);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "p", "e", "q", "a"]);
assert.equal(lis.callCount, 2);
assert.equal(lis.args[0][0].added, 1);
assert.equal(lis.args[1][0].added, 1);
lis.resetHistory();
src.trigger('rowChange', 'remove', ["a", "c"]);
assert.deepEqual(sortedArray.peek(), ["b", "d", "p", "e", "q"]);
assert.equal(lis.callCount, 2);
assert.deepEqual(lis.args[0][0].deleted, ["a"]);
assert.deepEqual(lis.args[1][0].deleted, ["c"]);
// Bigger changes trigger full array reassignment.
lis.resetHistory();
src.trigger('rowChange', 'remove', ['d', 'e', 'q']);
assert.deepEqual(sortedArray.peek(), ["b", "p"]);
assert.equal(lis.callCount, 1);
lis.resetHistory();
src.trigger('rowChange', 'add', ['a', 'c', 'd', 'e', 'q']);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "p", "e", "q", "a"]);
assert.equal(lis.callCount, 1);
});
it('should maintain sort on updates', function() {
var lis = sinon.spy();
sortedArray.subscribe(lis, null, 'spliceChange');
sortedSet.subscribeTo(src);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
assert.equal(lis.callCount, 1);
assert.equal(lis.args[0][0].added, 5);
// Small changes (currently < 2 elements) trigger individual splice events.
lis.resetHistory();
_.extend(order, {"b": 1.5, "a": 2.5});
src.trigger('rowChange', 'update', ["b", "a"]);
assert.deepEqual(sortedArray.peek(), ["c", "b", "d", "a", "e"]);
assert.equal(lis.callCount, 4);
assert.deepEqual(lis.args[0][0].deleted, ["b"]);
assert.deepEqual(lis.args[1][0].deleted, ["a"]);
assert.deepEqual(lis.args[2][0].added, 1);
assert.deepEqual(lis.args[3][0].added, 1);
// Bigger changes trigger full array reassignment.
lis.resetHistory();
_.extend(order, {"b": 0, "a": 5, "c": 6});
src.trigger('rowChange', 'update', ["c", "b", "a"]);
assert.deepEqual(sortedArray.peek(), ["b", "d", "e", "a", "c"]);
assert.equal(lis.callCount, 1);
assert.deepEqual(lis.args[0][0].added, 5);
});
it('should not splice on irrelevant changes', function() {
var lis = sinon.spy();
sortedArray.subscribe(lis, null, 'spliceChange');
sortedSet.subscribeTo(src);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
// Changes that don't affect the order do not cause splices.
lis.resetHistory();
src.trigger('rowChange', 'update', ["d"]);
src.trigger('rowChange', 'update', ["a", "b", "c"]);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
assert.equal(lis.callCount, 0);
});
it('should pass on rowNotify events', function() {
var lis = sinon.spy(), spy = sinon.spy();
sortedSet.subscribeTo(src);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
sortedArray.subscribe(lis, null, 'spliceChange');
sortedSet.on('rowNotify', spy);
src.trigger('rowNotify', ["b", "e"], "hello");
src.trigger('rowNotify', rowset.ALL, "world");
assert.equal(lis.callCount, 0);
assert.deepEqual(spy.args, [[['b', 'e'], 'hello'], [rowset.ALL, 'world']]);
});
it('should allow changing compareFunc', function() {
sortedSet.subscribeTo(src);
assert.deepEqual(sortedArray.peek(), ["b", "c", "d", "e", "a"]);
var lis = sinon.spy();
sortedArray.subscribe(lis, null, 'spliceChange');
// Replace the compare function with its negation.
sortedSet.updateSort(function(a, b) { return order[b] - order[a]; });
assert.equal(lis.callCount, 1);
assert.deepEqual(lis.args[0][0].added, 5);
assert.deepEqual(sortedArray.peek(), ["a", "e", "d", "c", "b"]);
});
it('should defer sorting while paused', function() {
var sortCalled = false;
assert.deepEqual(sortedArray.peek(), []);
sortedSet.updateSort(function(a, b) { sortCalled = true; return order[a] - order[b]; });
sortCalled = false;
var lis = sinon.spy();
sortedArray.subscribe(lis, null, 'spliceChange');
// Check that our little setup catching sort calls works; then reset.
sortedSet.subscribeTo(src);
assert.equal(sortCalled, true);
assert.equal(lis.callCount, 1);
sortedSet.unsubscribeFrom(src);
sortCalled = false;
lis.resetHistory();
// Now pause, do a bunch of operations, and check that sort has not been called.
function checkNoEffect() {
assert.equal(sortCalled, false);
assert.equal(lis.callCount, 0);
}
sortedSet.pause(true);
// Note that the initial order is ["b", "c", "d", "e", "a"]
sortedSet.subscribeTo(src);
checkNoEffect();
_.extend(order, {p: 2.5, q: 3.5});
src.trigger('rowChange', 'add', ['p', 'q']);
checkNoEffect(); // But we should now expect b,c,d,p,e,q,a
src.trigger('rowChange', 'remove', ["q", "c"]);
checkNoEffect(); // But we should now expect b,d,p,e,a
_.extend(order, {"b": 2.7, "a": 1});
src.trigger('rowChange', 'update', ["b", "a"]);
checkNoEffect(); // But we should now expect a,d,p,b,e
sortedSet.updateSort(function(a, b) { sortCalled = true; return order[b] - order[a]; });
checkNoEffect(); // We should expect a reversal: e,b,p,d,a
// rowNotify events should still be passed through.
var spy = sinon.spy();
sortedSet.on('rowNotify', spy);
src.trigger('rowNotify', ["p", "e"], "hello");
assert.deepEqual(spy.args[0], [['p', 'e'], 'hello']);
checkNoEffect();
// Now unpause, check that things get updated, and that the result is correct.
sortedSet.pause(false);
assert.equal(sortCalled, true);
assert.equal(lis.callCount, 1);
assert.deepEqual(sortedArray.peek(), ["e", "b", "p", "d", "a"]);
});
});
});