+ /**
+ * @covers OutputPage::addParserOutputText
+ */
+ public function testAddParserOutputText() {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+
+ $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
+
+ $op->addParserOutputMetadata( $pOut );
+ $this->assertSame( '', $op->getHTML() );
+
+ $op->addParserOutputText( $pOut );
+ $this->assertSame( '<some text>', $op->getHTML() );
+ }
+
+ /**
+ * @covers OutputPage::addParserOutput
+ */
+ public function testAddParserOutput() {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+ $this->assertFalse( $op->showNewSectionLink() );
+
+ $pOut = $this->createParserOutputStub( [
+ 'getText' => '<some text>',
+ 'getNewSection' => true,
+ ] );
+
+ $op->addParserOutput( $pOut );
+ $this->assertSame( '<some text>', $op->getHTML() );
+ $this->assertTrue( $op->showNewSectionLink() );
+ }
+
+ /**
+ * @covers OutputPage::addTemplate
+ */
+ public function testAddTemplate() {
+ $template = $this->getMock( QuickTemplate::class );
+ $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
+
+ $op = $this->newInstance();
+ $op->addTemplate( $template );
+
+ $this->assertSame( '<abc>&def;', $op->getHTML() );
+ }
+
+ /**
+ * @dataProvider provideParse
+ * @covers OutputPage::parse
+ * @param array $args To pass to parse()
+ * @param string $expectedHTML Expected return value for parse()
+ * @param string $expectedHTML Expected return value for parseInline(), if different
+ */
+ public function testParse( array $args, $expectedHTML ) {
+ $op = $this->newInstance();
+ $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
+ }
+
+ /**
+ * @dataProvider provideParse
+ * @covers OutputPage::parseInline
+ */
+ public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
+ if ( count( $args ) > 3 ) {
+ // $language param not supported
+ $this->assertTrue( true );
+ return;
+ }
+ $op = $this->newInstance();
+ $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
+ }
+
+ public function provideParse() {
+ return [
+ 'List at start of line' => [
+ [ '* List' ],
+ "<div class=\"mw-parser-output\"><ul><li>List</li></ul>\n</div>",
+ ],
+ 'List not at start' => [
+ [ "* ''Not'' list", false ],
+ '<div class="mw-parser-output">* <i>Not</i> list</div>',
+ ],
+ 'Interface' => [
+ [ "''Italic''", true, true ],
+ "<p><i>Italic</i>\n</p>",
+ '<i>Italic</i>',
+ ],
+ 'formatnum' => [
+ [ '{{formatnum:123456.789}}' ],
+ "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
+ ],
+ 'Language' => [
+ [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
+ "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
+ ],
+ 'Language with interface' => [
+ [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
+ "<p>123.456,789\n</p>",
+ '123.456,789',
+ ],
+ 'No section edit links' => [
+ [ '== Header ==' ],
+ '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
+ "Header</span></h2>\n</div>",
+ ]
+ ];
+ }
+
+ /**
+ * @covers OutputPage::parse
+ */
+ public function testParseNullTitle() {
+ $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parse' );
+ $op = $this->newInstance( [], null, 'notitle' );
+ $op->parse( '' );
+ }
+
+ /**
+ * @covers OutputPage::parse
+ */
+ public function testParseInlineNullTitle() {
+ $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parse' );
+ $op = $this->newInstance( [], null, 'notitle' );
+ $op->parseInline( '' );
+ }
+
+ /**
+ * @covers OutputPage::setCdnMaxage
+ * @covers OutputPage::lowerCdnMaxage
+ */
+ public function testCdnMaxage() {
+ $op = $this->newInstance();
+ $wrapper = TestingAccessWrapper::newFromObject( $op );
+ $this->assertSame( 0, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( -1 );
+ $this->assertSame( -1, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 120 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 60 );
+ $this->assertSame( 60, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 180 );
+ $this->assertSame( 180, $wrapper->mCdnMaxage );
+
+ $op->lowerCdnMaxage( 240 );
+ $this->assertSame( 180, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 300 );
+ $this->assertSame( 240, $wrapper->mCdnMaxage );
+
+ $op->lowerCdnMaxage( 120 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 180 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 60 );
+ $this->assertSame( 60, $wrapper->mCdnMaxage );
+
+ $op->setCdnMaxage( 240 );
+ $this->assertSame( 120, $wrapper->mCdnMaxage );
+ }
+
+ /** @var int Faked time to set for tests that need it */
+ private static $fakeTime;
+
+ /**
+ * @dataProvider provideAdaptCdnTTL
+ * @covers OutputPage::adaptCdnTTL
+ * @param array $args To pass to adaptCdnTTL()
+ * @param int $expected Expected new value of mCdnMaxageLimit
+ * @param array $options Associative array:
+ * initialMaxage => Maxage to set before calling adaptCdnTTL() (default 86400)
+ */
+ public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
+ try {
+ MWTimestamp::setFakeTime( self::$fakeTime );
+
+ $op = $this->newInstance();
+ // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
+ // is 0, so adaptCdnTTL() won't mutate the object at all.
+ $initial = $options['initialMaxage'] ?? 86400;
+ $op->setCdnMaxage( $initial );
+
+ $op->adaptCdnTTL( ...$args );
+ } finally {
+ MWTimestamp::setFakeTime( false );
+ }
+
+ $wrapper = TestingAccessWrapper::newFromObject( $op );
+
+ // Special rules for false/null
+ if ( $args[0] === null || $args[0] === false ) {
+ $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
+ $op->setCdnMaxage( $expected + 1 );
+ $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
+ return;
+ }
+
+ $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
+
+ if ( $initial >= $expected ) {
+ $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
+ } else {
+ $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
+ }
+
+ $op->setCdnMaxage( $expected + 1 );
+ $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
+ }
+
+ public function provideAdaptCdnTTL() {
+ global $wgSquidMaxage;
+ $now = time();
+ self::$fakeTime = $now;
+ return [
+ 'Five minutes ago' => [ [ $now - 300 ], 270 ],
+ 'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
+ 'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
+ 'Five minutes ago, initial maxage four minutes' =>
+ [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
+ 'A very long time ago' => [ [ $now - 1000000000 ], $wgSquidMaxage ],
+ 'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
+
+ 'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
+ 'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
+ "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
+ 'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
+ // @todo These give incorrect results due to timezones, how to test?
+ //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
+ //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
+
+ 'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
+ 'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
+ 'A very long time ago, maxTTL even longer' =>
+ [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
+ ];
+ }
+
+ /**
+ * @covers OutputPage::enableClientCache
+ * @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::addParserOutput
+ */
+ public function testClientCache() {
+ $op = $this->newInstance();
+
+ // Test initial value
+ $this->assertSame( true, $op->enableClientCache( null ) );
+ // Test that calling with null doesn't change the value
+ $this->assertSame( true, $op->enableClientCache( null ) );
+
+ // Test setting to false
+ $this->assertSame( true, $op->enableClientCache( false ) );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+ // Test that calling with null doesn't change the value
+ $this->assertSame( false, $op->enableClientCache( null ) );
+
+ // Test that a cacheable ParserOutput doesn't set to true
+ $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
+ $op->addParserOutputMetadata( $pOutCacheable );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+
+ // Test setting back to true
+ $this->assertSame( false, $op->enableClientCache( true ) );
+ $this->assertSame( true, $op->enableClientCache( null ) );
+
+ // Test that an uncacheable ParserOutput does set to false
+ $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
+ $op->addParserOutput( $pOutUncacheable );
+ $this->assertSame( false, $op->enableClientCache( null ) );
+ }
+
+ /**
+ * @covers OutputPage::getCacheVaryCookies
+ */
+ public function testGetCacheVaryCookies() {
+ global $wgCookiePrefix, $wgDBname;
+ $op = $this->newInstance();
+ $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
+ $expectedCookies = [
+ "{$prefix}Token",
+ "{$prefix}LoggedOut",
+ "{$prefix}_session",
+ 'forceHTTPS',
+ 'cookie1',
+ 'cookie2',
+ ];
+
+ // We have to reset the cookies because getCacheVaryCookies may have already been called
+ TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
+
+ $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
+ $this->setTemporaryHook( 'GetCacheVaryCookies',
+ function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
+ $this->assertSame( $op, $innerOP );
+ $cookies[] = 'cookie2';
+ $this->assertSame( $expectedCookies, $cookies );
+ }
+ );
+
+ $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
+ }
+