gristlabs_grist-core/test/client/models/rowset.js
Paul Fitzpatrick bcbf57d590 (core) bump mocha version to allow parallel tests; move more tests to core
Summary:
This uses a newer version of mocha in grist-core so that tests can be run in parallel. That allows more tests to be moved without slowing things down overall. Tests moved are venerable browser tests; only the ones that "just work" or worked without too much trouble to are moved, in order to keep the diff from growing too large. Will wrestle with more in follow up.

Parallelism is at the file level, rather than the individual test.

The newer version of mocha isn't needed for grist-saas repo; tests are parallelized in our internal CI by other means. I've chosen to allocate files to workers in a cruder way than our internal CI, based on initial characters rather than an automated process. The automated process would need some reworking to be compatible with mocha running in parallel mode.

Test Plan: this diff was tested first on grist-core, then ported to grist-saas so saas repo history will correctly track history of moved files.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3927
2023-06-27 02:55:34 -04:00

429 lines
17 KiB
JavaScript

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"]);
});
});
});