1<?php 2namespace spec\Parsoid\Utils; 3 4use PHPUnit\Framework\TestCase; 5use Wikimedia\Parsoid\DOM\Document; 6use Wikimedia\Parsoid\DOM\Element; 7use Wikimedia\Parsoid\Html2Wt\DOMDiff; 8use Wikimedia\Parsoid\Mocks\MockEnv; 9use Wikimedia\Parsoid\Utils\ContentUtils; 10use Wikimedia\Parsoid\Utils\DOMCompat; 11use Wikimedia\Parsoid\Utils\DOMDataUtils; 12use Wikimedia\Parsoid\Utils\DOMUtils; 13 14/** 15 * Based on tests/mocha/domdiff.js 16 * @coversDefaultClass \Wikimedia\Parsoid\Utils\DOMUtils 17 */ 18class DOMUtilsTest extends TestCase { 19 20 /** @var Document[] */ 21 private $liveDocs = []; 22 23 /** 24 * @covers ::isDiffMarker 25 */ 26 public function testIsDiffMarker_changingTextInNode() { 27 $orig = '<p>a</p><p>b</p>'; 28 $edit = '<p>A</p><p>b</p>'; 29 30 $body = $this->parseAndDiff( $orig, $edit ); 31 32 $this->checkMarkers( 33 $this->selectNode( $body, 'body > p:first-child' ), 34 [ 'children-changed', 'subtree-changed' ] 35 ); 36 37 $this->assertTrue( DOMUtils::isDiffMarker( 38 $this->selectNode( $body, 'body > p:first-child > meta:first-child' ), 39 'deleted' 40 ) ); 41 } 42 43 /** 44 * @covers ::isDiffMarker 45 */ 46 public function testIsDiffMarker_deletingNode() { 47 $orig = '<p>a</p><p>b</p>'; 48 $edit = '<p>a</p>'; 49 50 $body = $this->parseAndDiff( $orig, $edit ); 51 52 $this->checkMarkers( $body, [ 'children-changed' ] ); 53 54 $this->assertTrue( DOMUtils::isDiffMarker( 55 $this->selectNode( $body, 'body > p + meta' ), 56 'deleted' 57 ) ); 58 } 59 60 /** 61 * @covers ::isDiffMarker 62 */ 63 public function testIsDiffMarker_reorderingNodes() { 64 $orig = '<p>a</p><p>b</p>'; 65 $edit = '<p>b</p><p>a</p>'; 66 67 $body = $this->parseAndDiff( $orig, $edit ); 68 69 $this->checkMarkers( 70 $this->selectNode( $body, 'body > p:nth-child(1)' ), 71 [ 'children-changed', 'subtree-changed' ] 72 ); 73 74 $this->assertTrue( DOMUtils::isDiffMarker( 75 $this->selectNode( $body, 'body > p:nth-child(1) > meta' ), 76 'deleted' 77 ) ); 78 79 $this->checkMarkers( 80 $this->selectNode( $body, 'body > p:nth-child(2)' ), 81 [ 'children-changed', 'subtree-changed' ] 82 ); 83 84 $this->assertTrue( DOMUtils::isDiffMarker( 85 $this->selectNode( $body, 'body > p:nth-child(2) > meta' ), 86 'deleted' 87 ) ); 88 } 89 90 /** 91 * @covers ::isDiffMarker 92 */ 93 public function testIsDiffMarker_addingMultipleNodes() { 94 $orig = '<p>a</p>'; 95 $edit = '<p>x</p><p>a</p><p>y</p>'; 96 97 $body = $this->parseAndDiff( $orig, $edit ); 98 99 $this->checkMarkers( $body, [ 'children-changed' ] ); 100 101 $this->checkMarkers( 102 $this->selectNode( $body, 'body > p:nth-child(1)' ), 103 [ 'children-changed', 'subtree-changed' ] 104 ); 105 106 $this->assertTrue( DOMUtils::isDiffMarker( 107 $this->selectNode( $body, 'body > p:nth-child(1) > meta' ), 108 'deleted' 109 ) ); 110 111 $this->checkMarkers( 112 $this->selectNode( $body, 'body > p:nth-child(2)' ), 113 [ 'inserted' ] 114 ); 115 116 $this->checkMarkers( 117 $this->selectNode( $body, 'body > p:nth-child(3)' ), 118 [ 'inserted' ] 119 ); 120 } 121 122 /** 123 * @covers ::isDiffMarker 124 */ 125 public function testIsDiffMarker_addingAndDeletingNodes() { 126 $orig = '<p>a</p><p>b</p><p>c</p>'; 127 $edit = '<p>x</p><p>b</p>'; 128 129 $body = $this->parseAndDiff( $orig, $edit ); 130 131 $this->checkMarkers( $body, [ 'children-changed' ] ); 132 133 $this->checkMarkers( 134 $this->selectNode( $body, 'body > p:nth-child(1)' ), 135 [ 'children-changed', 'subtree-changed' ] 136 ); 137 138 $this->assertTrue( DOMUtils::isDiffMarker( 139 $this->selectNode( $body, 'body > p:nth-child(1) > meta' ), 140 'deleted' 141 ) ); 142 143 $this->assertTrue( DOMUtils::isDiffMarker( 144 $this->selectNode( $body, 'body > meta:nth-child(3)' ), 145 'deleted' 146 ) ); 147 } 148 149 /** 150 * @coversNothing 151 */ 152 public function testSomething_changingAttribute() { 153 $orig = '<p class="a">a</p><p class="b">b</p>'; 154 $edit = '<p class="X">a</p><p class="b">b</p>'; 155 156 $body = $this->parseAndDiff( $orig, $edit ); 157 158 $this->checkMarkers( $body, [ 'children-changed' ] ); 159 160 $this->checkMarkers( 161 $this->selectNode( $body, 'body > p:nth-child(1)' ), 162 [ 'modified-wrapper' ] 163 ); 164 165 $this->expectNotToPerformAssertions(); 166 } 167 168 /** 169 * @coversNothing 170 */ 171 public function testSomething_changingDataMwForTemplate() { 172 $orig = '<p about="#mwt1" typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":' . 173 '{"target":{"wt":"1x","href":"./Template:1x"},"params":{"1":{"wt":"a"}},"i":0}}]}\'>a</p>'; 174 $edit = '<p about="#mwt1" typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":' . 175 '{"target":{"wt":"1x","href":"./Template:1x"},"params":{"1":{"wt":"foo"}},"i":0}}]}\'>foo</p>'; 176 177 $body = $this->parseAndDiff( $orig, $edit ); 178 179 $this->checkMarkers( $body, [ 'children-changed' ] ); 180 181 $this->checkMarkers( 182 $this->selectNode( $body, 'body > p:nth-child(1)' ), 183 [ 'modified-wrapper' ] 184 ); 185 186 $this->expectNotToPerformAssertions(); 187 } 188 189 /** 190 * @coversNothing 191 * The additional subtrees added to the template's content should simply be ignored 192 */ 193 public function testSomething_addingAdditionalDomTreesToTemplatedContent() { 194 $orig = '<p about="#mwt1" typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":' . 195 '{"target":{"wt":"1x","href":"./Template:1x"},"params":' . 196 '{"1":{"wt":"a"}},"i":0}}]}\'>a</p>'; 197 $edit = '<p about="#mwt1" typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":' . 198 '{"target":{"wt":"1x","href":"./Template:1x"},"params":' . 199 '{"1":{"wt":"foo\n\nbar\n\nbaz"}},"i":0}}]}\'>foo</p>' . 200 '<p about="#mwt1">bar</p><p about="#mwt1">baz</p>'; 201 202 $body = $this->parseAndDiff( $orig, $edit ); 203 204 $this->checkMarkers( $body, [ 'children-changed' ] ); 205 206 $this->checkMarkers( 207 $this->selectNode( $body, 'body > p:nth-child(1)' ), 208 [ 'modified-wrapper' ] 209 ); 210 211 $this->expectNotToPerformAssertions(); 212 } 213 214 /** 215 * @param string $html1 216 * @param string $html2 217 * @return Element 218 */ 219 private function parseAndDiff( string $html1, string $html2 ): Element { 220 $mockEnv = new MockEnv( [] ); 221 222 $doc1 = ContentUtils::createAndLoadDocument( $html1 ); 223 $doc2 = ContentUtils::createAndLoadDocument( $html2 ); 224 225 $body1 = DOMCompat::getBody( $doc1 ); 226 $body2 = DOMCompat::getBody( $doc2 ); 227 228 $domDiff = new DOMDiff( $mockEnv ); 229 $domDiff->diff( $body1, $body2 ); 230 231 // Prevent GC from reclaiming doc2 once we exit this function. 232 // Necessary hack because we use PHPDOM which wraps libxml. 233 $this->liveDocs[] = $doc2; 234 235 return DOMCompat::getBody( $doc2 ); 236 } 237 238 private function selectNode( Element $body, string $selector ): Element { 239 $nodes = DOMCompat::querySelectorAll( $body, $selector ); 240 if ( count( $nodes ) !== 1 ) { 241 $this->fail( 'It should be exactly one node for the selector' ); 242 } 243 return $nodes[0]; 244 } 245 246 private function checkMarkers( Element $node, array $markers ): void { 247 $data = DOMDataUtils::getNodeData( $node ); 248 $diff = $data->parsoid_diff->diff; 249 if ( count( $markers ) !== count( $diff ) ) { 250 var_dump( $markers ); 251 var_dump( $diff ); 252 $this->fail( 'Count of diff should be equal count of markers' ); 253 } 254 foreach ( $markers as $key => $value ) { 255 if ( $diff[$key] !== $value ) { 256 $this->fail( 'Diff is not equal to the marker' ); 257 } 258 } 259 } 260 261} 262