mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-12-24 04:22:51 +00:00
41bafe6f2c
Implicitly marking parameter $... as nullable is deprecated in PHP 8.4. The explicit nullable type must be used instead. Bug: T376276 Change-Id: I73a4ce1ecd9b4fe040e5bfd22889e783071fab0d
472 lines
13 KiB
PHP
472 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Cite\Tests\Integration;
|
|
|
|
use Cite\Cite;
|
|
use Cite\ErrorReporter;
|
|
use Cite\FootnoteMarkFormatter;
|
|
use Cite\ReferenceListFormatter;
|
|
use Cite\ReferenceStack;
|
|
use Cite\Tests\TestUtils;
|
|
use LogicException;
|
|
use MediaWiki\Language\Language;
|
|
use MediaWiki\Parser\Parser;
|
|
use MediaWiki\Parser\ParserOptions;
|
|
use MediaWiki\Parser\StripState;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @coversDefaultClass \Cite\Cite
|
|
* @license GPL-2.0-or-later
|
|
*/
|
|
class CiteTest extends \MediaWikiIntegrationTestCase {
|
|
|
|
/**
|
|
* @covers ::parseArguments
|
|
* @dataProvider provideParseArguments
|
|
*/
|
|
public function testParseArguments(
|
|
array $attributes,
|
|
array $expectedValue,
|
|
?string $expectedError = null
|
|
) {
|
|
/** @var Cite $cite */
|
|
$cite = TestingAccessWrapper::newFromObject( $this->newCite() );
|
|
$status = $cite->parseArguments(
|
|
$attributes,
|
|
[ 'dir', 'extends', 'follow', 'group', 'name' ]
|
|
);
|
|
$this->assertSame( $expectedValue, array_values( $status->getValue() ) );
|
|
if ( $expectedError ) {
|
|
$this->assertStatusError( $expectedError, $status );
|
|
} else {
|
|
$this->assertStatusGood( $status );
|
|
}
|
|
}
|
|
|
|
public static function provideParseArguments() {
|
|
// Note: Values are guaranteed to be trimmed by the parser, see
|
|
// Sanitizer::decodeTagAttributes()
|
|
return [
|
|
[
|
|
'attributes' => [],
|
|
'expectedValue' => [ null, null, null, null, null ],
|
|
],
|
|
|
|
// One attribute only
|
|
[
|
|
'attributes' => [ 'dir' => 'invalid' ],
|
|
'expectedValue' => [ 'invalid', null, null, null, null ] ],
|
|
[
|
|
'attributes' => [ 'dir' => 'RTL' ],
|
|
'expectedValue' => [ 'rtl', null, null, null, null ] ],
|
|
[
|
|
'attributes' => [ 'follow' => 'f' ],
|
|
'expectedValue' => [ null, null, 'f', null, null ] ],
|
|
[
|
|
'attributes' => [ 'group' => 'g' ],
|
|
'expectedValue' => [ null, null, null, 'g', null ] ],
|
|
[
|
|
'attributes' => [ 'invalid' => 'i' ],
|
|
'expectedValue' => [ null, null, null, null, null ],
|
|
'expectedError' => 'cite_error_ref_too_many_keys'
|
|
],
|
|
[
|
|
'attributes' => [ 'invalid' => null ],
|
|
'expectedValue' => [ null, null, null, null, null ],
|
|
'expectedError' => 'cite_error_ref_too_many_keys'
|
|
],
|
|
[
|
|
'attributes' => [ 'name' => 'n' ],
|
|
'expectedValue' => [ null, null, null, null, 'n' ]
|
|
],
|
|
[
|
|
'attributes' => [ 'name' => null ],
|
|
'expectedValue' => [ null, null, null, null, null ]
|
|
],
|
|
[
|
|
'attributes' => [ 'extends' => 'e' ],
|
|
'expectedValue' => [ null, 'e', null, null, null ]
|
|
],
|
|
|
|
// Pairs
|
|
[
|
|
'attributes' => [ 'follow' => 'f', 'name' => 'n' ],
|
|
'expectedValue' => [ null, null, 'f', null, 'n' ]
|
|
],
|
|
[
|
|
'attributes' => [ 'follow' => null, 'name' => null ],
|
|
'expectedValue' => [ null, null, null, null, null ]
|
|
],
|
|
[
|
|
'attributes' => [ 'follow' => 'f', 'extends' => 'e' ],
|
|
'expectedValue' => [ null, 'e', 'f', null, null ]
|
|
],
|
|
[
|
|
'attributes' => [ 'group' => 'g', 'name' => 'n' ],
|
|
'expectedValue' => [ null, null, null, 'g', 'n' ]
|
|
],
|
|
|
|
// Combinations of 3 or more attributes
|
|
[
|
|
'attributes' => [ 'group' => 'g', 'name' => 'n', 'extends' => 'e', 'dir' => 'rtl' ],
|
|
'expectedValue' => [ 'rtl', 'e', null, 'g', 'n' ]
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers ::references
|
|
* @dataProvider provideGuardedReferences
|
|
*/
|
|
public function testGuardedReferences(
|
|
?string $text,
|
|
array $argv,
|
|
int $expectedRollbackCount,
|
|
string $expectedInReferencesGroup,
|
|
bool $expectedResponsive,
|
|
string $expectedOutput
|
|
) {
|
|
$this->overrideConfigValue( 'CiteResponsiveReferences', false );
|
|
|
|
$parser = $this->createNoOpMock( Parser::class, [ 'recursiveTagParse' ] );
|
|
|
|
$cite = $this->newCite();
|
|
/** @var Cite $spy */
|
|
$spy = TestingAccessWrapper::newFromObject( $cite );
|
|
$spy->errorReporter = $this->createPartialMock( ErrorReporter::class, [ 'halfParsed' ] );
|
|
$spy->errorReporter->method( 'halfParsed' )->willReturnArgument( 1 );
|
|
$spy->referenceListFormatter = $this->createMock( ReferenceListFormatter::class );
|
|
$spy->referenceListFormatter->method( 'formatReferences' )
|
|
->with( $parser, [], $expectedResponsive, false )
|
|
->willReturn( 'references!' );
|
|
$spy->isSectionPreview = false;
|
|
$spy->referenceStack = $this->createMock( ReferenceStack::class );
|
|
$spy->referenceStack->method( 'popGroup' )
|
|
->with( $expectedInReferencesGroup )->willReturn( [] );
|
|
$spy->referenceStack->expects( $expectedRollbackCount ? $this->once() : $this->never() )
|
|
->method( 'rollbackRefs' )
|
|
->with( $expectedRollbackCount )
|
|
->willReturn( [ [ 't', [] ] ] );
|
|
|
|
$output = $cite->references( $parser, $text, $argv );
|
|
$this->assertSame( $expectedOutput, $output );
|
|
}
|
|
|
|
public static function provideGuardedReferences() {
|
|
return [
|
|
'Bare references tag' => [
|
|
'text' => null,
|
|
'argv' => [],
|
|
'expectedRollbackCount' => 0,
|
|
'expectedInReferencesGroup' => '',
|
|
'expectedResponsive' => false,
|
|
'expectedOutput' => 'references!'
|
|
],
|
|
'References with group' => [
|
|
'text' => null,
|
|
'argv' => [ 'group' => 'g' ],
|
|
'expectedRollbackCount' => 0,
|
|
'expectedInReferencesGroup' => 'g',
|
|
'expectedResponsive' => false,
|
|
'expectedOutput' => 'references!'
|
|
],
|
|
'Empty references tag' => [
|
|
'text' => '',
|
|
'argv' => [],
|
|
'expectedRollbackCount' => 0,
|
|
'expectedInReferencesGroup' => '',
|
|
'expectedResponsive' => false,
|
|
'expectedOutput' => 'references!'
|
|
],
|
|
'Set responsive' => [
|
|
'text' => '',
|
|
'argv' => [ 'responsive' => '1' ],
|
|
'expectedRollbackCount' => 0,
|
|
'expectedInReferencesGroup' => '',
|
|
'expectedResponsive' => true,
|
|
'expectedOutput' => 'references!'
|
|
],
|
|
'Unknown attribute' => [
|
|
'text' => '',
|
|
'argv' => [ 'blargh' => '0' ],
|
|
'expectedRollbackCount' => 0,
|
|
'expectedInReferencesGroup' => '',
|
|
'expectedResponsive' => false,
|
|
'expectedOutput' => 'cite_error_references_invalid_parameters',
|
|
],
|
|
'Contains refs (which are broken)' => [
|
|
'text' => Parser::MARKER_PREFIX . '-ref- and ' . Parser::MARKER_PREFIX . '-notref-',
|
|
'argv' => [],
|
|
'expectedRollbackCount' => 1,
|
|
'expectedInReferencesGroup' => '',
|
|
'expectedResponsive' => false,
|
|
'expectedOutput' => "references!\ncite_error_references_no_key"
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers ::guardedRef
|
|
* @dataProvider provideGuardedRef
|
|
*/
|
|
public function testGuardedRef(
|
|
string $text,
|
|
array $argv,
|
|
?string $inReferencesGroup,
|
|
array $initialRefs,
|
|
string $expectOutput,
|
|
?string $expectedError,
|
|
array $expectedRefs,
|
|
bool $isSectionPreview = false
|
|
) {
|
|
$mockParser = $this->createNoOpMock( Parser::class, [ 'getStripState' ] );
|
|
$mockParser->method( 'getStripState' )
|
|
->willReturn( $this->createMock( StripState::class ) );
|
|
|
|
$errorReporter = $this->createPartialMock( ErrorReporter::class, [ 'halfParsed' ] );
|
|
$errorReporter->method( 'halfParsed' )->willReturnArgument( 1 );
|
|
|
|
$referenceStack = new ReferenceStack();
|
|
/** @var ReferenceStack $stackSpy */
|
|
$stackSpy = TestingAccessWrapper::newFromObject( $referenceStack );
|
|
$stackSpy->refs = TestUtils::refGroupsFromArray( $initialRefs );
|
|
|
|
$mockFootnoteMarkFormatter = $this->createMock( FootnoteMarkFormatter::class );
|
|
$mockFootnoteMarkFormatter->method( 'linkRef' )->willReturn( '<foot />' );
|
|
|
|
$cite = $this->newCite( $isSectionPreview );
|
|
/** @var Cite $spy */
|
|
$spy = TestingAccessWrapper::newFromObject( $cite );
|
|
$spy->errorReporter = $errorReporter;
|
|
$spy->footnoteMarkFormatter = $mockFootnoteMarkFormatter;
|
|
$spy->inReferencesGroup = $inReferencesGroup;
|
|
$spy->referenceStack = $referenceStack;
|
|
|
|
$result = $spy->guardedRef( $mockParser, $text, $argv );
|
|
$this->assertSame( $expectOutput, $result );
|
|
if ( $expectedError ) {
|
|
$this->assertStatusError( $expectedError, $spy->mReferencesErrors );
|
|
} else {
|
|
$this->assertStatusGood( $spy->mReferencesErrors );
|
|
}
|
|
$expectedRefs = TestUtils::refGroupsFromArray( $expectedRefs );
|
|
$this->assertEquals( $expectedRefs, $stackSpy->refs );
|
|
}
|
|
|
|
public static function provideGuardedRef() {
|
|
return [
|
|
'Whitespace text' => [
|
|
'text' => ' ',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => null,
|
|
'initialRefs' => [],
|
|
'expectedOutput' => '<foot />',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'count' => 1,
|
|
'dir' => null,
|
|
'key' => 1,
|
|
'group' => '',
|
|
'name' => 'a',
|
|
'text' => null,
|
|
'number' => 1,
|
|
],
|
|
],
|
|
]
|
|
],
|
|
'Empty in default references' => [
|
|
'text' => '',
|
|
'argv' => [],
|
|
'inReferencesGroup' => '',
|
|
'initialRefs' => [ '' => [] ],
|
|
'expectedOutput' => '',
|
|
'expectedError' => 'cite_error_references_no_key',
|
|
'expectedRefs' => [ '' => [] ]
|
|
],
|
|
'Fallback to references group' => [
|
|
'text' => 'text',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => 'foo',
|
|
'initialRefs' => [
|
|
'foo' => [ 'a' => [] ],
|
|
],
|
|
'expectedOutput' => '',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'foo' => [
|
|
'a' => [
|
|
'text' => 'text',
|
|
'count' => 0,
|
|
],
|
|
],
|
|
]
|
|
],
|
|
'Successful ref' => [
|
|
'text' => 'text',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => null,
|
|
'initialRefs' => [],
|
|
'expectedOutput' => '<foot />',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'count' => 1,
|
|
'dir' => null,
|
|
'key' => 1,
|
|
'group' => '',
|
|
'name' => 'a',
|
|
'text' => 'text',
|
|
'number' => 1,
|
|
],
|
|
],
|
|
]
|
|
],
|
|
'Invalid ref' => [
|
|
'text' => 'text',
|
|
'argv' => [
|
|
'name' => 'a',
|
|
'badkey' => 'b',
|
|
],
|
|
'inReferencesGroup' => null,
|
|
'initialRefs' => [],
|
|
'expectedOutput' => 'cite_error_ref_too_many_keys',
|
|
'expectedError' => null,
|
|
'expectedRefs' => []
|
|
],
|
|
'Successful references ref' => [
|
|
'text' => 'text',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => '',
|
|
'initialRefs' => [
|
|
'' => [
|
|
'a' => []
|
|
]
|
|
],
|
|
'expectedOutput' => '',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'text' => 'text',
|
|
'count' => 0,
|
|
],
|
|
],
|
|
]
|
|
],
|
|
'T245376: Preview a list-defined ref that was never used' => [
|
|
'text' => 'T245376',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => '',
|
|
'initialRefs' => [],
|
|
'expectOutput' => '',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'text' => 'T245376',
|
|
'count' => 0,
|
|
],
|
|
],
|
|
],
|
|
'isSectionPreview' => true,
|
|
],
|
|
'Mismatched text in references' => [
|
|
'text' => 'text-2',
|
|
'argv' => [ 'name' => 'a' ],
|
|
'inReferencesGroup' => '',
|
|
'initialRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'text' => 'text-1',
|
|
'count' => 1,
|
|
],
|
|
]
|
|
],
|
|
'expectedOutput' => '',
|
|
'expectedError' => null,
|
|
'expectedRefs' => [
|
|
'' => [
|
|
'a' => [
|
|
'text' => 'text-1',
|
|
'count' => 1,
|
|
'warnings' => [ [ 'cite_error_references_duplicate_key', 'a' ] ],
|
|
],
|
|
],
|
|
]
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers ::guardedRef
|
|
*/
|
|
public function testGuardedRef_extendsUsageTracking() {
|
|
$this->overrideConfigValue( 'CiteBookReferencing', false );
|
|
|
|
$mockParser = $this->createNoOpMock( Parser::class, [ 'addTrackingCategory' ] );
|
|
// This will be our most important assertion.
|
|
$mockParser->expects( $this->once() )
|
|
->method( 'addTrackingCategory' )
|
|
->with( Cite::EXTENDS_TRACKING_CATEGORY );
|
|
|
|
$cite = $this->newCite();
|
|
/** @var Cite $spy */
|
|
$spy = TestingAccessWrapper::newFromObject( $cite );
|
|
$spy->errorReporter = $this->createMock( ErrorReporter::class );
|
|
|
|
$spy->guardedRef( $mockParser, 'text', [ Cite::SUBREF_ATTRIBUTE => 'a' ] );
|
|
}
|
|
|
|
/**
|
|
* @coversNothing
|
|
*/
|
|
public function testReferencesSectionPreview() {
|
|
$language = $this->createNoOpMock( Language::class );
|
|
|
|
$parserOptions = $this->createMock( ParserOptions::class );
|
|
$parserOptions->method( 'getIsSectionPreview' )->willReturn( true );
|
|
|
|
$parser = $this->createNoOpMock( Parser::class, [ 'getOptions', 'getContentLanguage' ] );
|
|
$parser->method( 'getOptions' )->willReturn( $parserOptions );
|
|
$parser->method( 'getContentLanguage' )->willReturn( $language );
|
|
|
|
$config = $this->getServiceContainer()->getMainConfig();
|
|
/** @var Cite $cite */
|
|
$cite = TestingAccessWrapper::newFromObject( new Cite( $parser, $config ) );
|
|
// Assume the currently parsed <ref> is wrapped in <references>
|
|
$cite->inReferencesGroup = '';
|
|
|
|
$html = $cite->guardedRef( $parser, 'a', [ 'name' => 'a' ] );
|
|
$this->assertSame( '', $html );
|
|
}
|
|
|
|
/**
|
|
* @covers ::__clone
|
|
* @covers ::__construct
|
|
*/
|
|
public function testClone() {
|
|
$cite = $this->newCite();
|
|
|
|
$this->expectException( LogicException::class );
|
|
clone $cite;
|
|
}
|
|
|
|
private function newCite( bool $isSectionPreview = false ): Cite {
|
|
$language = $this->createNoOpMock( Language::class, [ '__debugInfo' ] );
|
|
|
|
$mockOptions = $this->createMock( ParserOptions::class );
|
|
$mockOptions->method( 'getIsSectionPreview' )->willReturn( $isSectionPreview );
|
|
|
|
$mockParser = $this->createNoOpMock( Parser::class, [ 'getOptions', 'getContentLanguage' ] );
|
|
$mockParser->method( 'getOptions' )->willReturn( $mockOptions );
|
|
$mockParser->method( 'getContentLanguage' )->willReturn( $language );
|
|
$config = $this->getServiceContainer()->getMainConfig();
|
|
return new Cite( $mockParser, $config );
|
|
}
|
|
|
|
}
|