1<?php 2 3use Psr\Container\ContainerInterface; 4use Wikimedia\ObjectFactory; 5 6/** 7 * @group ResourceLoader 8 */ 9class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase { 10 11 protected function setUp(): void { 12 parent::setUp(); 13 14 $skinFactory = new SkinFactory( 15 new ObjectFactory( $this->createMock( ContainerInterface::class ) ), [] 16 ); 17 // The empty spec shouldn't matter since this test should never call it 18 $skinFactory->register( 19 'fakeskin', 20 'FakeSkin', 21 [] 22 ); 23 $this->setService( 'SkinFactory', $skinFactory ); 24 25 // This test is not expected to query any database 26 MediaWiki\MediaWikiServices::disableStorageBackend(); 27 } 28 29 private static function getModules() { 30 $base = [ 31 'localBasePath' => __DIR__, 32 ]; 33 34 return [ 35 'noTemplateModule' => [], 36 37 'deprecatedModule' => $base + [ 38 'deprecated' => true, 39 ], 40 'deprecatedTomorrow' => $base + [ 41 'deprecated' => 'Will be removed tomorrow.' 42 ], 43 44 'htmlTemplateModule' => $base + [ 45 'templates' => [ 46 'templates/template.html', 47 'templates/template2.html', 48 ] 49 ], 50 51 'htmlTemplateUnknown' => $base + [ 52 'templates' => [ 53 'templates/notfound.html', 54 ] 55 ], 56 57 'aliasedHtmlTemplateModule' => $base + [ 58 'templates' => [ 59 'foo.html' => 'templates/template.html', 60 'bar.html' => 'templates/template2.html', 61 ] 62 ], 63 64 'templateModuleHandlebars' => $base + [ 65 'templates' => [ 66 'templates/template_awesome.handlebars', 67 ], 68 ], 69 70 'aliasFooFromBar' => $base + [ 71 'templates' => [ 72 'foo.foo' => 'templates/template.bar', 73 ], 74 ], 75 ]; 76 } 77 78 public static function providerTemplateDependencies() { 79 $modules = self::getModules(); 80 81 return [ 82 [ 83 $modules['noTemplateModule'], 84 [], 85 ], 86 [ 87 $modules['htmlTemplateModule'], 88 [ 89 'mediawiki.template', 90 ], 91 ], 92 [ 93 $modules['templateModuleHandlebars'], 94 [ 95 'mediawiki.template', 96 'mediawiki.template.handlebars', 97 ], 98 ], 99 [ 100 $modules['aliasFooFromBar'], 101 [ 102 'mediawiki.template', 103 'mediawiki.template.foo', 104 ], 105 ], 106 ]; 107 } 108 109 /** 110 * @dataProvider providerTemplateDependencies 111 * @covers ResourceLoaderFileModule::__construct 112 * @covers ResourceLoaderFileModule::getDependencies 113 */ 114 public function testTemplateDependencies( $module, $expected ) { 115 $rl = new ResourceLoaderFileModule( $module ); 116 $rl->setName( 'testing' ); 117 $this->assertEquals( $rl->getDependencies(), $expected ); 118 } 119 120 public static function providerDeprecatedModules() { 121 return [ 122 [ 123 'deprecatedModule', 124 'mw.log.warn("This page is using the deprecated ResourceLoader module \"deprecatedModule\".");', 125 ], 126 [ 127 'deprecatedTomorrow', 128 'mw.log.warn(' . 129 '"This page is using the deprecated ResourceLoader module \"deprecatedTomorrow\".\\n' . 130 "Will be removed tomorrow." . 131 '");' 132 ] 133 ]; 134 } 135 136 /** 137 * @dataProvider providerDeprecatedModules 138 * @covers ResourceLoaderFileModule::getScript 139 */ 140 public function testDeprecatedModules( $name, $expected ) { 141 $modules = self::getModules(); 142 $module = new ResourceLoaderFileModule( $modules[$name] ); 143 $module->setName( $name ); 144 $ctx = $this->getResourceLoaderContext(); 145 $this->assertEquals( $module->getScript( $ctx ), $expected ); 146 } 147 148 /** 149 * @covers ResourceLoaderFileModule::getScript 150 * @covers ResourceLoaderFileModule::getScriptFiles 151 * @covers ResourceLoaderFileModule::readScriptFiles 152 */ 153 public function testGetScript() { 154 $module = new ResourceLoaderFileModule( [ 155 'localBasePath' => __DIR__ . '/../../data/resourceloader', 156 'scripts' => [ 'script-nosemi.js', 'script-comment.js' ], 157 ] ); 158 $module->setName( 'testing' ); 159 $ctx = $this->getResourceLoaderContext(); 160 $this->assertEquals( 161 "/* eslint-disable */\nmw.foo()\n" . 162 "/* eslint-disable */\nmw.foo()\n// mw.bar();\n", 163 $module->getScript( $ctx ), 164 'scripts with newline at the end are concatenated without a newline' 165 ); 166 167 $module = new ResourceLoaderFileModule( [ 168 'localBasePath' => __DIR__ . '/../../data/resourceloader', 169 'scripts' => [ 'script-nosemi-nonl.js', 'script-comment-nonl.js' ], 170 ] ); 171 $module->setName( 'testing' ); 172 $ctx = $this->getResourceLoaderContext(); 173 $this->assertEquals( 174 "/* eslint-disable */\nmw.foo()" . 175 "\n" . 176 "/* eslint-disable */\nmw.foo()\n// mw.bar();" . 177 "\n", 178 $module->getScript( $ctx ), 179 'scripts without newline at the end are concatenated with a newline' 180 ); 181 } 182 183 /** 184 * @covers ResourceLoaderFileModule 185 */ 186 public function testGetAllSkinStyleFiles() { 187 $baseParams = [ 188 'scripts' => [ 189 'foo.js', 190 'bar.js', 191 ], 192 'styles' => [ 193 'foo.css', 194 'bar.css' => [ 'media' => 'print' ], 195 'screen.less' => [ 'media' => 'screen' ], 196 'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ], 197 ], 198 'skinStyles' => [ 199 'default' => 'quux-fallback.less', 200 'fakeskin' => [ 201 'baz-vector.css', 202 'quux-vector.less', 203 ], 204 ], 205 'messages' => [ 206 'hello', 207 'world', 208 ], 209 ]; 210 211 $module = new ResourceLoaderFileModule( $baseParams ); 212 $module->setName( 'testing' ); 213 214 $this->assertEquals( 215 [ 216 'foo.css', 217 'baz-vector.css', 218 'quux-vector.less', 219 'quux-fallback.less', 220 'bar.css', 221 'screen.less', 222 'screen-query.css', 223 ], 224 array_map( 'basename', $module->getAllStyleFiles() ) 225 ); 226 } 227 228 /** 229 * Strip @noflip annotations from CSS code. 230 * @param string $css 231 * @return string 232 */ 233 private static function stripNoflip( $css ) { 234 return str_replace( '/*@noflip*/ ', '', $css ); 235 } 236 237 /** 238 * Confirm that 'ResourceModuleSkinStyles' skin attributes get injected 239 * into the module, and have their file contents read correctly from their 240 * own (out-of-module) directories. 241 * 242 * @covers ResourceLoader 243 * @covers ResourceLoaderFileModule 244 */ 245 public function testInjectSkinStyles() { 246 $moduleDir = __DIR__ . '/../../data/resourceloader'; 247 $skinDir = __DIR__ . '/../../data/resourceloader/myskin'; 248 $rl = new ResourceLoader( new HashConfig( self::getSettings() ) ); 249 $rl->setModuleSkinStyles( [ 250 'fakeskin' => [ 251 'localBasePath' => $skinDir, 252 'testing' => [ 253 'override.css', 254 ], 255 ], 256 ] ); 257 $rl->register( 'testing', [ 258 'localBasePath' => $moduleDir, 259 'styles' => [ 'simple.css' ], 260 ] ); 261 $ctx = $this->getResourceLoaderContext( [ 'skin' => 'fakeskin' ], $rl ); 262 263 $module = $rl->getModule( 'testing' ); 264 $this->assertInstanceOf( ResourceLoaderFileModule::class, $module ); 265 $this->assertEquals( 266 [ 'all' => ".example { color: blue; }\n\n.override { line-height: 2; }\n" ], 267 $module->getStyles( $ctx ) 268 ); 269 } 270 271 /** 272 * What happens when you mix @embed and @noflip? 273 * This really is an integration test, but oh well. 274 * 275 * @covers ResourceLoaderFileModule::getStyles 276 * @covers ResourceLoaderFileModule::getStyleFiles 277 * @covers ResourceLoaderFileModule::readStyleFiles 278 * @covers ResourceLoaderFileModule::readStyleFile 279 */ 280 public function testMixedCssAnnotations() { 281 $basePath = __DIR__ . '/../../data/css'; 282 $testModule = new ResourceLoaderFileTestModule( [ 283 'localBasePath' => $basePath, 284 'styles' => [ 'test.css' ], 285 ] ); 286 $testModule->setName( 'testing' ); 287 $expectedModule = new ResourceLoaderFileTestModule( [ 288 'localBasePath' => $basePath, 289 'styles' => [ 'expected.css' ], 290 ] ); 291 $expectedModule->setName( 'testing' ); 292 293 $contextLtr = $this->getResourceLoaderContext( [ 294 'lang' => 'en', 295 'dir' => 'ltr', 296 ] ); 297 $contextRtl = $this->getResourceLoaderContext( [ 298 'lang' => 'he', 299 'dir' => 'rtl', 300 ] ); 301 302 // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and 303 // the @noflip annotations are always preserved, we need to strip them first. 304 $this->assertEquals( 305 $expectedModule->getStyles( $contextLtr ), 306 self::stripNoflip( $testModule->getStyles( $contextLtr ) ), 307 "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode" 308 ); 309 $this->assertEquals( 310 $expectedModule->getStyles( $contextLtr ), 311 self::stripNoflip( $testModule->getStyles( $contextRtl ) ), 312 "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode" 313 ); 314 } 315 316 /** 317 * @covers ResourceLoaderFileModule 318 */ 319 public function testCssFlipping() { 320 $plain = new ResourceLoaderFileTestModule( [ 321 'localBasePath' => __DIR__ . '/../../data/resourceloader', 322 'styles' => [ 'direction.css' ], 323 ] ); 324 $plain->setName( 'test' ); 325 326 $context = $this->getResourceLoaderContext( [ 'lang' => 'en', 'dir' => 'ltr' ] ); 327 $this->assertEquals( 328 $plain->getStyles( $context ), 329 [ 'all' => ".example { text-align: left; }\n" ], 330 'Unchanged styles in LTR mode' 331 ); 332 $context = $this->getResourceLoaderContext( [ 'lang' => 'he', 'dir' => 'rtl' ] ); 333 $this->assertEquals( 334 $plain->getStyles( $context ), 335 [ 'all' => ".example { text-align: right; }\n" ], 336 'Flipped styles in RTL mode' 337 ); 338 339 $noflip = new ResourceLoaderFileTestModule( [ 340 'localBasePath' => __DIR__ . '/../../data/resourceloader', 341 'styles' => [ 'direction.css' ], 342 'noflip' => true, 343 ] ); 344 $noflip->setName( 'test' ); 345 $this->assertEquals( 346 $plain->getStyles( $context ), 347 [ 'all' => ".example { text-align: right; }\n" ], 348 'Unchanged styles in RTL mode with noflip at module level' 349 ); 350 } 351 352 /** 353 * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath. 354 * 355 * The use of ResourceLoaderFilePath objects resembles the way that ResourceLoader::getModule() 356 * injects additional files when 'ResourceModuleSkinStyles' or 'OOUIThemePaths' skin attributes 357 * apply to a given module. 358 * 359 * @covers ResourceLoaderFilePath::getLocalBasePath 360 * @covers ResourceLoaderFilePath::getRemoteBasePath 361 */ 362 public function testResourceLoaderFilePath() { 363 $basePath = __DIR__ . '/../../data/blahblah'; 364 $filePath = __DIR__ . '/../../data/rlfilepath'; 365 $testModule = new ResourceLoaderFileModule( [ 366 'localBasePath' => $basePath, 367 'remoteBasePath' => 'blahblah', 368 'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ), 369 'skinStyles' => [ 370 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ), 371 ], 372 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ), 373 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ), 374 ] ); 375 $expectedModule = new ResourceLoaderFileModule( [ 376 'localBasePath' => $filePath, 377 'remoteBasePath' => 'rlfilepath', 378 'styles' => 'style.css', 379 'skinStyles' => [ 380 'vector' => 'skinStyle.css', 381 ], 382 'scripts' => 'script.js', 383 'templates' => 'template.html', 384 ] ); 385 386 $context = $this->getResourceLoaderContext(); 387 $this->assertEquals( 388 $expectedModule->getModuleContent( $context ), 389 $testModule->getModuleContent( $context ), 390 "Using ResourceLoaderFilePath works correctly" 391 ); 392 } 393 394 public static function providerGetTemplates() { 395 $modules = self::getModules(); 396 397 return [ 398 [ 399 $modules['noTemplateModule'], 400 [], 401 ], 402 [ 403 $modules['templateModuleHandlebars'], 404 [ 405 'templates/template_awesome.handlebars' => "wow\n", 406 ], 407 ], 408 [ 409 $modules['htmlTemplateModule'], 410 [ 411 'templates/template.html' => "<strong>hello</strong>\n", 412 'templates/template2.html' => "<div>goodbye</div>\n", 413 ], 414 ], 415 [ 416 $modules['aliasedHtmlTemplateModule'], 417 [ 418 'foo.html' => "<strong>hello</strong>\n", 419 'bar.html' => "<div>goodbye</div>\n", 420 ], 421 ], 422 [ 423 $modules['htmlTemplateUnknown'], 424 false, 425 ], 426 ]; 427 } 428 429 /** 430 * @dataProvider providerGetTemplates 431 * @covers ResourceLoaderFileModule::getTemplates 432 */ 433 public function testGetTemplates( $module, $expected ) { 434 $rl = new ResourceLoaderFileModule( $module ); 435 $rl->setName( 'testing' ); 436 437 if ( $expected === false ) { 438 $this->expectException( RuntimeException::class ); 439 $rl->getTemplates(); 440 } else { 441 $this->assertEquals( $rl->getTemplates(), $expected ); 442 } 443 } 444 445 /** 446 * @covers ResourceLoaderFileModule::stripBom 447 */ 448 public function testBomConcatenation() { 449 $basePath = __DIR__ . '/../../data/css'; 450 $testModule = new ResourceLoaderFileTestModule( [ 451 'localBasePath' => $basePath, 452 'styles' => [ 'bom.css' ], 453 ] ); 454 $testModule->setName( 'testing' ); 455 $this->assertEquals( 456 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ), 457 "\xef\xbb\xbf.efbbbf", 458 'File has leading BOM' 459 ); 460 461 $context = $this->getResourceLoaderContext(); 462 $this->assertEquals( 463 $testModule->getStyles( $context ), 464 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ], 465 'Leading BOM removed when concatenating files' 466 ); 467 } 468 469 /** 470 * @covers ResourceLoaderFileModule 471 */ 472 public function testLessFileCompilation() { 473 $context = $this->getResourceLoaderContext(); 474 $basePath = __DIR__ . '/../../data/less/module'; 475 $module = new ResourceLoaderFileTestModule( [ 476 'localBasePath' => $basePath, 477 'styles' => [ 'styles.less' ], 478 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ] 479 ] ); 480 $module->setName( 'test.less' ); 481 $styles = $module->getStyles( $context ); 482 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] ); 483 } 484 485 public function provideGetVersionHash() { 486 $a = []; 487 $b = [ 488 'lessVars' => [ 'key' => 'value' ], 489 ]; 490 yield 'with and without Less variables' => [ $a, $b, false ]; 491 492 $a = [ 493 'lessVars' => [ 'key' => 'value1' ], 494 ]; 495 $b = [ 496 'lessVars' => [ 'key' => 'value2' ], 497 ]; 498 yield 'different Less variables' => [ $a, $b, false ]; 499 500 $x = [ 501 'lessVars' => [ 'key' => 'value' ], 502 ]; 503 yield 'identical Less variables' => [ $x, $x, true ]; 504 505 $a = [ 506 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () { 507 return [ 'aaa' ]; 508 } ] ] 509 ]; 510 $b = [ 511 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () { 512 return [ 'bbb' ]; 513 } ] ] 514 ]; 515 yield 'packageFiles with different callback' => [ $a, $b, false ]; 516 517 $a = [ 518 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => static function () { 519 return [ 'x' ]; 520 } ] ] 521 ]; 522 $b = [ 523 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => static function () { 524 return [ 'x' ]; 525 } ] ] 526 ]; 527 yield 'packageFiles with different file name and a callback' => [ $a, $b, false ]; 528 529 $a = [ 530 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () { 531 return [ 'A-version' ]; 532 }, 'callback' => static function () { 533 throw new Exception( 'Unexpected computation' ); 534 } ] ] 535 ]; 536 $b = [ 537 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () { 538 return [ 'B-version' ]; 539 }, 'callback' => static function () { 540 throw new Exception( 'Unexpected computation' ); 541 } ] ] 542 ]; 543 yield 'packageFiles with different versionCallback' => [ $a, $b, false ]; 544 545 $a = [ 546 'packageFiles' => [ [ 'name' => 'aaa.json', 547 'versionCallback' => static function () { 548 return [ 'X-version' ]; 549 }, 550 'callback' => static function () { 551 throw new Exception( 'Unexpected computation' ); 552 } 553 ] ] 554 ]; 555 $b = [ 556 'packageFiles' => [ [ 'name' => 'bbb.json', 557 'versionCallback' => static function () { 558 return [ 'X-version' ]; 559 }, 560 'callback' => static function () { 561 throw new Exception( 'Unexpected computation' ); 562 } 563 ] ] 564 ]; 565 yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ]; 566 } 567 568 /** 569 * @dataProvider provideGetVersionHash 570 * @covers ResourceLoaderFileModule::getDefinitionSummary 571 * @covers ResourceLoaderFileModule::getFileHashes 572 */ 573 public function testGetVersionHash( $a, $b, $isEqual ) { 574 $context = $this->getResourceLoaderContext(); 575 576 $moduleA = new ResourceLoaderFileTestModule( $a ); 577 $moduleA->setConfig( $context->getResourceLoader()->getConfig() ); 578 $versionA = $moduleA->getVersionHash( $context ); 579 $moduleB = new ResourceLoaderFileTestModule( $b ); 580 $moduleB->setConfig( $context->getResourceLoader()->getConfig() ); 581 $versionB = $moduleB->getVersionHash( $context ); 582 583 $this->assertSame( 584 $isEqual, 585 ( $versionA === $versionB ), 586 'Whether versions hashes are equal' 587 ); 588 } 589 590 public function provideGetScriptPackageFiles() { 591 $basePath = __DIR__ . '/../../data/resourceloader'; 592 $base = [ 'localBasePath' => $basePath ]; 593 $commentScript = file_get_contents( "$basePath/script-comment.js" ); 594 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" ); 595 $vueComponentDebug = trim( file_get_contents( "$basePath/vue-component-output-debug.js.txt" ) ); 596 $vueComponentNonDebug = trim( file_get_contents( "$basePath/vue-component-output-nondebug.js.txt" ) ); 597 $config = RequestContext::getMain()->getConfig(); 598 return [ 599 [ 600 $base + [ 601 'packageFiles' => [ 602 'script-comment.js', 603 'script-nosemi.js' 604 ] 605 ], 606 [ 607 'files' => [ 608 'script-comment.js' => [ 609 'type' => 'script', 610 'content' => $commentScript, 611 ], 612 'script-nosemi.js' => [ 613 'type' => 'script', 614 'content' => $nosemiScript 615 ] 616 ], 617 'main' => 'script-comment.js' 618 ] 619 ], 620 [ 621 $base + [ 622 'packageFiles' => [ 623 'script-comment.js', 624 [ 'name' => 'script-nosemi.js', 'main' => true ] 625 ], 626 'deprecated' => 'Deprecation test', 627 'name' => 'test-deprecated' 628 ], 629 [ 630 'files' => [ 631 'script-comment.js' => [ 632 'type' => 'script', 633 'content' => $commentScript, 634 ], 635 'script-nosemi.js' => [ 636 'type' => 'script', 637 'content' => 'mw.log.warn(' . 638 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' . 639 "Deprecation test" . 640 '");' . 641 $nosemiScript 642 ] 643 ], 644 'main' => 'script-nosemi.js' 645 ] 646 ], 647 [ 648 $base + [ 649 'packageFiles' => [ 650 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ], 651 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ], 652 ] 653 ], 654 [ 655 'files' => [ 656 'init.js' => [ 657 'type' => 'script', 658 'content' => $commentScript, 659 ], 660 'nosemi.js' => [ 661 'type' => 'script', 662 'content' => $nosemiScript 663 ] 664 ], 665 'main' => 'init.js' 666 ] 667 ], 668 'package file with callback' => [ 669 $base + [ 670 'packageFiles' => [ 671 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ], 672 'sample.json', 673 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ], 674 [ 675 'name' => 'data.json', 676 'callback' => static function ( $context, $config, $extra ) { 677 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ]; 678 }, 679 'callbackParam' => [ 'a' => 'b' ], 680 ], 681 [ 'name' => 'config.json', 'config' => [ 682 'Sitename', 683 'server' => 'ServerName', 684 ] ], 685 ] 686 ], 687 [ 688 'files' => [ 689 'foo.json' => [ 690 'type' => 'data', 691 'content' => [ 'Hello' => 'world' ], 692 ], 693 'sample.json' => [ 694 'type' => 'data', 695 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ], 696 ], 697 'bar.js' => [ 698 'type' => 'script', 699 'content' => "console.log('Hello');", 700 ], 701 'data.json' => [ 702 'type' => 'data', 703 'content' => [ 'langCode' => 'fy', 'extra' => [ 'a' => 'b' ] ], 704 ], 705 'config.json' => [ 706 'type' => 'data', 707 'content' => [ 708 'Sitename' => $config->get( 'Sitename' ), 709 'server' => $config->get( 'ServerName' ), 710 ] 711 ] 712 ], 713 'main' => 'bar.js' 714 ], 715 [ 716 'lang' => 'fy' 717 ] 718 ], 719 'package file with callback and versionCallback' => [ 720 $base + [ 721 'packageFiles' => [ 722 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ], 723 [ 724 'name' => 'data.json', 725 'versionCallback' => static function ( $context ) { 726 return 'x'; 727 }, 728 'callback' => static function ( $context, $config, $extra ) { 729 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ]; 730 }, 731 'callbackParam' => [ 'A', 'B' ] 732 ], 733 ] 734 ], 735 [ 736 'files' => [ 737 'bar.js' => [ 738 'type' => 'script', 739 'content' => "console.log('Hello');", 740 ], 741 'data.json' => [ 742 'type' => 'data', 743 'content' => [ 'langCode' => 'fy', 'extra' => [ 'A', 'B' ] ], 744 ], 745 ], 746 'main' => 'bar.js' 747 ], 748 [ 749 'lang' => 'fy' 750 ] 751 ], 752 'package file with callback that returns a file (1)' => [ 753 $base + [ 754 'packageFiles' => [ 755 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) { 756 $file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js'; 757 return new ResourceLoaderFilePath( $file ); 758 } ] 759 ] 760 ], 761 [ 762 'files' => [ 763 'dynamic.js' => [ 764 'type' => 'script', 765 'content' => $commentScript, 766 ] 767 ], 768 'main' => 'dynamic.js' 769 ], 770 [ 771 'lang' => 'fy' 772 ] 773 ], 774 'package file with callback that returns a file (2)' => [ 775 $base + [ 776 'packageFiles' => [ 777 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) { 778 $file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js'; 779 return new ResourceLoaderFilePath( $file ); 780 } ] 781 ] 782 ], 783 [ 784 'files' => [ 785 'dynamic.js' => [ 786 'type' => 'script', 787 'content' => $nosemiScript, 788 ] 789 ], 790 'main' => 'dynamic.js' 791 ], 792 [ 793 'lang' => 'nl' 794 ] 795 ], 796 '.vue file in debug mode' => [ 797 $base + [ 798 'packageFiles' => [ 799 'vue-component.vue' 800 ] 801 ], 802 [ 803 'files' => [ 804 'vue-component.vue' => [ 805 'type' => 'script', 806 'content' => $vueComponentDebug 807 ] 808 ], 809 'main' => 'vue-component.vue', 810 ], 811 [ 812 'debug' => 'true' 813 ] 814 ], 815 '.vue file in non-debug mode' => [ 816 $base + [ 817 'packageFiles' => [ 818 'vue-component.vue' 819 ], 820 'name' => 'nondebug', 821 ], 822 [ 823 'files' => [ 824 'vue-component.vue' => [ 825 'type' => 'script', 826 'content' => $vueComponentNonDebug 827 ] 828 ], 829 'main' => 'vue-component.vue' 830 ], 831 [ 832 'debug' => 'false' 833 ] 834 ], 835 [ 836 $base + [ 837 'packageFiles' => [ 838 [ 'file' => 'script-comment.js' ] 839 ] 840 ], 841 LogicException::class 842 ], 843 'package file with invalid callback' => [ 844 $base + [ 845 'packageFiles' => [ 846 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ] 847 ] 848 ], 849 LogicException::class 850 ], 851 [ 852 // 'config' not valid for 'script' type 853 $base + [ 854 'packageFiles' => [ 855 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ] 856 ] 857 ], 858 LogicException::class 859 ], 860 [ 861 // 'config' not valid for '*.js' file 862 $base + [ 863 'packageFiles' => [ 864 [ 'name' => 'foo.js', 'config' => 'Sitename' ] 865 ] 866 ], 867 LogicException::class 868 ], 869 [ 870 // missing type/name/file. 871 $base + [ 872 'packageFiles' => [ 873 'foo.js' => [ 'garbage' => 'data' ] 874 ] 875 ], 876 LogicException::class 877 ], 878 [ 879 $base + [ 880 'packageFiles' => [ 881 'filethatdoesnotexist142857.js' 882 ] 883 ], 884 RuntimeException::class 885 ], 886 [ 887 // JSON can't be a main file 888 $base + [ 889 'packageFiles' => [ 890 'script-nosemi.js', 891 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ] 892 ] 893 ], 894 LogicException::class 895 ] 896 ]; 897 } 898 899 /** 900 * @dataProvider provideGetScriptPackageFiles 901 * @covers ResourceLoaderFileModule::getScript 902 * @covers ResourceLoaderFileModule::getPackageFiles 903 * @covers ResourceLoaderFileModule::expandPackageFiles 904 */ 905 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) { 906 $module = new ResourceLoaderFileModule( $moduleDefinition ); 907 $context = $this->getResourceLoaderContext( $contextOptions ); 908 $module->setConfig( $context->getResourceLoader()->getConfig() ); 909 if ( isset( $moduleDefinition['name'] ) ) { 910 $module->setName( $moduleDefinition['name'] ); 911 } 912 if ( is_string( $expected ) ) { 913 // Class name of expected exception 914 $this->expectException( $expected ); 915 $module->getScript( $context ); 916 } else { 917 // Array of expected return value 918 $this->assertEquals( $expected, $module->getScript( $context ) ); 919 } 920 } 921 922 /** 923 * @covers ResourceLoaderFileModule::requiresES6 924 */ 925 public function testRequiresES6() { 926 $module = new ResourceLoaderFileModule(); 927 $this->assertFalse( $module->requiresES6(), 'requiresES6 defaults to false' ); 928 $module = new ResourceLoaderFileModule( [ 'es6' => false ] ); 929 $this->assertFalse( $module->requiresES6(), 'requiresES6 is false when set to false' ); 930 $module = new ResourceLoaderFileModule( [ 'es6' => true ] ); 931 $this->assertTrue( $module->requiresES6(), 'requiresES6 is true when set to true' ); 932 } 933} 934