diff --git a/package-lock.json b/package-lock.json index b352046..1235576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rich-text", - "version": "4.0.0", + "version": "4.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -881,6 +881,12 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index ac1714c..ad50837 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "chai": "^4.2.0", "lodash": "^4.17.15", "mocha": "^6.1.4", - "ot-fuzzer": "^1.2.1" + "ot-fuzzer": "^1.2.1", + "uuid": "^8.3.2" }, "engines": { "node": ">=0.10" diff --git a/test/fuzzer.js b/test/fuzzer.js index 0e065ed..89ddef1 100644 --- a/test/fuzzer.js +++ b/test/fuzzer.js @@ -3,12 +3,16 @@ var expect = require('chai').expect; var fuzzer = require('ot-fuzzer'); var richText = require('../lib/type'); var Delta = richText.Delta; +const { v4: uuidv4 } = require('uuid'); + +var DEBUG = false; var FORMATS = { color: ['red', 'orange', 'yellow', 'green', 'blue', 'purple', null], font: ['serif', 'sans-serif', 'monospace', null], bold: [true, null], - italic: [true, null] + italic: [true, null], + detectionId: [] }; function generateRandomEmbed () { @@ -20,13 +24,23 @@ function generateRandomEmbed () { } }; -function generateRandomFormat (includeNull) { +let detId = 0; + +function generateRandomFormat (includeNull, detectionIds) { var format = {}; for (var key in FORMATS) { if (fuzzer.randomReal() < 0.5) { - var value = FORMATS[key][fuzzer.randomInt(FORMATS[key].length)]; - if (value || includeNull) { - format[key] = value; + if (key === 'detectionId') { + if (fuzzer.randomReal() < 0.8) { + format[key] = uuidv4() + } else if (includeNull) { + format[key] = null; + } + } else { + var value = FORMATS[key][fuzzer.randomInt(FORMATS[key].length)]; + if (value || includeNull) { + format[key] = value; + } } } } @@ -35,15 +49,30 @@ function generateRandomFormat (includeNull) { function generateRandomOp (snapshot) { snapshot = _.cloneDeep(snapshot); + var originalDets = {}; var length = snapshot.ops.reduce(function(length, op) { if (!op.insert) { console.error(snapshot); throw new Error('Snapshot should only have inserts'); } + + const opLength = (_.isString(op.insert) ? op.insert.length : 1); + + if (op.attributes?.detectionId) { + if (originalDets[op.attributes.detectionId]) { + originalDets[op.attributes.detectionId] += opLength; + } else { + originalDets[op.attributes.detectionId] = opLength; + } + } + // Snapshot should only have inserts - return length + (_.isString(op.insert) ? op.insert.length : 1); + return length + opLength; }, 0); + DEBUG && console.log('snap', snapshot.ops); + DEBUG && console.log('og', originalDets); + var base = length > 100 ? 10 : 7; // Favor deleting on long documents var delta = new Delta(); var result = new Delta(); @@ -70,19 +99,19 @@ function generateRandomOp (snapshot) { case 1: // Insert formatted text var word = fuzzer.randomWord(); - var formats = generateRandomFormat(false); + var formats = generateRandomFormat(false, Object.keys(originalDets)); delta.insert(word, formats); result.insert(word, formats); break; case 2: // Insert embed var type = generateRandomEmbed(); - var formats = generateRandomFormat(false); + var formats = generateRandomFormat(false, Object.keys(originalDets)); delta.insert(type, formats); result.insert(type, formats); break; case 3: case 4: - var attributes = generateRandomFormat(true); + var attributes = generateRandomFormat(true, Object.keys(originalDets)); delta.retain(modLength, attributes); ops = next(snapshot, modLength); for (var i in ops) { @@ -114,7 +143,68 @@ function generateRandomOp (snapshot) { result.push(snapshot.ops[i]); } - return [delta, result]; + + // Validate detections.... + var resultDets = {}; + result.ops.reduce((length, op) => { + if (!op.insert) { + console.error(result); + throw new Error('Result should only have inserts'); + } + + const opLength = (_.isString(op.insert) ? op.insert.length : 1); + if (op.attributes?.detectionId) { + if (resultDets[op.attributes.detectionId]) { + resultDets[op.attributes.detectionId].opLength += opLength; + resultDets[op.attributes.detectionId].appearences.push({ start: length, end: length + opLength }); + } else { + resultDets[op.attributes.detectionId] = { opLength, appearences: [{ start: length, end: length + opLength }] }; + } + } + return length + opLength; + }, 0); + + const needToDelete = Object.keys(resultDets).filter((detId) => { + if (originalDets[detId]) { + if (originalDets[detId] !== resultDets[detId].opLength) { + return true; + } else { + const firstAppearence = resultDets[detId].appearences[0]; + const lastAppearence = resultDets[detId].appearences[resultDets[detId].appearences.length - 1]; + return resultDets[detId].opLength !== lastAppearence.end - firstAppearence.start; + } + } else { + return false; + } + }); + + DEBUG && console.log('delta', delta.ops) + + if (needToDelete.length === 0) { + DEBUG && console.log('no validation') + DEBUG && console.log('res', result.ops) + DEBUG && console.log('dets', resultDets); + return [delta, result]; + } + + DEBUG && console.log('has validation', needToDelete); + + const cloned = _.cloneDeep(result); + const validatedResult = new Delta(); + cloned.ops.forEach((op) => { + const deleteDet = needToDelete.findIndex((id) => id === op.attributes?.detectionId); + if (deleteDet === -1) { + validatedResult.push(op); + } else { + var newAttr = op.attributes + delete newAttr['detectionId']; + validatedResult.insert(op.insert, Object.keys(newAttr).length > 0 ? newAttr : undefined); + } + }) + + DEBUG && console.log('res', validatedResult.ops); + + return [delta, validatedResult]; }; function next (snapshot, length) {