mediawiki-extensions-Visual.../modules/ve/test/dm/ve.dm.TransactionProcessor.test.js
Roan Kattouw bab7689feb Actually use op.retainMetadata in structural replace mode
It was being used correctly in the textual replace case, but it was
omitted in the structural replace case. This caused rare, insidious
metadata placement bugs that I haven't been able to reproduce outside
of newFromDocumentInsertion testing, but this might explain some of
the odd category bugs that have been reported.

Change-Id: I1424e482303853f285e4516a93c9609076648eff
2013-07-12 15:57:26 -07:00

403 lines
11 KiB
JavaScript

/*!
* VisualEditor DataModel TransactionProcessor tests.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
QUnit.module( 've.dm.TransactionProcessor' );
/* Tests */
QUnit.test( 'commit/rollback', function ( assert ) {
var i, originalData, originalDoc,
msg, testDoc, tx, expectedData, expectedDoc,
n = 0,
store = ve.dm.example.createExampleDocument().getStore(),
bold = ve.dm.example.createAnnotation( ve.dm.example.bold ),
italic = ve.dm.example.createAnnotation( ve.dm.example.italic ),
underline = ve.dm.example.createAnnotation( ve.dm.example.underline ),
metaElementInsert = {
'type': 'alienMeta',
'attributes': {
'style': 'comment',
'text': ' inline '
}
},
metaElementInsertClose = { 'type': '/alienMeta' },
cases = {
'no operations': {
'calls': [],
'expected': function () {}
},
'retaining': {
'calls': [['pushRetain', 38]],
'expected': function () {}
},
'annotating content': {
'calls': [
['pushRetain', 1],
['pushStartAnnotating', 'set', bold],
['pushRetain', 1],
['pushStopAnnotating', 'set', bold],
['pushRetain', 1],
['pushStartAnnotating', 'clear', italic],
['pushStartAnnotating', 'set', bold],
['pushStartAnnotating', 'set', underline],
['pushRetain', 1],
['pushStopAnnotating', 'clear', italic],
['pushStopAnnotating', 'set', bold],
['pushStopAnnotating', 'set', underline]
],
'expected': function ( data ) {
data[1] = ['a', store.indexes( [ bold ] )];
data[2] = ['b', store.indexes( [ bold ] )];
data[3] = ['c', store.indexes( [ bold, underline ] )];
}
},
'annotating content and leaf elements': {
'calls': [
['pushRetain', 38],
['pushStartAnnotating', 'set', bold],
['pushRetain', 4],
['pushStopAnnotating', 'set', bold]
],
'expected': function ( data ) {
data[38] = ['h', store.indexes( [ bold ] )];
data[39].annotations = store.indexes( [ bold ] );
data[41] = ['i', store.indexes( [ bold ] )];
}
},
'using an annotation method other than set or clear throws an exception': {
'calls': [
['pushStartAnnotating', 'invalid-method', bold],
['pushRetain', 1],
['pushStopAnnotating', 'invalid-method', bold]
],
'exception': Error
},
'annotating branch opening element throws an exception': {
'calls': [
['pushStartAnnotating', 'set', bold],
['pushRetain', 1],
['pushStopAnnotating', 'set', bold]
],
'exception': Error
},
'annotating branch closing element throws an exception': {
'calls': [
['pushRetain', 4],
['pushStartAnnotating', 'set', bold],
['pushRetain', 1],
['pushStopAnnotating', 'set', bold]
],
'exception': Error
},
'setting duplicate annotations throws an exception': {
'calls': [
['pushRetain', 2],
['pushStartAnnotating', 'set', bold],
['pushRetain', 1],
['pushStopAnnotating', 'set', bold]
],
'exception': Error
},
'removing non-existent annotations throws an exception': {
'calls': [
['pushRetain', 1],
['pushStartAnnotating', 'clear', bold],
['pushRetain', 1],
['pushStopAnnotating', 'clear', bold]
],
'exception': Error
},
'changing, removing and adding attributes': {
'calls': [
['pushReplaceElementAttribute', 'level', 1, 2],
['pushRetain', 12],
['pushReplaceElementAttribute', 'style', 'bullet', 'number'],
['pushReplaceElementAttribute', 'test', undefined, 'abcd'],
['pushRetain', 27],
['pushReplaceElementAttribute', 'src', ve.dm.example.imgSrc, undefined]
],
'expected': function ( data ) {
data[0].attributes.level = 2;
data[12].attributes.style = 'number';
data[12].attributes.test = 'abcd';
delete data[39].attributes.src;
}
},
'changing attributes on non-element data throws an exception': {
'calls': [
['pushRetain', 1],
['pushReplaceElementAttribute', 'foo', 23, 42]
],
'exception': Error
},
'inserting text': {
'calls': [
['pushRetain', 1],
['pushReplace', 1, 0, ['F', 'O', 'O']]
],
'expected': function ( data ) {
data.splice( 1, 0, 'F', 'O', 'O' );
}
},
'removing text': {
'calls': [
['pushRetain', 1],
['pushReplace', 1, 1, []]
],
'expected': function ( data ) {
data.splice( 1, 1 );
}
},
'replacing text': {
'calls': [
['pushRetain', 1],
['pushReplace', 1, 1, ['F', 'O', 'O']]
],
'expected': function ( data ) {
data.splice( 1, 1, 'F', 'O', 'O' );
}
},
'emptying text': {
'calls': [
['pushRetain', 10],
['pushReplace', 10, 1, []]
],
'expected': function ( data ) {
data.splice( 10, 1 );
}
},
'inserting mixed content': {
'calls': [
['pushRetain', 1],
['pushReplace', 1, 1, ['F', 'O', 'O', {'type':'image'}, {'type':'/image'}, 'B', 'A', 'R']]
],
'expected': function ( data ) {
data.splice( 1, 1, 'F', 'O', 'O', {'type':'image'}, {'type':'/image'}, 'B', 'A', 'R' );
}
},
'converting an element': {
'calls': [
['pushReplace', 0, 1, [{ 'type': 'paragraph' }]],
['pushRetain', 3],
['pushReplace', 4, 1, [{ 'type': '/paragraph' }]]
],
'expected': function ( data ) {
data[0].type = 'paragraph';
delete data[0].attributes;
data[4].type = '/paragraph';
}
},
'splitting an element': {
'calls': [
['pushRetain', 2],
[
'pushReplace', 2, 0,
[{ 'type': '/heading' }, { 'type': 'heading', 'attributes': { 'level': 1 } }]
]
],
'expected': function ( data ) {
data.splice(
2,
0,
{ 'type': '/heading' },
{ 'type': 'heading', 'attributes': { 'level': 1 } }
);
}
},
'merging an element': {
'calls': [
['pushRetain', 57],
['pushReplace', 57, 2, []]
],
'expected': function ( data ) {
data.splice( 57, 2 );
}
},
'stripping elements': {
'calls': [
['pushRetain', 3],
['pushReplace', 3, 1, []],
['pushRetain', 6],
['pushReplace', 10, 1, []]
],
'expected': function ( data ) {
data.splice( 10, 1 );
data.splice( 3, 1 );
}
},
'inserting text after alien node at the end': {
'data': [
{ 'type': 'paragraph' },
'a',
{ 'type': 'alienInline' },
{ 'type': '/alienInline' },
{ 'type': '/paragraph' }
],
'calls': [
['pushRetain', 4],
['pushReplace', 4, 0, ['b']]
],
'expected': function ( data ) {
data.splice( 4, 0, 'b' );
}
},
'inserting metadata element into existing element list': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 11 ],
['pushRetainMetadata', 2 ],
['pushReplaceMetadata', [], [ metaElementInsert ] ],
['pushRetainMetadata', 2 ],
['pushRetain', 1 ]
],
'expected': function ( data ) {
data.splice( 25, 0, metaElementInsert, metaElementInsertClose );
}
},
'inserting metadata element into empty list': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 3 ],
['pushReplaceMetadata', [], [ metaElementInsert ] ],
['pushRetain', 9 ]
],
'expected': function ( data ) {
data.splice( 7, 0, metaElementInsert, metaElementInsertClose );
}
},
'removing all metadata elements from a metadata list': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 11 ],
['pushReplaceMetadata', ve.dm.example.withMetaMetaData[11], [] ],
['pushRetain', 1 ]
],
'expected': function ( data ) {
data.splice( 21, 8 );
}
},
'removing some metadata elements from metadata list': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 11 ],
['pushRetainMetadata', 1 ],
['pushReplaceMetadata', ve.dm.example.withMetaMetaData[11].slice( 1, 3 ), [] ],
['pushRetainMetadata', 1 ],
['pushRetain', 1 ]
],
'expected': function ( data ) {
data.splice( 23, 4 );
}
},
'replacing metadata at end of list': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 11 ],
['pushRetainMetadata', 3 ],
['pushReplaceMetadata', [ ve.dm.example.withMetaMetaData[11][3] ], [ metaElementInsert ] ],
['pushRetain', 1 ]
],
'expected': function ( data ) {
data.splice( 27, 2, metaElementInsert, metaElementInsertClose );
}
},
'removing data from between metadata merges metadata': {
'data': ve.dm.example.withMeta,
'calls': [
['pushRetain', 7 ],
['pushReplace', 7, 2, []],
['pushRetain', 2 ]
],
'expected': function ( data ) {
data.splice( 15, 2 );
}
},
'structural replacement starting at an offset without metadata': {
'data': [
{ 'type': 'paragraph' },
'F',
{
'type': 'alienMeta',
'attributes': {
'domElements': $( '<!-- foo -->' ).toArray()
}
},
{ 'type': '/alienMeta' },
'o', 'o',
{ 'type': '/paragraph' }
],
'calls': [
['pushReplace', 0, 5, [ { 'type': 'table' }, { 'type': '/table' } ]]
],
'expected': function ( data ) {
data.splice( 0, 2, { 'type': 'table' }, { 'type': '/table' } );
data.splice( 4, 3 );
}
}
};
for ( msg in cases ) {
n += ( 'expected' in cases[msg] ) ? 4 : 1;
}
QUnit.expect( n );
// Run tests
for ( msg in cases ) {
// Generate original document
originalData = cases[msg].data || ve.dm.example.data;
originalDoc = new ve.dm.Document(
ve.dm.example.preprocessAnnotations( ve.copyArray( originalData ), store )
);
testDoc = new ve.dm.Document(
ve.dm.example.preprocessAnnotations( ve.copyArray( originalData ), store )
);
tx = new ve.dm.Transaction();
for ( i = 0; i < cases[msg].calls.length; i++ ) {
// pushReplace needs the document as its first argument
if ( cases[msg].calls[i][0] === 'pushReplace' ) {
cases[msg].calls[i].splice( 1, 0, testDoc );
}
tx[cases[msg].calls[i][0]].apply( tx, cases[msg].calls[i].slice( 1 ) );
}
if ( 'expected' in cases[msg] ) {
// Generate expected document
expectedData = ve.copyArray( originalData );
cases[msg].expected( expectedData );
expectedDoc = new ve.dm.Document(
ve.dm.example.preprocessAnnotations( expectedData, store )
);
// Commit
testDoc.commit( tx );
assert.deepEqualWithDomElements( testDoc.getFullData(), expectedDoc.getFullData(), 'commit (data): ' + msg );
assert.equalNodeTree(
testDoc.getDocumentNode(),
expectedDoc.getDocumentNode(),
'commit (tree): ' + msg
);
// Rollback
testDoc.rollback( tx );
assert.deepEqualWithDomElements( testDoc.getFullData(), originalDoc.getFullData(), 'rollback (data): ' + msg );
assert.equalNodeTree(
testDoc.getDocumentNode(),
originalDoc.getDocumentNode(),
'rollback (tree): ' + msg
);
} else if ( 'exception' in cases[msg] ) {
/*jshint loopfunc:true */
assert.throws(
function () {
testDoc.commit( tx );
},
cases[msg].exception,
'commit: ' + msg
);
}
}
} );