1<?php 2 3namespace Cite\Tests\Unit; 4 5use Cite\AnchorFormatter; 6use Cite\ErrorReporter; 7use Cite\ReferenceMessageLocalizer; 8use Cite\ReferencesFormatter; 9use Message; 10use Parser; 11use Wikimedia\TestingAccessWrapper; 12 13/** 14 * @coversDefaultClass \Cite\ReferencesFormatter 15 * 16 * @license GPL-2.0-or-later 17 */ 18class ReferencesFormatterTest extends \MediaWikiUnitTestCase { 19 20 /** 21 * @covers ::__construct 22 * @covers ::formatReferences 23 * @covers ::formatRefsList 24 * @dataProvider provideFormatReferences 25 */ 26 public function testFormatReferences( array $refs, string $expectedOutput ) { 27 $mockParser = $this->createMock( Parser::class ); 28 $mockParser->method( 'recursiveTagParse' )->willReturnArgument( 0 ); 29 30 $mockErrorReporter = $this->createMock( ErrorReporter::class ); 31 $mockErrorReporter->method( 'plain' )->willReturnCallback( 32 function ( $parser, ...$args ) { 33 return '(' . implode( '|', $args ) . ')'; 34 } 35 ); 36 37 $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); 38 $mockMessageLocalizer->method( 'msg' )->willReturnCallback( 39 function ( ...$args ) { 40 $msg = $this->createMock( Message::class ); 41 $msg->method( 'plain' )->willReturn( '<li>(' . implode( '|', $args ) . ')</li>' ); 42 return $msg; 43 } 44 ); 45 46 /** @var Parser $mockParser */ 47 /** @var ErrorReporter $mockErrorReporter */ 48 /** @var ReferenceMessageLocalizer $mockMessageLocalizer */ 49 $formatter = new ReferencesFormatter( 50 $mockErrorReporter, 51 $this->createMock( AnchorFormatter::class ), 52 $mockMessageLocalizer 53 ); 54 55 $output = $formatter->formatReferences( $mockParser, $refs, true, false ); 56 $this->assertSame( $expectedOutput, $output ); 57 } 58 59 public function provideFormatReferences() { 60 return [ 61 'Empty' => [ 62 [], 63 '' 64 ], 65 'Minimal ref' => [ 66 [ 67 0 => [ 68 'key' => 1, 69 'text' => 't', 70 ] 71 ], 72 '<div class="mw-references-wrap"><ol class="references">' . "\n" . 73 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 74 "\n|)</li>\n</ol></div>" 75 ], 76 'Ref with extends' => [ 77 [ 78 0 => [ 79 'extends' => 'a', 80 'extendsIndex' => 1, 81 'key' => 2, 82 'number' => 10, 83 'text' => 't2', 84 ], 85 1 => [ 86 'number' => 11, 87 'text' => 't3', 88 ], 89 'a' => [ 90 'key' => 1, 91 'number' => 9, 92 'text' => 't1', 93 ], 94 ], 95 '<div class="mw-references-wrap"><ol class="references">' . "\n" . 96 '<li>(cite_references_link_many|||<span class="reference-text">t1</span>' . "\n" . 97 '|)<ol class="mw-extended-references"><li>(cite_references_link_many|||' . 98 '<span class="reference-text">t2</span>' . "\n|)</li>\n" . 99 "</ol></li>\n" . 100 '<li>(cite_references_link_many|||<span class="reference-text">t3</span>' . 101 "\n|)</li>\n" . 102 '</ol></div>' 103 ], 104 'Subref of subref' => [ 105 [ 106 0 => [ 107 'extends' => 'a', 108 'extendsIndex' => 1, 109 'key' => 1, 110 'number' => 1, 111 'text' => 't1', 112 ], 113 'a' => [ 114 'extends' => 'b', 115 'extendsIndex' => 1, 116 'key' => 2, 117 'number' => 1, 118 'text' => 't2', 119 ], 120 'b' => [ 121 'key' => 3, 122 'number' => 1, 123 'text' => 't3', 124 ], 125 ], 126 '<div class="mw-references-wrap"><ol class="references">' . "\n" . 127 '<li>(cite_references_link_many|||<span class="reference-text">t3</span>' . "\n" . 128 '|)<ol class="mw-extended-references"><li>(cite_references_link_many|||' . 129 '<span class="reference-text">t1 (cite_error_ref_nested_extends|a|b)</span>' . 130 "\n|)</li>\n" . 131 '<li>(cite_references_link_many|||<span class="reference-text">t2</span>' . 132 "\n|)</li>\n</ol></li>\n" . 133 '</ol></div>' 134 ], 135 'Use columns' => [ 136 array_map( 137 function ( $i ) { 138 return [ 'key' => $i, 'text' => 't' ]; 139 }, 140 range( 0, 10 ) 141 ), 142 '<div class="mw-references-wrap mw-references-columns"><ol class="references">' . 143 "\n" . '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 144 "\n|)</li>\n" . 145 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 146 "\n|)</li>\n" . 147 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 148 "\n|)</li>\n" . 149 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 150 "\n|)</li>\n" . 151 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 152 "\n|)</li>\n" . 153 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 154 "\n|)</li>\n" . 155 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 156 "\n|)</li>\n" . 157 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 158 "\n|)</li>\n" . 159 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 160 "\n|)</li>\n" . 161 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 162 "\n|)</li>\n" . 163 '<li>(cite_references_link_many|||<span class="reference-text">t</span>' . 164 "\n|)</li>\n</ol></div>" 165 ], 166 ]; 167 } 168 169 /** 170 * @covers ::closeIndention 171 * @dataProvider provideCloseIndention 172 */ 173 public function testCloseIndention( $closingLi, $expectedOutput ) { 174 /** @var ReferencesFormatter $formatter */ 175 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 176 $this->createMock( ErrorReporter::class ), 177 $this->createMock( AnchorFormatter::class ), 178 $this->createMock( ReferenceMessageLocalizer::class ) 179 ) ); 180 181 $output = $formatter->closeIndention( $closingLi ); 182 $this->assertSame( $expectedOutput, $output ); 183 } 184 185 public function provideCloseIndention() { 186 return [ 187 'No indention' => [ false, '' ], 188 'Indention string' => [ "</li>\n", "</ol></li>\n" ], 189 'Indention without string' => [ true, '</ol>' ], 190 ]; 191 } 192 193 /** 194 * @covers ::formatListItem 195 * @dataProvider provideFormatListItem 196 */ 197 public function testFormatListItem( 198 $key, 199 array $val, 200 string $expectedOutput 201 ) { 202 $mockErrorReporter = $this->createMock( ErrorReporter::class ); 203 $mockErrorReporter->method( 'plain' )->willReturnCallback( 204 function ( $parser, ...$args ) { 205 return '(' . implode( '|', $args ) . ')'; 206 } 207 ); 208 209 $anchorFormatter = $this->createMock( AnchorFormatter::class ); 210 $anchorFormatter->method( 'refKey' )->willReturnCallback( 211 function ( ...$args ) { 212 return implode( '+', $args ); 213 } 214 ); 215 216 $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); 217 $mockMessageLocalizer->method( 'formatNum' )->willReturnArgument( 0 ); 218 $mockMessageLocalizer->method( 'localizeDigits' )->willReturnArgument( 0 ); 219 $mockMessageLocalizer->method( 'msg' )->willReturnCallback( 220 function ( ...$args ) { 221 $msg = $this->createMock( Message::class ); 222 $msg->method( 'plain' )->willReturn( '(' . implode( '|', $args ) . ')' ); 223 return $msg; 224 } 225 ); 226 227 /** @var ErrorReporter $mockErrorReporter */ 228 /** @var AnchorFormatter $anchorFormatter */ 229 /** @var ReferenceMessageLocalizer $mockMessageLocalizer */ 230 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 231 $mockErrorReporter, 232 $anchorFormatter, 233 $mockMessageLocalizer 234 ) ); 235 236 /** @var ReferencesFormatter $formatter */ 237 $output = $formatter->formatListItem( 238 $this->createMock( Parser::class ), $key, $val, false ); 239 $this->assertSame( $expectedOutput, $output ); 240 } 241 242 public function provideFormatListItem() { 243 return [ 244 'Success' => [ 245 1, 246 [ 247 'text' => 't', 248 ], 249 '(cite_references_link_many|||<span class="reference-text">t</span>' . "\n|)" 250 ], 251 'With dir' => [ 252 1, 253 [ 254 'dir' => 'rtl', 255 'text' => 't', 256 ], 257 '(cite_references_link_many|||<span class="reference-text">t</span>' . 258 "\n" . '| class="mw-cite-dir-rtl")' 259 ], 260 'Incomplete follow' => [ 261 1, 262 [ 263 'follow' => 'f', 264 'text' => 't', 265 ], 266 '(cite_references_no_link||<span class="reference-text">t</span>' . "\n)" 267 ], 268 'Count zero' => [ 269 1, 270 [ 271 'count' => 0, 272 'key' => 5, 273 'text' => 't', 274 ], 275 '(cite_references_link_one||1+5-0|<span class="reference-text">t</span>' . "\n|)" 276 ], 277 'Count negative' => [ 278 1, 279 [ 280 'count' => -1, 281 'key' => 5, 282 'number' => 3, 283 'text' => 't', 284 ], 285 '(cite_references_link_one||5+|<span class="reference-text">t</span>' . "\n|)" 286 ], 287 'Count positive' => [ 288 1, 289 [ 290 'count' => 2, 291 'key' => 5, 292 'number' => 3, 293 'text' => 't', 294 ], 295 '(cite_references_link_many||(cite_references_link_many_format|1+5-0|3.0|' . 296 '(cite_references_link_many_format_backlink_labels))' . 297 '(cite_references_link_many_sep)(cite_references_link_many_format|1+5-1|3.1|' . 298 '(cite_error_references_no_backlink_label))(cite_references_link_many_and)' . 299 '(cite_references_link_many_format|1+5-2|3.2|(cite_error_references_no_backlink_label' . 300 '))|<span class="reference-text">t</span>' . "\n|)" 301 ], 302 ]; 303 } 304 305 /** 306 * @covers ::referenceText 307 * @dataProvider provideReferenceText 308 */ 309 public function testReferenceText( 310 $key, 311 ?string $text, 312 bool $isSectionPreview, 313 string $expectedOutput 314 ) { 315 $mockErrorReporter = $this->createMock( ErrorReporter::class ); 316 $mockErrorReporter->method( 'plain' )->willReturnCallback( 317 function ( $parser, ...$args ) { 318 return '(' . implode( '|', $args ) . ')'; 319 } 320 ); 321 322 /** @var ErrorReporter $mockErrorReporter */ 323 /** @var ReferencesFormatter $formatter */ 324 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 325 $mockErrorReporter, 326 $this->createMock( AnchorFormatter::class ), 327 $this->createMock( ReferenceMessageLocalizer::class ) 328 ) ); 329 330 $output = $formatter->referenceText( 331 $this->createMock( Parser::class ), $key, $text, $isSectionPreview ); 332 $this->assertSame( $expectedOutput, $output ); 333 } 334 335 public function provideReferenceText() { 336 return [ 337 'No text, not preview' => [ 338 1, 339 null, 340 false, 341 '(cite_error_references_no_text|1)' 342 ], 343 'No text, is preview' => [ 344 1, 345 null, 346 true, 347 '(cite_warning_sectionpreview_no_text|1)' 348 ], 349 'Has text' => [ 350 1, 351 'text', 352 true, 353 '<span class="reference-text">text</span>' . "\n" 354 ], 355 'Trims text' => [ 356 1, 357 "text\n\n", 358 true, 359 '<span class="reference-text">text</span>' . "\n" 360 ], 361 ]; 362 } 363 364 /** 365 * @covers ::referencesFormatEntryAlternateBacklinkLabel 366 * @dataProvider provideReferencesFormatEntryAlternateBacklinkLabel 367 */ 368 public function testReferencesFormatEntryAlternateBacklinkLabel( 369 ?string $expectedLabel, ?string $labelList, int $offset 370 ) { 371 $mockMessage = $this->createMock( Message::class ); 372 $mockMessage->method( 'exists' )->willReturn( (bool)$labelList ); 373 $mockMessage->method( 'plain' )->willReturn( $labelList ?? '<missing-junk>' ); 374 375 $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); 376 $mockMessageLocalizer->method( 'msg' ) 377 ->willReturn( $mockMessage ); 378 379 $mockErrorReporter = $this->createMock( ErrorReporter::class ); 380 if ( $expectedLabel === null ) { 381 $mockErrorReporter->expects( $this->once() )->method( 'plain' ); 382 } else { 383 $mockErrorReporter->expects( $this->never() )->method( 'plain' ); 384 } 385 386 /** @var ErrorReporter $mockErrorReporter */ 387 /** @var ReferenceMessageLocalizer $mockMessageLocalizer */ 388 /** @var ReferencesFormatter $formatter */ 389 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 390 $mockErrorReporter, 391 $this->createMock( AnchorFormatter::class ), 392 $mockMessageLocalizer 393 ) ); 394 395 $label = $formatter->referencesFormatEntryAlternateBacklinkLabel( 396 $this->createMock( Parser::class ), $offset ); 397 if ( $expectedLabel !== null ) { 398 $this->assertSame( $expectedLabel, $label ); 399 } 400 } 401 402 public function provideReferencesFormatEntryAlternateBacklinkLabel() { 403 yield [ 'aa', 'aa ab ac', 0 ]; 404 yield [ 'ab', 'aa ab ac', 1 ]; 405 yield [ 'å', 'å b c', 0 ]; 406 yield [ null, 'a b c', 10 ]; 407 } 408 409 /** 410 * @covers ::referencesFormatEntryNumericBacklinkLabel 411 * @dataProvider provideReferencesFormatEntryNumericBacklinkLabel 412 */ 413 public function testReferencesFormatEntryNumericBacklinkLabel( 414 string $expectedLabel, int $base, int $offset, int $max 415 ) { 416 $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); 417 $mockMessageLocalizer->method( 'formatNum' )->willReturnArgument( 0 ); 418 $mockMessageLocalizer->method( 'localizeDigits' )->willReturnArgument( 0 ); 419 420 /** @var ReferenceMessageLocalizer $mockMessageLocalizer */ 421 /** @var ReferencesFormatter $formatter */ 422 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 423 $this->createMock( ErrorReporter::class ), 424 $this->createMock( AnchorFormatter::class ), 425 $mockMessageLocalizer 426 ) ); 427 428 $label = $formatter->referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ); 429 $this->assertSame( $expectedLabel, $label ); 430 } 431 432 public function provideReferencesFormatEntryNumericBacklinkLabel() { 433 yield [ '1.2', 1, 2, 9 ]; 434 yield [ '1.02', 1, 2, 99 ]; 435 yield [ '1.002', 1, 2, 100 ]; 436 yield [ '2.1', 2, 1, 1 ]; 437 } 438 439 /** 440 * @covers ::listToText 441 * @dataProvider provideLists 442 */ 443 public function testListToText( array $list, $expected ) { 444 $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); 445 $mockMessageLocalizer->method( 'msg' )->willReturnCallback( 446 function ( ...$args ) { 447 $msg = $this->createMock( Message::class ); 448 $msg->method( 'plain' )->willReturn( '(' . implode( '|', $args ) . ')' ); 449 return $msg; 450 } 451 ); 452 453 /** @var ReferenceMessageLocalizer $mockMessageLocalizer */ 454 /** @var ReferencesFormatter $formatter */ 455 $formatter = TestingAccessWrapper::newFromObject( new ReferencesFormatter( 456 $this->createMock( ErrorReporter::class ), 457 $this->createMock( AnchorFormatter::class ), 458 $mockMessageLocalizer 459 ) ); 460 $this->assertSame( $expected, $formatter->listToText( $list ) ); 461 } 462 463 public function provideLists() { 464 return [ 465 [ 466 [], 467 '' 468 ], 469 [ 470 // This is intentionally using numbers to test the to-string cast 471 [ 1 ], 472 '1' 473 ], 474 [ 475 [ 1, 2 ], 476 '1(cite_references_link_many_and)2' 477 ], 478 [ 479 [ 1, 2, 3 ], 480 '1(cite_references_link_many_sep)2(cite_references_link_many_and)3' 481 ], 482 ]; 483 } 484 485} 486