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