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