UNPKG

array-changes-async

Version:
477 lines (422 loc) 19.9 kB
/*global describe, it, setTimeout, Symbol*/ var arrayChanges = require('../lib/arrayChanges'); var expect = require('unexpected').clone() .use(require('unexpected-check')); var generators = require('chance-generators'); function toArguments() { return arguments; } function promiseArrayChanges(actual, expected, equal, similar, includeNonNumericalProperties) { var isCalled = 0; return expect.promise(function (resolve, reject) { arrayChanges(actual, expected, equal, similar, includeNonNumericalProperties, function (itemDiff) { isCalled += 1; var stack = ''; // Grab the stack now, in case the callback is called more than once try { throw new Error(); } catch (e) { stack = e.stack; } setTimeout(function () { if (isCalled !== 1) { return reject('callback called more than once' + stack); } resolve(itemDiff); }); }); }); } function executeDiff(changes) { var result = []; changes.forEach(function (item) { switch (item.type) { case 'moveTarget': case 'insert': result.push(item.value); break; case 'equal': case 'similar': if (typeof item.expected === 'number') { result.push(item.expected); } break; } }); return result; } expect.addAssertion('<array> when diffed with <array> <assertion>', function (expect, actual, expected) { expect.errorMode = 'nested'; return promiseArrayChanges(actual, expected, function (a, b, aIndex, bIndex, callback) { return callback(a === b); }).then(function (diff) { return expect.shift(diff); }); }); expect.addAssertion('<array> when executing the diff <assertion>', function (expect, diff) { expect.errorMode = 'nested'; return expect.shift(executeDiff(diff)); }); describe('array-changes-async', function () { it('returns an empty change-list when the two arrays are both empty', function () { return expect(promiseArrayChanges([], [], function (a, b, callback) { return callback(a === b); }), 'when fulfilled', 'to equal', []); }); it('returns a change-list with no changes if the arrays are the same', function () { return expect(promiseArrayChanges([0, 1, 2, 3], [0, 1, 2, 3], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 0, expected: 0 }, { type: 'equal', value: 1, expected: 1 }, { type: 'equal', value: 2, expected: 2 }, { type: 'equal', value: 3, expected: 3, last: true } ]); }); it('should indicate item removals', function () { return expect(promiseArrayChanges([0, 1, 2, 3], [0, 1, 3], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 0, expected: 0 }, { type: 'equal', value: 1, expected: 1 }, { type: 'remove', value: 2 }, { type: 'equal', value: 3, expected: 3, last: true } ]); }); it('should indicate item removals at the end', function () { return expect(promiseArrayChanges([0], [], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to equal', [ { type: 'remove', value: 0, last: true } ]); }); it('should indicate missing items', function () { return expect(promiseArrayChanges([0, 1, 3], [0, 1, 2, 3], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 0, expected: 0 }, { type: 'equal', value: 1, expected: 1 }, { type: 'insert', value: 2 }, { type: 'equal', value: 3, last: true, expected: 3 } ]); }); it('should indicate a missing item at the end', function () { return expect(promiseArrayChanges([], [0], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'insert', value: 0, last: true } ]); }); it('should indicate moved items with two items', function () { return expect(promiseArrayChanges([1, 2, 3, 0], [0, 1, 2, 3], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'moveTarget', value: 0, last: false, actualIndex: 3 }, { type: 'equal', value: 1, expected: 1, actualIndex: 0, expectedIndex: 1 }, { type: 'equal', value: 2, expected: 2, actualIndex: 1, expectedIndex: 2 }, { type: 'equal', value: 3, expected: 3, actualIndex: 2, expectedIndex: 3 }, { type: 'moveSource', value: 0, actualIndex: 3, last: true } ]); }); it('shows items that are not equal but should be compared against each other as similar', function () { return expect(promiseArrayChanges([0, 1, 2, 3], [0, 2, 1, 3], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(true); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 0, expected: 0 }, { type: 'similar', value: 1, expected: 2 }, { type: 'similar', value: 2, expected: 1 }, { type: 'equal', value: 3, expected: 3, last: true } ] ); }); it('allows you to indicate which items should be considered similar', function () { return expect(promiseArrayChanges([ { type: 'dog', name: 'Fido' }, { type: 'dog', name: 'Teddy' }, { type: 'person', name: 'Sune' }, { type: 'dog', name: 'Charlie' }, { type: 'dog', name: 'Sam' } ], [ { type: 'dog', name: 'Fido' }, { type: 'dog', name: 'Teddy' }, { type: 'dog', name: 'Murphy' }, { type: 'person', name: 'Andreas' }, { type: 'dog', name: 'Charlie' }, { type: 'dog', name: 'Sam' } ], function (a, b, aIndex, bIndex, callback) { return callback(a.type === b.type && a.name === b.name); }, function (a, b, aIndex, bIndex, callback) { return callback(a.type === b.type); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: { type: 'dog', name: 'Fido' }, expected: { type: 'dog', name: 'Fido' } }, { type: 'equal', value: { type: 'dog', name: 'Teddy' }, expected: { type: 'dog', name: 'Teddy' } }, { type: 'insert', value: { type: 'dog', name: 'Murphy' } }, { type: 'similar', value: { type: 'person', name: 'Sune' }, expected: { type: 'person', name: 'Andreas' } }, { type: 'equal', value: { type: 'dog', name: 'Charlie' }, expected: { type: 'dog', name: 'Charlie' } }, { type: 'equal', value: { type: 'dog', name: 'Sam' }, last: true, expected: { type: 'dog', name: 'Sam' } } ]); }); it('supports diffing array-like objects', function () { return expect(promiseArrayChanges(toArguments(1, 2, 5), toArguments(3, 4), function (a, b, aIndex, bIndex, callback) { return callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'insert', value: toArguments( 3, 4 ) }, { type: 'remove', value: 1 }, { type: 'similar', value: 2, expected: 4 }, { type: 'similar', value: 5, last: true, expected: undefined } ]); }); it('supports diffing spotting non-similar objects', function () { return expect(promiseArrayChanges([ { name: 'steve' }, { name: 'monica' }, { name: 'sam' } ], [ { name: 'andreas' }, { name: 'sune' }, { name: 'peter' } ], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }, function (a, b, aIndex, bIndex, callback) { return callback(false); // nothing is similar }), 'when fulfilled', 'to satisfy', [ { type: 'similar', value: { name: 'steve' } }, // This feels wrong { type: 'similar', value: { name: 'monica' } }, { type: 'similar', value: { name: 'sam' } } ]); }); it('supports diffing non-matching types', function () { // This test is specifically targeted to exercise the "similar, but not object or string" // part of the algorithm. I believe the type checks should come out of array-changes, and // go back to unexpected (where they came from), as they don't make sense here // What is equal, and what is similar should be a pure decision of the functions passed in, // and nothing to do with the types of the values. // UPDATE: The "similar, but not object or string" checks have been removed, this test still works :) return expect(promiseArrayChanges([ true, false, true ], [ { name: 'steve' }, { name: 'monica' }, { name: 'sam' } ], function (a, b, aIndex, bIndex, callback) { return callback(a === b); }, function (a, b, aIndex, bIndex, callback) { return callback(true); // everything is similar }), 'when fulfilled', 'to satisfy', [ { type: 'similar', value: true }, { type: 'similar', value: false }, { type: 'similar', value: true } ]); }); it('matches the example in the readme', function () { return expect(promiseArrayChanges([ 1, 2, 4 ], [ 1, 2, 3, 4 ], function (a, b, aIndex, bIndex, callback) { callback(a === b); }, null), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 1 }, { type: 'equal', value: 2 }, { type: 'insert', value: 3 }, { type: 'equal', value: 4 } ]); }); it('marks items as similar only when the similar function reports they are similar', function () { return expect(promiseArrayChanges( [ 1, 2.4, /* missing*/ 4 ], [ 1, 2, 3, 4.7 ], function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { if (Math.floor(a) === Math.floor(b)) { return callback(true); } return callback(false); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', value: 1 }, { type: 'similar', value: 2.4, expected: 2 }, { type: 'insert', value: 3 }, { type: 'similar', value: 4, expected: 4.7 } ]); }); it('does not provide the value from the expected if the value moves', function () { // If the `similar` callback returns false when `equal` returns true, the algorithm // gets the wrong answer, and includes the expected value as `value` in the result // This isn't a problem with the algorithm, as an equal value must be similar, // but this test just checks that for a move operation, we don't get values from the // "wrong" side return expect(promiseArrayChanges( [ 4, 2, 3, 1 ], [ 100, 200, 300, 400 ], function (a, b, aIndex, bIndex, callback){ return callback(a * 100 === b); }, function (a, b, aIndex, bIndex, callback) { return callback(a * 100 === b); }), 'when fulfilled', 'to satisfy', [ { type: 'moveTarget', actualIndex: 3, value: 1, last: false }, { type: 'moveTarget', actualIndex: 1, value: 2, last: false }, { type: 'moveTarget', actualIndex: 2, value: 3, last: false }, { type: 'equal', actualIndex: 0, value: 4, expected: 400, expectedIndex: 3 }, { type: 'moveSource', actualIndex: 1, value: 2 }, { type: 'moveSource', actualIndex: 2, value: 3 }, { type: 'moveSource', actualIndex: 3, value: 1, last: true } ]); }); describe('when including non-numerical properties', function () { it('returns an empty change-list with an undefined key on the LHS', function () { var a = []; a.nothing = undefined; var b = []; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, undefined, true), 'when fulfilled', 'to equal', []); }); it('returns an empty change-list with an undefined key on the RHS', function () { var a = []; var b = []; b.nothing = undefined; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, undefined, true), 'when fulfilled', 'to equal', []); }); it('returns an empty change-list with undefined keys on both the LHS and RHS', function () { var a = []; a.nothing = undefined; var b = []; b.nothing = undefined; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, undefined, true), 'when fulfilled', 'to equal', []); }); it('returns a change-list containing remove when a LHS key is undefined on the RHS', function () { var a = []; a.nothing = true; var b = []; b.nothing = undefined; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, undefined, true), 'when fulfilled', 'to equal', [ { type: 'remove', actualIndex: 'nothing', value: true, expected: undefined, last: true } ]); }); it('returns a change-list containing similar when a RHS key is undefined on the LHS', function () { var a = []; a.nothing = undefined; var b = []; b.nothing = true; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, undefined, true), 'when fulfilled', 'to equal', [ { type: 'similar', expectedIndex: 'nothing', actualIndex: 'nothing', value: undefined, expected: true, last: true } ]); }); it('should diff arrays that have non-numerical property names', function () { var a = [1, 2, 3]; a.foo = 123; a.bar = 456; a.quux = {}; var b = [1, 2, 3]; b.bar = 456; b.baz = 789; b.quux = false; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, true), 'when fulfilled', 'to equal', [ { type: 'equal', value: 1, expected: 1, actualIndex: 0, expectedIndex: 0 }, { type: 'equal', value: 2, expected: 2, actualIndex: 1, expectedIndex: 1 }, { type: 'equal', value: 3, expected: 3, actualIndex: 2, expectedIndex: 2 }, { type: 'remove', value: 123, actualIndex: 'foo' }, { type: 'equal', value: 456, expected: 456, actualIndex: 'bar', expectedIndex: 'bar' }, { type: 'similar', value: {}, expected: false, actualIndex: 'quux', expectedIndex: 'quux' }, { type: 'insert', value: 789, expectedIndex: 'baz', last: true } ]); }); it('should support an array of specific non-numerical keys to diff', function () { var a = [1]; a.foo = 123; a.bar = 789; var b = [1]; a.foo = 456; a.bar = false; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, [ 'foo' ]), 'when fulfilled', 'to equal', [ { type: 'equal', actualIndex: 0, expectedIndex: 0, value: 1, expected: 1 }, { type: 'remove', actualIndex: 'foo', value: 456, last: true } ]); }); }); it('should report the expectedIndex of equal elements when arrays are identical', function () { var a = [ 1, 2 ]; var b = [ 1, 2 ]; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'equal', actualIndex: 0, expectedIndex: 0, value: 1, expected: 1 }, { type: 'equal', actualIndex: 1, expectedIndex: 1, value: 2, expected: 2 } ] ); }); it('should report the expectedIndex of equal elements when items are offset', function () { var a = [ 1, 2 ]; var b = [ 0, 1, 2 ]; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(a === b); }), 'when fulfilled', 'to satisfy', [ { type: 'insert', value: 0 }, { type: 'equal', actualIndex: 0, expectedIndex: 1, value: 1, expected: 1 }, { type: 'equal', actualIndex: 1, expectedIndex: 2, value: 2, expected: 2 } ] ); }); if (typeof Symbol !== 'undefined') { it('should diff arrays that have Symbol property names', function () { var aSymbol = Symbol('a'); var bSymbol = Symbol('b'); var a = [1, 2]; a[aSymbol] = 123; var b = [1, 2]; b[bSymbol] = 456; return expect(promiseArrayChanges(a, b, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, function (a, b, aIndex, bIndex, callback) { callback(a === b); }, true), 'when fulfilled', 'to equal', [ { type: 'equal', value: 1, expected: 1, actualIndex: 0, expectedIndex: 0 }, { type: 'equal', value: 2, expected: 2, actualIndex: 1, expectedIndex: 1 }, { type: 'remove', value: 123, actualIndex: aSymbol }, { type: 'insert', value: 456, expectedIndex: bSymbol, last: true } ]); }); } it('produces a valid plan', function () { var g = generators(42); var arrays = g.array(g.natural({ max: 10 }), g.natural({ max: 10 })); return expect(function (actual, expected) { return expect( actual, 'when diffed with', expected, 'when executing the diff', 'to equal', expected ); }, 'to be valid for all', arrays, arrays); }); });