This is just a cleanup change. The exception should never happen, but if it does, this can be reverted. Change-Id: I26a7c4105d39d83015c09b779a2de3fd1ddacec1
1437 lines
49 KiB
PHP
1437 lines
49 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Parser;
|
|
|
|
use LogicException;
|
|
use MediaWiki\Context\RequestContext;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Parser\ParserOutput;
|
|
use MediaWiki\Parser\ParserOutputFlags;
|
|
use MediaWiki\Parser\ParserOutputStringSets;
|
|
use MediaWiki\Title\Title;
|
|
use MediaWiki\Title\TitleValue;
|
|
use MediaWiki\Utils\MWTimestamp;
|
|
use MediaWikiLangTestCase;
|
|
use MWDebug;
|
|
use ParserOptions;
|
|
use Wikimedia\Bcp47Code\Bcp47CodeValue;
|
|
use Wikimedia\Parsoid\Core\SectionMetadata;
|
|
use Wikimedia\Parsoid\Core\TOCData;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
use Wikimedia\Tests\SerializationTestTrait;
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput
|
|
* @covers \CacheTime
|
|
* @group Database
|
|
* ^--- trigger DB shadowing because we are using Title magic
|
|
*/
|
|
class ParserOutputTest extends MediaWikiLangTestCase {
|
|
use SerializationTestTrait;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
MWTimestamp::setFakeTime( ParserCacheSerializationTestCases::FAKE_TIME );
|
|
$this->overrideConfigValue(
|
|
MainConfigNames::ParserCacheExpireTime,
|
|
ParserCacheSerializationTestCases::FAKE_CACHE_EXPIRY
|
|
);
|
|
// Serialization tests still use these methods.
|
|
$this->hideDeprecated( 'MediaWiki\Parser\ParserOutput::setTOCHTML' );
|
|
$this->hideDeprecated( 'MediaWiki\Parser\ParserOutput::getTOCHTML' );
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getClassToTest
|
|
* @return string
|
|
*/
|
|
protected function getClassToTest(): string {
|
|
return ParserOutput::class;
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getSerializedDataPath
|
|
* @return string
|
|
*/
|
|
protected function getSerializedDataPath(): string {
|
|
return __DIR__ . '/../../data/ParserCache';
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getTestInstancesAndAssertions
|
|
* @return array
|
|
*/
|
|
protected function getTestInstancesAndAssertions(): array {
|
|
return ParserCacheSerializationTestCases::getParserOutputTestCases();
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getSupportedSerializationFormats
|
|
* @return array
|
|
*/
|
|
protected function getSupportedSerializationFormats(): array {
|
|
return ParserCacheSerializationTestCases::getSupportedSerializationFormats(
|
|
$this->getClassToTest() );
|
|
}
|
|
|
|
public static function provideIsLinkInternal() {
|
|
return [
|
|
// Different domains
|
|
[ false, 'http://example.org', 'http://mediawiki.org' ],
|
|
// Same domains
|
|
[ true, 'http://example.org', 'http://example.org' ],
|
|
[ true, 'https://example.org', 'https://example.org' ],
|
|
[ true, '//example.org', '//example.org' ],
|
|
// Same domain different cases
|
|
[ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
|
|
// Paths, queries, and fragments are not relevant
|
|
[ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
|
|
[ true, 'http://example.org', 'http://example.org?my=query' ],
|
|
[ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
|
|
// Different protocols
|
|
[ false, 'http://example.org', 'https://example.org' ],
|
|
[ false, 'https://example.org', 'http://example.org' ],
|
|
// Protocol relative servers always match http and https links
|
|
[ true, '//example.org', 'http://example.org' ],
|
|
[ true, '//example.org', 'https://example.org' ],
|
|
// But they don't match strange things like this
|
|
[ false, '//example.org', 'irc://example.org' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test to make sure ParserOutput::isLinkInternal behaves properly
|
|
* @dataProvider provideIsLinkInternal
|
|
* @covers \MediaWiki\Parser\ParserOutput::isLinkInternal
|
|
*/
|
|
public function testIsLinkInternal( $shouldMatch, $server, $url ) {
|
|
$this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::appendJsConfigVar
|
|
* @covers \MediaWiki\Parser\ParserOutput::setJsConfigVar
|
|
* @covers \MediaWiki\Parser\ParserOutput::getJsConfigVars
|
|
*/
|
|
public function testJsConfigVars() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setJsConfigVar( 'a', '1' );
|
|
$po->appendJsConfigVar( 'b', 'a' );
|
|
$po->appendJsConfigVar( 'b', '0' );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'a' => 1,
|
|
'b' => [ 'a' => true, '0' => true ],
|
|
], $po->getJsConfigVars() );
|
|
|
|
$po->setJsConfigVar( 'c', '2' );
|
|
$po->appendJsConfigVar( 'b', 'b' );
|
|
$po->appendJsConfigVar( 'b', '1' );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'a' => 1,
|
|
'b' => [ 'a' => true, 'b' => true, '0' => true, '1' => true ],
|
|
'c' => 2,
|
|
], $po->getJsConfigVars() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::appendExtensionData
|
|
* @covers \MediaWiki\Parser\ParserOutput::setExtensionData
|
|
* @covers \MediaWiki\Parser\ParserOutput::getExtensionData
|
|
*/
|
|
public function testExtensionData() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setExtensionData( "one", "Foo" );
|
|
$po->appendExtensionData( "three", "abc" );
|
|
|
|
$this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
|
|
$this->assertNull( $po->getExtensionData( "spam" ) );
|
|
|
|
$po->setExtensionData( "two", "Bar" );
|
|
$this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
|
|
$this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
|
|
|
|
// Note that overwriting extension data (as this test case
|
|
// does) is deprecated and will eventually throw an
|
|
// exception. However, at the moment it is still worth testing
|
|
// this case to ensure backward compatibility. (T300981)
|
|
$po->setExtensionData( "one", null );
|
|
$this->assertNull( $po->getExtensionData( "one" ) );
|
|
$this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'abc' => true,
|
|
], $po->getExtensionData( "three" ) );
|
|
|
|
$po->appendExtensionData( "three", "xyz" );
|
|
$this->assertEqualsCanonicalizing( [
|
|
'abc' => true,
|
|
'xyz' => true,
|
|
], $po->getExtensionData( "three" ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::setPageProperty
|
|
* @covers \MediaWiki\Parser\ParserOutput::getPageProperty
|
|
* @covers \MediaWiki\Parser\ParserOutput::unsetPageProperty
|
|
* @covers \MediaWiki\Parser\ParserOutput::getPageProperties
|
|
*/
|
|
public function testProperties() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setPageProperty( 'foo', 'val' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( 'val', $po->getPageProperty( 'foo' ) );
|
|
$this->assertSame( 'val', $properties['foo'] );
|
|
|
|
$po->setPageProperty( 'foo', 'second val' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( 'second val', $po->getPageProperty( 'foo' ) );
|
|
$this->assertSame( 'second val', $properties['foo'] );
|
|
|
|
$po->unsetPageProperty( 'foo' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( null, $po->getPageProperty( 'foo' ) );
|
|
$this->assertArrayNotHasKey( 'foo', $properties );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::setLanguage
|
|
* @covers \MediaWiki\Parser\ParserOutput::getLanguage
|
|
*/
|
|
public function testLanguage() {
|
|
$po = new ParserOutput();
|
|
|
|
$langFr = new Bcp47CodeValue( 'fr' );
|
|
$langCrhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
|
|
|
|
// Fallback to null
|
|
$this->assertSame( null, $po->getLanguage() );
|
|
|
|
// Simple case
|
|
$po->setLanguage( $langFr );
|
|
$this->assertSame( $langFr->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
|
|
|
|
// Language with a variant
|
|
$po->setLanguage( $langCrhCyrl );
|
|
$this->assertSame( $langCrhCyrl->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getWrapperDivClass
|
|
* @covers \MediaWiki\Parser\ParserOutput::addWrapperDivClass
|
|
* @covers \MediaWiki\Parser\ParserOutput::clearWrapperDivClass
|
|
* @covers \MediaWiki\Parser\ParserOutput::getText
|
|
*/
|
|
public function testWrapperDivClass() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setRawText( 'Kittens' );
|
|
$this->assertStringContainsString( 'Kittens', $po->getText() );
|
|
$this->assertStringNotContainsString( '<div', $po->getText() );
|
|
$this->assertSame( 'Kittens', $po->getRawText() );
|
|
|
|
$po->addWrapperDivClass( 'foo' );
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr foo"', $text );
|
|
|
|
$po->addWrapperDivClass( 'bar' );
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr foo bar"', $text );
|
|
|
|
$po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
|
|
$text = $po->getText( [ 'unwrap' => true ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
|
|
$text = $po->getText( [ 'wrapperDivClass' => '' ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
|
|
$text = $po->getText( [ 'wrapperDivClass' => 'xyzzy' ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr xyzzy"', $text );
|
|
$this->assertStringNotContainsString( 'foo bar', $text );
|
|
|
|
$text = $po->getRawText();
|
|
$this->assertSame( 'Kittens', $text );
|
|
|
|
$po->clearWrapperDivClass();
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @covers \MediaWiki\Parser\ParserOutput::getText
|
|
* @dataProvider provideGetText
|
|
* @param array $options Options to getText()
|
|
* @param string $text Parser text
|
|
* @param string $expect Expected output
|
|
*/
|
|
public function testGetText( $options, $text, $expect ) {
|
|
// Avoid other skins affecting the section edit links
|
|
$this->overrideConfigValue( MainConfigNames::DefaultSkin, 'fallback' );
|
|
RequestContext::resetMain();
|
|
|
|
$this->overrideConfigValues( [
|
|
MainConfigNames::ArticlePath => '/wiki/$1',
|
|
MainConfigNames::ScriptPath => '/w',
|
|
MainConfigNames::Script => '/w/index.php',
|
|
] );
|
|
|
|
$po = new ParserOutput( $text );
|
|
self::initSections( $po );
|
|
$actual = $po->getText( $options );
|
|
$this->assertSame( $expect, $actual );
|
|
}
|
|
|
|
private static function initSections( ParserOutput $po ): void {
|
|
$po->setTOCData( new TOCData(
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "1",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "1",
|
|
'line' => "Section 1",
|
|
'anchor' => "Section_1"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "2",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "2",
|
|
'line' => "Section 2",
|
|
'anchor' => "Section_2"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "3",
|
|
'level' => 2,
|
|
'toclevel' => 2,
|
|
'number' => "2.1",
|
|
'line' => "Section 2.1",
|
|
'anchor' => "Section_2.1"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "4",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "3",
|
|
'line' => "Section 3",
|
|
'anchor' => "Section_3"
|
|
] ),
|
|
) );
|
|
}
|
|
|
|
public static function provideGetText() {
|
|
$text = <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<meta property="mw:PageProp/toc" />
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF;
|
|
|
|
$dedupText = <<<EOF
|
|
<p>This is a test document.</p>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
|
|
<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
|
|
<style>.Duplicate1 {}</style>
|
|
EOF;
|
|
|
|
return [
|
|
'No options' => [
|
|
[], $text, <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<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>
|
|
<ul>
|
|
<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
|
|
<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
|
|
<ul>
|
|
<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>
|
|
</ul>
|
|
</li>
|
|
<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF
|
|
],
|
|
'Disable section edit links' => [
|
|
[ 'enableSectionEditLinks' => false ], $text, <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<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>
|
|
<ul>
|
|
<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
|
|
<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
|
|
<ul>
|
|
<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>
|
|
</ul>
|
|
</li>
|
|
<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF
|
|
],
|
|
'Disable TOC, but wrap' => [
|
|
[ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
|
|
<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"><p>Test document.
|
|
</p>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Three
|
|
</p></div>
|
|
EOF
|
|
],
|
|
'Style deduplication' => [
|
|
[], $dedupText, <<<EOF
|
|
<p>This is a test document.</p>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2">
|
|
<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
|
|
<style>.Duplicate1 {}</style>
|
|
EOF
|
|
],
|
|
'Style deduplication disabled' => [
|
|
[ 'deduplicateStyles' => false ], $dedupText, $dedupText
|
|
],
|
|
];
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::hasText
|
|
*/
|
|
public function testHasText() {
|
|
$po = new ParserOutput( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( null );
|
|
$this->assertFalse( $po->hasText() );
|
|
|
|
$po = new ParserOutput();
|
|
$this->assertFalse( $po->hasText() );
|
|
|
|
$po = new ParserOutput( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( null );
|
|
$po->setRawText( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( 'foo' );
|
|
$po->setRawText( null );
|
|
$this->assertFalse( $po->hasText() );
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @covers \MediaWiki\Parser\ParserOutput::getText
|
|
*/
|
|
public function testGetText_failsIfNoText() {
|
|
$po = new ParserOutput( null );
|
|
|
|
$this->expectException( LogicException::class );
|
|
$po->getText();
|
|
}
|
|
|
|
public static function provideGetText_absoluteURLs() {
|
|
yield 'empty' => [
|
|
'text' => '',
|
|
'expectedText' => '',
|
|
];
|
|
yield 'no-links' => [
|
|
'text' => '<p>test</p>',
|
|
'expectedText' => '<p>test</p>',
|
|
];
|
|
yield 'simple link' => [
|
|
'text' => '<a href="/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'already absolute, relative' => [
|
|
'text' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'already absolute, https' => [
|
|
'text' => '<a href="https://TEST_SERVER/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="https://TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'external' => [
|
|
'text' => '<a href="https://en.wikipedia.org/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="https://en.wikipedia.org/wiki/Test">test</a>',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @dataProvider provideGetText_absoluteURLs
|
|
*/
|
|
public function testGetText_absoluteURLs( string $text, string $expectedText ) {
|
|
$this->overrideConfigValue( MainConfigNames::Server, '//TEST_SERVER' );
|
|
$parserOutput = new ParserOutput( $text );
|
|
$this->assertSame( $expectedText, $parserOutput->getText( [ 'absoluteURLs' => true ] ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getRawText
|
|
*/
|
|
public function testGetRawText_failsIfNoText() {
|
|
$po = new ParserOutput( null );
|
|
|
|
$this->expectException( LogicException::class );
|
|
$po->getRawText();
|
|
}
|
|
|
|
public static function provideMergeHtmlMetaDataFrom() {
|
|
// title text ------------
|
|
$a = new ParserOutput();
|
|
$a->setTitleText( 'X' );
|
|
$b = new ParserOutput();
|
|
yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setTitleText( 'Y' );
|
|
yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setTitleText( 'X' );
|
|
$b = new ParserOutput();
|
|
$b->setTitleText( 'Y' );
|
|
yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
|
|
|
|
// index policy ------------
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'index' );
|
|
$b = new ParserOutput();
|
|
yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'index' );
|
|
yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'noindex' );
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'index' );
|
|
yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'index' );
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'noindex' );
|
|
yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
|
|
|
|
$crhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
|
|
|
|
$a = new ParserOutput();
|
|
$a->setLanguage( $crhCyrl );
|
|
$b = new ParserOutput();
|
|
yield 'only left language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setLanguage( $crhCyrl );
|
|
yield 'only right language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
|
|
|
|
// head items and friends ------------
|
|
$a = new ParserOutput();
|
|
$a->addHeadItem( '<foo1>' );
|
|
$a->addHeadItem( '<bar1>', 'bar' );
|
|
$a->addModules( [ 'test-module-a' ] );
|
|
$a->addModuleStyles( [ 'test-module-styles-a' ] );
|
|
$a->setJsConfigVar( 'test-config-var-a', 'a' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'abc' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'def' );
|
|
$a->addExtraCSPStyleSrc( 'css.com' );
|
|
$a->addExtraCSPStyleSrc( 'css2.com' );
|
|
$a->addExtraCSPScriptSrc( 'js.com' );
|
|
$a->addExtraCSPDefaultSrc( 'img.com' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'noindex' );
|
|
$b->addHeadItem( '<foo2>' );
|
|
$b->addHeadItem( '<bar2>', 'bar' );
|
|
$b->addModules( [ 'test-module-b' ] );
|
|
$b->addModuleStyles( [ 'test-module-styles-b' ] );
|
|
$b->setJsConfigVar( 'test-config-var-b', 'b' );
|
|
$b->setJsConfigVar( 'test-config-var-a', 'X' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'xyz' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'def' );
|
|
$b->addExtraCSPStyleSrc( 'https://css.ca' );
|
|
$b->addExtraCSPScriptSrc( 'jscript.com' );
|
|
$b->addExtraCSPScriptSrc( 'vbscript.com' );
|
|
$b->addExtraCSPDefaultSrc( 'img.com/foo.jpg' );
|
|
|
|
// Note that overwriting test-config-var-a during the merge
|
|
// (as this test case does) is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300307)
|
|
yield 'head items and friends' => [ $a, $b, [
|
|
'getHeadItems' => [
|
|
'<foo1>',
|
|
'<foo2>',
|
|
'bar' => '<bar2>', // overwritten
|
|
],
|
|
'getModules' => [
|
|
'test-module-a',
|
|
'test-module-b',
|
|
],
|
|
'getModuleStyles' => [
|
|
'test-module-styles-a',
|
|
'test-module-styles-b',
|
|
],
|
|
'getJsConfigVars' => [
|
|
'test-config-var-a' => 'X', // overwritten
|
|
'test-config-var-b' => 'b',
|
|
'test-config-var-c' => [ // merged safely
|
|
'abc' => true, 'def' => true, 'xyz' => true,
|
|
],
|
|
],
|
|
'getExtraCSPStyleSrcs' => [
|
|
'css.com',
|
|
'css2.com',
|
|
'https://css.ca'
|
|
],
|
|
'getExtraCSPScriptSrcs' => [
|
|
'js.com',
|
|
'jscript.com',
|
|
'vbscript.com'
|
|
],
|
|
'getExtraCSPDefaultSrcs' => [
|
|
'img.com',
|
|
'img.com/foo.jpg'
|
|
]
|
|
] ];
|
|
|
|
// TOC ------------
|
|
$a = new ParserOutput( '' );
|
|
$a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
|
|
|
|
$b = new ParserOutput( '' );
|
|
$b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
|
|
|
|
yield 'concat TOC' => [ $a, $b, [
|
|
'getTOCHTML' => '',
|
|
'getSections' => [
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'A1' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'A2' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'B1' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'B2' ] )->toLegacy()
|
|
],
|
|
] ];
|
|
|
|
// Skin Control ------------
|
|
$a = new ParserOutput();
|
|
$a->setNewSection( true );
|
|
$a->setHideNewSection( true );
|
|
$a->setNoGallery( true );
|
|
$a->addWrapperDivClass( 'foo' );
|
|
|
|
$a->setIndicator( 'foo', 'Foo!' );
|
|
$a->setIndicator( 'bar', 'Bar!' );
|
|
|
|
$a->setExtensionData( 'foo', 'Foo!' );
|
|
$a->setExtensionData( 'bar', 'Bar!' );
|
|
$a->appendExtensionData( 'bat', 'abc' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->setNoGallery( true );
|
|
$b->setEnableOOUI( true );
|
|
$b->setPreventClickjacking( true );
|
|
$a->addWrapperDivClass( 'bar' );
|
|
|
|
$b->setIndicator( 'zoo', 'Zoo!' );
|
|
$b->setIndicator( 'bar', 'Barrr!' );
|
|
|
|
$b->setExtensionData( 'zoo', 'Zoo!' );
|
|
$b->setExtensionData( 'bar', 'Barrr!' );
|
|
$b->appendExtensionData( 'bat', 'xyz' );
|
|
|
|
// Note that overwriting extension data during the merge
|
|
// (as this test case does for 'bar') is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300981)
|
|
yield 'skin control flags' => [ $a, $b, [
|
|
'getNewSection' => true,
|
|
'getHideNewSection' => true,
|
|
'getNoGallery' => true,
|
|
'getEnableOOUI' => true,
|
|
'getPreventClickjacking' => true,
|
|
'getIndicators' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
],
|
|
'getWrapperDivClass' => 'foo bar',
|
|
'$mExtensionData' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
// internal strategy key is exposed here because we're looking
|
|
// at the raw property value, not using getExtensionData()
|
|
'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
|
|
],
|
|
] ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeHtmlMetaDataFrom
|
|
* @covers \MediaWiki\Parser\ParserOutput::mergeHtmlMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$a->mergeHtmlMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent (except for the TOC, see below)
|
|
$a->mergeHtmlMetaDataFrom( $b );
|
|
|
|
// XXX: TOC joining should get smarter. Can we make it idempotent as well?
|
|
unset( $expected['getTOCHTML'] );
|
|
unset( $expected['getSections'] );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
private function assertFieldValues( ParserOutput $po, $expected ) {
|
|
$po = TestingAccessWrapper::newFromObject( $po );
|
|
|
|
foreach ( $expected as $method => $value ) {
|
|
$canonicalize = false;
|
|
if ( $method[0] === '$' ) {
|
|
$field = substr( $method, 1 );
|
|
$actual = $po->__get( $field );
|
|
} else {
|
|
$actual = $po->__call( $method, [] );
|
|
}
|
|
if ( $method === 'getJsConfigVars' ) {
|
|
$canonicalize = true;
|
|
}
|
|
|
|
if ( $canonicalize ) {
|
|
// order of entries isn't significant
|
|
$this->assertEqualsCanonicalizing( $value, $actual, $method );
|
|
} else {
|
|
$this->assertEquals( $value, $actual, $method );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::addLink
|
|
* @covers \MediaWiki\Parser\ParserOutput::getLinks
|
|
*/
|
|
public function testAddLink() {
|
|
$a = new ParserOutput();
|
|
$a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
|
|
$a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
|
|
$a->addLink( new TitleValue( NS_MAIN, 'Goats_786827346' ) );
|
|
|
|
$expected = [
|
|
NS_MAIN => [ 'Kittens' => 6, 'Goats_786827346' => 0 ],
|
|
NS_TALK => [ 'Kittens' => 16 ]
|
|
];
|
|
$this->assertSame( $expected, $a->getLinks() );
|
|
}
|
|
|
|
public static function provideMergeTrackingMetaDataFrom() {
|
|
// links ------------
|
|
$a = new ParserOutput();
|
|
$a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
|
|
$a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
|
|
$a->addLink( new TitleValue( NS_MAIN, 'Goats' ), 7 );
|
|
|
|
$a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
|
|
|
|
$a->addLanguageLink( new TitleValue( NS_MAIN, 'de' ) );
|
|
$a->addLanguageLink( new TitleValue( NS_MAIN, 'ru' ) );
|
|
$a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
|
|
$a->addInterwikiLink( new TitleValue( NS_MAIN, 'Kittens RU', '', 'ru' ) );
|
|
$a->addExternalLink( 'https://kittens.wikimedia.test' );
|
|
$a->addExternalLink( 'https://goats.wikimedia.test' );
|
|
|
|
$a->addCategory( 'Foo', 'X' );
|
|
$a->addImage( new TitleValue( NS_FILE, 'Billy.jpg' ), '20180101000013', 'DEAD' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
|
|
$b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
|
|
$b->addLink( new TitleValue( NS_MAIN, 'Dragons' ), 8 );
|
|
$b->addLink( new TitleValue( NS_FILE, 'Dragons.jpg' ), 28 );
|
|
|
|
$b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons' ), 108, 1108 );
|
|
$a->addTemplate( new TitleValue( NS_MAIN, 'Dragons' ), 118, 1118 );
|
|
|
|
$b->addLanguageLink( new TitleValue( NS_MAIN, 'fr' ) );
|
|
$b->addLanguageLink( new TitleValue( NS_MAIN, 'ru' ) );
|
|
$b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
|
|
$b->addInterwikiLink( new TitleValue( NS_MAIN, 'Dragons RU', '', 'ru' ) );
|
|
$b->addExternalLink( 'https://dragons.wikimedia.test' );
|
|
$b->addExternalLink( 'https://goats.wikimedia.test' );
|
|
|
|
$b->addCategory( 'Bar', 'Y' );
|
|
$b->addImage( new TitleValue( NS_FILE, 'Puff.jpg' ), '20180101000017', 'BEEF' );
|
|
|
|
yield 'all kinds of links' => [ $a, $b, [
|
|
'getLinks' => [
|
|
NS_MAIN => [
|
|
'Kittens' => 6,
|
|
'Goats' => 7,
|
|
'Dragons' => 8,
|
|
],
|
|
NS_TALK => [
|
|
'Kittens' => 16,
|
|
'Goats' => 17,
|
|
],
|
|
NS_FILE => [
|
|
'Dragons.jpg' => 28,
|
|
],
|
|
],
|
|
'getTemplates' => [
|
|
NS_MAIN => [
|
|
'Dragons' => 118,
|
|
],
|
|
NS_TEMPLATE => [
|
|
'Dragons' => 108,
|
|
'Goats' => 107,
|
|
],
|
|
],
|
|
'getTemplateIds' => [
|
|
NS_MAIN => [
|
|
'Dragons' => 1118,
|
|
],
|
|
NS_TEMPLATE => [
|
|
'Dragons' => 1108,
|
|
'Goats' => 1107,
|
|
],
|
|
],
|
|
'getLanguageLinks' => [ 'de', 'ru', 'fr' ],
|
|
'getInterwikiLinks' => [
|
|
'de' => [ 'Kittens_DE' => 1 ],
|
|
'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
|
|
'fr' => [ 'Kittens_FR' => 1 ],
|
|
],
|
|
'getCategoryMap' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
|
|
'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
|
|
'getFileSearchOptions' => [
|
|
'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
|
|
'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
|
|
],
|
|
'getExternalLinks' => [
|
|
'https://dragons.wikimedia.test' => 1,
|
|
'https://kittens.wikimedia.test' => 1,
|
|
'https://goats.wikimedia.test' => 1,
|
|
]
|
|
] ];
|
|
|
|
// properties ------------
|
|
$a = new ParserOutput();
|
|
|
|
$a->setPageProperty( 'foo', 'Foo!' );
|
|
$a->setPageProperty( 'bar', 'Bar!' );
|
|
|
|
$a->setExtensionData( 'foo', 'Foo!' );
|
|
$a->setExtensionData( 'bar', 'Bar!' );
|
|
$a->appendExtensionData( 'bat', 'abc' );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->setPageProperty( 'zoo', 'Zoo!' );
|
|
$b->setPageProperty( 'bar', 'Barrr!' );
|
|
|
|
$b->setExtensionData( 'zoo', 'Zoo!' );
|
|
$b->setExtensionData( 'bar', 'Barrr!' );
|
|
$b->appendExtensionData( 'bat', 'xyz' );
|
|
|
|
// Note that overwriting extension data during the merge
|
|
// (as this test case does for 'bar') is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300981)
|
|
yield 'properties' => [ $a, $b, [
|
|
'getPageProperties' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
],
|
|
'$mExtensionData' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
// internal strategy key is exposed here because we're looking
|
|
// at the raw property value, not using getExtensionData()
|
|
'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
|
|
],
|
|
] ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeTrackingMetaDataFrom
|
|
* @covers \MediaWiki\Parser\ParserOutput::mergeTrackingMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$a->mergeTrackingMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent
|
|
$a->mergeTrackingMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
public function provideMergeInternalMetaDataFrom() {
|
|
MWDebug::filterDeprecationForTest( '/^CacheTime::setCacheTime called with -1 as an argument/' );
|
|
|
|
// flags & co
|
|
$a = new ParserOutput();
|
|
|
|
$a->addWarningMsg( 'duplicate-args-warning', 'A', 'B', 'C' );
|
|
$a->addWarningMsg( 'template-loop-warning', 'D' );
|
|
|
|
$a->setOutputFlag( 'foo' );
|
|
$a->setOutputFlag( 'bar' );
|
|
|
|
$a->recordOption( 'Foo' );
|
|
$a->recordOption( 'Bar' );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->addWarningMsg( 'template-equals-warning' );
|
|
$b->addWarningMsg( 'template-loop-warning', 'D' );
|
|
|
|
$b->setOutputFlag( 'zoo' );
|
|
$b->setOutputFlag( 'bar' );
|
|
|
|
$b->recordOption( 'Zoo' );
|
|
$b->recordOption( 'Bar' );
|
|
|
|
yield 'flags' => [ $a, $b, [
|
|
'getWarnings' => [
|
|
wfMessage( 'duplicate-args-warning', 'A', 'B', 'C' )->text(),
|
|
wfMessage( 'template-loop-warning', 'D' )->text(),
|
|
wfMessage( 'template-equals-warning' )->text(),
|
|
],
|
|
'$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
|
|
'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
|
|
] ];
|
|
|
|
// cache time
|
|
$someTime = "20240207202040";
|
|
$someLaterTime = "20240207202112";
|
|
$a = new ParserOutput();
|
|
$a->setCacheTime( $someTime );
|
|
$b = new ParserOutput();
|
|
yield 'only left cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setCacheTime( $someTime );
|
|
yield 'only right cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$a->setCacheTime( $someLaterTime );
|
|
$b->setCacheTime( $someTime );
|
|
yield 'left has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$a->setCacheTime( $someTime );
|
|
$b->setCacheTime( $someLaterTime );
|
|
yield 'right has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$a->setCacheTime( -1 );
|
|
$b->setCacheTime( $someTime );
|
|
yield 'left is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$a->setCacheTime( $someTime );
|
|
$b->setCacheTime( -1 );
|
|
yield 'right is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
|
|
|
|
// timestamp ------------
|
|
$a = new ParserOutput();
|
|
$a->setRevisionTimestamp( '20180101000011' );
|
|
$b = new ParserOutput();
|
|
yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setRevisionTimestamp( '20180101000011' );
|
|
yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setRevisionTimestamp( '20180101000011' );
|
|
$b = new ParserOutput();
|
|
$b->setRevisionTimestamp( '20180101000001' );
|
|
yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setRevisionTimestamp( '20180101000001' );
|
|
$b = new ParserOutput();
|
|
$b->setRevisionTimestamp( '20180101000011' );
|
|
yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
// speculative rev id ------------
|
|
$a = new ParserOutput();
|
|
$a->setSpeculativeRevIdUsed( 9 );
|
|
$b = new ParserOutput();
|
|
yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setSpeculativeRevIdUsed( 9 );
|
|
yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setSpeculativeRevIdUsed( 9 );
|
|
$b = new ParserOutput();
|
|
$b->setSpeculativeRevIdUsed( 9 );
|
|
yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
// limit report (recursive max) ------------
|
|
$a = new ParserOutput();
|
|
|
|
$a->setLimitReportData( 'naive1', 7 );
|
|
$a->setLimitReportData( 'naive2', 27 );
|
|
|
|
$a->setLimitReportData( 'limitreport-simple1', 7 );
|
|
$a->setLimitReportData( 'limitreport-simple2', 27 );
|
|
|
|
$a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
|
|
$a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
|
|
|
|
$a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
|
|
$a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
|
|
|
|
$a->setLimitReportData( 'limitreport-only-a', 13 );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->setLimitReportData( 'naive1', 17 );
|
|
$b->setLimitReportData( 'naive2', 17 );
|
|
|
|
$b->setLimitReportData( 'limitreport-simple1', 17 );
|
|
$b->setLimitReportData( 'limitreport-simple2', 17 );
|
|
|
|
$b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
|
|
$b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
|
|
|
|
$b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
|
|
$b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
|
|
|
|
$b->setLimitReportData( 'limitreport-only-b', 23 );
|
|
|
|
// first write wins
|
|
yield 'limit report' => [ $a, $b, [
|
|
'getLimitReportData' => [
|
|
'naive1' => 7,
|
|
'naive2' => 27,
|
|
'limitreport-simple1' => 7,
|
|
'limitreport-simple2' => 27,
|
|
'limitreport-pair1' => [ 7, 9 ],
|
|
'limitreport-pair2' => [ 27, 29 ],
|
|
'limitreport-more1' => [ 7, 9, 1 ],
|
|
'limitreport-more2' => [ 27, 29, 21 ],
|
|
'limitreport-only-a' => 13,
|
|
],
|
|
'getLimitReportJSData' => [
|
|
'naive1' => 7,
|
|
'naive2' => 27,
|
|
'limitreport' => [
|
|
'simple1' => 7,
|
|
'simple2' => 27,
|
|
'pair1' => [ 'value' => 7, 'limit' => 9 ],
|
|
'pair2' => [ 'value' => 27, 'limit' => 29 ],
|
|
'more1' => [ 7, 9, 1 ],
|
|
'more2' => [ 27, 29, 21 ],
|
|
'only-a' => 13,
|
|
],
|
|
],
|
|
] ];
|
|
|
|
MWDebug::clearDeprecationFilters();
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeInternalMetaDataFrom
|
|
* @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$this->filterDeprecated( '/^CacheTime::setCacheTime called with -1 as an argument/' );
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
|
|
* @covers \MediaWiki\Parser\ParserOutput::getTimes
|
|
* @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
|
|
*/
|
|
public function testMergeInternalMetaDataFrom_parseStartTime() {
|
|
/** @var object $a */
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->resetParseStartTime();
|
|
$aClocks = $a->mParseStartTime;
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $aClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with times in $b also set, and later than $a's
|
|
usleep( 1234 );
|
|
|
|
/** @var object $b */
|
|
$b = new ParserOutput();
|
|
$b = TestingAccessWrapper::newFromObject( $b );
|
|
|
|
$b->resetParseStartTime();
|
|
|
|
$bClocks = $b->mParseStartTime;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $aClocks[$clock], $timestamp, $clock );
|
|
$this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with $a's times being later
|
|
usleep( 1234 );
|
|
$a->resetParseStartTime();
|
|
$aClocks = $a->mParseStartTime;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $bClocks[$clock], $timestamp, $clock );
|
|
$this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with no times in $a set
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $bClocks[$clock], $timestamp, $clock );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
|
|
* @covers \MediaWiki\Parser\ParserOutput::getTimes
|
|
* @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
|
|
* @covers \MediaWiki\Parser\ParserOutput::recordTimeProfile
|
|
* @covers \MediaWiki\Parser\ParserOutput::getTimeProfile
|
|
*/
|
|
public function testMergeInternalMetaDataFrom_timeProfile() {
|
|
/** @var object $a */
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->resetParseStartTime();
|
|
usleep( 1234 );
|
|
$a->recordTimeProfile();
|
|
|
|
$aClocks = $a->mTimeProfile;
|
|
|
|
// make sure a second call to recordTimeProfile has no effect
|
|
usleep( 1234 );
|
|
$a->recordTimeProfile();
|
|
|
|
foreach ( $aClocks as $clock => $duration ) {
|
|
$this->assertNotNull( $duration );
|
|
$this->assertGreaterThan( 0, $duration );
|
|
$this->assertSame( $aClocks[$clock], $a->getTimeProfile( $clock ) );
|
|
}
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
$mergedClocks = $a->mTimeProfile;
|
|
|
|
foreach ( $mergedClocks as $clock => $duration ) {
|
|
$this->assertSame( $aClocks[$clock], $duration, $clock );
|
|
}
|
|
|
|
// try again, with times in $b also set, and later than $a's
|
|
$b->resetParseStartTime();
|
|
usleep( 1234 );
|
|
$b->recordTimeProfile();
|
|
|
|
$b = TestingAccessWrapper::newFromObject( $b );
|
|
$bClocks = $b->mTimeProfile;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mTimeProfile;
|
|
|
|
foreach ( $mergedClocks as $clock => $duration ) {
|
|
$this->assertGreaterThanOrEqual( $aClocks[$clock], $duration, $clock );
|
|
$this->assertGreaterThanOrEqual( $bClocks[$clock], $duration, $clock );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getCacheTime
|
|
* @covers \MediaWiki\Parser\ParserOutput::setCacheTime
|
|
*/
|
|
public function testGetCacheTime() {
|
|
$clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
|
|
MWTimestamp::setFakeTime( static function () use ( &$clock ) {
|
|
return $clock++;
|
|
} );
|
|
|
|
$po = new ParserOutput();
|
|
$time = $po->getCacheTime();
|
|
|
|
// Use current (fake) time per default. Ignore the last digit.
|
|
// Subsequent calls must yield the exact same timestamp as the first.
|
|
$this->assertStringStartsWith( '2010010100000', $time );
|
|
$this->assertSame( $time, $po->getCacheTime() );
|
|
|
|
// After setting, the getter must return the time that was set.
|
|
$time = '20110606112233';
|
|
$po->setCacheTime( $time );
|
|
$this->assertSame( $time, $po->getCacheTime() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::addExtraCSPScriptSrc
|
|
* @covers \MediaWiki\Parser\ParserOutput::addExtraCSPDefaultSrc
|
|
* @covers \MediaWiki\Parser\ParserOutput::addExtraCSPStyleSrc
|
|
* @covers \MediaWiki\Parser\ParserOutput::getExtraCSPScriptSrcs
|
|
* @covers \MediaWiki\Parser\ParserOutput::getExtraCSPDefaultSrcs
|
|
* @covers \MediaWiki\Parser\ParserOutput::getExtraCSPStyleSrcs
|
|
*/
|
|
public function testCSPSources() {
|
|
$po = new ParserOutput;
|
|
|
|
$this->assertEquals( [], $po->getExtraCSPScriptSrcs(), 'empty Script' );
|
|
$this->assertEquals( [], $po->getExtraCSPStyleSrcs(), 'empty Style' );
|
|
$this->assertEquals( [], $po->getExtraCSPDefaultSrcs(), 'empty Default' );
|
|
|
|
$po->addExtraCSPScriptSrc( 'foo.com' );
|
|
$po->addExtraCSPScriptSrc( 'bar.com' );
|
|
$po->addExtraCSPDefaultSrc( 'baz.com' );
|
|
$po->addExtraCSPStyleSrc( 'fred.com' );
|
|
$po->addExtraCSPStyleSrc( 'xyzzy.com' );
|
|
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs(), 'Script' );
|
|
$this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs(), 'Default' );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs(), 'Style' );
|
|
}
|
|
|
|
public function testOutputStrings() {
|
|
$po = new ParserOutput;
|
|
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
|
|
|
|
$this->assertEquals( [], $po->getModules() );
|
|
$this->assertEquals( [], $po->getModuleStyles() );
|
|
$this->assertEquals( [], $po->getExtraCSPScriptSrcs() );
|
|
$this->assertEquals( [], $po->getExtraCSPStyleSrcs() );
|
|
$this->assertEquals( [], $po->getExtraCSPDefaultSrcs() );
|
|
|
|
$po->appendOutputStrings( ParserOutputStringSets::MODULE, [ 'a' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::MODULE_STYLE, [ 'b' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC, [ 'foo.com', 'bar.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ 'baz.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'fred.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'xyzzy.com' ] );
|
|
|
|
$this->assertEquals( [ 'a' ], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
|
|
$this->assertEquals( [ 'b' ], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
|
|
$this->assertEquals( [ 'baz.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
|
|
|
|
$this->assertEquals( [ 'a' ], $po->getModules() );
|
|
$this->assertEquals( [ 'b' ], $po->getModuleStyles() );
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs() );
|
|
$this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs() );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getCacheTime()
|
|
* @covers \MediaWiki\Parser\ParserOutput::setCacheTime()
|
|
*/
|
|
public function testCacheTime() {
|
|
$po = new ParserOutput();
|
|
|
|
// Should not have a cache time yet
|
|
$this->assertFalse( $po->hasCacheTime() );
|
|
// But calling ::get assigns a cache time
|
|
$po->getCacheTime();
|
|
$this->assertTrue( $po->hasCacheTime() );
|
|
// Reset cache time
|
|
$po->setCacheTime( "20240207202040" );
|
|
$this->assertSame( "20240207202040", $po->getCacheTime() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getRenderId()
|
|
* @covers \MediaWiki\Parser\ParserOutput::setRenderId()
|
|
*/
|
|
public function testRenderId() {
|
|
$po = new ParserOutput();
|
|
|
|
// Should be null when unset
|
|
$this->assertNull( $po->getRenderId() );
|
|
|
|
// Sanity check for setter and getter
|
|
$po->setRenderId( "TestRenderId" );
|
|
$this->assertEquals( "TestRenderId", $po->getRenderId() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Parser\ParserOutput::getRenderId()
|
|
*/
|
|
public function testRenderIdBackCompat() {
|
|
$po = new ParserOutput();
|
|
|
|
// Parser cache used to contain extension data under a different name
|
|
$po->setExtensionData( 'parsoid-render-id', "1234/LegacyRenderId" );
|
|
$this->assertEquals( "LegacyRenderId", $po->getRenderId() );
|
|
}
|
|
|
|
public function testSetFromParserOptions() {
|
|
// parser output set from canonical parser options
|
|
$pOptions = ParserOptions::newFromAnon();
|
|
$pOutput = new ParserOutput;
|
|
$pOutput->setFromParserOptions( $pOptions );
|
|
$this->assertSame( 'mw-parser-output', $pOutput->getWrapperDivClass() );
|
|
$this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
|
|
$this->assertTrue( $pOutput->isCacheable() );
|
|
$this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
|
|
|
|
// set the various parser options and verify in parser output
|
|
$pOptions->setWrapOutputClass( 'test-wrapper' );
|
|
$pOptions->setIsPreview( true );
|
|
$pOptions->setSuppressSectionEditLinks();
|
|
$pOutput = new ParserOutput;
|
|
$pOutput->setFromParserOptions( $pOptions );
|
|
$this->assertEquals( 'test-wrapper', $pOutput->getWrapperDivClass() );
|
|
$this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
|
|
$this->assertFalse( $pOutput->isCacheable() );
|
|
$this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
|
|
}
|
|
}
|