diff --git a/.gitignore b/.gitignore index 691378f..3344b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.swp *.DS_Store node_modules +package-lock.json diff --git a/README.md b/README.md index cc1de54..6314a10 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ The JSON OT type can be used to edit arbitrary JSON documents. +It has been forked from https://github.com/ottypes/json0 and modified to add Presence. + +## Presence + +(inspired by https://github.com/Teamwork/ot-rich-text#presence) + +The shape of our presence data is as follows: + +``` +{ + u: '123', // user ID + c: 8, // number of changes made by this user + s: [ // list of selections + [ 1, 1 ], // collapsed selection + [ 5, 7 ], // forward selection + [ 9, 4 ] // backward selection + ] +} +``` + +Each selection listed in `s` ends with a 2-element array containing the selection start index and the selection end index. The elements in the array preceeding the last two represent the path of a `text0` entry within the `json0` data structure. + +For example, the following entry in the `s` array represents the user's cursor position within the `content` field (`data.content`): + +``` +['content', 2, 2] +``` + +We can access deeply nested entries with this structure as well. For example, the following `s` entry represents a text selection in `data.files[3].text`: + +``` +['files', 3, 'text', 4, 7] +``` + +The rest of the README content is from the original repo https://github.com/ottypes/json0. + ## Features The JSON OT type supports the following operations: diff --git a/lib/json0.js b/lib/json0.js index dc3a405..4b562e7 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -133,6 +133,63 @@ function convertToText(c) { delete c.o; } +// not checking anything here, we should probably check that u: exists +// (only thing we care about at json0 top level), and then delegate +// to any subtypes if there is already subtype presence data +json.createPresence = function(presence) { + return presence; +}; + +// this needs more thinking/testing, looking a bit more carefully at +// how this is implemented in ot-rich-text, etc. +json.comparePresence = function(pres1, pres2) { + if (!pres1 || !pres2) { + return false; + } + if (!pres1.p || !pres2.p) { + return false; + } + if (pres1.t !== pres2.t) { + return false; + } + if (pres1.t && subtypes[pres1.t]) { + if (pres1.p[0] === pres2.p[0]) { + return subtypes[pres1.t].comparePresence(pres1, pres2); + } + } else return pres1 === pres2; +}; + +// this is the key function, always run client-side, both on +// the client that creates a text-change, and on the clients +// that receive text-changes (ops). if there are no ops, just +// return presence, if there are ops, delegate to the subtype +// responsible for those ops (currently only ot-rich-text). +// I am making assumptions many places that all ops will be +// of the same subtype, not sure if this is a given. +// We're only concerned about the first level of object/array, +// not sure if the spec allows nesting of subtypes. +json.transformPresence = function(presence, op, isOwn) { + if (op.length < 1) { + return presence; + } + const representativeOp = op[0]; + const opType = op[0].t; + const path = representativeOp.p && representativeOp.p[0] + if (opType && subtypes[opType] && path) { + if (!presence.p || !presence.p[0] || presence.p[0] !== path) { + return presence + } + // return result of running the subtype's transformPresence, + // but add path and type, which the subtype will not include + presence = { + ...subtypes[opType].transformPresence(presence, op, isOwn), + p: op[0].p, + t: op[0].t + }; + } + return presence; +}; + json.apply = function(snapshot, op) { json.checkValidOp(op); diff --git a/package.json b/package.json index b6c9df6..d2331ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ot-json0", - "version": "1.1.0", + "name": "@houshuang/ot-json0", + "version": "1.2.0", "description": "JSON OT type", "main": "lib/index.js", "directories": { @@ -9,15 +9,14 @@ "dependencies": {}, "devDependencies": { "ot-fuzzer": "^1.0.0", - "mocha": "^1.20.1", - "coffee-script": "^1.7.1" + "mocha": "^1.20.1" }, "scripts": { "test": "mocha" }, "repository": { "type": "git", - "url": "git://github.com/ottypes/json0" + "url": "git://github.com/houshuang/json0" }, "keywords": [ "ot", diff --git a/test/json0-generator.coffee b/test/json0-generator.coffee deleted file mode 100644 index 7ca5734..0000000 --- a/test/json0-generator.coffee +++ /dev/null @@ -1,176 +0,0 @@ -json0 = require '../lib/json0' -{randomInt, randomReal, randomWord} = require 'ot-fuzzer' - -# This is an awful function to clone a document snapshot for use by the random -# op generator. .. Since we don't want to corrupt the original object with -# the changes the op generator will make. -clone = (o) -> JSON.parse(JSON.stringify(o)) - -randomKey = (obj) -> - if Array.isArray(obj) - if obj.length == 0 - undefined - else - randomInt obj.length - else - count = 0 - - for key of obj - result = key if randomReal() < 1/++count - result - -# Generate a random new key for a value in obj. -# obj must be an Object. -randomNewKey = (obj) -> - # There's no do-while loop in coffeescript. - key = randomWord() - key = randomWord() while obj[key] != undefined - key - -# Generate a random object -randomThing = -> - switch randomInt 6 - when 0 then null - when 1 then '' - when 2 then randomWord() - when 3 - obj = {} - obj[randomNewKey(obj)] = randomThing() for [1..randomInt(5)] - obj - when 4 then (randomThing() for [1..randomInt(5)]) - when 5 then randomInt(50) - -# Pick a random path to something in the object. -randomPath = (data) -> - path = [] - - while randomReal() > 0.85 and typeof data == 'object' - key = randomKey data - break unless key? - - path.push key - data = data[key] - - path - - -module.exports = genRandomOp = (data) -> - pct = 0.95 - - container = data: clone data - - op = while randomReal() < pct - pct *= 0.6 - - # Pick a random object in the document operate on. - path = randomPath(container['data']) - - # parent = the container for the operand. parent[key] contains the operand. - parent = container - key = 'data' - for p in path - parent = parent[key] - key = p - operand = parent[key] - - if randomReal() < 0.4 and parent != container and Array.isArray(parent) - # List move - newIndex = randomInt parent.length - - # Remove the element from its current position in the list - parent.splice key, 1 - # Insert it in the new position. - parent.splice newIndex, 0, operand - - {p:path, lm:newIndex} - - else if randomReal() < 0.3 or operand == null - # Replace - - newValue = randomThing() - parent[key] = newValue - - if Array.isArray(parent) - {p:path, ld:operand, li:clone(newValue)} - else - {p:path, od:operand, oi:clone(newValue)} - - else if typeof operand == 'string' - # String. This code is adapted from the text op generator. - - if randomReal() > 0.5 or operand.length == 0 - # Insert - pos = randomInt(operand.length + 1) - str = randomWord() + ' ' - - path.push pos - parent[key] = operand[...pos] + str + operand[pos..] - c = {p:path, si:str} - else - # Delete - pos = randomInt(operand.length) - length = Math.min(randomInt(4), operand.length - pos) - str = operand[pos...(pos + length)] - - path.push pos - parent[key] = operand[...pos] + operand[pos + length..] - c = {p:path, sd:str} - - if json0._testStringSubtype - # Subtype - subOp = {p:path.pop()} - if c.si? - subOp.i = c.si - else - subOp.d = c.sd - - c = {p:path, t:'text0', o:[subOp]} - - c - - else if typeof operand == 'number' - # Number - inc = randomInt(10) - 3 - parent[key] += inc - {p:path, na:inc} - - else if Array.isArray(operand) - # Array. Replace is covered above, so we'll just randomly insert or delete. - # This code looks remarkably similar to string insert, above. - - if randomReal() > 0.5 or operand.length == 0 - # Insert - pos = randomInt(operand.length + 1) - obj = randomThing() - - path.push pos - operand.splice pos, 0, obj - {p:path, li:clone(obj)} - else - # Delete - pos = randomInt operand.length - obj = operand[pos] - - path.push pos - operand.splice pos, 1 - {p:path, ld:clone(obj)} - else - # Object - k = randomKey(operand) - - if randomReal() > 0.5 or not k? - # Insert - k = randomNewKey(operand) - obj = randomThing() - - path.push k - operand[k] = obj - {p:path, oi:clone(obj)} - else - obj = operand[k] - - path.push k - delete operand[k] - {p:path, od:clone(obj)} - - [op, container.data] diff --git a/test/json0-generator.js b/test/json0-generator.js new file mode 100644 index 0000000..968ef2a --- /dev/null +++ b/test/json0-generator.js @@ -0,0 +1,234 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let genRandomOp; +const json0 = require('../lib/json0'); +const { randomInt, randomReal, randomWord } = require('ot-fuzzer'); + +// This is an awful function to clone a document snapshot for use by the random +// op generator. .. Since we don't want to corrupt the original object with +// the changes the op generator will make. +const clone = o => JSON.parse(JSON.stringify(o)); + +const randomKey = function(obj) { + if (Array.isArray(obj)) { + if (obj.length === 0) { + return undefined; + } else { + return randomInt(obj.length); + } + } else { + let result; + let count = 0; + + for (let key in obj) { + if (randomReal() < 1 / ++count) { + result = key; + } + } + return result; + } +}; + +// Generate a random new key for a value in obj. +// obj must be an Object. +const randomNewKey = function(obj) { + // There's no do-while loop in coffeescript. + let key = randomWord(); + while (obj[key] !== undefined) { + key = randomWord(); + } + return key; +}; + +// Generate a random object +var randomThing = function() { + switch (randomInt(6)) { + case 0: + return null; + case 1: + return ''; + case 2: + return randomWord(); + case 3: + var obj = {}; + for ( + let i = 1, end = randomInt(5), asc = 1 <= end; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + obj[randomNewKey(obj)] = randomThing(); + } + return obj; + case 4: + return __range__(1, randomInt(5), true).map(j => randomThing()); + case 5: + return randomInt(50); + } +}; + +// Pick a random path to something in the object. +const randomPath = function(data) { + const path = []; + + while (randomReal() > 0.85 && typeof data === 'object') { + const key = randomKey(data); + if (key == null) { + break; + } + + path.push(key); + data = data[key]; + } + + return path; +}; + +module.exports = genRandomOp = function(data) { + let pct = 0.95; + + const container = { data: clone(data) }; + + const op = (() => { + const result = []; + while (randomReal() < pct) { + var length, obj, p, pos; + pct *= 0.6; + + // Pick a random object in the document operate on. + const path = randomPath(container['data']); + + // parent = the container for the operand. parent[key] contains the operand. + let parent = container; + let key = 'data'; + for (p of Array.from(path)) { + parent = parent[key]; + key = p; + } + const operand = parent[key]; + + if (randomReal() < 0.4 && parent !== container && Array.isArray(parent)) { + // List move + const newIndex = randomInt(parent.length); + + // Remove the element from its current position in the list + parent.splice(key, 1); + // Insert it in the new position. + parent.splice(newIndex, 0, operand); + + result.push({ p: path, lm: newIndex }); + } else if (randomReal() < 0.3 || operand === null) { + // Replace + + const newValue = randomThing(); + parent[key] = newValue; + + if (Array.isArray(parent)) { + result.push({ p: path, ld: operand, li: clone(newValue) }); + } else { + result.push({ p: path, od: operand, oi: clone(newValue) }); + } + } else if (typeof operand === 'string') { + // String. This code is adapted from the text op generator. + + var c, str; + if (randomReal() > 0.5 || operand.length === 0) { + // Insert + pos = randomInt(operand.length + 1); + str = randomWord() + ' '; + + path.push(pos); + parent[key] = operand.slice(0, pos) + str + operand.slice(pos); + c = { p: path, si: str }; + } else { + // Delete + pos = randomInt(operand.length); + length = Math.min(randomInt(4), operand.length - pos); + str = operand.slice(pos, pos + length); + + path.push(pos); + parent[key] = operand.slice(0, pos) + operand.slice(pos + length); + c = { p: path, sd: str }; + } + + if (json0._testStringSubtype) { + // Subtype + const subOp = { p: path.pop() }; + if (c.si != null) { + subOp.i = c.si; + } else { + subOp.d = c.sd; + } + + c = { p: path, t: 'text0', o: [subOp] }; + } + + result.push(c); + } else if (typeof operand === 'number') { + // Number + const inc = randomInt(10) - 3; + parent[key] += inc; + result.push({ p: path, na: inc }); + } else if (Array.isArray(operand)) { + // Array. Replace is covered above, so we'll just randomly insert or delete. + // This code looks remarkably similar to string insert, above. + + if (randomReal() > 0.5 || operand.length === 0) { + // Insert + pos = randomInt(operand.length + 1); + obj = randomThing(); + + path.push(pos); + operand.splice(pos, 0, obj); + result.push({ p: path, li: clone(obj) }); + } else { + // Delete + pos = randomInt(operand.length); + obj = operand[pos]; + + path.push(pos); + operand.splice(pos, 1); + result.push({ p: path, ld: clone(obj) }); + } + } else { + // Object + let k = randomKey(operand); + + if (randomReal() > 0.5 || k == null) { + // Insert + k = randomNewKey(operand); + obj = randomThing(); + + path.push(k); + operand[k] = obj; + result.push({ p: path, oi: clone(obj) }); + } else { + obj = operand[k]; + + path.push(k); + delete operand[k]; + result.push({ p: path, od: clone(obj) }); + } + } + } + return result; + })(); + + return [op, container.data]; +}; + +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} diff --git a/test/json0.coffee b/test/json0.coffee deleted file mode 100644 index 531f76e..0000000 --- a/test/json0.coffee +++ /dev/null @@ -1,405 +0,0 @@ -# Tests for JSON OT type. - -assert = require 'assert' -nativetype = require '../lib/json0' - -fuzzer = require 'ot-fuzzer' - -nativetype.registerSubtype - name: 'mock' - transform: (a, b, side) -> - return { mock: true } - -# Cross-transform helper function. Transform server by client and client by -# server. Returns [server, client]. -transformX = (type, left, right) -> - [type.transform(left, right, 'left'), type.transform(right, left, 'right')] - -genTests = (type) -> - # The random op tester above will test that the OT functions are admissable, - # but debugging problems it detects is a pain. - # - # These tests should pick up *most* problems with a normal JSON OT - # implementation. - - describe 'sanity', -> - describe '#create()', -> it 'returns null', -> - assert.deepEqual type.create(), null - - describe '#compose()', -> - it 'od,oi --> od+oi', -> - assert.deepEqual [{p:['foo'], od:1, oi:2}], type.compose [{p:['foo'],od:1}],[{p:['foo'],oi:2}] - assert.deepEqual [{p:['foo'], od:1},{p:['bar'], oi:2}], type.compose [{p:['foo'],od:1}],[{p:['bar'],oi:2}] - it 'merges od+oi, od+oi -> od+oi', -> - assert.deepEqual [{p:['foo'], od:1, oi:2}], type.compose [{p:['foo'],od:1,oi:3}],[{p:['foo'],od:3,oi:2}] - - - describe '#transform()', -> it 'returns sane values', -> - t = (op1, op2) -> - assert.deepEqual op1, type.transform op1, op2, 'left' - assert.deepEqual op1, type.transform op1, op2, 'right' - - t [], [] - t [{p:['foo'], oi:1}], [] - t [{p:['foo'], oi:1}], [{p:['bar'], oi:2}] - - describe 'number', -> - it 'Adds a number', -> - assert.deepEqual 3, type.apply 1, [{p:[], na:2}] - assert.deepEqual [3], type.apply [1], [{p:[0], na:2}] - - it 'compresses two adds together in compose', -> - assert.deepEqual [{p:['a', 'b'], na:3}], type.compose [{p:['a', 'b'], na:1}], [{p:['a', 'b'], na:2}] - assert.deepEqual [{p:['a'], na:1}, {p:['b'], na:2}], type.compose [{p:['a'], na:1}], [{p:['b'], na:2}] - - it 'doesn\'t overwrite values when it merges na in append', -> - rightHas = 21 - leftHas = 3 - - rightOp = [{"p":[],"od":0,"oi":15},{"p":[],"na":4},{"p":[],"na":1},{"p":[],"na":1}] - leftOp = [{"p":[],"na":4},{"p":[],"na":-1}] - [right_, left_] = transformX type, rightOp, leftOp - - s_c = type.apply rightHas, left_ - c_s = type.apply leftHas, right_ - assert.deepEqual s_c, c_s - - - # Strings should be handled internally by the text type. We'll just do some basic sanity checks here. - describe 'string', -> - describe '#apply()', -> it 'works', -> - assert.deepEqual 'abc', type.apply 'a', [{p:[1], si:'bc'}] - assert.deepEqual 'bc', type.apply 'abc', [{p:[0], sd:'a'}] - assert.deepEqual {x:'abc'}, type.apply {x:'a'}, [{p:['x', 1], si:'bc'}] - - describe '#transform()', -> - it 'splits deletes', -> - assert.deepEqual type.transform([{p:[0], sd:'ab'}], [{p:[1], si:'x'}], 'left'), [{p:[0], sd:'a'}, {p:[1], sd:'b'}] - - it 'cancels out other deletes', -> - assert.deepEqual type.transform([{p:['k', 5], sd:'a'}], [{p:['k', 5], sd:'a'}], 'left'), [] - - it 'does not throw errors with blank inserts', -> - assert.deepEqual type.transform([{p: ['k', 5], si:''}], [{p: ['k', 3], si: 'a'}], 'left'), [] - - describe 'string subtype', -> - describe '#apply()', -> - it 'works', -> - assert.deepEqual 'abc', type.apply 'a', [{p:[], t:'text0', o:[{p:1, i:'bc'}]}] - assert.deepEqual 'bc', type.apply 'abc', [{p:[], t:'text0', o:[{p:0, d:'a'}]}] - assert.deepEqual {x:'abc'}, type.apply {x:'a'}, [{p:['x'], t:'text0', o:[{p:1, i:'bc'}]}] - - describe '#transform()', -> - it 'splits deletes', -> - a = [{p:[], t:'text0', o:[{p:0, d:'ab'}]}] - b = [{p:[], t:'text0', o:[{p:1, i:'x'}]}] - assert.deepEqual type.transform(a, b, 'left'), [{p:[], t:'text0', o:[{p:0, d:'a'}, {p:1, d:'b'}]}] - - it 'cancels out other deletes', -> - assert.deepEqual type.transform([{p:['k'], t:'text0', o:[{p:5, d:'a'}]}], [{p:['k'], t:'text0', o:[{p:5, d:'a'}]}], 'left'), [] - - it 'does not throw errors with blank inserts', -> - assert.deepEqual type.transform([{p:['k'], t:'text0', o:[{p:5, i:''}]}], [{p:['k'], t:'text0', o:[{p:3, i:'a'}]}], 'left'), [] - - describe 'subtype with non-array operation', -> - describe '#transform()', -> - it 'works', -> - a = [{p:[], t:'mock', o:'foo'}] - b = [{p:[], t:'mock', o:'bar'}] - assert.deepEqual type.transform(a, b, 'left'), [{p:[], t:'mock', o:{mock:true}}] - - describe 'list', -> - describe 'apply', -> - it 'inserts', -> - assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'c'], [{p:[0], li:'a'}] - assert.deepEqual ['a', 'b', 'c'], type.apply ['a', 'c'], [{p:[1], li:'b'}] - assert.deepEqual ['a', 'b', 'c'], type.apply ['a', 'b'], [{p:[2], li:'c'}] - - it 'deletes', -> - assert.deepEqual ['b', 'c'], type.apply ['a', 'b', 'c'], [{p:[0], ld:'a'}] - assert.deepEqual ['a', 'c'], type.apply ['a', 'b', 'c'], [{p:[1], ld:'b'}] - assert.deepEqual ['a', 'b'], type.apply ['a', 'b', 'c'], [{p:[2], ld:'c'}] - - it 'replaces', -> - assert.deepEqual ['a', 'y', 'b'], type.apply ['a', 'x', 'b'], [{p:[1], ld:'x', li:'y'}] - - it 'moves', -> - assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[1], lm:0}] - assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[0], lm:1}] - - ### - 'null moves compose to nops', -> - assert.deepEqual [], type.compose [], [{p:[3],lm:3}] - assert.deepEqual [], type.compose [], [{p:[0,3],lm:3}] - assert.deepEqual [], type.compose [], [{p:['x','y',0],lm:0}] - ### - - describe '#transform()', -> - it 'bumps paths when list elements are inserted or removed', -> - assert.deepEqual [{p:[2, 200], si:'hi'}], type.transform [{p:[1, 200], si:'hi'}], [{p:[0], li:'x'}], 'left' - assert.deepEqual [{p:[1, 201], si:'hi'}], type.transform [{p:[0, 201], si:'hi'}], [{p:[0], li:'x'}], 'right' - assert.deepEqual [{p:[0, 202], si:'hi'}], type.transform [{p:[0, 202], si:'hi'}], [{p:[1], li:'x'}], 'left' - assert.deepEqual [{p:[2], t:'text0', o:[{p:200, i:'hi'}]}], type.transform [{p:[1], t:'text0', o:[{p:200, i:'hi'}]}], [{p:[0], li:'x'}], 'left' - assert.deepEqual [{p:[1], t:'text0', o:[{p:201, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:201, i:'hi'}]}], [{p:[0], li:'x'}], 'right' - assert.deepEqual [{p:[0], t:'text0', o:[{p:202, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:202, i:'hi'}]}], [{p:[1], li:'x'}], 'left' - - assert.deepEqual [{p:[0, 203], si:'hi'}], type.transform [{p:[1, 203], si:'hi'}], [{p:[0], ld:'x'}], 'left' - assert.deepEqual [{p:[0, 204], si:'hi'}], type.transform [{p:[0, 204], si:'hi'}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [{p:['x',3], si: 'hi'}], type.transform [{p:['x',3], si:'hi'}], [{p:['x',0,'x'], li:0}], 'left' - assert.deepEqual [{p:['x',3,'x'], si: 'hi'}], type.transform [{p:['x',3,'x'], si:'hi'}], [{p:['x',5], li:0}], 'left' - assert.deepEqual [{p:['x',4,'x'], si: 'hi'}], type.transform [{p:['x',3,'x'], si:'hi'}], [{p:['x',0], li:0}], 'left' - assert.deepEqual [{p:[0], t:'text0', o:[{p:203, i:'hi'}]}], type.transform [{p:[1], t:'text0', o:[{p:203, i:'hi'}]}], [{p:[0], ld:'x'}], 'left' - assert.deepEqual [{p:[0], t:'text0', o:[{p:204, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:204, i:'hi'}]}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [{p:['x'], t:'text0', o:[{p:3,i: 'hi'}]}], type.transform [{p:['x'], t:'text0', o:[{p:3, i:'hi'}]}], [{p:['x',0,'x'], li:0}], 'left' - - assert.deepEqual [{p:[1],ld:2}], type.transform [{p:[0],ld:2}], [{p:[0],li:1}], 'left' - assert.deepEqual [{p:[1],ld:2}], type.transform [{p:[0],ld:2}], [{p:[0],li:1}], 'right' - - it 'converts ops on deleted elements to noops', -> - assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [{p:[0],li:'x'}], type.transform [{p:[0],li:'x'}], [{p:[0],ld:'y'}], 'left' - assert.deepEqual [], type.transform [{p:[0],na:-3}], [{p:[0],ld:48}], 'left' - - it 'converts ops on replaced elements to noops', -> - assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], ld:'x', li:'y'}], 'left' - assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], ld:'x', li:'y'}], 'left' - assert.deepEqual [{p:[0], li:'hi'}], type.transform [{p:[0], li:'hi'}], [{p:[0], ld:'x', li:'y'}], 'left' - - it 'changes deleted data to reflect edits', -> - assert.deepEqual [{p:[1], ld:'abc'}], type.transform [{p:[1], ld:'a'}], [{p:[1, 1], si:'bc'}], 'left' - assert.deepEqual [{p:[1], ld:'abc'}], type.transform [{p:[1], ld:'a'}], [{p:[1], t:'text0', o:[{p:1, i:'bc'}]}], 'left' - - it 'Puts the left op first if two inserts are simultaneous', -> - assert.deepEqual [{p:[1], li:'a'}], type.transform [{p:[1], li:'a'}], [{p:[1], li:'b'}], 'left' - assert.deepEqual [{p:[2], li:'b'}], type.transform [{p:[1], li:'b'}], [{p:[1], li:'a'}], 'right' - - it 'converts an attempt to re-delete a list element into a no-op', -> - assert.deepEqual [], type.transform [{p:[1], ld:'x'}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [], type.transform [{p:[1], ld:'x'}], [{p:[1], ld:'x'}], 'right' - - - describe '#compose()', -> - it 'composes insert then delete into a no-op', -> - assert.deepEqual [], type.compose [{p:[1], li:'abc'}], [{p:[1], ld:'abc'}] - assert.deepEqual [{p:[1],ld:null,li:'x'}], type.transform [{p:[0],ld:null,li:"x"}], [{p:[0],li:"The"}], 'right' - - it 'doesn\'t change the original object', -> - a = [{p:[0],ld:'abc',li:null}] - assert.deepEqual [{p:[0],ld:'abc'}], type.compose a, [{p:[0],ld:null}] - assert.deepEqual [{p:[0],ld:'abc',li:null}], a - - it 'composes together adjacent string ops', -> - assert.deepEqual [{p:[100], si:'hi'}], type.compose [{p:[100], si:'h'}], [{p:[101], si:'i'}] - assert.deepEqual [{p:[], t:'text0', o:[{p:100, i:'hi'}]}], type.compose [{p:[], t:'text0', o:[{p:100, i:'h'}]}], [{p:[], t:'text0', o:[{p:101, i:'i'}]}] - - it 'moves ops on a moved element with the element', -> - assert.deepEqual [{p:[10], ld:'x'}], type.transform [{p:[4], ld:'x'}], [{p:[4], lm:10}], 'left' - assert.deepEqual [{p:[10, 1], si:'a'}], type.transform [{p:[4, 1], si:'a'}], [{p:[4], lm:10}], 'left' - assert.deepEqual [{p:[10], t:'text0', o:[{p:1, i:'a'}]}], type.transform [{p:[4], t:'text0', o:[{p:1, i:'a'}]}], [{p:[4], lm:10}], 'left' - assert.deepEqual [{p:[10, 1], li:'a'}], type.transform [{p:[4, 1], li:'a'}], [{p:[4], lm:10}], 'left' - assert.deepEqual [{p:[10, 1], ld:'b', li:'a'}], type.transform [{p:[4, 1], ld:'b', li:'a'}], [{p:[4], lm:10}], 'left' - - assert.deepEqual [{p:[0],li:null}], type.transform [{p:[0],li:null}], [{p:[0],lm:1}], 'left' - # [_,_,_,_,5,6,7,_] - # c: [_,_,_,_,5,'x',6,7,_] p:5 li:'x' - # s: [_,6,_,_,_,5,7,_] p:5 lm:1 - # correct: [_,6,_,_,_,5,'x',7,_] - assert.deepEqual [{p:[6],li:'x'}], type.transform [{p:[5],li:'x'}], [{p:[5],lm:1}], 'left' - # [_,_,_,_,5,6,7,_] - # c: [_,_,_,_,5,6,7,_] p:5 ld:6 - # s: [_,6,_,_,_,5,7,_] p:5 lm:1 - # correct: [_,_,_,_,5,7,_] - assert.deepEqual [{p:[1],ld:6}], type.transform [{p:[5],ld:6}], [{p:[5],lm:1}], 'left' - #assert.deepEqual [{p:[0],li:{}}], type.transform [{p:[0],li:{}}], [{p:[0],lm:0}], 'right' - assert.deepEqual [{p:[0],li:[]}], type.transform [{p:[0],li:[]}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[2],li:'x'}], type.transform [{p:[2],li:'x'}], [{p:[0],lm:1}], 'left' - - it 'moves target index on ld/li', -> - assert.deepEqual [{p:[0],lm:1}], type.transform [{p:[0], lm: 2}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [{p:[1],lm:3}], type.transform [{p:[2], lm: 4}], [{p:[1], ld:'x'}], 'left' - assert.deepEqual [{p:[0],lm:3}], type.transform [{p:[0], lm: 2}], [{p:[1], li:'x'}], 'left' - assert.deepEqual [{p:[3],lm:5}], type.transform [{p:[2], lm: 4}], [{p:[1], li:'x'}], 'left' - assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0], lm: 0}], [{p:[0], li:28}], 'left' - - it 'tiebreaks lm vs. ld/li', -> - assert.deepEqual [], type.transform [{p:[0], lm: 2}], [{p:[0], ld:'x'}], 'left' - assert.deepEqual [], type.transform [{p:[0], lm: 2}], [{p:[0], ld:'x'}], 'right' - assert.deepEqual [{p:[1], lm:3}], type.transform [{p:[0], lm: 2}], [{p:[0], li:'x'}], 'left' - assert.deepEqual [{p:[1], lm:3}], type.transform [{p:[0], lm: 2}], [{p:[0], li:'x'}], 'right' - - it 'replacement vs. deletion', -> - assert.deepEqual [{p:[0],li:'y'}], type.transform [{p:[0],ld:'x',li:'y'}], [{p:[0],ld:'x'}], 'right' - - it 'replacement vs. insertion', -> - assert.deepEqual [{p:[1],ld:{},li:"brillig"}], type.transform [{p:[0],ld:{},li:"brillig"}], [{p:[0],li:36}], 'left' - - it 'replacement vs. replacement', -> - assert.deepEqual [], type.transform [{p:[0],ld:null,li:[]}], [{p:[0],ld:null,li:0}], 'right' - assert.deepEqual [{p:[0],ld:[],li:0}], type.transform [{p:[0],ld:null,li:0}], [{p:[0],ld:null,li:[]}], 'left' - - it 'composes replace with delete of replaced element results in insert', -> - assert.deepEqual [{p:[2],ld:[]}], type.compose [{p:[2],ld:[],li:null}], [{p:[2],ld:null}] - - it 'lm vs lm', -> - assert.deepEqual [{p:[0],lm:2}], type.transform [{p:[0],lm:2}], [{p:[2],lm:1}], 'left' - assert.deepEqual [{p:[4],lm:4}], type.transform [{p:[3],lm:3}], [{p:[5],lm:0}], 'left' - assert.deepEqual [{p:[2],lm:0}], type.transform [{p:[2],lm:0}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[2],lm:1}], type.transform [{p:[2],lm:0}], [{p:[1],lm:0}], 'right' - assert.deepEqual [{p:[3],lm:1}], type.transform [{p:[2],lm:0}], [{p:[5],lm:0}], 'right' - assert.deepEqual [{p:[3],lm:0}], type.transform [{p:[2],lm:0}], [{p:[5],lm:0}], 'left' - assert.deepEqual [{p:[0],lm:5}], type.transform [{p:[2],lm:5}], [{p:[2],lm:0}], 'left' - assert.deepEqual [{p:[0],lm:5}], type.transform [{p:[2],lm:5}], [{p:[2],lm:0}], 'left' - assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],lm:5}], 'right' - assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],lm:1}], 'right' - assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0],lm:1}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[1],lm:2}], type.transform [{p:[0],lm:1}], [{p:[5],lm:0}], 'right' - assert.deepEqual [{p:[3],lm:2}], type.transform [{p:[2],lm:1}], [{p:[5],lm:0}], 'right' - assert.deepEqual [{p:[2],lm:1}], type.transform [{p:[3],lm:1}], [{p:[1],lm:3}], 'left' - assert.deepEqual [{p:[2],lm:3}], type.transform [{p:[1],lm:3}], [{p:[3],lm:1}], 'left' - assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[0],lm:1}], 'left' - assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[0],lm:1}], 'right' - assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[1],lm:0}], 'right' - assert.deepEqual [{p:[0],lm:2}], type.transform [{p:[0],lm:1}], [{p:[2],lm:1}], 'left' - assert.deepEqual [{p:[2],lm:0}], type.transform [{p:[2],lm:1}], [{p:[0],lm:1}], 'right' - assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0],lm:0}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[0],lm:1}], [{p:[1],lm:3}], 'left' - assert.deepEqual [{p:[3],lm:1}], type.transform [{p:[2],lm:1}], [{p:[3],lm:2}], 'left' - assert.deepEqual [{p:[3],lm:3}], type.transform [{p:[3],lm:2}], [{p:[2],lm:1}], 'left' - - it 'changes indices correctly around a move', -> - assert.deepEqual [{p:[1,0],li:{}}], type.transform [{p:[0,0],li:{}}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],ld:{}}], 'left' - assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[0],lm:1}], [{p:[1],ld:{}}], 'left' - assert.deepEqual [{p:[5],lm:0}], type.transform [{p:[6],lm:0}], [{p:[2],ld:{}}], 'left' - assert.deepEqual [{p:[1],lm:0}], type.transform [{p:[1],lm:0}], [{p:[2],ld:{}}], 'left' - assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[2],lm:1}], [{p:[1],ld:3}], 'right' - - assert.deepEqual [{p:[1],ld:{}}], type.transform [{p:[2],ld:{}}], [{p:[1],lm:2}], 'right' - assert.deepEqual [{p:[2],ld:{}}], type.transform [{p:[1],ld:{}}], [{p:[2],lm:1}], 'left' - - - assert.deepEqual [{p:[0],ld:{}}], type.transform [{p:[1],ld:{}}], [{p:[0],lm:1}], 'right' - - assert.deepEqual [{p:[0],ld:1,li:2}], type.transform [{p:[1],ld:1,li:2}], [{p:[1],lm:0}], 'left' - assert.deepEqual [{p:[0],ld:2,li:3}], type.transform [{p:[1],ld:2,li:3}], [{p:[0],lm:1}], 'left' - assert.deepEqual [{p:[1],ld:3,li:4}], type.transform [{p:[0],ld:3,li:4}], [{p:[1],lm:0}], 'left' - - it 'li vs lm', -> - li = (p) -> [{p:[p],li:[]}] - lm = (f,t) -> [{p:[f],lm:t}] - xf = type.transform - - assert.deepEqual (li 0), xf (li 0), (lm 1, 3), 'left' - assert.deepEqual (li 1), xf (li 1), (lm 1, 3), 'left' - assert.deepEqual (li 1), xf (li 2), (lm 1, 3), 'left' - assert.deepEqual (li 2), xf (li 3), (lm 1, 3), 'left' - assert.deepEqual (li 4), xf (li 4), (lm 1, 3), 'left' - - assert.deepEqual (lm 2, 4), xf (lm 1, 3), (li 0), 'right' - assert.deepEqual (lm 2, 4), xf (lm 1, 3), (li 1), 'right' - assert.deepEqual (lm 1, 4), xf (lm 1, 3), (li 2), 'right' - assert.deepEqual (lm 1, 4), xf (lm 1, 3), (li 3), 'right' - assert.deepEqual (lm 1, 3), xf (lm 1, 3), (li 4), 'right' - - assert.deepEqual (li 0), xf (li 0), (lm 1, 2), 'left' - assert.deepEqual (li 1), xf (li 1), (lm 1, 2), 'left' - assert.deepEqual (li 1), xf (li 2), (lm 1, 2), 'left' - assert.deepEqual (li 3), xf (li 3), (lm 1, 2), 'left' - - assert.deepEqual (li 0), xf (li 0), (lm 3, 1), 'left' - assert.deepEqual (li 1), xf (li 1), (lm 3, 1), 'left' - assert.deepEqual (li 3), xf (li 2), (lm 3, 1), 'left' - assert.deepEqual (li 4), xf (li 3), (lm 3, 1), 'left' - assert.deepEqual (li 4), xf (li 4), (lm 3, 1), 'left' - - assert.deepEqual (lm 4, 2), xf (lm 3, 1), (li 0), 'right' - assert.deepEqual (lm 4, 2), xf (lm 3, 1), (li 1), 'right' - assert.deepEqual (lm 4, 1), xf (lm 3, 1), (li 2), 'right' - assert.deepEqual (lm 4, 1), xf (lm 3, 1), (li 3), 'right' - assert.deepEqual (lm 3, 1), xf (lm 3, 1), (li 4), 'right' - - assert.deepEqual (li 0), xf (li 0), (lm 2, 1), 'left' - assert.deepEqual (li 1), xf (li 1), (lm 2, 1), 'left' - assert.deepEqual (li 3), xf (li 2), (lm 2, 1), 'left' - assert.deepEqual (li 3), xf (li 3), (lm 2, 1), 'left' - - - describe 'object', -> - it 'passes sanity checks', -> - assert.deepEqual {x:'a', y:'b'}, type.apply {x:'a'}, [{p:['y'], oi:'b'}] - assert.deepEqual {}, type.apply {x:'a'}, [{p:['x'], od:'a'}] - assert.deepEqual {x:'b'}, type.apply {x:'a'}, [{p:['x'], od:'a', oi:'b'}] - - it 'Ops on deleted elements become noops', -> - assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], od:'x'}], 'left' - assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], od:'x'}], 'left' - assert.deepEqual [], type.transform [{p:[9],si:"bite "}], [{p:[],od:"agimble s",oi:null}], 'right' - assert.deepEqual [], type.transform [{p:[], t:'text0', o:[{p:9, i:"bite "}]}], [{p:[],od:"agimble s",oi:null}], 'right' - - it 'Ops on replaced elements become noops', -> - assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], od:'x', oi:'y'}], 'left' - assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], od:'x', oi:'y'}], 'left' - - it 'Deleted data is changed to reflect edits', -> - assert.deepEqual [{p:[1], od:'abc'}], type.transform [{p:[1], od:'a'}], [{p:[1, 1], si:'bc'}], 'left' - assert.deepEqual [{p:[1], od:'abc'}], type.transform [{p:[1], od:'a'}], [{p:[1], t:'text0', o:[{p:1, i:'bc'}]}], 'left' - assert.deepEqual [{p:[],od:25,oi:[]}], type.transform [{p:[],od:22,oi:[]}], [{p:[],na:3}], 'left' - assert.deepEqual [{p:[],od:{toves:""},oi:4}], type.transform [{p:[],od:{toves:0},oi:4}], [{p:["toves"],od:0,oi:""}], 'left' - assert.deepEqual [{p:[],od:"thou an",oi:[]}], type.transform [{p:[],od:"thou and ",oi:[]}], [{p:[7],sd:"d "}], 'left' - assert.deepEqual [{p:[],od:"thou an",oi:[]}], type.transform [{p:[],od:"thou and ",oi:[]}], [{p:[], t:'text0', o:[{p:7, d:"d "}]}], 'left' - assert.deepEqual [], type.transform([{p:["bird"],na:2}], [{p:[],od:{bird:38},oi:20}], 'right') - assert.deepEqual [{p:[],od:{bird:40},oi:20}], type.transform([{p:[],od:{bird:38},oi:20}], [{p:["bird"],na:2}], 'left') - assert.deepEqual [{p:['He'],od:[]}], type.transform [{p:["He"],od:[]}], [{p:["The"],na:-3}], 'right' - assert.deepEqual [], type.transform [{p:["He"],oi:{}}], [{p:[],od:{},oi:"the"}], 'left' - - it 'If two inserts are simultaneous, the lefts insert will win', -> - assert.deepEqual [{p:[1], oi:'a', od:'b'}], type.transform [{p:[1], oi:'a'}], [{p:[1], oi:'b'}], 'left' - assert.deepEqual [], type.transform [{p:[1], oi:'b'}], [{p:[1], oi:'a'}], 'right' - - it 'parallel ops on different keys miss each other', -> - assert.deepEqual [{p:['a'], oi: 'x'}], type.transform [{p:['a'], oi:'x'}], [{p:['b'], oi:'z'}], 'left' - assert.deepEqual [{p:['a'], oi: 'x'}], type.transform [{p:['a'], oi:'x'}], [{p:['b'], od:'z'}], 'left' - assert.deepEqual [{p:["in","he"],oi:{}}], type.transform [{p:["in","he"],oi:{}}], [{p:["and"],od:{}}], 'right' - assert.deepEqual [{p:['x',0],si:"his "}], type.transform [{p:['x',0],si:"his "}], [{p:['y'],od:0,oi:1}], 'right' - assert.deepEqual [{p:['x'], t:'text0', o:[{p:0, i:"his "}]}], type.transform [{p:['x'],t:'text0', o:[{p:0, i:"his "}]}], [{p:['y'],od:0,oi:1}], 'right' - - it 'replacement vs. deletion', -> - assert.deepEqual [{p:[],oi:{}}], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:['']}], 'right' - - it 'replacement vs. replacement', -> - assert.deepEqual [], type.transform [{p:[],od:['']},{p:[],oi:{}}], [{p:[],od:['']},{p:[],oi:null}], 'right' - assert.deepEqual [{p:[],od:null,oi:{}}], type.transform [{p:[],od:['']},{p:[],oi:{}}], [{p:[],od:['']},{p:[],oi:null}], 'left' - assert.deepEqual [], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:[''],oi:null}], 'right' - assert.deepEqual [{p:[],od:null,oi:{}}], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:[''],oi:null}], 'left' - - # test diamond property - rightOps = [ {"p":[],"od":null,"oi":{}} ] - leftOps = [ {"p":[],"od":null,"oi":""} ] - rightHas = type.apply(null, rightOps) - leftHas = type.apply(null, leftOps) - - [left_, right_] = transformX type, leftOps, rightOps - assert.deepEqual leftHas, type.apply rightHas, left_ - assert.deepEqual leftHas, type.apply leftHas, right_ - - - it 'An attempt to re-delete a key becomes a no-op', -> - assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'left' - assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'right' - - describe 'randomizer', -> - @timeout 20000 - @slow 6000 - it 'passes', -> - fuzzer type, require('./json0-generator'), 1000 - - it 'passes with string subtype', -> - type._testStringSubtype = true # hack - fuzzer type, require('./json0-generator'), 1000 - delete type._testStringSubtype - -describe 'json', -> - describe 'native type', -> genTests nativetype - #exports.webclient = genTests require('../helpers/webclient').types.json diff --git a/test/json0.js b/test/json0.js new file mode 100644 index 0000000..db2eca9 --- /dev/null +++ b/test/json0.js @@ -0,0 +1,1169 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Tests for JSON OT type. + +const assert = require('assert'); +const nativetype = require('../lib/json0'); + +const fuzzer = require('ot-fuzzer'); + +nativetype.registerSubtype({ + name: 'mock', + transform(a, b, side) { + return { mock: true }; + } +}); + +// Cross-transform helper function. Transform server by client and client by +// server. Returns [server, client]. +const transformX = (type, left, right) => [ + type.transform(left, right, 'left'), + type.transform(right, left, 'right') +]; + +const genTests = function(type) { + // The random op tester above will test that the OT functions are admissable, + // but debugging problems it detects is a pain. + // + // These tests should pick up *most* problems with a normal JSON OT + // implementation. + + describe('sanity', function() { + describe('#create()', () => + it('returns null', () => assert.deepEqual(type.create(), null))); + + describe('#compose()', function() { + it('od,oi --> od+oi', function() { + assert.deepEqual( + [{ p: ['foo'], od: 1, oi: 2 }], + type.compose( + [{ p: ['foo'], od: 1 }], + [{ p: ['foo'], oi: 2 }] + ) + ); + return assert.deepEqual( + [{ p: ['foo'], od: 1 }, { p: ['bar'], oi: 2 }], + type.compose( + [{ p: ['foo'], od: 1 }], + [{ p: ['bar'], oi: 2 }] + ) + ); + }); + return it('merges od+oi, od+oi -> od+oi', () => + assert.deepEqual( + [{ p: ['foo'], od: 1, oi: 2 }], + type.compose( + [{ p: ['foo'], od: 1, oi: 3 }], + [{ p: ['foo'], od: 3, oi: 2 }] + ) + )); + }); + + return describe('#transform()', () => + it('returns sane values', function() { + const t = function(op1, op2) { + assert.deepEqual(op1, type.transform(op1, op2, 'left')); + return assert.deepEqual(op1, type.transform(op1, op2, 'right')); + }; + + t([], []); + t([{ p: ['foo'], oi: 1 }], []); + return t([{ p: ['foo'], oi: 1 }], [{ p: ['bar'], oi: 2 }]); + })); + }); + + describe('number', function() { + it('Adds a number', function() { + assert.deepEqual(3, type.apply(1, [{ p: [], na: 2 }])); + return assert.deepEqual([3], type.apply([1], [{ p: [0], na: 2 }])); + }); + + it('compresses two adds together in compose', function() { + assert.deepEqual( + [{ p: ['a', 'b'], na: 3 }], + type.compose( + [{ p: ['a', 'b'], na: 1 }], + [{ p: ['a', 'b'], na: 2 }] + ) + ); + return assert.deepEqual( + [{ p: ['a'], na: 1 }, { p: ['b'], na: 2 }], + type.compose( + [{ p: ['a'], na: 1 }], + [{ p: ['b'], na: 2 }] + ) + ); + }); + + return it("doesn't overwrite values when it merges na in append", function() { + const rightHas = 21; + const leftHas = 3; + + const rightOp = [ + { p: [], od: 0, oi: 15 }, + { p: [], na: 4 }, + { p: [], na: 1 }, + { p: [], na: 1 } + ]; + const leftOp = [{ p: [], na: 4 }, { p: [], na: -1 }]; + const [right_, left_] = Array.from(transformX(type, rightOp, leftOp)); + + const s_c = type.apply(rightHas, left_); + const c_s = type.apply(leftHas, right_); + return assert.deepEqual(s_c, c_s); + }); + }); + + // Strings should be handled internally by the text type. We'll just do some basic sanity checks here. + describe('string', function() { + describe('#apply()', () => + it('works', function() { + assert.deepEqual('abc', type.apply('a', [{ p: [1], si: 'bc' }])); + assert.deepEqual('bc', type.apply('abc', [{ p: [0], sd: 'a' }])); + return assert.deepEqual( + { x: 'abc' }, + type.apply({ x: 'a' }, [{ p: ['x', 1], si: 'bc' }]) + ); + })); + + return describe('#transform()', function() { + it('splits deletes', () => + assert.deepEqual( + type.transform([{ p: [0], sd: 'ab' }], [{ p: [1], si: 'x' }], 'left'), + [{ p: [0], sd: 'a' }, { p: [1], sd: 'b' }] + )); + + it('cancels out other deletes', () => + assert.deepEqual( + type.transform( + [{ p: ['k', 5], sd: 'a' }], + [{ p: ['k', 5], sd: 'a' }], + 'left' + ), + [] + )); + + return it('does not throw errors with blank inserts', () => + assert.deepEqual( + type.transform( + [{ p: ['k', 5], si: '' }], + [{ p: ['k', 3], si: 'a' }], + 'left' + ), + [] + )); + }); + }); + + describe('string subtype', function() { + describe('#apply()', () => + it('works', function() { + assert.deepEqual( + 'abc', + type.apply('a', [{ p: [], t: 'text0', o: [{ p: 1, i: 'bc' }] }]) + ); + assert.deepEqual( + 'bc', + type.apply('abc', [{ p: [], t: 'text0', o: [{ p: 0, d: 'a' }] }]) + ); + return assert.deepEqual( + { x: 'abc' }, + type.apply({ x: 'a' }, [ + { p: ['x'], t: 'text0', o: [{ p: 1, i: 'bc' }] } + ]) + ); + })); + + return describe('#transform()', function() { + it('splits deletes', function() { + const a = [{ p: [], t: 'text0', o: [{ p: 0, d: 'ab' }] }]; + const b = [{ p: [], t: 'text0', o: [{ p: 1, i: 'x' }] }]; + return assert.deepEqual(type.transform(a, b, 'left'), [ + { p: [], t: 'text0', o: [{ p: 0, d: 'a' }, { p: 1, d: 'b' }] } + ]); + }); + + it('cancels out other deletes', () => + assert.deepEqual( + type.transform( + [{ p: ['k'], t: 'text0', o: [{ p: 5, d: 'a' }] }], + [{ p: ['k'], t: 'text0', o: [{ p: 5, d: 'a' }] }], + 'left' + ), + [] + )); + + return it('does not throw errors with blank inserts', () => + assert.deepEqual( + type.transform( + [{ p: ['k'], t: 'text0', o: [{ p: 5, i: '' }] }], + [{ p: ['k'], t: 'text0', o: [{ p: 3, i: 'a' }] }], + 'left' + ), + [] + )); + }); + }); + + describe('subtype with non-array operation', () => + describe('#transform()', () => + it('works', function() { + const a = [{ p: [], t: 'mock', o: 'foo' }]; + const b = [{ p: [], t: 'mock', o: 'bar' }]; + return assert.deepEqual(type.transform(a, b, 'left'), [ + { p: [], t: 'mock', o: { mock: true } } + ]); + }))); + + describe('list', function() { + describe('apply', function() { + it('inserts', function() { + assert.deepEqual( + ['a', 'b', 'c'], + type.apply(['b', 'c'], [{ p: [0], li: 'a' }]) + ); + assert.deepEqual( + ['a', 'b', 'c'], + type.apply(['a', 'c'], [{ p: [1], li: 'b' }]) + ); + return assert.deepEqual( + ['a', 'b', 'c'], + type.apply(['a', 'b'], [{ p: [2], li: 'c' }]) + ); + }); + + it('deletes', function() { + assert.deepEqual( + ['b', 'c'], + type.apply(['a', 'b', 'c'], [{ p: [0], ld: 'a' }]) + ); + assert.deepEqual( + ['a', 'c'], + type.apply(['a', 'b', 'c'], [{ p: [1], ld: 'b' }]) + ); + return assert.deepEqual( + ['a', 'b'], + type.apply(['a', 'b', 'c'], [{ p: [2], ld: 'c' }]) + ); + }); + + it('replaces', () => + assert.deepEqual( + ['a', 'y', 'b'], + type.apply(['a', 'x', 'b'], [{ p: [1], ld: 'x', li: 'y' }]) + )); + + return it('moves', function() { + assert.deepEqual( + ['a', 'b', 'c'], + type.apply(['b', 'a', 'c'], [{ p: [1], lm: 0 }]) + ); + return assert.deepEqual( + ['a', 'b', 'c'], + type.apply(['b', 'a', 'c'], [{ p: [0], lm: 1 }]) + ); + }); + + /* + 'null moves compose to nops', -> + assert.deepEqual [], type.compose [], [{p:[3],lm:3}] + assert.deepEqual [], type.compose [], [{p:[0,3],lm:3}] + assert.deepEqual [], type.compose [], [{p:['x','y',0],lm:0}] + */ + }); + + describe('#transform()', function() { + it('bumps paths when list elements are inserted or removed', function() { + assert.deepEqual( + [{ p: [2, 200], si: 'hi' }], + type.transform( + [{ p: [1, 200], si: 'hi' }], + [{ p: [0], li: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [1, 201], si: 'hi' }], + type.transform( + [{ p: [0, 201], si: 'hi' }], + [{ p: [0], li: 'x' }], + 'right' + ) + ); + assert.deepEqual( + [{ p: [0, 202], si: 'hi' }], + type.transform( + [{ p: [0, 202], si: 'hi' }], + [{ p: [1], li: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [2], t: 'text0', o: [{ p: 200, i: 'hi' }] }], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 200, i: 'hi' }] }], + [{ p: [0], li: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [1], t: 'text0', o: [{ p: 201, i: 'hi' }] }], + type.transform( + [{ p: [0], t: 'text0', o: [{ p: 201, i: 'hi' }] }], + [{ p: [0], li: 'x' }], + 'right' + ) + ); + assert.deepEqual( + [{ p: [0], t: 'text0', o: [{ p: 202, i: 'hi' }] }], + type.transform( + [{ p: [0], t: 'text0', o: [{ p: 202, i: 'hi' }] }], + [{ p: [1], li: 'x' }], + 'left' + ) + ); + + assert.deepEqual( + [{ p: [0, 203], si: 'hi' }], + type.transform( + [{ p: [1, 203], si: 'hi' }], + [{ p: [0], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [0, 204], si: 'hi' }], + type.transform( + [{ p: [0, 204], si: 'hi' }], + [{ p: [1], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: ['x', 3], si: 'hi' }], + type.transform( + [{ p: ['x', 3], si: 'hi' }], + [{ p: ['x', 0, 'x'], li: 0 }], + 'left' + ) + ); + assert.deepEqual( + [{ p: ['x', 3, 'x'], si: 'hi' }], + type.transform( + [{ p: ['x', 3, 'x'], si: 'hi' }], + [{ p: ['x', 5], li: 0 }], + 'left' + ) + ); + assert.deepEqual( + [{ p: ['x', 4, 'x'], si: 'hi' }], + type.transform( + [{ p: ['x', 3, 'x'], si: 'hi' }], + [{ p: ['x', 0], li: 0 }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [0], t: 'text0', o: [{ p: 203, i: 'hi' }] }], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 203, i: 'hi' }] }], + [{ p: [0], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [0], t: 'text0', o: [{ p: 204, i: 'hi' }] }], + type.transform( + [{ p: [0], t: 'text0', o: [{ p: 204, i: 'hi' }] }], + [{ p: [1], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: ['x'], t: 'text0', o: [{ p: 3, i: 'hi' }] }], + type.transform( + [{ p: ['x'], t: 'text0', o: [{ p: 3, i: 'hi' }] }], + [{ p: ['x', 0, 'x'], li: 0 }], + 'left' + ) + ); + + assert.deepEqual( + [{ p: [1], ld: 2 }], + type.transform([{ p: [0], ld: 2 }], [{ p: [0], li: 1 }], 'left') + ); + return assert.deepEqual( + [{ p: [1], ld: 2 }], + type.transform([{ p: [0], ld: 2 }], [{ p: [0], li: 1 }], 'right') + ); + }); + + it('converts ops on deleted elements to noops', function() { + assert.deepEqual( + [], + type.transform( + [{ p: [1, 0], si: 'hi' }], + [{ p: [1], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 0, i: 'hi' }] }], + [{ p: [1], ld: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [0], li: 'x' }], + type.transform([{ p: [0], li: 'x' }], [{ p: [0], ld: 'y' }], 'left') + ); + return assert.deepEqual( + [], + type.transform([{ p: [0], na: -3 }], [{ p: [0], ld: 48 }], 'left') + ); + }); + + it('converts ops on replaced elements to noops', function() { + assert.deepEqual( + [], + type.transform( + [{ p: [1, 0], si: 'hi' }], + [{ p: [1], ld: 'x', li: 'y' }], + 'left' + ) + ); + assert.deepEqual( + [], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 0, i: 'hi' }] }], + [{ p: [1], ld: 'x', li: 'y' }], + 'left' + ) + ); + return assert.deepEqual( + [{ p: [0], li: 'hi' }], + type.transform( + [{ p: [0], li: 'hi' }], + [{ p: [0], ld: 'x', li: 'y' }], + 'left' + ) + ); + }); + + it('changes deleted data to reflect edits', function() { + assert.deepEqual( + [{ p: [1], ld: 'abc' }], + type.transform( + [{ p: [1], ld: 'a' }], + [{ p: [1, 1], si: 'bc' }], + 'left' + ) + ); + return assert.deepEqual( + [{ p: [1], ld: 'abc' }], + type.transform( + [{ p: [1], ld: 'a' }], + [{ p: [1], t: 'text0', o: [{ p: 1, i: 'bc' }] }], + 'left' + ) + ); + }); + + it('Puts the left op first if two inserts are simultaneous', function() { + assert.deepEqual( + [{ p: [1], li: 'a' }], + type.transform([{ p: [1], li: 'a' }], [{ p: [1], li: 'b' }], 'left') + ); + return assert.deepEqual( + [{ p: [2], li: 'b' }], + type.transform([{ p: [1], li: 'b' }], [{ p: [1], li: 'a' }], 'right') + ); + }); + + return it('converts an attempt to re-delete a list element into a no-op', function() { + assert.deepEqual( + [], + type.transform([{ p: [1], ld: 'x' }], [{ p: [1], ld: 'x' }], 'left') + ); + return assert.deepEqual( + [], + type.transform([{ p: [1], ld: 'x' }], [{ p: [1], ld: 'x' }], 'right') + ); + }); + }); + + describe('#compose()', function() { + it('composes insert then delete into a no-op', function() { + assert.deepEqual( + [], + type.compose( + [{ p: [1], li: 'abc' }], + [{ p: [1], ld: 'abc' }] + ) + ); + return assert.deepEqual( + [{ p: [1], ld: null, li: 'x' }], + type.transform( + [{ p: [0], ld: null, li: 'x' }], + [{ p: [0], li: 'The' }], + 'right' + ) + ); + }); + + it("doesn't change the original object", function() { + const a = [{ p: [0], ld: 'abc', li: null }]; + assert.deepEqual( + [{ p: [0], ld: 'abc' }], + type.compose( + a, + [{ p: [0], ld: null }] + ) + ); + return assert.deepEqual([{ p: [0], ld: 'abc', li: null }], a); + }); + + return it('composes together adjacent string ops', function() { + assert.deepEqual( + [{ p: [100], si: 'hi' }], + type.compose( + [{ p: [100], si: 'h' }], + [{ p: [101], si: 'i' }] + ) + ); + return assert.deepEqual( + [{ p: [], t: 'text0', o: [{ p: 100, i: 'hi' }] }], + type.compose( + [{ p: [], t: 'text0', o: [{ p: 100, i: 'h' }] }], + [{ p: [], t: 'text0', o: [{ p: 101, i: 'i' }] }] + ) + ); + }); + }); + + it('moves ops on a moved element with the element', function() { + assert.deepEqual( + [{ p: [10], ld: 'x' }], + type.transform([{ p: [4], ld: 'x' }], [{ p: [4], lm: 10 }], 'left') + ); + assert.deepEqual( + [{ p: [10, 1], si: 'a' }], + type.transform([{ p: [4, 1], si: 'a' }], [{ p: [4], lm: 10 }], 'left') + ); + assert.deepEqual( + [{ p: [10], t: 'text0', o: [{ p: 1, i: 'a' }] }], + type.transform( + [{ p: [4], t: 'text0', o: [{ p: 1, i: 'a' }] }], + [{ p: [4], lm: 10 }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [10, 1], li: 'a' }], + type.transform([{ p: [4, 1], li: 'a' }], [{ p: [4], lm: 10 }], 'left') + ); + assert.deepEqual( + [{ p: [10, 1], ld: 'b', li: 'a' }], + type.transform( + [{ p: [4, 1], ld: 'b', li: 'a' }], + [{ p: [4], lm: 10 }], + 'left' + ) + ); + + assert.deepEqual( + [{ p: [0], li: null }], + type.transform([{ p: [0], li: null }], [{ p: [0], lm: 1 }], 'left') + ); + // [_,_,_,_,5,6,7,_] + // c: [_,_,_,_,5,'x',6,7,_] p:5 li:'x' + // s: [_,6,_,_,_,5,7,_] p:5 lm:1 + // correct: [_,6,_,_,_,5,'x',7,_] + assert.deepEqual( + [{ p: [6], li: 'x' }], + type.transform([{ p: [5], li: 'x' }], [{ p: [5], lm: 1 }], 'left') + ); + // [_,_,_,_,5,6,7,_] + // c: [_,_,_,_,5,6,7,_] p:5 ld:6 + // s: [_,6,_,_,_,5,7,_] p:5 lm:1 + // correct: [_,_,_,_,5,7,_] + assert.deepEqual( + [{ p: [1], ld: 6 }], + type.transform([{ p: [5], ld: 6 }], [{ p: [5], lm: 1 }], 'left') + ); + //assert.deepEqual [{p:[0],li:{}}], type.transform [{p:[0],li:{}}], [{p:[0],lm:0}], 'right' + assert.deepEqual( + [{ p: [0], li: [] }], + type.transform([{ p: [0], li: [] }], [{ p: [1], lm: 0 }], 'left') + ); + return assert.deepEqual( + [{ p: [2], li: 'x' }], + type.transform([{ p: [2], li: 'x' }], [{ p: [0], lm: 1 }], 'left') + ); + }); + + it('moves target index on ld/li', function() { + assert.deepEqual( + [{ p: [0], lm: 1 }], + type.transform([{ p: [0], lm: 2 }], [{ p: [1], ld: 'x' }], 'left') + ); + assert.deepEqual( + [{ p: [1], lm: 3 }], + type.transform([{ p: [2], lm: 4 }], [{ p: [1], ld: 'x' }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 3 }], + type.transform([{ p: [0], lm: 2 }], [{ p: [1], li: 'x' }], 'left') + ); + assert.deepEqual( + [{ p: [3], lm: 5 }], + type.transform([{ p: [2], lm: 4 }], [{ p: [1], li: 'x' }], 'left') + ); + return assert.deepEqual( + [{ p: [1], lm: 1 }], + type.transform([{ p: [0], lm: 0 }], [{ p: [0], li: 28 }], 'left') + ); + }); + + it('tiebreaks lm vs. ld/li', function() { + assert.deepEqual( + [], + type.transform([{ p: [0], lm: 2 }], [{ p: [0], ld: 'x' }], 'left') + ); + assert.deepEqual( + [], + type.transform([{ p: [0], lm: 2 }], [{ p: [0], ld: 'x' }], 'right') + ); + assert.deepEqual( + [{ p: [1], lm: 3 }], + type.transform([{ p: [0], lm: 2 }], [{ p: [0], li: 'x' }], 'left') + ); + return assert.deepEqual( + [{ p: [1], lm: 3 }], + type.transform([{ p: [0], lm: 2 }], [{ p: [0], li: 'x' }], 'right') + ); + }); + + it('replacement vs. deletion', () => + assert.deepEqual( + [{ p: [0], li: 'y' }], + type.transform( + [{ p: [0], ld: 'x', li: 'y' }], + [{ p: [0], ld: 'x' }], + 'right' + ) + )); + + it('replacement vs. insertion', () => + assert.deepEqual( + [{ p: [1], ld: {}, li: 'brillig' }], + type.transform( + [{ p: [0], ld: {}, li: 'brillig' }], + [{ p: [0], li: 36 }], + 'left' + ) + )); + + it('replacement vs. replacement', function() { + assert.deepEqual( + [], + type.transform( + [{ p: [0], ld: null, li: [] }], + [{ p: [0], ld: null, li: 0 }], + 'right' + ) + ); + return assert.deepEqual( + [{ p: [0], ld: [], li: 0 }], + type.transform( + [{ p: [0], ld: null, li: 0 }], + [{ p: [0], ld: null, li: [] }], + 'left' + ) + ); + }); + + it('composes replace with delete of replaced element results in insert', () => + assert.deepEqual( + [{ p: [2], ld: [] }], + type.compose( + [{ p: [2], ld: [], li: null }], + [{ p: [2], ld: null }] + ) + )); + + it('lm vs lm', function() { + assert.deepEqual( + [{ p: [0], lm: 2 }], + type.transform([{ p: [0], lm: 2 }], [{ p: [2], lm: 1 }], 'left') + ); + assert.deepEqual( + [{ p: [4], lm: 4 }], + type.transform([{ p: [3], lm: 3 }], [{ p: [5], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 0 }], + type.transform([{ p: [2], lm: 0 }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 1 }], + type.transform([{ p: [2], lm: 0 }], [{ p: [1], lm: 0 }], 'right') + ); + assert.deepEqual( + [{ p: [3], lm: 1 }], + type.transform([{ p: [2], lm: 0 }], [{ p: [5], lm: 0 }], 'right') + ); + assert.deepEqual( + [{ p: [3], lm: 0 }], + type.transform([{ p: [2], lm: 0 }], [{ p: [5], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 5 }], + type.transform([{ p: [2], lm: 5 }], [{ p: [2], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 5 }], + type.transform([{ p: [2], lm: 5 }], [{ p: [2], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 0 }], + type.transform([{ p: [1], lm: 0 }], [{ p: [0], lm: 5 }], 'right') + ); + assert.deepEqual( + [{ p: [0], lm: 0 }], + type.transform([{ p: [1], lm: 0 }], [{ p: [0], lm: 1 }], 'right') + ); + assert.deepEqual( + [{ p: [1], lm: 1 }], + type.transform([{ p: [0], lm: 1 }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [1], lm: 2 }], + type.transform([{ p: [0], lm: 1 }], [{ p: [5], lm: 0 }], 'right') + ); + assert.deepEqual( + [{ p: [3], lm: 2 }], + type.transform([{ p: [2], lm: 1 }], [{ p: [5], lm: 0 }], 'right') + ); + assert.deepEqual( + [{ p: [2], lm: 1 }], + type.transform([{ p: [3], lm: 1 }], [{ p: [1], lm: 3 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 3 }], + type.transform([{ p: [1], lm: 3 }], [{ p: [3], lm: 1 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 6 }], + type.transform([{ p: [2], lm: 6 }], [{ p: [0], lm: 1 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 6 }], + type.transform([{ p: [2], lm: 6 }], [{ p: [0], lm: 1 }], 'right') + ); + assert.deepEqual( + [{ p: [2], lm: 6 }], + type.transform([{ p: [2], lm: 6 }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 6 }], + type.transform([{ p: [2], lm: 6 }], [{ p: [1], lm: 0 }], 'right') + ); + assert.deepEqual( + [{ p: [0], lm: 2 }], + type.transform([{ p: [0], lm: 1 }], [{ p: [2], lm: 1 }], 'left') + ); + assert.deepEqual( + [{ p: [2], lm: 0 }], + type.transform([{ p: [2], lm: 1 }], [{ p: [0], lm: 1 }], 'right') + ); + assert.deepEqual( + [{ p: [1], lm: 1 }], + type.transform([{ p: [0], lm: 0 }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 0 }], + type.transform([{ p: [0], lm: 1 }], [{ p: [1], lm: 3 }], 'left') + ); + assert.deepEqual( + [{ p: [3], lm: 1 }], + type.transform([{ p: [2], lm: 1 }], [{ p: [3], lm: 2 }], 'left') + ); + return assert.deepEqual( + [{ p: [3], lm: 3 }], + type.transform([{ p: [3], lm: 2 }], [{ p: [2], lm: 1 }], 'left') + ); + }); + + it('changes indices correctly around a move', function() { + assert.deepEqual( + [{ p: [1, 0], li: {} }], + type.transform([{ p: [0, 0], li: {} }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 0 }], + type.transform([{ p: [1], lm: 0 }], [{ p: [0], ld: {} }], 'left') + ); + assert.deepEqual( + [{ p: [0], lm: 0 }], + type.transform([{ p: [0], lm: 1 }], [{ p: [1], ld: {} }], 'left') + ); + assert.deepEqual( + [{ p: [5], lm: 0 }], + type.transform([{ p: [6], lm: 0 }], [{ p: [2], ld: {} }], 'left') + ); + assert.deepEqual( + [{ p: [1], lm: 0 }], + type.transform([{ p: [1], lm: 0 }], [{ p: [2], ld: {} }], 'left') + ); + assert.deepEqual( + [{ p: [1], lm: 1 }], + type.transform([{ p: [2], lm: 1 }], [{ p: [1], ld: 3 }], 'right') + ); + + assert.deepEqual( + [{ p: [1], ld: {} }], + type.transform([{ p: [2], ld: {} }], [{ p: [1], lm: 2 }], 'right') + ); + assert.deepEqual( + [{ p: [2], ld: {} }], + type.transform([{ p: [1], ld: {} }], [{ p: [2], lm: 1 }], 'left') + ); + + assert.deepEqual( + [{ p: [0], ld: {} }], + type.transform([{ p: [1], ld: {} }], [{ p: [0], lm: 1 }], 'right') + ); + + assert.deepEqual( + [{ p: [0], ld: 1, li: 2 }], + type.transform([{ p: [1], ld: 1, li: 2 }], [{ p: [1], lm: 0 }], 'left') + ); + assert.deepEqual( + [{ p: [0], ld: 2, li: 3 }], + type.transform([{ p: [1], ld: 2, li: 3 }], [{ p: [0], lm: 1 }], 'left') + ); + return assert.deepEqual( + [{ p: [1], ld: 3, li: 4 }], + type.transform([{ p: [0], ld: 3, li: 4 }], [{ p: [1], lm: 0 }], 'left') + ); + }); + + return it('li vs lm', function() { + const li = p => [{ p: [p], li: [] }]; + const lm = (f, t) => [{ p: [f], lm: t }]; + const xf = type.transform; + + assert.deepEqual(li(0), xf(li(0), lm(1, 3), 'left')); + assert.deepEqual(li(1), xf(li(1), lm(1, 3), 'left')); + assert.deepEqual(li(1), xf(li(2), lm(1, 3), 'left')); + assert.deepEqual(li(2), xf(li(3), lm(1, 3), 'left')); + assert.deepEqual(li(4), xf(li(4), lm(1, 3), 'left')); + + assert.deepEqual(lm(2, 4), xf(lm(1, 3), li(0), 'right')); + assert.deepEqual(lm(2, 4), xf(lm(1, 3), li(1), 'right')); + assert.deepEqual(lm(1, 4), xf(lm(1, 3), li(2), 'right')); + assert.deepEqual(lm(1, 4), xf(lm(1, 3), li(3), 'right')); + assert.deepEqual(lm(1, 3), xf(lm(1, 3), li(4), 'right')); + + assert.deepEqual(li(0), xf(li(0), lm(1, 2), 'left')); + assert.deepEqual(li(1), xf(li(1), lm(1, 2), 'left')); + assert.deepEqual(li(1), xf(li(2), lm(1, 2), 'left')); + assert.deepEqual(li(3), xf(li(3), lm(1, 2), 'left')); + + assert.deepEqual(li(0), xf(li(0), lm(3, 1), 'left')); + assert.deepEqual(li(1), xf(li(1), lm(3, 1), 'left')); + assert.deepEqual(li(3), xf(li(2), lm(3, 1), 'left')); + assert.deepEqual(li(4), xf(li(3), lm(3, 1), 'left')); + assert.deepEqual(li(4), xf(li(4), lm(3, 1), 'left')); + + assert.deepEqual(lm(4, 2), xf(lm(3, 1), li(0), 'right')); + assert.deepEqual(lm(4, 2), xf(lm(3, 1), li(1), 'right')); + assert.deepEqual(lm(4, 1), xf(lm(3, 1), li(2), 'right')); + assert.deepEqual(lm(4, 1), xf(lm(3, 1), li(3), 'right')); + assert.deepEqual(lm(3, 1), xf(lm(3, 1), li(4), 'right')); + + assert.deepEqual(li(0), xf(li(0), lm(2, 1), 'left')); + assert.deepEqual(li(1), xf(li(1), lm(2, 1), 'left')); + assert.deepEqual(li(3), xf(li(2), lm(2, 1), 'left')); + return assert.deepEqual(li(3), xf(li(3), lm(2, 1), 'left')); + }); + }); + + describe('object', function() { + it('passes sanity checks', function() { + assert.deepEqual( + { x: 'a', y: 'b' }, + type.apply({ x: 'a' }, [{ p: ['y'], oi: 'b' }]) + ); + assert.deepEqual({}, type.apply({ x: 'a' }, [{ p: ['x'], od: 'a' }])); + return assert.deepEqual( + { x: 'b' }, + type.apply({ x: 'a' }, [{ p: ['x'], od: 'a', oi: 'b' }]) + ); + }); + + it('Ops on deleted elements become noops', function() { + assert.deepEqual( + [], + type.transform([{ p: [1, 0], si: 'hi' }], [{ p: [1], od: 'x' }], 'left') + ); + assert.deepEqual( + [], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 0, i: 'hi' }] }], + [{ p: [1], od: 'x' }], + 'left' + ) + ); + assert.deepEqual( + [], + type.transform( + [{ p: [9], si: 'bite ' }], + [{ p: [], od: 'agimble s', oi: null }], + 'right' + ) + ); + return assert.deepEqual( + [], + type.transform( + [{ p: [], t: 'text0', o: [{ p: 9, i: 'bite ' }] }], + [{ p: [], od: 'agimble s', oi: null }], + 'right' + ) + ); + }); + + it('Ops on replaced elements become noops', function() { + assert.deepEqual( + [], + type.transform( + [{ p: [1, 0], si: 'hi' }], + [{ p: [1], od: 'x', oi: 'y' }], + 'left' + ) + ); + return assert.deepEqual( + [], + type.transform( + [{ p: [1], t: 'text0', o: [{ p: 0, i: 'hi' }] }], + [{ p: [1], od: 'x', oi: 'y' }], + 'left' + ) + ); + }); + + it('Deleted data is changed to reflect edits', function() { + assert.deepEqual( + [{ p: [1], od: 'abc' }], + type.transform([{ p: [1], od: 'a' }], [{ p: [1, 1], si: 'bc' }], 'left') + ); + assert.deepEqual( + [{ p: [1], od: 'abc' }], + type.transform( + [{ p: [1], od: 'a' }], + [{ p: [1], t: 'text0', o: [{ p: 1, i: 'bc' }] }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [], od: 25, oi: [] }], + type.transform([{ p: [], od: 22, oi: [] }], [{ p: [], na: 3 }], 'left') + ); + assert.deepEqual( + [{ p: [], od: { toves: '' }, oi: 4 }], + type.transform( + [{ p: [], od: { toves: 0 }, oi: 4 }], + [{ p: ['toves'], od: 0, oi: '' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [], od: 'thou an', oi: [] }], + type.transform( + [{ p: [], od: 'thou and ', oi: [] }], + [{ p: [7], sd: 'd ' }], + 'left' + ) + ); + assert.deepEqual( + [{ p: [], od: 'thou an', oi: [] }], + type.transform( + [{ p: [], od: 'thou and ', oi: [] }], + [{ p: [], t: 'text0', o: [{ p: 7, d: 'd ' }] }], + 'left' + ) + ); + assert.deepEqual( + [], + type.transform( + [{ p: ['bird'], na: 2 }], + [{ p: [], od: { bird: 38 }, oi: 20 }], + 'right' + ) + ); + assert.deepEqual( + [{ p: [], od: { bird: 40 }, oi: 20 }], + type.transform( + [{ p: [], od: { bird: 38 }, oi: 20 }], + [{ p: ['bird'], na: 2 }], + 'left' + ) + ); + assert.deepEqual( + [{ p: ['He'], od: [] }], + type.transform( + [{ p: ['He'], od: [] }], + [{ p: ['The'], na: -3 }], + 'right' + ) + ); + return assert.deepEqual( + [], + type.transform( + [{ p: ['He'], oi: {} }], + [{ p: [], od: {}, oi: 'the' }], + 'left' + ) + ); + }); + + it('If two inserts are simultaneous, the lefts insert will win', function() { + assert.deepEqual( + [{ p: [1], oi: 'a', od: 'b' }], + type.transform([{ p: [1], oi: 'a' }], [{ p: [1], oi: 'b' }], 'left') + ); + return assert.deepEqual( + [], + type.transform([{ p: [1], oi: 'b' }], [{ p: [1], oi: 'a' }], 'right') + ); + }); + + it('parallel ops on different keys miss each other', function() { + assert.deepEqual( + [{ p: ['a'], oi: 'x' }], + type.transform([{ p: ['a'], oi: 'x' }], [{ p: ['b'], oi: 'z' }], 'left') + ); + assert.deepEqual( + [{ p: ['a'], oi: 'x' }], + type.transform([{ p: ['a'], oi: 'x' }], [{ p: ['b'], od: 'z' }], 'left') + ); + assert.deepEqual( + [{ p: ['in', 'he'], oi: {} }], + type.transform( + [{ p: ['in', 'he'], oi: {} }], + [{ p: ['and'], od: {} }], + 'right' + ) + ); + assert.deepEqual( + [{ p: ['x', 0], si: 'his ' }], + type.transform( + [{ p: ['x', 0], si: 'his ' }], + [{ p: ['y'], od: 0, oi: 1 }], + 'right' + ) + ); + return assert.deepEqual( + [{ p: ['x'], t: 'text0', o: [{ p: 0, i: 'his ' }] }], + type.transform( + [{ p: ['x'], t: 'text0', o: [{ p: 0, i: 'his ' }] }], + [{ p: ['y'], od: 0, oi: 1 }], + 'right' + ) + ); + }); + + it('replacement vs. deletion', () => + assert.deepEqual( + [{ p: [], oi: {} }], + type.transform( + [{ p: [], od: [''], oi: {} }], + [{ p: [], od: [''] }], + 'right' + ) + )); + + it('replacement vs. replacement', function() { + assert.deepEqual( + [], + type.transform( + [{ p: [], od: [''] }, { p: [], oi: {} }], + [{ p: [], od: [''] }, { p: [], oi: null }], + 'right' + ) + ); + assert.deepEqual( + [{ p: [], od: null, oi: {} }], + type.transform( + [{ p: [], od: [''] }, { p: [], oi: {} }], + [{ p: [], od: [''] }, { p: [], oi: null }], + 'left' + ) + ); + assert.deepEqual( + [], + type.transform( + [{ p: [], od: [''], oi: {} }], + [{ p: [], od: [''], oi: null }], + 'right' + ) + ); + assert.deepEqual( + [{ p: [], od: null, oi: {} }], + type.transform( + [{ p: [], od: [''], oi: {} }], + [{ p: [], od: [''], oi: null }], + 'left' + ) + ); + + // test diamond property + const rightOps = [{ p: [], od: null, oi: {} }]; + const leftOps = [{ p: [], od: null, oi: '' }]; + const rightHas = type.apply(null, rightOps); + const leftHas = type.apply(null, leftOps); + + const [left_, right_] = Array.from(transformX(type, leftOps, rightOps)); + assert.deepEqual(leftHas, type.apply(rightHas, left_)); + return assert.deepEqual(leftHas, type.apply(leftHas, right_)); + }); + + return it('An attempt to re-delete a key becomes a no-op', function() { + assert.deepEqual( + [], + type.transform([{ p: ['k'], od: 'x' }], [{ p: ['k'], od: 'x' }], 'left') + ); + return assert.deepEqual( + [], + type.transform( + [{ p: ['k'], od: 'x' }], + [{ p: ['k'], od: 'x' }], + 'right' + ) + ); + }); + }); + + // Skip this as it takes a long time. + return describe.skip('randomizer', function() { + this.timeout(20000); + this.slow(6000); + it('passes', () => fuzzer(type, require('./json0-generator'), 1000)); + + return it('passes with string subtype', function() { + type._testStringSubtype = true; // hack + fuzzer(type, require('./json0-generator'), 1000); + return delete type._testStringSubtype; + }); + }); +}; + +describe('json', () => describe('native type', () => genTests(nativetype))); +//exports.webclient = genTests require('../helpers/webclient').types.json diff --git a/test/mocha.opts b/test/mocha.opts index cf3c054..4d7bcbf 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,2 @@ ---compilers coffee:coffee-script/register --reporter spec --check-leaks diff --git a/test/text0-generator.coffee b/test/text0-generator.coffee deleted file mode 100644 index c3df0d5..0000000 --- a/test/text0-generator.coffee +++ /dev/null @@ -1,30 +0,0 @@ -# Random op generator for the embedded text0 OT type. This is used by the fuzzer -# test. - -{randomReal, randomWord} = require 'ot-fuzzer' -text0 = require '../lib/text0' - -module.exports = genRandomOp = (docStr) -> - pct = 0.9 - - op = [] - - while randomReal() < pct -# console.log "docStr = #{i docStr}" - pct /= 2 - - if randomReal() > 0.5 - # Append an insert - pos = Math.floor(randomReal() * (docStr.length + 1)) - str = randomWord() + ' ' - text0._append op, {i:str, p:pos} - docStr = docStr[...pos] + str + docStr[pos..] - else - # Append a delete - pos = Math.floor(randomReal() * docStr.length) - length = Math.min(Math.floor(randomReal() * 4), docStr.length - pos) - text0._append op, {d:docStr[pos...(pos + length)], p:pos} - docStr = docStr[...pos] + docStr[(pos + length)..] - -# console.log "generated op #{i op} -> #{i docStr}" - [op, docStr] diff --git a/test/text0-generator.js b/test/text0-generator.js new file mode 100644 index 0000000..be9ff07 --- /dev/null +++ b/test/text0-generator.js @@ -0,0 +1,43 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Random op generator for the embedded text0 OT type. This is used by the fuzzer +// test. + +let genRandomOp; +const { randomReal, randomWord } = require('ot-fuzzer'); +const text0 = require('../lib/text0'); + +module.exports = genRandomOp = function(docStr) { + let pct = 0.9; + + const op = []; + + while (randomReal() < pct) { + // console.log "docStr = #{i docStr}" + var pos; + pct /= 2; + + if (randomReal() > 0.5) { + // Append an insert + pos = Math.floor(randomReal() * (docStr.length + 1)); + const str = randomWord() + ' '; + text0._append(op, { i: str, p: pos }); + docStr = docStr.slice(0, pos) + str + docStr.slice(pos); + } else { + // Append a delete + pos = Math.floor(randomReal() * docStr.length); + const length = Math.min( + Math.floor(randomReal() * 4), + docStr.length - pos + ); + text0._append(op, { d: docStr.slice(pos, pos + length), p: pos }); + docStr = docStr.slice(0, pos) + docStr.slice(pos + length); + } + } + + // console.log "generated op #{i op} -> #{i docStr}" + return [op, docStr]; +}; diff --git a/test/text0.coffee b/test/text0.coffee deleted file mode 100644 index 15592dd..0000000 --- a/test/text0.coffee +++ /dev/null @@ -1,120 +0,0 @@ -# Tests for the embedded non-composable text type text0. - -assert = require 'assert' -fuzzer = require 'ot-fuzzer' -text0 = require '../lib/text0' - -describe 'text0', -> - describe 'compose', -> - # Compose is actually pretty easy - it 'is sane', -> - assert.deepEqual text0.compose([], []), [] - assert.deepEqual text0.compose([{i:'x', p:0}], []), [{i:'x', p:0}] - assert.deepEqual text0.compose([], [{i:'x', p:0}]), [{i:'x', p:0}] - assert.deepEqual text0.compose([{i:'y', p:100}], [{i:'x', p:0}]), [{i:'y', p:100}, {i:'x', p:0}] - - describe 'transform', -> - it 'is sane', -> - assert.deepEqual [], text0.transform [], [], 'left' - assert.deepEqual [], text0.transform [], [], 'right' - - assert.deepEqual [{i:'y', p:100}, {i:'x', p:0}], text0.transform [{i:'y', p:100}, {i:'x', p:0}], [], 'left' - assert.deepEqual [], text0.transform [], [{i:'y', p:100}, {i:'x', p:0}], 'right' - - it 'inserts', -> - assert.deepEqual [[{i:'x', p:10}], [{i:'a', p:1}]], text0.transformX [{i:'x', p:9}], [{i:'a', p:1}] - assert.deepEqual [[{i:'x', p:10}], [{i:'a', p:11}]], text0.transformX [{i:'x', p:10}], [{i:'a', p:10}] - - assert.deepEqual [[{i:'x', p:10}], [{d:'a', p:9}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:9}] - assert.deepEqual [[{i:'x', p:10}], [{d:'a', p:10}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:10}] - assert.deepEqual [[{i:'x', p:11}], [{d:'a', p:12}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:11}] - - assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:11}], 'left' - assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:10}], 'left' - assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:10}], 'right' - - it 'deletes', -> - assert.deepEqual [[{d:'abc', p:8}], [{d:'xy', p:4}]], text0.transformX [{d:'abc', p:10}], [{d:'xy', p:4}] - assert.deepEqual [[{d:'ac', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'b', p:11}] - assert.deepEqual [[], [{d:'ac', p:10}]], text0.transformX [{d:'b', p:11}], [{d:'abc', p:10}] - assert.deepEqual [[{d:'a', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'bc', p:11}] - assert.deepEqual [[{d:'c', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'ab', p:10}] - assert.deepEqual [[{d:'a', p:10}], [{d:'d', p:10}]], text0.transformX [{d:'abc', p:10}], [{d:'bcd', p:11}] - assert.deepEqual [[{d:'d', p:10}], [{d:'a', p:10}]], text0.transformX [{d:'bcd', p:11}], [{d:'abc', p:10}] - assert.deepEqual [[{d:'abc', p:10}], [{d:'xy', p:10}]], text0.transformX [{d:'abc', p:10}], [{d:'xy', p:13}] - - describe 'transformCursor', -> - it 'is sane', -> - assert.strictEqual 0, text0.transformCursor 0, [], 'right' - assert.strictEqual 0, text0.transformCursor 0, [], 'left' - assert.strictEqual 100, text0.transformCursor 100, [] - - it 'works vs insert', -> - assert.strictEqual 0, text0.transformCursor 0, [{i:'asdf', p:100}], 'right' - assert.strictEqual 0, text0.transformCursor 0, [{i:'asdf', p:100}], 'left' - - assert.strictEqual 204, text0.transformCursor 200, [{i:'asdf', p:100}], 'right' - assert.strictEqual 204, text0.transformCursor 200, [{i:'asdf', p:100}], 'left' - - assert.strictEqual 104, text0.transformCursor 100, [{i:'asdf', p:100}], 'right' - assert.strictEqual 100, text0.transformCursor 100, [{i:'asdf', p:100}], 'left' - - it 'works vs delete', -> - assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}], 'right' - assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}], 'left' - assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}] - - assert.strictEqual 196, text0.transformCursor 200, [{d:'asdf', p:100}] - - assert.strictEqual 100, text0.transformCursor 100, [{d:'asdf', p:100}] - assert.strictEqual 100, text0.transformCursor 102, [{d:'asdf', p:100}] - assert.strictEqual 100, text0.transformCursor 104, [{d:'asdf', p:100}] - assert.strictEqual 101, text0.transformCursor 105, [{d:'asdf', p:100}] - - describe 'normalize', -> - it 'is sane', -> - testUnchanged = (op) -> assert.deepEqual op, text0.normalize op - testUnchanged [] - testUnchanged [{i:'asdf', p:100}] - testUnchanged [{i:'asdf', p:100}, {d:'fdsa', p:123}] - - it 'adds missing p:0', -> - assert.deepEqual [{i:'abc', p:0}], text0.normalize [{i:'abc'}] - assert.deepEqual [{d:'abc', p:0}], text0.normalize [{d:'abc'}] - assert.deepEqual [{i:'abc', p:0}, {d:'abc', p:0}], text0.normalize [{i:'abc'}, {d:'abc'}] - - it 'converts op to an array', -> - assert.deepEqual [{i:'abc', p:0}], text0.normalize {i:'abc', p:0} - assert.deepEqual [{d:'abc', p:0}], text0.normalize {d:'abc', p:0} - - it 'works with a really simple op', -> - assert.deepEqual [{i:'abc', p:0}], text0.normalize {i:'abc'} - - it 'compress inserts', -> - assert.deepEqual [{i:'xyzabc', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:10}] - assert.deepEqual [{i:'axyzbc', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:11}] - assert.deepEqual [{i:'abcxyz', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:13}] - - it 'doesnt compress separate inserts', -> - t = (op) -> assert.deepEqual op, text0.normalize op - - t [{i:'abc', p:10}, {i:'xyz', p:9}] - t [{i:'abc', p:10}, {i:'xyz', p:14}] - - it 'compress deletes', -> - assert.deepEqual [{d:'xyabc', p:8}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:8}] - assert.deepEqual [{d:'xabcy', p:9}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:9}] - assert.deepEqual [{d:'abcxy', p:10}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:10}] - - it 'doesnt compress separate deletes', -> - t = (op) -> assert.deepEqual op, text0.normalize op - - t [{d:'abc', p:10}, {d:'xyz', p:6}] - t [{d:'abc', p:10}, {d:'xyz', p:11}] - - - describe 'randomizer', -> it 'passes', -> - @timeout 4000 - @slow 4000 - fuzzer text0, require('./text0-generator') - diff --git a/test/text0.js b/test/text0.js new file mode 100644 index 0000000..151c54b --- /dev/null +++ b/test/text0.js @@ -0,0 +1,617 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Tests for the embedded non-composable text type text0. + +const assert = require('assert'); +const fuzzer = require('ot-fuzzer'); +const text0 = require('../lib/text0'); + +describe('text0', function() { + describe('compose', () => + // Compose is actually pretty easy + it('is sane', function() { + assert.deepEqual( + text0.compose( + [], + [] + ), + [] + ); + assert.deepEqual( + text0.compose( + [{ i: 'x', p: 0 }], + [] + ), + [{ i: 'x', p: 0 }] + ); + assert.deepEqual( + text0.compose( + [], + [{ i: 'x', p: 0 }] + ), + [{ i: 'x', p: 0 }] + ); + return assert.deepEqual( + text0.compose( + [{ i: 'y', p: 100 }], + [{ i: 'x', p: 0 }] + ), + [{ i: 'y', p: 100 }, { i: 'x', p: 0 }] + ); + })); + + describe('transform', function() { + it('is sane', function() { + assert.deepEqual([], text0.transform([], [], 'left')); + assert.deepEqual([], text0.transform([], [], 'right')); + + assert.deepEqual( + [{ i: 'y', p: 100 }, { i: 'x', p: 0 }], + text0.transform([{ i: 'y', p: 100 }, { i: 'x', p: 0 }], [], 'left') + ); + return assert.deepEqual( + [], + text0.transform([], [{ i: 'y', p: 100 }, { i: 'x', p: 0 }], 'right') + ); + }); + + it('inserts', function() { + assert.deepEqual( + [[{ i: 'x', p: 10 }], [{ i: 'a', p: 1 }]], + text0.transformX([{ i: 'x', p: 9 }], [{ i: 'a', p: 1 }]) + ); + assert.deepEqual( + [[{ i: 'x', p: 10 }], [{ i: 'a', p: 11 }]], + text0.transformX([{ i: 'x', p: 10 }], [{ i: 'a', p: 10 }]) + ); + + assert.deepEqual( + [[{ i: 'x', p: 10 }], [{ d: 'a', p: 9 }]], + text0.transformX([{ i: 'x', p: 11 }], [{ d: 'a', p: 9 }]) + ); + assert.deepEqual( + [[{ i: 'x', p: 10 }], [{ d: 'a', p: 10 }]], + text0.transformX([{ i: 'x', p: 11 }], [{ d: 'a', p: 10 }]) + ); + assert.deepEqual( + [[{ i: 'x', p: 11 }], [{ d: 'a', p: 12 }]], + text0.transformX([{ i: 'x', p: 11 }], [{ d: 'a', p: 11 }]) + ); + + assert.deepEqual( + [{ i: 'x', p: 10 }], + text0.transform([{ i: 'x', p: 10 }], [{ d: 'a', p: 11 }], 'left') + ); + assert.deepEqual( + [{ i: 'x', p: 10 }], + text0.transform([{ i: 'x', p: 10 }], [{ d: 'a', p: 10 }], 'left') + ); + return assert.deepEqual( + [{ i: 'x', p: 10 }], + text0.transform([{ i: 'x', p: 10 }], [{ d: 'a', p: 10 }], 'right') + ); + }); + + return it('deletes', function() { + assert.deepEqual( + [[{ d: 'abc', p: 8 }], [{ d: 'xy', p: 4 }]], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'xy', p: 4 }]) + ); + assert.deepEqual( + [[{ d: 'ac', p: 10 }], []], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'b', p: 11 }]) + ); + assert.deepEqual( + [[], [{ d: 'ac', p: 10 }]], + text0.transformX([{ d: 'b', p: 11 }], [{ d: 'abc', p: 10 }]) + ); + assert.deepEqual( + [[{ d: 'a', p: 10 }], []], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'bc', p: 11 }]) + ); + assert.deepEqual( + [[{ d: 'c', p: 10 }], []], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'ab', p: 10 }]) + ); + assert.deepEqual( + [[{ d: 'a', p: 10 }], [{ d: 'd', p: 10 }]], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'bcd', p: 11 }]) + ); + assert.deepEqual( + [[{ d: 'd', p: 10 }], [{ d: 'a', p: 10 }]], + text0.transformX([{ d: 'bcd', p: 11 }], [{ d: 'abc', p: 10 }]) + ); + return assert.deepEqual( + [[{ d: 'abc', p: 10 }], [{ d: 'xy', p: 10 }]], + text0.transformX([{ d: 'abc', p: 10 }], [{ d: 'xy', p: 13 }]) + ); + }); + }); + + describe('transformCursor', function() { + it('is sane', function() { + assert.strictEqual(0, text0.transformCursor(0, [], 'right')); + assert.strictEqual(0, text0.transformCursor(0, [], 'left')); + return assert.strictEqual(100, text0.transformCursor(100, [])); + }); + + it('works vs insert', function() { + assert.strictEqual( + 0, + text0.transformCursor(0, [{ i: 'asdf', p: 100 }], 'right') + ); + assert.strictEqual( + 0, + text0.transformCursor(0, [{ i: 'asdf', p: 100 }], 'left') + ); + + assert.strictEqual( + 204, + text0.transformCursor(200, [{ i: 'asdf', p: 100 }], 'right') + ); + assert.strictEqual( + 204, + text0.transformCursor(200, [{ i: 'asdf', p: 100 }], 'left') + ); + + assert.strictEqual( + 104, + text0.transformCursor(100, [{ i: 'asdf', p: 100 }], 'right') + ); + return assert.strictEqual( + 100, + text0.transformCursor(100, [{ i: 'asdf', p: 100 }], 'left') + ); + }); + + return it('works vs delete', function() { + assert.strictEqual( + 0, + text0.transformCursor(0, [{ d: 'asdf', p: 100 }], 'right') + ); + assert.strictEqual( + 0, + text0.transformCursor(0, [{ d: 'asdf', p: 100 }], 'left') + ); + assert.strictEqual(0, text0.transformCursor(0, [{ d: 'asdf', p: 100 }])); + + assert.strictEqual( + 196, + text0.transformCursor(200, [{ d: 'asdf', p: 100 }]) + ); + + assert.strictEqual( + 100, + text0.transformCursor(100, [{ d: 'asdf', p: 100 }]) + ); + assert.strictEqual( + 100, + text0.transformCursor(102, [{ d: 'asdf', p: 100 }]) + ); + assert.strictEqual( + 100, + text0.transformCursor(104, [{ d: 'asdf', p: 100 }]) + ); + return assert.strictEqual( + 101, + text0.transformCursor(105, [{ d: 'asdf', p: 100 }]) + ); + }); + }); + + describe('normalize', function() { + it('is sane', function() { + const testUnchanged = op => assert.deepEqual(op, text0.normalize(op)); + testUnchanged([]); + testUnchanged([{ i: 'asdf', p: 100 }]); + return testUnchanged([{ i: 'asdf', p: 100 }, { d: 'fdsa', p: 123 }]); + }); + + it('adds missing p:0', function() { + assert.deepEqual([{ i: 'abc', p: 0 }], text0.normalize([{ i: 'abc' }])); + assert.deepEqual([{ d: 'abc', p: 0 }], text0.normalize([{ d: 'abc' }])); + return assert.deepEqual( + [{ i: 'abc', p: 0 }, { d: 'abc', p: 0 }], + text0.normalize([{ i: 'abc' }, { d: 'abc' }]) + ); + }); + + it('converts op to an array', function() { + assert.deepEqual( + [{ i: 'abc', p: 0 }], + text0.normalize({ i: 'abc', p: 0 }) + ); + return assert.deepEqual( + [{ d: 'abc', p: 0 }], + text0.normalize({ d: 'abc', p: 0 }) + ); + }); + + it('works with a really simple op', () => + assert.deepEqual([{ i: 'abc', p: 0 }], text0.normalize({ i: 'abc' }))); + + it('compress inserts', function() { + assert.deepEqual( + [{ i: 'xyzabc', p: 10 }], + text0.normalize([{ i: 'abc', p: 10 }, { i: 'xyz', p: 10 }]) + ); + assert.deepEqual( + [{ i: 'axyzbc', p: 10 }], + text0.normalize([{ i: 'abc', p: 10 }, { i: 'xyz', p: 11 }]) + ); + return assert.deepEqual( + [{ i: 'abcxyz', p: 10 }], + text0.normalize([{ i: 'abc', p: 10 }, { i: 'xyz', p: 13 }]) + ); + }); + + it('doesnt compress separate inserts', function() { + const t = op => assert.deepEqual(op, text0.normalize(op)); + + t([{ i: 'abc', p: 10 }, { i: 'xyz', p: 9 }]); + return t([{ i: 'abc', p: 10 }, { i: 'xyz', p: 14 }]); + }); + + it('compress deletes', function() { + assert.deepEqual( + [{ d: 'xyabc', p: 8 }], + text0.normalize([{ d: 'abc', p: 10 }, { d: 'xy', p: 8 }]) + ); + assert.deepEqual( + [{ d: 'xabcy', p: 9 }], + text0.normalize([{ d: 'abc', p: 10 }, { d: 'xy', p: 9 }]) + ); + return assert.deepEqual( + [{ d: 'abcxy', p: 10 }], + text0.normalize([{ d: 'abc', p: 10 }, { d: 'xy', p: 10 }]) + ); + }); + + return it('doesnt compress separate deletes', function() { + const t = op => assert.deepEqual(op, text0.normalize(op)); + + t([{ d: 'abc', p: 10 }, { d: 'xyz', p: 6 }]); + return t([{ d: 'abc', p: 10 }, { d: 'xyz', p: 11 }]); + }); + }); + + // Skip this as it takes a long time. + describe.skip('randomizer', () => + it('passes', function() { + this.timeout(4000); + this.slow(4000); + return fuzzer(text0, require('./text0-generator')); + })); + + describe('createPresence', function() { + it('basic tests', function() { + const defaultPresence = { u: '', c: 0, s: [] }; + const presence = { u: '5', c: 8, s: [[1, 2], [9, 5]] }; + + assert.deepEqual(createPresence(), defaultPresence); + assert.deepEqual(createPresence(null), defaultPresence); + assert.deepEqual(createPresence(true), defaultPresence); + assert.deepEqual( + createPresence({ u: 5, c: 8, s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: '8', s: [1, 2] }), + defaultPresence + ); + assert.deepEqual( + createPresence({ u: '5', c: 8, s: [1.5, 2] }), + defaultPresence + ); + assert.strictEqual(createPresence(presence), presence); + }); + }); + + describe('transformPresence', function() { + it('basic tests', function() { + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [], + true + ), + { + u: 'user', + c: 8, + s: [[5, 7]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [], + false + ), + { + u: 'user', + c: 8, + s: [[5, 7]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(3), createDelete(2), createInsertText('a')], + true + ), + { + u: 'user', + c: 8, + s: [[4, 6]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(3), createDelete(2), createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[3, 6]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(5), createDelete(2), createInsertText('a')], + true + ), + { + u: 'user', + c: 8, + s: [[6, 6]] + } + ); + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7]] + }, + [createRetain(5), createDelete(2), createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[5, 5]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[5, 7], [8, 2]] + }, + [createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[6, 8], [9, 3]] + } + ); + + assert.deepEqual( + transformPresence( + { + u: 'user', + c: 8, + s: [[1, 1], [2, 2]] + }, + [createInsertText('a')], + false + ), + { + u: 'user', + c: 8, + s: [[2, 2], [3, 3]] + } + ); + }); + }); + + describe('comparePresence', function() { + it('basic tests', function() { + assert.strictEqual(comparePresence(), true); + assert.strictEqual(comparePresence(undefined, undefined), true); + assert.strictEqual(comparePresence(null, null), true); + assert.strictEqual(comparePresence(null, undefined), false); + assert.strictEqual(comparePresence(undefined, null), false); + assert.strictEqual( + comparePresence(undefined, { u: '', c: 0, s: [] }), + false + ); + assert.strictEqual(comparePresence(null, { u: '', c: 0, s: [] }), false); + assert.strictEqual( + comparePresence({ u: '', c: 0, s: [] }, undefined), + false + ); + assert.strictEqual(comparePresence({ u: '', c: 0, s: [] }, null), false); + + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 2]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2], [4, 6]] }, + { u: 'user', c: 8, s: [[1, 2], [4, 6]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 }, + { u: 'user', c: 8, s: [[1, 2]] } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 2]], unknownProperty: 5 } + ), + true + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'userX', c: 8, s: [[1, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 9, s: [[1, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[3, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[1, 2]] }, + { u: 'user', c: 8, s: [[1, 3]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8], [3, 2]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8], [1, 3]] } + ), + false + ); + assert.strictEqual( + comparePresence( + { u: 'user', c: 8, s: [[9, 8], [1, 2]] }, + { u: 'user', c: 8, s: [[9, 8]] } + ), + false + ); + }); + }); + + describe('isValidPresence', function() { + it('basic tests', function() { + assert.strictEqual(isValidPresence(), false); + assert.strictEqual(isValidPresence(null), false); + assert.strictEqual(isValidPresence([]), false); + assert.strictEqual(isValidPresence({}), false); + assert.strictEqual(isValidPresence({ u: 5, c: 8, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: '8', s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8.5, s: [] }), false); + assert.strictEqual( + isValidPresence({ u: '5', c: Infinity, s: [] }), + false + ); + assert.strictEqual(isValidPresence({ u: '5', c: NaN, s: [] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: {} }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [] }), true); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[]] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1]] }), false); + assert.strictEqual(isValidPresence({ u: '5', c: 8, s: [[1, 2]] }), true); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2, 3]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], []] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, '6']] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, 6.1]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, Infinity]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, NaN]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -0]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], [3, -1]] }), + true + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, 2], ['3', 0]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [[1, '2'], [4, 0]] }), + false + ); + assert.strictEqual( + isValidPresence({ u: '5', c: 8, s: [['1', 2], [4, 0]] }), + false + ); + }); + }); +});