3 namespace MediaWiki\Tests\ResourceLoader
;
6 use MediaWiki\Config\HashConfig
;
7 use MediaWiki\MainConfigNames
;
8 use MediaWiki\MediaWikiServices
;
9 use MediaWiki\ResourceLoader\FileModule
;
10 use MediaWiki\ResourceLoader\FilePath
;
11 use MediaWiki\ResourceLoader\ResourceLoader
;
12 use MediaWiki\Tests\Unit\DummyServicesTrait
;
15 use Wikimedia\TestingAccessWrapper
;
18 * @group ResourceLoader
19 * @covers \MediaWiki\ResourceLoader\FileModule
21 class FileModuleTest
extends ResourceLoaderTestCase
{
22 use DummyServicesTrait
;
24 protected function setUp(): void
{
27 $skinFactory = new SkinFactory( $this->getDummyObjectFactory(), [] );
28 // The empty spec shouldn't matter since this test should never call it
29 $skinFactory->register(
34 $this->setService( 'SkinFactory', $skinFactory );
36 // This test is not expected to query any database
37 $this->getServiceContainer()->disableStorage();
40 private static function getModules() {
42 'localBasePath' => __DIR__
,
46 'noTemplateModule' => [],
48 'htmlTemplateModule' => $base +
[
50 'templates/template.html',
51 'templates/template2.html',
55 'htmlTemplateUnknown' => $base +
[
57 'templates/notfound.html',
61 'aliasedHtmlTemplateModule' => $base +
[
63 'foo.html' => 'templates/template.html',
64 'bar.html' => 'templates/template2.html',
68 'templateModuleHandlebars' => $base +
[
70 'templates/template_awesome.handlebars',
74 'aliasFooFromBar' => $base +
[
76 'foo.foo' => 'templates/template.bar',
82 public static function providerTemplateDependencies() {
83 $modules = self
::getModules();
87 $modules['noTemplateModule'],
91 $modules['htmlTemplateModule'],
97 $modules['templateModuleHandlebars'],
100 'mediawiki.template.handlebars',
104 $modules['aliasFooFromBar'],
106 'mediawiki.template',
107 'mediawiki.template.foo',
114 * @dataProvider providerTemplateDependencies
116 public function testTemplateDependencies( $module, $expected ) {
117 $rl = new FileModule( $module );
118 $rl->setName( 'testing' );
119 $this->assertEquals( $expected, $rl->getDependencies() );
122 public function testGetScript() {
123 $localBasePath = __DIR__
. '/../../data/resourceloader';
124 $remoteBasePath = '/w';
125 $module = new FileModule( [
126 'localBasePath' => $localBasePath,
127 'remoteBasePath' => $remoteBasePath,
128 'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
130 $module->setName( 'testing' );
131 $ctx = $this->getResourceLoaderContext();
135 'script-nosemi.js' => [
136 'name' => 'script-nosemi.js',
137 'content' => "/* eslint-disable */\nmw.foo()\n",
139 'filePath' => new FilePath(
145 'script-comment.js' => [
146 'name' => 'script-comment.js',
147 'content' => "/* eslint-disable */\nmw.foo()\n// mw.bar();\n",
149 'filePath' => new FilePath(
157 $module->getScript( $ctx )
162 * @covers \MediaWiki\ResourceLoader\FileModule
163 * @covers \MediaWiki\ResourceLoader\Module
164 * @covers \MediaWiki\ResourceLoader\ResourceLoader
166 public function testGetURLsForDebug() {
167 $ctx = $this->getResourceLoaderContext();
168 $module = new FileModule( [
169 'localBasePath' => __DIR__
. '/../../data/resourceloader',
170 'remoteBasePath' => '/w/something',
171 'styles' => [ 'simple.css' ],
172 'scripts' => [ 'script-comment.js' ],
174 $module->setName( 'testing' );
175 $module->setConfig( $ctx->getResourceLoader()->getConfig() );
179 'https://example.org/w/something/script-comment.js'
181 $module->getScriptURLsForDebug( $ctx ),
186 '/w/something/simple.css'
188 $module->getStyleURLsForDebug( $ctx ),
193 public function testGetAllSkinStyleFiles() {
201 'bar.css' => [ 'media' => 'print' ],
202 'screen.less' => [ 'media' => 'screen' ],
203 'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ],
206 'default' => 'quux-fallback.less',
218 $module = new FileModule( $baseParams );
219 $module->setName( 'testing' );
226 'quux-fallback.less',
231 array_map( 'basename', $module->getAllStyleFiles() )
236 * Strip @noflip annotations from CSS code.
240 private static function stripNoflip( $css ) {
241 return str_replace( '/*@noflip*/ ', '', $css );
245 * Confirm that 'ResourceModuleSkinStyles' skin attributes get injected
246 * into the module, and have their file contents read correctly from their
247 * own (out-of-module) directories.
249 * @covers \MediaWiki\ResourceLoader\FileModule
250 * @covers \MediaWiki\ResourceLoader\ResourceLoader
252 public function testInjectSkinStyles() {
253 $moduleDir = __DIR__
. '/../../data/resourceloader';
254 $skinDir = __DIR__
. '/../../data/resourceloader/myskin';
255 $rl = new ResourceLoader( new HashConfig( self
::getSettings() ) );
256 $rl->setModuleSkinStyles( [
258 'localBasePath' => $skinDir,
264 $rl->register( 'testing', [
265 'localBasePath' => $moduleDir,
266 'styles' => [ 'simple.css' ],
268 $ctx = $this->getResourceLoaderContext( [ 'skin' => 'fakeskin' ], $rl );
270 $module = $rl->getModule( 'testing' );
271 $this->assertInstanceOf( FileModule
::class, $module );
273 [ 'all' => ".example { color: blue; }\n\n.override { line-height: 2; }\n" ],
274 $module->getStyles( $ctx )
279 * Verify what happens when you mix @embed and @noflip.
281 public function testMixedCssAnnotations() {
282 $basePath = __DIR__
. '/../../data/css';
283 $testModule = new ResourceLoaderFileTestModule( [
284 'localBasePath' => $basePath,
285 'styles' => [ 'test.css' ],
287 $testModule->setName( 'testing' );
288 $expectedModule = new ResourceLoaderFileTestModule( [
289 'localBasePath' => $basePath,
290 'styles' => [ 'expected.css' ],
292 $expectedModule->setName( 'testing' );
294 $contextLtr = $this->getResourceLoaderContext( [
298 $contextRtl = $this->getResourceLoaderContext( [
303 // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
304 // the @noflip annotations are always preserved, we need to strip them first.
306 $expectedModule->getStyles( $contextLtr ),
307 self
::stripNoflip( $testModule->getStyles( $contextLtr ) ),
308 "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
311 $expectedModule->getStyles( $contextLtr ),
312 self
::stripNoflip( $testModule->getStyles( $contextRtl ) ),
313 "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
317 public function testCssFlipping() {
318 $plain = new ResourceLoaderFileTestModule( [
319 'localBasePath' => __DIR__
. '/../../data/resourceloader',
320 'styles' => [ 'direction.css' ],
322 $plain->setName( 'test' );
324 $context = $this->getResourceLoaderContext( [ 'lang' => 'en', 'dir' => 'ltr' ] );
326 [ 'all' => ".example { text-align: left; }\n" ],
327 $plain->getStyles( $context ),
328 'Unchanged styles in LTR mode'
330 $context = $this->getResourceLoaderContext( [ 'lang' => 'he', 'dir' => 'rtl' ] );
332 [ 'all' => ".example { text-align: right; }\n" ],
333 $plain->getStyles( $context ),
334 'Flipped styles in RTL mode'
337 $noflip = new ResourceLoaderFileTestModule( [
338 'localBasePath' => __DIR__
. '/../../data/resourceloader',
339 'styles' => [ 'direction.css' ],
342 $noflip->setName( 'test' );
344 [ 'all' => ".example { text-align: right; }\n" ],
345 $plain->getStyles( $context ),
346 'Unchanged styles in RTL mode with noflip at module level'
351 * Test reading files from elsewhere than localBasePath using FilePath.
353 * The use of FilePath objects resembles the way that ResourceLoader::getModule()
354 * injects additional files when 'ResourceModuleSkinStyles' or 'OOUIThemePaths'
355 * skin attributes apply to a given module.
357 public function testResourceLoaderFilePath() {
358 $basePath = __DIR__
. '/../../data/blahblah';
359 $filePath = __DIR__
. '/../../data/rlfilepath';
360 $testModule = new FileModule( [
361 'localBasePath' => $basePath,
362 'remoteBasePath' => 'blahblah',
363 'styles' => new FilePath( 'style.css', $filePath, 'rlfilepath' ),
365 'vector' => new FilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
367 'scripts' => new FilePath( 'script.js', $filePath, 'rlfilepath' ),
368 'templates' => new FilePath( 'template.html', $filePath, 'rlfilepath' ),
370 $testModule->setName( 'testModule' );
371 $expectedModule = new FileModule( [
372 'localBasePath' => $filePath,
373 'remoteBasePath' => 'rlfilepath',
374 'styles' => 'style.css',
376 'vector' => 'skinStyle.css',
378 'scripts' => 'script.js',
379 'templates' => 'template.html',
381 $expectedModule->setName( 'expectedModule' );
383 $context = $this->getResourceLoaderContext();
385 $expectedModule->getModuleContent( $context ),
386 $testModule->getModuleContent( $context ),
387 "Using ResourceLoaderFilePath works correctly"
391 public static function providerGetTemplates() {
392 $modules = self
::getModules();
396 $modules['noTemplateModule'],
400 $modules['templateModuleHandlebars'],
402 'templates/template_awesome.handlebars' => "wow\n",
406 $modules['htmlTemplateModule'],
408 'templates/template.html' => "<strong>hello</strong>\n",
409 'templates/template2.html' => "<div>goodbye</div>\n",
413 $modules['aliasedHtmlTemplateModule'],
415 'foo.html' => "<strong>hello</strong>\n",
416 'bar.html' => "<div>goodbye</div>\n",
420 $modules['htmlTemplateUnknown'],
427 * @dataProvider providerGetTemplates
429 public function testGetTemplates( $module, $expected ) {
430 $rl = new FileModule( $module );
431 $rl->setName( 'testing' );
433 if ( $expected === false ) {
434 $this->expectException( RuntimeException
::class );
437 $this->assertEquals( $expected, $rl->getTemplates() );
441 public function testBomConcatenation() {
442 $basePath = __DIR__
. '/../../data/css';
443 $testModule = new ResourceLoaderFileTestModule( [
444 'localBasePath' => $basePath,
445 'styles' => [ 'bom.css' ],
447 $testModule->setName( 'testing' );
449 "\xef\xbb\xbf.efbbbf",
450 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
451 'File has leading BOM'
454 $context = $this->getResourceLoaderContext();
456 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
457 $testModule->getStyles( $context ),
458 'Leading BOM removed when concatenating files'
462 public function testLessFileCompilation() {
463 $context = $this->getResourceLoaderContext();
464 $basePath = __DIR__
. '/../../data/less/module';
465 $module = new ResourceLoaderFileTestModule( [
466 'localBasePath' => $basePath,
467 'styles' => [ 'styles.less' ],
468 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
470 $module->setName( 'test.less' );
471 $module->setConfig( $context->getResourceLoader()->getConfig() );
472 $styles = $module->getStyles( $context );
473 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
476 public static function provideGetVersionHash() {
479 'lessVars' => [ 'key' => 'value' ],
481 yield
'with and without Less variables' => [ $a, $b, false ];
484 'lessVars' => [ 'key' => 'value1' ],
487 'lessVars' => [ 'key' => 'value2' ],
489 yield
'different Less variables' => [ $a, $b, false ];
492 'lessVars' => [ 'key' => 'value' ],
494 yield
'identical Less variables' => [ $x, $x, true ];
497 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
502 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
506 yield
'packageFiles with different callback' => [ $a, $b, false ];
509 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => static function () {
514 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => static function () {
518 yield
'packageFiles with different file name and a callback' => [ $a, $b, false ];
521 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
522 return [ 'A-version' ];
523 }, 'callback' => static function () {
524 throw new LogicException( 'Unexpected computation' );
528 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
529 return [ 'B-version' ];
530 }, 'callback' => static function () {
531 throw new LogicException( 'Unexpected computation' );
534 yield
'packageFiles with different versionCallback' => [ $a, $b, false ];
537 'packageFiles' => [ [ 'name' => 'aaa.json',
538 'versionCallback' => static function () {
539 return [ 'X-version' ];
541 'callback' => static function () {
542 throw new LogicException( 'Unexpected computation' );
547 'packageFiles' => [ [ 'name' => 'bbb.json',
548 'versionCallback' => static function () {
549 return [ 'X-version' ];
551 'callback' => static function () {
552 throw new LogicException( 'Unexpected computation' );
556 yield
'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
560 * @dataProvider provideGetVersionHash
562 public function testGetVersionHash( $a, $b, $isEqual ) {
563 $context = $this->getResourceLoaderContext( [ 'debug' => 'false' ] );
565 $moduleA = new ResourceLoaderFileTestModule( $a );
566 $moduleA->setConfig( $context->getResourceLoader()->getConfig() );
567 $versionA = $moduleA->getVersionHash( $context );
568 $moduleB = new ResourceLoaderFileTestModule( $b );
569 $moduleB->setConfig( $context->getResourceLoader()->getConfig() );
570 $versionB = $moduleB->getVersionHash( $context );
574 ( $versionA === $versionB ),
575 'Whether versions hashes are equal'
579 public static function provideGetScriptPackageFiles() {
580 $basePath = __DIR__
. '/../../data/resourceloader';
581 $basePathB = __DIR__
. '/../../data/resourceloader-b';
582 $base = [ 'localBasePath' => $basePath ];
583 $commentScript = file_get_contents( "$basePath/script-comment.js" );
584 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
585 $nosemiBScript = file_get_contents( "$basePathB/script-nosemi.js" );
586 $vueComponentDebug = trim( file_get_contents( "$basePath/vue-component-output-debug.js.txt" ) );
587 $vueComponentNonDebug = trim( file_get_contents( "$basePath/vue-component-output-nondebug.js.txt" ) );
588 $config = MediaWikiServices
::getInstance()->getMainConfig();
599 'script-comment.js' => [
601 'content' => $commentScript,
602 'filePath' => 'script-comment.js'
604 'script-nosemi.js' => [
606 'content' => $nosemiScript,
607 'filePath' => 'script-nosemi.js'
610 'main' => 'script-comment.js'
613 'explicit main file' => [
616 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
617 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
624 'content' => $commentScript,
625 'filePath' => 'script-comment.js',
629 'content' => $nosemiScript,
630 'filePath' => 'script-nosemi.js',
636 'package file with callback' => [
639 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
641 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
643 'name' => 'data.json',
644 'callback' => static function ( $context, $config, $extra ) {
645 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
647 'callbackParam' => [ 'a' => 'b' ],
649 [ 'name' => 'config.json', 'config' => [
651 'server' => 'ServerName',
659 'content' => [ 'Hello' => 'world' ],
660 'virtualFilePath' => 'foo.json',
664 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
665 'filePath' => 'sample.json',
669 'content' => "console.log('Hello');",
670 'virtualFilePath' => 'bar.js',
674 'content' => [ 'langCode' => 'fy', 'extra' => [ 'a' => 'b' ] ],
675 'virtualFilePath' => 'data.json',
680 'Sitename' => $config->get( MainConfigNames
::Sitename
),
681 'server' => $config->get( MainConfigNames
::ServerName
),
683 'virtualFilePath' => 'config.json',
692 'package file with callback and versionCallback' => [
695 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
697 'name' => 'data.json',
698 'versionCallback' => static function ( $context ) {
701 'callback' => static function ( $context, $config, $extra ) {
702 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
704 'callbackParam' => [ 'A', 'B' ]
712 'content' => "console.log('Hello');",
713 'virtualFilePath' => 'bar.js',
717 'content' => [ 'langCode' => 'fy', 'extra' => [ 'A', 'B' ] ],
718 'virtualFilePath' => 'data.json',
727 'package file with callback that returns a file (1)' => [
730 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
731 $file = $context->getLanguage() === 'fy' ?
'script-comment.js' : 'script-nosemi.js';
732 return new FilePath( $file );
740 'content' => $commentScript,
741 'filePath' => 'script-comment.js',
744 'main' => 'dynamic.js'
750 'package file with callback that returns a file (2)' => [
753 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
754 $file = $context->getLanguage() === 'fy' ?
'script-comment.js' : 'script-nosemi.js';
755 return new FilePath( $file );
763 'content' => $nosemiScript,
764 'filePath' => 'script-nosemi.js'
767 'main' => 'dynamic.js'
773 'package file with callback that returns a file with base path' => [
776 [ 'name' => 'dynamic.js', 'callback' => static function () use ( $basePathB ) {
777 return new FilePath( 'script-nosemi.js', $basePathB );
785 'content' => $nosemiBScript,
786 'filePath' => 'script-nosemi.js',
789 'main' => 'dynamic.js'
792 '.vue file in debug mode' => [
800 'vue-component.vue' => [
802 'content' => $vueComponentDebug,
803 'filePath' => 'vue-component.vue',
806 'main' => 'vue-component.vue',
812 '.vue file in non-debug mode' => [
817 'name' => 'nondebug',
821 'vue-component.vue' => [
823 'content' => $vueComponentNonDebug,
824 'filePath' => 'vue-component.vue',
827 'main' => 'vue-component.vue'
836 [ 'file' => 'script-comment.js' ]
839 LogicException
::class
841 'package file with invalid callback' => [
844 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
847 LogicException
::class
849 'config not valid for script type' => [
852 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
855 LogicException
::class
857 'config not valid for *.js file' => [
860 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
863 LogicException
::class
865 'missing type/name/file' => [
868 'foo.js' => [ 'garbage' => 'data' ]
871 LogicException
::class
873 'nonexistent file' => [
876 'filethatdoesnotexist142857.js'
879 RuntimeException
::class
881 'JSON can\'t be a main file' => [
885 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
888 LogicException
::class
894 * @dataProvider provideGetScriptPackageFiles
896 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
897 $module = new FileModule( $moduleDefinition );
898 $context = $this->getResourceLoaderContext( $contextOptions );
899 $module->setConfig( $context->getResourceLoader()->getConfig() );
900 if ( isset( $moduleDefinition['name'] ) ) {
901 $module->setName( $moduleDefinition['name'] );
903 if ( is_string( $expected ) ) {
904 // $expected is the class name of the expected exception
905 $this->expectException( $expected );
906 $module->getScript( $context );
907 $this->fail( "$expected exception expected" );
910 // Check name property and convert filePath to plain data
911 $result = $module->getScript( $context );
912 foreach ( $result['files'] as $name => &$file ) {
913 $this->assertSame( $name, $file['name'] );
914 unset( $file['name'] );
915 if ( isset( $file['filePath'] ) ) {
916 $this->assertInstanceOf( FilePath
::class, $file['filePath'] );
917 $file['filePath'] = $file['filePath']->getPath();
919 if ( isset( $file['virtualFilePath'] ) ) {
920 $this->assertInstanceOf( FilePath
::class, $file['virtualFilePath'] );
921 $file['virtualFilePath'] = $file['virtualFilePath']->getPath();
924 // Check the rest of the result
925 $this->assertEquals( $expected, $result );
928 public function testRequiresES6() {
929 $module = new FileModule();
930 $this->assertTrue( $module->requiresES6(), 'requiresES6 defaults to true' );
931 $module = new FileModule( [ 'es6' => false ] );
932 $this->assertTrue( $module->requiresES6(), 'requiresES6 is true even when set to false' );
933 $module = new FileModule( [ 'es6' => true ] );
934 $this->assertTrue( $module->requiresES6(), 'requiresES6 is true when set to true' );
938 * @covers \Wikimedia\DependencyStore\DependencyStore
940 public function testIndirectDependencies() {
941 $context = $this->getResourceLoaderContext();
942 $moduleInfo = [ 'dir' => __DIR__
. '/../../data/less/module',
943 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ], 'name' => 'styles-dependencies' ];
945 $module = $this->newModuleRequest( $moduleInfo, $context );
946 $module->getStyles( $context );
948 $module = $this->newModuleRequest( $moduleInfo, $context );
949 $dependencies = $module->getFileDependencies( $context );
951 $expectedDependencies = [ realpath( __DIR__
. '/../../data/less/common/test.common.mixins.less' ),
952 realpath( __DIR__
. '/../../data/less/module/dependency.less' ) ];
954 $this->assertEquals( $expectedDependencies, $dependencies );
958 * @covers \Wikimedia\DependencyStore\DependencyStore
960 public function testIndirectDependenciesUpdate() {
961 $context = $this->getResourceLoaderContext();
962 $tempDir = $this->getNewTempDirectory();
963 $moduleInfo = [ 'dir' => $tempDir, 'name' => 'new-dependencies' ];
965 file_put_contents( "$tempDir/styles.less", "@import './test.less';" );
966 file_put_contents( "$tempDir/test.less", "div { color: red; } " );
968 $module = $this->newModuleRequest( $moduleInfo, $context );
969 $module->getStyles( $context );
971 $module = $this->newModuleRequest( $moduleInfo, $context );
972 $dependencies = $module->getFileDependencies( $context );
974 $expectedDependencies = [ realpath( $tempDir . '/test.less' ) ];
976 $this->assertEquals( $expectedDependencies, $dependencies );
978 file_put_contents( "$tempDir/styles.less", "@import './pink.less';" );
979 file_put_contents( "$tempDir/pink.less", "div { color: pink; } " );
981 $module = $this->newModuleRequest( $moduleInfo, $context );
982 $module->getStyles( $context );
984 $module = $this->newModuleRequest( $moduleInfo, $context );
985 $dependencies = $module->getFileDependencies( $context );
987 $expectedDependencies = [ realpath( $tempDir . '/pink.less' ) ];
989 $this->assertEquals( $expectedDependencies, $dependencies );
992 public function newModuleRequest( $moduleInfo, $context ) {
993 $module = new ResourceLoaderFileTestModule( [
994 'localBasePath' => $moduleInfo['dir'],
995 'styles' => [ 'styles.less' ],
996 'lessVars' => $moduleInfo['lessVars'] ??
null
999 $module->setName( $moduleInfo['name'] );
1000 $module->setConfig( $context->getResourceLoader()->getConfig() );
1001 $wrapper = TestingAccessWrapper
::newFromObject( $module );