diff --git a/package-lock.json b/package-lock.json index b194d5c..8917150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,382 +4,51 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@goto-bus-stop/common-shake": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@goto-bus-stop/common-shake/-/common-shake-2.2.0.tgz", - "integrity": "sha512-AlNzclZ0UebzHyxYfHSKqmlwCtwcECirZDLhN96gGuj5oHAkba/27+4AlCWyXaycM9cUh12L8/2vjmvjn60pkQ==", - "requires": { - "acorn": "^5.1.1", - "debug": "^2.6.8", - "escope": "^3.6.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "acorn": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", - "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==" - }, - "acorn-dynamic-import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", - "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==" - }, - "acorn-node": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.2.tgz", - "integrity": "sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg==", - "requires": { - "acorn": "^6.0.2", - "acorn-dynamic-import": "^4.0.0", - "acorn-walk": "^6.1.0", - "xtend": "^4.0.1" - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "browser-pack": { - "version": "5.0.1", - "resolved": "http://registry.npmjs.org/browser-pack/-/browser-pack-5.0.1.tgz", - "integrity": "sha1-QZdxmyDG4KqglFHFER5T77b7wY0=", - "requires": { - "JSONStream": "^1.0.3", - "combine-source-map": "~0.6.1", - "defined": "^1.0.0", - "through2": "^1.0.0", - "umd": "^3.0.0" - }, - "dependencies": { - "combine-source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.6.1.tgz", - "integrity": "sha1-m0oJwxYDPXaODxHgKfonMOB5rZY=", - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.5.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.4.2" - } - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - }, - "inline-source-map": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.5.0.tgz", - "integrity": "sha1-Skxd2OT7Xps82mDIIt+tyu5m4K8=", - "requires": { - "source-map": "~0.4.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "source-map": { - "version": "0.4.4", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", - "requires": { - "readable-stream": ">=1.1.13-1 <1.2.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "browser-pack-flat": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/browser-pack-flat/-/browser-pack-flat-3.2.0.tgz", - "integrity": "sha512-tk/LexpgMImZyDfpWSPyIlQ3frZYTyGLpW+Ytd0Fj9VW03Fil9IrKzcVKN87wZHWhP6LbdKh3STRnIkHIR+UTQ==", - "requires": { - "JSONStream": "^1.3.2", - "combine-source-map": "^0.8.0", - "convert-source-map": "^1.5.1", - "count-lines": "^0.1.2", - "dedent": "^0.7.0", - "estree-is-member-expression": "^1.0.0", - "estree-is-require": "^1.0.0", - "esutils": "^2.0.2", - "path-parse": "^1.0.5", - "scope-analyzer": "^2.0.0", - "stream-combiner": "^0.2.2", - "through2": "^2.0.3", - "transform-ast": "^2.4.2", - "umd": "^3.0.3", - "wrap-comment": "^1.0.0" - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "browser-unpack": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-unpack/-/browser-unpack-1.3.0.tgz", - "integrity": "sha512-hK81IeAN/PcqzSKTqKWe3jzmB41PV/23N1w1w2N/KBPv+sjp31sMlen9arVIENzrH8IzaCWA2Fx6SV0kJyhAFQ==", - "requires": { - "acorn": "^5.6.2", - "concat-stream": "^1.5.0", - "minimist": "^1.1.1" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - }, - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "bundle-collapser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bundle-collapser/-/bundle-collapser-1.3.0.tgz", - "integrity": "sha1-9LT/WLLyLudwGyD6djBuI/U6P7Y=", - "requires": { - "browser-pack": "^5.0.1", - "browser-unpack": "^1.1.0", - "concat-stream": "^1.5.0", - "falafel": "^2.1.0", - "minimist": "^1.1.1", - "through2": "^2.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "call-matcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/call-matcher/-/call-matcher-1.1.0.tgz", - "integrity": "sha512-IoQLeNwwf9KTNbtSA7aEBb1yfDbdnzwjCetjkC8io5oGeOmK2CBNdg0xr+tadRYKO0p7uQyZzvon0kXlZbvGrw==", - "requires": { - "core-js": "^2.0.0", - "deep-equal": "^1.0.0", - "espurify": "^1.6.0", - "estraverse": "^4.0.0" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, "coffeescript": { - "version": "2.3.2", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.4.1.tgz", + "integrity": "sha512-34GV1aHrsMpTaO3KfMJL40ZNuvKDR/g98THHnE9bQj8HjMaZvSrLik99WWqyMhRtbe8V5hpx5iLgdcSvM/S2wg==", "dev": true }, - "combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - }, - "dependencies": { - "convert-source-map": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, "commander": { "version": "2.15.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, - "common-shakeify": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/common-shakeify/-/common-shakeify-0.5.2.tgz", - "integrity": "sha512-znhuzdj4zgvMz5u6cM9FI1WFcxTJ8lvGGW1FaaU0eMZ9o9LqIy3j43SrUYsQJKwdI8+1p/55YpeHTz68G+0Zsw==", - "requires": { - "@goto-bus-stop/common-shake": "^2.2.0", - "convert-source-map": "^1.5.1", - "through2": "^2.0.3", - "transform-ast": "^2.4.3", - "wrap-comment": "^1.0.1" - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "core-js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.0.tgz", - "integrity": "sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "count-lines": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/count-lines/-/count-lines-0.1.2.tgz", - "integrity": "sha1-4zST+2hgqC9xWdgjeEP7+u/uWWI=" - }, - "d": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "requires": { - "es5-ext": "^0.10.9" - } - }, - "dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "debug": { "version": "3.1.0", @@ -390,282 +59,17 @@ "ms": "2.0.0" } }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, - "duplexer": { - "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, - "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "envify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz", - "integrity": "sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==", - "requires": { - "esprima": "^4.0.0", - "through": "~2.3.4" - } - }, - "es5-ext": { - "version": "0.10.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", - "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "espurify": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.8.1.tgz", - "integrity": "sha512-ZDko6eY/o+D/gHCWyHTU85mKDgYcS4FJj7S+YD6WIInm7GQ6AnOjmcL4+buFV/JOztVLELi/7MmuGU5NHta0Mg==", - "requires": { - "core-js": "^2.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "estree-is-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz", - "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==" - }, - "estree-is-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-identifier/-/estree-is-identifier-1.0.0.tgz", - "integrity": "sha512-2BDRGrkQJV/NhCAmmE33A35WAaxq3WQaGHgQuD//7orGWfpFqj8Srkwvx0TH+20yIdOF1yMQwi8anv5ISec2AQ==" - }, - "estree-is-member-expression": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-member-expression/-/estree-is-member-expression-1.0.0.tgz", - "integrity": "sha512-Ec+X44CapIGExvSZN+pGkmr5p7HwUVQoPQSd458Lqwvaf4/61k/invHSh4BYK8OXnCkfEhWuIoG5hayKLQStIg==" - }, - "estree-is-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-require/-/estree-is-require-1.0.0.tgz", - "integrity": "sha512-oWxQdSEmnUwNZsDQYiBNpVxKEhMmsJQSSxnDrwsr1MWtooCLfhgzsNGzmokdmfK0EzEIS5V4LPvqxv1Kmb1vvA==", - "requires": { - "estree-is-identifier": "^1.0.0" - } - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "extend": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", - "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=" - }, - "falafel": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz", - "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=", - "requires": { - "acorn": "^5.0.0", - "foreach": "^2.0.5", - "isarray": "0.0.1", - "object-keys": "^1.0.6" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "from2-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz", - "integrity": "sha1-GCgrJ9CKJnyzAwzSuLSw8hKvdSo=", - "requires": { - "from2": "^2.0.3" - } + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "fs.realpath": { "version": "1.0.0", @@ -673,11 +77,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" - }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -698,14 +97,6 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -731,105 +122,27 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "requires": { - "source-map": "~0.5.3" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" - }, - "magic-string": { - "version": "0.23.2", - "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.23.2.tgz", - "integrity": "sha512-oIUZaAxbcxYIp4AyLafV6OVKoB3YouZs0UTCJ8mOKBHNyJgGDaMJ4TgA+VylJh6fx7EQCC52XkbURxxG9IoJXA==", - "requires": { - "sourcemap-codec": "^1.4.1" - } - }, - "merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", - "requires": { - "source-map": "^0.5.6" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "minify-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minify-stream/-/minify-stream-1.2.0.tgz", - "integrity": "sha512-bIjBH7uGROwzWwgtbLO7U/yi+NBTLGs5YYidUiGD9nJZ5wuxX0485c48vtJ7WlNZNnKvHXA1D1ZXpfWJqf4fyg==", - "requires": { - "concat-stream": "^1.6.0", - "convert-source-map": "^1.5.0", - "duplexify": "^3.5.1", - "from2-string": "^1.1.0", - "terser": "^3.7.5", - "xtend": "^4.0.1" - } + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -838,6 +151,8 @@ }, "mocha": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { "browser-stdout": "1.3.1", @@ -856,509 +171,66 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multi-stage-sourcemap": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.2.1.tgz", - "integrity": "sha1-sJ/IWG6qF/gdV1xK0C4Pej9rEQU=", - "requires": { - "source-map": "^0.1.34" - }, - "dependencies": { - "source-map": { - "version": "0.1.43", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "mutexify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.2.0.tgz", - "integrity": "sha512-oprzxd2zhfrJqEuB98qc1dRMMonClBQ57UPDjnbcrah4orEMTq1jq3+AcdFe5ePzdbJXI7zmdhfftIdMnhYFoQ==" - }, - "nanobench": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nanobench/-/nanobench-2.1.1.tgz", - "integrity": "sha512-z+Vv7zElcjN+OpzAxAquUayFLGK3JI/ubCl0Oh64YQqsTGG09CGqieJVQw4ui8huDnnAgrvTv93qi5UaOoNj8A==", - "requires": { - "browser-process-hrtime": "^0.1.2", - "chalk": "^1.1.3", - "mutexify": "^1.1.0", - "pretty-hrtime": "^1.0.2" - } - }, - "next-tick": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, "ot-fuzzer": { - "version": "1.2.0", - "dev": true, - "requires": { - "cli-progress": "^2.1.1", - "seedrandom": "^2.4.4" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "cli-progress": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "colors": "^1.1.2", - "string-width": "^2.1.1" - } - }, - "coffee-script": { - "version": "1.12.7", - "bundled": true - }, - "colors": { - "version": "1.3.3", - "bundled": true, - "dev": true - }, - "commander": { - "version": "2.3.0", - "bundled": true - }, - "debug": { - "version": "2.0.0", - "bundled": true, - "requires": { - "ms": "0.6.2" - } - }, - "diff": { - "version": "1.0.8", - "bundled": true - }, - "escape-string-regexp": { - "version": "1.0.2", - "bundled": true - }, - "glob": { - "version": "3.2.3", - "bundled": true, - "requires": { - "graceful-fs": "~2.0.0", - "inherits": "2", - "minimatch": "~0.2.11" - } - }, - "graceful-fs": { - "version": "2.0.3", - "bundled": true - }, - "growl": { - "version": "1.8.1", - "bundled": true - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "jade": { - "version": "0.26.3", - "bundled": true, - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "bundled": true - }, - "mkdirp": { - "version": "0.3.0", - "bundled": true - } - } - }, - "lru-cache": { - "version": "2.7.3", - "bundled": true - }, - "minimatch": { - "version": "0.2.14", - "bundled": true, - "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "mkdirp": { - "version": "0.5.0", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "1.21.5", - "bundled": true, - "requires": { - "commander": "2.3.0", - "debug": "2.0.0", - "diff": "1.0.8", - "escape-string-regexp": "1.0.2", - "glob": "3.2.3", - "growl": "1.8.1", - "jade": "0.26.3", - "mkdirp": "0.5.0" - } - }, - "ms": { - "version": "0.6.2", - "bundled": true - }, - "seedrandom": { - "version": "2.4.4", - "bundled": true, - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "bundled": true - }, - "string-width": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ot-fuzzer/-/ot-fuzzer-1.1.0.tgz", + "integrity": "sha512-48w+h509Sq48JMsit5/qPCxhWIzztS52DkhIztqiFV0UT2MA6BWu2JiLHSw6cLren1O5IWhNjU7LObOmVOAFGg==", + "dev": true }, "ot-simple": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ot-simple/-/ot-simple-1.0.0.tgz", + "integrity": "sha1-B42ED4HqOq04y+aUdfgbLGdU+1A=", "dev": true }, "ot-text": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ot-text/-/ot-text-1.0.2.tgz", + "integrity": "sha512-1xjcAjB57tYtv722j8J+IaYe4fKZOeaQ3zDa0clUlkhboawrq+dk1cpDfe4+TBSb9NNe7Yra9+tJKsw+8q4H9A==", "dev": true }, "ot-text-unicode": { - "version": "2.0.0", - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "bundled": true - }, - "coffeescript": { - "bundled": true - }, - "commander": { - "version": "2.15.1", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "debug": { - "version": "3.1.0", - "bundled": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "3.5.0", - "bundled": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "bundled": true - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "growl": { - "version": "1.10.5", - "bundled": true - }, - "has-flag": { - "version": "3.0.0", - "bundled": true - }, - "he": { - "version": "1.1.1", - "bundled": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "bundled": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1" - } - }, - "ot-fuzzer": { - "bundled": true - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "supports-color": { - "version": "5.4.0", - "bundled": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "typescript": { - "bundled": true - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ot-text-unicode/-/ot-text-unicode-3.0.0.tgz", + "integrity": "sha512-YwVxEFVLV9WE40KavenjlHy3Y0C2OZoJeNCHKEpJlmu/scJaY9tlSrlYNBU7WJgboIz/IoKsd0Poxf+SYLRmrw==", + "requires": { + "unicount": "^1.0.0" } }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "scope-analyzer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.0.5.tgz", - "integrity": "sha512-+U5H0417mnTEstCD5VwOYO7V4vYuSqwqjFap40ythe67bhMFL5C3UgPwyBv7KDJsqUBIKafOD57xMlh1rN7eaw==", - "requires": { - "array-from": "^2.1.1", - "es6-map": "^0.1.5", - "es6-set": "^0.1.5", - "es6-symbol": "^3.1.1", - "estree-is-function": "^1.0.0", - "get-assigned-identifiers": "^1.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "sourcemap-codec": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz", - "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==" - }, - "stream-combiner": { - "version": "0.2.2", - "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "requires": { - "duplexer": "~0.1.1", - "through": "~2.3.4" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", @@ -1372,6 +244,7 @@ "version": "3.11.0", "resolved": "https://registry.npmjs.org/terser/-/terser-3.11.0.tgz", "integrity": "sha512-5iLMdhEPIq3zFWskpmbzmKwMQixKmTYwY3Ox9pjtSklBLnHiuQ0GKJLhL1HSYtyffHM3/lDIFBnb82m9D7ewwQ==", + "dev": true, "requires": { "commander": "~2.17.1", "source-map": "~0.6.1", @@ -1381,154 +254,21 @@ "commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "tinyify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tinyify/-/tinyify-2.5.0.tgz", - "integrity": "sha512-SdKsUZM0k57hwdhjqZedWI4YSUcuVMmJabP6kOAvwHSlJJFR9PkTJ5meIiBRwjOnVwc0Q1xiTdj/FjzyGi099A==", - "requires": { - "browser-pack-flat": "^3.0.9", - "bundle-collapser": "^1.3.0", - "common-shakeify": "^0.5.2", - "envify": "^4.1.0", - "minify-stream": "^1.1.0", - "uglifyify": "^5.0.0", - "unassertify": "^2.1.1" - } - }, - "transform-ast": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/transform-ast/-/transform-ast-2.4.4.tgz", - "integrity": "sha512-AxjeZAcIOUO2lev2GDe3/xZ1Q0cVGjIMk5IsriTy8zbWlsEnjeB025AhkhBJHoy997mXpLd4R+kRbvnnQVuQHQ==", - "requires": { - "acorn-node": "^1.3.0", - "convert-source-map": "^1.5.1", - "dash-ast": "^1.0.0", - "is-buffer": "^2.0.0", - "magic-string": "^0.23.2", - "merge-source-map": "1.0.4", - "nanobench": "^2.1.1" - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uglifyify": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/uglifyify/-/uglifyify-5.0.1.tgz", - "integrity": "sha512-PO44rgExvwj3rkK0UzenHVnPU18drBy9x9HOUmgkuRh6K2KIsDqrB5LqxGtjybgGTOS1JeP8SBc+TN5rhiva6w==", - "requires": { - "convert-source-map": "~1.1.0", - "extend": "^1.2.1", - "minimatch": "^3.0.2", - "terser": "^3.7.5", - "through": "~2.3.4" - }, - "dependencies": { - "convert-source-map": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - } - } - }, - "umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==" - }, - "unassert": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/unassert/-/unassert-1.5.1.tgz", - "integrity": "sha1-y8iOw4dBfFpeTALTzQe+mL11/3Y=", - "requires": { - "acorn": "^4.0.0", - "call-matcher": "^1.0.1", - "deep-equal": "^1.0.0", - "espurify": "^1.3.0", - "estraverse": "^4.1.0", - "esutils": "^2.0.2", - "object-assign": "^4.1.0" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } - }, - "unassertify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/unassertify/-/unassertify-2.1.1.tgz", - "integrity": "sha512-YIAaIlc6/KC9Oib8cVZLlpDDhK1UTEuaDyx9BwD97xqxDZC0cJOqwFcs/Y6K3m73B5VzHsRTBLXNO0dxS/GkTw==", - "requires": { - "acorn": "^5.1.0", - "convert-source-map": "^1.1.1", - "escodegen": "^1.6.1", - "multi-stage-sourcemap": "^0.2.1", - "through": "^2.3.7", - "unassert": "^1.3.1" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true } } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "wordwrap": { + "unicount": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-comment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wrap-comment/-/wrap-comment-1.0.1.tgz", - "integrity": "sha512-APccrMwl/ont0RHFTXNAQfM647duYYEfs6cngrIyTByTI0xbWnDnPSptFZhS68L4WCjt2ZxuhCFwuY6Pe88KZQ==" + "resolved": "https://registry.npmjs.org/unicount/-/unicount-1.0.0.tgz", + "integrity": "sha512-laO0gw4tnW759KI25w8z+58D4K+pnXehZvfKdFj3meYn/NSH85/0I8Gb4jj32tuZaX7zv1rSeP2K87rdChIZUw==" }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true } } } diff --git a/package.json b/package.json index af4f737..df7bcff 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ot-text-unicode": "^3.0.0" }, "devDependencies": { - "coffeescript": "^2.3.2", + "coffeescript": "^2.4.1", "mocha": "^5.2.0", "ot-fuzzer": "^1.1.0", "ot-simple": "^1.0.0", @@ -16,7 +16,7 @@ "terser": "^3.11.0" }, "scripts": { - "test": "mocha test/cursor.coffee test/test.coffee test/immutable.coffee", + "test": "mocha test/cursor.js test/test.js test/immutable.js", "fuzzer": "node test/fuzzer.js", "prepare": "terser -d process.env.JSON1_RELEASE_MODE=true -c pure_funcs=log,keep_fargs=false,passes=2 -b < lib/json1.js > lib/json1.release.js" }, diff --git a/test/cursor.coffee b/test/cursor.coffee deleted file mode 100644 index 113803c..0000000 --- a/test/cursor.coffee +++ /dev/null @@ -1,76 +0,0 @@ -{writeCursor, readCursor} = require '../lib/cursor' -assert = require 'assert' - -data = require('fs').readFileSync(__dirname + '/ops.json', 'utf8').split('\n').filter((x) -> x != '').map(JSON.parse) - -describe 'cursors', -> - describe 'writeCursor duplicates', -> - test = (op) -> - w = writeCursor() - - f = (l) -> - assert Array.isArray l - depth = 0 - for c in l - if typeof c in ['string', 'number'] - depth++ - w.descend c - else if Array.isArray c - f c - else if typeof c is 'object' - for k, v of c - w.write k, v - - w.ascend() for [0...depth] - - f op if op != null - assert.deepEqual op, w.get() - - for d in data - do (d) -> it "#{JSON.stringify d}", -> test d - - describe 'copy using read cursors', -> - test = (op) -> - r = readCursor op - w = writeCursor() - path = [] - do f = -> - if component = r.getComponent() - # console.log 'component', component - w.write k, v for k, v of component - - assert.deepStrictEqual r.getPath(), path - assert.deepStrictEqual w.getPath(), path - - for k from r - path.push k - w.descend k - f() - w.ascend() - path.pop() - - assert.deepEqual op, w.get() - # console.log op - # console.log w.get() - for d in data - do (d) -> it "#{JSON.stringify d}", -> test d - - describe 'fuzzer', -> - - it 'cleans up position after mergeTree', -> - a = [ 1, 'c', { d: 1 } ] - w = writeCursor(a) - - w.descend(0) - - w.descend('a') - w.write('p', 1) - w.ascend() - w.ascend() - - w.descend(1) - w.mergeTree([{r:true}]) - w.descend('b') - w.write('p', 0) # Crash! - w.ascend() - w.ascend() diff --git a/test/cursor.js b/test/cursor.js new file mode 100644 index 0000000..1efa76b --- /dev/null +++ b/test/cursor.js @@ -0,0 +1,111 @@ +const { writeCursor, readCursor } = require('../lib/cursor') +const assert = require('assert') + +const data = require('fs') + .readFileSync(__dirname + '/ops.json', 'utf8') + .split('\n') + .filter(x => x !== '') + .map(JSON.parse) + +describe('cursors', function() { + describe('writeCursor duplicates', function() { + const test = op => { + const w = writeCursor() + + const f = l => { + assert(Array.isArray(l)) + let depth = 0 + for (let c of l) { + if (['string', 'number'].includes(typeof c)) { + depth++ + w.descend(c) + } else if (Array.isArray(c)) { + f(c) + } else if (typeof c === 'object') { + for (let k in c) { + const v = c[k] + w.write(k, v) + } + } + } + + return __range__(0, depth, false).map(i => w.ascend()) + } + + if (op !== null) { + f(op) + } + return assert.deepEqual(op, w.get()) + } + + return data.map(d => (d => it(`${JSON.stringify(d)}`, () => test(d)))(d)) + }) + + describe('copy using read cursors', function() { + const test = op => { + const r = readCursor(op) + const w = writeCursor() + const path = [] + const f = () => { + const component = r.getComponent() + if (component) { + // console.log 'component', component + for (let k in component) { + const v = component[k] + w.write(k, v) + } + } + + assert.deepStrictEqual(r.getPath(), path) + assert.deepStrictEqual(w.getPath(), path) + + const result = [] + for (let k of r) { + path.push(k) + w.descend(k) + f() + w.ascend() + result.push(path.pop()) + } + return result + } + + f() + + return assert.deepEqual(op, w.get()) + } + // console.log op + // console.log w.get() + return data.map(d => it(`${JSON.stringify(d)}`, () => test(d))) + }) + + return describe('fuzzer', () => + it('cleans up position after mergeTree', () => { + const a = [1, 'c', { d: 1 }] + const w = writeCursor(a) + + w.descend(0) + + w.descend('a') + w.write('p', 1) + w.ascend() + w.ascend() + + w.descend(1) + w.mergeTree([{ r: true }]) + w.descend('b') + w.write('p', 0) // Crash! + w.ascend() + return w.ascend() + })) +}) + +function __range__(left, right, inclusive) { + const range = [] + const ascending = left < right + const 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/fuzzer.js b/test/fuzzer.js index b705856..9bfa43a 100644 --- a/test/fuzzer.js +++ b/test/fuzzer.js @@ -1,10 +1,8 @@ -require('coffeescript/register') - const assert = require('assert') // const {type} = require('../index') -const {type} = require('../lib/json1') +const { type } = require('../lib/json1') -const run = module.exports = () => { +const run = (module.exports = () => { // require('./lib/log').quiet = true // type.debug = true const fuzzer = require('ot-fuzzer') @@ -15,6 +13,6 @@ const run = module.exports = () => { // const tracer = require('./tracer')(_t, genOp) //fuzzer(tracer, tracer.genOp, 100000) fuzzer(_t, genOp, 10000000) -} +}) if (require.main === module) run() diff --git a/test/genOp.coffee b/test/genOp.coffee deleted file mode 100644 index 63215c6..0000000 --- a/test/genOp.coffee +++ /dev/null @@ -1,215 +0,0 @@ -{randomInt, randomReal, randomWord} = require 'ot-fuzzer' -# require 'ot-fuzzer' - -assert = require 'assert' -{writeCursor} = require '../lib/cursor' -log = require '../lib/log' -{type} = require '../lib/json1' - - -# 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) -> if typeof o is 'object' then JSON.parse(JSON.stringify(o)) else o - -randomKey = (obj) -> # this works on arrays too! - if Array.isArray obj - if obj.length is 0 - return undefined - else - return randomInt obj.length - else - keys = Object.keys obj - if keys.length is 0 - return undefined - else - return keys[randomInt keys.length] - -letters = 'abxyz' - -# Generate a random new key for a value in obj. -randomNewKey = (obj) -> - if Array.isArray obj - return randomInt obj.length + 1 - else - loop - # Mostly try just use a small letter - key = if randomInt(20) == 0 then randomWord() else letters[randomInt(letters.length)] - break if obj[key] == undefined - return key - -# Generate a random object -randomThing = -> - switch randomInt 7 - when 0 then null - when 1 then '' - when 2 then randomWord() - when 3 - obj = {} - obj[randomNewKey(obj)] = randomThing() for [1..randomInt(2)] - obj - when 4 then (randomThing() for [1..randomInt(2)]) - when 5 then 0 # bias toward zeros to find accidental truthy checks - when 6 then randomInt(50) - -# Pick a random path to something in the object. -randomPath = (data) -> - return [] if !data? or typeof data != 'object' - - path = [] - while randomReal() < 0.9 and data? and typeof data == 'object' - key = randomKey data - break unless key? - - path.push key - data = data[key] - - path - -randomWalkPick = (w, container) -> - path = randomPath container.data - parent = container - key = 'data' - #log 'rwp', container, path, parent - - for p in path - parent = parent[key] - key = p - w.descend key - operand = parent[key] - return [path, parent, key, operand] - -# Returns [path, parent, key] if we can drop here, or null if no drop is -# possible -randomWalkDrop = (w, container) -> - if container.data == undefined - return [[], container, 'data'] - else if typeof container.data != 'object' or container.data == null - return null # Can't insert into a document that is a string or number - - [path, parent, key, operand] = randomWalkPick w, container - if typeof operand == 'object' and operand != null - parent = operand - else - w.ascend() - w.reset() - path.pop() - key = randomNewKey parent - # log 'key', key - return [path, parent, key] - -set = (container, key, value) -> - if value == undefined - if Array.isArray container - container.splice key, 1 - else - delete container[key] - else - if Array.isArray container - container.splice key, 0, value - else - container[key] = value - -genRandomOpPart = (data) -> - # log 'genRandomOp', data - - container = {data} - w = writeCursor() - - # Remove something - - # Things we can do: - # 0. remove something - # 1. move something - # 2. insert something - # 3. edit a string - mode = if data == undefined and randomReal() < 0.9 then 2 else randomInt 4 - #mode = 1 - #log 'mode', mode - switch mode - when 0, 1 - [path, parent, key, operand] = randomWalkPick w, container - #log 'ppko', path, parent, key, operand - if mode is 1 and typeof operand is 'string' - # Edit it! - genString = require 'ot-text/test/genOp' - [stringOp, result] = genString operand - w.write 'es', stringOp - parent[key] = result - else if mode is 1 and typeof operand is 'number' - increment = randomInt 10 - w.write 'ena', increment - parent[key] += increment - else - # Remove it - if operand != undefined - w.write 'r', true #operand - set parent, key, undefined - - when 2 - # insert something - walk = randomWalkDrop w, container - if walk != null - [path, parent, key] = walk - #log 'walk', walk - val = randomThing() - w.descend key if parent != container - w.write 'i', val - set parent, key, clone val - - when 3 - # Move something. We'll pick up the current operand... - [path1, parent1, key1, operand] = randomWalkPick w, container - if operand != undefined - set parent1, key1, undefined # remove it from the result... - - if parent1 == container # We're removing the whole thing. - w.write 'r', true - else - w.write 'p', 0 - - # ... and find another place to insert it! - w.ascend() for [0...path1.length] - w.reset() - [path2, parent2, key2] = randomWalkDrop w, container - w.descend key2 - set parent2, key2, operand - w.write 'd', 0 - - doc = container.data - op = w.get() - - type.checkValidOp op - - # assert.deepEqual doc, type.apply clone(data), op - - return [op, doc] - -module.exports = genRandomOp = (doc) -> - doc = clone doc - - # 90% chance of adding an op the first time through, then 50% each successive time. - chance = 0.99 - - op = null # Aggregate op - - while randomReal() < chance - [opc, doc] = genRandomOpPart(doc) - log opc - # type.setDebug false - op = type.compose op, opc - - chance = 0.5 - - # log.quiet = false - log '-> generated op', op, 'doc', doc - return [op, doc] - -if require.main == module - # log genRandomOp {} - # log genRandomOp({x:5, y:7, z:[1,2,3]}) for [1..10] - for [1..10] - type.debug = true - log.quiet = false - log genRandomOp({x:"hi", y:'omg', z:[1,'whoa',3]}) - # log genRandomOp(undefined) diff --git a/test/genOp.js b/test/genOp.js new file mode 100644 index 0000000..0e35598 --- /dev/null +++ b/test/genOp.js @@ -0,0 +1,278 @@ +let genRandomOp +const { randomInt, randomReal, randomWord } = require('ot-fuzzer') + +const assert = require('assert') +const { writeCursor } = require('../lib/cursor') +const log = require('../lib/log') +const { type } = require('../lib/json1') + +// 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 => (typeof o === 'object' ? JSON.parse(JSON.stringify(o)) : o) + +const randomKey = obj => { + // this works on arrays too! + if (Array.isArray(obj)) { + return obj.length === 0 ? undefined : randomInt(obj.length) + } else { + const keys = Object.keys(obj) + return keys.length === 0 ? undefined : keys[randomInt(keys.length)] + } +} + +const letters = 'abxyz' + +// Generate a random new key for a value in obj. +const randomNewKey = obj => { + if (Array.isArray(obj)) { + return randomInt(obj.length + 1) + } else { + let key + while (true) { + // Mostly try just use a small letter + key = + randomInt(20) === 0 ? randomWord() : letters[randomInt(letters.length)] + if (obj[key] === undefined) { + break + } + } + return key + } +} + +// Generate a random object +const randomThing = () => { + switch (randomInt(7)) { + case 0: + return null + case 1: + return '' + case 2: + return randomWord() + case 3: + const obj = {} + for ( + let i = 1, end = randomInt(2), asc = 1 <= end; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + obj[randomNewKey(obj)] = randomThing() + } + return obj + case 4: + return __range__(1, randomInt(2), true).map(j => randomThing()) + case 5: + return 0 // bias toward zeros to find accidental truthy checks + case 6: + return randomInt(50) + } +} + +// Pick a random path to something in the object. +const randomPath = data => { + if (data == null || typeof data !== 'object') { + return [] + } + + const path = [] + while (randomReal() < 0.9 && data != null && typeof data === 'object') { + const key = randomKey(data) + if (key == null) { + break + } + + path.push(key) + data = data[key] + } + + return path +} + +const randomWalkPick = (w, container) => { + const path = randomPath(container.data) + let parent = container + let key = 'data' + + for (let p of Array.from(path)) { + parent = parent[key] + key = p + w.descend(key) + } + const operand = parent[key] + return [path, parent, key, operand] +} + +// Returns [path, parent, key] if we can drop here, +// or null if no drop is possible +const randomWalkDrop = (w, container) => { + if (container.data === undefined) { + return [[], container, 'data'] + } else if (typeof container.data !== 'object' || container.data === null) { + return null // Can't insert into a document that is a string or number + } + + let [path, parent, key, operand] = randomWalkPick(w, container) + if (typeof operand === 'object' && operand !== null) { + parent = operand + } else { + w.ascend() + w.reset() + path.pop() + } + key = randomNewKey(parent) + return [path, parent, key] +} + +const set = (container, key, value) => + value === undefined + ? Array.isArray(container) + ? container.splice(key, 1) + : delete container[key] + : Array.isArray(container) + ? container.splice(key, 0, value) + : (container[key] = value) + +const genRandomOpPart = data => { + // log 'genRandomOp', data + + let key1, parent1, path1 + const container = { data } + const w = writeCursor() + + // Remove something + + // Things we can do: + // 0. remove something + // 1. move something + // 2. insert something + // 3. edit a string + const mode = data === undefined && randomReal() < 0.9 ? 2 : randomInt(4) + //mode = 1 + //log 'mode', mode + switch (mode) { + case 0: + case 1: + const [path, parent, key, operand] = randomWalkPick(w, container) + //log 'ppko', path, parent, key, operand + if (mode === 1 && typeof operand === 'string') { + // Edit it! + const genString = require('ot-text/test/genOp') + const [stringOp, result] = genString(operand) + w.write('es', stringOp) + parent[key] = result + } else if (mode === 1 && typeof operand === 'number') { + const increment = randomInt(10) + w.write('ena', increment) + parent[key] += increment + } else { + // Remove it + if (operand !== undefined) { + w.write('r', true) //operand + set(parent, key, undefined) + } + } + break + + case 2: + // insert something + const walk = randomWalkDrop(w, container) + if (walk !== null) { + const [path, parent, key] = walk + //log 'walk', walk + const val = randomThing() + if (parent !== container) { + w.descend(key) + } + w.write('i', val) + set(parent, key, clone(val)) + } + break + + case 3: + // Move something. We'll pick up the current operand... + const [path1, parent1, key1, operand1] = randomWalkPick(w, container) + if (operand1 !== undefined) { + set(parent1, key1, undefined) // remove it from the result... + + if (parent1 === container) { + // We're removing the whole thing. + w.write('r', true) + } else { + w.write('p', 0) + + // ... and find another place to insert it! + for ( + let i = 0, end = path1.length, asc = 0 <= end; + asc ? i < end : i > end; + asc ? i++ : i-- + ) { + w.ascend() + } + w.reset() + const [path2, parent2, key2] = Array.from( + randomWalkDrop(w, container) + ) + w.descend(key2) + set(parent2, key2, operand1) + w.write('d', 0) + } + } + break + } + + const doc = container.data + const op = w.get() + + type.checkValidOp(op) + + // assert.deepEqual doc, type.apply clone(data), op + + return [op, doc] +} + +module.exports = genRandomOp = doc => { + doc = clone(doc) + + // 90% chance of adding an op the first time through, then 50% each successive time. + let chance = 0.99 + + let op = null // Aggregate op + + while (randomReal() < chance) { + let opc + ;[opc, doc] = genRandomOpPart(doc) + log(opc) + // type.setDebug false + op = type.compose( + op, + opc + ) + chance = 0.5 + } + + // log.quiet = false + log('-> generated op', op, 'doc', doc) + return [op, doc] +} + +if (require.main === module) { + // log genRandomOp {} + // log genRandomOp({x:5, y:7, z:[1,2,3]}) for [1..10] + for (let i = 1; i <= 10; i++) { + type.debug = true + log.quiet = false + log(genRandomOp({ x: 'hi', y: 'omg', z: [1, 'whoa', 3] })) + } +} +// log genRandomOp(undefined) + +function __range__(left, right, inclusive) { + const range = [] + const ascending = left < right + const 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/immutable.coffee b/test/immutable.coffee deleted file mode 100644 index 0f9522d..0000000 --- a/test/immutable.coffee +++ /dev/null @@ -1,50 +0,0 @@ -assert = require 'assert' -{type} = require '../lib/json1' -log = require '../lib/log' -genOp = require './genOp' -deepClone = require '../lib/deepClone' - -# This tests that none of apply / compose / transform / genOp mutate their -# input - -describe 'immutable guarantees', -> - origDoc = {x:"hi", y:'omg', z:[1,'whoa',3]} - expectDoc = deepClone origDoc - n = 1000 - this.slow n*10 - - it 'apply does not mutate', -> - for [1..n] - [op, doc] = genOp origDoc - assert.deepStrictEqual origDoc, expectDoc - - expectOp = deepClone op - type.apply origDoc, op - - assert.deepStrictEqual origDoc, expectDoc - assert.deepStrictEqual op, expectOp - - it 'compose does not mutate', -> - for [1..n] - [op1, doc] = genOp origDoc - [op2, doc] = genOp doc - - expectOp1 = deepClone op1 - expectOp2 = deepClone op2 - type.compose op1, op2 - - assert.deepStrictEqual op1, expectOp1 - assert.deepStrictEqual op2, expectOp2 - - it 'transform does not mutate', -> - for [1..n] - [op1, doc1] = genOp origDoc - [op2, doc2] = genOp origDoc - - expectOp1 = deepClone op1 - expectOp2 = deepClone op2 - - type.transformNoConflict op1, op2, 'left' - type.transformNoConflict op2, op1, 'right' - assert.deepStrictEqual op1, expectOp1 - assert.deepStrictEqual op2, expectOp2 diff --git a/test/immutable.js b/test/immutable.js new file mode 100644 index 0000000..a88ab11 --- /dev/null +++ b/test/immutable.js @@ -0,0 +1,73 @@ +const assert = require('assert') +const { type } = require('../lib/json1') +const log = require('../lib/log') +const genOp = require('./genOp') +const deepClone = require('../lib/deepClone') + +// This tests that none of apply / compose / transform / genOp mutate their input +describe('immutable guarantees', function() { + const origDoc = { x: 'hi', y: 'omg', z: [1, 'whoa', 3] } + const expectDoc = deepClone(origDoc) + const n = 1000 + this.slow(n * 10) + + it('apply does not mutate', () => { + const result = [] + for ( + let i = 1, end = n, asc = 1 <= end; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + const [op, doc] = genOp(origDoc) + assert.deepStrictEqual(origDoc, expectDoc) + + const expectOp = deepClone(op) + type.apply(origDoc, op) + + assert.deepStrictEqual(origDoc, expectDoc) + result.push(assert.deepStrictEqual(op, expectOp)) + } + return result + }) + + it('compose does not mutate', () => { + for ( + let i = 1, end = n, asc = 1 <= end; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + let op2 + let [op1, doc] = genOp(origDoc) + ;[op2, doc] = genOp(doc) + + const expectOp1 = deepClone(op1) + const expectOp2 = deepClone(op2) + type.compose( + op1, + op2 + ) + + assert.deepStrictEqual(op1, expectOp1) + assert.deepStrictEqual(op2, expectOp2) + } + }) + + it('transform does not mutate', () => { + for ( + let i = 1, end = n, asc = 1 <= end; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + const [op1, doc1] = genOp(origDoc) + const [op2, doc2] = genOp(origDoc) + + const expectOp1 = deepClone(op1) + const expectOp2 = deepClone(op2) + + type.transformNoConflict(op1, op2, 'left') + type.transformNoConflict(op2, op1, 'right') + assert.deepStrictEqual(op1, expectOp1) + assert.deepStrictEqual(op2, expectOp2) + } + }) +}) diff --git a/test/test.coffee b/test/test.coffee deleted file mode 100644 index 8d550ec..0000000 --- a/test/test.coffee +++ /dev/null @@ -1,2268 +0,0 @@ -# Unit tests for the JSON1 OT type. -# -# These tests are quite unstructured. You can see the skeletons of a few -# organizing systems, but ultimately there's just lots of test cases to run. -# -# Cleanups welcome, so long as you don't remove any tests. - -assert = require 'assert' -# {type} = require '../index' -{type} = require '../lib/json1' -log = require '../lib/log' -deepClone = require '../lib/deepClone' - -{transform} = type -{DROP_COLLISION, RM_UNEXPECTED_CONTENT, BLACKHOLE} = type - -apply = ({doc:snapshot, op, expect}) -> - type.setDebug(false) - - orig = deepClone snapshot - try - result = type.apply snapshot, op - assert.deepStrictEqual snapshot, orig, 'Original snapshot was mutated' - assert.deepStrictEqual result, expect - catch e - console.log "Apply failed! Repro apply( #{JSON.stringify(snapshot)}, #{JSON.stringify(op)} )" - console.log "expected output: #{JSON.stringify(expect)}" - throw e - -d = (fn) -> - type.setDebug(true) - fn() - type.setDebug(false) - -compose = ({op1, op2, expect}) -> - try - result = type.compose op1, op2 - assert.deepStrictEqual result, expect - catch e - d -> - console.error 'FAIL! Repro with:' - console.log "compose( #{JSON.stringify(op1)}, #{JSON.stringify(op2)} )" - console.log "expected output: #{JSON.stringify(expect)}" - type.compose op1, op2 - throw e - -invConflict = ({type, op1, op2}) -> {type, op1:op2, op2:op1} - - - -otherSide = (side) -> if side == 'left' then 'right' else 'left' -checkConflict = ({op1, op2, side, conflict: expectConflict, expect}) -> - # We should get the same conflict with xf(op1, op2, left) and xf(op2, op1, right). - if expectConflict? - expectConflict.op1 = type.normalize(op1) if !expectConflict.op1 - expectConflict.op2 = type.normalize(op2) if !expectConflict.op2 - - for [side_, op1_, op2_, ec] in [ - [side, op1, op2, expectConflict], - [otherSide(side), op2, op1, if expectConflict then invConflict(expectConflict) else null] - ] - try - - # d -> log('tryTransform', side_, op1_, op2_) - {ok, conflict} = type.tryTransform op1_, op2_, side_ - if !ec? - # We don't care what the result is here; just that it doesn't conflict. - assert ok - else - assert !ok, "Conflict erroneously succeeded (#{side_})" - # d -> log('conflict', conflict) - conflict.op1 = type.normalize conflict.op1 - conflict.op2 = type.normalize conflict.op2 - assert.deepStrictEqual conflict, ec - catch e - d -> - console.error 'FAIL! Repro with:' - console.log "tryTransform(#{JSON.stringify(op1_)}, #{JSON.stringify(op2_)}, '#{side_}')" - type.tryTransform op1_, op2_, side_ - throw e - -xf = ({op1, op2, conflict, conflictLeft, conflictRight, expect, expectLeft, expectRight}) -> - if expect != undefined then expectLeft = expectRight = expect - if conflict != undefined then conflictLeft = conflictRight = conflict - - for [side, e, c] in [['left', expectLeft, conflictLeft], ['right', expectRight, conflictRight]] - checkConflict {op1, op2, side, conflict: c, expect: e} - - try - result = if c? then type.transformNoConflict op1, op2, side else transform op1, op2, side - assert.deepStrictEqual result, e - catch e - d -> - console.error 'FAIL! Repro with:' - console.log "transform(#{JSON.stringify(op1)}, #{JSON.stringify(op2)}, '#{side}')" - # if c? then type.transformNoConflict op1, op2, side else transform op1, op2, side - throw e - - - -diamond = ({doc, op1, op2}) -> - type.setDebug(false) - - try - # Test that the diamond property holds - op1_ = transform op1, op2, 'left' - op2_ = transform op2, op1, 'right' - - doc1 = type.apply doc, op1 - doc2 = type.apply doc, op2 - - doc12 = type.apply doc1, op2_ - doc21 = type.apply doc2, op1_ - - assert.deepStrictEqual doc12, doc21 - catch e - log.quiet = false - log '\nOops! Diamond property does not hold. Given document', doc - log 'op1 ', op1, ' / op2', op2 - log 'op1_', op1_, ' / op2_', op2_ - log '---- 1' - log 'op1', op1, '->', doc1 - log 'op2', op2_, '->', doc12 - log '---- 2' - log 'op2', op2, '->', doc2 - log 'op1', op1_, '->', doc21 - log '----------' - log doc12, '!=', doc21 - throw e - - -path = (path, {op, expect}) -> - expect = path.slice() if expect == undefined - - result = type.transformPosition path, op - assert.deepStrictEqual result, expect - - # Also check that path+X = expect+X - path2 = path.concat 'x' - expect2 = if expect? then expect.concat('x') else null - - result2 = type.transformPosition path2, op - assert.deepStrictEqual result2, expect2 - - -describe 'json1', -> - before -> - type.registerSubtype require 'ot-simple' - type.setDebug(true) - after -> type.setDebug(false) - - describe 'checkOp', -> - pass = (op) -> - try - type.checkValidOp op - catch e - console.log("FAIL! Repro with:\ncheckOp( #{JSON.stringify(op)} )") - throw e - - fail = (op) -> - try - assert.throws -> type.checkValidOp op - catch e - console.log("FAIL! Repro with:\ncheckOp( #{JSON.stringify(op)} )") - console.log('Should throw!') - throw e - - it 'allows some simple valid ops', -> - pass null - pass [{i:[1,2,3]}] - pass [{r:{}}] - pass [['x',{p:0}], ['y',{d:0}]] - pass [[0,{p:0}], [10,{d:0}]] - pass [['a',{p:0}],['b',{d:0}],['x',{p:1}],['y',{d:1}]] - pass [{e:"hi", et:'simple'}] - pass [{es:["hi"]}] - pass [{ena:5}] - - it 'disallows invalid syntax', -> - fail undefined - fail {} - fail "hi" - fail true - fail false - fail 0 - fail 10 - fail [{}] - fail [{invalid:true}] - fail [10, {}] - fail [10, {invalid:true}] - fail [10, 'hi'] - - it 'throws if there is any empty leaves', -> - fail [] - fail ['x'] - fail ['x', {}] - fail ['x', []] - fail [10] - fail [10, {}] - fail [10, []] - - it 'ensures path components are non-zero integers or strings', -> - fail [-1, {r:{}}] - fail [0.5, {r:{}}] - fail [true, {r:{}}] - fail [false, {r:{}}] - fail [null, {r:{}}] - fail [undefined, {r:{}}] - - it 'does not allow two pickups or two drops in a component', -> - fail [{p:0, r:{}}] - fail [{p:1, r:{}}] - fail ['x', {p:0, r:{}}] - fail ['x', {p:1, r:{}}] - - fail [{d:0, i:'hi'}] - fail [{d:1, i:'hi'}] - fail [10, {d:0, i:'hi'}] - fail [10, {d:1, i:'hi'}] - - it 'throws if there are mismatched pickups / drops', -> - fail [{p:0}] - fail [{d:0}] - fail ['x', {p:0}] - fail [10, {p:0}] - fail ['x', {d:0}] - fail [10, {d:0}] - - it 'throws if pick/drop indexes dont start at 0', -> - fail [['x', {p:1}], ['y', {d:1}]] - fail [[10, {p:1}], [20, {d:1}]] - - it 'throws if a descent starts with an edit', -> - fail [10, [{i:"hi"}]] - - it 'throws if descents are out of order', -> - fail ['x', ['b', {r:{}}], ['a', {r:{}}]] - fail ['x', [10, {r:{}}], [5, {r:{}}]] - fail ['x', ['a', {r:{}}], [5, {r:{}}]] - fail ['x', ['a', {r:{}}], ['a', {r:{}}]] - fail ['x', [10, {r:{}}], [10, {r:{}}]] - - it 'throws if descents start with the same scalar', -> - fail ['x', ['a', {r:{}}], ['a', {e:{}}]] - - it 'throws if descents have two adjacent edits', -> - fail [{r:{}}, {p:0}] - fail ['x', {r:{}}, {p:0}] - fail ['x', {r:{}}, {p:0}, 'y', {r:{}}] - - it.skip 'does not allow ops to overwrite their own inserted data', -> - fail [{i:{x:5}}, 'x', {i:6}] - fail [{i:['hi']}, 0, {i:'omg'}] - - it.skip 'does not allow immediate data directly parented in other immediate data', -> - fail [{i:{}}, 'x', {i:5}] - fail [{i:{x:5}}, 'x', 'y', {i:6}] - fail [{i:[]}, 0, {i:5}] - - it 'does not allow the final item to be a single descent', -> - fail ['a', ['b', r:{}]] # It should be ['a', 'b', r:{}] - - it 'does not allow anything after the descents at the end', -> - fail [[1, r:{}], [2, r:{}], 5] - fail [[1, r:{}], [2, r:{}], 5, r:{}] - fail [[1, r:{}], [2, r:{}], r:{}] - - it 'allows removes inside removes', -> - pass ['x', r:true, 'y', r:true] - pass ['x', r:{}, 'y', r:true] - pass [['x', r:true, 'y', p:0, 'z', r:true], ['y', d:0]] - pass [['x', r:{}, 'y', p:0, 'z', r:true], ['y', d:0]] - - it 'allows inserts inside inserts', -> - pass [1, i:{}, 'x', i:10] - pass [[0, 'x', p:0], [1, i:{}, 'x', d:0, 'y', i:10]] - - it.skip 'fails if the operation drops items inside something it picked up', -> - fail ['x', r:true, 1, i:'hi'] - fail ['x', d:0, 1, p:0] - fail [r:true, 1, p:0, d:0] - - describe 'edit', -> - it 'requires all edits to specify their type', -> - fail [{e:{}}] - fail [5, {e:{}}] - pass [{e:{}, et:'simple'}] - - it 'allows edits to have null or false for the operation', -> - # These aren't valid operations according to the simple type, but the - # type doesn't define a checkValidOp so we wouldn't be able to tell - # anyway. - pass [{e:null, et:'simple'}] - pass [5, {e:null, et:'simple'}] - pass [{e:false, et:'simple'}] - pass [5, {e:false, et:'simple'}] - - it 'does not allow an edit to use an unregistered type', -> - fail [{e:{}, et:'an undefined type'}] - fail [{e:null, et:'an undefined type'}] - - it 'does not allow two edits in the same operation', -> - fail [{e:{}, et:'simple', es:[1,2,3]}] - fail [{es:[], ena:5}] - fail [{e:{}, et:'simple', ena:5}] - - it 'fails if the type is missing', -> - fail [et:'missing', e:{}] - - it 'does not allow anything inside an edited subtree' - - it.skip 'does not allow an edit inside removed or picked up content', -> - fail [r:true, 1, es:['hi']] - pass [1, r:true, 1, es:['hi']] - fail ['x', r:true, 1, es:['hi']] - pass [[1, p:0, 1, es:['hi']], [2, d:0]] - fail [['x', p:0, 1, es:['hi']], ['y', d:0]] - - # This is actually ok. - pass [ 0, { p: 0 }, [ 'a', { es: [], r: true } ], [ 'x', { d: 0 } ] ] - - it.skip 'does not allow you to drop inside something that was removed', -> - # These insert into the next list item - pass [[1, r:true, 1, d:0], [2, p:0]] - pass [1, {p: 0}, 'x', {d: 0}] - - # But this is not ok. - fail ['x', {p:0}, 'a', {d:0}] - - describe 'normalize', -> - n = (opIn, expect) -> - expect = opIn if expect == undefined - op = type.normalize opIn - assert.deepStrictEqual op, expect - - it 'does the right thing for noops', -> - n null - n [], null - - it 'normalizes some regular ops', -> - n [{i:'hi'}] - n [{i:'hi'}, 1,2,3], [{i:'hi'}] - n [[1,2,3, {p:0}], [1,2,3, {d:0}]], [1,2,3, {p:0, d:0}] - n [[1,2,3, {p:0}], [1,2,30, {d:0}]], [1,2, [3, {p:0}], [30, {d:0}]] - n [[1,2,30, {p:0}], [1,2,3, {d:0}]], [1,2, [3, {d:0}], [30, {p:0}]] - - it 'will let you insert null', -> - n [{i:null}] - - it 'normalizes embedded ops when available', -> - n [{es:[0, 'hi']}], [{es:['hi']}] - n [{et:'text-unicode', e:['hi']}], [{es:['hi']}] - n [{et:'text-unicode', e:[0, 'hi']}], [{es:['hi']}] - n [{et:'simple', e:{}}] - n [{et:'number', e:5}], [{ena:5}] - n [{ena:5}] - - it.skip 'normalizes embedded removes', -> - n [1, {r:true}, 2, {r:true}], [1, {r:true}] - n [{r:true}, 2, {r:true}], [{r:true}] - - it 'throws if the type is missing', -> - # Not sure if this is the best behaviour but ... eh. - assert.throws -> n [et:'missing', e:{}] - - it 'corrects weird pick and drop ids', -> - n [['x', p:1], ['y', d:1]], [['x', p:0], ['y', d:0]] - -# ****** Apply ****** - - describe 'apply', -> - it 'Can set properties', -> - apply - doc: [] - op: [0, {i:17}] - expect: [17] - - apply - doc: {} - op: ['x', {i:5}] - expect: {x:5} - - it 'can edit the root', -> - apply - doc: {x:5} - op: [r:true] - expect: undefined - - apply - doc: '' - op: [r:true] - expect: undefined - - apply - doc: 'hi' - op: [r:true, i:null] - expect: null - - apply - doc: 'hi' - op: [es:[2, ' there']] - expect: 'hi there' - - assert.throws -> type.apply null, [{i:5}] - - apply - doc: undefined - op: [i:5] - expect: 5 - - apply - doc: {x:5} - op: [r:{}, i:[1,2,3]] - expect: [1,2,3] - - # TODO: And an edit of the root. - - it 'can move 1', -> apply - doc: {x:5} - op: [['x', p:0], ['y', d:0]] - expect: {y:5} - - it 'can move 2', -> apply - doc: [0,1,2] - op: [[1, p:0], [2, d:0]] - expect: [0,2,1] - - it 'can handle complex list index stuff', -> apply - doc: [0,1,2,3,4,5] - op: [[1, r:{}, i:11], [2, r:{}, i:12]] - expect: [0,11,12,3,4,5] - - it 'correctly handles interspersed descent and edits', -> apply - doc: {x: {y: {was:'y'}, was:'x'}} - op: [['X',{d:0},'Y',{d:1}], ['x',{p:0},'y',{p:1}]] - expect: {X: {Y: {was:'y'}, was:'x'}} - - it 'can edit strings', -> apply - doc: "errd" - op: [{es:[2,"maghe"]}] - expect: "ermagherd" - - it 'can edit numbers', -> apply - doc: 5 - op: [ena:10] - expect: 15 - - it 'can edit child numbers', -> apply - doc: [20] - op: [0, ena:-100] - expect: [-80] - - it 'can edit subdocuments using an embedded type', -> apply - doc: {str:'hai'} - op: [{e:{position:2, text:'wai'}, et:'simple'}] - expect: {str:'hawaii'} - - it 'applies edits after drops', -> apply - doc: {x: "yooo"} - op: [['x', p:0], ['y', d:0, es:['sup']]] - expect: {y: "supyooo"} - - it 'throws when the op traverses missing items', -> - assert.throws -> type.apply [0, 'hi'], [1, p:0, 'x', d:0] - assert.throws -> type.apply {}, [p:0, 'a', d:0] - - it 'throws if the type is missing', -> - assert.throws -> type.apply {}, [et:'missing', e:{}] - - - describe 'apply path', -> - it 'does not modify path when op is unrelated', -> - path ['a', 'b', 'c'], op: null - path ['a', 'b', 'c'], op: ['x', i:5] - path ['a', 'b', 'c'], op: ['x', r:true] - path ['a', 'b', 'c'], op: [['x', p:0], ['y', d:0]] - path [1,2,3], op: [2, i:5] - path [1,2,3], op: [1, 2, 4, i:5] - path [1], op: [1, 2, r:true] - path ['x'], op: ['x', 'y', r:true] - - it 'adjusts list indicies', -> - path [2], op: [1, i:5], expect: [3] - path [2], op: [2, i:5], expect: [3] - path [2], op: [1, r:true], expect: [1] - path [2], op: [[1, p:0], [3, d:0]], expect: [1] - path [2], op: [[1, d:0], [3, p:0]], expect: [3] - path [2], op: [[2, d:0], [3, p:0]], expect: [3] - - it 'returns null when the object at the path was removed', -> - path ['x'], op: [r:true], expect: null - path ['x'], op: ['x', r:true], expect: null - path [1], op: [r:true], expect: null - path [1], op: [1, r:true], expect: null - - it 'moves the path', -> - path ['a', 'z'], op: [['a', p:0], ['y', d:0]], expect: ['y', 'z'] - path ['a', 'b'], op: [['a', 'b', p:0], ['z', d:0]], expect: ['z'] - path ['a', 'b'], op: [['a', 'b', 'c', p:0], ['z', d:0]] - path [1,2], op: [[1, p:0], [10, d:0]], expect: [10, 2] - path [1,2], op: [[1, 2, p:0], [10, d:0]], expect: [10] - path [1,2], op: [1, [1, d:0], [2, p:0]], expect: [1, 1] - path [1,2], op: [[1, 2, 3, p:0], [10, d:0]] - - it 'handles pick parent and move', -> - path ['a', 'b', 'c'], op: [['a', r:true, 'b', p:0], ['x', d:0]], expect: ['x', 'c'] - - it 'adjusts indicies under a pick', -> - path ['a', 'b', 10], op: [['a', p:0, 'b', 1, r:true], ['x', d:0]], expect: ['x', 'b', 9] - - it.skip 'gen ops', -> - # This should do something like: - # - Generate a document - # - Generate op, a random operation - # - Generate a path to somewhere in the document and an edit we can do there -> op2 - # - Check that transform(op2, op) == op2 at transformPosition(path) or something like that. - - it 'calls transformPosition with embedded string edits if available', -> - # For embedded string operations (and other things that have - # transformPosition or transformPosition or whatever) we should call that. - path ['x','y','z', 1], op: ['x','y','z', es:['abc']], expect: ['x','y','z', 4] - path ['x','y','z', 1], op: ['x','y','z', es:['💃']], expect: ['x','y','z', 2] - path ['x','y','z'], op: ['x','y','z', es:['💃']], expect: ['x','y','z'] - - -# ******* Compose ******* - - describe 'compose', -> - it 'composes empty ops to nothing', -> compose - op1: null - op2: null - expect: null - - describe 'op1 drop', -> - it 'vs remove', -> compose - op1: [['x', p:0], ['y', d:0]] - op2: ['y', r:true] - expect: ['x', r:true] - - it 'vs remove parent', -> compose - op1: [['x', p:0], ['y', 0, d:0]] - op2: ['y', r:true] - expect: [['x', r:true], ['y', r:true]] - - it 'vs remove child', -> compose - op1: [['x', p:0], ['y', d:0]] - op2: ['y', 'a', r:true] - expect: [['x', p:0, 'a', r:true], ['y', d:0]] - - it 'vs remove and pick child', -> compose - op1: [['x', p:0], ['y', d:0]] - op2: [['y', r:true, 'a', p:0], ['z', d:0]] - expect: [['x', r:true, 'a', p:0], ['z', d:0]] - - it 'vs pick', -> compose - op1: [['x', p:0], ['y', d:0]] - op2: [['y', p:0], ['z', d:0]] - expect: [['x', p:0], ['z', d:0]] - - it 'is transformed by op2 picks', -> compose - op1: [['x', p:0], ['y', 10, d:0]] - op2: ['y', 0, r:true] - expect: [['x', p:0], ['y', [0, r:true], [9, d:0]]] - - describe 'op1 insert', -> - it 'vs remove', -> compose - op1: ['x', i:{a:'hi'}] - op2: ['x', r:true] - expect: null - - it 'vs remove parent', -> compose - op1: ['x', 0, i:{a:'hi'}] - op2: ['x', r:true] - expect: ['x', r:true] - - it 'vs remove child', -> compose - op1: ['x', i:{a:'hi', b:'woo'}] - op2: ['x', 'a', r:true] - expect: ['x', i:{b:'woo'}] - - it 'vs remove and pick child', -> compose - op1: ['x', i:{a:'hi', b:'woo'}] - op2: [['x', r:true, 'a', p:0], ['y', d:0]] - expect: ['y', i:'hi'] - - it 'vs remove an embedded insert', -> compose - op1: ['x', i:{}, 'y', i:'hi'] - op2: ['x', 'y', r:true] - expect: ['x', i:{}] - - it 'vs remove from an embedded insert', -> compose - op1: ['x', i:{}, 'y', i:[1,2,3]] - op2: ['x', 'y', 1, r:true] - expect: ['x', i:{}, 'y', i:[1, 3]] - - it 'picks the correct element of an embedded insert', -> compose - op1: ['x', i:['a', 'b', 'c'], 1, i:'XX'] - op2: [['x', 1, p:0], ['y', d:0]] - expect: [['x', i:['a', 'b', 'c']], ['y', i:'XX']] - - it 'picks the correct element of an embedded insert 2', -> compose - op1: ['x', i:['a', 'b', 'c'], 1, i:'XX'] - op2: [['x', 3, p:0], ['y', d:0]] # should grab 'c'. - expect: [['x', i:['a', 'b'], 1, i:'XX'], ['y', i:'c']] - - - it 'moves all children', -> compose - op1: ['x', i:{}, 'y', i:[1,2,3]] - op2: [['x', p:0], ['z', d:0]] - expect: ['z', i:{}, 'y', i:[1,2,3]] - - it 'removes all children', -> compose - op1: ['x', i:{}, 'y', i:[1,2,3]] - op2: ['x', r:true] - expect: null - - it 'removes all children when removed at the destination', -> compose - op1: [['x', p:0], ['y', d:0, 0, i:'hi']] - op2: ['y', r:true] - expect: ['x', r:true] - - it 'vs op2 insert', -> compose # Inserts aren't folded together. - op1: [i:{}] - op2: ['x', i:'hi'] - expect: [i:{}, 'x', i:'hi'] - - it 'vs op2 string edit', -> compose - op1: [i:'hi'] - op2: [es:[2, ' there']] - expect: [i:'hi', es:[2, ' there']] - - it 'vs op2 number edit', -> compose - op1: [i:10] - op2: [ena:20] - expect: [i:10, ena:20] - - describe 'op1 edit', -> - it 'removes the edit if the edited object is deleted', -> compose - op1: ['x', es:['hi']] - op2: ['x', r:true] - expect: ['x', r:true] - - it 'removes the edit in an embedded insert 1', -> compose - op1: ['x', i:'', es:['hi']] - op2: ['x', r:true] - expect: null - - it 'removes the edit in an embedded insert 2', -> compose - op1: ['x', i:[''], 0, es:['hi']] - op2: ['x', 0, r:true] - expect: ['x', i:[]] - - it 'composes string edits', -> compose - op1: [es:['hi']] - op2: [es:[2, ' there']] - expect: [es:['hi there']] - - it 'composes number edits', -> compose - op1: [ena:10] - op2: [ena:-8] - expect: [ena:2] - - it 'transforms and composes edits', -> compose - op1: ['x', es:['hi']] - op2: [['x', p:0], ['y', d:0, es:[2, ' there']]] - expect: [['x', p:0], ['y', d:0, es:['hi there']]] - - it 'preserves inserts with edits', -> compose - op1: ['x', i:'hi'] - op2: [['x', p:0], ['y', d:0, es:[' there']]] - expect: ['y', i:'hi', es:[' there']] - - it 'allows a different edit in the same location', -> compose - op1: ['x', es:['hi']] - op2: ['x', r:true, i:'yo', es:[2, ' there']] - expect: ['x', r:true, i:'yo', es:[2, ' there']] - - it 'throws if the type is missing', -> - assert.throws -> type.compose [et:'missing', e:{}], [et:'missing', e:{}] - - describe 'op2 pick', -> - it 'gets untransformed by op1 drops', -> - op1: [5, i:'hi'] - op2: [6, r:true] - expect: [5, r:true, i:'hi'] - - describe 'op1 insert containing a drop', -> - it 'vs pick at insert', -> compose - op1: [['x', p:0], ['y', i:{}, 'x', d:0]] - op2: [['y', p:0], ['z', d:0]] - expect: [['x', p:0], ['z', i:{}, 'x', d:0]] - - describe 'fuzzer tests', -> - it 'complicated transform of indicies', -> compose - op1: [ 0, { p: 0 }, 'x', 2, { d: 0 } ] - op2: [ 0, 'x', 0, { r: true } ] - expect: [ - [0, p:0, 'x', 1, d:0], - [1, 'x', 0, r:true] - ] - - describe 'setnull interaction', -> - # Currently failing. - it 'reorders items inside a setnull region', -> compose - op1: [{i:[]}, [0, {i:'a'}], [1, {i:'b'}]] - op2: [[0, {p:0}], [1, {d:0}]] - expect: [{i:[]}, [0, {i:'b'}], [1, {i:'a'}]] - - it 'lets a setnull child be moved', -> compose - op1: ['list', {i:[]}, 0, {i:'hi'}] - op2: [['list', 0, p:0], ['z', d:0]] - expect: [['list', {i:[]}], ['z', i:'hi']] - - it 'lets a setnull child get modified', -> compose - op1: [{i:[]}, 0, {i:['a']}] - op2: [0, 0, {r:'a', i:'b'}] - expect: [{i:[]}, 0, {i: []}, 0, {i: 'b'}] - #expect: [{i:[]}, 0, {i:['b']}] # Maybe better?? - - describe 'regression', -> - it 'skips op2 drops when calculating op1 drop index simple', -> compose - op1: [[ 0, { p: 0 } ], [ 2, { d: 0 } ]] - op2: [[ 0, { p: 0 } ], [ 1, { d: 0 } ]] - expect: [ [ 0, { p: 1 } ], [ 1, { p: 0, d: 0 } ], [ 2, { d: 1 } ] ] - - it 'skips op2 drops when calculating op1 drop index complex', -> compose - op1: [[0, {p:0, d:1}], [1, p:1], [2, d:0]] - op2: [[0, p:0], [1, d:0]] - # expect: [[0, {p:1}], [1, {d:0, p:0}], [2, d:1]] - expect: [[0, p:1], [1, {p:0, d:0}], [2, d:1]] - - it '3', -> compose - op1: [ { i: [ null, [] ] }, 0, { i: '' } ] - op2: [ 1, { p: 0 }, 0, { d: 0 } ] - # ... it'd be way more consistent to drop the null separately rather than merging it?? - expect: [ { i: [ [] ] }, [ 0, { i: '' } ], [ 1, 0, { i: null } ] ] - - it '4', -> compose # This one triggered a bug in cursor! - op1: [ 0, - [ 0, [ 'a', { r: true } ], [ 'b', { d: 0 } ] ], - [ 2, { p: 0 } ] ] - op2: [ 0, 0, 'c', { i: 'd' } ] - expect: [ 0, - [ 0, [ 'a', { r: true } ], [ 'b', { d: 0 } ], [ 'c', { i: 'd' } ] ], - [ 2, { p: 0 } ] - ] - - # *** Old stuff - describe 'old compose', -> - it 'gloms together unrelated edits', -> - compose - op1: [['a', p:0], ['b', d:0]] - op2: [['x', p:0], ['y', d:0]] - expect: [['a', p:0], ['b', d:0], ['x', p:1], ['y', d:1]] - - compose - op1: [2, i:'hi'] - op2: [0, 'x', r:true] - expect: [[0, 'x', r:true], [2, i:"hi"]] - - it 'translates drops in objects', -> compose - op1: ['x', ['a', p:0], ['b', d:0]] # x.a -> x.b - op2: [['x', p:0], ['y', d:0]] # x -> y - expect: [['x', {p:0}, 'a', {p:1}], ['y', {d:0}, 'b', {d:1}]] # x.a -> y.b, x -> y - - it 'untranslates picks in objects', -> compose - op1: [['x', p:0], ['y', d:0]] # x -> y - op2: [['y', 'a', p:0], ['z', d:0]] # y.a -> z - expect: [['x',p:0,'a',p:1], ['y',d:0], ['z',d:1]] # x.a -> z, x -> y - - it 'insert gets carried wholesale', -> compose - op1: ['x', i:'hi there'] - op2: [['x', p:0], ['y', d:0]] # x -> y - expect: ['y', i:'hi there'] - - it 'insert gets edited by the op', -> compose - op1: ['x', {i:{a:1, b:2, c:3}}] - op2: [['x', 'a', p:0], ['y', d:0]] - expect: [['x', {i:{b:2, c:3}}], ['y', i:1]] - - it 'does not merge mutual inserts', -> compose - op1: [i:{}] - op2: ['x', i:"hi"] - expect: [i:{}, 'x', i:'hi'] - - # TODO: List nonsense. - - # TODO: Edits. - - -# ****** Transform ****** - - describe 'transform', -> - describe 'op1 pick', -> - it 'vs delete', -> xf - op1: [['x', p:0], ['y', d:0]] - op2: ['x', r:true] - expect: null - it 'vs delete parent', -> xf - op1: [['x', 'a', p:0], ['y', d:0]] - op2: ['x', r:true] - expect: null - it 'vs delete parent 2', -> xf - op1: ['x', ['a', p:0], ['b', d:0]] - op2: ['x', r:true] - expect: null - - it 'vs pick', -> xf - op1: [['x', p:0], ['z', d:0]] - op2: [['x', p:0], ['y', d:0]] - # Consider adding a conflict for this case. - expectLeft: [['y', p:0], ['z', d:0]] - expectRight: null - it 'vs pick parent', -> xf - op1: [['x', 'a', p:0], ['z', d:0]] - op2: [['x', p:0], ['y', d:0]] - expect: [['y', 'a', p:0], ['z', d:0]] - - it 'vs pick and pick child', -> xf # regression - op1: [ # a -> xa, a.c -> xc - ['a', p:0, 'c', p:1] - ['xa', d:0] - ['xc', d:1] - ] - op2: [['a', p:0], ['b', d:0]] # a -> b - expectLeft: [ - ['b', p:0, 'c', p:1] - ['xa', d:0] - ['xc', d:1] - ] - expectRight: [ - ['b', 'c', p:0] - ['xc', d:0] - ] - - it 'vs edit', -> xf - op1: [['x', p:0], ['z', d:0]] - op2: ['x', es:['hi']] - expect: [['x', p:0], ['z', d:0]] - - it 'vs delete, drop', -> xf - op1: [['x', p:0], ['y', d:0]] - op2: [['a', p:0], ['x', r:0, d:0]] - expect: null - - it 'vs delete, insert', -> xf - op1: [['x', p:0], ['y', d:0]] - op2: ['x', r:0, i:5] - expect: null - - it 'vs pick, drop to self', - -> xf - op1: [['x', p:0], ['y', d:0]] - op2: [['x', p:0], ['y', d:0]] - expect: null - - -> xf - op1: [['a', 1, p:0], ['y', d:0]] - op2: [['a', 1, p:0], ['y', d:0]] - expect: null - - it 'vs pick, drop', -> xf - op1: [['x', p:0], ['z', d:0]] # x->z - op2: [['a', p:0], ['x', p:1, d:0], ['y', d:1]] # a->x, x->y - expectLeft: [['y', p:0], ['z', d:0]] - expectRight: null - - it 'vs pick, insert', -> xf - op1: [['x', p:0], ['z', d:0]] - op2: [['x', p:0, i:5], ['y', d:0]] - expectLeft: [['y', p:0], ['z', d:0]] - expectRight: null - - it 'vs pick, edit', -> - op1: [['x', p:0], ['z', d:0]] - op2: [['x', es:['hi'], p:0], ['y', d:0]] - expectLeft: [['y', p:0], ['z', d:0]] - expectRight: null - - describe 'op1 delete', -> - it 'vs delete', -> xf - op1: ['x', r:true] - op2: ['x', r:true] - expect: null - it 'vs delete parent', -> xf - op1: ['x', 'a', r:true] - op2: ['x', r:true] - expect: null - - it 'vs pick', -> xf - op1: ['x', r:true] - op2: [['x', p:0], ['y', d:0]] - expect: ['y', r:true] - it 'vs pick parent', -> xf - op1: ['x', 'a', r:true] - op2: [['x', p:0], ['y', d:0]] - expect: ['y', 'a', r:true] - - it 'vs pick and drop', -> xf - op1: ['x', r:true] - op2: [['a', p:0], ['x', d:0, p:1], ['z', d:1]] - expect: ['z', r:true] - - it 'vs edit', -> xf - op1: ['x', r:true] - op2: ['x', es:['hi']] - conflict: type: RM_UNEXPECTED_CONTENT - expect: ['x', r:true] - - it 'vs move and insert', -> xf - op1: [ 'a', 1, { r: true } ] - op2: [ - [ 'a', { p: 0 } ], - [ 'b', { d: 0 }, [ 0, { i: 5 } ], [ 1, { i: 5 } ] ] - ] - expect: ['b', 3, r:true] - - describe 'vs pick child', -> - it 'move in', -> xf - op1: ['x', r:true] - op2: [['a', p:0], ['x', 'y', d:0]] - conflict: type: RM_UNEXPECTED_CONTENT - expect: ['x', r:true, 'y', r:true] # Also ok if its just x, r:true - - it 'move across', -> xf - op1: ['x', r:true] # delete doc.x - op2: ['x', ['y', p:0], ['z', d:0]] - expect: ['x', r:true] - - it 'move out', -> xf - op1: ['x', r:true] - op2: [['x', 'y', p:0], ['y', d:0]] # move doc.x.y -> doc.y - expect: [['x', r:true], ['y', r:true]] # delete doc.x and doc.y - - it 'multiple out', -> xf - op1: ['x', r:true] - op2: [['x', 'y', p:0, 'z', p:1], ['y', d:0], ['z', d:1]] - expect: [['x', r:true], ['y', r:true], ['z', r:true]] - - it 'chain out', -> xf - op1: ['x', r:true] - op2: [['x', 'y', p:0], ['y', p:1], ['z', d:0, 'a', d:1]] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: [['y', p:0], ['z', 'a', d:0]] # cMv(['y'], ['z', 'a']) - expect: [['x', r:true], ['z', r:true, 'a', r:true]] - - it 'mess', -> xf - # yeesh - op1: [['x', r:true, 'y', 'z', p:0], ['z', d:0]] - op2: [['x', 'y', p:0], ['y', d:0]] - expect: [['x', r:true], ['y', r:true, 'z', p:0], ['z', d:0]] - - describe 'op1 drop', -> - it 'vs delete parent', -> xf - op1: [['x', p:0], ['y', 'a', d:0]] - op2: ['y', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: ['x', r:true] - - it 'vs a cancelled parent', -> xf - # This is actually a really complicated case. - op1: [['x', 'y', p:0], ['y', p:1], ['z', d:0, 'a', d:1]] - op2: ['x', r:true] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [['y', p:0], ['z', 'a', d:0]] # c1: cMv(['y'], ['z', 'a']) - expect: ['y', r:true] - - it 'vs pick parent', -> xf - op1: [['x', p:0], ['y', 'a', d:0]] - op2: [['y', p:0], ['z', d:0]] - expect: [['x', p:0], ['z', 'a', d:0]] - - it 'vs drop', -> xf - op1: [['x', p:0], ['z', d:0]] - op2: [['y', p:0], ['z', d:0]] - conflict: type: DROP_COLLISION - expectLeft: [['x', p:0], ['z', r:true, d:0]] - expectRight: ['x', r:true] - - it 'vs drop (list)', -> xf - op1: [[0, p:0], [4, d:0]] - op2: [[5, d:0], [10, p:0]] - expectLeft: [[0, p:0], [4, d:0]] - expectRight: [[0, p:0], [5, d:0]] - - it 'vs drop (chained)', -> xf - op1: [['a', p:1], ['x', p:0], ['z', d:0, 'a', d:1]] - op2: [['y', p:0], ['z', d:0]] - conflict: - type: DROP_COLLISION - op1: [['x', p:0], ['z', d:0]] #cMv(['x'], ['z']) - expectLeft: [['a', p:0], ['x', p:1], ['z', r:true, d:1, 'a', d:0]] - expectRight: [['a', r:true], ['x', r:true]] - - it 'vs insert', -> xf - op1: [['x', p:0], ['z', d:0]] - op2: ['z', i:5] - conflict: type: DROP_COLLISION - expectLeft: [['x', p:0], ['z', r:true, d:0]] - expectRight: ['x', r:true] - - it 'vs pick (a->b->c vs b->x)', -> xf - op1: [['a', p:0], ['b', {p:1, d:0}], ['c', d:1]] - op2: [['b', p:0], ['x', d:0]] - expectLeft: [['a', p:0], ['b', d:0], ['c', d:1], ['x', p:1]] - expectRight: [['a', p:0], ['b', d:0]] - - describe.skip 'vs move inside me', -> - # Note: This is *not* blackholeing! The edits are totally fine; we - # just need one edit to win. - # The current behaviour just nukes both. - it 'in objects', -> xf - op1: [['x', p:0], ['y', 'a', d:0]] - op2: [['x', 'a', d:0], ['y', p:0]] - expectLeft: [['x', p:0, 'a', p:1], ['y', d:1, 'x', d:0]] - expectRight: null - - it 'in lists', -> xf - op1: [0, p:0, 'x', d:0] - op2: [[0, 'y', d:0], [1, p:0]] - expectLeft: [0, {p:0, d:1}, ['x', d:0], ['y', p:1]] - expectRight: null - - it 'multiple', -> xf - # a->x.a, b->x.b - op1: [['a', p:0], ['b', p:1], ['x', 'a', d:0, 'b', d:1]] - op2: [['a', 'x', d:0], ['x', p:0]] # x->a.x - expectLeft: [['a', p:0, 'x', p:1], ['b', p:2], - ['x', d:1, ['a', d:0], ['b', d:2]]] - expectRight: null - - describe 'op1 insert', -> - it 'vs delete parent', -> xf - op1: ['y', 'a', i:5] - op2: ['y', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'vs pick parent', -> xf - op1: ['y', 'a', i:5] - op2: [['y', p:0], ['z', d:0]] - expect: ['z', 'a', i:5] - - it 'vs drop', -> xf - op1: ['z', i:5] - op2: [['y', p:0], ['z', d:0]] - conflict: type: DROP_COLLISION - expectLeft: ['z', r:true, i:5] - expectRight: null - - it 'vs insert', -> xf - op1: ['z', i:5] - op2: ['z', i:10] - conflict: type: DROP_COLLISION - expectLeft: ['z', r:true, i:5] - expectRight: null - - it 'vs insert at list position', -> xf - op1: [5, i:'hi'] - op2: [5, i:'there'] - expectLeft: [5, i:'hi'] - expectRight: [6, i:'hi'] - - it 'vs identical insert', -> xf - op1: ['z', i:5] - op2: ['z', i:5] - expect: null - - # This is the new setNull for setting up schemas - it 'vs embedded inserts', -> - xf - op1: ['x', i:{}] - op2: ['x', i:{}, 'y', i:5] - expect: null - - xf - op1: ['x', i:{}, 'y', i:5] - op2: ['x', i:{}] - expect: ['x', 'y', i:5] - - xf - op1: ['x', i:{}, 'y', i:5] - op2: ['x', i:{}, 'y', i:5] - expect: null - - xf - op1: ['x', i:{}, 'y', i:5] - op2: ['x', i:{}, 'y', i:6] - conflict: - type: DROP_COLLISION - op1: ['x', 'y', i:5] - op2: ['x', 'y', i:6] - expectLeft: ['x', 'y', r:true, i:5] - expectRight: null - - it 'with embedded edits', -> xf - op1: [i:'', es:['aaa']] - op2: [i:'', es:['bbb']] - expectLeft: [es:['aaa']] - expectRight: [es:[3, 'aaa']] - - describe 'op1 edit', -> - it 'vs delete', -> xf - op1: ['x', es:['hi']] - op2: ['x', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'vs delete parent', -> xf - op1: ['x', 'y', es:['hi']] - op2: ['x', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'vs pick', -> xf - op1: ['x', es:['hi']] - op2: [['x', p:0], ['y', d:0]] - expect: ['y', es:['hi']] - - it 'vs edit string', -> xf - op1: ['x', es:['ab']] - op2: ['x', es:['cd']] - expectLeft: ['x', es:['ab']] - expectRight: ['x', es:[2, 'ab']] - - it 'vs edit number', -> xf - op1: [ena:5] - op2: [ena:100] - expect: [ena:5] - - it 'throws if edit types arent compatible', -> - assert.throws -> type.transform [es:[]], [ena:5], 'left' - - it 'vs move and edit', -> xf - op1: ['x', es:[1, 'ab']] - op2: [['x', p:0], ['y', d:0, es:[d:1, 'cd']]] - expectLeft: ['y', es:['ab']] - expectRight: ['y', es:[2, 'ab']] - - it 'throws if the type is missing', -> - assert.throws -> type.transform [et:'missing', e:{}], [et:'missing', e:{}], 'left' - - describe 'op2 cancel move', -> - it 'and insert', -> xf - op1: ['x', r:true] - op2: [['x', 'a', p:0], ['y', d:0, 'b', i:5]] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: ['y', 'b', i:5] - expect: [['x', r:true], ['y', r:true, 'b', r:true]] - - it 'and another move (rm x vs x.a -> y, q -> y.b)', -> xf - op1: ['x', r:true] - op2: [['q', p:1], ['x', 'a', p:0], ['y', d:0, 'b', d:1]] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: [['q', p:0], ['y', 'b', d:0]] - expect: [['x', r:true], ['y', r:true, 'b', r:true]] - - describe 'op2 list move an op1 drop', -> - it 'vs op1 remove', -> xf - op1: [[0, r:true, 'a', i:'hi'], [5, r:true]] - op2: [[1, p:0], [4, d:0]] - expect: [[0, r:true], [3, 'a', i:'hi'], [5, r:true]] - - it 'vs op1 remove 2', -> xf - op1: [[0, r:true, 'a', i:'hi'], [1, r:true], [2, r:true]] - op2: [[3, p:0], [4, d:0]] - expect: [[0, r:true], [1, r:true, 'a', i:'hi'], [2, r:true]] - - it 'vs op1 insert before', -> xf - op1: [[0, i:'a'], [1, i:'b'], [2, 'a', i:'hi']] - op2: [[0, p:0], [1, d:0]] - expect: [[0, i:'a'], [1, i:'b'], [3, 'a', i:'hi']] - - - it 'vs op1 insert before and replace', -> xf - op1: [[0, i:'xx', 'a', r:true], [1, 'a', i:'hi']] - op2: [[0, p:0], [3, d:0]] - expect: [[0, i:'xx'], [3, 'a', r:true], [4, 'a', i:'hi']] - - - describe 'list', -> - describe 'drop', -> - it 'transforms by p1 drops', -> xf - op1: [[5, i:5], [10, i:10]] - op2: [9, i:9] - expectLeft: [[5, i:5], [10, i:10]] - expectRight: [[5, i:5], [11, i:10]] - - it 'transforms by p1 picks' - it 'transforms by p2 picks' - it 'transforms by p2 drops' - - describe 'conflicts', -> - describe 'drop into remove / rm unexpected', -> - # xfConflict does both xf(op1, op2, left) and xf(op2, op1, right), and - # uses invConflict. So this also tests RM_UNEXPECTED_CONTENT with each - # test case. - it 'errors if you insert', -> xf - op1: ['a', 'b', i:5] - op2: ['a', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'errors if you drop', -> xf - op1: [['a', p:0], ['x', 'b', d:0]] - op2: ['x', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: ['a', r:true] - - it 'errors if you rm then insert in a child', -> xf - op1: ['a', 'b', r:true, i:5] - op2: ['a', r:true] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: ['a', 'b', i:5] - expect: null - - it 'errors if the object is replaced', -> xf - op1: ['a', 'b', i:5] - op2: ['a', r:true, i:10] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: ['a', r:true] - expect: null - - it 'handles a delete of the source parent by op2', -> xf - op1: [['a', p:0], ['b', 'b', d:0]] - op2: [['a', p:0], ['b', r:true, 'c', d:0]] - conflictLeft: - type: RM_UNEXPECTED_CONTENT - op2: ['b', r:true] - expectLeft: ['b', 'c', r:true] - expectRight: null - - it.skip 'returns symmetric errors when both ops delete the other', -> xf - # The problem here is that there's two conflicts we want to return. - # Which one should be returned first? It'd be nice for the order of - # conflict returning to be symmetric - that is, if we know multiple - # conflicts happen, order them based on left/right. But I haven't done - # that, so we get different conflicts out of this in a first pass. - op1: [ [ 'x', { r: true } ], [ 'y', 'a', { i: {} } ] ] - op2: [ [ 'x', 'a', { i: {} } ], [ 'y', { r: true } ] ] - conflict: type: RM_UNEXPECTED_CONTENT - expect: ['x', r:true] - - describe 'overlapping drop', -> - it 'errors if two ops insert different content into the same place in an object', -> xf - op1: ['x', i:'hi'] - op2: ['x', i:'yo'] - conflict: type: DROP_COLLISION - expectLeft: ['x', r:true, i:'hi'] - expectRight: null - - it 'does not conflict if inserts are identical', -> xf - op1: ['x', i:'hi'] - op2: ['x', i:'hi'] - expectLeft: null - expectRight: null - - it 'does not conflict if the two operations make identical moves', -> xf - op1: [['a', p:0], ['x', d:0]] - op2: [['a', p:0], ['x', d:0]] - expect: null # ??? Also ok for left: ['x', p:0, d:0] - - it 'does not conflict if inserts are into a list', -> xf - op1: [1, i:'hi'] - op2: [1, i:'yo'] - expectLeft: [1, i:'hi'] - expectRight: [2, i:'hi'] - - it 'errors if the inserts are at the root', -> xf - op1: [i:1] - op2: [i:2] - conflict: type: DROP_COLLISION - expectLeft: [r:true, i:1] - expectRight: null - - it 'errors with insert vs drop', -> xf - op1: ['x', i:'hi'] - op2: [['a', p:0], ['x', d:0]] - # ???? - conflict: type: DROP_COLLISION - expectLeft: ['x', r:true, i:'hi'] - expectRight: null - - it 'errors with drop vs insert', -> xf - op1: [['a', p:0], ['x', d:0]] - op2: ['x', i:'hi'] - conflict: type: DROP_COLLISION - expectLeft: [['a', p:0], ['x', r:true, d:0]] - expectRight: ['a', r:true] - - it 'errors with drop vs drop', -> xf - op1: [['a', p:0], ['x', d:0]] - op2: [['b', p:0], ['x', d:0]] - conflict: type: DROP_COLLISION - expectLeft: [['a', p:0], ['x', r:true, d:0]] - expectRight: ['a', r:true] - - it 'errors if the two sides insert in the vacuum', -> xf - op1: [['a', p:0], ['b', d:0], ['c', i:5]] - op2: [['a', p:0], ['b', i:6], ['c', d:0]] - conflictLeft: - type: DROP_COLLISION - op1: [['a', p:0], ['b', d:0]] - op2: ['b', i:6] - expectLeft: [['b', r:true, d:0], ['c', p:0, i:5]] - conflictRight: - type: DROP_COLLISION - op1: ['c', i:5] - op2: [['a', p:0], ['c', d:0]] - expectRight: null - - - describe 'discarded edit', -> - it 'edit removed directly', -> xf - op1: ['a', es:[]] - op2: ['a', r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'edit inside new content throws RM_UNEXPECTED_CONTENT', -> xf - op1: ['a', 'b', i: 'hi', es:[]] - op2: ['a', r:true] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: ['a', 'b', i:'hi'] - expect: null - - describe 'blackhole', -> - it 'detects and errors', -> xf - op1: [['x', p:0], ['y', 'a', d:0]] - op2: [['x', 'a', d:0], ['y', p:0]] - conflict: type: BLACKHOLE - expect: ['x', r:true, 'a', r:true] # Also equivalent: ['x', r:true] - - it 'blackhole logic does not apply when op2 removes parent', -> xf - # TODO: Although you wouldn't know it, since this result is very similar. - op1: [['x', p:0], ['y', 'xx', 'a', d:0]] - op2: [['x', 'a', d:0], ['y', p:0, 'xx', r:true]] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: ['y', 'xx', r:true] - expect: ['x', r:true, 'a', r:true] # Also ok: ['x', r:true] - - it 'blackhole logic still applies when op2 inserts', -> xf - op1: [['x', p:0], ['y', 'a', i:{}, 'b', d:0]] - op2: [['x', 'a', i:{}, 'b', d:0], ['y', p:0]] - conflict: - type: BLACKHOLE - op1: [['x', p:0], ['y', 'a', 'b', d:0]] - op2: [['x', 'a', 'b', d:0], ['y', p:0]] - expect: ['x', r:true, 'a', r:true, 'b', r:true] - - it 'blackholes items in lists correctly', -> xf - op1: [1, p:0, 'a', d:0] - op2: [[1, 'b', d:0], [2, p:0]] - conflict: type: BLACKHOLE - expect: [1, r:true, 'b', r:true] - - it 'blackholes items despite scrambled pick and drop slots', -> xf - op1: [ [ 'a', { p: 1, d: 1 } ], [ 'x', { p: 0 } ], [ 'y', 'a', { d: 0 } ] ] - op2: [ [ 'x', 'a', { d: 0 } ], [ 'y', { p: 0 } ] ] - conflict: - type: BLACKHOLE - op1: [ [ 'x', { p: 0 } ], [ 'y', 'a', { d: 0 } ] ] - expect: [['a', p:0, d:0], ['x', r:true, 'a', r:true]] - - it 'handles chained blackholes', -> xf - op1: [ [ 'a', { p: 0 } ], # a->b.b, c->d.d - [ 'b', 'b', { d: 0 } ], - [ 'c', { p: 1 } ], - [ 'd', 'd', { d: 1 } ] - ] - op2: [ [ 'a', 'a', { d: 1 } ], # b->c.c, d->a.a - [ 'b', { p: 0 } ], - [ 'c', 'c', { d: 0 } ], - [ 'd', { p: 1 } ] - ] - conflict: type: BLACKHOLE - # c1: cMv(['a'], ['b', 'b']) - # c2: cMv(['b'], ['c', 'c']) - expect: [['a', r:true, 'a', r:true], ['c', r:true, 'c', r:true]] - - it 'creates conflict return values with valid slot ids', -> xf - op1: [['a', p:0], ['b', d:0], ['x', p:1], ['y', 'a', d:1]] - op2: [['x', 'a', d:0], ['y', p:0]] - conflict: - type: BLACKHOLE - op1: [['x', p:0], ['y', 'a', d:0]] - expect: [['a', p:0], ['b', d:0], ['x', r:true, 'a', r:true]] - - - describe 'transform-old', -> - it 'foo', -> - xf - op1: [ - ['x', ['a', {p:0}], ['b', {d:0}]], - ['y', ['a', {p:1}], ['b', {d:1}]] - ] - op2: ['x', {r:true}] - expect: ['y', ['a', {p:0}], ['b', {d:0}]] - - # it 'hard', -> - # op1: ['x', [1, r:true], [2, r:true, es:['hi']]] # Edit at index 4 originally. - # # move the edited string to .y[4] which - # op2: [['x', 4, p:0], ['y', [2, r:true], [4, d:0]]] - # expect: - - describe 'object edits', -> - it 'can reparent with some extra junk', -> xf - op1: [['x', p:0], ['y', d:0]] - op2: [ - ['_a', d:1] - ['_x', d:0] - ['x', p:0, 'a', p:1] - ] - expectLeft: [['_x', p:0], ['y', d:0]] - expectRight: null # the object was moved fair and square. - - describe 'deletes', -> - - it.skip 'delete parent of a move', -> xf - # The current logic of transform actually just burns everything (in a - # consistant way of course). I'm not sure if this is better or worse - - # basically we'd be saying that if a move could end up in one of two places, - # put it in the place where it won't be killed forever. But that introduces new - # complexity, so I'm going to skip this for now. - - # x.a -> a, delete x - op1: [['x', r:true, 'a', p:0], ['z', d:0]] - # x.a -> x.b. - op2: ['x', ['a', p:0], ['b', d:0]] - expect: [['x', r:true, 'b', p:0], ['z', d:0]] # TODO: It would be better to do this in both cases. - #expectRight: ['x', r:true] - - it 'awful delete nonsense', -> - xf - op1: [['x', r:true], ['y', i:'hi']] # delete doc.x, insert doc.y - op2: [['x', 'a', p:0], ['y', d:0]] # move doc.x.a -> doc.y - expect: [['x', r:true], ['y', r:true, i:'hi']] # del doc.x and doc.y, insert doc.y - - xf - op1: [['x', 'a', p:0], ['y', d:0]] # x.a -> y - op2: [['x', r:true], ['y', i:'hi']] # delete x, ins y - expect: null - - xf - op1: [10, r:true] - op2: [[5, d:0], [10, 1, p:0]] - expect: [[5, r:true], [11, r:true]] - # And how do those indexes interact with pick / drop operations?? - - - describe 'swap', -> - swap = [ - ['a', p:0, 'b', p:1] - ['b', d:1, 'a', d:0] - ] - - it 'noop vs swap', -> xf - op1: null - op2: swap - expect: null - - it 'can swap two edits', -> xf - op1: ['a', es:['a edit'], 'b', es:['b edit']] - op2: swap - expect: ['b', es:['b edit'], 'a', es:['a edit']] - - describe 'lists', -> - it 'can rewrite simple list indexes', -> - xf - op1: [10, es:['edit']] - op2: [0, i:'oh hi'] - expect: [11, es:['edit']] - - xf - op1: [10, r:true] - op2: [0, i:'oh hi'] - expect: [11, r:true] - - xf - op1: [10, i:{}] - op2: [0, i:'oh hi'] - expect: [11, i:{}] - - it 'can change the root from an object to a list', -> xf - op1: ['a', es:['hi']] - op2: [{i:[], r:true}, [0, d:0], ['a', p:0]] - expect: [0, es:['hi']] - - it 'can handle adjacent drops', -> xf - op1: [[11, i:1], [12, i:2], [13, i:3]] - op2: [0, r:true] - expect: [[10, i:1], [11, i:2], [12, i:3]] - - it 'fixes drop indexes correctly 1', -> xf - op1: [[0, r:true], [1, i:'hi']] - op2: [1, r:true] - expect: [0, r:true, i:'hi'] - - it 'list drop vs delete uses the correct result index', -> - xf - op1: [2, i:'hi'] - op2: [2, r:true] - expect: [2, i:'hi'] - - xf - op1: [3, i:'hi'] - op2: [2, r:true] - expect: [2, i:'hi'] - - it 'list drop vs drop uses the correct result index', -> xf - op1: [2, i:'hi'] - op2: [2, i:'other'] - expectLeft: [2, i:'hi'] - expectRight: [3, i:'hi'] - - it 'list drop vs delete and drop', -> - xf - op1: [2, i:'hi'] - op2: [2, r:true, i:'other'] - expectLeft: [2, i:'hi'] - expectRight: [3, i:'hi'] - - xf - op1: [3, i:'hi'] - op2: [[2, r:true], [3, i:'other']] - expect: [2, i:'hi'] - - xf - op1: [4, i:'hi'] - op2: [[2, r:true], [3, i:'other']] - expectLeft: [3, i:'hi'] - expectRight: [4, i:'hi'] - - it 'list delete vs drop', -> - xf - op1: [1, r:true] - op2: [2, i:'hi'] - expect: [1, r:true] - - xf - op1: [2, r:true] - op2: [2, i:'hi'] - expect: [3, r:true] - - xf - op1: [3, r:true] - op2: [2, i:'hi'] - expect: [4, r:true] - - it 'list delete vs delete', -> - xf - op1: [1, r:true] - op2: [1, r:true] - expect: null # It was already deleted. - - it 'fixes drop indexes correctly 2', -> xf - op1: [[0, r:true], [1, i:'hi']] - op2: [2, r:true] # Shouldn't affect the op. - expect: [[0, r:true], [1, i:'hi']] - - it 'insert vs delete parent', -> xf - op1: [2, 'x', i:'hi'] - op2: [2, r:true] - conflict: type: RM_UNEXPECTED_CONTENT - expect: null - - it 'transforms against inserts in my own list', -> - xf #[0,1,2,3] -> [a,0,b,1,2,3...] - op1: [[0, i:'a'], [2, i:'b']] - op2: [1, r:true] - expect: [[0, i:'a'], [2, i:'b']] - - it 'vs cancelled op2 drop', -> xf - doc: {x:{a:'x.a'}, y:['a','b','c']} - op1: [['x', r:true], ['y', 3, i:5]] - op2: [['x', 'a', p:0], ['y', 2, d:0]] - expect: [['x', r:true], ['y', [2, r:true], [3, i:5]]] - - it 'vs cancelled op1 drop', -> xf - op1: [['x', p:0], ['y', [3, d:0], [4, i:5]]] - op2: ['x', r:true] - expect: ['y', 3, i:5] - - it 'vs cancelled op1 pick', -> xf - doc: Array.from 'abcdefg' - op1: [[1, p:0], [4, r:true, i:4], [6, d:0]] - op2: [1, r:true] - expect: [[3, r:true], [4, i:4]] - - it 'xxxxx 1', -> diamond # TODO Regression. - doc: Array.from('abcdef') - op1: [[1, {p:0, i:'AAA'}], [3, {i:'BBB'}], [5, {d:0}]] - op2: [1, {r:true}] - - it 'xxxxx 2', -> diamond - doc: Array.from('abcdef') - op1: [[1, {p:0, i:'AAA'}], [3, {d:0}], [5, {i:'CCC'}]] - op2: [1, {r:true}] - - - describe 'edit', -> - it 'transforms edits by one another', -> xf - op1: [1, es:[2, 'hi']] - op2: [1, es:['yo']] - expect: [1, es:[4, 'hi']] - - it 'copies in ops otherwise', -> xf - op1: ['x', {e:{position:2, text:'wai'}, et:'simple'}] - op2: ['y', r:true] - expect: ['x', {e:{position:2, text:'wai'}, et:'simple'}] - - it 'allows edits at the root', -> xf - op1: [{e:{position:2, text:'wai'}, et:'simple'}] - op2: [{e:{position:0, text:'omg'}, et:'simple'}] - expect: [{e:{position:5, text:'wai'}, et:'simple'}] - - it 'applies edits in the right order', -> xf - # Edits happen *after* the drop phase. - op1: [1, es:[2, 'hi']] - op2: [[1, i:{}], [2, es:['yo']]] - expect: [2, es:[4, 'hi']] - - it 'an edit on a deleted object goes away', -> xf - op1: [1, es:[2, 'hi']] - op2: [1, r:"yo"] - conflict: - type: RM_UNEXPECTED_CONTENT - op2: [1, r:true] # .... It'd be better if this copied the remove. - expect: null - - # TODO Numbers - - -# ***** Test cases found by the fuzzer which have caused issues - describe 'fuzzer tests', -> - it 'asdf', -> apply - doc: { the: '', Twas: 'the' } - op: [ 'the', { es: [] } ] - expect: { the: '', Twas: 'the' } - - it 'does not duplicate list items from edits', -> apply - doc: ['eyes'] - op: [ 0, { es: [] } ] - expect: ['eyes'] - - it 'will edit the root document', -> apply - doc: '' - op: [es:[]] - expect: '' - - # ------ These have nothing to do with apply. TODO: Move them out of this grouping. - - it 'diamond', -> - # TODO: Do this for all combinations. - diamond - doc: Array.from 'abcde' - op1: [ [ 0, { p: 0 } ], [ 1, { d: 0 } ] ] - op2: [ [ 0, { p: 0 } ], [ 4, { d: 0 } ] ] - - it 'shuffles lists correctly', -> xf - op1: [ [ 0, { p: 0 } ], [ 1, { d: 0 } ] ] - op2: [ [ 0, { p: 0 } ], [ 10, { d: 0 } ] ] - expectLeft: [ [ 1, { d: 0 } ], [ 10, { p: 0 } ] ] - expectRight: null - - it 'inserts before edits', -> - xf - op1: [0, 'x', i:5] - op2: [0, i:35] - expect: [1, 'x', i:5] - - xf - op1: [0, es:[]] - op2: [0, i:35] - expect: [1, es:[]] - - it 'duplicates become noops in a list', - -> xf - op1: [0,{"p":0,"d":0}] - op2: [0,{"p":0,"d":0}] - expectLeft: [0,{"p":0,"d":0}] # This is a bit weird. - expectRight: null - - -> xf - op1: [0, r:true, i:'a'] - op2: [0, i:'b'] - expectLeft: [[0, i:'a'], [1, r:true]] - expectRight: [1, r:true, i:'a'] - - -> xf - op1: [0, r:true, i:5] - op2: [0, r:true] - expect: [0, i:5] - - it 'p1 pick descends correctly', -> - xf - op1: [2, r:true, 1, es:['hi']] - op2: [3, 1, r:true] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [2, 1, es:['hi']] - expect: [2, r:true] - - xf - op1: [[2, r:true, 1, es:['hi']], [3, 1, r:true]] - op2: [3, 2, r:true] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [2, 1, es:['hi']] - expect: [[2, r:true], [3, 1, r:true]] - - it 'transforms picks correctly', -> xf - op1: [1, 1, r:true] - op2: [0, p:0, d:0] - expect: [1, 1, r:true] - - it 'pick & drop vs insert after the picked item', -> xf - op1: [0, p:0,d:0] # Remove / insert works the same. - op2: [1, i:"hi"] - expectLeft: [0, p:0,d:0] - expectRight: [[0, p:0], [1, d:0]] - - it 'pick same item vs shuffle list', -> xf - op1: [1, ['x', p:0], ['y', d:0]] - op2: [1, {d:0}, 'x', {p:0}] - expectLeft: [1, p:0, 'y', d:0] - expectRight: null - - it 'remove the same item in a list', -> xf - op1: [ 0, { r: true } ] - op2: [ 0, { r: true } ] - expect: null - - it 'rm vs hold item', -> xf - op1: [ 0, { r: true } ] - op2: [ 0, { p: 0, d: 0 } ] - expect: [ 0, { r: true } ] - - it 'moves child elements correctly', -> xf - doc: ['a', [0,10,20], 'c'] - op1: [ 1, 0, { p: 0, d: 0 } ] - op2: [ [ 1, { d: 0 } ], [ 2, { p: 0 } ] ] - expect: [ 2, 0, { d:0, p:0 } ] - - it 'moves list indexes', -> xf - doc: [[], 'b', 'c'] - op1: [ [ 0, 'hi', { d: 0 } ], [ 1, { p: 0 } ] ] - op2: [ [ 0, { p: 0 } ], [ 20, { d: 0 } ] ] - expect: [[0, p:0], [19, 'hi', d:0]] - - it 'insert empty string vs insert null', -> xf - doc: undefined - op1: [i:'hi'] - op2: [i:null] - conflict: type: DROP_COLLISION - expectLeft: [r:true, i:'hi'] - expectRight: null - - it 'move vs emplace', -> xf - doc: ['a', 'b'] - op1: [[0, p:0], [1, d:0]] - op2: [1, {p:0, d:0}] - expectLeft: [0, {p:0, d:0}] - expectRight: [[0, p:0], [1, d:0]] - - it 'rm chases a subdocument that was moved out', -> xf - doc: [ [ 'aaa' ] ] - op1: [ 0, { r: true } ] - op2: [ 0, { d: 0 }, 0, { p: 0 } ] # Valid because lists. - expect: [[0, r:true], [1, r:true]] - - it 'colliding drops', -> xf - doc: [ 'a', 'b', {} ] - op1: [[0, p:0], [1, 'x', d:0]] # -> ['b', x:'a'] - op2: [1, p:0, 'x', d:0] # -> ['a', x:'b'] - conflict: type: DROP_COLLISION - expectLeft: [[0, p:0, 'x', d:0], [1, 'x', r:true]] - expectRight: [0, r:true] - - it 'transform crash', -> xf - op1: [ [ 'the', { r: true, d: 0 } ], [ 'whiffling', { p: 0 } ] ] - op2: [ 'the', { p: 0, d: 0 } ] - expect: [ [ 'the', { d: 0, r: true } ], [ 'whiffling', { p: 0 } ] ] - - it 'transforms drops when the parent is moved by a remove', -> xf - op1: [['a', {p:0}], ['b', {d:0}, 1, {i:2}]] - op2: ['a', 0, {r:1}] - expect: [['a', {p:0}], ['b', {d:0}, 0, {i:2}]] - - it 'transforms drops when the parent is moved by a drop', -> xf - op1: [['a', {p:0}], ['b', {d:0}, 1, {i:2}]] - op2: ['a', 0, {i:1}] - expect: [['a', {p:0}], ['b', {d:0}, 2, {i:2}]] - - it 'transforms conflicting drops obfuscated by a move', -> xf - op1: [['a', {p:0}], ['b', {d:0}, 1, {i:2}]] - op2: ['a', 1, {i:1}] - expectLeft: [['a', {p:0}], ['b', {d:0}, 1, {i:2}]] - expectRight: [['a', {p:0}], ['b', {d:0}, 2, {i:2}]] - - it 'transforms edits when the parent is moved', -> xf - op1: [ [ 'x', { p: 0 } ], [ 'y', { d: 0, es: [ 1, 'xxx' ] } ] ] - op2: [ 'x', { es: [ d: 1, 'Z' ] } ] - expectLeft: [ [ 'x', { p: 0 } ], [ 'y', { d: 0, es: [ 'xxx' ] } ] ] - expectRight: [ [ 'x', { p: 0 } ], [ 'y', { d: 0, es: [ 1, 'xxx' ] } ] ] - - it 'xf lots', -> xf - op1: [['a', p:0], ['b', d:0, es:['hi']]] - op2: [['a', p:0], ['c', d:0]] - expectLeft: [['b', d:0, es:['hi']], ['c', p:0]] - expectRight: ['c', es:['hi']] - - it 'inserts are moved back by the other op', -> xf - op1: [['a', p:0], ['b', d:0, 'x', i:'hi']] - op2: [['a', p:0], ['c', d:0]] - expectLeft: [['b', d:0, 'x', i:'hi'], ['c', p:0]] - expectRight: ['c', 'x', i:'hi'] - - it 'more awful edit moves', -> xf - op1: [['a', p:0], ['c', d:0, 'x', es:[]]] - op2: ['a', ['b', d:0], ['x', p:0]] - expect: [['a', p:0], ['c', d:0, 'b', es:[]]] - - it 'inserts null', -> xf - op1: [ 'x', 'a', { i: null } ] - op2: [ [ 'x', { p: 0 } ], [ 'y', { d: 0 } ] ] - expect: [ 'y', 'a', { i: null } ] - - it 'preserves local insert if both sides delete', -> xf - op1: [ { i: {}, r: true }, 'x', { i: 'yo' } ] - op2: [ { r: true } ] - expect: [ { i: {} }, 'x', { i: 'yo' } ] - - it 'handles insert/delete vs move', -> xf - op1: [ 'a', { i: {}, r: true }, 'x', { i: 'yo' } ] - op2: [ [ 'a', { p: 0 } ], [ 'b', { d: 0 } ] ] - expect: [ [ 'a', { i: {} }, 'x', { i: 'yo' } ], [ 'b', { r: true }, ] ] - - it 'insert pushes edit target', -> xf - op1: [[ 0, { i: "yo" } ], [ 1, 'a', { es: [] }]] - op2: [0, [ 'a', { p: 0 } ], [ 'b', { d: 0 } ]] - expect: [[0, { i: 'yo' }], [1, 'b', { es: [] }]] - - it 'composes simple regression', -> - compose - op1: [ 0, { p: 0, d: 0 } ] - op2: [ { r: true } ] - expect: [ { r: true }, 0, { r: true } ] - - compose - op1: [ 'a', 1, { r: true } ] - op2: [ 'a', { r: true } ] - expect: [ 'a', { r: true }, 1, { r: true } ] - - it 'ignores op2 inserts for index position after op1 insert', -> xf - op1: [ { r:true, i: [] }, 0, { i: '' } ] - op2: [ 0, { i: 0 } ], - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [r:true] - expect: [ { r: true, i: [] }, 0, { r:true, i: '' } ] - - it 'edit moved inside a removed area should be removed', -> xf - op1: [[ 0, { r: true } ], [ 2, { es: [ ] } ]] - op2: [[ 0, 'x', { d: 0 } ], [ 3, { p: 0 } ]] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [0, r:true] - expect: [ 0, { r: true }, 'x', {r:true} ] - - it 'advances indexes correctly with mixed numbers', -> xf - op1: [ [ 'x', [ 0, { p: 0 } ], [ 1, { d: 1 } ] ], [ 'y', { p: 1 } ], [ 'zzz', { d: 0 } ] ] - op2: [ [ 'x', 2, { i: 'hi' } ], [ 'y', { p: 0 } ], [ 'z', { d: 0 } ] ] - expectLeft: [ [ 'x', [ 0, { p: 1 } ], [ 1, { d: 0 } ] ], [ 'z', { p: 0 } ], [ 'zzz', { d: 1 } ] ] - expectRight: [ [ 'x', 0, { p: 0 } ], [ 'zzz', { d: 0 } ] ] - - it 'handles index positions past cancelled drops 1', -> xf - op1: [ 0, { r: true, i: [ '' ] } ], - op2: [ [ 0, { p: 0, d: 0 } ], [ 1, { i: 23 } ] ] - expectLeft: [ 0, { r: true, i: [ '' ] } ] - expectRight: [ [ 0, { r: true } ], [ 1, { i: [ '' ] } ] ] - - it 'handles index positions past cancelled drops 2', -> xf - # This looks more complicated, but its a simpler version of the above test. - op1: [ [ 'a', { r: true } ], [ 'b', 0, { i: 'hi' } ] ] - op2: [ [ 'a', { p: 0 } ], [ 'b', [ 0, { d: 0 } ], [ 1, { i: 'yo' } ] ] ] - expectLeft: [ 'b', 0, { i: 'hi', r: true } ] - expectRight: [ 'b', [ 0, { r: true } ], [ 1, { i: 'hi' } ] ] - - it 'calculates removed drop indexes correctly', -> xf - op1: [ [ 0, { i: 'hi', p: 0 } ], [ 1, 1, { d: 0 } ], [ 2, { r: true } ] ] - op2: [ [ 0, { i: 'yo', p: 0 } ], [ 1, 1, { d: 0 } ] ] - expectLeft: [ [ 0, { i: 'hi' } ], [ 1, 1, { p: 0 } ], [ 2, { r: true }, 1, { d: 0 } ] ] - expectRight: [ [ 1, { i: 'hi' } ], [ 2, { r: true } ] ] - - it 'removed drop indexes calc regression', -> xf - op1: [ [ 1, { p: 0 }, 'burbled', { d: 0 } ], [ 3, { r: true } ] ] - op2: [ [ 0, { i: 'to', r: true } ], [ 1, { p: 1 }, [ 'its', { d: 0 } ], [ 'thought', { d: 1 } ] ], [ 3, { p: 0 } ] ] - expectLeft: [ 1, [ 'burbled', { d: 0 } ], [ 'its', { r: true } ], [ 'thought', { p: 0 } ] ] - expectRight: [ 1, 'its', { r: true } ] - - it 'removed drop indexes tele to op1 pick', -> xf - op1: [ 'a', 0, [ 0, { es: [] } ], [ 2, { r: true } ] ] - op2: [ [ 'a', { p: 0 }, 0, 0, { p: 1 } ], [ 'b', { d: 0 }, 0, 1, 0, { d: 1 } ] ] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: ['a', 0, 2, r:true] - op2: [ [ 'a', 0, 0, { p: 0 } ], [ 'b', 0, 1, 0, { d: 0 } ] ] - expect: [ 'b', 0, 1, { r: true }, 0, { r: true } ] - - it 'tracks removed drop index teleports', -> xf - # rm 0.a, move 0.b -> 0.c - doc: [{a:['a'], b:'b'}] - op1: [ 0, [ 'a', { r: true } ], [ 'b', { p: 0 } ], [ 'c', { d: 0 } ] ] # [{c:'b'}] - op2: [ 0, { d: 0, p: 1 }, [ 0, { d: 1 } ], [ 'a', { p: 0 } ] ] # [[{b:'b'}, 'a']] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: [0, 'a', r:true] - op2: [0, p:0, 0, d:0] - expect: [ 0, { r: true }, 0, { r: true } ] - - it 'handles transforming past cancelled move', -> xf - op1: [ [ 0, { r: true } ], [ 10, { i: [ '' ] } ] ] - op2: [ 0, { p: 0, d: 0 } ] - expect: [ [ 0, { r: true } ], [ 10, { i: [ '' ] } ] ] - - it 'correctly adjusts indexes in another fuzzer great', -> xf - op1: [ [ 0, { d: 0, r: true } ], [ 3, { p: 0 } ] ] - op2: [ [ 0, { p: 0 } ], [ 3, { d: 0 } ] ] - expect: [[0, d:0], [2, p:0], [3, r:true]] - - it 'op2 moves into something op1 removes and op1 moves into that', -> xf - op1: [ [ 'a', { r: true }, 'aa', { p: 0 } ], [ 'b', 'x', { d: 0 } ] ] - op2: [ [ 'a', 'bb', { d: 0 } ], [ 'b', { p: 0 } ] ] - conflict: - type: RM_UNEXPECTED_CONTENT - op1: ['a', r:true] - expect: [ 'a', { r: true }, ['aa', r:true], ['bb', r:true]] # Also ok if we miss the second rs. - - it 'op2 moves into op1 remove edge cases', -> - # Sorry not minified. - xf - op1: [ 'Came', 0, [ 0, { r: true }, 'he', { p: 0 } ], [ 1, { d: 0 }, 0, { i: 'time' } ] ] - op2: [ 'Came', 0, [ 0, 'he', [ 0, { d: 0 } ], [ 1, { es: [] } ] ], [ 1, { p: 0 } ] ], - expectLeft: [ 'Came', 0, 0, { r: true, d: 0 }, [ 0, { i: 'time' } ], [ 'he', { p: 0 } ] ] - expectRight: [ 'Came', 0, 0, { r: true, d: 0 }, [ 1, { i: 'time' } ], [ 'he', { p: 0 } ] ] - - xf - op1: [ [ 0, [ 1, { p: 0 } ], [ 2, { r: true } ] ], [ 1, 'xxx', { d: 0 } ] ] - op2: [ 0, 1, { i: {}, p: 0 }, 'b', { d: 0 } ] - expectLeft: [ [ 0, [ 1, 'b', { p: 0 } ], [ 2, { r: true } ] ], [ 1, 'xxx', { d: 0 } ] ] - expectRight: [ 0, 2, { r: true } ] - - it 'translates indexes correctly in this fuzzer find', -> xf - op1: [ 0, { p: 0 }, 'x', { d: 0 } ] - op2: [ [ 0, { p: 0, d: 0 } ], [ 1, { i: 'y' } ] ] - expectLeft: [[0, { p: 0 }], [1, 'x', { d: 0 }]] - expectRight: null - - it 'buries children of blackholed values', -> xf - op1: [ [ 0, [ 'a', { p: 0 } ], [ 'b', { d: 0 } ], [ 'c', { d: 1 } ] ], [ 1, { p: 1 } ] ] - op2: [ 0, { p: 0 }, 'x', { d: 0 } ] - # This is a bit interesting. The question is, which op2 picks and drops - # should we include in the output? For now the answer is that we include - # anything in both ops thats going to end up inside the blackholed - # content. - conflict: type: BLACKHOLE - - # op1: [[0, 'c', d:0], [1, p:0]] - expect: [ 0, r: true, 'x', r:true ] - - it 'does not conflict when removed target gets moved inside removed container', -> - # This edge case is interesting because we don't generate the same - # conflicts on left and right. We want our move of a.x to escape the - # object before removing it, but when we're right, the other operation's - # move holds the object and we get an unexpected rm conflict. - xf - op1: [ [ 'a', { r: true }, 'x', { p: 0 } ], [ 'b', { d: 0 } ] ] - op2: [ 'a', [ 'x', { p: 0 } ], [ 'y', { d: 0 } ] ] - conflictRight: - type: RM_UNEXPECTED_CONTENT - op1: ['a', r:true] - expectLeft: [ [ 'a', { r: true }, 'y', { p: 0 } ], [ 'b', { d: 0 } ] ] - expectRight: [ 'a', { r: true }, 'y', {r:true}] - - xf - op1: [ [ 'a', { r: true }, 1, { p: 0 } ], [ 'b', { d: 0 } ] ] - op2: [ 'a', [ 0, { d: 0 } ], [ 1, { p: 0 } ] ] - expectLeft: [ [ 'a', { r: true }, 0, { p: 0 } ], [ 'b', { d: 0 } ] ] - conflictRight: - type: RM_UNEXPECTED_CONTENT - op1: ['a', r:true] - expectRight: [ 'a', { r: true }, 0, {r:true}] - - expect: [ [ 'a', { r: true }, 0, { p: 0 } ], [ 'b', { d: 0 } ] ] - - it 'compose copies op2 edit data', -> compose - op1: [ 'a', { r: true } ] - op2: [ [ 'x', { p: 0 } ], [ 'y', { d: 0 }, 'b', { es: [] } ] ] - expect: [ - ['a', r:true] - ['x', {p:0}] - ['y', {d: 0}, 'b', {es: []}] - ] - - it 'does not conflict when the dest is salvaged', -> xf - op1: [ [ 'a', { p: 0 } ], [ 'b', { i: 'hi' } ], [ 'c', { d: 0 } ] ] - op2: [ [ 'a', { p: 0 } ], [ 'b', { d: 0 } ] ] - expectLeft: [['b', {p:0, i:'hi'}], ['c', d:0]] - conflictRight: - type: DROP_COLLISION - op1: [ 'b', { i: 'hi' } ] - expectRight: null - - it 'does not conflict on identical r/i pairs', -> xf - op1: [{ i: [], r: true }] - op2: [{ i: [], r: true }] - expect: null - - it 'allows embedded edits in identical r/i', -> xf - op1: [ { r: true, i: '', es: [] } ] - op2: [ { r: true, i: '' } ] - expect: [es:[]] - - it 'does not conflict on identical r/i pairs with identical drops inside', -> xf - op1: [ { i: {}, r: true }, 'a', { i: 'a' } ] - op2: [ { i: {}, r: true }, 'a', { i: 'a' } ] - expect: null - - it 'generates a DROP_COLLISION on children', -> xf - op1: [ { i: {}, r: true }, 'a', { i: 'a' } ] - op2: [ { i: {}, r: true }, 'a', { i: 'b' } ] - conflict: - type: DROP_COLLISION - op1: ['a', { i: 'a' } ] - op2: ['a', { i: 'b' } ] - expectLeft: ['a', r:true, i:'a'] - expectRight: null - - it 'Transforms edit moves into the right dest', -> xf - op1: [ 0, { p: 0, d: 0 }, - # These parts are all needed for some reason. - [ 0, { i: 1 } ], - [ 1, { r: true } ], - [ 3, { es: [] } ] - ] - op2: [ 0, [ 0, { d: 0 } ], [ 3, { p: 0 } ] ] - expectLeft: [ 0, {p:0, d:0}, - [0, i:1], - [1, es:[]], - [2, r:true] - ] - expectRight: [0, {p:0, d:0} - [0, es:[]], - [1, i:1], - [2, r:true] - ] - - it 'adjusts indexes of pick -> drop', -> xf - op1: [ 0, { p: 0, d: 0 } ] - op2: [ [ 0, { i: 'yo', p: 0 } ], [ 1, { d: 0 } ] ], - expectLeft: [ [ 0, { d: 0 } ], [ 1, { p: 0 } ] ] - expectRight: null - - it 'clears output outDrop when theres no pick', -> xf - # Again, not minimized. We return the right data, we were just double- - # descending into outDrop. - op1: [ [ 'the', { d: 0, p: 0 } ], [ 'toves', { r: true } ] ] - op2: [ - [ 'bird', { d: 0 } ], - [ 'slain', { d: 1 } ], - [ 'the', { p: 1 } ], - [ 'toves', { p: 0 } ] - ] - expectLeft: [ - [ 'bird', { r: true } ], - [ 'slain', { p: 0 } ], - [ 'the', { d: 0 } ] - ] - expectRight: [ 'bird', { r: true } ] - - it 'pushes drop indexes by other held items', -> xf - op1: [ [ 0, { p: 0 }], - [ 1, - [ 0, { i: 'hi' } ], - [ 1, { d: 0, es: [] } ] ] - ] - op2: [ - [ 0, { p: 1 }, 1, { d: 0 }, 2, { d: 1 } ], - [ 2, { p: 0 } ] - ] - expectLeft: [ 0, 1, - [ 0, { i: 'hi' } ], - [ 1, { d: 0, es: [] } ], - [ 2, { p: 0 } ] - ] - expectRight: [ 0, 1, [ 0, { i: 'hi' } ], [ 3, { es: [] } ] ] - - it 'composes correctly with lots of removes', -> compose - op1: [ 3, 1, { r: true } ], - op2: [ - [ 0, { es: [] } ], - [ 1, { r: true, es: [] } ], - [ 2, { r: true } ] - ] - expect: [ - [ 0, { es: [] } ], - [ 1, { es: [], r: true } ], - [ 2, { r: true } ], - [ 3, 1, { r: true } ] - ] - - it 'does not descend twice when p/r on an identical insert', -> xf - op1: [ [ 'a', { p: 0, i: '' } ], [ 'b', { d: 0 } ] ] - op2: [ 'a', { r: true, i: '' } ] - expect: null - - it 'conflicts underneath a moved / inserted child', -> xf - op1: [ [ 'a', { p: 0, i: {} }, 'x', {i:5} ], [ 'b', { d: 0 } ] ] - op2: [ 'a', { r: true, i: {} }, 'x', {i:6} ] - conflict: - type: DROP_COLLISION - op1: ['a', 'x', i:5] - op2: ['a', 'x', i:6] - expectLeft: ['a', 'x', {r:true, i:5}] - expectRight: null - - it 'clears drop2 in transform moves', -> xf - doc: [b: a: 'hi'] - op1: [0, d:0, - [ 'a', { es: [] } ], - [ 'b', { p: 0 } ] - ] - op2: [ 0, 'b', - [ 'a', { p: 0 } ], - [ 'b', { d: 0 } ] - ] - expect: [0, d:0, 'b', { p: 0, es:[] }] - - it 'descends correctly when op2 picks and drops', -> xf - op1: [ - [ 'b', { d: 0 }, [ 1, { es: [] } ], [ 2, { i: null } ] ], - [ 'e', { p: 0 } ] - ] - op2: [ { p: 0, d: 0 }, 'e', 1, { p: 1, d: 1 } ] - expectLeft: [ - [ 'b', { d: 0 }, [ 1, { i: null } ], [ 2, { es: [] } ]], - [ 'e', { p: 0 } ] - ] - expectRight: [ - [ 'b', { d: 0 }, [ 1, { es: [] } ], [ 2, { i: null } ] ], - [ 'e', { p: 0 } ] - ] - - it 'composes a pick out of the insert', -> compose - op1: [ { i: [ 5, { x: 6 } ] } ] - op2: [ [ 0, { r: true }, 'c', { d: 0 } ], [ 1, 'x', { p: 0 } ] ] - # expect: [{i: [{c: 6}]}] - expect: [ { i: [ {} ] }, 0, 'c', { i: 6 } ] - - it 'is not overeager to remove intermediate literal array items', -> compose - op1: [ [ 0, { i: [ 'a', 'b' ] }, 0, { p: 0 } ], [ 1, 0, { d: 0 } ] ] - op2: [ 0, { r: ['a'] }, 1, { r: 'b' } ] - expect: [ 0, 0, { d: 0, p: 0 } ] - - it 'descends down insert indexes correctly', -> compose - op1: [ { i: [ {}, 'a' ] }, 1, { i: 'b' } ] - op2: [ [ 1, { r: 'b' } ], [ 2, { r: 'a' } ] ] - expect: [ { i: [ {} ] } ] - - it 'handles composes with ena: 0', -> compose - op1: [i:10] - op2: [ena:0] - expect: [i:10, ena:0] # Also ok: just discarding the ena:0. - - it 'handles rm parent with cross move', -> compose - op1: [ [ 'a', { p: 0 } ], [ 'b', 1, { d: 0 } ] ] - op2: [ [ 'b', { r: true }, 1, { p: 0 } ], [ 'c', { d: 0 } ]] - expect: [ [ 'a', { p: 0 } ], [ 'b', { r: true } ], [ 'c', { d: 0 } ] ] - - it 'lets you remove children of an op at 2 levels', -> compose - op1: [ { i: [ 'a', { x: 'hi' } ] } ] - op2: [ { r: true }, 1, 'x', { r: true } ] - expect: null - - it 'discards op1 inserts inside a removed chunk', -> compose - op1: [ 'y', [ 1, { i: 'x' } ], [ 2, { i: [ 'a', 'b' ] } ] ] - op2: [ { r: true }, 'y', 2, 0, { r: true } ] - expect: [ { r: true } ] - - it 'handles deeply nested blackhole operations', -> xf - op1: [ - [ 'x', { p: 0 } ], - [ 'y', - [ 'a', - [ 'j', { p: 1 } ], - [ 'k', { d: 1 } ] - ], - [ 'b', { d: 0 }] - ] - ] - op2: [ - [ 'x', 'xx', { d: 0 }, 'j', 'jj', { d: 1 } ], - [ 'y', { p: 1 }, 'a', { p: 0 } ] - ] - conflict: type: BLACKHOLE - expect: ['x', r:true, 'xx', r:true, 'j', 'jj', r:true] - - it 'does not list removed op1 moves in the blackhole info', -> xf - op1: [ - [ 'a', [ 'j', { d: 0 } ], [ 'k', { d: 1 } ] ], - [ 'b', { p: 0 }, 'z', 0, { p: 1 } ] - ] - op2: [ - [ 'a', { p: 0 } ], - [ 'b', [ 'y', { d: 0 } ], [ 'z', { r: true } ] ] - ] - conflict: - type: BLACKHOLE - op1: [ [ 'a', 'j', { d: 0 } ], [ 'b', { p: 0 } ] ] - op2: [ [ 'a', { p: 0 } ], [ 'b', 'y', { d: 0 } ] ] - expect: ['b', r:true, 'y', r:true] - - it 'handles overlapping pick in blackholes', -> xf - # This looks complicated, but its really not so bad. Its: - # a->b.0, a.x -> z - # vs - # b -> a.x -> a.y - # - # Its a bit twisty because we're both picking up the same element and - # putting it in different places. This is why we have different left and - # right results. - op1: [ - [ 'a', { p: 1 }, 'x', { p: 0 } ], - [ 'b', 0, { d: 1 } ], - [ 'z', { d: 0 } ] - ] - op2: [ - [ 'a', [ 'x', { d: 0, p: 1 } ], [ 'y', { d: 1 } ] ], - [ 'b', { p: 0 } ] - ] - conflictLeft: - type: BLACKHOLE - op1: [['a', p:0], ['b', 0, d:0]] - op2: [['a', 'x', d:0], ['b', p:0]] - expectLeft: [ - [ 'a', { r: true }, - [ 'x', { r: true } ], - [ 'y', { p: 0 } ] - ], - [ 'z', { d: 0 } ] - ] - conflictRight: - type: BLACKHOLE - op1: [['a', p:0], ['b', 0, d:0]] - expectRight: [ 'a', { r: true }, - [ 'x', { r: true } ], - [ 'y', { r: true } ] - ] - \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..e59b242 --- /dev/null +++ b/test/test.js @@ -0,0 +1,3078 @@ +// Unit tests for the JSON1 OT type. +// +// These tests are quite unstructured. You can see the skeletons of a few +// organizing systems, but ultimately there's just lots of test cases to run. +// +// Cleanups welcome, so long as you don't remove any tests. + +const assert = require('assert') +const { type } = require('../lib/json1') +const log = require('../lib/log') +const deepClone = require('../lib/deepClone') + +const { transform } = type +const { DROP_COLLISION, RM_UNEXPECTED_CONTENT, BLACKHOLE } = type + +const apply = ({ doc: snapshot, op, expect }) => { + type.setDebug(false) + + const orig = deepClone(snapshot) + try { + const result = type.apply(snapshot, op) + assert.deepStrictEqual(snapshot, orig, 'Original snapshot was mutated') + return assert.deepStrictEqual(result, expect) + } catch (e) { + console.log( + [ + 'Apply failed! Repro ', + `apply( ${JSON.stringify(snapshot)}, ${JSON.stringify(op)} )` + ].join('') + ) + console.log(`expected output: ${JSON.stringify(expect)}`) + throw e + } +} + +const d = fn => { + type.setDebug(true) + fn() + return type.setDebug(false) +} + +const compose = ({ op1, op2, expect }) => { + try { + const result = type.compose( + op1, + op2 + ) + return assert.deepStrictEqual(result, expect) + } catch (e) { + d(() => { + console.error('FAIL! Repro with:') + console.log(`compose( ${JSON.stringify(op1)}, ${JSON.stringify(op2)} )`) + console.log(`expected output: ${JSON.stringify(expect)}`) + return type.compose( + op1, + op2 + ) + }) + throw e + } +} + +const invConflict = ({ type, op1, op2 }) => ({ type, op1: op2, op2: op1 }) + +const otherSide = side => (side === 'left' ? 'right' : 'left') + +const checkConflict = ({ + op1, + op2, + side, + conflict: expectConflict, + expect +}) => { + // We should get the same conflict with xf(op1, op2, left) and xf(op2, op1, right). + if (expectConflict != null) { + if (!expectConflict.op1) { + expectConflict.op1 = type.normalize(op1) + } + if (!expectConflict.op2) { + expectConflict.op2 = type.normalize(op2) + } + } + + const result = [] + for (let [side_, op1_, op2_, ec] of [ + [side, op1, op2, expectConflict], + [ + otherSide(side), + op2, + op1, + expectConflict ? invConflict(expectConflict) : null + ] + ]) { + try { + // d -> log('tryTransform', side_, op1_, op2_) + const { ok, conflict } = type.tryTransform(op1_, op2_, side_) + if (ec == null) { + // We don't care what the result is here; just that it doesn't conflict. + result.push(assert(ok)) + } else { + assert(!ok, `Conflict erroneously succeeded (${side_})`) + // d -> log('conflict', conflict) + conflict.op1 = type.normalize(conflict.op1) + conflict.op2 = type.normalize(conflict.op2) + result.push(assert.deepStrictEqual(conflict, ec)) + } + } catch (e) { + d(() => { + console.error('FAIL! Repro with:') + console.log( + `tryTransform(${JSON.stringify(op1_)}, ${JSON.stringify( + op2_ + )}, '${side_}')` + ) + return type.tryTransform(op1_, op2_, side_) + }) + throw e + } + } +} + +const xf = ({ + op1, + op2, + conflict, + conflictLeft, + conflictRight, + expect, + expectLeft, + expectRight +}) => { + if (expect !== undefined) { + expectLeft = expectRight = expect + } + if (conflict !== undefined) { + conflictLeft = conflictRight = conflict + } + + for (let [side, e, c] of [ + ['left', expectLeft, conflictLeft], + ['right', expectRight, conflictRight] + ]) { + checkConflict({ op1, op2, side, conflict: c, expect: e }) + + try { + const result = + c != null + ? type.transformNoConflict(op1, op2, side) + : transform(op1, op2, side) + assert.deepStrictEqual(result, e) + } catch (error) { + e = error + d(() => { + console.error('FAIL! Repro with:') + console.log( + `transform(${JSON.stringify(op1)}, ${JSON.stringify(op2)}, '${side}')` + ) + }) + // if c? then type.transformNoConflict op1, op2, side else transform op1, op2, side + throw e + } + } +} + +const diamond = ({ doc, op1, op2 }) => { + let doc1, doc12, doc2, doc21, op1_, op2_ + type.setDebug(false) + + try { + // Test that the diamond property holds + op1_ = transform(op1, op2, 'left') + op2_ = transform(op2, op1, 'right') + + doc1 = type.apply(doc, op1) + doc2 = type.apply(doc, op2) + + doc12 = type.apply(doc1, op2_) + doc21 = type.apply(doc2, op1_) + + return assert.deepStrictEqual(doc12, doc21) + } catch (e) { + log.quiet = false + log('\nOops! Diamond property does not hold. Given document', doc) + log('op1 ', op1, ' / op2', op2) + log('op1_', op1_, ' / op2_', op2_) + log('---- 1') + log('op1', op1, '->', doc1) + log('op2', op2_, '->', doc12) + log('---- 2') + log('op2', op2, '->', doc2) + log('op1', op1_, '->', doc21) + log('----------') + log(doc12, '!=', doc21) + throw e + } +} + +const path = (path, { op, expect }) => { + if (expect === undefined) { + expect = path.slice() + } + + const result = type.transformPosition(path, op) + assert.deepStrictEqual(result, expect) + + // Also check that path+X = expect+X + const path2 = path.concat('x') + const expect2 = expect != null ? expect.concat('x') : null + + const result2 = type.transformPosition(path2, op) + assert.deepStrictEqual(result2, expect2) +} + +describe('json1', () => { + before(() => { + type.registerSubtype(require('ot-simple')) + return type.setDebug(true) + }) + after(() => type.setDebug(false)) + + describe('checkOp', () => { + const pass = op => { + try { + return type.checkValidOp(op) + } catch (e) { + console.log(`FAIL! Repro with:\ncheckOp( ${JSON.stringify(op)} )`) + throw e + } + } + + const fail = op => { + try { + return assert.throws(() => type.checkValidOp(op)) + } catch (e) { + console.log(`FAIL! Repro with:\ncheckOp( ${JSON.stringify(op)} )`) + console.log('Should throw!') + throw e + } + } + + it('allows some simple valid ops', () => { + pass(null) + pass([{ i: [1, 2, 3] }]) + pass([{ r: {} }]) + pass([['x', { p: 0 }], ['y', { d: 0 }]]) + pass([[0, { p: 0 }], [10, { d: 0 }]]) + pass([['a', { p: 0 }], ['b', { d: 0 }], ['x', { p: 1 }], ['y', { d: 1 }]]) + pass([{ e: 'hi', et: 'simple' }]) + pass([{ es: ['hi'] }]) + pass([{ ena: 5 }]) + }) + + it('disallows invalid syntax', () => { + fail(undefined) + fail({}) + fail('hi') + fail(true) + fail(false) + fail(0) + fail(10) + fail([{}]) + fail([{ invalid: true }]) + fail([10, {}]) + fail([10, { invalid: true }]) + fail([10, 'hi']) + }) + + it('throws if there is any empty leaves', () => { + fail([]) + fail(['x']) + fail(['x', {}]) + fail(['x', []]) + fail([10]) + fail([10, {}]) + fail([10, []]) + }) + + it('ensures path components are non-zero integers or strings', () => { + fail([-1, { r: {} }]) + fail([0.5, { r: {} }]) + fail([true, { r: {} }]) + fail([false, { r: {} }]) + fail([null, { r: {} }]) + fail([undefined, { r: {} }]) + }) + + it('does not allow two pickups or two drops in a component', () => { + fail([{ p: 0, r: {} }]) + fail([{ p: 1, r: {} }]) + fail(['x', { p: 0, r: {} }]) + fail(['x', { p: 1, r: {} }]) + + fail([{ d: 0, i: 'hi' }]) + fail([{ d: 1, i: 'hi' }]) + fail([10, { d: 0, i: 'hi' }]) + fail([10, { d: 1, i: 'hi' }]) + }) + + it('throws if there are mismatched pickups / drops', () => { + fail([{ p: 0 }]) + fail([{ d: 0 }]) + fail(['x', { p: 0 }]) + fail([10, { p: 0 }]) + fail(['x', { d: 0 }]) + fail([10, { d: 0 }]) + }) + + it('throws if pick/drop indexes dont start at 0', () => { + fail([['x', { p: 1 }], ['y', { d: 1 }]]) + fail([[10, { p: 1 }], [20, { d: 1 }]]) + }) + + it('throws if a descent starts with an edit', () => + fail([10, [{ i: 'hi' }]])) + + it('throws if descents are out of order', () => { + fail(['x', ['b', { r: {} }], ['a', { r: {} }]]) + fail(['x', [10, { r: {} }], [5, { r: {} }]]) + fail(['x', ['a', { r: {} }], [5, { r: {} }]]) + fail(['x', ['a', { r: {} }], ['a', { r: {} }]]) + fail(['x', [10, { r: {} }], [10, { r: {} }]]) + }) + + it('throws if descents start with the same scalar', () => + fail(['x', ['a', { r: {} }], ['a', { e: {} }]])) + + it('throws if descents have two adjacent edits', () => { + fail([{ r: {} }, { p: 0 }]) + fail(['x', { r: {} }, { p: 0 }]) + fail(['x', { r: {} }, { p: 0 }, 'y', { r: {} }]) + }) + + it.skip('does not allow ops to overwrite their own inserted data', () => { + fail([{ i: { x: 5 } }, 'x', { i: 6 }]) + fail([{ i: ['hi'] }, 0, { i: 'omg' }]) + }) + + it.skip('does not allow immediate data directly parented in other immediate data', () => { + fail([{ i: {} }, 'x', { i: 5 }]) + fail([{ i: { x: 5 } }, 'x', 'y', { i: 6 }]) + fail([{ i: [] }, 0, { i: 5 }]) + }) + + it('does not allow the final item to be a single descent', () => + fail(['a', ['b', { r: {} }]])) // It should be ['a', 'b', r:{}] + + it('does not allow anything after the descents at the end', () => { + fail([[1, { r: {} }], [2, { r: {} }], 5]) + fail([[1, { r: {} }], [2, { r: {} }], 5, { r: {} }]) + fail([[1, { r: {} }], [2, { r: {} }], { r: {} }]) + }) + + it('allows removes inside removes', () => { + pass(['x', { r: true }, 'y', { r: true }]) + pass(['x', { r: {} }, 'y', { r: true }]) + pass([ + ['x', { r: true }, 'y', { p: 0 }, 'z', { r: true }], + ['y', { d: 0 }] + ]) + pass([['x', { r: {} }, 'y', { p: 0 }, 'z', { r: true }], ['y', { d: 0 }]]) + }) + + it('allows inserts inside inserts', () => { + pass([1, { i: {} }, 'x', { i: 10 }]) + pass([[0, 'x', { p: 0 }], [1, { i: {} }, 'x', { d: 0 }, 'y', { i: 10 }]]) + }) + + it.skip('fails if the operation drops items inside something it picked up', () => { + fail(['x', { r: true }, 1, { i: 'hi' }]) + fail(['x', { d: 0 }, 1, { p: 0 }]) + fail([{ r: true }, 1, { p: 0, d: 0 }]) + }) + + describe('edit', () => { + it('requires all edits to specify their type', () => { + fail([{ e: {} }]) + fail([5, { e: {} }]) + pass([{ e: {}, et: 'simple' }]) + }) + + it('allows edits to have null or false for the operation', () => { + // These aren't valid operations according to the simple type, but the + // type doesn't define a checkValidOp so we wouldn't be able to tell + // anyway. + pass([{ e: null, et: 'simple' }]) + pass([5, { e: null, et: 'simple' }]) + pass([{ e: false, et: 'simple' }]) + pass([5, { e: false, et: 'simple' }]) + }) + + it('does not allow an edit to use an unregistered type', () => { + fail([{ e: {}, et: 'an undefined type' }]) + fail([{ e: null, et: 'an undefined type' }]) + }) + + it('does not allow two edits in the same operation', () => { + fail([{ e: {}, et: 'simple', es: [1, 2, 3] }]) + fail([{ es: [], ena: 5 }]) + fail([{ e: {}, et: 'simple', ena: 5 }]) + }) + + it('fails if the type is missing', () => fail([{ et: 'missing', e: {} }])) + + it('does not allow anything inside an edited subtree') + + it.skip('does not allow an edit inside removed or picked up content', () => { + fail([{ r: true }, 1, { es: ['hi'] }]) + pass([1, { r: true }, 1, { es: ['hi'] }]) + fail(['x', { r: true }, 1, { es: ['hi'] }]) + pass([[1, { p: 0 }, 1, { es: ['hi'] }], [2, { d: 0 }]]) + fail([['x', { p: 0 }, 1, { es: ['hi'] }], ['y', { d: 0 }]]) + + // This is actually ok. + pass([0, { p: 0 }, ['a', { es: [], r: true }], ['x', { d: 0 }]]) + }) + + it.skip('does not allow you to drop inside something that was removed', () => { + // These insert into the next list item + pass([[1, { r: true }, 1, { d: 0 }], [2, { p: 0 }]]) + pass([1, { p: 0 }, 'x', { d: 0 }]) + + // But this is not ok. + fail(['x', { p: 0 }, 'a', { d: 0 }]) + }) + }) + }) + + describe('normalize', () => { + const n = (opIn, expect) => { + if (expect === undefined) { + expect = opIn + } + const op = type.normalize(opIn) + assert.deepStrictEqual(op, expect) + } + + it('does the right thing for noops', () => { + n(null) + n([], null) + }) + + it('normalizes some regular ops', () => { + n([{ i: 'hi' }]) + n([{ i: 'hi' }, 1, 2, 3], [{ i: 'hi' }]) + n([[1, 2, 3, { p: 0 }], [1, 2, 3, { d: 0 }]], [1, 2, 3, { p: 0, d: 0 }]) + n( + [[1, 2, 3, { p: 0 }], [1, 2, 30, { d: 0 }]], + [1, 2, [3, { p: 0 }], [30, { d: 0 }]] + ) + n( + [[1, 2, 30, { p: 0 }], [1, 2, 3, { d: 0 }]], + [1, 2, [3, { d: 0 }], [30, { p: 0 }]] + ) + }) + + it('will let you insert null', () => n([{ i: null }])) + + it('normalizes embedded ops when available', () => { + n([{ es: [0, 'hi'] }], [{ es: ['hi'] }]) + n([{ et: 'text-unicode', e: ['hi'] }], [{ es: ['hi'] }]) + n([{ et: 'text-unicode', e: [0, 'hi'] }], [{ es: ['hi'] }]) + n([{ et: 'simple', e: {} }]) + n([{ et: 'number', e: 5 }], [{ ena: 5 }]) + n([{ ena: 5 }]) + }) + + it.skip('normalizes embedded removes', () => { + n([1, { r: true }, 2, { r: true }], [1, { r: true }]) + n([{ r: true }, 2, { r: true }], [{ r: true }]) + }) + + it('throws if the type is missing', () => + // Not sure if this is the best behaviour but ... eh. + assert.throws(() => n([{ et: 'missing', e: {} }]))) + + it('corrects weird pick and drop ids', () => + n([['x', { p: 1 }], ['y', { d: 1 }]], [['x', { p: 0 }], ['y', { d: 0 }]])) + }) + + // ****** Apply ****** + + describe('apply', () => { + it('Can set properties', () => { + apply({ + doc: [], + op: [0, { i: 17 }], + expect: [17] + }) + + apply({ + doc: {}, + op: ['x', { i: 5 }], + expect: { x: 5 } + }) + }) + + it('can edit the root', () => { + apply({ + doc: { x: 5 }, + op: [{ r: true }], + expect: undefined + }) + + apply({ + doc: '', + op: [{ r: true }], + expect: undefined + }) + + apply({ + doc: 'hi', + op: [{ r: true, i: null }], + expect: null + }) + + apply({ + doc: 'hi', + op: [{ es: [2, ' there'] }], + expect: 'hi there' + }) + + assert.throws(() => type.apply(null, [{ i: 5 }])) + + apply({ + doc: undefined, + op: [{ i: 5 }], + expect: 5 + }) + + apply({ + doc: { x: 5 }, + op: [{ r: {}, i: [1, 2, 3] }], + expect: [1, 2, 3] + }) + }) + + // TODO: And an edit of the root. + + it('can move 1', () => + apply({ + doc: { x: 5 }, + op: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: { y: 5 } + })) + + it('can move 2', () => + apply({ + doc: [0, 1, 2], + op: [[1, { p: 0 }], [2, { d: 0 }]], + expect: [0, 2, 1] + })) + + it('can handle complex list index stuff', () => + apply({ + doc: [0, 1, 2, 3, 4, 5], + op: [[1, { r: {}, i: 11 }], [2, { r: {}, i: 12 }]], + expect: [0, 11, 12, 3, 4, 5] + })) + + it('correctly handles interspersed descent and edits', () => + apply({ + doc: { x: { y: { was: 'y' }, was: 'x' } }, + op: [['X', { d: 0 }, 'Y', { d: 1 }], ['x', { p: 0 }, 'y', { p: 1 }]], + expect: { X: { Y: { was: 'y' }, was: 'x' } } + })) + + it('can edit strings', () => + apply({ + doc: 'errd', + op: [{ es: [2, 'maghe'] }], + expect: 'ermagherd' + })) + + it('can edit numbers', () => + apply({ + doc: 5, + op: [{ ena: 10 }], + expect: 15 + })) + + it('can edit child numbers', () => + apply({ + doc: [20], + op: [0, { ena: -100 }], + expect: [-80] + })) + + it('can edit subdocuments using an embedded type', () => + apply({ + doc: { str: 'hai' }, + op: [{ e: { position: 2, text: 'wai' }, et: 'simple' }], + expect: { str: 'hawaii' } + })) + + it('applies edits after drops', () => + apply({ + doc: { x: 'yooo' }, + op: [['x', { p: 0 }], ['y', { d: 0, es: ['sup'] }]], + expect: { y: 'supyooo' } + })) + + it('throws when the op traverses missing items', () => { + assert.throws(() => type.apply([0, 'hi'], [1, { p: 0 }, 'x', { d: 0 }])) + assert.throws(() => type.apply({}, [{ p: 0 }, 'a', { d: 0 }])) + }) + + it('throws if the type is missing', () => + assert.throws(() => type.apply({}, [{ et: 'missing', e: {} }]))) + }) + + describe('apply path', () => { + it('does not modify path when op is unrelated', () => { + path(['a', 'b', 'c'], { op: null }) + path(['a', 'b', 'c'], { op: ['x', { i: 5 }] }) + path(['a', 'b', 'c'], { op: ['x', { r: true }] }) + path(['a', 'b', 'c'], { op: [['x', { p: 0 }], ['y', { d: 0 }]] }) + path([1, 2, 3], { op: [2, { i: 5 }] }) + path([1, 2, 3], { op: [1, 2, 4, { i: 5 }] }) + path([1], { op: [1, 2, { r: true }] }) + path(['x'], { op: ['x', 'y', { r: true }] }) + }) + + it('adjusts list indicies', () => { + path([2], { op: [1, { i: 5 }], expect: [3] }) + path([2], { op: [2, { i: 5 }], expect: [3] }) + path([2], { op: [1, { r: true }], expect: [1] }) + path([2], { op: [[1, { p: 0 }], [3, { d: 0 }]], expect: [1] }) + path([2], { op: [[1, { d: 0 }], [3, { p: 0 }]], expect: [3] }) + path([2], { op: [[2, { d: 0 }], [3, { p: 0 }]], expect: [3] }) + }) + + it('returns null when the object at the path was removed', () => { + path(['x'], { op: [{ r: true }], expect: null }) + path(['x'], { op: ['x', { r: true }], expect: null }) + path([1], { op: [{ r: true }], expect: null }) + path([1], { op: [1, { r: true }], expect: null }) + }) + + it('moves the path', () => { + path(['a', 'z'], { + op: [['a', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', 'z'] + }) + path(['a', 'b'], { + op: [['a', 'b', { p: 0 }], ['z', { d: 0 }]], + expect: ['z'] + }) + path(['a', 'b'], { op: [['a', 'b', 'c', { p: 0 }], ['z', { d: 0 }]] }) + path([1, 2], { op: [[1, { p: 0 }], [10, { d: 0 }]], expect: [10, 2] }) + path([1, 2], { op: [[1, 2, { p: 0 }], [10, { d: 0 }]], expect: [10] }) + path([1, 2], { op: [1, [1, { d: 0 }], [2, { p: 0 }]], expect: [1, 1] }) + path([1, 2], { op: [[1, 2, 3, { p: 0 }], [10, { d: 0 }]] }) + }) + + it('handles pick parent and move', () => + path(['a', 'b', 'c'], { + op: [['a', { r: true }, 'b', { p: 0 }], ['x', { d: 0 }]], + expect: ['x', 'c'] + })) + + it('adjusts indicies under a pick', () => + path(['a', 'b', 10], { + op: [['a', { p: 0 }, 'b', 1, { r: true }], ['x', { d: 0 }]], + expect: ['x', 'b', 9] + })) + + it.skip('gen ops', () => {}) + // This should do something like: + // - Generate a document + // - Generate op, a random operation + // - Generate a path to somewhere in the document and an edit we can do there -> op2 + // - Check that transform(op2, op) == op2 at transformPosition(path) or something like that. + + it('calls transformPosition with embedded string edits if available', () => { + // For embedded string operations (and other things that have + // transformPosition or transformPosition or whatever) we should call that. + path(['x', 'y', 'z', 1], { + op: ['x', 'y', 'z', { es: ['abc'] }], + expect: ['x', 'y', 'z', 4] + }) + path(['x', 'y', 'z', 1], { + op: ['x', 'y', 'z', { es: ['💃'] }], + expect: ['x', 'y', 'z', 2] + }) + path(['x', 'y', 'z'], { + op: ['x', 'y', 'z', { es: ['💃'] }], + expect: ['x', 'y', 'z'] + }) + }) + }) + + // ******* Compose ******* + + describe('compose', () => { + it('composes empty ops to nothing', () => + compose({ + op1: null, + op2: null, + expect: null + })) + + describe('op1 drop', () => { + it('vs remove', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: ['y', { r: true }], + expect: ['x', { r: true }] + })) + + it('vs remove parent', () => + compose({ + op1: [['x', { p: 0 }], ['y', 0, { d: 0 }]], + op2: ['y', { r: true }], + expect: [['x', { r: true }], ['y', { r: true }]] + })) + + it('vs remove child', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: ['y', 'a', { r: true }], + expect: [['x', { p: 0 }, 'a', { r: true }], ['y', { d: 0 }]] + })) + + it('vs remove and pick child', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: [['y', { r: true }, 'a', { p: 0 }], ['z', { d: 0 }]], + expect: [['x', { r: true }, 'a', { p: 0 }], ['z', { d: 0 }]] + })) + + it('vs pick', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + expect: [['x', { p: 0 }], ['z', { d: 0 }]] + })) + + it('is transformed by op2 picks', () => + compose({ + op1: [['x', { p: 0 }], ['y', 10, { d: 0 }]], + op2: ['y', 0, { r: true }], + expect: [['x', { p: 0 }], ['y', [0, { r: true }], [9, { d: 0 }]]] + })) + }) + + describe('op1 insert', () => { + it('vs remove', () => + compose({ + op1: ['x', { i: { a: 'hi' } }], + op2: ['x', { r: true }], + expect: null + })) + + it('vs remove parent', () => + compose({ + op1: ['x', 0, { i: { a: 'hi' } }], + op2: ['x', { r: true }], + expect: ['x', { r: true }] + })) + + it('vs remove child', () => + compose({ + op1: ['x', { i: { a: 'hi', b: 'woo' } }], + op2: ['x', 'a', { r: true }], + expect: ['x', { i: { b: 'woo' } }] + })) + + it('vs remove and pick child', () => + compose({ + op1: ['x', { i: { a: 'hi', b: 'woo' } }], + op2: [['x', { r: true }, 'a', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', { i: 'hi' }] + })) + + it('vs remove an embedded insert', () => + compose({ + op1: ['x', { i: {} }, 'y', { i: 'hi' }], + op2: ['x', 'y', { r: true }], + expect: ['x', { i: {} }] + })) + + it('vs remove from an embedded insert', () => + compose({ + op1: ['x', { i: {} }, 'y', { i: [1, 2, 3] }], + op2: ['x', 'y', 1, { r: true }], + expect: ['x', { i: {} }, 'y', { i: [1, 3] }] + })) + + it('picks the correct element of an embedded insert', () => + compose({ + op1: ['x', { i: ['a', 'b', 'c'] }, 1, { i: 'XX' }], + op2: [['x', 1, { p: 0 }], ['y', { d: 0 }]], + expect: [['x', { i: ['a', 'b', 'c'] }], ['y', { i: 'XX' }]] + })) + + it('picks the correct element of an embedded insert 2', () => + compose({ + op1: ['x', { i: ['a', 'b', 'c'] }, 1, { i: 'XX' }], + op2: [['x', 3, { p: 0 }], ['y', { d: 0 }]], // should grab 'c'. + expect: [['x', { i: ['a', 'b'] }, 1, { i: 'XX' }], ['y', { i: 'c' }]] + })) + + it('moves all children', () => + compose({ + op1: ['x', { i: {} }, 'y', { i: [1, 2, 3] }], + op2: [['x', { p: 0 }], ['z', { d: 0 }]], + expect: ['z', { i: {} }, 'y', { i: [1, 2, 3] }] + })) + + it('removes all children', () => + compose({ + op1: ['x', { i: {} }, 'y', { i: [1, 2, 3] }], + op2: ['x', { r: true }], + expect: null + })) + + it('removes all children when removed at the destination', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }, 0, { i: 'hi' }]], + op2: ['y', { r: true }], + expect: ['x', { r: true }] + })) + + it('vs op2 insert', () => + compose({ + // Inserts aren't folded together. + op1: [{ i: {} }], + op2: ['x', { i: 'hi' }], + expect: [{ i: {} }, 'x', { i: 'hi' }] + })) + + it('vs op2 string edit', () => + compose({ + op1: [{ i: 'hi' }], + op2: [{ es: [2, ' there'] }], + expect: [{ i: 'hi', es: [2, ' there'] }] + })) + + it('vs op2 number edit', () => + compose({ + op1: [{ i: 10 }], + op2: [{ ena: 20 }], + expect: [{ i: 10, ena: 20 }] + })) + }) + + describe('op1 edit', () => { + it('removes the edit if the edited object is deleted', () => + compose({ + op1: ['x', { es: ['hi'] }], + op2: ['x', { r: true }], + expect: ['x', { r: true }] + })) + + it('removes the edit in an embedded insert 1', () => + compose({ + op1: ['x', { i: '', es: ['hi'] }], + op2: ['x', { r: true }], + expect: null + })) + + it('removes the edit in an embedded insert 2', () => + compose({ + op1: ['x', { i: [''] }, 0, { es: ['hi'] }], + op2: ['x', 0, { r: true }], + expect: ['x', { i: [] }] + })) + + it('composes string edits', () => + compose({ + op1: [{ es: ['hi'] }], + op2: [{ es: [2, ' there'] }], + expect: [{ es: ['hi there'] }] + })) + + it('composes number edits', () => + compose({ + op1: [{ ena: 10 }], + op2: [{ ena: -8 }], + expect: [{ ena: 2 }] + })) + + it('transforms and composes edits', () => + compose({ + op1: ['x', { es: ['hi'] }], + op2: [['x', { p: 0 }], ['y', { d: 0, es: [2, ' there'] }]], + expect: [['x', { p: 0 }], ['y', { d: 0, es: ['hi there'] }]] + })) + + it('preserves inserts with edits', () => + compose({ + op1: ['x', { i: 'hi' }], + op2: [['x', { p: 0 }], ['y', { d: 0, es: [' there'] }]], + expect: ['y', { i: 'hi', es: [' there'] }] + })) + + it('allows a different edit in the same location', () => + compose({ + op1: ['x', { es: ['hi'] }], + op2: ['x', { r: true, i: 'yo', es: [2, ' there'] }], + expect: ['x', { r: true, i: 'yo', es: [2, ' there'] }] + })) + + it('throws if the type is missing', () => + assert.throws(() => + type.compose( + [{ et: 'missing', e: {} }], + [{ et: 'missing', e: {} }] + ) + )) + }) + + describe('op2 pick', () => + it('gets untransformed by op1 drops', () => ({ + op1: [5, { i: 'hi' }], + op2: [6, { r: true }], + expect: [5, { r: true, i: 'hi' }] + }))) + + describe('op1 insert containing a drop', () => + it('vs pick at insert', () => + compose({ + op1: [['x', { p: 0 }], ['y', { i: {} }, 'x', { d: 0 }]], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + expect: [['x', { p: 0 }], ['z', { i: {} }, 'x', { d: 0 }]] + }))) + + describe('fuzzer tests', () => + it('complicated transform of indicies', () => + compose({ + op1: [0, { p: 0 }, 'x', 2, { d: 0 }], + op2: [0, 'x', 0, { r: true }], + expect: [[0, { p: 0 }, 'x', 1, { d: 0 }], [1, 'x', 0, { r: true }]] + }))) + + describe('setnull interaction', () => { + // Currently failing. + it('reorders items inside a setnull region', () => + compose({ + op1: [{ i: [] }, [0, { i: 'a' }], [1, { i: 'b' }]], + op2: [[0, { p: 0 }], [1, { d: 0 }]], + expect: [{ i: [] }, [0, { i: 'b' }], [1, { i: 'a' }]] + })) + + it('lets a setnull child be moved', () => + compose({ + op1: ['list', { i: [] }, 0, { i: 'hi' }], + op2: [['list', 0, { p: 0 }], ['z', { d: 0 }]], + expect: [['list', { i: [] }], ['z', { i: 'hi' }]] + })) + + it('lets a setnull child get modified', () => + compose({ + op1: [{ i: [] }, 0, { i: ['a'] }], + op2: [0, 0, { r: 'a', i: 'b' }], + expect: [{ i: [] }, 0, { i: [] }, 0, { i: 'b' }] + })) + }) + //expect: [{i:[]}, 0, {i:['b']}] # Maybe better?? + + describe('regression', () => { + it('skips op2 drops when calculating op1 drop index simple', () => + compose({ + op1: [[0, { p: 0 }], [2, { d: 0 }]], + op2: [[0, { p: 0 }], [1, { d: 0 }]], + expect: [[0, { p: 1 }], [1, { p: 0, d: 0 }], [2, { d: 1 }]] + })) + + it('skips op2 drops when calculating op1 drop index complex', () => + compose({ + op1: [[0, { p: 0, d: 1 }], [1, { p: 1 }], [2, { d: 0 }]], + op2: [[0, { p: 0 }], [1, { d: 0 }]], + // expect: [[0, {p:1}], [1, {d:0, p:0}], [2, d:1]] + expect: [[0, { p: 1 }], [1, { p: 0, d: 0 }], [2, { d: 1 }]] + })) + + it('3', () => + compose({ + op1: [{ i: [null, []] }, 0, { i: '' }], + op2: [1, { p: 0 }, 0, { d: 0 }], + // ... it'd be way more consistent to drop the null separately rather than merging it?? + expect: [{ i: [[]] }, [0, { i: '' }], [1, 0, { i: null }]] + })) + + it('4', () => + compose({ + // This one triggered a bug in cursor! + op1: [0, [0, ['a', { r: true }], ['b', { d: 0 }]], [2, { p: 0 }]], + op2: [0, 0, 'c', { i: 'd' }], + expect: [ + 0, + [0, ['a', { r: true }], ['b', { d: 0 }], ['c', { i: 'd' }]], + [2, { p: 0 }] + ] + })) + }) + }) + + // *** Old stuff + describe('old compose', () => { + it('gloms together unrelated edits', () => { + compose({ + op1: [['a', { p: 0 }], ['b', { d: 0 }]], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: [ + ['a', { p: 0 }], + ['b', { d: 0 }], + ['x', { p: 1 }], + ['y', { d: 1 }] + ] + }) + + compose({ + op1: [2, { i: 'hi' }], + op2: [0, 'x', { r: true }], + expect: [[0, 'x', { r: true }], [2, { i: 'hi' }]] + }) + }) + + it('translates drops in objects', () => + compose({ + op1: ['x', ['a', { p: 0 }], ['b', { d: 0 }]], // x.a -> x.b + op2: [['x', { p: 0 }], ['y', { d: 0 }]], // x -> y + expect: [['x', { p: 0 }, 'a', { p: 1 }], ['y', { d: 0 }, 'b', { d: 1 }]] + })) // x.a -> y.b, x -> y + + it('untranslates picks in objects', () => + compose({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], // x -> y + op2: [['y', 'a', { p: 0 }], ['z', { d: 0 }]], // y.a -> z + expect: [ + ['x', { p: 0 }, 'a', { p: 1 }], + ['y', { d: 0 }], + ['z', { d: 1 }] + ] + })) // x.a -> z, x -> y + + it('insert gets carried wholesale', () => + compose({ + op1: ['x', { i: 'hi there' }], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], // x -> y + expect: ['y', { i: 'hi there' }] + })) + + it('insert gets edited by the op', () => + compose({ + op1: ['x', { i: { a: 1, b: 2, c: 3 } }], + op2: [['x', 'a', { p: 0 }], ['y', { d: 0 }]], + expect: [['x', { i: { b: 2, c: 3 } }], ['y', { i: 1 }]] + })) + + it('does not merge mutual inserts', () => + compose({ + op1: [{ i: {} }], + op2: ['x', { i: 'hi' }], + expect: [{ i: {} }, 'x', { i: 'hi' }] + })) + }) + + // TODO: List nonsense. + + // TODO: Edits. + + // ****** Transform ****** + + describe('transform', () => { + describe('op1 pick', () => { + it('vs delete', () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: ['x', { r: true }], + expect: null + })) + it('vs delete parent', () => + xf({ + op1: [['x', 'a', { p: 0 }], ['y', { d: 0 }]], + op2: ['x', { r: true }], + expect: null + })) + it('vs delete parent 2', () => + xf({ + op1: ['x', ['a', { p: 0 }], ['b', { d: 0 }]], + op2: ['x', { r: true }], + expect: null + })) + + it('vs pick', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + // Consider adding a conflict for this case. + expectLeft: [['y', { p: 0 }], ['z', { d: 0 }]], + expectRight: null + })) + it('vs pick parent', () => + xf({ + op1: [['x', 'a', { p: 0 }], ['z', { d: 0 }]], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: [['y', 'a', { p: 0 }], ['z', { d: 0 }]] + })) + + it('vs pick and pick child', () => + xf({ + // regression + op1: [ + // a -> xa, a.c -> xc + ['a', { p: 0 }, 'c', { p: 1 }], + ['xa', { d: 0 }], + ['xc', { d: 1 }] + ], + op2: [['a', { p: 0 }], ['b', { d: 0 }]], // a -> b + expectLeft: [ + ['b', { p: 0 }, 'c', { p: 1 }], + ['xa', { d: 0 }], + ['xc', { d: 1 }] + ], + expectRight: [['b', 'c', { p: 0 }], ['xc', { d: 0 }]] + })) + + it('vs edit', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: ['x', { es: ['hi'] }], + expect: [['x', { p: 0 }], ['z', { d: 0 }]] + })) + + it('vs delete, drop', () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: [['a', { p: 0 }], ['x', { r: 0, d: 0 }]], + expect: null + })) + + it('vs delete, insert', () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: ['x', { r: 0, i: 5 }], + expect: null + })) + + it( + 'vs pick, drop to self', + () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: null + }), + + () => + xf({ + op1: [['a', 1, { p: 0 }], ['y', { d: 0 }]], + op2: [['a', 1, { p: 0 }], ['y', { d: 0 }]], + expect: null + }) + ) + + it('vs pick, drop', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], // x->z + op2: [['a', { p: 0 }], ['x', { p: 1, d: 0 }], ['y', { d: 1 }]], // a->x, x->y + expectLeft: [['y', { p: 0 }], ['z', { d: 0 }]], + expectRight: null + })) + + it('vs pick, insert', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: [['x', { p: 0, i: 5 }], ['y', { d: 0 }]], + expectLeft: [['y', { p: 0 }], ['z', { d: 0 }]], + expectRight: null + })) + + it('vs pick, edit', () => ({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: [['x', { es: ['hi'], p: 0 }], ['y', { d: 0 }]], + expectLeft: [['y', { p: 0 }], ['z', { d: 0 }]], + expectRight: null + })) + }) + + describe('op1 delete', () => { + it('vs delete', () => + xf({ + op1: ['x', { r: true }], + op2: ['x', { r: true }], + expect: null + })) + it('vs delete parent', () => + xf({ + op1: ['x', 'a', { r: true }], + op2: ['x', { r: true }], + expect: null + })) + + it('vs pick', () => + xf({ + op1: ['x', { r: true }], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', { r: true }] + })) + it('vs pick parent', () => + xf({ + op1: ['x', 'a', { r: true }], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', 'a', { r: true }] + })) + + it('vs pick and drop', () => + xf({ + op1: ['x', { r: true }], + op2: [['a', { p: 0 }], ['x', { d: 0, p: 1 }], ['z', { d: 1 }]], + expect: ['z', { r: true }] + })) + + it('vs edit', () => + xf({ + op1: ['x', { r: true }], + op2: ['x', { es: ['hi'] }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: ['x', { r: true }] + })) + + it('vs move and insert', () => + xf({ + op1: ['a', 1, { r: true }], + op2: [['a', { p: 0 }], ['b', { d: 0 }, [0, { i: 5 }], [1, { i: 5 }]]], + expect: ['b', 3, { r: true }] + })) + + return describe('vs pick child', function() { + it('move in', () => + xf({ + op1: ['x', { r: true }], + op2: [['a', { p: 0 }], ['x', 'y', { d: 0 }]], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: ['x', { r: true }, 'y', { r: true }] + })) // Also ok if its just x, r:true + + it('move across', () => + xf({ + op1: ['x', { r: true }], // delete doc.x + op2: ['x', ['y', { p: 0 }], ['z', { d: 0 }]], + expect: ['x', { r: true }] + })) + + it('move out', () => + xf({ + op1: ['x', { r: true }], + op2: [['x', 'y', { p: 0 }], ['y', { d: 0 }]], // move doc.x.y -> doc.y + expect: [['x', { r: true }], ['y', { r: true }]] + })) // delete doc.x and doc.y + + it('multiple out', () => + xf({ + op1: ['x', { r: true }], + op2: [ + ['x', 'y', { p: 0 }, 'z', { p: 1 }], + ['y', { d: 0 }], + ['z', { d: 1 }] + ], + expect: [['x', { r: true }], ['y', { r: true }], ['z', { r: true }]] + })) + + it('chain out', () => + xf({ + op1: ['x', { r: true }], + op2: [ + ['x', 'y', { p: 0 }], + ['y', { p: 1 }], + ['z', { d: 0 }, 'a', { d: 1 }] + ], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: [['y', { p: 0 }], ['z', 'a', { d: 0 }]] + }, // cMv(['y'], ['z', 'a']) + expect: [['x', { r: true }], ['z', { r: true }, 'a', { r: true }]] + })) + + it('mess', () => + xf({ + // yeesh + op1: [['x', { r: true }, 'y', 'z', { p: 0 }], ['z', { d: 0 }]], + op2: [['x', 'y', { p: 0 }], ['y', { d: 0 }]], + expect: [ + ['x', { r: true }], + ['y', { r: true }, 'z', { p: 0 }], + ['z', { d: 0 }] + ] + })) + }) + }) + + describe('op1 drop', () => { + it('vs delete parent', () => + xf({ + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]], + op2: ['y', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: ['x', { r: true }] + })) + + it('vs a cancelled parent', () => + xf({ + // This is actually a really complicated case. + op1: [ + ['x', 'y', { p: 0 }], + ['y', { p: 1 }], + ['z', { d: 0 }, 'a', { d: 1 }] + ], + op2: ['x', { r: true }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [['y', { p: 0 }], ['z', 'a', { d: 0 }]] + }, // c1: cMv(['y'], ['z', 'a']) + expect: ['y', { r: true }] + })) + + it('vs pick parent', () => + xf({ + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + expect: [['x', { p: 0 }], ['z', 'a', { d: 0 }]] + })) + + it('vs drop', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + conflict: { type: DROP_COLLISION }, + expectLeft: [['x', { p: 0 }], ['z', { r: true, d: 0 }]], + expectRight: ['x', { r: true }] + })) + + it('vs drop (list)', () => + xf({ + op1: [[0, { p: 0 }], [4, { d: 0 }]], + op2: [[5, { d: 0 }], [10, { p: 0 }]], + expectLeft: [[0, { p: 0 }], [4, { d: 0 }]], + expectRight: [[0, { p: 0 }], [5, { d: 0 }]] + })) + + it('vs drop (chained)', () => + xf({ + op1: [ + ['a', { p: 1 }], + ['x', { p: 0 }], + ['z', { d: 0 }, 'a', { d: 1 }] + ], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + conflict: { + type: DROP_COLLISION, + op1: [['x', { p: 0 }], ['z', { d: 0 }]] + }, //cMv(['x'], ['z']) + expectLeft: [ + ['a', { p: 0 }], + ['x', { p: 1 }], + ['z', { r: true, d: 1 }, 'a', { d: 0 }] + ], + expectRight: [['a', { r: true }], ['x', { r: true }]] + })) + + it('vs insert', () => + xf({ + op1: [['x', { p: 0 }], ['z', { d: 0 }]], + op2: ['z', { i: 5 }], + conflict: { type: DROP_COLLISION }, + expectLeft: [['x', { p: 0 }], ['z', { r: true, d: 0 }]], + expectRight: ['x', { r: true }] + })) + + it('vs pick (a->b->c vs b->x)', () => + xf({ + op1: [['a', { p: 0 }], ['b', { p: 1, d: 0 }], ['c', { d: 1 }]], + op2: [['b', { p: 0 }], ['x', { d: 0 }]], + expectLeft: [ + ['a', { p: 0 }], + ['b', { d: 0 }], + ['c', { d: 1 }], + ['x', { p: 1 }] + ], + expectRight: [['a', { p: 0 }], ['b', { d: 0 }]] + })) + + return describe.skip('vs move inside me', function() { + // Note: This is *not* blackholeing! The edits are totally fine; we + // just need one edit to win. + // The current behaviour just nukes both. + it('in objects', () => + xf({ + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]], + op2: [['x', 'a', { d: 0 }], ['y', { p: 0 }]], + expectLeft: [ + ['x', { p: 0 }, 'a', { p: 1 }], + ['y', { d: 1 }, 'x', { d: 0 }] + ], + expectRight: null + })) + + it('in lists', () => + xf({ + op1: [0, { p: 0 }, 'x', { d: 0 }], + op2: [[0, 'y', { d: 0 }], [1, { p: 0 }]], + expectLeft: [0, { p: 0, d: 1 }, ['x', { d: 0 }], ['y', { p: 1 }]], + expectRight: null + })) + + it('multiple', () => + xf({ + // a->x.a, b->x.b + op1: [ + ['a', { p: 0 }], + ['b', { p: 1 }], + ['x', 'a', { d: 0 }, 'b', { d: 1 }] + ], + op2: [['a', 'x', { d: 0 }], ['x', { p: 0 }]], // x->a.x + expectLeft: [ + ['a', { p: 0 }, 'x', { p: 1 }], + ['b', { p: 2 }], + ['x', { d: 1 }, ['a', { d: 0 }], ['b', { d: 2 }]] + ], + expectRight: null + })) + }) + }) + + describe('op1 insert', () => { + it('vs delete parent', () => + xf({ + op1: ['y', 'a', { i: 5 }], + op2: ['y', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('vs pick parent', () => + xf({ + op1: ['y', 'a', { i: 5 }], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + expect: ['z', 'a', { i: 5 }] + })) + + it('vs drop', () => + xf({ + op1: ['z', { i: 5 }], + op2: [['y', { p: 0 }], ['z', { d: 0 }]], + conflict: { type: DROP_COLLISION }, + expectLeft: ['z', { r: true, i: 5 }], + expectRight: null + })) + + it('vs insert', () => + xf({ + op1: ['z', { i: 5 }], + op2: ['z', { i: 10 }], + conflict: { type: DROP_COLLISION }, + expectLeft: ['z', { r: true, i: 5 }], + expectRight: null + })) + + it('vs insert at list position', () => + xf({ + op1: [5, { i: 'hi' }], + op2: [5, { i: 'there' }], + expectLeft: [5, { i: 'hi' }], + expectRight: [6, { i: 'hi' }] + })) + + it('vs identical insert', () => + xf({ + op1: ['z', { i: 5 }], + op2: ['z', { i: 5 }], + expect: null + })) + + // This is the new setNull for setting up schemas + it('vs embedded inserts', function() { + xf({ + op1: ['x', { i: {} }], + op2: ['x', { i: {} }, 'y', { i: 5 }], + expect: null + }) + + xf({ + op1: ['x', { i: {} }, 'y', { i: 5 }], + op2: ['x', { i: {} }], + expect: ['x', 'y', { i: 5 }] + }) + + xf({ + op1: ['x', { i: {} }, 'y', { i: 5 }], + op2: ['x', { i: {} }, 'y', { i: 5 }], + expect: null + }) + + return xf({ + op1: ['x', { i: {} }, 'y', { i: 5 }], + op2: ['x', { i: {} }, 'y', { i: 6 }], + conflict: { + type: DROP_COLLISION, + op1: ['x', 'y', { i: 5 }], + op2: ['x', 'y', { i: 6 }] + }, + expectLeft: ['x', 'y', { r: true, i: 5 }], + expectRight: null + }) + }) + + it('with embedded edits', () => + xf({ + op1: [{ i: '', es: ['aaa'] }], + op2: [{ i: '', es: ['bbb'] }], + expectLeft: [{ es: ['aaa'] }], + expectRight: [{ es: [3, 'aaa'] }] + })) + }) + + describe('op1 edit', () => { + it('vs delete', () => + xf({ + op1: ['x', { es: ['hi'] }], + op2: ['x', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('vs delete parent', () => + xf({ + op1: ['x', 'y', { es: ['hi'] }], + op2: ['x', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('vs pick', () => + xf({ + op1: ['x', { es: ['hi'] }], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', { es: ['hi'] }] + })) + + it('vs edit string', () => + xf({ + op1: ['x', { es: ['ab'] }], + op2: ['x', { es: ['cd'] }], + expectLeft: ['x', { es: ['ab'] }], + expectRight: ['x', { es: [2, 'ab'] }] + })) + + it('vs edit number', () => + xf({ + op1: [{ ena: 5 }], + op2: [{ ena: 100 }], + expect: [{ ena: 5 }] + })) + + it('throws if edit types arent compatible', () => + assert.throws(() => type.transform([{ es: [] }], [{ ena: 5 }], 'left'))) + + it('vs move and edit', () => + xf({ + op1: ['x', { es: [1, 'ab'] }], + op2: [['x', { p: 0 }], ['y', { d: 0, es: [{ d: 1 }, 'cd'] }]], + expectLeft: ['y', { es: ['ab'] }], + expectRight: ['y', { es: [2, 'ab'] }] + })) + + it('throws if the type is missing', () => + assert.throws(() => + type.transform( + [{ et: 'missing', e: {} }], + [{ et: 'missing', e: {} }], + 'left' + ) + )) + }) + + describe('op2 cancel move', () => { + it('and insert', () => + xf({ + op1: ['x', { r: true }], + op2: [['x', 'a', { p: 0 }], ['y', { d: 0 }, 'b', { i: 5 }]], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: ['y', 'b', { i: 5 }] + }, + expect: [['x', { r: true }], ['y', { r: true }, 'b', { r: true }]] + })) + + it('and another move (rm x vs x.a -> y, q -> y.b)', () => + xf({ + op1: ['x', { r: true }], + op2: [ + ['q', { p: 1 }], + ['x', 'a', { p: 0 }], + ['y', { d: 0 }, 'b', { d: 1 }] + ], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: [['q', { p: 0 }], ['y', 'b', { d: 0 }]] + }, + expect: [['x', { r: true }], ['y', { r: true }, 'b', { r: true }]] + })) + }) + + describe('op2 list move an op1 drop', () => { + it('vs op1 remove', () => + xf({ + op1: [[0, { r: true }, 'a', { i: 'hi' }], [5, { r: true }]], + op2: [[1, { p: 0 }], [4, { d: 0 }]], + expect: [[0, { r: true }], [3, 'a', { i: 'hi' }], [5, { r: true }]] + })) + + it('vs op1 remove 2', () => + xf({ + op1: [ + [0, { r: true }, 'a', { i: 'hi' }], + [1, { r: true }], + [2, { r: true }] + ], + op2: [[3, { p: 0 }], [4, { d: 0 }]], + expect: [ + [0, { r: true }], + [1, { r: true }, 'a', { i: 'hi' }], + [2, { r: true }] + ] + })) + + it('vs op1 insert before', () => + xf({ + op1: [[0, { i: 'a' }], [1, { i: 'b' }], [2, 'a', { i: 'hi' }]], + op2: [[0, { p: 0 }], [1, { d: 0 }]], + expect: [[0, { i: 'a' }], [1, { i: 'b' }], [3, 'a', { i: 'hi' }]] + })) + + it('vs op1 insert before and replace', () => + xf({ + op1: [[0, { i: 'xx' }, 'a', { r: true }], [1, 'a', { i: 'hi' }]], + op2: [[0, { p: 0 }], [3, { d: 0 }]], + expect: [ + [0, { i: 'xx' }], + [3, 'a', { r: true }], + [4, 'a', { i: 'hi' }] + ] + })) + }) + + describe('list', () => + describe('drop', () => { + it('transforms by p1 drops', () => + xf({ + op1: [[5, { i: 5 }], [10, { i: 10 }]], + op2: [9, { i: 9 }], + expectLeft: [[5, { i: 5 }], [10, { i: 10 }]], + expectRight: [[5, { i: 5 }], [11, { i: 10 }]] + })) + + it('transforms by p1 picks') + it('transforms by p2 picks') + it('transforms by p2 drops') + })) + }) + + describe('conflicts', () => { + describe('drop into remove / rm unexpected', () => { + // xfConflict does both xf(op1, op2, left) and xf(op2, op1, right), and + // uses invConflict. So this also tests RM_UNEXPECTED_CONTENT with each + // test case. + it('errors if you insert', () => + xf({ + op1: ['a', 'b', { i: 5 }], + op2: ['a', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('errors if you drop', () => + xf({ + op1: [['a', { p: 0 }], ['x', 'b', { d: 0 }]], + op2: ['x', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: ['a', { r: true }] + })) + + it('errors if you rm then insert in a child', () => + xf({ + op1: ['a', 'b', { r: true, i: 5 }], + op2: ['a', { r: true }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', 'b', { i: 5 }] + }, + expect: null + })) + + it('errors if the object is replaced', () => + xf({ + op1: ['a', 'b', { i: 5 }], + op2: ['a', { r: true, i: 10 }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: ['a', { r: true }] + }, + expect: null + })) + + it('handles a delete of the source parent by op2', () => + xf({ + op1: [['a', { p: 0 }], ['b', 'b', { d: 0 }]], + op2: [['a', { p: 0 }], ['b', { r: true }, 'c', { d: 0 }]], + conflictLeft: { + type: RM_UNEXPECTED_CONTENT, + op2: ['b', { r: true }] + }, + expectLeft: ['b', 'c', { r: true }], + expectRight: null + })) + + it.skip('returns symmetric errors when both ops delete the other', () => + xf({ + // The problem here is that there's two conflicts we want to return. + // Which one should be returned first? It'd be nice for the order of + // conflict returning to be symmetric - that is, if we know multiple + // conflicts happen, order them based on left/right. But I haven't done + // that, so we get different conflicts out of this in a first pass. + op1: [['x', { r: true }], ['y', 'a', { i: {} }]], + op2: [['x', 'a', { i: {} }], ['y', { r: true }]], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: ['x', { r: true }] + })) + }) + + describe('overlapping drop', () => { + it('errors if two ops insert different content into the same place in an object', () => + xf({ + op1: ['x', { i: 'hi' }], + op2: ['x', { i: 'yo' }], + conflict: { type: DROP_COLLISION }, + expectLeft: ['x', { r: true, i: 'hi' }], + expectRight: null + })) + + it('does not conflict if inserts are identical', () => + xf({ + op1: ['x', { i: 'hi' }], + op2: ['x', { i: 'hi' }], + expectLeft: null, + expectRight: null + })) + + it('does not conflict if the two operations make identical moves', () => + xf({ + op1: [['a', { p: 0 }], ['x', { d: 0 }]], + op2: [['a', { p: 0 }], ['x', { d: 0 }]], + expect: null + })) // ??? Also ok for left: ['x', p:0, d:0] + + it('does not conflict if inserts are into a list', () => + xf({ + op1: [1, { i: 'hi' }], + op2: [1, { i: 'yo' }], + expectLeft: [1, { i: 'hi' }], + expectRight: [2, { i: 'hi' }] + })) + + it('errors if the inserts are at the root', () => + xf({ + op1: [{ i: 1 }], + op2: [{ i: 2 }], + conflict: { type: DROP_COLLISION }, + expectLeft: [{ r: true, i: 1 }], + expectRight: null + })) + + it('errors with insert vs drop', () => + xf({ + op1: ['x', { i: 'hi' }], + op2: [['a', { p: 0 }], ['x', { d: 0 }]], + // ???? + conflict: { type: DROP_COLLISION }, + expectLeft: ['x', { r: true, i: 'hi' }], + expectRight: null + })) + + it('errors with drop vs insert', () => + xf({ + op1: [['a', { p: 0 }], ['x', { d: 0 }]], + op2: ['x', { i: 'hi' }], + conflict: { type: DROP_COLLISION }, + expectLeft: [['a', { p: 0 }], ['x', { r: true, d: 0 }]], + expectRight: ['a', { r: true }] + })) + + it('errors with drop vs drop', () => + xf({ + op1: [['a', { p: 0 }], ['x', { d: 0 }]], + op2: [['b', { p: 0 }], ['x', { d: 0 }]], + conflict: { type: DROP_COLLISION }, + expectLeft: [['a', { p: 0 }], ['x', { r: true, d: 0 }]], + expectRight: ['a', { r: true }] + })) + + it('errors if the two sides insert in the vacuum', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0 }], ['c', { i: 5 }]], + op2: [['a', { p: 0 }], ['b', { i: 6 }], ['c', { d: 0 }]], + conflictLeft: { + type: DROP_COLLISION, + op1: [['a', { p: 0 }], ['b', { d: 0 }]], + op2: ['b', { i: 6 }] + }, + expectLeft: [['b', { r: true, d: 0 }], ['c', { p: 0, i: 5 }]], + conflictRight: { + type: DROP_COLLISION, + op1: ['c', { i: 5 }], + op2: [['a', { p: 0 }], ['c', { d: 0 }]] + }, + expectRight: null + })) + }) + + describe('discarded edit', () => { + it('edit removed directly', () => + xf({ + op1: ['a', { es: [] }], + op2: ['a', { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('edit inside new content throws RM_UNEXPECTED_CONTENT', () => + xf({ + op1: ['a', 'b', { i: 'hi', es: [] }], + op2: ['a', { r: true }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', 'b', { i: 'hi' }] + }, + expect: null + })) + }) + + describe('blackhole', () => { + it('detects and errors', () => + xf({ + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]], + op2: [['x', 'a', { d: 0 }], ['y', { p: 0 }]], + conflict: { type: BLACKHOLE }, + expect: ['x', { r: true }, 'a', { r: true }] + })) // Also equivalent: ['x', r:true] + + it('blackhole logic does not apply when op2 removes parent', () => + xf({ + // TODO: Although you wouldn't know it, since this result is very similar. + op1: [['x', { p: 0 }], ['y', 'xx', 'a', { d: 0 }]], + op2: [['x', 'a', { d: 0 }], ['y', { p: 0 }, 'xx', { r: true }]], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: ['y', 'xx', { r: true }] + }, + expect: ['x', { r: true }, 'a', { r: true }] + })) // Also ok: ['x', r:true] + + it('blackhole logic still applies when op2 inserts', () => + xf({ + op1: [['x', { p: 0 }], ['y', 'a', { i: {} }, 'b', { d: 0 }]], + op2: [['x', 'a', { i: {} }, 'b', { d: 0 }], ['y', { p: 0 }]], + conflict: { + type: BLACKHOLE, + op1: [['x', { p: 0 }], ['y', 'a', 'b', { d: 0 }]], + op2: [['x', 'a', 'b', { d: 0 }], ['y', { p: 0 }]] + }, + expect: ['x', { r: true }, 'a', { r: true }, 'b', { r: true }] + })) + + it('blackholes items in lists correctly', () => + xf({ + op1: [1, { p: 0 }, 'a', { d: 0 }], + op2: [[1, 'b', { d: 0 }], [2, { p: 0 }]], + conflict: { type: BLACKHOLE }, + expect: [1, { r: true }, 'b', { r: true }] + })) + + it('blackholes items despite scrambled pick and drop slots', () => + xf({ + op1: [['a', { p: 1, d: 1 }], ['x', { p: 0 }], ['y', 'a', { d: 0 }]], + op2: [['x', 'a', { d: 0 }], ['y', { p: 0 }]], + conflict: { + type: BLACKHOLE, + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]] + }, + expect: [['a', { p: 0, d: 0 }], ['x', { r: true }, 'a', { r: true }]] + })) + + it('handles chained blackholes', () => + xf({ + op1: [ + ['a', { p: 0 }], // a->b.b, c->d.d + ['b', 'b', { d: 0 }], + ['c', { p: 1 }], + ['d', 'd', { d: 1 }] + ], + op2: [ + ['a', 'a', { d: 1 }], // b->c.c, d->a.a + ['b', { p: 0 }], + ['c', 'c', { d: 0 }], + ['d', { p: 1 }] + ], + conflict: { type: BLACKHOLE }, + // c1: cMv(['a'], ['b', 'b']) + // c2: cMv(['b'], ['c', 'c']) + expect: [ + ['a', { r: true }, 'a', { r: true }], + ['c', { r: true }, 'c', { r: true }] + ] + })) + + it('creates conflict return values with valid slot ids', () => + xf({ + op1: [ + ['a', { p: 0 }], + ['b', { d: 0 }], + ['x', { p: 1 }], + ['y', 'a', { d: 1 }] + ], + op2: [['x', 'a', { d: 0 }], ['y', { p: 0 }]], + conflict: { + type: BLACKHOLE, + op1: [['x', { p: 0 }], ['y', 'a', { d: 0 }]] + }, + expect: [ + ['a', { p: 0 }], + ['b', { d: 0 }], + ['x', { r: true }, 'a', { r: true }] + ] + })) + }) + }) + + describe('transform-old', () => { + it('foo', () => + xf({ + op1: [ + ['x', ['a', { p: 0 }], ['b', { d: 0 }]], + ['y', ['a', { p: 1 }], ['b', { d: 1 }]] + ], + op2: ['x', { r: true }], + expect: ['y', ['a', { p: 0 }], ['b', { d: 0 }]] + })) + + // it 'hard', -> + // op1: ['x', [1, r:true], [2, r:true, es:['hi']]] # Edit at index 4 originally. + // # move the edited string to .y[4] which + // op2: [['x', 4, p:0], ['y', [2, r:true], [4, d:0]]] + // expect: + + describe('object edits', () => + it('can reparent with some extra junk', () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0 }]], + op2: [ + ['_a', { d: 1 }], + ['_x', { d: 0 }], + ['x', { p: 0 }, 'a', { p: 1 }] + ], + expectLeft: [['_x', { p: 0 }], ['y', { d: 0 }]], + expectRight: null + }))) // the object was moved fair and square. + + describe('deletes', () => { + it.skip('delete parent of a move', () => + xf({ + // The current logic of transform actually just burns everything (in a + // consistant way of course). I'm not sure if this is better or worse - + // basically we'd be saying that if a move could end up in one of two places, + // put it in the place where it won't be killed forever. But that introduces new + // complexity, so I'm going to skip this for now. + + // x.a -> a, delete x + op1: [['x', { r: true }, 'a', { p: 0 }], ['z', { d: 0 }]], + // x.a -> x.b. + op2: ['x', ['a', { p: 0 }], ['b', { d: 0 }]], + expect: [['x', { r: true }, 'b', { p: 0 }], ['z', { d: 0 }]] + })) // TODO: It would be better to do this in both cases. + //expectRight: ['x', r:true] + + it('awful delete nonsense', () => { + xf({ + op1: [['x', { r: true }], ['y', { i: 'hi' }]], // delete doc.x, insert doc.y + op2: [['x', 'a', { p: 0 }], ['y', { d: 0 }]], // move doc.x.a -> doc.y + expect: [['x', { r: true }], ['y', { r: true, i: 'hi' }]] + }) // del doc.x and doc.y, insert doc.y + + xf({ + op1: [['x', 'a', { p: 0 }], ['y', { d: 0 }]], // x.a -> y + op2: [['x', { r: true }], ['y', { i: 'hi' }]], // delete x, ins y + expect: null + }) + + xf({ + op1: [10, { r: true }], + op2: [[5, { d: 0 }], [10, 1, { p: 0 }]], + expect: [[5, { r: true }], [11, { r: true }]] + }) + }) + }) + // And how do those indexes interact with pick / drop operations?? + + describe('swap', () => { + const swap = [ + ['a', { p: 0 }, 'b', { p: 1 }], + ['b', { d: 1 }, 'a', { d: 0 }] + ] + + it('noop vs swap', () => + xf({ + op1: null, + op2: swap, + expect: null + })) + + it('can swap two edits', () => + xf({ + op1: ['a', { es: ['a edit'] }, 'b', { es: ['b edit'] }], + op2: swap, + expect: ['b', { es: ['b edit'] }, 'a', { es: ['a edit'] }] + })) + }) + + describe('lists', () => { + it('can rewrite simple list indexes', () => { + xf({ + op1: [10, { es: ['edit'] }], + op2: [0, { i: 'oh hi' }], + expect: [11, { es: ['edit'] }] + }) + + xf({ + op1: [10, { r: true }], + op2: [0, { i: 'oh hi' }], + expect: [11, { r: true }] + }) + + xf({ + op1: [10, { i: {} }], + op2: [0, { i: 'oh hi' }], + expect: [11, { i: {} }] + }) + }) + + it('can change the root from an object to a list', () => + xf({ + op1: ['a', { es: ['hi'] }], + op2: [{ i: [], r: true }, [0, { d: 0 }], ['a', { p: 0 }]], + expect: [0, { es: ['hi'] }] + })) + + it('can handle adjacent drops', () => + xf({ + op1: [[11, { i: 1 }], [12, { i: 2 }], [13, { i: 3 }]], + op2: [0, { r: true }], + expect: [[10, { i: 1 }], [11, { i: 2 }], [12, { i: 3 }]] + })) + + it('fixes drop indexes correctly 1', () => + xf({ + op1: [[0, { r: true }], [1, { i: 'hi' }]], + op2: [1, { r: true }], + expect: [0, { r: true, i: 'hi' }] + })) + + it('list drop vs delete uses the correct result index', () => { + xf({ + op1: [2, { i: 'hi' }], + op2: [2, { r: true }], + expect: [2, { i: 'hi' }] + }) + + xf({ + op1: [3, { i: 'hi' }], + op2: [2, { r: true }], + expect: [2, { i: 'hi' }] + }) + }) + + it('list drop vs drop uses the correct result index', () => + xf({ + op1: [2, { i: 'hi' }], + op2: [2, { i: 'other' }], + expectLeft: [2, { i: 'hi' }], + expectRight: [3, { i: 'hi' }] + })) + + it('list drop vs delete and drop', () => { + xf({ + op1: [2, { i: 'hi' }], + op2: [2, { r: true, i: 'other' }], + expectLeft: [2, { i: 'hi' }], + expectRight: [3, { i: 'hi' }] + }) + + xf({ + op1: [3, { i: 'hi' }], + op2: [[2, { r: true }], [3, { i: 'other' }]], + expect: [2, { i: 'hi' }] + }) + + xf({ + op1: [4, { i: 'hi' }], + op2: [[2, { r: true }], [3, { i: 'other' }]], + expectLeft: [3, { i: 'hi' }], + expectRight: [4, { i: 'hi' }] + }) + }) + + it('list delete vs drop', () => { + xf({ + op1: [1, { r: true }], + op2: [2, { i: 'hi' }], + expect: [1, { r: true }] + }) + + xf({ + op1: [2, { r: true }], + op2: [2, { i: 'hi' }], + expect: [3, { r: true }] + }) + + xf({ + op1: [3, { r: true }], + op2: [2, { i: 'hi' }], + expect: [4, { r: true }] + }) + }) + + it('list delete vs delete', () => + xf({ + op1: [1, { r: true }], + op2: [1, { r: true }], + expect: null + })) // It was already deleted. + + it('fixes drop indexes correctly 2', () => + xf({ + op1: [[0, { r: true }], [1, { i: 'hi' }]], + op2: [2, { r: true }], // Shouldn't affect the op. + expect: [[0, { r: true }], [1, { i: 'hi' }]] + })) + + it('insert vs delete parent', () => + xf({ + op1: [2, 'x', { i: 'hi' }], + op2: [2, { r: true }], + conflict: { type: RM_UNEXPECTED_CONTENT }, + expect: null + })) + + it('transforms against inserts in my own list', () => + xf({ + //[0,1,2,3] -> [a,0,b,1,2,3...] + op1: [[0, { i: 'a' }], [2, { i: 'b' }]], + op2: [1, { r: true }], + expect: [[0, { i: 'a' }], [2, { i: 'b' }]] + })) + + it('vs cancelled op2 drop', () => + xf({ + doc: { x: { a: 'x.a' }, y: ['a', 'b', 'c'] }, + op1: [['x', { r: true }], ['y', 3, { i: 5 }]], + op2: [['x', 'a', { p: 0 }], ['y', 2, { d: 0 }]], + expect: [['x', { r: true }], ['y', [2, { r: true }], [3, { i: 5 }]]] + })) + + it('vs cancelled op1 drop', () => + xf({ + op1: [['x', { p: 0 }], ['y', [3, { d: 0 }], [4, { i: 5 }]]], + op2: ['x', { r: true }], + expect: ['y', 3, { i: 5 }] + })) + + it('vs cancelled op1 pick', () => + xf({ + doc: Array.from('abcdefg'), + op1: [[1, { p: 0 }], [4, { r: true, i: 4 }], [6, { d: 0 }]], + op2: [1, { r: true }], + expect: [[3, { r: true }], [4, { i: 4 }]] + })) + + it('xxxxx 1', () => + diamond({ + // TODO Regression. + doc: Array.from('abcdef'), + op1: [[1, { p: 0, i: 'AAA' }], [3, { i: 'BBB' }], [5, { d: 0 }]], + op2: [1, { r: true }] + })) + + it('xxxxx 2', () => + diamond({ + doc: Array.from('abcdef'), + op1: [[1, { p: 0, i: 'AAA' }], [3, { d: 0 }], [5, { i: 'CCC' }]], + op2: [1, { r: true }] + })) + }) + + describe('edit', () => { + it('transforms edits by one another', () => + xf({ + op1: [1, { es: [2, 'hi'] }], + op2: [1, { es: ['yo'] }], + expect: [1, { es: [4, 'hi'] }] + })) + + it('copies in ops otherwise', () => + xf({ + op1: ['x', { e: { position: 2, text: 'wai' }, et: 'simple' }], + op2: ['y', { r: true }], + expect: ['x', { e: { position: 2, text: 'wai' }, et: 'simple' }] + })) + + it('allows edits at the root', () => + xf({ + op1: [{ e: { position: 2, text: 'wai' }, et: 'simple' }], + op2: [{ e: { position: 0, text: 'omg' }, et: 'simple' }], + expect: [{ e: { position: 5, text: 'wai' }, et: 'simple' }] + })) + + it('applies edits in the right order', () => + xf({ + // Edits happen *after* the drop phase. + op1: [1, { es: [2, 'hi'] }], + op2: [[1, { i: {} }], [2, { es: ['yo'] }]], + expect: [2, { es: [4, 'hi'] }] + })) + + it('an edit on a deleted object goes away', () => + xf({ + op1: [1, { es: [2, 'hi'] }], + op2: [1, { r: 'yo' }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op2: [1, { r: true }] + }, // .... It'd be better if this copied the remove. + expect: null + })) + }) + }) + + // TODO Numbers + + // ***** Test cases found by the fuzzer which have caused issues + describe('fuzzer tests', () => { + it('asdf', () => + apply({ + doc: { the: '', Twas: 'the' }, + op: ['the', { es: [] }], + expect: { the: '', Twas: 'the' } + })) + + it('does not duplicate list items from edits', () => + apply({ + doc: ['eyes'], + op: [0, { es: [] }], + expect: ['eyes'] + })) + + it('will edit the root document', () => + apply({ + doc: '', + op: [{ es: [] }], + expect: '' + })) + + // ------ These have nothing to do with apply. TODO: Move them out of this grouping. + + it('diamond', () => + // TODO: Do this for all combinations. + diamond({ + doc: Array.from('abcde'), + op1: [[0, { p: 0 }], [1, { d: 0 }]], + op2: [[0, { p: 0 }], [4, { d: 0 }]] + })) + + it('shuffles lists correctly', () => + xf({ + op1: [[0, { p: 0 }], [1, { d: 0 }]], + op2: [[0, { p: 0 }], [10, { d: 0 }]], + expectLeft: [[1, { d: 0 }], [10, { p: 0 }]], + expectRight: null + })) + + it('inserts before edits', () => { + xf({ + op1: [0, 'x', { i: 5 }], + op2: [0, { i: 35 }], + expect: [1, 'x', { i: 5 }] + }) + + xf({ + op1: [0, { es: [] }], + op2: [0, { i: 35 }], + expect: [1, { es: [] }] + }) + }) + + it( + 'duplicates become noops in a list', + () => + xf({ + op1: [0, { p: 0, d: 0 }], + op2: [0, { p: 0, d: 0 }], + expectLeft: [0, { p: 0, d: 0 }], // This is a bit weird. + expectRight: null + }), + + () => + xf({ + op1: [0, { r: true, i: 'a' }], + op2: [0, { i: 'b' }], + expectLeft: [[0, { i: 'a' }], [1, { r: true }]], + expectRight: [1, { r: true, i: 'a' }] + }), + + () => + xf({ + op1: [0, { r: true, i: 5 }], + op2: [0, { r: true }], + expect: [0, { i: 5 }] + }) + ) + + it('p1 pick descends correctly', () => { + xf({ + op1: [2, { r: true }, 1, { es: ['hi'] }], + op2: [3, 1, { r: true }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [2, 1, { es: ['hi'] }] + }, + expect: [2, { r: true }] + }) + + xf({ + op1: [[2, { r: true }, 1, { es: ['hi'] }], [3, 1, { r: true }]], + op2: [3, 2, { r: true }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [2, 1, { es: ['hi'] }] + }, + expect: [[2, { r: true }], [3, 1, { r: true }]] + }) + }) + + it('transforms picks correctly', () => + xf({ + op1: [1, 1, { r: true }], + op2: [0, { p: 0, d: 0 }], + expect: [1, 1, { r: true }] + })) + + it('pick & drop vs insert after the picked item', () => + xf({ + op1: [0, { p: 0, d: 0 }], // Remove / insert works the same. + op2: [1, { i: 'hi' }], + expectLeft: [0, { p: 0, d: 0 }], + expectRight: [[0, { p: 0 }], [1, { d: 0 }]] + })) + + it('pick same item vs shuffle list', () => + xf({ + op1: [1, ['x', { p: 0 }], ['y', { d: 0 }]], + op2: [1, { d: 0 }, 'x', { p: 0 }], + expectLeft: [1, { p: 0 }, 'y', { d: 0 }], + expectRight: null + })) + + it('remove the same item in a list', () => + xf({ + op1: [0, { r: true }], + op2: [0, { r: true }], + expect: null + })) + + it('rm vs hold item', () => + xf({ + op1: [0, { r: true }], + op2: [0, { p: 0, d: 0 }], + expect: [0, { r: true }] + })) + + it('moves child elements correctly', () => + xf({ + doc: ['a', [0, 10, 20], 'c'], + op1: [1, 0, { p: 0, d: 0 }], + op2: [[1, { d: 0 }], [2, { p: 0 }]], + expect: [2, 0, { d: 0, p: 0 }] + })) + + it('moves list indexes', () => + xf({ + doc: [[], 'b', 'c'], + op1: [[0, 'hi', { d: 0 }], [1, { p: 0 }]], + op2: [[0, { p: 0 }], [20, { d: 0 }]], + expect: [[0, { p: 0 }], [19, 'hi', { d: 0 }]] + })) + + it('insert empty string vs insert null', () => + xf({ + doc: undefined, + op1: [{ i: 'hi' }], + op2: [{ i: null }], + conflict: { type: DROP_COLLISION }, + expectLeft: [{ r: true, i: 'hi' }], + expectRight: null + })) + + it('move vs emplace', () => + xf({ + doc: ['a', 'b'], + op1: [[0, { p: 0 }], [1, { d: 0 }]], + op2: [1, { p: 0, d: 0 }], + expectLeft: [0, { p: 0, d: 0 }], + expectRight: [[0, { p: 0 }], [1, { d: 0 }]] + })) + + it('rm chases a subdocument that was moved out', () => + xf({ + doc: [['aaa']], + op1: [0, { r: true }], + op2: [0, { d: 0 }, 0, { p: 0 }], // Valid because lists. + expect: [[0, { r: true }], [1, { r: true }]] + })) + + it('colliding drops', () => + xf({ + doc: ['a', 'b', {}], + op1: [[0, { p: 0 }], [1, 'x', { d: 0 }]], // -> ['b', x:'a'] + op2: [1, { p: 0 }, 'x', { d: 0 }], // -> ['a', x:'b'] + conflict: { type: DROP_COLLISION }, + expectLeft: [[0, { p: 0 }, 'x', { d: 0 }], [1, 'x', { r: true }]], + expectRight: [0, { r: true }] + })) + + it('transform crash', () => + xf({ + op1: [['the', { r: true, d: 0 }], ['whiffling', { p: 0 }]], + op2: ['the', { p: 0, d: 0 }], + expect: [['the', { d: 0, r: true }], ['whiffling', { p: 0 }]] + })) + + it('transforms drops when the parent is moved by a remove', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0 }, 1, { i: 2 }]], + op2: ['a', 0, { r: 1 }], + expect: [['a', { p: 0 }], ['b', { d: 0 }, 0, { i: 2 }]] + })) + + it('transforms drops when the parent is moved by a drop', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0 }, 1, { i: 2 }]], + op2: ['a', 0, { i: 1 }], + expect: [['a', { p: 0 }], ['b', { d: 0 }, 2, { i: 2 }]] + })) + + it('transforms conflicting drops obfuscated by a move', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0 }, 1, { i: 2 }]], + op2: ['a', 1, { i: 1 }], + expectLeft: [['a', { p: 0 }], ['b', { d: 0 }, 1, { i: 2 }]], + expectRight: [['a', { p: 0 }], ['b', { d: 0 }, 2, { i: 2 }]] + })) + + it('transforms edits when the parent is moved', () => + xf({ + op1: [['x', { p: 0 }], ['y', { d: 0, es: [1, 'xxx'] }]], + op2: ['x', { es: [{ d: 1 }, 'Z'] }], + expectLeft: [['x', { p: 0 }], ['y', { d: 0, es: ['xxx'] }]], + expectRight: [['x', { p: 0 }], ['y', { d: 0, es: [1, 'xxx'] }]] + })) + + it('xf lots', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0, es: ['hi'] }]], + op2: [['a', { p: 0 }], ['c', { d: 0 }]], + expectLeft: [['b', { d: 0, es: ['hi'] }], ['c', { p: 0 }]], + expectRight: ['c', { es: ['hi'] }] + })) + + it('inserts are moved back by the other op', () => + xf({ + op1: [['a', { p: 0 }], ['b', { d: 0 }, 'x', { i: 'hi' }]], + op2: [['a', { p: 0 }], ['c', { d: 0 }]], + expectLeft: [['b', { d: 0 }, 'x', { i: 'hi' }], ['c', { p: 0 }]], + expectRight: ['c', 'x', { i: 'hi' }] + })) + + it('more awful edit moves', () => + xf({ + op1: [['a', { p: 0 }], ['c', { d: 0 }, 'x', { es: [] }]], + op2: ['a', ['b', { d: 0 }], ['x', { p: 0 }]], + expect: [['a', { p: 0 }], ['c', { d: 0 }, 'b', { es: [] }]] + })) + + it('inserts null', () => + xf({ + op1: ['x', 'a', { i: null }], + op2: [['x', { p: 0 }], ['y', { d: 0 }]], + expect: ['y', 'a', { i: null }] + })) + + it('preserves local insert if both sides delete', () => + xf({ + op1: [{ i: {}, r: true }, 'x', { i: 'yo' }], + op2: [{ r: true }], + expect: [{ i: {} }, 'x', { i: 'yo' }] + })) + + it('handles insert/delete vs move', () => + xf({ + op1: ['a', { i: {}, r: true }, 'x', { i: 'yo' }], + op2: [['a', { p: 0 }], ['b', { d: 0 }]], + expect: [['a', { i: {} }, 'x', { i: 'yo' }], ['b', { r: true }]] + })) + + it('insert pushes edit target', () => + xf({ + op1: [[0, { i: 'yo' }], [1, 'a', { es: [] }]], + op2: [0, ['a', { p: 0 }], ['b', { d: 0 }]], + expect: [[0, { i: 'yo' }], [1, 'b', { es: [] }]] + })) + + it('composes simple regression', () => { + compose({ + op1: [0, { p: 0, d: 0 }], + op2: [{ r: true }], + expect: [{ r: true }, 0, { r: true }] + }) + + compose({ + op1: ['a', 1, { r: true }], + op2: ['a', { r: true }], + expect: ['a', { r: true }, 1, { r: true }] + }) + }) + + it('ignores op2 inserts for index position after op1 insert', () => + xf({ + op1: [{ r: true, i: [] }, 0, { i: '' }], + op2: [0, { i: 0 }], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [{ r: true }] + }, + expect: [{ r: true, i: [] }, 0, { r: true, i: '' }] + })) + + it('edit moved inside a removed area should be removed', () => + xf({ + op1: [[0, { r: true }], [2, { es: [] }]], + op2: [[0, 'x', { d: 0 }], [3, { p: 0 }]], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [0, { r: true }] + }, + expect: [0, { r: true }, 'x', { r: true }] + })) + + it('advances indexes correctly with mixed numbers', () => + xf({ + op1: [ + ['x', [0, { p: 0 }], [1, { d: 1 }]], + ['y', { p: 1 }], + ['zzz', { d: 0 }] + ], + op2: [['x', 2, { i: 'hi' }], ['y', { p: 0 }], ['z', { d: 0 }]], + expectLeft: [ + ['x', [0, { p: 1 }], [1, { d: 0 }]], + ['z', { p: 0 }], + ['zzz', { d: 1 }] + ], + expectRight: [['x', 0, { p: 0 }], ['zzz', { d: 0 }]] + })) + + it('handles index positions past cancelled drops 1', () => + xf({ + op1: [0, { r: true, i: [''] }], + op2: [[0, { p: 0, d: 0 }], [1, { i: 23 }]], + expectLeft: [0, { r: true, i: [''] }], + expectRight: [[0, { r: true }], [1, { i: [''] }]] + })) + + it('handles index positions past cancelled drops 2', () => + xf({ + // This looks more complicated, but its a simpler version of the above test. + op1: [['a', { r: true }], ['b', 0, { i: 'hi' }]], + op2: [['a', { p: 0 }], ['b', [0, { d: 0 }], [1, { i: 'yo' }]]], + expectLeft: ['b', 0, { i: 'hi', r: true }], + expectRight: ['b', [0, { r: true }], [1, { i: 'hi' }]] + })) + + it('calculates removed drop indexes correctly', () => + xf({ + op1: [[0, { i: 'hi', p: 0 }], [1, 1, { d: 0 }], [2, { r: true }]], + op2: [[0, { i: 'yo', p: 0 }], [1, 1, { d: 0 }]], + expectLeft: [ + [0, { i: 'hi' }], + [1, 1, { p: 0 }], + [2, { r: true }, 1, { d: 0 }] + ], + expectRight: [[1, { i: 'hi' }], [2, { r: true }]] + })) + + it('removed drop indexes calc regression', () => + xf({ + op1: [[1, { p: 0 }, 'burbled', { d: 0 }], [3, { r: true }]], + op2: [ + [0, { i: 'to', r: true }], + [1, { p: 1 }, ['its', { d: 0 }], ['thought', { d: 1 }]], + [3, { p: 0 }] + ], + expectLeft: [ + 1, + ['burbled', { d: 0 }], + ['its', { r: true }], + ['thought', { p: 0 }] + ], + expectRight: [1, 'its', { r: true }] + })) + + it('removed drop indexes tele to op1 pick', () => + xf({ + op1: ['a', 0, [0, { es: [] }], [2, { r: true }]], + op2: [ + ['a', { p: 0 }, 0, 0, { p: 1 }], + ['b', { d: 0 }, 0, 1, 0, { d: 1 }] + ], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', 0, 2, { r: true }], + op2: [['a', 0, 0, { p: 0 }], ['b', 0, 1, 0, { d: 0 }]] + }, + expect: ['b', 0, 1, { r: true }, 0, { r: true }] + })) + + it('tracks removed drop index teleports', () => + xf({ + // rm 0.a, move 0.b -> 0.c + doc: [{ a: ['a'], b: 'b' }], + op1: [0, ['a', { r: true }], ['b', { p: 0 }], ['c', { d: 0 }]], // [{c:'b'}] + op2: [0, { d: 0, p: 1 }, [0, { d: 1 }], ['a', { p: 0 }]], // [[{b:'b'}, 'a']] + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: [0, 'a', { r: true }], + op2: [0, { p: 0 }, 0, { d: 0 }] + }, + expect: [0, { r: true }, 0, { r: true }] + })) + + it('handles transforming past cancelled move', () => + xf({ + op1: [[0, { r: true }], [10, { i: [''] }]], + op2: [0, { p: 0, d: 0 }], + expect: [[0, { r: true }], [10, { i: [''] }]] + })) + + it('correctly adjusts indexes in another fuzzer great', () => + xf({ + op1: [[0, { d: 0, r: true }], [3, { p: 0 }]], + op2: [[0, { p: 0 }], [3, { d: 0 }]], + expect: [[0, { d: 0 }], [2, { p: 0 }], [3, { r: true }]] + })) + + it('op2 moves into something op1 removes and op1 moves into that', () => + xf({ + op1: [['a', { r: true }, 'aa', { p: 0 }], ['b', 'x', { d: 0 }]], + op2: [['a', 'bb', { d: 0 }], ['b', { p: 0 }]], + conflict: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', { r: true }] + }, + expect: ['a', { r: true }, ['aa', { r: true }], ['bb', { r: true }]] + })) // Also ok if we miss the second rs. + + it('op2 moves into op1 remove edge cases', () => { + // Sorry not minified. + xf({ + op1: [ + 'Came', + 0, + [0, { r: true }, 'he', { p: 0 }], + [1, { d: 0 }, 0, { i: 'time' }] + ], + op2: [ + 'Came', + 0, + [0, 'he', [0, { d: 0 }], [1, { es: [] }]], + [1, { p: 0 }] + ], + expectLeft: [ + 'Came', + 0, + 0, + { r: true, d: 0 }, + [0, { i: 'time' }], + ['he', { p: 0 }] + ], + expectRight: [ + 'Came', + 0, + 0, + { r: true, d: 0 }, + [1, { i: 'time' }], + ['he', { p: 0 }] + ] + }) + + xf({ + op1: [[0, [1, { p: 0 }], [2, { r: true }]], [1, 'xxx', { d: 0 }]], + op2: [0, 1, { i: {}, p: 0 }, 'b', { d: 0 }], + expectLeft: [ + [0, [1, 'b', { p: 0 }], [2, { r: true }]], + [1, 'xxx', { d: 0 }] + ], + expectRight: [0, 2, { r: true }] + }) + }) + + it('translates indexes correctly in this fuzzer find', () => + xf({ + op1: [0, { p: 0 }, 'x', { d: 0 }], + op2: [[0, { p: 0, d: 0 }], [1, { i: 'y' }]], + expectLeft: [[0, { p: 0 }], [1, 'x', { d: 0 }]], + expectRight: null + })) + + it('buries children of blackholed values', () => + xf({ + op1: [ + [0, ['a', { p: 0 }], ['b', { d: 0 }], ['c', { d: 1 }]], + [1, { p: 1 }] + ], + op2: [0, { p: 0 }, 'x', { d: 0 }], + // This is a bit interesting. The question is, which op2 picks and drops + // should we include in the output? For now the answer is that we include + // anything in both ops thats going to end up inside the blackholed + // content. + conflict: { type: BLACKHOLE }, + + // op1: [[0, 'c', d:0], [1, p:0]] + expect: [0, { r: true }, 'x', { r: true }] + })) + + it('does not conflict when removed target gets moved inside removed container', () => { + // This edge case is interesting because we don't generate the same + // conflicts on left and right. We want our move of a.x to escape the + // object before removing it, but when we're right, the other operation's + // move holds the object and we get an unexpected rm conflict. + xf({ + op1: [['a', { r: true }, 'x', { p: 0 }], ['b', { d: 0 }]], + op2: ['a', ['x', { p: 0 }], ['y', { d: 0 }]], + conflictRight: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', { r: true }] + }, + expectLeft: [['a', { r: true }, 'y', { p: 0 }], ['b', { d: 0 }]], + expectRight: ['a', { r: true }, 'y', { r: true }] + }) + + xf({ + op1: [['a', { r: true }, 1, { p: 0 }], ['b', { d: 0 }]], + op2: ['a', [0, { d: 0 }], [1, { p: 0 }]], + expectLeft: [['a', { r: true }, 0, { p: 0 }], ['b', { d: 0 }]], + conflictRight: { + type: RM_UNEXPECTED_CONTENT, + op1: ['a', { r: true }] + }, + expectRight: ['a', { r: true }, 0, { r: true }] + }) + + // TODO have a look at this - doesn't seem right to just define an object + { + expect: [['a', { r: true }, 0, { p: 0 }], ['b', { d: 0 }]] + } + }) + + it('compose copies op2 edit data', () => + compose({ + op1: ['a', { r: true }], + op2: [['x', { p: 0 }], ['y', { d: 0 }, 'b', { es: [] }]], + expect: [ + ['a', { r: true }], + ['x', { p: 0 }], + ['y', { d: 0 }, 'b', { es: [] }] + ] + })) + + it('does not conflict when the dest is salvaged', () => + xf({ + op1: [['a', { p: 0 }], ['b', { i: 'hi' }], ['c', { d: 0 }]], + op2: [['a', { p: 0 }], ['b', { d: 0 }]], + expectLeft: [['b', { p: 0, i: 'hi' }], ['c', { d: 0 }]], + conflictRight: { + type: DROP_COLLISION, + op1: ['b', { i: 'hi' }] + }, + expectRight: null + })) + + it('does not conflict on identical r/i pairs', () => + xf({ + op1: [{ i: [], r: true }], + op2: [{ i: [], r: true }], + expect: null + })) + + it('allows embedded edits in identical r/i', () => + xf({ + op1: [{ r: true, i: '', es: [] }], + op2: [{ r: true, i: '' }], + expect: [{ es: [] }] + })) + + it('does not conflict on identical r/i pairs with identical drops inside', () => + xf({ + op1: [{ i: {}, r: true }, 'a', { i: 'a' }], + op2: [{ i: {}, r: true }, 'a', { i: 'a' }], + expect: null + })) + + it('generates a DROP_COLLISION on children', () => + xf({ + op1: [{ i: {}, r: true }, 'a', { i: 'a' }], + op2: [{ i: {}, r: true }, 'a', { i: 'b' }], + conflict: { + type: DROP_COLLISION, + op1: ['a', { i: 'a' }], + op2: ['a', { i: 'b' }] + }, + expectLeft: ['a', { r: true, i: 'a' }], + expectRight: null + })) + + it('Transforms edit moves into the right dest', () => + xf({ + op1: [ + 0, + { p: 0, d: 0 }, + // These parts are all needed for some reason. + [0, { i: 1 }], + [1, { r: true }], + [3, { es: [] }] + ], + op2: [0, [0, { d: 0 }], [3, { p: 0 }]], + expectLeft: [ + 0, + { p: 0, d: 0 }, + [0, { i: 1 }], + [1, { es: [] }], + [2, { r: true }] + ], + expectRight: [ + 0, + { p: 0, d: 0 }, + [0, { es: [] }], + [1, { i: 1 }], + [2, { r: true }] + ] + })) + + it('adjusts indexes of pick -> drop', () => + xf({ + op1: [0, { p: 0, d: 0 }], + op2: [[0, { i: 'yo', p: 0 }], [1, { d: 0 }]], + expectLeft: [[0, { d: 0 }], [1, { p: 0 }]], + expectRight: null + })) + + it('clears output outDrop when theres no pick', () => + xf({ + // Again, not minimized. We return the right data, we were just double- + // descending into outDrop. + op1: [['the', { d: 0, p: 0 }], ['toves', { r: true }]], + op2: [ + ['bird', { d: 0 }], + ['slain', { d: 1 }], + ['the', { p: 1 }], + ['toves', { p: 0 }] + ], + expectLeft: [ + ['bird', { r: true }], + ['slain', { p: 0 }], + ['the', { d: 0 }] + ], + expectRight: ['bird', { r: true }] + })) + + it('pushes drop indexes by other held items', () => + xf({ + op1: [[0, { p: 0 }], [1, [0, { i: 'hi' }], [1, { d: 0, es: [] }]]], + op2: [[0, { p: 1 }, 1, { d: 0 }, 2, { d: 1 }], [2, { p: 0 }]], + expectLeft: [ + 0, + 1, + [0, { i: 'hi' }], + [1, { d: 0, es: [] }], + [2, { p: 0 }] + ], + expectRight: [0, 1, [0, { i: 'hi' }], [3, { es: [] }]] + })) + + it('composes correctly with lots of removes', () => + compose({ + op1: [3, 1, { r: true }], + op2: [[0, { es: [] }], [1, { r: true, es: [] }], [2, { r: true }]], + expect: [ + [0, { es: [] }], + [1, { es: [], r: true }], + [2, { r: true }], + [3, 1, { r: true }] + ] + })) + + it('does not descend twice when p/r on an identical insert', () => + xf({ + op1: [['a', { p: 0, i: '' }], ['b', { d: 0 }]], + op2: ['a', { r: true, i: '' }], + expect: null + })) + + it('conflicts underneath a moved / inserted child', () => + xf({ + op1: [['a', { p: 0, i: {} }, 'x', { i: 5 }], ['b', { d: 0 }]], + op2: ['a', { r: true, i: {} }, 'x', { i: 6 }], + conflict: { + type: DROP_COLLISION, + op1: ['a', 'x', { i: 5 }], + op2: ['a', 'x', { i: 6 }] + }, + expectLeft: ['a', 'x', { r: true, i: 5 }], + expectRight: null + })) + + it('clears drop2 in transform moves', () => + xf({ + doc: [{ b: { a: 'hi' } }], + op1: [0, { d: 0 }, ['a', { es: [] }], ['b', { p: 0 }]], + op2: [0, 'b', ['a', { p: 0 }], ['b', { d: 0 }]], + expect: [0, { d: 0 }, 'b', { p: 0, es: [] }] + })) + + it('descends correctly when op2 picks and drops', () => + xf({ + op1: [ + ['b', { d: 0 }, [1, { es: [] }], [2, { i: null }]], + ['e', { p: 0 }] + ], + op2: [{ p: 0, d: 0 }, 'e', 1, { p: 1, d: 1 }], + expectLeft: [ + ['b', { d: 0 }, [1, { i: null }], [2, { es: [] }]], + ['e', { p: 0 }] + ], + expectRight: [ + ['b', { d: 0 }, [1, { es: [] }], [2, { i: null }]], + ['e', { p: 0 }] + ] + })) + + it('composes a pick out of the insert', () => + compose({ + op1: [{ i: [5, { x: 6 }] }], + op2: [[0, { r: true }, 'c', { d: 0 }], [1, 'x', { p: 0 }]], + // expect: [{i: [{c: 6}]}] + expect: [{ i: [{}] }, 0, 'c', { i: 6 }] + })) + + it('is not overeager to remove intermediate literal array items', () => + compose({ + op1: [[0, { i: ['a', 'b'] }, 0, { p: 0 }], [1, 0, { d: 0 }]], + op2: [0, { r: ['a'] }, 1, { r: 'b' }], + expect: [0, 0, { d: 0, p: 0 }] + })) + + it('descends down insert indexes correctly', () => + compose({ + op1: [{ i: [{}, 'a'] }, 1, { i: 'b' }], + op2: [[1, { r: 'b' }], [2, { r: 'a' }]], + expect: [{ i: [{}] }] + })) + + it('handles composes with ena: 0', () => + compose({ + op1: [{ i: 10 }], + op2: [{ ena: 0 }], + expect: [{ i: 10, ena: 0 }] + })) // Also ok: just discarding the ena:0. + + it('handles rm parent with cross move', () => + compose({ + op1: [['a', { p: 0 }], ['b', 1, { d: 0 }]], + op2: [['b', { r: true }, 1, { p: 0 }], ['c', { d: 0 }]], + expect: [['a', { p: 0 }], ['b', { r: true }], ['c', { d: 0 }]] + })) + + it('lets you remove children of an op at 2 levels', () => + compose({ + op1: [{ i: ['a', { x: 'hi' }] }], + op2: [{ r: true }, 1, 'x', { r: true }], + expect: null + })) + + it('discards op1 inserts inside a removed chunk', () => + compose({ + op1: ['y', [1, { i: 'x' }], [2, { i: ['a', 'b'] }]], + op2: [{ r: true }, 'y', 2, 0, { r: true }], + expect: [{ r: true }] + })) + + it('handles deeply nested blackhole operations', () => + xf({ + op1: [ + ['x', { p: 0 }], + ['y', ['a', ['j', { p: 1 }], ['k', { d: 1 }]], ['b', { d: 0 }]] + ], + op2: [ + ['x', 'xx', { d: 0 }, 'j', 'jj', { d: 1 }], + ['y', { p: 1 }, 'a', { p: 0 }] + ], + conflict: { type: BLACKHOLE }, + expect: ['x', { r: true }, 'xx', { r: true }, 'j', 'jj', { r: true }] + })) + + it('does not list removed op1 moves in the blackhole info', () => + xf({ + op1: [ + ['a', ['j', { d: 0 }], ['k', { d: 1 }]], + ['b', { p: 0 }, 'z', 0, { p: 1 }] + ], + op2: [['a', { p: 0 }], ['b', ['y', { d: 0 }], ['z', { r: true }]]], + conflict: { + type: BLACKHOLE, + op1: [['a', 'j', { d: 0 }], ['b', { p: 0 }]], + op2: [['a', { p: 0 }], ['b', 'y', { d: 0 }]] + }, + expect: ['b', { r: true }, 'y', { r: true }] + })) + + it('handles overlapping pick in blackholes', () => + xf({ + // This looks complicated, but its really not so bad. Its: + // a->b.0, a.x -> z + // vs + // b -> a.x -> a.y + // + // Its a bit twisty because we're both picking up the same element and + // putting it in different places. This is why we have different left and + // right results. + op1: [ + ['a', { p: 1 }, 'x', { p: 0 }], + ['b', 0, { d: 1 }], + ['z', { d: 0 }] + ], + op2: [['a', ['x', { d: 0, p: 1 }], ['y', { d: 1 }]], ['b', { p: 0 }]], + conflictLeft: { + type: BLACKHOLE, + op1: [['a', { p: 0 }], ['b', 0, { d: 0 }]], + op2: [['a', 'x', { d: 0 }], ['b', { p: 0 }]] + }, + expectLeft: [ + ['a', { r: true }, ['x', { r: true }], ['y', { p: 0 }]], + ['z', { d: 0 }] + ], + conflictRight: { + type: BLACKHOLE, + op1: [['a', { p: 0 }], ['b', 0, { d: 0 }]] + }, + expectRight: ['a', { r: true }, ['x', { r: true }], ['y', { r: true }]] + })) + }) +}) diff --git a/yarn.lock b/yarn.lock index e106f3f..eec7e90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -25,20 +30,28 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -coffeescript@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.3.2.tgz#e854a7020dfe47b7cf4dd412042e32ef1e269810" - integrity sha512-YObiFDoukx7qPBi/K0kUKyntEZDfBQiqs/DbrR1xzASKOBjGT7auD85/DiPeRr9k++lRj7l3uA9TNMLfyfcD/Q== +cli-progress@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-2.1.1.tgz#45ee1b143487c19043a3262131ccb4676f87f032" + integrity sha512-TSJw3LY9ZRSis7yYzQ7flIdtQMbacd9oYoiFphJhI4SzgmqF0zErO+uNv0lbUjk1L4AGfHQJ4OVYYzW+JV66KA== + dependencies: + colors "^1.1.2" + string-width "^2.1.1" + +colors@^1.1.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" + integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== commander@2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== +commander@^2.19.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== concat-map@0.0.1: version "0.0.1" @@ -107,6 +120,11 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -156,9 +174,12 @@ once@^1.3.0: wrappy "1" ot-fuzzer@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ot-fuzzer/-/ot-fuzzer-1.1.0.tgz#b556a799117ceeb458794ef376f34b985f62dc8d" - integrity sha512-48w+h509Sq48JMsit5/qPCxhWIzztS52DkhIztqiFV0UT2MA6BWu2JiLHSw6cLren1O5IWhNjU7LObOmVOAFGg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/ot-fuzzer/-/ot-fuzzer-1.2.1.tgz#41f70305fdd1d55268f6cc169c14c0eb9e5fb31c" + integrity sha512-dOm+Wb1Mqrw8ql5ksiZFf3Bdsruj5r4mHaa3COqtbg0ClkGjxZXwMGgMLjWslq/b0maeiw5yvYwIdH6As4svHg== + dependencies: + cli-progress "^2.1.1" + seedrandom "^2.4.4" ot-simple@^1.0.0: version "1.0.0" @@ -182,10 +203,15 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -source-map-support@~0.5.6: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== +seedrandom@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba" + integrity sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA== + +source-map-support@~0.5.10: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -195,6 +221,21 @@ source-map@^0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -203,13 +244,13 @@ supports-color@5.4.0: has-flag "^3.0.0" terser@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.11.0.tgz#60782893e1f4d6788acc696351f40636d0e37af0" - integrity sha512-5iLMdhEPIq3zFWskpmbzmKwMQixKmTYwY3Ox9pjtSklBLnHiuQ0GKJLhL1HSYtyffHM3/lDIFBnb82m9D7ewwQ== + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== dependencies: - commander "~2.17.1" + commander "^2.19.0" source-map "~0.6.1" - source-map-support "~0.5.6" + source-map-support "~0.5.10" unicount@^1.0.0: version "1.0.0"