Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / parser / ParserOutputTest.php
blob56f6cdcddb353aab6237add63c84e1b1db760b1a
1 <?php
3 namespace MediaWiki\Tests\Parser;
5 use LogicException;
6 use MediaWiki\Context\RequestContext;
7 use MediaWiki\Debug\MWDebug;
8 use MediaWiki\MainConfigNames;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Parser\ParserOptions;
11 use MediaWiki\Parser\ParserOutput;
12 use MediaWiki\Parser\ParserOutputFlags;
13 use MediaWiki\Parser\ParserOutputLinkTypes;
14 use MediaWiki\Parser\ParserOutputStringSets;
15 use MediaWiki\Title\Title;
16 use MediaWiki\Title\TitleValue;
17 use MediaWiki\Utils\MWTimestamp;
18 use MediaWikiLangTestCase;
19 use Wikimedia\Bcp47Code\Bcp47CodeValue;
20 use Wikimedia\Parsoid\Core\SectionMetadata;
21 use Wikimedia\Parsoid\Core\TOCData;
22 use Wikimedia\TestingAccessWrapper;
23 use Wikimedia\Tests\SerializationTestTrait;
25 /**
26 * @covers \MediaWiki\Parser\ParserOutput
27 * @covers \Mediawiki\Parser\CacheTime
28 * @group Database
29 * ^--- trigger DB shadowing because we are using Title magic
31 class ParserOutputTest extends MediaWikiLangTestCase {
32 use SerializationTestTrait;
34 protected function setUp(): void {
35 parent::setUp();
37 MWTimestamp::setFakeTime( ParserCacheSerializationTestCases::FAKE_TIME );
38 $this->overrideConfigValue(
39 MainConfigNames::ParserCacheExpireTime,
40 ParserCacheSerializationTestCases::FAKE_CACHE_EXPIRY
42 $this->overrideConfigValue(
43 MainConfigNames::ParserCacheAsyncExpireTime,
44 ParserCacheSerializationTestCases::FAKE_ASYNC_CACHE_EXPIRY
48 public static function getClassToTest(): string {
49 return ParserOutput::class;
52 public static function getSerializedDataPath(): string {
53 return __DIR__ . '/../../data/ParserCache';
56 public static function getTestInstancesAndAssertions(): array {
57 return ParserCacheSerializationTestCases::getParserOutputTestCases();
60 public static function getSupportedSerializationFormats(): array {
61 return ParserCacheSerializationTestCases::getSupportedSerializationFormats(
62 self::getClassToTest() );
65 public static function provideIsLinkInternal() {
66 return [
67 // Different domains
68 [ false, 'http://example.org', 'http://mediawiki.org' ],
69 // Same domains
70 [ true, 'http://example.org', 'http://example.org' ],
71 [ true, 'https://example.org', 'https://example.org' ],
72 [ true, '//example.org', '//example.org' ],
73 // Same domain different cases
74 [ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
75 // Paths, queries, and fragments are not relevant
76 [ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
77 [ true, 'http://example.org', 'http://example.org?my=query' ],
78 [ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
79 // Different protocols
80 [ false, 'http://example.org', 'https://example.org' ],
81 [ false, 'https://example.org', 'http://example.org' ],
82 // Protocol relative servers always match http and https links
83 [ true, '//example.org', 'http://example.org' ],
84 [ true, '//example.org', 'https://example.org' ],
85 // But they don't match strange things like this
86 [ false, '//example.org', 'irc://example.org' ],
90 /**
91 * Test to make sure ParserOutput::isLinkInternal behaves properly
92 * @dataProvider provideIsLinkInternal
93 * @covers \MediaWiki\Parser\ParserOutput::isLinkInternal
95 public function testIsLinkInternal( $shouldMatch, $server, $url ) {
96 $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
99 /**
100 * @covers \MediaWiki\Parser\ParserOutput::appendJsConfigVar
101 * @covers \MediaWiki\Parser\ParserOutput::setJsConfigVar
102 * @covers \MediaWiki\Parser\ParserOutput::getJsConfigVars
104 public function testJsConfigVars() {
105 $po = new ParserOutput();
107 $po->setJsConfigVar( 'a', '1' );
108 $po->appendJsConfigVar( 'b', 'a' );
109 $po->appendJsConfigVar( 'b', '0' );
111 $this->assertEqualsCanonicalizing( [
112 'a' => 1,
113 'b' => [ 'a' => true, '0' => true ],
114 ], $po->getJsConfigVars() );
116 $po->setJsConfigVar( 'c', '2' );
117 $po->appendJsConfigVar( 'b', 'b' );
118 $po->appendJsConfigVar( 'b', '1' );
120 $this->assertEqualsCanonicalizing( [
121 'a' => 1,
122 'b' => [ 'a' => true, 'b' => true, '0' => true, '1' => true ],
123 'c' => 2,
124 ], $po->getJsConfigVars() );
128 * @covers \MediaWiki\Parser\ParserOutput::appendExtensionData
129 * @covers \MediaWiki\Parser\ParserOutput::setExtensionData
130 * @covers \MediaWiki\Parser\ParserOutput::getExtensionData
132 public function testExtensionData() {
133 $po = new ParserOutput();
135 $po->setExtensionData( "one", "Foo" );
136 $po->appendExtensionData( "three", "abc" );
138 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
139 $this->assertNull( $po->getExtensionData( "spam" ) );
141 $po->setExtensionData( "two", "Bar" );
142 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
143 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
145 // Note that overwriting extension data (as this test case
146 // does) is deprecated and will eventually throw an
147 // exception. However, at the moment it is still worth testing
148 // this case to ensure backward compatibility. (T300981)
149 $po->setExtensionData( "one", null );
150 $this->assertNull( $po->getExtensionData( "one" ) );
151 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
153 $this->assertEqualsCanonicalizing( [
154 'abc' => true,
155 ], $po->getExtensionData( "three" ) );
157 $po->appendExtensionData( "three", "xyz" );
158 $this->assertEqualsCanonicalizing( [
159 'abc' => true,
160 'xyz' => true,
161 ], $po->getExtensionData( "three" ) );
165 * @covers \MediaWiki\Parser\ParserOutput::setPageProperty
166 * @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
167 * @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
168 * @covers \MediaWiki\Parser\ParserOutput::getPageProperty
169 * @covers \MediaWiki\Parser\ParserOutput::unsetPageProperty
170 * @covers \MediaWiki\Parser\ParserOutput::getPageProperties
171 * @dataProvider providePageProperties
173 public function testPageProperties( string $setPageProperty, $value1, $value2, bool $expectDeprecation = false ) {
174 $po = new ParserOutput();
175 if ( $expectDeprecation ) {
176 MWDebug::filterDeprecationForTest( '/::setPageProperty with non-string value/' );
179 $po->$setPageProperty( 'foo', $value1 );
181 $properties = $po->getPageProperties();
182 $this->assertSame( $value1, $po->getPageProperty( 'foo' ) );
183 $this->assertSame( $value1, $properties['foo'] );
185 $po->$setPageProperty( 'foo', $value2 );
187 $properties = $po->getPageProperties();
188 $this->assertSame( $value2, $po->getPageProperty( 'foo' ) );
189 $this->assertSame( $value2, $properties['foo'] );
191 $po->unsetPageProperty( 'foo' );
193 $properties = $po->getPageProperties();
194 $this->assertSame( null, $po->getPageProperty( 'foo' ) );
195 $this->assertArrayNotHasKey( 'foo', $properties );
198 public static function providePageProperties() {
199 yield 'Unsorted' => [ 'setUnsortedPageProperty', 'val', 'second val' ];
200 yield 'Numeric' => [ 'setNumericPageProperty', 42, 3.14 ];
201 yield 'Unsorted (old style)' => [ 'setPageProperty', 'val', 'second val' ];
202 yield 'Numeric (old style)' => [ 'setPageProperty', 123, 456, true ];
206 * @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
208 public function testNumericPageProperties() {
209 $po = new ParserOutput();
211 $po->setNumericPageProperty( 'foo', '123' );
213 $properties = $po->getPageProperties();
214 $this->assertSame( 123, $po->getPageProperty( 'foo' ) );
215 $this->assertSame( 123, $properties['foo'] );
219 * @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
221 public function testUnsortedPageProperties() {
222 $po = new ParserOutput();
224 $po->setUnsortedPageProperty( 'foo', 123 );
226 $properties = $po->getPageProperties();
227 $this->assertSame( '123', $po->getPageProperty( 'foo' ) );
228 $this->assertSame( '123', $properties['foo'] );
232 * @covers \MediaWiki\Parser\ParserOutput::setLanguage
233 * @covers \MediaWiki\Parser\ParserOutput::getLanguage
235 public function testLanguage() {
236 $po = new ParserOutput();
238 $langFr = new Bcp47CodeValue( 'fr' );
239 $langCrhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
241 // Fallback to null
242 $this->assertSame( null, $po->getLanguage() );
244 // Simple case
245 $po->setLanguage( $langFr );
246 $this->assertSame( $langFr->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
248 // Language with a variant
249 $po->setLanguage( $langCrhCyrl );
250 $this->assertSame( $langCrhCyrl->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
254 * @covers \MediaWiki\Parser\ParserOutput::getWrapperDivClass
255 * @covers \MediaWiki\Parser\ParserOutput::addWrapperDivClass
256 * @covers \MediaWiki\Parser\ParserOutput::clearWrapperDivClass
258 public function testWrapperDivClass() {
259 $po = new ParserOutput();
260 $opts = ParserOptions::newFromAnon();
261 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
263 $po->setRawText( 'Kittens' );
264 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
265 $this->assertStringContainsString( 'Kittens', $text );
266 $this->assertStringNotContainsString( '<div', $text );
267 $this->assertSame( 'Kittens', $po->getRawText() );
269 $po->addWrapperDivClass( 'foo' );
270 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
271 $this->assertStringContainsString( 'Kittens', $text );
272 $this->assertStringContainsString( '<div', $text );
273 $this->assertStringContainsString( 'class="mw-content-ltr foo"', $text );
275 $po->addWrapperDivClass( 'bar' );
276 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
277 $this->assertStringContainsString( 'Kittens', $text );
278 $this->assertStringContainsString( '<div', $text );
279 $this->assertStringContainsString( 'class="mw-content-ltr foo bar"', $text );
281 $po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
282 $text = $pipeline->run( $po, $opts, [ 'unwrap' => true ] )->getContentHolderText();
283 $this->assertStringContainsString( 'Kittens', $text );
284 $this->assertStringNotContainsString( '<div', $text );
285 $this->assertStringNotContainsString( 'class="', $text );
287 $text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => '' ] )->getContentHolderText();
288 $this->assertStringContainsString( 'Kittens', $text );
289 $this->assertStringNotContainsString( '<div', $text );
290 $this->assertStringNotContainsString( 'class="', $text );
292 $text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => 'xyzzy' ] )->getContentHolderText();
293 $this->assertStringContainsString( 'Kittens', $text );
294 $this->assertStringContainsString( '<div', $text );
295 $this->assertStringContainsString( 'class="mw-content-ltr xyzzy"', $text );
296 $this->assertStringNotContainsString( 'foo bar', $text );
298 $text = $po->getRawText();
299 $this->assertSame( 'Kittens', $text );
301 $po->clearWrapperDivClass();
302 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
303 $this->assertStringContainsString( 'Kittens', $text );
304 $this->assertStringNotContainsString( '<div', $text );
305 $this->assertStringNotContainsString( 'class="', $text );
309 * This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
310 * ParserOutput::getText gets deprecated.
311 * @covers \MediaWiki\Parser\ParserOutput::getText
312 * @dataProvider provideGetText
313 * @param array $options Options to getText()
314 * @param string $text Parser text
315 * @param string $expect Expected output
317 public function testGetText( $options, $text, $expect ) {
318 // Avoid other skins affecting the section edit links
319 $this->overrideConfigValue( MainConfigNames::DefaultSkin, 'fallback' );
320 RequestContext::resetMain();
322 $this->overrideConfigValues( [
323 MainConfigNames::ScriptPath => '/w',
324 MainConfigNames::Script => '/w/index.php',
325 ] );
327 $po = new ParserOutput( $text );
328 self::initSections( $po );
329 $actual = $po->getText( $options );
330 $this->assertSame( $expect, $actual );
333 private static function initSections( ParserOutput $po ): void {
334 $po->setTOCData( new TOCData(
335 SectionMetadata::fromLegacy( [
336 'index' => "1",
337 'level' => 1,
338 'toclevel' => 1,
339 'number' => "1",
340 'line' => "Section 1",
341 'anchor' => "Section_1"
342 ] ),
343 SectionMetadata::fromLegacy( [
344 'index' => "2",
345 'level' => 1,
346 'toclevel' => 1,
347 'number' => "2",
348 'line' => "Section 2",
349 'anchor' => "Section_2"
350 ] ),
351 SectionMetadata::fromLegacy( [
352 'index' => "3",
353 'level' => 2,
354 'toclevel' => 2,
355 'number' => "2.1",
356 'line' => "Section 2.1",
357 'anchor' => "Section_2.1"
358 ] ),
359 SectionMetadata::fromLegacy( [
360 'index' => "4",
361 'level' => 1,
362 'toclevel' => 1,
363 'number' => "3",
364 'line' => "Section 3",
365 'anchor' => "Section_3"
366 ] ),
367 ) );
370 public static function provideGetText() {
371 $text = <<<EOF
372 <p>Test document.
373 </p>
374 <meta property="mw:PageProp/toc" />
375 <div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></div>
376 <p>One
377 </p>
378 <div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></div>
379 <p>Two
380 </p>
381 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
382 <p>Two point one
383 </p>
384 <div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></div>
385 <p>Three
386 </p>
387 EOF;
389 $dedupText = <<<EOF
390 <p>This is a test document.</p>
391 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
392 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
393 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
394 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
395 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
396 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
397 <style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
398 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
399 <style>.Duplicate1 {}</style>
400 EOF;
402 return [
403 'No options' => [
404 [], $text, <<<EOF
405 <p>Test document.
406 </p>
407 <div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none" /><div class="toctitle" lang="en" dir="ltr"><h2 id="mw-toc-heading">Contents</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
408 <ul>
409 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
410 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
411 <ul>
412 <li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
413 </ul>
414 </li>
415 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
416 </ul>
417 </div>
419 <div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></div>
420 <p>One
421 </p>
422 <div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></div>
423 <p>Two
424 </p>
425 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
426 <p>Two point one
427 </p>
428 <div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></div>
429 <p>Three
430 </p>
433 'Disable section edit links' => [
434 [ 'enableSectionEditLinks' => false ], $text, <<<EOF
435 <p>Test document.
436 </p>
437 <div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none" /><div class="toctitle" lang="en" dir="ltr"><h2 id="mw-toc-heading">Contents</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
438 <ul>
439 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
440 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
441 <ul>
442 <li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
443 </ul>
444 </li>
445 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
446 </ul>
447 </div>
449 <div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2></div>
450 <p>One
451 </p>
452 <div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2></div>
453 <p>Two
454 </p>
455 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
456 <p>Two point one
457 </p>
458 <div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2></div>
459 <p>Three
460 </p>
463 'Disable TOC, but wrap' => [
464 [ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
465 <div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"><p>Test document.
466 </p>
468 <div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></div>
469 <p>One
470 </p>
471 <div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></div>
472 <p>Two
473 </p>
474 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
475 <p>Two point one
476 </p>
477 <div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></div>
478 <p>Three
479 </p></div>
482 'Style deduplication' => [
483 [], $dedupText, <<<EOF
484 <p>This is a test document.</p>
485 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
486 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
487 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
488 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
489 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2">
490 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
491 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
492 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
493 <style>.Duplicate1 {}</style>
496 'Style deduplication disabled' => [
497 [ 'deduplicateStyles' => false ], $dedupText, $dedupText
500 // phpcs:enable
504 * @covers \MediaWiki\Parser\ParserOutput::hasText
506 public function testHasText() {
507 $po = new ParserOutput( '' );
508 $this->assertTrue( $po->hasText() );
510 $po = new ParserOutput( null );
511 $this->assertFalse( $po->hasText() );
513 $po = new ParserOutput();
514 $this->assertFalse( $po->hasText() );
516 $po = new ParserOutput( '' );
517 $this->assertTrue( $po->hasText() );
519 $po = new ParserOutput( null );
520 $po->setRawText( '' );
521 $this->assertTrue( $po->hasText() );
523 $po = new ParserOutput( 'foo' );
524 $po->setRawText( null );
525 $this->assertFalse( $po->hasText() );
529 * This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
530 * ParserOutput::getText gets deprecated.
531 * @covers \MediaWiki\Parser\ParserOutput::getText
533 public function testGetText_failsIfNoText() {
534 $po = new ParserOutput( null );
536 $this->expectException( LogicException::class );
537 $po->getText();
541 * @covers \MediaWiki\Parser\ParserOutput::getRawText
543 public function testGetRawText_failsIfNoText() {
544 $po = new ParserOutput( null );
546 $this->expectException( LogicException::class );
547 $po->getRawText();
550 public static function provideMergeHtmlMetaDataFrom() {
551 // title text ------------
552 $a = new ParserOutput();
553 $a->setTitleText( 'X' );
554 $b = new ParserOutput();
555 yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
557 $a = new ParserOutput();
558 $b = new ParserOutput();
559 $b->setTitleText( 'Y' );
560 yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
562 $a = new ParserOutput();
563 $a->setTitleText( 'X' );
564 $b = new ParserOutput();
565 $b->setTitleText( 'Y' );
566 yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
568 // index policy ------------
569 $a = new ParserOutput();
570 $a->setIndexPolicy( 'index' );
571 $b = new ParserOutput();
572 yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
574 $a = new ParserOutput();
575 $b = new ParserOutput();
576 $b->setIndexPolicy( 'index' );
577 yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
579 $a = new ParserOutput();
580 $a->setIndexPolicy( 'noindex' );
581 $b = new ParserOutput();
582 $b->setIndexPolicy( 'index' );
583 yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
585 $a = new ParserOutput();
586 $a->setIndexPolicy( 'index' );
587 $b = new ParserOutput();
588 $b->setIndexPolicy( 'noindex' );
589 yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
591 $crhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
593 $a = new ParserOutput();
594 $a->setLanguage( $crhCyrl );
595 $b = new ParserOutput();
596 yield 'only left language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
598 $a = new ParserOutput();
599 $b = new ParserOutput();
600 $b->setLanguage( $crhCyrl );
601 yield 'only right language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
603 // head items and friends ------------
604 $a = new ParserOutput();
605 $a->addHeadItem( '<foo1>' );
606 $a->addHeadItem( '<bar1>', 'bar' );
607 $a->addModules( [ 'test-module-a' ] );
608 $a->addModuleStyles( [ 'test-module-styles-a' ] );
609 $a->setJsConfigVar( 'test-config-var-a', 'a' );
610 $a->appendJsConfigVar( 'test-config-var-c', 'abc' );
611 $a->appendJsConfigVar( 'test-config-var-c', 'def' );
612 $a->addExtraCSPStyleSrc( 'css.com' );
613 $a->addExtraCSPStyleSrc( 'css2.com' );
614 $a->addExtraCSPScriptSrc( 'js.com' );
615 $a->addExtraCSPDefaultSrc( 'img.com' );
617 $b = new ParserOutput();
618 $b->setIndexPolicy( 'noindex' );
619 $b->addHeadItem( '<foo2>' );
620 $b->addHeadItem( '<bar2>', 'bar' );
621 $b->addModules( [ 'test-module-b' ] );
622 $b->addModuleStyles( [ 'test-module-styles-b' ] );
623 $b->setJsConfigVar( 'test-config-var-b', 'b' );
624 $b->setJsConfigVar( 'test-config-var-a', 'X' );
625 $a->appendJsConfigVar( 'test-config-var-c', 'xyz' );
626 $a->appendJsConfigVar( 'test-config-var-c', 'def' );
627 $b->addExtraCSPStyleSrc( 'https://css.ca' );
628 $b->addExtraCSPScriptSrc( 'jscript.com' );
629 $b->addExtraCSPScriptSrc( 'vbscript.com' );
630 $b->addExtraCSPDefaultSrc( 'img.com/foo.jpg' );
632 // Note that overwriting test-config-var-a during the merge
633 // (as this test case does) is deprecated and will eventually
634 // throw an exception. However, at the moment it is still worth
635 // testing this case to ensure backward compatibility. (T300307)
636 yield 'head items and friends' => [ $a, $b, [
637 'getHeadItems' => [
638 '<foo1>',
639 '<foo2>',
640 'bar' => '<bar2>', // overwritten
642 'getModules' => [
643 'test-module-a',
644 'test-module-b',
646 'getModuleStyles' => [
647 'test-module-styles-a',
648 'test-module-styles-b',
650 'getJsConfigVars' => [
651 'test-config-var-a' => 'X', // overwritten
652 'test-config-var-b' => 'b',
653 'test-config-var-c' => [ // merged safely
654 'abc' => true, 'def' => true, 'xyz' => true,
657 'getExtraCSPStyleSrcs' => [
658 'css.com',
659 'css2.com',
660 'https://css.ca'
662 'getExtraCSPScriptSrcs' => [
663 'js.com',
664 'jscript.com',
665 'vbscript.com'
667 'getExtraCSPDefaultSrcs' => [
668 'img.com',
669 'img.com/foo.jpg'
671 ] ];
673 // TOC ------------
674 $a = new ParserOutput( '' );
675 $a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
677 $b = new ParserOutput( '' );
678 $b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
680 yield 'concat TOC' => [ $a, $b, [
681 'getSections' => [
682 SectionMetadata::fromLegacy( [ 'fromtitle' => 'A1' ] )->toLegacy(),
683 SectionMetadata::fromLegacy( [ 'fromtitle' => 'A2' ] )->toLegacy(),
684 SectionMetadata::fromLegacy( [ 'fromtitle' => 'B1' ] )->toLegacy(),
685 SectionMetadata::fromLegacy( [ 'fromtitle' => 'B2' ] )->toLegacy()
687 ] ];
689 // Skin Control ------------
690 $a = new ParserOutput();
691 $a->setNewSection( true );
692 $a->setHideNewSection( true );
693 $a->setNoGallery( true );
694 $a->addWrapperDivClass( 'foo' );
696 $a->setIndicator( 'foo', 'Foo!' );
697 $a->setIndicator( 'bar', 'Bar!' );
699 $a->setExtensionData( 'foo', 'Foo!' );
700 $a->setExtensionData( 'bar', 'Bar!' );
701 $a->appendExtensionData( 'bat', 'abc' );
703 $b = new ParserOutput();
704 $b->setNoGallery( true );
705 $b->setEnableOOUI( true );
706 $b->setPreventClickjacking( true );
707 $a->addWrapperDivClass( 'bar' );
709 $b->setIndicator( 'zoo', 'Zoo!' );
710 $b->setIndicator( 'bar', 'Barrr!' );
712 $b->setExtensionData( 'zoo', 'Zoo!' );
713 $b->setExtensionData( 'bar', 'Barrr!' );
714 $b->appendExtensionData( 'bat', 'xyz' );
716 // Note that overwriting extension data during the merge
717 // (as this test case does for 'bar') is deprecated and will eventually
718 // throw an exception. However, at the moment it is still worth
719 // testing this case to ensure backward compatibility. (T300981)
720 yield 'skin control flags' => [ $a, $b, [
721 'getNewSection' => true,
722 'getHideNewSection' => true,
723 'getNoGallery' => true,
724 'getEnableOOUI' => true,
725 'getPreventClickjacking' => true,
726 'getIndicators' => [
727 'foo' => 'Foo!',
728 'bar' => 'Barrr!', // overwritten
729 'zoo' => 'Zoo!',
731 'getWrapperDivClass' => 'foo bar',
732 '$mExtensionData' => [
733 'foo' => 'Foo!',
734 'bar' => 'Barrr!', // overwritten
735 'zoo' => 'Zoo!',
736 // internal strategy key is exposed here because we're looking
737 // at the raw property value, not using getExtensionData()
738 'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
740 ] ];
744 * @dataProvider provideMergeHtmlMetaDataFrom
745 * @covers \MediaWiki\Parser\ParserOutput::mergeHtmlMetaDataFrom
747 * @param ParserOutput $a
748 * @param ParserOutput $b
749 * @param array $expected
751 public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
752 $a->mergeHtmlMetaDataFrom( $b );
754 $this->assertFieldValues( $a, $expected );
756 // test twice, to make sure the operation is idempotent (except for the TOC, see below)
757 $a->mergeHtmlMetaDataFrom( $b );
759 // XXX: TOC joining should get smarter. Can we make it idempotent as well?
760 unset( $expected['getSections'] );
762 $this->assertFieldValues( $a, $expected );
765 private function assertFieldValues( ParserOutput $po, $expected ) {
766 $po = TestingAccessWrapper::newFromObject( $po );
768 foreach ( $expected as $method => $value ) {
769 $canonicalize = false;
770 if ( $method[0] === '$' ) {
771 $field = substr( $method, 1 );
772 $actual = $po->__get( $field );
773 } elseif ( str_contains( $method, '!' ) ) {
774 [ $trimmedMethod, $ignore ] = explode( '!', $method, 2 );
775 $args = $value['_args_'] ?? [];
776 unset( $value['_args_'] );
777 $actual = $po->__call( $trimmedMethod, $args );
778 } else {
779 $actual = $po->__call( $method, [] );
781 if ( $method === 'getJsConfigVars' ) {
782 $canonicalize = true;
785 if ( $canonicalize ) {
786 // order of entries isn't significant
787 $this->assertEqualsCanonicalizing( $value, $actual, $method );
788 } else {
789 $this->assertEquals( $value, $actual, $method );
795 * @covers \MediaWiki\Parser\ParserOutput::addLink
796 * @covers \MediaWiki\Parser\ParserOutput::getLinks
797 * @covers \MediaWiki\Parser\ParserOutput::getLinkList
799 public function testAddLink() {
800 $a = new ParserOutput();
801 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
802 $a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
803 $a->addLink( new TitleValue( NS_MAIN, 'Goats_786827346' ) );
804 # fragments are stripped for local links
805 $a->addLink( new TitleValue( NS_TALK, 'Puppies', 'Topic' ), 17 );
807 $expected = [
808 NS_MAIN => [ 'Kittens' => 6, 'Goats_786827346' => 0 ],
809 NS_TALK => [ 'Kittens' => 16, 'Puppies' => 17 ]
811 $this->assertSame( $expected, $a->getLinks() );
812 $expected = [
814 'link' => new TitleValue( NS_MAIN, 'Kittens' ),
815 'pageid' => 6,
818 'link' => new TitleValue( NS_MAIN, 'Goats_786827346' ),
819 'pageid' => 0,
822 'link' => new TitleValue( NS_TALK, 'Kittens' ),
823 'pageid' => 16,
826 'link' => new TitleValue( NS_TALK, 'Puppies' ),
827 'pageid' => 17,
830 $this->assertEquals( $expected, $a->getLinkList( ParserOutputLinkTypes::LOCAL ) );
833 public static function provideMergeTrackingMetaDataFrom() {
834 // links ------------
835 $a = new ParserOutput();
836 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
837 $a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
838 # fragments are stripped in local links
839 $a->addLink( new TitleValue( NS_MAIN, 'Goats', 'Kids' ), 7 );
841 $a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
843 $a->addLanguageLink( new TitleValue( NS_MAIN, 'de', '', 'de' ) );
844 # fragments are preserved in language links
845 $a->addLanguageLink( new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ) );
846 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
847 # fragments are stripped in interwiki links
848 $a->addInterwikiLink( new TitleValue( NS_MAIN, 'Kittens RU', 'ru', 'ru' ) );
849 $a->addExternalLink( 'https://kittens.wikimedia.test' );
850 # fragments are preserved in external links
851 $a->addExternalLink( 'https://goats.wikimedia.test#kids' );
853 # fragments are stripped for categories (syntax is overloaded for sort)
854 $a->addCategory( new TitleValue( NS_CATEGORY, 'Foo', 'bar' ), 'X' );
855 # fragments are stripped for images
856 $a->addImage( new TitleValue( NS_FILE, 'Billy.jpg', 'fragment' ), '20180101000013', 'DEAD' );
857 # fragments are stripped for links to special pages
858 $a->addLink( new TitleValue( NS_SPECIAL, 'Version', 'section' ) );
860 $b = new ParserOutput();
861 $b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
862 $b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
863 $b->addLink( new TitleValue( NS_MAIN, 'Dragons' ), 8 );
864 $b->addLink( new TitleValue( NS_FILE, 'Dragons.jpg' ), 28 );
866 # fragments are stripped from template links
867 $b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons', 'red' ), 108, 1108 );
868 $a->addTemplate( new TitleValue( NS_MAIN, 'Dragons', 'platinum' ), 118, 1118 );
870 $b->addLanguageLink( new TitleValue( NS_MAIN, 'fr', '', 'fr' ) );
871 $b->addLanguageLink( new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ) );
872 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
873 $b->addInterwikiLink( new TitleValue( NS_MAIN, 'Dragons RU', '', 'ru' ) );
874 $b->addExternalLink( 'https://dragons.wikimedia.test' );
875 $b->addExternalLink( 'https://goats.wikimedia.test#kids' );
877 $b->addCategory( 'Bar', 'Y' );
878 $b->addImage( new TitleValue( NS_FILE, 'Puff.jpg' ), '20180101000017', 'BEEF' );
880 yield 'all kinds of links' => [ $a, $b, [
881 'getLinks' => [
882 NS_MAIN => [
883 'Kittens' => 6,
884 'Goats' => 7,
885 'Dragons' => 8,
887 NS_TALK => [
888 'Kittens' => 16,
889 'Goats' => 17,
891 NS_FILE => [
892 'Dragons.jpg' => 28,
895 'getLinkList!LOCAL' => [
896 '_args_' => [ ParserOutputLinkTypes::LOCAL ],
898 'link' => new TitleValue( NS_MAIN, 'Kittens' ),
899 'pageid' => 6,
902 'link' => new TitleValue( NS_MAIN, 'Goats' ),
903 'pageid' => 7,
906 'link' => new TitleValue( NS_MAIN, 'Dragons' ),
907 'pageid' => 8,
910 'link' => new TitleValue( NS_TALK, 'Kittens' ),
911 'pageid' => 16,
914 'link' => new TitleValue( NS_TALK, 'Goats' ),
915 'pageid' => 17,
918 'link' => new TitleValue( NS_FILE, 'Dragons.jpg' ),
919 'pageid' => 28,
922 'getTemplates' => [
923 NS_MAIN => [
924 'Dragons' => 118,
926 NS_TEMPLATE => [
927 'Dragons' => 108,
928 'Goats' => 107,
931 'getTemplateIds' => [
932 NS_MAIN => [
933 'Dragons' => 1118,
935 NS_TEMPLATE => [
936 'Dragons' => 1108,
937 'Goats' => 1107,
940 'getLinkList!TEMPLATE' => [
941 '_args_' => [ ParserOutputLinkTypes::TEMPLATE ],
943 'link' => new TitleValue( NS_TEMPLATE, 'Goats' ),
944 'pageid' => 107,
945 'revid' => 1107,
948 'link' => new TitleValue( NS_TEMPLATE, 'Dragons' ),
949 'pageid' => 108,
950 'revid' => 1108,
953 'link' => new TitleValue( NS_MAIN, 'Dragons' ),
954 'pageid' => 118,
955 'revid' => 1118,
958 'getLanguageLinks' => [ 'de:de', 'ru:ru#ru', 'fr:fr' ],
959 'getLinkList!LANGUAGE' => [
960 '_args_' => [ ParserOutputLinkTypes::LANGUAGE ],
962 'link' => new TitleValue( NS_MAIN, 'de', '', 'de' ),
965 'link' => new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ),
968 'link' => new TitleValue( NS_MAIN, 'fr', '', 'fr' ),
971 'getInterwikiLinks' => [
972 'de' => [ 'Kittens_DE' => 1 ],
973 'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
974 'fr' => [ 'Kittens_FR' => 1 ],
976 'getLinkList!INTERWIKI' => [
977 '_args_' => [ ParserOutputLinkTypes::INTERWIKI ],
979 'link' => new TitleValue( NS_MAIN, 'Kittens_DE', '', 'de' ),
982 'link' => new TitleValue( NS_MAIN, 'Kittens_RU', '', 'ru' ),
985 'link' => new TitleValue( NS_MAIN, 'Dragons_RU', '', 'ru' ),
988 'link' => new TitleValue( NS_MAIN, 'Kittens_FR', '', 'fr' ),
991 'getCategoryMap' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
992 'getLinkList!CATEGORY' => [
993 '_args_' => [ ParserOutputLinkTypes::CATEGORY ],
995 'link' => new TitleValue( NS_CATEGORY, 'Foo' ),
996 'sort' => 'X',
999 'link' => new TitleValue( NS_CATEGORY, 'Bar' ),
1000 'sort' => 'Y',
1003 'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
1004 'getFileSearchOptions' => [
1005 'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
1006 'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
1008 'getLinkList!MEDIA' => [
1009 '_args_' => [ ParserOutputLinkTypes::MEDIA ],
1011 'link' => new TitleValue( NS_FILE, 'Billy.jpg' ),
1012 'time' => '20180101000013',
1013 'sha1' => 'DEAD',
1016 'link' => new TitleValue( NS_FILE, 'Puff.jpg' ),
1017 'time' => '20180101000017',
1018 'sha1' => 'BEEF',
1021 'getExternalLinks' => [
1022 'https://dragons.wikimedia.test' => 1,
1023 'https://kittens.wikimedia.test' => 1,
1024 'https://goats.wikimedia.test#kids' => 1,
1026 'getLinkList!SPECIAL' => [
1027 '_args_' => [ ParserOutputLinkTypes::SPECIAL ],
1029 'link' => new TitleValue( NS_SPECIAL, 'Version' ),
1032 ] ];
1034 // properties ------------
1035 $a = new ParserOutput();
1037 $a->setPageProperty( 'foo', 'Foo!' );
1038 $a->setPageProperty( 'bar', 'Bar!' );
1040 $a->setExtensionData( 'foo', 'Foo!' );
1041 $a->setExtensionData( 'bar', 'Bar!' );
1042 $a->appendExtensionData( 'bat', 'abc' );
1044 $b = new ParserOutput();
1046 $b->setPageProperty( 'zoo', 'Zoo!' );
1047 $b->setPageProperty( 'bar', 'Barrr!' );
1049 $b->setExtensionData( 'zoo', 'Zoo!' );
1050 $b->setExtensionData( 'bar', 'Barrr!' );
1051 $b->appendExtensionData( 'bat', 'xyz' );
1053 // Note that overwriting extension data during the merge
1054 // (as this test case does for 'bar') is deprecated and will eventually
1055 // throw an exception. However, at the moment it is still worth
1056 // testing this case to ensure backward compatibility. (T300981)
1057 yield 'properties' => [ $a, $b, [
1058 'getPageProperties' => [
1059 'foo' => 'Foo!',
1060 'bar' => 'Barrr!', // overwritten
1061 'zoo' => 'Zoo!',
1063 '$mExtensionData' => [
1064 'foo' => 'Foo!',
1065 'bar' => 'Barrr!', // overwritten
1066 'zoo' => 'Zoo!',
1067 // internal strategy key is exposed here because we're looking
1068 // at the raw property value, not using getExtensionData()
1069 'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
1071 ] ];
1075 * @dataProvider provideMergeTrackingMetaDataFrom
1076 * @covers \MediaWiki\Parser\ParserOutput::mergeTrackingMetaDataFrom
1078 * @param ParserOutput $a
1079 * @param ParserOutput $b
1080 * @param array $expected
1082 public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
1083 $a->mergeTrackingMetaDataFrom( $b );
1085 $this->assertFieldValues( $a, $expected );
1087 // test twice, to make sure the operation is idempotent
1088 $a->mergeTrackingMetaDataFrom( $b );
1090 $this->assertFieldValues( $a, $expected );
1094 * @dataProvider provideMergeTrackingMetaDataFrom
1095 * @covers \MediaWiki\Parser\ParserOutput::collectMetadata
1097 * @param ParserOutput $a
1098 * @param ParserOutput $b
1099 * @param array $expected
1101 public function testCollectMetaData( ParserOutput $a, ParserOutput $b, $expected ) {
1102 $b->collectMetadata( $a );
1104 $this->assertFieldValues( $a, $expected );
1107 public function provideMergeInternalMetaDataFrom() {
1108 $this->filterDeprecated( '/^.*CacheTime::setCacheTime called with -1 as an argument/' );
1110 // flags & co
1111 $a = new ParserOutput();
1113 $a->addWarningMsg( 'duplicate-args-warning', 'A', 'B', 'C' );
1114 $a->addWarningMsg( 'template-loop-warning', 'D' );
1116 $a->setOutputFlag( 'foo' );
1117 $a->setOutputFlag( 'bar' );
1119 $a->recordOption( 'Foo' );
1120 $a->recordOption( 'Bar' );
1122 $b = new ParserOutput();
1124 $b->addWarningMsg( 'template-equals-warning' );
1125 $b->addWarningMsg( 'template-loop-warning', 'D' );
1127 $b->setOutputFlag( 'zoo' );
1128 $b->setOutputFlag( 'bar' );
1130 $b->recordOption( 'Zoo' );
1131 $b->recordOption( 'Bar' );
1133 yield 'flags' => [ $a, $b, [
1134 'getWarnings' => [
1135 wfMessage( 'duplicate-args-warning', 'A', 'B', 'C' )->text(),
1136 wfMessage( 'template-loop-warning', 'D' )->text(),
1137 wfMessage( 'template-equals-warning' )->text(),
1139 '$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
1140 'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
1141 ] ];
1143 // cache time
1144 $someTime = "20240207202040";
1145 $someLaterTime = "20240207202112";
1146 $a = new ParserOutput();
1147 $a->setCacheTime( $someTime );
1148 $b = new ParserOutput();
1149 yield 'only left cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
1151 $a = new ParserOutput();
1152 $b = new ParserOutput();
1153 $b->setCacheTime( $someTime );
1154 yield 'only right cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
1156 $a = new ParserOutput();
1157 $b = new ParserOutput();
1158 $a->setCacheTime( $someLaterTime );
1159 $b->setCacheTime( $someTime );
1160 yield 'left has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
1162 $a = new ParserOutput();
1163 $b = new ParserOutput();
1164 $a->setCacheTime( $someTime );
1165 $b->setCacheTime( $someLaterTime );
1166 yield 'right has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
1168 $a = new ParserOutput();
1169 $b = new ParserOutput();
1170 $a->setCacheTime( -1 );
1171 $b->setCacheTime( $someTime );
1172 yield 'left is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
1174 $a = new ParserOutput();
1175 $b = new ParserOutput();
1176 $a->setCacheTime( $someTime );
1177 $b->setCacheTime( -1 );
1178 yield 'right is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
1180 // timestamp ------------
1181 $a = new ParserOutput();
1182 $a->setRevisionTimestamp( '20180101000011' );
1183 $b = new ParserOutput();
1184 yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1186 $a = new ParserOutput();
1187 $b = new ParserOutput();
1188 $b->setRevisionTimestamp( '20180101000011' );
1189 yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1191 $a = new ParserOutput();
1192 $a->setRevisionTimestamp( '20180101000011' );
1193 $b = new ParserOutput();
1194 $b->setRevisionTimestamp( '20180101000001' );
1195 yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1197 $a = new ParserOutput();
1198 $a->setRevisionTimestamp( '20180101000001' );
1199 $b = new ParserOutput();
1200 $b->setRevisionTimestamp( '20180101000011' );
1201 yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1203 // speculative rev id ------------
1204 $a = new ParserOutput();
1205 $a->setSpeculativeRevIdUsed( 9 );
1206 $b = new ParserOutput();
1207 yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1209 $a = new ParserOutput();
1210 $b = new ParserOutput();
1211 $b->setSpeculativeRevIdUsed( 9 );
1212 yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1214 $a = new ParserOutput();
1215 $a->setSpeculativeRevIdUsed( 9 );
1216 $b = new ParserOutput();
1217 $b->setSpeculativeRevIdUsed( 9 );
1218 yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1220 // limit report (recursive max) ------------
1221 $a = new ParserOutput();
1223 $a->setLimitReportData( 'naive1', 7 );
1224 $a->setLimitReportData( 'naive2', 27 );
1226 $a->setLimitReportData( 'limitreport-simple1', 7 );
1227 $a->setLimitReportData( 'limitreport-simple2', 27 );
1229 $a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
1230 $a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
1232 $a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
1233 $a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
1235 $a->setLimitReportData( 'limitreport-only-a', 13 );
1237 $b = new ParserOutput();
1239 $b->setLimitReportData( 'naive1', 17 );
1240 $b->setLimitReportData( 'naive2', 17 );
1242 $b->setLimitReportData( 'limitreport-simple1', 17 );
1243 $b->setLimitReportData( 'limitreport-simple2', 17 );
1245 $b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
1246 $b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
1248 $b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
1249 $b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
1251 $b->setLimitReportData( 'limitreport-only-b', 23 );
1253 // first write wins
1254 yield 'limit report' => [ $a, $b, [
1255 'getLimitReportData' => [
1256 'naive1' => 7,
1257 'naive2' => 27,
1258 'limitreport-simple1' => 7,
1259 'limitreport-simple2' => 27,
1260 'limitreport-pair1' => [ 7, 9 ],
1261 'limitreport-pair2' => [ 27, 29 ],
1262 'limitreport-more1' => [ 7, 9, 1 ],
1263 'limitreport-more2' => [ 27, 29, 21 ],
1264 'limitreport-only-a' => 13,
1266 'getLimitReportJSData' => [
1267 'naive1' => 7,
1268 'naive2' => 27,
1269 'limitreport' => [
1270 'simple1' => 7,
1271 'simple2' => 27,
1272 'pair1' => [ 'value' => 7, 'limit' => 9 ],
1273 'pair2' => [ 'value' => 27, 'limit' => 29 ],
1274 'more1' => [ 7, 9, 1 ],
1275 'more2' => [ 27, 29, 21 ],
1276 'only-a' => 13,
1279 ] ];
1281 MWDebug::clearDeprecationFilters();
1285 * @dataProvider provideMergeInternalMetaDataFrom
1286 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1288 * @param ParserOutput $a
1289 * @param ParserOutput $b
1290 * @param array $expected
1292 public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
1293 $this->filterDeprecated( '/^.*CacheTime::setCacheTime called with -1 as an argument/' );
1294 $a->mergeInternalMetaDataFrom( $b );
1296 $this->assertFieldValues( $a, $expected );
1298 // test twice, to make sure the operation is idempotent
1299 $a->mergeInternalMetaDataFrom( $b );
1301 $this->assertFieldValues( $a, $expected );
1305 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1306 * @covers \MediaWiki\Parser\ParserOutput::getTimes
1307 * @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
1309 public function testMergeInternalMetaDataFrom_parseStartTime() {
1310 /** @var object $a */
1311 $a = new ParserOutput();
1312 $a = TestingAccessWrapper::newFromObject( $a );
1314 $a->resetParseStartTime();
1315 $aClocks = $a->mParseStartTime;
1317 $b = new ParserOutput();
1319 $a->mergeInternalMetaDataFrom( $b );
1320 $mergedClocks = $a->mParseStartTime;
1322 foreach ( $mergedClocks as $clock => $timestamp ) {
1323 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
1326 // try again, with times in $b also set, and later than $a's
1327 usleep( 1234 );
1329 /** @var object $b */
1330 $b = new ParserOutput();
1331 $b = TestingAccessWrapper::newFromObject( $b );
1333 $b->resetParseStartTime();
1335 $bClocks = $b->mParseStartTime;
1337 $a->mergeInternalMetaDataFrom( $b->object );
1338 $mergedClocks = $a->mParseStartTime;
1340 foreach ( $mergedClocks as $clock => $timestamp ) {
1341 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
1342 $this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
1345 // try again, with $a's times being later
1346 usleep( 1234 );
1347 $a->resetParseStartTime();
1348 $aClocks = $a->mParseStartTime;
1350 $a->mergeInternalMetaDataFrom( $b->object );
1351 $mergedClocks = $a->mParseStartTime;
1353 foreach ( $mergedClocks as $clock => $timestamp ) {
1354 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
1355 $this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
1358 // try again, with no times in $a set
1359 $a = new ParserOutput();
1360 $a = TestingAccessWrapper::newFromObject( $a );
1362 $a->mergeInternalMetaDataFrom( $b->object );
1363 $mergedClocks = $a->mParseStartTime;
1365 foreach ( $mergedClocks as $clock => $timestamp ) {
1366 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
1371 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1372 * @covers \MediaWiki\Parser\ParserOutput::getTimes
1373 * @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
1374 * @covers \MediaWiki\Parser\ParserOutput::recordTimeProfile
1375 * @covers \MediaWiki\Parser\ParserOutput::getTimeProfile
1377 public function testMergeInternalMetaDataFrom_timeProfile() {
1378 /** @var object $a */
1379 $a = new ParserOutput();
1380 $a = TestingAccessWrapper::newFromObject( $a );
1382 $a->resetParseStartTime();
1383 usleep( 1234 );
1384 $a->recordTimeProfile();
1386 $aClocks = $a->mTimeProfile;
1388 // make sure a second call to recordTimeProfile has no effect
1389 usleep( 1234 );
1390 $a->recordTimeProfile();
1392 foreach ( $aClocks as $clock => $duration ) {
1393 $this->assertNotNull( $duration );
1394 $this->assertGreaterThan( 0, $duration );
1395 $this->assertSame( $aClocks[$clock], $a->getTimeProfile( $clock ) );
1398 $b = new ParserOutput();
1400 $a->mergeInternalMetaDataFrom( $b );
1401 $mergedClocks = $a->mTimeProfile;
1403 foreach ( $mergedClocks as $clock => $duration ) {
1404 $this->assertSame( $aClocks[$clock], $duration, $clock );
1407 // try again, with times in $b also set, and later than $a's
1408 $b->resetParseStartTime();
1409 usleep( 1234 );
1410 $b->recordTimeProfile();
1412 $b = TestingAccessWrapper::newFromObject( $b );
1413 $bClocks = $b->mTimeProfile;
1415 $a->mergeInternalMetaDataFrom( $b->object );
1416 $mergedClocks = $a->mTimeProfile;
1418 foreach ( $mergedClocks as $clock => $duration ) {
1419 $this->assertGreaterThanOrEqual( $aClocks[$clock], $duration, $clock );
1420 $this->assertGreaterThanOrEqual( $bClocks[$clock], $duration, $clock );
1425 * @covers \MediaWiki\Parser\ParserOutput::getCacheTime
1426 * @covers \MediaWiki\Parser\ParserOutput::setCacheTime
1428 public function testGetCacheTime() {
1429 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
1430 MWTimestamp::setFakeTime( static function () use ( &$clock ) {
1431 return $clock++;
1432 } );
1434 $po = new ParserOutput();
1435 $time = $po->getCacheTime();
1437 // Use current (fake) time by default. Ignore the last digit.
1438 // Subsequent calls must yield the exact same timestamp as the first.
1439 $this->assertStringStartsWith( '2010010100000', $time );
1440 $this->assertSame( $time, $po->getCacheTime() );
1442 // After setting, the getter must return the time that was set.
1443 $time = '20110606112233';
1444 $po->setCacheTime( $time );
1445 $this->assertSame( $time, $po->getCacheTime() );
1449 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPScriptSrc
1450 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPDefaultSrc
1451 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPStyleSrc
1452 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPScriptSrcs
1453 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPDefaultSrcs
1454 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPStyleSrcs
1456 public function testCSPSources() {
1457 $po = new ParserOutput;
1459 $this->assertEquals( [], $po->getExtraCSPScriptSrcs(), 'empty Script' );
1460 $this->assertEquals( [], $po->getExtraCSPStyleSrcs(), 'empty Style' );
1461 $this->assertEquals( [], $po->getExtraCSPDefaultSrcs(), 'empty Default' );
1463 $po->addExtraCSPScriptSrc( 'foo.com' );
1464 $po->addExtraCSPScriptSrc( 'bar.com' );
1465 $po->addExtraCSPDefaultSrc( 'baz.com' );
1466 $po->addExtraCSPStyleSrc( 'fred.com' );
1467 $po->addExtraCSPStyleSrc( 'xyzzy.com' );
1469 $this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs(), 'Script' );
1470 $this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs(), 'Default' );
1471 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs(), 'Style' );
1474 public function testOutputStrings() {
1475 $po = new ParserOutput;
1477 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
1478 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
1479 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
1480 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
1481 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
1483 $this->assertEquals( [], $po->getModules() );
1484 $this->assertEquals( [], $po->getModuleStyles() );
1485 $this->assertEquals( [], $po->getExtraCSPScriptSrcs() );
1486 $this->assertEquals( [], $po->getExtraCSPStyleSrcs() );
1487 $this->assertEquals( [], $po->getExtraCSPDefaultSrcs() );
1489 $po->appendOutputStrings( ParserOutputStringSets::MODULE, [ 'a' ] );
1490 $po->appendOutputStrings( ParserOutputStringSets::MODULE_STYLE, [ 'b' ] );
1491 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC, [ 'foo.com', 'bar.com' ] );
1492 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ 'baz.com' ] );
1493 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'fred.com' ] );
1494 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'xyzzy.com' ] );
1496 $this->assertEquals( [ 'a' ], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
1497 $this->assertEquals( [ 'b' ], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
1498 $this->assertEquals( [ 'foo.com', 'bar.com' ],
1499 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
1500 $this->assertEquals( [ 'baz.com' ],
1501 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
1502 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ],
1503 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
1505 $this->assertEquals( [ 'a' ], $po->getModules() );
1506 $this->assertEquals( [ 'b' ], $po->getModuleStyles() );
1507 $this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs() );
1508 $this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs() );
1509 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs() );
1513 * @covers \MediaWiki\Parser\ParserOutput::getCacheTime()
1514 * @covers \MediaWiki\Parser\ParserOutput::setCacheTime()
1516 public function testCacheTime() {
1517 $po = new ParserOutput();
1519 // Should not have a cache time yet
1520 $this->assertFalse( $po->hasCacheTime() );
1521 // But calling ::get assigns a cache time
1522 $po->getCacheTime();
1523 $this->assertTrue( $po->hasCacheTime() );
1524 $this->assertTrue( $po->isCacheable() );
1525 // Reset cache time
1526 $po->setCacheTime( "20240207202040" );
1527 $this->assertSame( "20240207202040", $po->getCacheTime() );
1531 * @covers \MediaWiki\Parser\ParserOutput::isCacheable()
1532 * @covers \MediaWiki\Parser\ParserOutput::getCacheExpiry()
1533 * @covers \MediaWiki\Parser\ParserOutput::hasReducedExpiry()
1535 public function testAsyncNotReady() {
1536 $defaultExpiry = ParserCacheSerializationTestCases::FAKE_CACHE_EXPIRY;
1537 $asyncExpiry = ParserCacheSerializationTestCases::FAKE_ASYNC_CACHE_EXPIRY;
1538 // $asyncExpiry has to be smaller than the default for these tests to
1539 // work properly.
1540 $this->assertTrue( $asyncExpiry < $defaultExpiry );
1542 $po = new ParserOutput();
1543 $po->getCacheTime(); // assign a cache time
1544 $this->assertTrue( $po->isCacheable() );
1545 $this->assertFalse( $po->hasReducedExpiry() );
1547 // hasReducedExpiry is set if there is/was any async content
1548 $po->setOutputFlag( ParserOutputFlags::HAS_ASYNC_CONTENT );
1549 $this->assertTrue( $po->isCacheable() );
1550 $this->assertTrue( $po->hasReducedExpiry() );
1551 $this->assertTrue( $po->getCacheExpiry() === $defaultExpiry );
1553 // Setting ASYNC_NOT_READY also shortens the cache expiry
1554 $po->setOutputFlag( ParserOutputFlags::ASYNC_NOT_READY );
1555 $this->assertTrue( $po->isCacheable() );
1556 $this->assertTrue( $po->hasReducedExpiry() );
1557 $this->assertTrue( $po->getCacheExpiry() === $asyncExpiry );
1559 $po->updateCacheExpiry( $defaultExpiry - 1 );
1560 $this->assertTrue( $po->isCacheable() );
1561 $this->assertTrue( $po->hasReducedExpiry() );
1562 $this->assertTrue( $po->getCacheExpiry() === $asyncExpiry );
1566 * @covers \MediaWiki\Parser\ParserOutput::getRenderId()
1567 * @covers \MediaWiki\Parser\ParserOutput::setRenderId()
1569 public function testRenderId() {
1570 $po = new ParserOutput();
1572 // Should be null when unset
1573 $this->assertNull( $po->getRenderId() );
1575 // Sanity check for setter and getter
1576 $po->setRenderId( "TestRenderId" );
1577 $this->assertEquals( "TestRenderId", $po->getRenderId() );
1581 * @covers \MediaWiki\Parser\ParserOutput::getRenderId()
1583 public function testRenderIdBackCompat() {
1584 $po = new ParserOutput();
1586 // Parser cache used to contain extension data under a different name
1587 $po->setExtensionData( 'parsoid-render-id', "1234/LegacyRenderId" );
1588 $this->assertEquals( "LegacyRenderId", $po->getRenderId() );
1591 public function testSetFromParserOptions() {
1592 // parser output set from canonical parser options
1593 $pOptions = ParserOptions::newFromAnon();
1594 $pOutput = new ParserOutput;
1595 $pOutput->setFromParserOptions( $pOptions );
1596 $this->assertSame( 'mw-parser-output', $pOutput->getWrapperDivClass() );
1597 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
1598 $this->assertTrue( $pOutput->isCacheable() );
1599 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
1600 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );
1602 // set the various parser options and verify in parser output
1603 $pOptions->setWrapOutputClass( 'test-wrapper' );
1604 $pOptions->setIsPreview( true );
1605 $pOptions->setSuppressSectionEditLinks();
1606 $pOptions->setCollapsibleSections();
1607 $pOutput = new ParserOutput;
1608 $pOutput->setFromParserOptions( $pOptions );
1609 $this->assertEquals( 'test-wrapper', $pOutput->getWrapperDivClass() );
1610 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
1611 $this->assertFalse( $pOutput->isCacheable() );
1612 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
1613 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );