mediawiki-extensions-Visual.../modules/ve/test/dm/ve.dm.Document.test.js
Ed Sanders ff7b8a2591 Transactions to store metadata merge info when present
ve.dm.Transaction
* Replace operations are now built directly from the
  linear model and automatically determine what metadata
  replace information they need to include:
** retainMetadata
** replaceMetadata
** insertMetadata

ve.dm.Document
* Metadata array created empty and padded out after data parsing
  as we are no longer using Document.spliceData to build it (a new
  test checks for correct metadata length)
* spliceData replaced with getMetadataReplace, which instead returns
  transactional steps of spliceData (retain, replace, insert)

ve.dm.MetaLinearData
* Add function for merging metadata items together. Only used
  once in the code (Document.getMetadataReplace) but useful
  for generating test data.

ve.dm.MetaList
* Replace operations with metadata need to calculate new offset
  and indices directly, but can't be applied immediately lest they
  put a metaItem out of place and affect findItem.

ve.dm.MetaItem
* Add methods to support queued moves as required by MetaList

Test files
* Updated to match new pushReplace API
* Remove any instances of Document.spliceData
* Extra check on sparse metadata array length
* Rewrite spliceData tests as getMetadataReplace tests
* Count expected cases in Transaction(Processor) tests

Bug: 46954
Change-Id: I4edad1c2dd37c723bff2792bab7d694ef17a86dc
2013-04-16 22:26:56 +01:00

395 lines
12 KiB
JavaScript

/*!
* VisualEditor DataModel Document tests.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
QUnit.module( 've.dm.Document' );
/* Tests */
QUnit.test( 'constructor', 8, function ( assert ) {
var doc = ve.dm.example.createExampleDocument();
assert.equalNodeTree( doc.getDocumentNode(), ve.dm.example.tree, 'node tree matches example data' );
assert.throws(
function () {
doc = new ve.dm.Document( [
{ 'type': '/paragraph' },
{ 'type': 'paragraph' }
] );
},
Error,
'unbalanced input causes exception'
);
// TODO data provider?
doc = new ve.dm.Document( [ 'a', 'b', 'c', 'd' ] );
assert.equalNodeTree(
doc.getDocumentNode(),
new ve.dm.DocumentNode( [ new ve.dm.TextNode( 4 ) ] ),
'plain text input is handled correctly'
);
assert.deepEqual( doc.getMetadata(), new Array( 5 ),
'sparse metadata array is created'
);
doc = new ve.dm.Document( [ { 'type': 'paragraph' }, { 'type': '/paragraph' } ] );
assert.equalNodeTree(
doc.getDocumentNode(),
new ve.dm.DocumentNode( [ new ve.dm.ParagraphNode( [], { 'type': 'paragraph' } ) ] ),
'empty paragraph no longer has a text node'
);
doc = ve.dm.example.createExampleDocument( 'withMeta' );
assert.deepEqual( doc.getData(), ve.dm.example.withMetaPlainData,
'metadata is stripped out of the linear model'
);
assert.deepEqual( doc.getMetadata(), ve.dm.example.withMetaMetaData,
'metadata is put in the meta-linmod'
);
assert.equalNodeTree(
doc.getDocumentNode(),
new ve.dm.DocumentNode( [ new ve.dm.ParagraphNode(
[ new ve.dm.TextNode( 9 ) ], ve.dm.example.withMetaPlainData[0] ) ] ),
'node tree does not contain metadata'
);
} );
QUnit.test( 'getData', 1, function ( assert ) {
var doc = ve.dm.example.createExampleDocument(),
expectedData = ve.dm.example.preprocessAnnotations( ve.copyArray( ve.dm.example.data ) );
assert.deepEqual( doc.getData(), expectedData.getData() );
} );
QUnit.test( 'getFullData', 1, function ( assert ) {
var doc = ve.dm.example.createExampleDocument( 'withMeta' );
assert.deepEqual( doc.getFullData(), ve.dm.example.withMeta );
} );
QUnit.test( 'getMetadataReplace', 3, function ( assert ) {
var replace, expectedReplace,
doc = ve.dm.example.createExampleDocument( 'withMeta' );
replace = doc.getMetadataReplace( 10, 1, [] );
expectedReplace = {
'retain': 0,
'remove': doc.getMetadata().slice( 10, 12 ),
'insert': ve.dm.MetaLinearData.static.merge( doc.getMetadata().slice( 10, 12 ) )
};
assert.deepEqual( replace, expectedReplace, 'removing one element at offset 10' );
replace = doc.getMetadataReplace( 5, 2, [] );
expectedReplace = {
'retain': 0,
'remove': doc.getMetadata().slice( 5, 8 ),
'insert': ve.dm.MetaLinearData.static.merge( doc.getMetadata().slice( 5, 8 ) )
};
assert.deepEqual( replace, expectedReplace, 'removing two elements at offset 5' );
replace = doc.getMetadataReplace( 1, 8, [] );
expectedReplace = {
'retain': 0,
'remove': doc.getMetadata().slice( 1, 10 ),
'insert': ve.dm.MetaLinearData.static.merge( doc.getMetadata().slice( 1, 10 ) )
};
assert.deepEqual( replace, expectedReplace, 'blanking paragraph, removing 8 elements at offset 1' );
} );
QUnit.test( 'getNodeFromOffset', function ( assert ) {
var i, j, node,
doc = ve.dm.example.createExampleDocument(),
root = doc.getDocumentNode().getRoot(),
expected = [
[], // 0 - document
[0], // 1 - heading
[0], // 2 - heading
[0], // 3 - heading
[0], // 4 - heading
[], // 5 - document
[1], // 6 - table
[1, 0], // 7 - tableSection
[1, 0, 0], // 7 - tableRow
[1, 0, 0, 0], // 8 - tableCell
[1, 0, 0, 0, 0], // 9 - paragraph
[1, 0, 0, 0, 0], // 10 - paragraph
[1, 0, 0, 0], // 11 - tableCell
[1, 0, 0, 0, 1], // 12 - list
[1, 0, 0, 0, 1, 0], // 13 - listItem
[1, 0, 0, 0, 1, 0, 0], // 14 - paragraph
[1, 0, 0, 0, 1, 0, 0], // 15 - paragraph
[1, 0, 0, 0, 1, 0], // 16 - listItem
[1, 0, 0, 0, 1, 0, 1], // 17 - list
[1, 0, 0, 0, 1, 0, 1, 0], // 18 - listItem
[1, 0, 0, 0, 1, 0, 1, 0, 0], // 19 - paragraph
[1, 0, 0, 0, 1, 0, 1, 0, 0], // 20 - paragraph
[1, 0, 0, 0, 1, 0, 1, 0], // 21 - listItem
[1, 0, 0, 0, 1, 0, 1], // 22 - list
[1, 0, 0, 0, 1, 0], // 23 - listItem
[1, 0, 0, 0, 1], // 24 - list
[1, 0, 0, 0], // 25 - tableCell
[1, 0, 0, 0, 2], // 26 - list
[1, 0, 0, 0, 2, 0], // 27 - listItem
[1, 0, 0, 0, 2, 0, 0], // 28 - paragraph
[1, 0, 0, 0, 2, 0, 0], // 29 - paragraph
[1, 0, 0, 0, 2, 0], // 30 - listItem
[1, 0, 0, 0, 2], // 31 - list
[1, 0, 0, 0], // 32 - tableCell
[1, 0, 0], // 33 - tableRow
[1, 0], // 33 - tableSection
[1], // 34 - table
[], // 35- document
[2], // 36 - preformatted
[2], // 37 - preformatted
[2], // 38 - preformatted
[2], // 39 - preformatted
[2], // 40 - preformatted
[], // 41 - document
[3], // 42 - definitionList
[3, 0], // 43 - definitionListItem
[3, 0, 0], // 44 - paragraph
[3, 0, 0], // 45 - paragraph
[3, 0], // 46 - definitionListItem
[3], // 47 - definitionList
[3, 1], // 48 - definitionListItem
[3, 1, 0], // 49 - paragraph
[3, 1, 0], // 50 - paragraph
[3, 1], // 51 - definitionListItem
[3], // 52 - definitionList
[], // 53 - document
[4], // 54 - paragraph
[4], // 55 - paragraph
[], // 56 - document
[5], // 57 - paragraph
[5], // 58 - paragraph
[] // 59 - document
];
QUnit.expect( expected.length );
for ( i = 0; i < expected.length; i++ ) {
node = root;
for ( j = 0; j < expected[i].length; j++ ) {
node = node.children[expected[i][j]];
}
assert.ok( node === doc.getNodeFromOffset( i ), 'reference at offset ' + i );
}
} );
QUnit.test( 'getDataFromNode', 3, function ( assert ) {
var doc = ve.dm.example.createExampleDocument(),
expectedData = ve.dm.example.preprocessAnnotations( ve.copyArray( ve.dm.example.data ) );
assert.deepEqual(
doc.getDataFromNode( doc.getDocumentNode().getChildren()[0] ),
expectedData.slice( 1, 4 ),
'branch with leaf children'
);
assert.deepEqual(
doc.getDataFromNode( doc.getDocumentNode().getChildren()[1] ),
expectedData.slice( 6, 36 ),
'branch with branch children'
);
assert.deepEqual(
doc.getDataFromNode( doc.getDocumentNode().getChildren()[2].getChildren()[1] ),
[],
'leaf without children'
);
} );
QUnit.test( 'getOuterLength', 1, function ( assert ) {
var doc = ve.dm.example.createExampleDocument();
assert.strictEqual(
doc.getDocumentNode().getOuterLength(),
ve.dm.example.data.length,
'document does not have elements around it'
);
} );
QUnit.test( 'rebuildNodes', 2, function ( assert ) {
var tree,
doc = ve.dm.example.createExampleDocument(),
documentNode = doc.getDocumentNode();
// Rebuild table without changes
doc.rebuildNodes( documentNode, 1, 1, 5, 32 );
assert.equalNodeTree(
documentNode,
ve.dm.example.tree,
'rebuild without changes'
);
// XXX: Create a new document node tree from the old one
tree = new ve.dm.DocumentNode( ve.dm.example.tree.getChildren() );
// Replace table with paragraph
doc.data.batchSplice( 5, 32, [ { 'type': 'paragraph' }, 'a', 'b', 'c', { 'type': '/paragraph' } ] );
tree.splice( 1, 1, new ve.dm.ParagraphNode(
[new ve.dm.TextNode( 3 )], doc.data.getData( 5 )
) );
// Rebuild with changes
doc.rebuildNodes( documentNode, 1, 1, 5, 5 );
assert.equalNodeTree(
documentNode,
tree,
'replace table with paragraph'
);
} );
QUnit.test( 'selectNodes', 21, function ( assert ) {
var i,
doc = ve.dm.example.createExampleDocument(),
cases = ve.example.getSelectNodesCases( doc );
for ( i = 0; i < cases.length; i++ ) {
assert.equalNodeSelection( cases[i].actual, cases[i].expected, cases[i].msg );
}
} );
QUnit.test( 'getSlice', function ( assert ) {
var i, data, doc = ve.dm.example.createExampleDocument(),
cases = [
{
'msg': 'empty range',
'range': new ve.Range( 2, 2 ),
'expected': []
},
{
'msg': 'range with one character',
'range': new ve.Range( 2, 3 ),
'expected': [
['b', [ ve.dm.example.bold ]]
]
},
{
'msg': 'range with two characters',
'range': new ve.Range( 2, 4 ),
'expected': [
['b', [ ve.dm.example.bold ]],
['c', [ ve.dm.example.italic ]]
]
},
{
'msg': 'range with two characters and a header closing',
'range': new ve.Range( 2, 5 ),
'expected': [
{ 'type': 'heading', 'attributes': { 'level': 1 } },
['b', [ ve.dm.example.bold ]],
['c', [ ve.dm.example.italic ]],
{ 'type': '/heading' }
]
},
{
'msg': 'range with one character, a header closing and a table opening',
'range': new ve.Range( 3, 6 ),
'expected': [
{ 'type': 'heading', 'attributes': { 'level': 1 } },
['c', [ ve.dm.example.italic ]],
{ 'type': '/heading' },
{ 'type': 'table' },
{ 'type': '/table' }
]
},
{
'msg': 'range from a paragraph into a list',
'range': new ve.Range( 15, 21 ),
'expected': [
{ 'type': 'paragraph' },
'e',
{ 'type': '/paragraph' },
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'paragraph' },
'f',
{ 'type': '/paragraph' },
{ 'type': '/listItem' },
{ 'type': '/list' }
]
},
{
'msg': 'range from a paragraph inside a nested list into the next list',
'range': new ve.Range( 20, 27 ),
'expected': [
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'paragraph' },
'f',
{ 'type': '/paragraph' },
{ 'type': '/listItem' },
{ 'type': '/list' },
{ 'type': '/listItem' },
{ 'type': '/list' },
{ 'type': 'list', 'attributes': { 'style': 'number' } },
{ 'type': '/list' }
]
},
{
'msg': 'range from a paragraph inside a nested list out of both lists',
'range': new ve.Range( 20, 26 ),
'expected': [
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'paragraph' },
'f',
{ 'type': '/paragraph' },
{ 'type': '/listItem' },
{ 'type': '/list' },
{ 'type': '/listItem' },
{ 'type': '/list' }
]
},
{
'msg': 'range from a paragraph inside a nested list out of the outer listItem',
'range': new ve.Range( 20, 25 ),
'expected': [
{ 'type': 'listItem' },
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
{ 'type': 'listItem' },
{ 'type': 'paragraph' },
'f',
{ 'type': '/paragraph' },
{ 'type': '/listItem' },
{ 'type': '/list' },
{ 'type': '/listItem' }
]
}
];
QUnit.expect( cases.length );
for ( i = 0; i < cases.length; i++ ) {
data = ve.dm.example.preprocessAnnotations( cases[i].expected.slice(), doc.getStore() );
assert.deepEqual(
doc.getSlice( cases[i].range ).getBalancedData(),
data.getData(),
cases[i].msg
);
}
} );
QUnit.test( 'protection against double application of transactions', 3, function ( assert ) {
var tx = new ve.dm.Transaction(),
testDocument = new ve.dm.Document( ve.dm.example.data );
tx.pushRetain( 1 );
tx.pushReplace( testDocument, 1, 0, ['H', 'e', 'l', 'l', 'o' ] );
assert.throws(
function () {
testDocument.rollback( tx );
},
Error,
'exception thrown when trying to rollback an uncommitted transaction'
);
testDocument.commit( tx );
assert.throws(
function () {
testDocument.commit( tx );
},
Error,
'exception thrown when trying to commit an already-committed transaction'
);
testDocument.rollback( tx );
assert.throws(
function () {
testDocument.rollback( tx );
},
Error,
'exception thrown when trying to roll back a transaction that has already been rolled back'
);
} );