Index: trunk/extensions/VisualEditor/tests/ve/ve.dm.DocumentSynchronizer.test.js |
— | — | @@ -1,62 +1,159 @@ |
2 | 2 | module( 've/dm' ); |
3 | 3 | |
4 | | -test( 've.dm.TransactionSynchronizer', function() { |
5 | | - var model, |
6 | | - sync, |
7 | | - node, |
8 | | - data; |
| 4 | +test( 've.dm.TransactionSynchronizer', 9, function() { |
| 5 | + var tests = { |
| 6 | + // Test 1 |
| 7 | + 'resize actions adjust node lengths': { |
| 8 | + 'actual': function( sync ) { |
| 9 | + var model = sync.getModel(); |
| 10 | + // Delete bold "b" from first paragraph |
| 11 | + model.data.splice( 2, 1 ); |
| 12 | + // Push resize action |
| 13 | + sync.pushAction( 'resize', model.getChildren()[0], 0, -1 ); |
| 14 | + // Sync |
| 15 | + sync.synchronize(); |
| 16 | + return model.getChildren()[0].getContentLength(); |
| 17 | + }, |
| 18 | + 'expected': 2 |
| 19 | + }, |
| 20 | + // Test 2 |
| 21 | + 'insert actions can add new nodes in the middle': { |
| 22 | + 'actual': function( sync ) { |
| 23 | + var model = sync.getModel(), |
| 24 | + data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }], |
| 25 | + node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
| 26 | + // Insert element after first paragraph |
| 27 | + ve.insertIntoArray( model.data, 5, data ); |
| 28 | + // Push insertion action |
| 29 | + sync.pushAction( 'insert', node, 5 ); |
| 30 | + // Sync |
| 31 | + sync.synchronize(); |
| 32 | + return model.getChildren()[1].getContentData(); |
| 33 | + }, |
| 34 | + 'expected': ['x'] |
| 35 | + }, |
| 36 | + // Test 3 |
| 37 | + 'insert actions can add new nodes at the beginning': { |
| 38 | + 'actual': function( sync ) { |
| 39 | + var model = sync.getModel(), |
| 40 | + data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }], |
| 41 | + node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
| 42 | + // Insert element after first paragraph |
| 43 | + ve.insertIntoArray( model.data, 0, data ); |
| 44 | + // Push insertion action |
| 45 | + sync.pushAction( 'insert', node, 0 ); |
| 46 | + // Sync |
| 47 | + sync.synchronize(); |
| 48 | + return model.getChildren()[0].getContentData(); |
| 49 | + }, |
| 50 | + 'expected': ['x'] |
| 51 | + }, |
| 52 | + // Test 4 |
| 53 | + 'insert actions can add new nodes at the end': { |
| 54 | + 'actual': function( sync ) { |
| 55 | + var model = sync.getModel(), |
| 56 | + data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }], |
| 57 | + node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
| 58 | + // Insert element after first paragraph |
| 59 | + ve.insertIntoArray( model.data, 34, data ); |
| 60 | + // Push insertion action |
| 61 | + sync.pushAction( 'insert', node, 34 ); |
| 62 | + // Sync |
| 63 | + sync.synchronize(); |
| 64 | + return model.getChildren()[3].getContentData(); |
| 65 | + }, |
| 66 | + 'expected': ['x'] |
| 67 | + }, |
| 68 | + // Test 5 |
| 69 | + 'delete actions can remove nodes from the middle': { |
| 70 | + 'actual': function( sync ) { |
| 71 | + var model = sync.getModel(), |
| 72 | + node = model.getChildren()[1]; |
| 73 | + // Delete the table |
| 74 | + model.data.splice( 5, 26 ); |
| 75 | + // Push deletion action |
| 76 | + sync.pushAction( 'delete', node, 5 ); |
| 77 | + // Sync |
| 78 | + sync.synchronize(); |
| 79 | + return model.getChildren().length; |
| 80 | + }, |
| 81 | + 'expected': 2 |
| 82 | + }, |
| 83 | + // Test 6 |
| 84 | + 'delete actions can remove nodes from the beginning': { |
| 85 | + 'actual': function( sync ) { |
| 86 | + var model = sync.getModel(), |
| 87 | + node = model.getChildren()[0]; |
| 88 | + // Delete the first paragraph |
| 89 | + model.data.splice( 0, 5 ); |
| 90 | + // Push deletion action |
| 91 | + sync.pushAction( 'delete', node, 0 ); |
| 92 | + // Sync |
| 93 | + sync.synchronize(); |
| 94 | + return model.getChildren().length; |
| 95 | + }, |
| 96 | + 'expected': 2 |
| 97 | + }, |
| 98 | + // Test 7 |
| 99 | + 'delete actions can remove nodes from the end': { |
| 100 | + 'actual': function( sync ) { |
| 101 | + var model = sync.getModel(), |
| 102 | + node = model.getChildren()[2]; |
| 103 | + // Delete the first paragraph |
| 104 | + model.data.splice( 31, 3 ); |
| 105 | + // Push deletion action |
| 106 | + sync.pushAction( 'delete', node, 31 ); |
| 107 | + // Sync |
| 108 | + sync.synchronize(); |
| 109 | + return model.getChildren().length; |
| 110 | + }, |
| 111 | + 'expected': 2 |
| 112 | + }, |
| 113 | + // Test 8 |
| 114 | + 'rebuild actions can convert element types': { |
| 115 | + 'actual': function( sync ) { |
| 116 | + var model = sync.getModel(), |
| 117 | + node = model.getChildren()[0]; |
| 118 | + // Convert the first paragraph to a level 1 heading |
| 119 | + model.data[0].type = 'heading'; |
| 120 | + model.data[0].attributes = { 'level': 1 }; |
| 121 | + model.data[4].type = '/heading'; |
| 122 | + // Push rebuild action |
| 123 | + sync.pushAction( 'rebuild', node, 0 ); |
| 124 | + // Sync |
| 125 | + sync.synchronize(); |
| 126 | + return model.getChildren()[0].getElementType(); |
| 127 | + }, |
| 128 | + 'expected': 'heading' |
| 129 | + }, |
| 130 | + // Test 9 |
| 131 | + 'rebuild actions can replace one node with more than one node': { |
| 132 | + 'actual': function( sync ) { |
| 133 | + var model = sync.getModel(), |
| 134 | + node = model.getChildren()[0], |
| 135 | + data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }]; |
| 136 | + // Insert element after first paragraph |
| 137 | + ve.insertIntoArray( model.data, 5, data ); |
| 138 | + // Push rebuild action with a length adustment of 3 to account for the new element |
| 139 | + sync.pushAction( 'rebuild', node, 0, 3 ); |
| 140 | + // Sync |
| 141 | + sync.synchronize(); |
| 142 | + return model.getChildren()[1].getContentData(); |
| 143 | + }, |
| 144 | + 'expected': ['x'] |
| 145 | + } |
| 146 | + }; |
9 | 147 | |
10 | | - // Test 1 - node resizing |
11 | | - |
12 | | - model = ve.dm.DocumentNode.newFromPlainObject( veTest.obj ); |
13 | | - sync = new ve.dm.DocumentSynchronizer( model ); |
14 | | - // Delete bold "b" from first paragraph |
15 | | - model.data.splice( 2, 1 ); |
16 | | - // Push resize action |
17 | | - sync.pushAction( 'resize', model.getChildren()[0], 0, -1 ); |
18 | | - // Sync |
19 | | - sync.synchronize(); |
20 | | - equal( model.getChildren()[0].getContentLength(), 2, 'resize actions adjust node lengths' ); |
21 | | - |
22 | | - // Test 2 - node insertion (in the middle) |
23 | | - |
24 | | - model = ve.dm.DocumentNode.newFromPlainObject( veTest.obj ); |
25 | | - sync = new ve.dm.DocumentSynchronizer( model ); |
26 | | - // Insert element after first paragraph |
27 | | - data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }]; |
28 | | - node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
29 | | - ve.insertIntoArray( model.data, 5, data ); |
30 | | - // Push insertion action |
31 | | - sync.pushAction( 'insert', node, 5 ); |
32 | | - // Sync |
33 | | - sync.synchronize(); |
34 | | - deepEqual( model.getChildren()[1].getContentData(), ['x'], 'insert actions add new nodes' ); |
35 | | - |
36 | | - // Test 3 - node insertion (at the start) |
37 | | - |
38 | | - model = ve.dm.DocumentNode.newFromPlainObject( veTest.obj ); |
39 | | - sync = new ve.dm.DocumentSynchronizer( model ); |
40 | | - // Insert element after first paragraph |
41 | | - data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }]; |
42 | | - node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
43 | | - ve.insertIntoArray( model.data, 0, data ); |
44 | | - // Push insertion action |
45 | | - sync.pushAction( 'insert', node, 0 ); |
46 | | - // Sync |
47 | | - sync.synchronize(); |
48 | | - deepEqual( model.getChildren()[0].getContentData(), ['x'], 'insert actions add new nodes' ); |
49 | | - |
50 | | - // Test 4 - node insertion (at the end) |
51 | | - model = ve.dm.DocumentNode.newFromPlainObject( veTest.obj ); |
52 | | - sync = new ve.dm.DocumentSynchronizer( model ); |
53 | | - // Insert element after first paragraph |
54 | | - data = [{ 'type': 'paragraph' }, 'x', { 'type': '/paragraph' }]; |
55 | | - node = ve.dm.DocumentNode.createNodesFromData( data )[0]; |
56 | | - ve.insertIntoArray( model.data, 34, data ); |
57 | | - // Push insertion action |
58 | | - sync.pushAction( 'insert', node, 34 ); |
59 | | - // Sync |
60 | | - sync.synchronize(); |
61 | | - deepEqual( model.getChildren()[3].getContentData(), ['x'], 'insert actions add new nodes' ); |
| 148 | + // Run tests |
| 149 | + for ( var test in tests ) { |
| 150 | + deepEqual( |
| 151 | + tests[test].actual( |
| 152 | + new ve.dm.DocumentSynchronizer( |
| 153 | + ve.dm.DocumentNode.newFromPlainObject( veTest.obj ) |
| 154 | + ) |
| 155 | + ), |
| 156 | + tests[test].expected, |
| 157 | + test |
| 158 | + ); |
| 159 | + } |
62 | 160 | } ); |
63 | | - |
Index: trunk/extensions/VisualEditor/modules/ve/dm/ve.dm.DocumentSynchronizer.js |
— | — | @@ -15,6 +15,10 @@ |
16 | 16 | |
17 | 17 | /* Methods */ |
18 | 18 | |
| 19 | +ve.dm.DocumentSynchronizer.prototype.getModel = function() { |
| 20 | + return this.model; |
| 21 | +}; |
| 22 | + |
19 | 23 | /** |
20 | 24 | * Adds an action to the synchronizer. |
21 | 25 | * |
— | — | @@ -72,19 +76,23 @@ |
73 | 77 | case 'delete': |
74 | 78 | // Replace original node with new node |
75 | 79 | parent = action.node.getParent(); |
76 | | - parentNode.splice( parentNode.indexOf( action.node ), 1 ); |
| 80 | + parent.splice( parent.indexOf( action.node ), 1 ); |
77 | 81 | // Adjust proceeding offsets negatively by the length of the node being deleted |
78 | 82 | adjustment -= action.node.getElementLength(); |
79 | 83 | break; |
80 | 84 | case 'rebuild': |
81 | 85 | // Replace original node with new node |
82 | | - var newNode = ve.dm.DocumentNode.createNodesFromData( this.model.getData( |
| 86 | + var newNodes = ve.dm.DocumentNode.createNodesFromData( this.model.getData( |
83 | 87 | new ve.Range( offset, action.node.getElementLength() + action.adjustment ) |
84 | 88 | ) ); |
85 | 89 | parent = action.node.getParent(); |
86 | | - parentNode.splice( parentNode.indexOf( action.node ), 1, newNode ); |
| 90 | + parent.splice.apply( parent, [parent.indexOf( action.node ), 1].concat( newNodes ) ); |
87 | 91 | // Adjust proceeding offsets by the difference between the original and new nodes |
88 | | - adjustment += newNode.getElementLength() - action.node.getElementLength(); |
| 92 | + var newNodesLength = 0; |
| 93 | + for ( var j = 0, jlen = newNodes.length; j < jlen; j++ ) { |
| 94 | + newNodesLength += newNodes[j].getElementLength(); |
| 95 | + } |
| 96 | + adjustment += newNodesLength - action.node.getElementLength(); |
89 | 97 | break; |
90 | 98 | case 'resize': |
91 | 99 | // Adjust node length - causes update events to be emitted |