* @return string JavaScript code for registering all modules with the client loader
*/
public function getModuleRegistrations( ResourceLoaderContext $context ) {
-
$resourceLoader = $context->getResourceLoader();
$target = $context->getRequest()->getVal( 'target', 'desktop' );
// Bypass target filter if this request is Special:JavaScriptTest.
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
$out = '';
+ $states = [];
$registryData = [];
// Get registry data
continue;
}
- $versionHash = $module->getVersionHash( $context );
- if ( strlen( $versionHash ) !== 7 ) {
+ try {
+ $versionHash = $module->getVersionHash( $context );
+ } catch ( Exception $e ) {
+ // See also T152266 and ResourceLoader::getCombinedVersion()
+ MWExceptionHandler::logException( $e );
+ $context->getLogger()->warning(
+ 'Calculating version for "{module}" failed: {exception}',
+ [
+ 'module' => $name,
+ 'exception' => $e,
+ ]
+ );
+ $versionHash = '';
+ $states[$name] = 'error';
+ }
+
+ if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
$context->getLogger()->warning(
"Module '{module}' produced an invalid version hash: '{version}'.",
[
// Register modules
$out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
+ if ( $states ) {
+ $out .= "\n" . ResourceLoader::makeLoaderStateScript( $states );
+ }
+
return $out;
}
* @param array|string $options Language code or options array
* - string 'lang' Language code
* - string 'dir' Language direction (ltr or rtl)
+ * - string 'modules' Pipe-separated list of module names
+ * - string|null 'only' "scripts" (unwrapped script), "styles" (stylesheet), or null
+ * (mw.loader.implement).
* @return ResourceLoaderContext
*/
- protected function getResourceLoaderContext( $options = [] ) {
+ protected function getResourceLoaderContext( $options = [], ResourceLoader $rl = null ) {
if ( is_string( $options ) ) {
// Back-compat for extension tests
$options = [ 'lang' => $options ];
$options += [
'lang' => 'en',
'dir' => 'ltr',
+ 'modules' => 'startup',
+ 'only' => 'scripts',
];
- $resourceLoader = new ResourceLoader();
+ $resourceLoader = $rl ?: new ResourceLoader();
$request = new FauxRequest( [
'lang' => $options['lang'],
- 'modules' => 'startup',
- 'only' => 'scripts',
+ 'modules' => $options['modules'],
+ 'only' => $options['only'],
'skin' => 'vector',
'target' => 'phpunit',
] );
public function __construct( Config $config = null, LoggerInterface $logger = null ) {
$this->setLogger( $logger ?: new NullLogger() );
$this->config = $config ?: MediaWikiServices::getInstance()->getMainConfig();
+ // Source "local" is required by StartupModule
+ $this->addSource( 'local', $this->config->get( 'LoadScript' ) );
$this->setMessageBlobStore( new MessageBlobStore( $this, $this->getLogger() ) );
}
+
+ public function getErrors() {
+ return $this->errors;
+ }
}
'Foo' => '#eeeeee',
'bar' => 5,
],
+ // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
+ // to avoid notices during testMakeModuleResponse for missing
+ // wgResourceLoaderLESSVars keys in extension hooks.
+ 'wgHooks' => [],
] );
}
$this->assertTrue( true );
}
}
+
+ protected function getFailFerryMock() {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getScript' ] )
+ ->getMock();
+ $mock->method( 'getScript' )->will( $this->throwException(
+ new Exception( 'Ferry not found' )
+ ) );
+ return $mock;
+ }
+
+ protected function getSimpleModuleMock( $script = '' ) {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getScript' ] )
+ ->getMock();
+ $mock->method( 'getScript' )->willReturn( $script );
+ return $mock;
+ }
+
+ /**
+ * @covers ResourceLoader::getCombinedVersion
+ */
+ public function testGetCombinedVersion() {
+ $rl = new EmptyResourceLoader();
+ $rl->register( [
+ 'foo' => self::getSimpleModuleMock(),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock(),
+ ] );
+ $context = $this->getResourceLoaderContext( [], $rl );
+
+ $this->assertEquals(
+ ResourceLoader::makeHash( self::BLANK_VERSION ),
+ $rl->getCombinedVersion( $context, [ 'foo' ] ),
+ 'compute foo'
+ );
+
+ // Verify that getCombinedVersion() does not throw when ferry fails.
+ // Instead it gracefully continues to combine the remaining modules.
+ $this->assertEquals(
+ ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
+ $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
+ 'compute foo+ferry+bar (T152266)'
+ );
+ }
+
+ /**
+ * Verify that when building module content in a load.php response,
+ * an exception from one module will not break script output from
+ * other modules.
+ */
+ public function testMakeModuleResponseError() {
+ $modules = [
+ 'foo' => self::getSimpleModuleMock( 'foo();' ),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock( 'bar();' ),
+ ];
+ $rl = new EmptyResourceLoader();
+ $rl->register( $modules );
+ $context = $this->getResourceLoaderContext(
+ [
+ 'modules' => 'foo|ferry|bar',
+ 'only' => 'scripts',
+ ],
+ $rl
+ );
+
+ $response = $rl->makeModuleResponse( $context, $modules );
+ $errors = $rl->getErrors();
+
+ $this->assertCount( 1, $errors );
+ $this->assertRegExp( '/Ferry not found/', $errors[0] );
+ $this->assertEquals(
+ 'foo();bar();mw.loader.state( {
+ "ferry": "error",
+ "foo": "ready",
+ "bar": "ready"
+} );',
+ $response
+ );
+ }
+
+ /**
+ * Verify that when building the startup module response,
+ * an exception from one module class will not break the entire
+ * startup module response. See T152266.
+ */
+ public function testMakeModuleResponseStartupError() {
+ $rl = new EmptyResourceLoader();
+ $rl->register( [
+ 'foo' => self::getSimpleModuleMock( 'foo();' ),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock( 'bar();' ),
+ 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
+ ] );
+ $context = $this->getResourceLoaderContext(
+ [
+ 'modules' => 'startup',
+ 'only' => 'scripts',
+ ],
+ $rl
+ );
+
+ $this->assertEquals(
+ [ 'foo', 'ferry', 'bar', 'startup' ],
+ $rl->getModuleNames(),
+ 'getModuleNames'
+ );
+
+ $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
+ $response = $rl->makeModuleResponse( $context, $modules );
+ $errors = $rl->getErrors();
+
+ $this->assertRegExp( '/Ferry not found/', $errors[0] );
+ $this->assertCount( 1, $errors );
+ $this->assertRegExp(
+ '/isCompatible.*function startUp/s',
+ $response,
+ 'startup response undisrupted (T152266)'
+ );
+ $this->assertRegExp(
+ '/register\([^)]+"ferry",\s*""/s',
+ $response,
+ 'startup response registers broken module'
+ );
+ $this->assertRegExp(
+ '/state\([^)]+"ferry":\s*"error"/s',
+ $response,
+ 'startup response sets state to error'
+ );
+ }
}