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: '- one
- 1.1
- 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: '- item
- item1
- 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: '- a
- b
- 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: '- should be 1
hello
- should be 1
- 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- b
- c
notlistfoo
',
- 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: '- should be 1
- should be 2
- 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;
-}