Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / parser / ParserOutputTest.php
blob76ad7647794f4cb9fd175632c5d9ddab29f188c5
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
44 /**
45 * Overrides SerializationTestTrait::getClassToTest
46 * @return string
48 public static function getClassToTest(): string {
49 return ParserOutput::class;
52 /**
53 * Overrides SerializationTestTrait::getSerializedDataPath
54 * @return string
56 public static function getSerializedDataPath(): string {
57 return __DIR__ . '/../../data/ParserCache';
60 /**
61 * Overrides SerializationTestTrait::getTestInstancesAndAssertions
62 * @return array
64 public static function getTestInstancesAndAssertions(): array {
65 return ParserCacheSerializationTestCases::getParserOutputTestCases();
68 /**
69 * Overrides SerializationTestTrait::getSupportedSerializationFormats
70 * @return array
72 public static function getSupportedSerializationFormats(): array {
73 return ParserCacheSerializationTestCases::getSupportedSerializationFormats(
74 self::getClassToTest() );
77 public static function provideIsLinkInternal() {
78 return [
79 // Different domains
80 [ false, 'http://example.org', 'http://mediawiki.org' ],
81 // Same domains
82 [ true, 'http://example.org', 'http://example.org' ],
83 [ true, 'https://example.org', 'https://example.org' ],
84 [ true, '//example.org', '//example.org' ],
85 // Same domain different cases
86 [ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
87 // Paths, queries, and fragments are not relevant
88 [ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
89 [ true, 'http://example.org', 'http://example.org?my=query' ],
90 [ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
91 // Different protocols
92 [ false, 'http://example.org', 'https://example.org' ],
93 [ false, 'https://example.org', 'http://example.org' ],
94 // Protocol relative servers always match http and https links
95 [ true, '//example.org', 'http://example.org' ],
96 [ true, '//example.org', 'https://example.org' ],
97 // But they don't match strange things like this
98 [ false, '//example.org', 'irc://example.org' ],
103 * Test to make sure ParserOutput::isLinkInternal behaves properly
104 * @dataProvider provideIsLinkInternal
105 * @covers \MediaWiki\Parser\ParserOutput::isLinkInternal
107 public function testIsLinkInternal( $shouldMatch, $server, $url ) {
108 $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
112 * @covers \MediaWiki\Parser\ParserOutput::appendJsConfigVar
113 * @covers \MediaWiki\Parser\ParserOutput::setJsConfigVar
114 * @covers \MediaWiki\Parser\ParserOutput::getJsConfigVars
116 public function testJsConfigVars() {
117 $po = new ParserOutput();
119 $po->setJsConfigVar( 'a', '1' );
120 $po->appendJsConfigVar( 'b', 'a' );
121 $po->appendJsConfigVar( 'b', '0' );
123 $this->assertEqualsCanonicalizing( [
124 'a' => 1,
125 'b' => [ 'a' => true, '0' => true ],
126 ], $po->getJsConfigVars() );
128 $po->setJsConfigVar( 'c', '2' );
129 $po->appendJsConfigVar( 'b', 'b' );
130 $po->appendJsConfigVar( 'b', '1' );
132 $this->assertEqualsCanonicalizing( [
133 'a' => 1,
134 'b' => [ 'a' => true, 'b' => true, '0' => true, '1' => true ],
135 'c' => 2,
136 ], $po->getJsConfigVars() );
140 * @covers \MediaWiki\Parser\ParserOutput::appendExtensionData
141 * @covers \MediaWiki\Parser\ParserOutput::setExtensionData
142 * @covers \MediaWiki\Parser\ParserOutput::getExtensionData
144 public function testExtensionData() {
145 $po = new ParserOutput();
147 $po->setExtensionData( "one", "Foo" );
148 $po->appendExtensionData( "three", "abc" );
150 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
151 $this->assertNull( $po->getExtensionData( "spam" ) );
153 $po->setExtensionData( "two", "Bar" );
154 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
155 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
157 // Note that overwriting extension data (as this test case
158 // does) is deprecated and will eventually throw an
159 // exception. However, at the moment it is still worth testing
160 // this case to ensure backward compatibility. (T300981)
161 $po->setExtensionData( "one", null );
162 $this->assertNull( $po->getExtensionData( "one" ) );
163 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
165 $this->assertEqualsCanonicalizing( [
166 'abc' => true,
167 ], $po->getExtensionData( "three" ) );
169 $po->appendExtensionData( "three", "xyz" );
170 $this->assertEqualsCanonicalizing( [
171 'abc' => true,
172 'xyz' => true,
173 ], $po->getExtensionData( "three" ) );
177 * @covers \MediaWiki\Parser\ParserOutput::setPageProperty
178 * @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
179 * @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
180 * @covers \MediaWiki\Parser\ParserOutput::getPageProperty
181 * @covers \MediaWiki\Parser\ParserOutput::unsetPageProperty
182 * @covers \MediaWiki\Parser\ParserOutput::getPageProperties
183 * @dataProvider providePageProperties
185 public function testPageProperties( string $setPageProperty, $value1, $value2, bool $expectDeprecation = false ) {
186 $po = new ParserOutput();
187 if ( $expectDeprecation ) {
188 MWDebug::filterDeprecationForTest( '/::setPageProperty with non-string value/' );
191 $po->$setPageProperty( 'foo', $value1 );
193 $properties = $po->getPageProperties();
194 $this->assertSame( $value1, $po->getPageProperty( 'foo' ) );
195 $this->assertSame( $value1, $properties['foo'] );
197 $po->$setPageProperty( 'foo', $value2 );
199 $properties = $po->getPageProperties();
200 $this->assertSame( $value2, $po->getPageProperty( 'foo' ) );
201 $this->assertSame( $value2, $properties['foo'] );
203 $po->unsetPageProperty( 'foo' );
205 $properties = $po->getPageProperties();
206 $this->assertSame( null, $po->getPageProperty( 'foo' ) );
207 $this->assertArrayNotHasKey( 'foo', $properties );
210 public static function providePageProperties() {
211 yield 'Unsorted' => [ 'setUnsortedPageProperty', 'val', 'second val' ];
212 yield 'Numeric' => [ 'setNumericPageProperty', 42, 3.14 ];
213 yield 'Unsorted (old style)' => [ 'setPageProperty', 'val', 'second val' ];
214 yield 'Numeric (old style)' => [ 'setPageProperty', 123, 456, true ];
218 * @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
220 public function testNumericPageProperties() {
221 $po = new ParserOutput();
223 $po->setNumericPageProperty( 'foo', '123' );
225 $properties = $po->getPageProperties();
226 $this->assertSame( 123, $po->getPageProperty( 'foo' ) );
227 $this->assertSame( 123, $properties['foo'] );
231 * @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
233 public function testUnsortedPageProperties() {
234 $po = new ParserOutput();
236 $po->setUnsortedPageProperty( 'foo', 123 );
238 $properties = $po->getPageProperties();
239 $this->assertSame( '123', $po->getPageProperty( 'foo' ) );
240 $this->assertSame( '123', $properties['foo'] );
244 * @covers \MediaWiki\Parser\ParserOutput::setLanguage
245 * @covers \MediaWiki\Parser\ParserOutput::getLanguage
247 public function testLanguage() {
248 $po = new ParserOutput();
250 $langFr = new Bcp47CodeValue( 'fr' );
251 $langCrhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
253 // Fallback to null
254 $this->assertSame( null, $po->getLanguage() );
256 // Simple case
257 $po->setLanguage( $langFr );
258 $this->assertSame( $langFr->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
260 // Language with a variant
261 $po->setLanguage( $langCrhCyrl );
262 $this->assertSame( $langCrhCyrl->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
266 * @covers \MediaWiki\Parser\ParserOutput::getWrapperDivClass
267 * @covers \MediaWiki\Parser\ParserOutput::addWrapperDivClass
268 * @covers \MediaWiki\Parser\ParserOutput::clearWrapperDivClass
270 public function testWrapperDivClass() {
271 $po = new ParserOutput();
272 $opts = ParserOptions::newFromAnon();
273 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
275 $po->setRawText( 'Kittens' );
276 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
277 $this->assertStringContainsString( 'Kittens', $text );
278 $this->assertStringNotContainsString( '<div', $text );
279 $this->assertSame( 'Kittens', $po->getRawText() );
281 $po->addWrapperDivClass( 'foo' );
282 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
283 $this->assertStringContainsString( 'Kittens', $text );
284 $this->assertStringContainsString( '<div', $text );
285 $this->assertStringContainsString( 'class="mw-content-ltr foo"', $text );
287 $po->addWrapperDivClass( 'bar' );
288 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
289 $this->assertStringContainsString( 'Kittens', $text );
290 $this->assertStringContainsString( '<div', $text );
291 $this->assertStringContainsString( 'class="mw-content-ltr foo bar"', $text );
293 $po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
294 $text = $pipeline->run( $po, $opts, [ 'unwrap' => true ] )->getContentHolderText();
295 $this->assertStringContainsString( 'Kittens', $text );
296 $this->assertStringNotContainsString( '<div', $text );
297 $this->assertStringNotContainsString( 'class="', $text );
299 $text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => '' ] )->getContentHolderText();
300 $this->assertStringContainsString( 'Kittens', $text );
301 $this->assertStringNotContainsString( '<div', $text );
302 $this->assertStringNotContainsString( 'class="', $text );
304 $text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => 'xyzzy' ] )->getContentHolderText();
305 $this->assertStringContainsString( 'Kittens', $text );
306 $this->assertStringContainsString( '<div', $text );
307 $this->assertStringContainsString( 'class="mw-content-ltr xyzzy"', $text );
308 $this->assertStringNotContainsString( 'foo bar', $text );
310 $text = $po->getRawText();
311 $this->assertSame( 'Kittens', $text );
313 $po->clearWrapperDivClass();
314 $text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
315 $this->assertStringContainsString( 'Kittens', $text );
316 $this->assertStringNotContainsString( '<div', $text );
317 $this->assertStringNotContainsString( 'class="', $text );
321 * This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
322 * ParserOutput::getText gets deprecated.
323 * @covers \MediaWiki\Parser\ParserOutput::getText
324 * @dataProvider provideGetText
325 * @param array $options Options to getText()
326 * @param string $text Parser text
327 * @param string $expect Expected output
329 public function testGetText( $options, $text, $expect ) {
330 // Avoid other skins affecting the section edit links
331 $this->overrideConfigValue( MainConfigNames::DefaultSkin, 'fallback' );
332 RequestContext::resetMain();
334 $this->overrideConfigValues( [
335 MainConfigNames::ScriptPath => '/w',
336 MainConfigNames::Script => '/w/index.php',
337 ] );
339 $po = new ParserOutput( $text );
340 self::initSections( $po );
341 $actual = $po->getText( $options );
342 $this->assertSame( $expect, $actual );
345 private static function initSections( ParserOutput $po ): void {
346 $po->setTOCData( new TOCData(
347 SectionMetadata::fromLegacy( [
348 'index' => "1",
349 'level' => 1,
350 'toclevel' => 1,
351 'number' => "1",
352 'line' => "Section 1",
353 'anchor' => "Section_1"
354 ] ),
355 SectionMetadata::fromLegacy( [
356 'index' => "2",
357 'level' => 1,
358 'toclevel' => 1,
359 'number' => "2",
360 'line' => "Section 2",
361 'anchor' => "Section_2"
362 ] ),
363 SectionMetadata::fromLegacy( [
364 'index' => "3",
365 'level' => 2,
366 'toclevel' => 2,
367 'number' => "2.1",
368 'line' => "Section 2.1",
369 'anchor' => "Section_2.1"
370 ] ),
371 SectionMetadata::fromLegacy( [
372 'index' => "4",
373 'level' => 1,
374 'toclevel' => 1,
375 'number' => "3",
376 'line' => "Section 3",
377 'anchor' => "Section_3"
378 ] ),
379 ) );
382 public static function provideGetText() {
383 $text = <<<EOF
384 <p>Test document.
385 </p>
386 <meta property="mw:PageProp/toc" />
387 <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>
388 <p>One
389 </p>
390 <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>
391 <p>Two
392 </p>
393 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
394 <p>Two point one
395 </p>
396 <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>
397 <p>Three
398 </p>
399 EOF;
401 $dedupText = <<<EOF
402 <p>This is a test document.</p>
403 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
404 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
405 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
406 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
407 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
408 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
409 <style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
410 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
411 <style>.Duplicate1 {}</style>
412 EOF;
414 return [
415 'No options' => [
416 [], $text, <<<EOF
417 <p>Test document.
418 </p>
419 <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>
420 <ul>
421 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
422 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
423 <ul>
424 <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>
425 </ul>
426 </li>
427 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
428 </ul>
429 </div>
431 <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>
432 <p>One
433 </p>
434 <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>
435 <p>Two
436 </p>
437 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
438 <p>Two point one
439 </p>
440 <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>
441 <p>Three
442 </p>
445 'Disable section edit links' => [
446 [ 'enableSectionEditLinks' => false ], $text, <<<EOF
447 <p>Test document.
448 </p>
449 <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>
450 <ul>
451 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
452 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
453 <ul>
454 <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>
455 </ul>
456 </li>
457 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
458 </ul>
459 </div>
461 <div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2></div>
462 <p>One
463 </p>
464 <div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2></div>
465 <p>Two
466 </p>
467 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
468 <p>Two point one
469 </p>
470 <div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2></div>
471 <p>Three
472 </p>
475 'Disable TOC, but wrap' => [
476 [ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
477 <div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"><p>Test document.
478 </p>
480 <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>
481 <p>One
482 </p>
483 <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>
484 <p>Two
485 </p>
486 <div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
487 <p>Two point one
488 </p>
489 <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>
490 <p>Three
491 </p></div>
494 'Style deduplication' => [
495 [], $dedupText, <<<EOF
496 <p>This is a test document.</p>
497 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
498 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
499 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
500 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
501 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2">
502 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
503 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
504 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
505 <style>.Duplicate1 {}</style>
508 'Style deduplication disabled' => [
509 [ 'deduplicateStyles' => false ], $dedupText, $dedupText
512 // phpcs:enable
516 * @covers \MediaWiki\Parser\ParserOutput::hasText
518 public function testHasText() {
519 $po = new ParserOutput( '' );
520 $this->assertTrue( $po->hasText() );
522 $po = new ParserOutput( null );
523 $this->assertFalse( $po->hasText() );
525 $po = new ParserOutput();
526 $this->assertFalse( $po->hasText() );
528 $po = new ParserOutput( '' );
529 $this->assertTrue( $po->hasText() );
531 $po = new ParserOutput( null );
532 $po->setRawText( '' );
533 $this->assertTrue( $po->hasText() );
535 $po = new ParserOutput( 'foo' );
536 $po->setRawText( null );
537 $this->assertFalse( $po->hasText() );
541 * This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
542 * ParserOutput::getText gets deprecated.
543 * @covers \MediaWiki\Parser\ParserOutput::getText
545 public function testGetText_failsIfNoText() {
546 $po = new ParserOutput( null );
548 $this->expectException( LogicException::class );
549 $po->getText();
553 * @covers \MediaWiki\Parser\ParserOutput::getRawText
555 public function testGetRawText_failsIfNoText() {
556 $po = new ParserOutput( null );
558 $this->expectException( LogicException::class );
559 $po->getRawText();
562 public static function provideMergeHtmlMetaDataFrom() {
563 // title text ------------
564 $a = new ParserOutput();
565 $a->setTitleText( 'X' );
566 $b = new ParserOutput();
567 yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
569 $a = new ParserOutput();
570 $b = new ParserOutput();
571 $b->setTitleText( 'Y' );
572 yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
574 $a = new ParserOutput();
575 $a->setTitleText( 'X' );
576 $b = new ParserOutput();
577 $b->setTitleText( 'Y' );
578 yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
580 // index policy ------------
581 $a = new ParserOutput();
582 $a->setIndexPolicy( 'index' );
583 $b = new ParserOutput();
584 yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
586 $a = new ParserOutput();
587 $b = new ParserOutput();
588 $b->setIndexPolicy( 'index' );
589 yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
591 $a = new ParserOutput();
592 $a->setIndexPolicy( 'noindex' );
593 $b = new ParserOutput();
594 $b->setIndexPolicy( 'index' );
595 yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
597 $a = new ParserOutput();
598 $a->setIndexPolicy( 'index' );
599 $b = new ParserOutput();
600 $b->setIndexPolicy( 'noindex' );
601 yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
603 $crhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
605 $a = new ParserOutput();
606 $a->setLanguage( $crhCyrl );
607 $b = new ParserOutput();
608 yield 'only left language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
610 $a = new ParserOutput();
611 $b = new ParserOutput();
612 $b->setLanguage( $crhCyrl );
613 yield 'only right language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
615 // head items and friends ------------
616 $a = new ParserOutput();
617 $a->addHeadItem( '<foo1>' );
618 $a->addHeadItem( '<bar1>', 'bar' );
619 $a->addModules( [ 'test-module-a' ] );
620 $a->addModuleStyles( [ 'test-module-styles-a' ] );
621 $a->setJsConfigVar( 'test-config-var-a', 'a' );
622 $a->appendJsConfigVar( 'test-config-var-c', 'abc' );
623 $a->appendJsConfigVar( 'test-config-var-c', 'def' );
624 $a->addExtraCSPStyleSrc( 'css.com' );
625 $a->addExtraCSPStyleSrc( 'css2.com' );
626 $a->addExtraCSPScriptSrc( 'js.com' );
627 $a->addExtraCSPDefaultSrc( 'img.com' );
629 $b = new ParserOutput();
630 $b->setIndexPolicy( 'noindex' );
631 $b->addHeadItem( '<foo2>' );
632 $b->addHeadItem( '<bar2>', 'bar' );
633 $b->addModules( [ 'test-module-b' ] );
634 $b->addModuleStyles( [ 'test-module-styles-b' ] );
635 $b->setJsConfigVar( 'test-config-var-b', 'b' );
636 $b->setJsConfigVar( 'test-config-var-a', 'X' );
637 $a->appendJsConfigVar( 'test-config-var-c', 'xyz' );
638 $a->appendJsConfigVar( 'test-config-var-c', 'def' );
639 $b->addExtraCSPStyleSrc( 'https://css.ca' );
640 $b->addExtraCSPScriptSrc( 'jscript.com' );
641 $b->addExtraCSPScriptSrc( 'vbscript.com' );
642 $b->addExtraCSPDefaultSrc( 'img.com/foo.jpg' );
644 // Note that overwriting test-config-var-a during the merge
645 // (as this test case does) is deprecated and will eventually
646 // throw an exception. However, at the moment it is still worth
647 // testing this case to ensure backward compatibility. (T300307)
648 yield 'head items and friends' => [ $a, $b, [
649 'getHeadItems' => [
650 '<foo1>',
651 '<foo2>',
652 'bar' => '<bar2>', // overwritten
654 'getModules' => [
655 'test-module-a',
656 'test-module-b',
658 'getModuleStyles' => [
659 'test-module-styles-a',
660 'test-module-styles-b',
662 'getJsConfigVars' => [
663 'test-config-var-a' => 'X', // overwritten
664 'test-config-var-b' => 'b',
665 'test-config-var-c' => [ // merged safely
666 'abc' => true, 'def' => true, 'xyz' => true,
669 'getExtraCSPStyleSrcs' => [
670 'css.com',
671 'css2.com',
672 'https://css.ca'
674 'getExtraCSPScriptSrcs' => [
675 'js.com',
676 'jscript.com',
677 'vbscript.com'
679 'getExtraCSPDefaultSrcs' => [
680 'img.com',
681 'img.com/foo.jpg'
683 ] ];
685 // TOC ------------
686 $a = new ParserOutput( '' );
687 $a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
689 $b = new ParserOutput( '' );
690 $b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
692 yield 'concat TOC' => [ $a, $b, [
693 'getSections' => [
694 SectionMetadata::fromLegacy( [ 'fromtitle' => 'A1' ] )->toLegacy(),
695 SectionMetadata::fromLegacy( [ 'fromtitle' => 'A2' ] )->toLegacy(),
696 SectionMetadata::fromLegacy( [ 'fromtitle' => 'B1' ] )->toLegacy(),
697 SectionMetadata::fromLegacy( [ 'fromtitle' => 'B2' ] )->toLegacy()
699 ] ];
701 // Skin Control ------------
702 $a = new ParserOutput();
703 $a->setNewSection( true );
704 $a->setHideNewSection( true );
705 $a->setNoGallery( true );
706 $a->addWrapperDivClass( 'foo' );
708 $a->setIndicator( 'foo', 'Foo!' );
709 $a->setIndicator( 'bar', 'Bar!' );
711 $a->setExtensionData( 'foo', 'Foo!' );
712 $a->setExtensionData( 'bar', 'Bar!' );
713 $a->appendExtensionData( 'bat', 'abc' );
715 $b = new ParserOutput();
716 $b->setNoGallery( true );
717 $b->setEnableOOUI( true );
718 $b->setPreventClickjacking( true );
719 $a->addWrapperDivClass( 'bar' );
721 $b->setIndicator( 'zoo', 'Zoo!' );
722 $b->setIndicator( 'bar', 'Barrr!' );
724 $b->setExtensionData( 'zoo', 'Zoo!' );
725 $b->setExtensionData( 'bar', 'Barrr!' );
726 $b->appendExtensionData( 'bat', 'xyz' );
728 // Note that overwriting extension data during the merge
729 // (as this test case does for 'bar') is deprecated and will eventually
730 // throw an exception. However, at the moment it is still worth
731 // testing this case to ensure backward compatibility. (T300981)
732 yield 'skin control flags' => [ $a, $b, [
733 'getNewSection' => true,
734 'getHideNewSection' => true,
735 'getNoGallery' => true,
736 'getEnableOOUI' => true,
737 'getPreventClickjacking' => true,
738 'getIndicators' => [
739 'foo' => 'Foo!',
740 'bar' => 'Barrr!', // overwritten
741 'zoo' => 'Zoo!',
743 'getWrapperDivClass' => 'foo bar',
744 '$mExtensionData' => [
745 'foo' => 'Foo!',
746 'bar' => 'Barrr!', // overwritten
747 'zoo' => 'Zoo!',
748 // internal strategy key is exposed here because we're looking
749 // at the raw property value, not using getExtensionData()
750 'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
752 ] ];
756 * @dataProvider provideMergeHtmlMetaDataFrom
757 * @covers \MediaWiki\Parser\ParserOutput::mergeHtmlMetaDataFrom
759 * @param ParserOutput $a
760 * @param ParserOutput $b
761 * @param array $expected
763 public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
764 $a->mergeHtmlMetaDataFrom( $b );
766 $this->assertFieldValues( $a, $expected );
768 // test twice, to make sure the operation is idempotent (except for the TOC, see below)
769 $a->mergeHtmlMetaDataFrom( $b );
771 // XXX: TOC joining should get smarter. Can we make it idempotent as well?
772 unset( $expected['getSections'] );
774 $this->assertFieldValues( $a, $expected );
777 private function assertFieldValues( ParserOutput $po, $expected ) {
778 $po = TestingAccessWrapper::newFromObject( $po );
780 foreach ( $expected as $method => $value ) {
781 $canonicalize = false;
782 if ( $method[0] === '$' ) {
783 $field = substr( $method, 1 );
784 $actual = $po->__get( $field );
785 } elseif ( str_contains( $method, '!' ) ) {
786 [ $trimmedMethod, $ignore ] = explode( '!', $method, 2 );
787 $args = $value['_args_'] ?? [];
788 unset( $value['_args_'] );
789 $actual = $po->__call( $trimmedMethod, $args );
790 } else {
791 $actual = $po->__call( $method, [] );
793 if ( $method === 'getJsConfigVars' ) {
794 $canonicalize = true;
797 if ( $canonicalize ) {
798 // order of entries isn't significant
799 $this->assertEqualsCanonicalizing( $value, $actual, $method );
800 } else {
801 $this->assertEquals( $value, $actual, $method );
807 * @covers \MediaWiki\Parser\ParserOutput::addLink
808 * @covers \MediaWiki\Parser\ParserOutput::getLinks
809 * @covers \MediaWiki\Parser\ParserOutput::getLinkList
811 public function testAddLink() {
812 $a = new ParserOutput();
813 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
814 $a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
815 $a->addLink( new TitleValue( NS_MAIN, 'Goats_786827346' ) );
816 # fragments are stripped for local links
817 $a->addLink( new TitleValue( NS_TALK, 'Puppies', 'Topic' ), 17 );
819 $expected = [
820 NS_MAIN => [ 'Kittens' => 6, 'Goats_786827346' => 0 ],
821 NS_TALK => [ 'Kittens' => 16, 'Puppies' => 17 ]
823 $this->assertSame( $expected, $a->getLinks() );
824 $expected = [
826 'link' => new TitleValue( NS_MAIN, 'Kittens' ),
827 'pageid' => 6,
830 'link' => new TitleValue( NS_MAIN, 'Goats_786827346' ),
831 'pageid' => 0,
834 'link' => new TitleValue( NS_TALK, 'Kittens' ),
835 'pageid' => 16,
838 'link' => new TitleValue( NS_TALK, 'Puppies' ),
839 'pageid' => 17,
842 $this->assertEquals( $expected, $a->getLinkList( ParserOutputLinkTypes::LOCAL ) );
845 public static function provideMergeTrackingMetaDataFrom() {
846 // links ------------
847 $a = new ParserOutput();
848 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
849 $a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
850 # fragments are stripped in local links
851 $a->addLink( new TitleValue( NS_MAIN, 'Goats', 'Kids' ), 7 );
853 $a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
855 $a->addLanguageLink( new TitleValue( NS_MAIN, 'de', '', 'de' ) );
856 # fragments are preserved in language links
857 $a->addLanguageLink( new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ) );
858 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
859 # fragments are stripped in interwiki links
860 $a->addInterwikiLink( new TitleValue( NS_MAIN, 'Kittens RU', 'ru', 'ru' ) );
861 $a->addExternalLink( 'https://kittens.wikimedia.test' );
862 # fragments are preserved in external links
863 $a->addExternalLink( 'https://goats.wikimedia.test#kids' );
865 # fragments are stripped for categories (syntax is overloaded for sort)
866 $a->addCategory( new TitleValue( NS_CATEGORY, 'Foo', 'bar' ), 'X' );
867 # fragments are stripped for images
868 $a->addImage( new TitleValue( NS_FILE, 'Billy.jpg', 'fragment' ), '20180101000013', 'DEAD' );
869 # fragments are stripped for links to special pages
870 $a->addLink( new TitleValue( NS_SPECIAL, 'Version', 'section' ) );
872 $b = new ParserOutput();
873 $b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
874 $b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
875 $b->addLink( new TitleValue( NS_MAIN, 'Dragons' ), 8 );
876 $b->addLink( new TitleValue( NS_FILE, 'Dragons.jpg' ), 28 );
878 # fragments are stripped from template links
879 $b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons', 'red' ), 108, 1108 );
880 $a->addTemplate( new TitleValue( NS_MAIN, 'Dragons', 'platinum' ), 118, 1118 );
882 $b->addLanguageLink( new TitleValue( NS_MAIN, 'fr', '', 'fr' ) );
883 $b->addLanguageLink( new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ) );
884 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
885 $b->addInterwikiLink( new TitleValue( NS_MAIN, 'Dragons RU', '', 'ru' ) );
886 $b->addExternalLink( 'https://dragons.wikimedia.test' );
887 $b->addExternalLink( 'https://goats.wikimedia.test#kids' );
889 $b->addCategory( 'Bar', 'Y' );
890 $b->addImage( new TitleValue( NS_FILE, 'Puff.jpg' ), '20180101000017', 'BEEF' );
892 yield 'all kinds of links' => [ $a, $b, [
893 'getLinks' => [
894 NS_MAIN => [
895 'Kittens' => 6,
896 'Goats' => 7,
897 'Dragons' => 8,
899 NS_TALK => [
900 'Kittens' => 16,
901 'Goats' => 17,
903 NS_FILE => [
904 'Dragons.jpg' => 28,
907 'getLinkList!LOCAL' => [
908 '_args_' => [ ParserOutputLinkTypes::LOCAL ],
910 'link' => new TitleValue( NS_MAIN, 'Kittens' ),
911 'pageid' => 6,
914 'link' => new TitleValue( NS_MAIN, 'Goats' ),
915 'pageid' => 7,
918 'link' => new TitleValue( NS_MAIN, 'Dragons' ),
919 'pageid' => 8,
922 'link' => new TitleValue( NS_TALK, 'Kittens' ),
923 'pageid' => 16,
926 'link' => new TitleValue( NS_TALK, 'Goats' ),
927 'pageid' => 17,
930 'link' => new TitleValue( NS_FILE, 'Dragons.jpg' ),
931 'pageid' => 28,
934 'getTemplates' => [
935 NS_MAIN => [
936 'Dragons' => 118,
938 NS_TEMPLATE => [
939 'Dragons' => 108,
940 'Goats' => 107,
943 'getTemplateIds' => [
944 NS_MAIN => [
945 'Dragons' => 1118,
947 NS_TEMPLATE => [
948 'Dragons' => 1108,
949 'Goats' => 1107,
952 'getLinkList!TEMPLATE' => [
953 '_args_' => [ ParserOutputLinkTypes::TEMPLATE ],
955 'link' => new TitleValue( NS_TEMPLATE, 'Goats' ),
956 'pageid' => 107,
957 'revid' => 1107,
960 'link' => new TitleValue( NS_TEMPLATE, 'Dragons' ),
961 'pageid' => 108,
962 'revid' => 1108,
965 'link' => new TitleValue( NS_MAIN, 'Dragons' ),
966 'pageid' => 118,
967 'revid' => 1118,
970 'getLanguageLinks' => [ 'de:de', 'ru:ru#ru', 'fr:fr' ],
971 'getLinkList!LANGUAGE' => [
972 '_args_' => [ ParserOutputLinkTypes::LANGUAGE ],
974 'link' => new TitleValue( NS_MAIN, 'de', '', 'de' ),
977 'link' => new TitleValue( NS_MAIN, 'ru', 'ru', 'ru' ),
980 'link' => new TitleValue( NS_MAIN, 'fr', '', 'fr' ),
983 'getInterwikiLinks' => [
984 'de' => [ 'Kittens_DE' => 1 ],
985 'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
986 'fr' => [ 'Kittens_FR' => 1 ],
988 'getLinkList!INTERWIKI' => [
989 '_args_' => [ ParserOutputLinkTypes::INTERWIKI ],
991 'link' => new TitleValue( NS_MAIN, 'Kittens_DE', '', 'de' ),
994 'link' => new TitleValue( NS_MAIN, 'Kittens_RU', '', 'ru' ),
997 'link' => new TitleValue( NS_MAIN, 'Dragons_RU', '', 'ru' ),
1000 'link' => new TitleValue( NS_MAIN, 'Kittens_FR', '', 'fr' ),
1003 'getCategoryMap' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
1004 'getLinkList!CATEGORY' => [
1005 '_args_' => [ ParserOutputLinkTypes::CATEGORY ],
1007 'link' => new TitleValue( NS_CATEGORY, 'Foo' ),
1008 'sort' => 'X',
1011 'link' => new TitleValue( NS_CATEGORY, 'Bar' ),
1012 'sort' => 'Y',
1015 'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
1016 'getFileSearchOptions' => [
1017 'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
1018 'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
1020 'getLinkList!MEDIA' => [
1021 '_args_' => [ ParserOutputLinkTypes::MEDIA ],
1023 'link' => new TitleValue( NS_FILE, 'Billy.jpg' ),
1024 'time' => '20180101000013',
1025 'sha1' => 'DEAD',
1028 'link' => new TitleValue( NS_FILE, 'Puff.jpg' ),
1029 'time' => '20180101000017',
1030 'sha1' => 'BEEF',
1033 'getExternalLinks' => [
1034 'https://dragons.wikimedia.test' => 1,
1035 'https://kittens.wikimedia.test' => 1,
1036 'https://goats.wikimedia.test#kids' => 1,
1038 'getLinkList!SPECIAL' => [
1039 '_args_' => [ ParserOutputLinkTypes::SPECIAL ],
1041 'link' => new TitleValue( NS_SPECIAL, 'Version' ),
1044 ] ];
1046 // properties ------------
1047 $a = new ParserOutput();
1049 $a->setPageProperty( 'foo', 'Foo!' );
1050 $a->setPageProperty( 'bar', 'Bar!' );
1052 $a->setExtensionData( 'foo', 'Foo!' );
1053 $a->setExtensionData( 'bar', 'Bar!' );
1054 $a->appendExtensionData( 'bat', 'abc' );
1056 $b = new ParserOutput();
1058 $b->setPageProperty( 'zoo', 'Zoo!' );
1059 $b->setPageProperty( 'bar', 'Barrr!' );
1061 $b->setExtensionData( 'zoo', 'Zoo!' );
1062 $b->setExtensionData( 'bar', 'Barrr!' );
1063 $b->appendExtensionData( 'bat', 'xyz' );
1065 // Note that overwriting extension data during the merge
1066 // (as this test case does for 'bar') is deprecated and will eventually
1067 // throw an exception. However, at the moment it is still worth
1068 // testing this case to ensure backward compatibility. (T300981)
1069 yield 'properties' => [ $a, $b, [
1070 'getPageProperties' => [
1071 'foo' => 'Foo!',
1072 'bar' => 'Barrr!', // overwritten
1073 'zoo' => 'Zoo!',
1075 '$mExtensionData' => [
1076 'foo' => 'Foo!',
1077 'bar' => 'Barrr!', // overwritten
1078 'zoo' => 'Zoo!',
1079 // internal strategy key is exposed here because we're looking
1080 // at the raw property value, not using getExtensionData()
1081 'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
1083 ] ];
1087 * @dataProvider provideMergeTrackingMetaDataFrom
1088 * @covers \MediaWiki\Parser\ParserOutput::mergeTrackingMetaDataFrom
1090 * @param ParserOutput $a
1091 * @param ParserOutput $b
1092 * @param array $expected
1094 public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
1095 $a->mergeTrackingMetaDataFrom( $b );
1097 $this->assertFieldValues( $a, $expected );
1099 // test twice, to make sure the operation is idempotent
1100 $a->mergeTrackingMetaDataFrom( $b );
1102 $this->assertFieldValues( $a, $expected );
1106 * @dataProvider provideMergeTrackingMetaDataFrom
1107 * @covers \MediaWiki\Parser\ParserOutput::collectMetadata
1109 * @param ParserOutput $a
1110 * @param ParserOutput $b
1111 * @param array $expected
1113 public function testCollectMetaData( ParserOutput $a, ParserOutput $b, $expected ) {
1114 $b->collectMetadata( $a );
1116 $this->assertFieldValues( $a, $expected );
1119 public function provideMergeInternalMetaDataFrom() {
1120 $this->filterDeprecated( '/^.*CacheTime::setCacheTime called with -1 as an argument/' );
1122 // flags & co
1123 $a = new ParserOutput();
1125 $a->addWarningMsg( 'duplicate-args-warning', 'A', 'B', 'C' );
1126 $a->addWarningMsg( 'template-loop-warning', 'D' );
1128 $a->setOutputFlag( 'foo' );
1129 $a->setOutputFlag( 'bar' );
1131 $a->recordOption( 'Foo' );
1132 $a->recordOption( 'Bar' );
1134 $b = new ParserOutput();
1136 $b->addWarningMsg( 'template-equals-warning' );
1137 $b->addWarningMsg( 'template-loop-warning', 'D' );
1139 $b->setOutputFlag( 'zoo' );
1140 $b->setOutputFlag( 'bar' );
1142 $b->recordOption( 'Zoo' );
1143 $b->recordOption( 'Bar' );
1145 yield 'flags' => [ $a, $b, [
1146 'getWarnings' => [
1147 wfMessage( 'duplicate-args-warning', 'A', 'B', 'C' )->text(),
1148 wfMessage( 'template-loop-warning', 'D' )->text(),
1149 wfMessage( 'template-equals-warning' )->text(),
1151 '$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
1152 'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
1153 ] ];
1155 // cache time
1156 $someTime = "20240207202040";
1157 $someLaterTime = "20240207202112";
1158 $a = new ParserOutput();
1159 $a->setCacheTime( $someTime );
1160 $b = new ParserOutput();
1161 yield 'only left cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
1163 $a = new ParserOutput();
1164 $b = new ParserOutput();
1165 $b->setCacheTime( $someTime );
1166 yield 'only right cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
1168 $a = new ParserOutput();
1169 $b = new ParserOutput();
1170 $a->setCacheTime( $someLaterTime );
1171 $b->setCacheTime( $someTime );
1172 yield 'left has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
1174 $a = new ParserOutput();
1175 $b = new ParserOutput();
1176 $a->setCacheTime( $someTime );
1177 $b->setCacheTime( $someLaterTime );
1178 yield 'right has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
1180 $a = new ParserOutput();
1181 $b = new ParserOutput();
1182 $a->setCacheTime( -1 );
1183 $b->setCacheTime( $someTime );
1184 yield 'left is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
1186 $a = new ParserOutput();
1187 $b = new ParserOutput();
1188 $a->setCacheTime( $someTime );
1189 $b->setCacheTime( -1 );
1190 yield 'right is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
1192 // timestamp ------------
1193 $a = new ParserOutput();
1194 $a->setRevisionTimestamp( '20180101000011' );
1195 $b = new ParserOutput();
1196 yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1198 $a = new ParserOutput();
1199 $b = new ParserOutput();
1200 $b->setRevisionTimestamp( '20180101000011' );
1201 yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1203 $a = new ParserOutput();
1204 $a->setRevisionTimestamp( '20180101000011' );
1205 $b = new ParserOutput();
1206 $b->setRevisionTimestamp( '20180101000001' );
1207 yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1209 $a = new ParserOutput();
1210 $a->setRevisionTimestamp( '20180101000001' );
1211 $b = new ParserOutput();
1212 $b->setRevisionTimestamp( '20180101000011' );
1213 yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
1215 // speculative rev id ------------
1216 $a = new ParserOutput();
1217 $a->setSpeculativeRevIdUsed( 9 );
1218 $b = new ParserOutput();
1219 yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1221 $a = new ParserOutput();
1222 $b = new ParserOutput();
1223 $b->setSpeculativeRevIdUsed( 9 );
1224 yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1226 $a = new ParserOutput();
1227 $a->setSpeculativeRevIdUsed( 9 );
1228 $b = new ParserOutput();
1229 $b->setSpeculativeRevIdUsed( 9 );
1230 yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
1232 // limit report (recursive max) ------------
1233 $a = new ParserOutput();
1235 $a->setLimitReportData( 'naive1', 7 );
1236 $a->setLimitReportData( 'naive2', 27 );
1238 $a->setLimitReportData( 'limitreport-simple1', 7 );
1239 $a->setLimitReportData( 'limitreport-simple2', 27 );
1241 $a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
1242 $a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
1244 $a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
1245 $a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
1247 $a->setLimitReportData( 'limitreport-only-a', 13 );
1249 $b = new ParserOutput();
1251 $b->setLimitReportData( 'naive1', 17 );
1252 $b->setLimitReportData( 'naive2', 17 );
1254 $b->setLimitReportData( 'limitreport-simple1', 17 );
1255 $b->setLimitReportData( 'limitreport-simple2', 17 );
1257 $b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
1258 $b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
1260 $b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
1261 $b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
1263 $b->setLimitReportData( 'limitreport-only-b', 23 );
1265 // first write wins
1266 yield 'limit report' => [ $a, $b, [
1267 'getLimitReportData' => [
1268 'naive1' => 7,
1269 'naive2' => 27,
1270 'limitreport-simple1' => 7,
1271 'limitreport-simple2' => 27,
1272 'limitreport-pair1' => [ 7, 9 ],
1273 'limitreport-pair2' => [ 27, 29 ],
1274 'limitreport-more1' => [ 7, 9, 1 ],
1275 'limitreport-more2' => [ 27, 29, 21 ],
1276 'limitreport-only-a' => 13,
1278 'getLimitReportJSData' => [
1279 'naive1' => 7,
1280 'naive2' => 27,
1281 'limitreport' => [
1282 'simple1' => 7,
1283 'simple2' => 27,
1284 'pair1' => [ 'value' => 7, 'limit' => 9 ],
1285 'pair2' => [ 'value' => 27, 'limit' => 29 ],
1286 'more1' => [ 7, 9, 1 ],
1287 'more2' => [ 27, 29, 21 ],
1288 'only-a' => 13,
1291 ] ];
1293 MWDebug::clearDeprecationFilters();
1297 * @dataProvider provideMergeInternalMetaDataFrom
1298 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1300 * @param ParserOutput $a
1301 * @param ParserOutput $b
1302 * @param array $expected
1304 public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
1305 $this->filterDeprecated( '/^.*CacheTime::setCacheTime called with -1 as an argument/' );
1306 $a->mergeInternalMetaDataFrom( $b );
1308 $this->assertFieldValues( $a, $expected );
1310 // test twice, to make sure the operation is idempotent
1311 $a->mergeInternalMetaDataFrom( $b );
1313 $this->assertFieldValues( $a, $expected );
1317 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1318 * @covers \MediaWiki\Parser\ParserOutput::getTimes
1319 * @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
1321 public function testMergeInternalMetaDataFrom_parseStartTime() {
1322 /** @var object $a */
1323 $a = new ParserOutput();
1324 $a = TestingAccessWrapper::newFromObject( $a );
1326 $a->resetParseStartTime();
1327 $aClocks = $a->mParseStartTime;
1329 $b = new ParserOutput();
1331 $a->mergeInternalMetaDataFrom( $b );
1332 $mergedClocks = $a->mParseStartTime;
1334 foreach ( $mergedClocks as $clock => $timestamp ) {
1335 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
1338 // try again, with times in $b also set, and later than $a's
1339 usleep( 1234 );
1341 /** @var object $b */
1342 $b = new ParserOutput();
1343 $b = TestingAccessWrapper::newFromObject( $b );
1345 $b->resetParseStartTime();
1347 $bClocks = $b->mParseStartTime;
1349 $a->mergeInternalMetaDataFrom( $b->object );
1350 $mergedClocks = $a->mParseStartTime;
1352 foreach ( $mergedClocks as $clock => $timestamp ) {
1353 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
1354 $this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
1357 // try again, with $a's times being later
1358 usleep( 1234 );
1359 $a->resetParseStartTime();
1360 $aClocks = $a->mParseStartTime;
1362 $a->mergeInternalMetaDataFrom( $b->object );
1363 $mergedClocks = $a->mParseStartTime;
1365 foreach ( $mergedClocks as $clock => $timestamp ) {
1366 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
1367 $this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
1370 // try again, with no times in $a set
1371 $a = new ParserOutput();
1372 $a = TestingAccessWrapper::newFromObject( $a );
1374 $a->mergeInternalMetaDataFrom( $b->object );
1375 $mergedClocks = $a->mParseStartTime;
1377 foreach ( $mergedClocks as $clock => $timestamp ) {
1378 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
1383 * @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
1384 * @covers \MediaWiki\Parser\ParserOutput::getTimes
1385 * @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
1386 * @covers \MediaWiki\Parser\ParserOutput::recordTimeProfile
1387 * @covers \MediaWiki\Parser\ParserOutput::getTimeProfile
1389 public function testMergeInternalMetaDataFrom_timeProfile() {
1390 /** @var object $a */
1391 $a = new ParserOutput();
1392 $a = TestingAccessWrapper::newFromObject( $a );
1394 $a->resetParseStartTime();
1395 usleep( 1234 );
1396 $a->recordTimeProfile();
1398 $aClocks = $a->mTimeProfile;
1400 // make sure a second call to recordTimeProfile has no effect
1401 usleep( 1234 );
1402 $a->recordTimeProfile();
1404 foreach ( $aClocks as $clock => $duration ) {
1405 $this->assertNotNull( $duration );
1406 $this->assertGreaterThan( 0, $duration );
1407 $this->assertSame( $aClocks[$clock], $a->getTimeProfile( $clock ) );
1410 $b = new ParserOutput();
1412 $a->mergeInternalMetaDataFrom( $b );
1413 $mergedClocks = $a->mTimeProfile;
1415 foreach ( $mergedClocks as $clock => $duration ) {
1416 $this->assertSame( $aClocks[$clock], $duration, $clock );
1419 // try again, with times in $b also set, and later than $a's
1420 $b->resetParseStartTime();
1421 usleep( 1234 );
1422 $b->recordTimeProfile();
1424 $b = TestingAccessWrapper::newFromObject( $b );
1425 $bClocks = $b->mTimeProfile;
1427 $a->mergeInternalMetaDataFrom( $b->object );
1428 $mergedClocks = $a->mTimeProfile;
1430 foreach ( $mergedClocks as $clock => $duration ) {
1431 $this->assertGreaterThanOrEqual( $aClocks[$clock], $duration, $clock );
1432 $this->assertGreaterThanOrEqual( $bClocks[$clock], $duration, $clock );
1437 * @covers \MediaWiki\Parser\ParserOutput::getCacheTime
1438 * @covers \MediaWiki\Parser\ParserOutput::setCacheTime
1440 public function testGetCacheTime() {
1441 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
1442 MWTimestamp::setFakeTime( static function () use ( &$clock ) {
1443 return $clock++;
1444 } );
1446 $po = new ParserOutput();
1447 $time = $po->getCacheTime();
1449 // Use current (fake) time by default. Ignore the last digit.
1450 // Subsequent calls must yield the exact same timestamp as the first.
1451 $this->assertStringStartsWith( '2010010100000', $time );
1452 $this->assertSame( $time, $po->getCacheTime() );
1454 // After setting, the getter must return the time that was set.
1455 $time = '20110606112233';
1456 $po->setCacheTime( $time );
1457 $this->assertSame( $time, $po->getCacheTime() );
1461 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPScriptSrc
1462 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPDefaultSrc
1463 * @covers \MediaWiki\Parser\ParserOutput::addExtraCSPStyleSrc
1464 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPScriptSrcs
1465 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPDefaultSrcs
1466 * @covers \MediaWiki\Parser\ParserOutput::getExtraCSPStyleSrcs
1468 public function testCSPSources() {
1469 $po = new ParserOutput;
1471 $this->assertEquals( [], $po->getExtraCSPScriptSrcs(), 'empty Script' );
1472 $this->assertEquals( [], $po->getExtraCSPStyleSrcs(), 'empty Style' );
1473 $this->assertEquals( [], $po->getExtraCSPDefaultSrcs(), 'empty Default' );
1475 $po->addExtraCSPScriptSrc( 'foo.com' );
1476 $po->addExtraCSPScriptSrc( 'bar.com' );
1477 $po->addExtraCSPDefaultSrc( 'baz.com' );
1478 $po->addExtraCSPStyleSrc( 'fred.com' );
1479 $po->addExtraCSPStyleSrc( 'xyzzy.com' );
1481 $this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs(), 'Script' );
1482 $this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs(), 'Default' );
1483 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs(), 'Style' );
1486 public function testOutputStrings() {
1487 $po = new ParserOutput;
1489 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
1490 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
1491 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
1492 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
1493 $this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
1495 $this->assertEquals( [], $po->getModules() );
1496 $this->assertEquals( [], $po->getModuleStyles() );
1497 $this->assertEquals( [], $po->getExtraCSPScriptSrcs() );
1498 $this->assertEquals( [], $po->getExtraCSPStyleSrcs() );
1499 $this->assertEquals( [], $po->getExtraCSPDefaultSrcs() );
1501 $po->appendOutputStrings( ParserOutputStringSets::MODULE, [ 'a' ] );
1502 $po->appendOutputStrings( ParserOutputStringSets::MODULE_STYLE, [ 'b' ] );
1503 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC, [ 'foo.com', 'bar.com' ] );
1504 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ 'baz.com' ] );
1505 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'fred.com' ] );
1506 $po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'xyzzy.com' ] );
1508 $this->assertEquals( [ 'a' ], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
1509 $this->assertEquals( [ 'b' ], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
1510 $this->assertEquals( [ 'foo.com', 'bar.com' ],
1511 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
1512 $this->assertEquals( [ 'baz.com' ],
1513 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
1514 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ],
1515 $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
1517 $this->assertEquals( [ 'a' ], $po->getModules() );
1518 $this->assertEquals( [ 'b' ], $po->getModuleStyles() );
1519 $this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs() );
1520 $this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs() );
1521 $this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs() );
1525 * @covers \MediaWiki\Parser\ParserOutput::getCacheTime()
1526 * @covers \MediaWiki\Parser\ParserOutput::setCacheTime()
1528 public function testCacheTime() {
1529 $po = new ParserOutput();
1531 // Should not have a cache time yet
1532 $this->assertFalse( $po->hasCacheTime() );
1533 // But calling ::get assigns a cache time
1534 $po->getCacheTime();
1535 $this->assertTrue( $po->hasCacheTime() );
1536 // Reset cache time
1537 $po->setCacheTime( "20240207202040" );
1538 $this->assertSame( "20240207202040", $po->getCacheTime() );
1542 * @covers \MediaWiki\Parser\ParserOutput::getRenderId()
1543 * @covers \MediaWiki\Parser\ParserOutput::setRenderId()
1545 public function testRenderId() {
1546 $po = new ParserOutput();
1548 // Should be null when unset
1549 $this->assertNull( $po->getRenderId() );
1551 // Sanity check for setter and getter
1552 $po->setRenderId( "TestRenderId" );
1553 $this->assertEquals( "TestRenderId", $po->getRenderId() );
1557 * @covers \MediaWiki\Parser\ParserOutput::getRenderId()
1559 public function testRenderIdBackCompat() {
1560 $po = new ParserOutput();
1562 // Parser cache used to contain extension data under a different name
1563 $po->setExtensionData( 'parsoid-render-id', "1234/LegacyRenderId" );
1564 $this->assertEquals( "LegacyRenderId", $po->getRenderId() );
1567 public function testSetFromParserOptions() {
1568 // parser output set from canonical parser options
1569 $pOptions = ParserOptions::newFromAnon();
1570 $pOutput = new ParserOutput;
1571 $pOutput->setFromParserOptions( $pOptions );
1572 $this->assertSame( 'mw-parser-output', $pOutput->getWrapperDivClass() );
1573 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
1574 $this->assertTrue( $pOutput->isCacheable() );
1575 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
1576 $this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );
1578 // set the various parser options and verify in parser output
1579 $pOptions->setWrapOutputClass( 'test-wrapper' );
1580 $pOptions->setIsPreview( true );
1581 $pOptions->setSuppressSectionEditLinks();
1582 $pOptions->setCollapsibleSections();
1583 $pOutput = new ParserOutput;
1584 $pOutput->setFromParserOptions( $pOptions );
1585 $this->assertEquals( 'test-wrapper', $pOutput->getWrapperDivClass() );
1586 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
1587 $this->assertFalse( $pOutput->isCacheable() );
1588 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
1589 $this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );