graph-crdt
Version:
A Delta State Graph CRDT variant
443 lines (323 loc) • 13.3 kB
JavaScript
;
var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray');
var _slicedToArray3 = _interopRequireDefault(_slicedToArray2);
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _stringify = require('babel-runtime/core-js/json/stringify');
var _stringify2 = _interopRequireDefault(_stringify);
var _expect = require('expect');
var _expect2 = _interopRequireDefault(_expect);
var _index = require('./index');
var _index2 = _interopRequireDefault(_index);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/* eslint-env mocha */
describe('Node static method', function () {
describe('"from"', function () {
it('turns a POJO into a node', function () {
var node = _index2.default.from({});
(0, _expect2.default)(node).toBeA(_index2.default);
});
it('imports the properties from the target object', function () {
var date = new Date('1 Jan 1980');
var node = _index2.default.from({
name: 'Sam',
birthday: date
});
(0, _expect2.default)(node.value('name')).toBe('Sam');
(0, _expect2.default)(node.value('birthday')).toBe(date);
});
it('provides an initial state', function () {
var node = _index2.default.from({ name: 'Alvin' });
(0, _expect2.default)(node.state('name')).toBe(1);
});
});
describe('"source"', function () {
it('creates a node that draws from an object', function () {
var node = _index2.default.create();
node.merge({ data: 'intact' });
var string = (0, _stringify2.default)(node);
var object = JSON.parse(string);
var copy = _index2.default.source(object);
(0, _expect2.default)(copy.value('data')).toBe('intact');
});
});
});
describe('Node', function () {
var node = void 0;
beforeEach(function () {
node = _index2.default.create();
});
/* Generic tests */
it('does not have properties upon creation', function () {
var keys = [].concat((0, _toConsumableArray3.default)(node)).map(function (_ref) {
var _ref2 = (0, _slicedToArray3.default)(_ref, 1),
key = _ref2[0];
return key;
});
(0, _expect2.default)(keys.length).toBe(0);
});
it('returns the uid when `toString` is called', function () {
var result = node.toString();
var _node$meta = node.meta(),
uid = _node$meta.uid;
(0, _expect2.default)(result).toBe(uid);
});
it('returns the node when `toJSON` is called', function () {
node.merge({ 'json worked': true });
var result = (0, _stringify2.default)(node);
(0, _expect2.default)(result).toContain('json worked');
});
/* Not so generic tests */
describe('uid', function () {
it('exists on creation', function () {
var _node$meta2 = node.meta(),
uid = _node$meta2.uid;
(0, _expect2.default)(uid).toNotBe(undefined);
});
it('is unique', function () {
var _Node$create$meta = _index2.default.create().meta(),
uid1 = _Node$create$meta.uid;
var _Node$create$meta2 = _index2.default.create().meta(),
uid2 = _Node$create$meta2.uid;
(0, _expect2.default)(uid1).toNotBe(uid2);
});
it('uses the configuration provided', function () {
var node = _index2.default.create({ uid: 'custom' });
var _node$meta3 = node.meta(),
uid = _node$meta3.uid;
(0, _expect2.default)(uid).toBe('custom');
});
});
describe('state()', function () {
it('returns 0 when there is no property', function () {
var state = node.state('no such key');
(0, _expect2.default)(state).toBe(0);
});
});
describe('meta()', function () {
it('returns null if no values exist', function () {
var result = node.meta('no such key');
(0, _expect2.default)(result).toBe(null);
});
it('returns the metadata if the field exists', function () {
node.merge({ name: 'Steve' });
var result = node.meta('name');
(0, _expect2.default)(result).toBeAn(Object);
});
});
describe('Symbol.iterator()', function () {
it('skips the node metadata field', function () {
node.merge({ name: 'John' });
var keys = [].concat((0, _toConsumableArray3.default)(node)).map(function (_ref3) {
var _ref4 = (0, _slicedToArray3.default)(_ref3, 1),
key = _ref4[0];
return key;
});
// Should not contain '@object'.
(0, _expect2.default)(keys).toEqual(['name']);
});
it('includes key/value pairs for every field', function () {
node.merge({
name: 'Stewart',
tier: 'premium'
});
var pairs = [].concat((0, _toConsumableArray3.default)(node));
(0, _expect2.default)(pairs).toEqual([['name', 'Stewart'], ['tier', 'premium']]);
});
});
describe('value()', function () {
it('returns undefined if the key cannot be found', function () {
var result = node.value('no such key');
(0, _expect2.default)(result).toBe(undefined);
});
it('returns undefined if called on reserved fields', function () {
// Please, never do this in your code.
node.meta().value = 'failure!';
var result = node.value('@object');
(0, _expect2.default)(result).toBe(undefined);
});
});
describe('new()', function () {
it('creates a node with the same ID', function () {
var _node$meta4 = node.meta(),
uid = _node$meta4.uid;
var copy = node.new();
(0, _expect2.default)(copy.meta()).toContain({ uid: uid });
});
it('does not carry over any properties', function () {
node.merge({ original: true });
var copy = node.new();
(0, _expect2.default)(copy.snapshot()).toEqual({});
});
});
describe('setMetadata()', function () {
it('updates the field metadata', function () {
var update = {
value: 'jack',
lastChanged: 1491531671735
};
node.setMetadata('username', update);
(0, _expect2.default)(node.meta('username')).toContain(update);
});
it('sets the state', function () {
var update = {
value: 6.28,
logs: 'address'
};
node.setMetadata('pressure', update);
(0, _expect2.default)(node.state('pressure')).toBe(1);
});
it('bumps the state if the field existed before', function () {
node.merge({ 'temp': 31 });
var update = {
value: 30,
encoding: 'KELVIN'
};
node.setMetadata('temp', update);
(0, _expect2.default)(node.state('temp')).toBe(2);
});
it('ignores state if specified in the metadata', function () {
node.setMetadata('dangerous', { state: 6 });
(0, _expect2.default)(node.state('dangerous')).toBe(1);
});
it('does not mutate the metadata object', function () {
var update = { state: 5 };
node.setMetadata('field', update);
(0, _expect2.default)(update.state).toBe(5);
(0, _expect2.default)(node.meta('field')).toNotBe(update);
});
it('returns the merge delta', function () {
var result = node.setMetadata('speed', { value: 150 });
(0, _expect2.default)(result.update).toBeA(_index2.default);
(0, _expect2.default)(result.history).toBeA(_index2.default);
});
it('emits "update"', function () {
var spy = (0, _expect.createSpy)();
node.on('update', spy);
node.setMetadata('field', { value: 'update!' });
(0, _expect2.default)(spy).toHaveBeenCalled();
var _spy$calls$0$argument = (0, _slicedToArray3.default)(spy.calls[0].arguments, 1),
update = _spy$calls$0$argument[0];
(0, _expect2.default)(update).toBeA(_index2.default);
(0, _expect2.default)(update.meta().uid).toBe(node.meta().uid);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(update))).toEqual([['field', 'update!']]);
});
it('emits "history"', function () {
var spy = (0, _expect.createSpy)();
node.on('history', spy);
node.merge({ field: 'history' });
node.setMetadata('field', 'update');
(0, _expect2.default)(spy).toHaveBeenCalled();
var _spy$calls$0$argument2 = (0, _slicedToArray3.default)(spy.calls[0].arguments, 1),
update = _spy$calls$0$argument2[0];
(0, _expect2.default)(update).toBeA(_index2.default);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(update))).toEqual([['field', 'history']]);
});
});
describe('rebase()', function () {
var target = void 0;
beforeEach(function () {
target = new _index2.default();
});
it('returns a new node', function () {
var result = node.rebase();
(0, _expect2.default)(result).toBeA(_index2.default);
(0, _expect2.default)(result).toNotBe(node);
});
it('returns the same node id', function () {
var _node$rebase$meta = node.rebase(target).meta(),
uid = _node$rebase$meta.uid;
(0, _expect2.default)(uid).toBe(node.meta().uid);
});
it('does not change state if the node is empty', function () {
node.merge({ initial: true });
var result = node.rebase(target);
(0, _expect2.default)(result.state('initial')).toBe(1);
});
it('does not change state if the fields do not overlap', function () {
node.merge({ initial: true });
target.merge({ different: true });
target.meta('different').state = 2001;
var result = node.rebase(target);
(0, _expect2.default)(result.state('initial')).toBe(1);
});
it('increments the state for overlapping fields', function () {
node.merge({ existing: 'should replace' });
target.merge({ existing: 'replace me', different: true });
var result = node.rebase(target);
(0, _expect2.default)(result.state('existing')).toBe(2);
(0, _expect2.default)(result.state('different')).toBe(1);
});
it('does not mutate field metadata', function () {
node.merge({ old: false });
target.merge({ old: true });
var result = node.rebase(target);
(0, _expect2.default)(result.meta('old')).toNotBe(node.meta('old')).toNotBe(target.meta('old')).toNotContain({ state: 1 });
});
it('does not change fields which are already greater', function () {
var state = 20000;
node.merge({ old: false });
node.meta('old').state = state;
target.merge({ old: true });
var result = node.rebase(target);
(0, _expect2.default)(result.state('old')).toBe(state);
});
});
describe('overlap()', function () {
var target = void 0;
beforeEach(function () {
target = new _index2.default();
});
it('returns a new node', function () {
var result = node.overlap(target);
(0, _expect2.default)(result).toBeA(_index2.default);
(0, _expect2.default)(result).toNotBe(node);
(0, _expect2.default)(result).toNotBe(target);
});
it('has the same id', function () {
var _node$overlap$meta = node.overlap(target).meta(),
uid = _node$overlap$meta.uid;
(0, _expect2.default)(uid).toBe(node.meta().uid);
});
it('has no fields if the target is empty', function () {
node.merge({ existing: true });
var result = node.overlap(target);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(result))).toEqual([]);
});
it('ignores additional properties in the target', function () {
node.merge({ existing: true });
target.merge({ existing: true, different: true });
var result = node.overlap(target);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(result))).toEqual([].concat((0, _toConsumableArray3.default)(node)));
});
it('ignores additional properties in the source', function () {
node.merge({ shared: true, extra: true });
target.merge({ shared: true });
var result = node.overlap(target);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(result))).toEqual([].concat((0, _toConsumableArray3.default)(target)));
});
it('is identical when compared against itself', function () {
node.merge({ exhausted: 'field name creativity' });
var result = node.overlap(node);
(0, _expect2.default)([].concat((0, _toConsumableArray3.default)(result))).toEqual([].concat((0, _toConsumableArray3.default)(node)));
});
it('contains the values from the source node', function () {
node.merge({ shared: 'node value' });
target.merge({ shared: 'target value' });
var result = node.overlap(target);
(0, _expect2.default)(result.meta('shared')).toBe(node.meta('shared'));
});
});
describe('snapshot()', function () {
it('returns an object', function () {
var result = node.snapshot();
(0, _expect2.default)(result).toBeAn(Object);
});
it('contains every key/value pair in the node', function () {
var state = { name: 'Living Room Lamp', state: 'ON' };
node.merge(state);
var result = node.snapshot();
(0, _expect2.default)(result).toEqual(state);
});
});
});