diff --git a/src/tests/backend/specs/contentcollector.js b/src/tests/backend/specs/contentcollector.js index f7bc539e6..a6ff8c2d7 100644 --- a/src/tests/backend/specs/contentcollector.js +++ b/src/tests/backend/specs/contentcollector.js @@ -10,35 +10,64 @@ */ const AttributePool = require('../../../static/js/AttributePool'); +const Changeset = require('../../../static/js/Changeset'); const assert = require('assert').strict; const contentcollector = require('../../../static/js/contentcollector'); const jsdom = require('jsdom'); -const tests = { - nestedLi: { +// All test case `wantAlines` values must only refer to attributes in this list so that the +// attribute numbers do not change due to changes in pool insertion order. +const knownAttribs = [ + ['insertorder', 'first'], + ['italic', 'true'], + ['list', 'bullet1'], + ['list', 'bullet2'], + ['list', 'number1'], + ['list', 'number2'], + ['lmkr', '1'], + ['start', '1'], + ['start', '2'], +]; + +const testCases = [ + { + description: 'Simple', + html: '

foo

', + wantAlines: ['+3'], + wantText: ['foo'], + }, + { + description: 'Line starts with asterisk', + html: '

*foo

', + wantAlines: ['+4'], + wantText: ['*foo'], + }, + { description: 'Complex nested Li', html: '
  1. one
    1. 1.1
  2. two
', - wantLineAttribs: [ - '*0*1*2*3+1+3', '*0*4*2*5+1+3', '*0*1*2*5+1+3', + wantAlines: [ + '*0*4*6*7+1+3', + '*0*5*6*8+1+3', + '*0*4*6*8+1+3', ], wantText: [ '*one', '*1.1', '*two', ], }, - complexNest: { + { description: 'Complex list of different types', html: '
  1. item
    1. item1
    2. item2
', - wantLineAttribs: [ - '*0*1*2+1+3', - '*0*1*2+1+3', - '*0*1*2+1+1', - '*0*1*2+1+1', - '*0*1*2+1+1', - '*0*3*2+1+1', - '*0*3*2+1+1', - '*0*4*2*5+1+4', - '*0*6*2*7+1+5', - '*0*6*2*7+1+5', + wantAlines: [ + '*0*2*6+1+3', + '*0*2*6+1+3', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*3*6+1+1', + '*0*4*6*7+1+4', + '*0*5*6*8+1+5', + '*0*5*6*8+1+5', ], wantText: [ '*one', @@ -53,147 +82,174 @@ const tests = { '*item2', ], }, - ul: { + { description: 'Tests if uls properly get attributes', html: '
div

foo

', - wantLineAttribs: ['*0*1*2+1+1', '*0*1*2+1+1', '+3', '+3'], + wantAlines: [ + '*0*2*6+1+1', + '*0*2*6+1+1', + '+3', + '+3', + ], wantText: ['*a', '*b', 'div', 'foo'], }, - ulIndented: { + { description: 'Tests if indented uls properly get attributes', html: '

foo

', - wantLineAttribs: ['*0*1*2+1+1', '*0*3*2+1+1', '*0*1*2+1+1', '+3'], + wantAlines: [ + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*2*6+1+1', + '+3', + ], wantText: ['*a', '*b', '*a', 'foo'], }, - ol: { + { description: 'Tests if ols properly get line numbers when in a normal OL', html: '
  1. a
  2. b
  3. c

test

', - wantLineAttribs: ['*0*1*2*3+1+1', '*0*1*2*3+1+1', '*0*1*2*3+1+1', '+4'], + wantAlines: [ + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '+4', + ], wantText: ['*a', '*b', '*c', 'test'], noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', }, - lineDoBreakInOl: { + { description: 'A single completely empty line break within an ol should reset count if OL is closed off..', html: '
  1. should be 1

hello

  1. should be 1
  2. should be 2

', - wantLineAttribs: ['*0*1*2*3+1+b', '+5', '*0*1*2*4+1+b', '*0*1*2*4+1+b', ''], + wantAlines: [ + '*0*4*6*7+1+b', + '+5', + '*0*4*6*8+1+b', + '*0*4*6*8+1+b', + '', + ], wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''], noteToSelf: "Shouldn't include attribute marker in the

line", }, - testP: { + { description: 'A single

should create a new line', html: '

', - wantLineAttribs: ['', ''], + wantAlines: ['', ''], wantText: ['', ''], noteToSelf: '

should create a line break but not break numbering', }, - nestedOl: { - description: 'Tests if ols properly get line numbers when in a normal OL', + { + description: 'Tests if ols properly get line numbers when in a normal OL #2', html: 'a
  1. b
    1. c
notlist

foo

', - wantLineAttribs: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1', '+7', '+3'], + wantAlines: [ + '+1', + '*0*4*6*7+1+1', + '*0*5*6*8+1+1', + '+7', + '+3', + ], wantText: ['a', '*b', '*c', 'notlist', 'foo'], noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', }, - nestedOl2: { + { description: 'First item being an UL then subsequent being OL will fail', html: '', - wantLineAttribs: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'], + wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'], wantText: ['a', '*b', '*c'], noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', disabled: true, }, - lineDontBreakOL: { + { description: 'A single completely empty line break within an ol should NOT reset count', html: '
  1. should be 1
  2. should be 2
  3. should be 3

', - wantLineAttribs: [], + wantAlines: [], wantText: ['*should be 1', '*should be 2', '*should be 3'], noteToSelf: "

should create a line break but not break numbering -- This is what I can't get working!", disabled: true, }, - ignoreAnyTagsOutsideBody: { + { description: 'Content outside body should be ignored', html: 'titleempty
', - wantLineAttribs: ['+5'], + wantAlines: ['+5'], wantText: ['empty'], }, - lineWithMultipleSpaces: { + { description: 'Multiple spaces should be preserved', html: 'Text with more than one space.
', - wantLineAttribs: ['+10'], + wantAlines: ['+10'], wantText: ['Text with more than one space.'], }, - lineWithMultipleNonBreakingAndNormalSpaces: { + { description: 'non-breaking and normal space should be preserved', html: 'Text with  more   than  one space.
', - wantLineAttribs: ['+10'], + wantAlines: ['+10'], wantText: ['Text with more than one space.'], }, - multiplenbsp: { + { description: 'Multiple nbsp should be preserved', html: '  
', - wantLineAttribs: ['+2'], + wantAlines: ['+2'], wantText: [' '], }, - multipleNonBreakingSpaceBetweenWords: { + { description: 'Multiple nbsp between words ', html: '  word1  word2   word3
', - wantLineAttribs: ['+m'], + wantAlines: ['+m'], wantText: [' word1 word2 word3'], }, - nonBreakingSpacePreceededBySpaceBetweenWords: { + { description: 'A non-breaking space preceded by a normal space', html: '  word1  word2  word3
', - wantLineAttribs: ['+l'], + wantAlines: ['+l'], wantText: [' word1 word2 word3'], }, - nonBreakingSpaceFollowededBySpaceBetweenWords: { + { description: 'A non-breaking space followed by a normal space', html: '  word1  word2  word3
', - wantLineAttribs: ['+l'], + wantAlines: ['+l'], wantText: [' word1 word2 word3'], }, - spacesAfterNewline: { + { description: 'Don\'t collapse spaces that follow a newline', html: 'something
something
', - wantLineAttribs: ['+9', '+m'], + wantAlines: ['+9', '+m'], wantText: ['something', ' something'], }, - spacesAfterNewlineP: { + { description: 'Don\'t collapse spaces that follow a empty paragraph', html: 'something

something
', - wantLineAttribs: ['+9', '', '+m'], + wantAlines: ['+9', '', '+m'], wantText: ['something', '', ' something'], }, - spacesAtEndOfLine: { + { description: 'Don\'t collapse spaces that preceed/follow a newline', html: 'something
something
', - wantLineAttribs: ['+l', '+m'], + wantAlines: ['+l', '+m'], wantText: ['something ', ' something'], }, - spacesAtEndOfLineP: { + { description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', html: 'something

something
', - wantLineAttribs: ['+l', '', '+m'], + wantAlines: ['+l', '', '+m'], wantText: ['something ', '', ' something'], }, - nonBreakingSpacesAfterNewlines: { + { description: 'Don\'t collapse non-breaking spaces that follow a newline', html: 'something
   something
', - wantLineAttribs: ['+9', '+c'], + wantAlines: ['+9', '+c'], wantText: ['something', ' something'], }, - nonBreakingSpacesAfterNewlinesP: { + { description: 'Don\'t collapse non-breaking spaces that follow a paragraph', html: 'something

   something
', - wantLineAttribs: ['+9', '', '+c'], + wantAlines: ['+9', '', '+c'], wantText: ['something', '', ' something'], }, - preserveSpacesInsideElements: { + { description: 'Preserve all spaces when multiple are present', html: 'Need more space s !
', - wantLineAttribs: ['+h*0+4+2'], + wantAlines: ['+h*1+4+2'], wantText: ['Need more space s !'], }, - preserveSpacesAcrossNewlines: { + { description: 'Newlines and multiple spaces across newlines should be preserved', html: ` Need @@ -201,25 +257,25 @@ const tests = { space s !
`, - wantLineAttribs: ['+19*0+4+b'], + wantAlines: ['+19*1+4+b'], wantText: ['Need more space s !'], }, - multipleNewLinesAtBeginning: { + { description: 'Multiple new lines at the beginning should be preserved', html: '

first line

second line
', - wantLineAttribs: ['', '', '', '', '+a', '', '+b'], + wantAlines: ['', '', '', '', '+a', '', '+b'], wantText: ['', '', '', '', 'first line', '', 'second line'], }, - multiLineParagraph: { + { description: 'A paragraph with multiple lines should not loose spaces when lines are combined', html: `

а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

`, - wantLineAttribs: ['+1t'], + wantAlines: ['+1t'], wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'], }, - multiLineParagraphWithPre: { + { description: 'lines in preformatted text should be kept intact', html: `

а б в г ґ д е є ж з и і ї й к л м н о

multiple
@@ -229,7 +285,7 @@ pre
 

п р с т у ф х ц ч ш щ ю я ь

`, - wantLineAttribs: ['+11', '+8', '+5', '+2', '+3', '+r'], + wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'], wantText: [ 'а б в г ґ д е є ж з и і ї й к л м н о', 'multiple', @@ -239,85 +295,100 @@ pre 'п р с т у ф х ц ч ш щ ю я ь', ], }, - preIntroducesASpace: { + { description: 'pre should be on a new line not preceded by a space', html: `

1

preline
 
`, - wantLineAttribs: ['+6', '+7'], + wantAlines: ['+6', '+7'], wantText: [' 1 ', 'preline'], }, - dontDeleteSpaceInsideElements: { + { description: 'Preserve spaces on the beginning and end of a element', html: 'Need more space s !
', - wantLineAttribs: ['+f*0+3+1'], + wantAlines: ['+f*1+3+1'], wantText: ['Need more space s !'], }, - dontDeleteSpaceOutsideElements: { + { description: 'Preserve spaces outside elements', html: 'Need more space s !
', - wantLineAttribs: ['+g*0+1+2'], + wantAlines: ['+g*1+1+2'], wantText: ['Need more space s !'], }, - dontDeleteSpaceAtEndOfElement: { + { description: 'Preserve spaces at the end of an element', html: 'Need more space s !
', - wantLineAttribs: ['+g*0+2+1'], + wantAlines: ['+g*1+2+1'], wantText: ['Need more space s !'], }, - dontDeleteSpaceAtBeginOfElements: { + { description: 'Preserve spaces at the start of an element', html: 'Need more space s !
', - wantLineAttribs: ['+f*0+2+2'], + wantAlines: ['+f*1+2+2'], wantText: ['Need more space s !'], }, -}; +]; describe(__filename, function () { - for (const test of Object.keys(tests)) { - const testObj = tests[test]; - describe(test, function () { - if (testObj.disabled) { - return xit('DISABLED:', test, function (done) { - done(); - }); - } + for (const tc of testCases) { + describe(tc.description, function () { + let apool; + let result; - it(testObj.description, async function () { - const {window: {document}} = new jsdom.JSDOM(testObj.html); - // Create an empty attribute pool - const apool = new AttributePool(); - // Convert a dom tree into a list of lines and attribute liens - // using the content collector object + before(async function () { + if (tc.disabled) return this.skip(); + const {window: {document}} = new jsdom.JSDOM(tc.html); + apool = new AttributePool(); + // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all + // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute + // numbers do not change if the attribute processing code changes.) + for (const attrib of knownAttribs) apool.putAttrib(attrib); + for (const aline of tc.wantAlines) { + const opIter = Changeset.opIterator(aline); + while (opIter.hasNext()) { + const op = opIter.next(); + Changeset.eachAttribNumber(op.attribs, (n) => assert(n < knownAttribs.length)); + } + } const cc = contentcollector.makeContentCollector(true, null, apool); cc.collectContent(document.body); - const result = cc.finish(); - const gotAttributes = result.lineAttribs; - const wantAttributes = testObj.wantLineAttribs; - const gotText = new Array(result.lines); - const wantText = testObj.wantText; + result = cc.finish(); + }); - assert.deepEqual(gotText[0], wantText); - assert.deepEqual(gotAttributes, wantAttributes); + it('text matches', async function () { + assert.deepEqual(result.lines, tc.wantText); + }); + + it('alines match', async function () { + assert.deepEqual(result.lineAttribs, tc.wantAlines); + }); + + it('attributes are sorted in canonical order', async function () { + const gotAttribs = []; + const wantAttribs = []; + for (const aline of result.lineAttribs) { + const gotAlineAttribs = []; + gotAttribs.push(gotAlineAttribs); + const wantAlineAttribs = []; + wantAttribs.push(wantAlineAttribs); + const opIter = Changeset.opIterator(aline); + while (opIter.hasNext()) { + const op = opIter.next(); + const gotOpAttribs = []; + gotAlineAttribs.push(gotOpAttribs); + const wantOpAttribs = []; + wantAlineAttribs.push(wantOpAttribs); + Changeset.eachAttribNumber(op.attribs, (n) => { + const attrib = apool.getAttrib(n); + gotOpAttribs.push(attrib); + wantOpAttribs.push(attrib); + }); + wantOpAttribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0)); + } + } + assert.deepEqual(gotAttribs, wantAttribs); }); }); } }); - - -function arraysEqual(a, b) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length !== b.length) return false; - - // If you don't care about the order of the elements inside - // the array, you should sort both arrays here. - // Please note that calling sort on an array will modify that array. - // you might want to clone your array first. - - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; - } - return true; -}