diff options
Diffstat (limited to 'tests/phpunit/includes/resourceloader')
4 files changed, 836 insertions, 0 deletions
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php new file mode 100644 index 00000000..b0edaaf7 --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php @@ -0,0 +1,132 @@ +<?php + +class ResourceLoaderModuleTest extends ResourceLoaderTestCase { + + protected function setUp() { + parent::setUp(); + + // The return value of the closure shouldn't matter since this test should + // never call it + SkinFactory::getDefaultInstance()->register( + 'fakeskin', + 'FakeSkin', + function () { + } + ); + } + + /** + * @covers ResourceLoaderFileModule::getAllSkinStyleFiles + */ + public function testGetAllSkinStyleFiles() { + $context = self::getResourceLoaderContext(); + + $baseParams = array( + 'scripts' => array( + 'foo.js', + 'bar.js', + ), + 'styles' => array( + 'foo.css', + 'bar.css' => array( 'media' => 'print' ), + 'screen.less' => array( 'media' => 'screen' ), + 'screen-query.css' => array( 'media' => 'screen and (min-width: 400px)' ), + ), + 'skinStyles' => array( + 'default' => 'quux-fallback.less', + 'fakeskin' => array( + 'baz-vector.css', + 'quux-vector.less', + ), + ), + 'messages' => array( + 'hello', + 'world', + ), + ); + + $module = new ResourceLoaderFileModule( $baseParams ); + + $this->assertEquals( + array( + 'foo.css', + 'baz-vector.css', + 'quux-vector.less', + 'quux-fallback.less', + 'bar.css', + 'screen.less', + 'screen-query.css', + ), + array_map( 'basename', $module->getAllStyleFiles() ) + ); + } + + /** + * @covers ResourceLoaderModule::getDefinitionSummary + * @covers ResourceLoaderFileModule::getDefinitionSummary + */ + public function testDefinitionSummary() { + $context = self::getResourceLoaderContext(); + + $baseParams = array( + 'scripts' => array( 'foo.js', 'bar.js' ), + 'dependencies' => array( 'jquery', 'mediawiki' ), + 'messages' => array( 'hello', 'world' ), + ); + + $module = new ResourceLoaderFileModule( $baseParams ); + + $jsonSummary = json_encode( $module->getDefinitionSummary( $context ) ); + + // Exactly the same + $module = new ResourceLoaderFileModule( $baseParams ); + + $this->assertEquals( + $jsonSummary, + json_encode( $module->getDefinitionSummary( $context ) ), + 'Instance is insignificant' + ); + + // Re-order dependencies + $module = new ResourceLoaderFileModule( array( + 'dependencies' => array( 'mediawiki', 'jquery' ), + ) + $baseParams ); + + $this->assertEquals( + $jsonSummary, + json_encode( $module->getDefinitionSummary( $context ) ), + 'Order of dependencies is insignificant' + ); + + // Re-order messages + $module = new ResourceLoaderFileModule( array( + 'messages' => array( 'world', 'hello' ), + ) + $baseParams ); + + $this->assertEquals( + $jsonSummary, + json_encode( $module->getDefinitionSummary( $context ) ), + 'Order of messages is insignificant' + ); + + // Re-order scripts + $module = new ResourceLoaderFileModule( array( + 'scripts' => array( 'bar.js', 'foo.js' ), + ) + $baseParams ); + + $this->assertNotEquals( + $jsonSummary, + json_encode( $module->getDefinitionSummary( $context ) ), + 'Order of scripts is significant' + ); + + // Subclass + $module = new ResourceLoaderFileModuleTestModule( $baseParams ); + + $this->assertNotEquals( + $jsonSummary, + json_encode( $module->getDefinitionSummary( $context ) ), + 'Class is significant' + ); + } +} diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php new file mode 100644 index 00000000..a1893873 --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php @@ -0,0 +1,388 @@ +<?php + +class ResourceLoaderStartupModuleTest extends ResourceLoaderTestCase { + + public static function provideGetModuleRegistrations() { + return array( + array( array( + 'msg' => 'Empty registry', + 'modules' => array(), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [] );' + ) ), + array( array( + 'msg' => 'Basic registry', + 'modules' => array( + 'test.blank' => new ResourceLoaderTestModule(), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400" + ] +] );', + ) ), + array( array( + 'msg' => 'Group signature', + 'modules' => array( + 'test.blank' => new ResourceLoaderTestModule(), + 'test.group.foo' => new ResourceLoaderTestModule( array( 'group' => 'x-foo' ) ), + 'test.group.bar' => new ResourceLoaderTestModule( array( 'group' => 'x-bar' ) ), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400" + ], + [ + "test.group.foo", + "1388534400", + [], + "x-foo" + ], + [ + "test.group.bar", + "1388534400", + [], + "x-bar" + ] +] );' + ) ), + array( array( + 'msg' => 'Different target (non-test should not be registered)', + 'modules' => array( + 'test.blank' => new ResourceLoaderTestModule(), + 'test.target.foo' => new ResourceLoaderTestModule( array( 'targets' => array( 'x-foo' ) ) ), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400" + ] +] );' + ) ), + array( array( + 'msg' => 'Foreign source', + 'sources' => array( + 'example' => array( + 'loadScript' => 'http://example.org/w/load.php', + 'apiScript' => 'http://example.org/w/api.php', + ), + ), + 'modules' => array( + 'test.blank' => new ResourceLoaderTestModule( array( 'source' => 'example' ) ), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php", + "example": "http://example.org/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400", + [], + null, + "example" + ] +] );' + ) ), + array( array( + 'msg' => 'Conditional dependency function', + 'modules' => array( + 'test.x.core' => new ResourceLoaderTestModule(), + 'test.x.polyfill' => new ResourceLoaderTestModule( array( + 'skipFunction' => 'return true;' + ) ), + 'test.y.polyfill' => new ResourceLoaderTestModule( array( + 'skipFunction' => + 'return !!(' . + ' window.JSON &&' . + ' JSON.parse &&' . + ' JSON.stringify' . + ');' + ) ), + 'test.z.foo' => new ResourceLoaderTestModule( array( + 'dependencies' => array( + 'test.x.core', + 'test.x.polyfil', + 'test.y.polyfil', + ), + ) ), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [ + [ + "test.x.core", + "1388534400" + ], + [ + "test.x.polyfill", + "1388534400", + [], + null, + "local", + "return true;" + ], + [ + "test.y.polyfill", + "1388534400", + [], + null, + "local", + "return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);" + ], + [ + "test.z.foo", + "1388534400", + [ + "test.x.core", + "test.x.polyfil", + "test.y.polyfil" + ] + ] +] );', + ) ), + array( array( + // This may seem like an edge case, but a plain MediaWiki core install + // with a few extensions installed is likely far more complex than this + // even, not to mention an install like Wikipedia. + // TODO: Make this even more realistic. + 'msg' => 'Advanced (everything combined)', + 'sources' => array( + 'example' => array( + 'loadScript' => 'http://example.org/w/load.php', + 'apiScript' => 'http://example.org/w/api.php', + ), + ), + 'modules' => array( + 'test.blank' => new ResourceLoaderTestModule(), + 'test.x.core' => new ResourceLoaderTestModule(), + 'test.x.util' => new ResourceLoaderTestModule( array( + 'dependencies' => array( + 'test.x.core', + ), + ) ), + 'test.x.foo' => new ResourceLoaderTestModule( array( + 'dependencies' => array( + 'test.x.core', + ), + ) ), + 'test.x.bar' => new ResourceLoaderTestModule( array( + 'dependencies' => array( + 'test.x.core', + 'test.x.util', + ), + ) ), + 'test.x.quux' => new ResourceLoaderTestModule( array( + 'dependencies' => array( + 'test.x.foo', + 'test.x.bar', + 'test.x.util', + 'test.x.unknown', + ), + ) ), + 'test.group.foo.1' => new ResourceLoaderTestModule( array( + 'group' => 'x-foo', + ) ), + 'test.group.foo.2' => new ResourceLoaderTestModule( array( + 'group' => 'x-foo', + ) ), + 'test.group.bar.1' => new ResourceLoaderTestModule( array( + 'group' => 'x-bar', + ) ), + 'test.group.bar.2' => new ResourceLoaderTestModule( array( + 'group' => 'x-bar', + 'source' => 'example', + ) ), + 'test.target.foo' => new ResourceLoaderTestModule( array( + 'targets' => array( 'x-foo' ), + ) ), + 'test.target.bar' => new ResourceLoaderTestModule( array( + 'source' => 'example', + 'targets' => array( 'x-foo' ), + ) ), + ), + 'out' => ' +mw.loader.addSource( { + "local": "/w/load.php", + "example": "http://example.org/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400" + ], + [ + "test.x.core", + "1388534400" + ], + [ + "test.x.util", + "1388534400", + [ + "test.x.core" + ] + ], + [ + "test.x.foo", + "1388534400", + [ + "test.x.core" + ] + ], + [ + "test.x.bar", + "1388534400", + [ + "test.x.util" + ] + ], + [ + "test.x.quux", + "1388534400", + [ + "test.x.foo", + "test.x.bar", + "test.x.unknown" + ] + ], + [ + "test.group.foo.1", + "1388534400", + [], + "x-foo" + ], + [ + "test.group.foo.2", + "1388534400", + [], + "x-foo" + ], + [ + "test.group.bar.1", + "1388534400", + [], + "x-bar" + ], + [ + "test.group.bar.2", + "1388534400", + [], + "x-bar", + "example" + ] +] );' + ) ), + ); + } + + /** + * @dataProvider provideGetModuleRegistrations + * @covers ResourceLoaderStartupModule::optimizeDependencies + * @covers ResourceLoaderStartUpModule::getModuleRegistrations + * @covers ResourceLoader::makeLoaderSourcesScript + * @covers ResourceLoader::makeLoaderRegisterScript + */ + public function testGetModuleRegistrations( $case ) { + if ( isset( $case['sources'] ) ) { + $this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] ); + } + + $context = self::getResourceLoaderContext(); + $rl = $context->getResourceLoader(); + + $rl->register( $case['modules'] ); + + $module = new ResourceLoaderStartUpModule(); + $this->assertEquals( + ltrim( $case['out'], "\n" ), + $module->getModuleRegistrations( $context ), + $case['msg'] + ); + } + + public static function provideRegistrations() { + return array( + array( array( + 'test.blank' => new ResourceLoaderTestModule(), + 'test.min' => new ResourceLoaderTestModule( array( + 'skipFunction' => + 'return !!(' . + ' window.JSON &&' . + ' JSON.parse &&' . + ' JSON.stringify' . + ');', + 'dependencies' => array( + 'test.blank', + ), + ) ), + ) ) + ); + } + /** + * @dataProvider provideRegistrations + */ + public function testRegistrationsMinified( $modules ) { + $this->setMwGlobals( 'wgResourceLoaderDebug', false ); + + $context = self::getResourceLoaderContext(); + $rl = $context->getResourceLoader(); + $rl->register( $modules ); + $module = new ResourceLoaderStartUpModule(); + $this->assertEquals( +'mw.loader.addSource({"local":"/w/load.php"});' +. 'mw.loader.register([' +. '["test.blank","1388534400"],' +. '["test.min","1388534400",["test.blank"],null,"local",' +. '"return!!(window.JSON\u0026\u0026JSON.parse\u0026\u0026JSON.stringify);"' +. ']]);', + $module->getModuleRegistrations( $context ), + 'Minified output' + ); + } + + /** + * @dataProvider provideRegistrations + */ + public function testRegistrationsUnminified( $modules ) { + $context = self::getResourceLoaderContext(); + $rl = $context->getResourceLoader(); + $rl->register( $modules ); + $module = new ResourceLoaderStartUpModule(); + $this->assertEquals( +'mw.loader.addSource( { + "local": "/w/load.php" +} );mw.loader.register( [ + [ + "test.blank", + "1388534400" + ], + [ + "test.min", + "1388534400", + [ + "test.blank" + ], + null, + "local", + "return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);" + ] +] );', + $module->getModuleRegistrations( $context ), + 'Unminified output' + ); + } + +} diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php new file mode 100644 index 00000000..f19f6886 --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php @@ -0,0 +1,249 @@ +<?php + +class ResourceLoaderTest extends ResourceLoaderTestCase { + + protected static $resourceLoaderRegisterModulesHook; + + protected function setUp() { + parent::setUp(); + + // $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths; $wgResourceLoaderLESSVars; + + $this->setMwGlobals( array( + 'wgResourceLoaderLESSFunctions' => array( + 'test-sum' => function ( $frame, $less ) { + $sum = 0; + foreach ( $frame[2] as $arg ) { + $sum += (int)$arg[1]; + } + return $sum; + }, + ), + 'wgResourceLoaderLESSImportPaths' => array( + dirname( dirname( __DIR__ ) ) . '/data/less/common', + ), + 'wgResourceLoaderLESSVars' => array( + 'foo' => '2px', + 'Foo' => '#eeeeee', + 'bar' => 5, + ), + ) ); + } + + /* Hook Methods */ + + /** + * ResourceLoaderRegisterModules hook + */ + public static function resourceLoaderRegisterModules( &$resourceLoader ) { + self::$resourceLoaderRegisterModulesHook = true; + + return true; + } + + /* Provider Methods */ + public static function provideValidModules() { + return array( + array( 'TEST.validModule1', new ResourceLoaderTestModule() ), + ); + } + + /* Test Methods */ + + /** + * Ensures that the ResourceLoaderRegisterModules hook is called when a new + * ResourceLoader object is constructed. + * @covers ResourceLoader::__construct + */ + public function testCreatingNewResourceLoaderCallsRegistrationHook() { + self::$resourceLoaderRegisterModulesHook = false; + $resourceLoader = new ResourceLoader(); + $this->assertTrue( self::$resourceLoaderRegisterModulesHook ); + + return $resourceLoader; + } + + /** + * @dataProvider provideValidModules + * @depends testCreatingNewResourceLoaderCallsRegistrationHook + * @covers ResourceLoader::register + * @covers ResourceLoader::getModule + */ + public function testRegisteredValidModulesAreAccessible( + $name, ResourceLoaderModule $module, ResourceLoader $resourceLoader + ) { + $resourceLoader->register( $name, $module ); + $this->assertEquals( $module, $resourceLoader->getModule( $name ) ); + } + + /** + * @covers ResourceLoaderFileModule::compileLessFile + */ + public function testLessFileCompilation() { + $context = self::getResourceLoaderContext(); + $basePath = __DIR__ . '/../../data/less/module'; + $module = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'styles.less' ), + ) ); + $module->setName( 'test.less' ); + $styles = $module->getStyles( $context ); + $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] ); + } + + /** + * Strip @noflip annotations from CSS code. + * @param string $css + * @return string + */ + private function stripNoflip( $css ) { + return str_replace( '/*@noflip*/ ', '', $css ); + } + + /** + * What happens when you mix @embed and @noflip? + * This really is an integration test, but oh well. + */ + public function testMixedCssAnnotations( ) { + $basePath = __DIR__ . '/../../data/css'; + $testModule = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'test.css' ), + ) ); + $expectedModule = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'expected.css' ), + ) ); + + $contextLtr = self::getResourceLoaderContext( 'en' ); + $contextRtl = self::getResourceLoaderContext( 'he' ); + + // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and + // the @noflip annotations are always preserved, we need to strip them first. + $this->assertEquals( + $expectedModule->getStyles( $contextLtr ), + $this->stripNoflip( $testModule->getStyles( $contextLtr ) ), + "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode" + ); + $this->assertEquals( + $expectedModule->getStyles( $contextLtr ), + $this->stripNoflip( $testModule->getStyles( $contextRtl ) ), + "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode" + ); + } + + /** + * @dataProvider providePackedModules + * @covers ResourceLoader::makePackedModulesString + */ + public function testMakePackedModulesString( $desc, $modules, $packed ) { + $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc ); + } + + /** + * @dataProvider providePackedModules + * @covers ResourceLoaderContext::expandModuleNames + */ + public function testexpandModuleNames( $desc, $modules, $packed ) { + $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc ); + } + + public static function providePackedModules() { + return array( + array( + 'Example from makePackedModulesString doc comment', + array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ), + 'foo.bar,baz|bar.baz,quux', + ), + array( + 'Example from expandModuleNames doc comment', + array( 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ), + 'jquery.foo,bar|jquery.ui.baz,quux', + ), + array( + 'Regression fixed in r88706 with dotless names', + array( 'foo', 'bar', 'baz' ), + 'foo,bar,baz', + ), + array( + 'Prefixless modules after a prefixed module', + array( 'single.module', 'foobar', 'foobaz' ), + 'single.module|foobar,foobaz', + ), + ); + } + + public static function provideAddSource() { + return array( + array( 'examplewiki', '//example.org/w/load.php', 'examplewiki' ), + array( 'example2wiki', array( 'loadScript' => '//example.com/w/load.php' ), 'example2wiki' ), + array( + array( 'foowiki' => '//foo.org/w/load.php', 'bazwiki' => '//baz.org/w/load.php' ), + null, + array( 'foowiki', 'bazwiki' ) + ), + array( + array( 'foowiki' => '//foo.org/w/load.php' ), + null, + false, + ), + ); + } + + /** + * @dataProvider provideAddSource + * @covers ResourceLoader::addSource + */ + public function testAddSource( $name, $info, $expected ) { + $rl = new ResourceLoader; + if ( $expected === false ) { + $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' ); + $rl->addSource( $name, $info ); + } + $rl->addSource( $name, $info ); + if ( is_array( $expected ) ) { + foreach ( $expected as $source ) { + $this->assertArrayHasKey( $source, $rl->getSources() ); + } + } else { + $this->assertArrayHasKey( $expected, $rl->getSources() ); + } + } + + public static function fakeSources() { + return array( + 'examplewiki' => array( + 'loadScript' => '//example.org/w/load.php', + 'apiScript' => '//example.org/w/api.php', + ), + 'example2wiki' => array( + 'loadScript' => '//example.com/w/load.php', + 'apiScript' => '//example.com/w/api.php', + ), + ); + } + + /** + * @covers ResourceLoader::getLoadScript + */ + public function testGetLoadScript() { + $this->setMwGlobals( 'wgResourceLoaderSources', array() ); + $rl = new ResourceLoader(); + $sources = self::fakeSources(); + $rl->addSource( $sources ); + foreach ( array( 'examplewiki', 'example2wiki' ) as $name ) { + $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] ); + } + + try { + $rl->getLoadScript( 'thiswasneverreigstered' ); + $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' ); + } catch ( MWException $e ) { + $this->assertTrue( true ); + } + } +} + +/* Hooks */ +global $wgHooks; +$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules'; diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php new file mode 100644 index 00000000..9dc18050 --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php @@ -0,0 +1,67 @@ +<?php + +class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { + + /** + * @covers ResourceLoaderWikiModule::isKnownEmpty + * @dataProvider provideIsKnownEmpty + */ + public function testIsKnownEmpty( $titleInfo, $group, $expected ) { + $module = $this->getMockBuilder( 'ResourceLoaderWikiModuleTestModule' ) + ->setMethods( array( 'getTitleInfo', 'getGroup' ) ) + ->getMock(); + $module->expects( $this->any() ) + ->method( 'getTitleInfo' ) + ->will( $this->returnValue( $titleInfo ) ); + $module->expects( $this->any() ) + ->method( 'getGroup' ) + ->will( $this->returnValue( $group ) ); + $context = $this->getMockBuilder( 'ResourceLoaderContext' ) + ->disableOriginalConstructor() + ->getMock(); + $this->assertEquals( $expected, $module->isKnownEmpty( $context ) ); + } + + public static function provideIsKnownEmpty() { + return array( + // No valid pages + array( array(), 'test1', true ), + // 'site' module with a non-empty page + array( + array( + 'MediaWiki:Common.js' => array( + 'timestamp' => 123456789, + 'length' => 1234 + ) + ), 'site', false, + ), + // 'site' module with an empty page + array( + array( + 'MediaWiki:Monobook.js' => array( + 'timestamp' => 987654321, + 'length' => 0, + ), + ), 'site', false, + ), + // 'user' module with a non-empty page + array( + array( + 'User:FooBar/common.js' => array( + 'timestamp' => 246813579, + 'length' => 25, + ), + ), 'user', false, + ), + // 'user' module with an empty page + array( + array( + 'User:FooBar/monobook.js' => array( + 'timestamp' => 1357924680, + 'length' => 0, + ), + ), 'user', true, + ), + ); + } +} |