mediawiki-extensions-Visual.../modules/ve/test/dm/ve.dm.MetaList.test.js
Roan Kattouw 6772f92e70 Get rid of 'reversed' flag on transactions
The way we implemented undoing transactions was horrible. We'd process
the original transaction, but with a reversed=true flag. That meant we
had to keep track of the 'reversed' flag everywhere, and use ternaries
like insert = reversed ? op.remove : op.insert; all over the place to
access transaction operations. Redo then worked by reapplying the
transaction. We would verify that this was OK by tracking whether the
transaction was in an applied state or an undone state.

This commit makes it so every transaction can only be applied once. To
undo, you obtain a mirror image of the transaction with tx.reverse(),
then apply that. To redo, you clone the original transaction with
tx.clone() and apply that. All the code that had to use ternaries to
check whether the transaction was being applied in reverse or not is
gone now, because you can only apply a given transaction forwards,
never in reverse.

Bonus:
* Make ve.dm.Document's .completeHistory a simple array of
  transactions, rather than transaction/boolean pairs
* In the protection of double application test, clone the example
  document properly; it modified ve.dm.example.data, which was "fine"
  because it ran .commit() and .rollback() the same number of times

Change-Id: I3050c5430be4a12510f22e20853560b92acebb67
2013-10-02 19:37:08 -07:00

251 lines
9 KiB
JavaScript

/*!
* VisualEditor MetaList tests.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
QUnit.module( 've.dm.MetaList' );
/* Tests */
function assertItemsMatchMetadata( assert, metadata, list, msg, full ) {
var i, j, k = 0, items = list.getAllItems();
for ( i in metadata.getData() ) {
if ( ve.isArray( metadata.getData( i ) ) ) {
for ( j = 0; j < metadata.getData( i ).length; j++ ) {
assert.strictEqual( items[k].getOffset(), Number( i ), msg + ' (' + k + ': offset (' + i + ', ' + j + '))' );
assert.strictEqual( items[k].getIndex(), j, msg + ' (' + k + ': index(' + i + ', ' + j + '))' );
if ( full ) {
assert.strictEqual( items[k].getElement(), metadata.getData( i, j ), msg + ' (' + k + ': element(' + i + ', ' + j + '))' );
assert.strictEqual( items[k].getParentList(), list, msg + ' (' + k + ': parentList(' + i + ', ' + j + '))' );
}
k++;
}
}
}
assert.strictEqual( items.length, k, msg + ' (number of items)' );
}
QUnit.test( 'constructor', function ( assert ) {
var doc = ve.dm.example.createExampleDocument( 'withMeta' ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface ),
metadata = doc.metadata;
QUnit.expect( 4*metadata.getTotalDataLength() + 1 );
assertItemsMatchMetadata( assert, metadata, list, 'Constructor', true );
} );
QUnit.test( 'onTransact', function ( assert ) {
var i, j, surface, tx, list,
doc = ve.dm.example.createExampleDocument( 'withMeta' ),
comment = { 'type': 'alienMeta', 'attributes': { 'style': 'comment', 'text': 'onTransact test' } },
heading = { 'type': 'heading', 'attributes': { 'level': 2 } },
cases = [
{
// delta: 0
'calls': [
[ 'pushRetain', 1 ],
[ 'pushReplace', doc, 1, 0, [ 'Q', 'u', 'u', 'x' ] ],
[ 'pushRetain', 3 ],
[ 'pushReplace', doc, 4, 1, [] ],
[ 'pushRetain', 1 ],
[ 'pushReplace', doc, 6, 4, [ '!' ] ],
[ 'pushRetain', 2 ]
],
'msg': 'Transaction inserting, replacing and removing text'
},
{
'calls': [
[ 'pushRetain', 1 ],
[
'pushReplace', doc, 1, 9,
[ 'f', 'O', 'O', 'b', 'A', 'R', 'b', 'A', 'Z' ],
[
undefined,
[ ve.dm.example.withMetaMetaData[9][0], ve.dm.example.withMetaMetaData[7][0] ],
undefined, undefined, undefined, [ ve.dm.example.withMetaMetaData[4][0] ],
undefined, undefined, undefined
]
]
],
'msg': 'Transaction replacing text and metadata at the same time'
},
{
// delta: 0
'calls': [
[ 'pushRetainMetadata', 1 ],
[ 'pushReplaceMetadata', [], [ comment ] ],
[ 'pushRetain', 4 ],
[ 'pushReplaceMetadata', [ ve.dm.example.withMetaMetaData[4][0] ], [] ],
[ 'pushRetain', 3 ],
[ 'pushReplaceMetadata', [ ve.dm.example.withMetaMetaData[7][0] ], [ comment ] ],
[ 'pushRetain', 4 ],
[ 'pushRetainMetadata', 1 ],
[ 'pushReplaceMetadata', [ ve.dm.example.withMetaMetaData[11][1] ], [] ],
[ 'pushRetainMetadata', 1 ],
[ 'pushReplaceMetadata', [], [ comment ] ]
],
'msg': 'Transaction inserting, replacing and removing metadata'
},
{
// delta: 0
'calls': [
[ 'pushReplace', doc, 0, 1, [ heading ] ],
[ 'pushRetain', 9 ],
[ 'pushReplace', doc, 10, 1, [ { 'type': '/heading' } ] ]
],
'msg': 'Transaction converting paragraph to heading'
},
{
// delta: -9
'calls': [
[ 'pushRetain', 1 ],
[ 'pushReplace', doc, 1, 9, [] ],
[ 'pushRetain', 1 ]
],
'msg': 'Transaction blanking paragraph'
},
{
// delta: +11
'calls': [
[ 'pushRetain', 11 ],
[ 'pushReplace', doc, 11, 0, ve.dm.example.withMetaPlainData ]
],
'msg': 'Transaction adding second paragraph at the end'
},
{
// delta: -2
'calls': [
[ 'pushRetain', 1 ],
[ 'pushReplace', doc, 1, 7, [] ],
[ 'pushRetain', 1 ],
[ 'pushReplaceMetadata', [ ve.dm.example.withMetaMetaData[9][0] ], [] ],
[ 'pushRetain', 2 ],
// The two operations below have to be in this order because of bug 46138
[ 'pushReplace', doc, 11, 0, [ { 'type': 'paragraph' }, 'a', 'b', 'c', { 'type': '/paragraph' } ] ],
[ 'pushReplaceMetadata', [], [ comment ] ]
],
'msg': 'Transaction adding and removing text and metadata'
}
];
// HACK: This works because most transactions above don't change the document length, and the
// ones that do change it cancel out
QUnit.expect( cases.length*( 8*doc.metadata.getTotalDataLength() + 2 ) );
for ( i = 0; i < cases.length; i++ ) {
tx = new ve.dm.Transaction();
for ( j = 0; j < cases[i].calls.length; j++ ) {
tx[cases[i].calls[j][0]].apply( tx, cases[i].calls[j].slice( 1 ) );
}
doc = ve.dm.example.createExampleDocument( 'withMeta' );
surface = new ve.dm.Surface( doc );
list = new ve.dm.MetaList( surface );
// Test both the transaction-via-surface and transaction-via-document flows
surface.change( tx );
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg, true );
surface.change( tx.reversed() );
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg + ' (rollback)', true );
}
} );
QUnit.test( 'findItem', function ( assert ) {
var i, j, g, item, element, expectedElement, group, groupDesc, items, next,
groups = [ null ],
doc = ve.dm.example.createExampleDocument( 'withMeta' ),
surface = new ve.dm.Surface( doc ),
metadata = doc.metadata,
list = new ve.dm.MetaList( surface );
for ( i = 0; i < metadata.getLength(); i++ ) {
for ( j = 0; j < metadata.getDataLength( i ); j++ ) {
group = ve.dm.metaItemFactory.getGroup( metadata.getData( i, j ).type );
if ( ve.indexOf( group, groups ) === -1 ) {
groups.push( group );
}
}
}
QUnit.expect( 2*( metadata.getLength() + metadata.getTotalDataLength() )*groups.length );
for ( g = 0; g < groups.length; g++ ) {
groupDesc = groups[g] === null ? 'all items' : groups[g];
items = groups[g] === null ? list.items : list.groups[groups[g]];
next = 0;
for ( i = 0; i < metadata.getLength(); i++ ) {
for ( j = 0; j < metadata.getDataLength( i ); j++ ) {
item = list.findItem( i, j, groups[g] );
next = item !== null ? item + 1 : next;
element = item === null ? null : items[item].getElement();
expectedElement = metadata.getData( i, j );
if (
groups[g] !== null && expectedElement &&
ve.dm.metaItemFactory.getGroup( expectedElement.type ) !== groups[g]
) {
expectedElement = null;
}
assert.strictEqual( element, expectedElement, groupDesc + ' (' + i + ', ' + j + ')' );
assert.strictEqual( list.findItem( i, j, groups[g], true ), item !== null ? item : next,
groupDesc + ' (forInsertion) (' + i + ', ' + j + ')' );
}
assert.strictEqual( list.findItem( i, j, groups[g] ), null, groupDesc + ' (' + i + ', ' + j + ')' );
assert.strictEqual( list.findItem( i, j, groups[g], true ), next, groupDesc + ' (forInsertion) (' + i + ', ' + j + ')' );
}
}
} );
QUnit.test( 'insertMeta', 5, function ( assert ) {
var expected,
doc = ve.dm.example.createExampleDocument( 'withMeta' ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface ),
insert = {
'type': 'alienMeta',
'attributes': {
'style': 'comment',
'text': 'insertMeta test'
}
};
list.insertMeta( insert, 2, 0 );
assert.deepEqual( doc.metadata.getData( 2 ), [ insert ], 'Inserting metadata at an offset without pre-existing metadata' );
expected = doc.metadata.getData( 0 ).slice( 0 );
expected.splice( 1, 0, insert );
list.insertMeta( insert, 0, 1 );
assert.deepEqual( doc.metadata.getData( 0 ), expected, 'Inserting metadata in the middle' );
expected.push( insert );
list.insertMeta( insert, 0 );
assert.deepEqual( doc.metadata.getData( 0 ), expected, 'Inserting metadata without passing an index adds to the end' );
list.insertMeta( insert, 1 );
assert.deepEqual( doc.metadata.getData( 1 ), [ insert ], 'Inserting metadata without passing an index without pre-existing metadata' );
list.insertMeta( new ve.dm.AlienMetaItem( insert ), 1 );
assert.deepEqual( doc.metadata.getData( 1 ), [ insert, insert ], 'Passing a MetaItem rather than an element' );
} );
QUnit.test( 'removeMeta', 4, function ( assert ) {
var expected,
doc = ve.dm.example.createExampleDocument( 'withMeta' ),
surface = new ve.dm.Surface( doc ),
list = new ve.dm.MetaList( surface );
list.removeMeta( list.getItemAt( 4, 0 ) );
assert.deepEqual( doc.metadata.getData( 4 ), [], 'Removing the only item at offset 4' );
expected = doc.metadata.getData( 0 ).slice( 0 );
expected.splice( 1, 1 );
list.removeMeta( list.getItemAt( 0, 1 ) );
assert.deepEqual( doc.metadata.getData( 0 ), expected, 'Removing the item at (0,1)' );
expected = doc.metadata.getData( 11 ).slice( 0 );
expected.splice( 0, 1 );
list.getItemAt( 11, 0 ).remove();
assert.deepEqual( doc.metadata.getData( 11 ), expected, 'Removing (11,0) using .remove()' );
expected.splice( 1, 1 );
list.getItemAt( 11, 1 ).remove();
assert.deepEqual( doc.metadata.getData( 11 ), expected, 'Removing (11,1) (formerly (11,2)) using .remove()' );
} );