Developers can use this to test their local version of Codex with MediaWiki by pointing $wgCodexDevelopmentDir to their local clone of the Codex repo, e.g. $wgCodexDevelopmentDir = '/home/yourname/git/codex'; Setting $wgCodexDevelopmentDir affects where the following things come from: - Codex JS/CSS files for the full library - Codex JS/CSS files for code-split chunks, and the manifest.json file that points to them - Icons retrieved by CodexModule::getIcons() - CSS-only icons imported in Less - Design tokens imported in Less Other changes in this patch: - Add CodexModule::makeFilePath() to centralize the repeated path concatenation. This makes it easier to switch out the regular path for the dev mode path. - Replace all uses of $IP (which is deprecated) and MW_INSTALL_PATH in CodexModule with the BaseDirectory config setting. - Make CodexModule::getIcons() reset its static cache if the path to the icons file changes. Without this, it's impossible to make the unit tests pass. - Move the i18n messages code from the CodexModule constructor to getMessages(). It can't be in the constructor because makeFilePath() doesn't work there (it fails because the Config object hasn't been set up yet). - Add a 'mediawiki.skin.codex' import path so that we can stop hard-coding the path to the Codex mixins file. Without this, we can't make the Codex mixins come from the right place in development mode. - Consider $wgCodexDevelopmentDir in setting the cache key for compiled Less code, since changing this setting can change the output of Less compilation (by changing design tokens, icons or mixins). - Add unit tests for (the non-dev mode behavior of) CodexModule::getIcons() and the i18n message key handling. Bug: T314507 Change-Id: I11c6a81a1ba34fe10f4b1c98bf76f0db40c1ce98
1339 lines
38 KiB
PHP
1339 lines
38 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\ResourceLoader;
|
|
|
|
use Exception;
|
|
use InvalidArgumentException;
|
|
use MediaWiki\Config\Config;
|
|
use MediaWiki\Config\HashConfig;
|
|
use MediaWiki\Html\HtmlJsCode;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Registration\ExtensionRegistry;
|
|
use MediaWiki\Request\FauxRequest;
|
|
use MediaWiki\ResourceLoader\Context;
|
|
use MediaWiki\ResourceLoader\FileModule;
|
|
use MediaWiki\ResourceLoader\ResourceLoader;
|
|
use MediaWiki\ResourceLoader\SkinModule;
|
|
use MediaWiki\ResourceLoader\StartUpModule;
|
|
use MediaWiki\User\Options\StaticUserOptionsLookup;
|
|
use NullStatsdDataFactory;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use RuntimeException;
|
|
use UnexpectedValueException;
|
|
use Wikimedia\Minify\IdentityMinifierState;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @group ResourceLoader
|
|
* @covers \MediaWiki\ResourceLoader\ResourceLoader
|
|
*/
|
|
class ResourceLoaderTest extends ResourceLoaderTestCase {
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->overrideConfigValue( MainConfigNames::ShowExceptionDetails, true );
|
|
}
|
|
|
|
/**
|
|
* Ensure the ResourceLoaderRegisterModules hook is called.
|
|
* @coversNothing
|
|
*/
|
|
public function testServiceWiring() {
|
|
$ranHook = 0;
|
|
$this->setTemporaryHook(
|
|
'ResourceLoaderRegisterModules',
|
|
static function ( &$resourceLoader ) use ( &$ranHook ) {
|
|
$ranHook++;
|
|
}
|
|
);
|
|
|
|
$this->getServiceContainer()->getResourceLoader();
|
|
|
|
$this->assertSame( 1, $ranHook, 'Hook was called' );
|
|
}
|
|
|
|
public static function provideInvalidModuleName() {
|
|
return [
|
|
'name with 300 chars' => [ str_repeat( 'x', 300 ) ],
|
|
'name with bang' => [ 'this!that' ],
|
|
'name with comma' => [ 'this,that' ],
|
|
'name with pipe' => [ 'this|that' ],
|
|
];
|
|
}
|
|
|
|
public static function provideValidModuleName() {
|
|
return [
|
|
'empty string' => [ '' ],
|
|
'simple name' => [ 'this.and-that2' ],
|
|
'name with 100 chars' => [ str_repeat( 'x', 100 ) ],
|
|
'name with hash' => [ 'this#that' ],
|
|
'name with slash' => [ 'this/that' ],
|
|
'name with at' => [ 'this@that' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideInvalidModuleName
|
|
*/
|
|
public function testIsValidModuleName_invalid( $name ) {
|
|
$this->assertFalse( ResourceLoader::isValidModuleName( $name ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideValidModuleName
|
|
*/
|
|
public function testIsValidModuleName_valid( $name ) {
|
|
$this->assertTrue( ResourceLoader::isValidModuleName( $name ) );
|
|
}
|
|
|
|
public function testRegisterValidArray() {
|
|
$resourceLoader = new EmptyResourceLoader();
|
|
// Covers case of register() setting $rl->moduleInfos,
|
|
// but $rl->modules lazy-populated by getModule()
|
|
$resourceLoader->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
|
|
$this->assertInstanceOf(
|
|
ResourceLoaderTestModule::class,
|
|
$resourceLoader->getModule( 'test' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @group medium
|
|
*/
|
|
public function testRegisterEmptyString() {
|
|
$resourceLoader = new EmptyResourceLoader();
|
|
$resourceLoader->register( '', [ 'class' => ResourceLoaderTestModule::class ] );
|
|
$this->assertInstanceOf(
|
|
ResourceLoaderTestModule::class,
|
|
$resourceLoader->getModule( '' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @group medium
|
|
*/
|
|
public function testRegisterInvalidName() {
|
|
$resourceLoader = new EmptyResourceLoader();
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$this->expectExceptionMessage( "name 'test!invalid' is invalid" );
|
|
$resourceLoader->register( 'test!invalid', [] );
|
|
}
|
|
|
|
public function testRegisterInvalidType() {
|
|
$resourceLoader = new EmptyResourceLoader();
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$this->expectExceptionMessage( 'Invalid module info' );
|
|
$resourceLoader->register( [ 'test' => (object)[] ] );
|
|
}
|
|
|
|
public function testRegisterDuplicate() {
|
|
$logger = $this->createMock( LoggerInterface::class );
|
|
$logger->expects( $this->once() )
|
|
->method( 'warning' );
|
|
$resourceLoader = new EmptyResourceLoader( null, $logger );
|
|
|
|
$resourceLoader->register( 'test', [ 'class' => SkinModule::class ] );
|
|
$resourceLoader->register( 'test', [ 'class' => StartUpModule::class ] );
|
|
$this->assertInstanceOf(
|
|
StartUpModule::class,
|
|
$resourceLoader->getModule( 'test' ),
|
|
'last one wins'
|
|
);
|
|
}
|
|
|
|
public function testGetModuleNames() {
|
|
// Use an empty one so that core and extension modules don't get in.
|
|
$resourceLoader = new EmptyResourceLoader();
|
|
$resourceLoader->register( 'test.foo', [] );
|
|
$resourceLoader->register( 'test.bar', [] );
|
|
$this->assertEquals(
|
|
[ 'startup', 'test.foo', 'test.bar' ],
|
|
$resourceLoader->getModuleNames()
|
|
);
|
|
}
|
|
|
|
public function testIsModuleRegistered() {
|
|
$rl = new EmptyResourceLoader();
|
|
$rl->register( 'test', [] );
|
|
$this->assertTrue( $rl->isModuleRegistered( 'test' ) );
|
|
$this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
|
|
}
|
|
|
|
public function testGetModuleUnknown() {
|
|
$rl = new EmptyResourceLoader();
|
|
$this->assertSame( null, $rl->getModule( 'test' ) );
|
|
}
|
|
|
|
public function testGetModuleClass() {
|
|
$rl = new EmptyResourceLoader();
|
|
$rl->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
|
|
$this->assertInstanceOf(
|
|
ResourceLoaderTestModule::class,
|
|
$rl->getModule( 'test' )
|
|
);
|
|
}
|
|
|
|
public function testGetModuleFactory() {
|
|
$factory = function ( array $info ) {
|
|
$this->assertArrayHasKey( 'kitten', $info );
|
|
unset( $info['kitten'] );
|
|
return new ResourceLoaderTestModule( $info );
|
|
};
|
|
|
|
$rl = new EmptyResourceLoader();
|
|
$rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
|
|
$this->assertInstanceOf(
|
|
ResourceLoaderTestModule::class,
|
|
$rl->getModule( 'test' )
|
|
);
|
|
}
|
|
|
|
public function testGetModuleClassDefault() {
|
|
$rl = new EmptyResourceLoader();
|
|
$rl->register( 'test', [] );
|
|
$this->assertInstanceOf(
|
|
FileModule::class,
|
|
$rl->getModule( 'test' ),
|
|
'Array-style module registrations default to FileModule'
|
|
);
|
|
}
|
|
|
|
public function testGetVersionHash_length() {
|
|
$hash = ResourceLoader::makeHash(
|
|
'Anything you do could have serious repercussions on future events.'
|
|
);
|
|
$this->assertSame( 'xhh1x', $hash, 'Hash' );
|
|
$this->assertSame( ResourceLoader::HASH_LENGTH, strlen( $hash ), 'Hash length' );
|
|
}
|
|
|
|
public function testLessImportDirs() {
|
|
$rl = new EmptyResourceLoader();
|
|
$lc = $rl->getLessCompiler( [ 'foo' => '2px', 'Foo' => '#eeeeee' ] );
|
|
$basePath = dirname( dirname( __DIR__ ) ) . '/data/less';
|
|
$lc->SetImportDirs( [
|
|
"$basePath/common" => '',
|
|
] );
|
|
$css = $lc->parseFile( "$basePath/module/use-import-dir.less" )->getCss();
|
|
$this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
|
|
}
|
|
|
|
public static function provideLessImportRemappingCases() {
|
|
$basePath = dirname( dirname( __DIR__ ) ) . '/data/less';
|
|
return [
|
|
[
|
|
'input' => "$basePath/import-codex-icons.less",
|
|
'expected' => "$basePath/import-codex-icons.css"
|
|
],
|
|
[
|
|
'input' => "$basePath/import-codex-icons.less",
|
|
'expected' => "$basePath/import-codex-icons-devmode.css",
|
|
'exception' => null,
|
|
'devmode' => true
|
|
],
|
|
[
|
|
'input' => "$basePath/import-codex-tokens.less",
|
|
'expected' => "$basePath/import-codex-tokens.css"
|
|
],
|
|
[
|
|
'input' => "$basePath/import-codex-tokens.less",
|
|
'expected' => "$basePath/import-codex-tokens-devmode.css",
|
|
'exception' => null,
|
|
'devmode' => true
|
|
],
|
|
[
|
|
'input' => "$basePath/import-codex-tokens-npm.less",
|
|
'expected' => null,
|
|
'exception' => [
|
|
'class' => Exception::class,
|
|
'message' => 'Importing from @wikimedia/codex-design-tokens is not supported. ' .
|
|
"To use the Codex tokens, use `@import 'mediawiki.skin.variables.less';` instead."
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideLessImportRemappingCases
|
|
*/
|
|
public function testLessImportRemapping( $input, $expected, $exception = null, $devmode = false ) {
|
|
$configOverrides = [];
|
|
if ( $devmode ) {
|
|
$devDir = MW_INSTALL_PATH . '/tests/phpunit/data/resourceloader/codex-devmode';
|
|
$configOverrides += [
|
|
MainConfigNames::CodexDevelopmentDir => $devDir
|
|
];
|
|
}
|
|
|
|
$this->overrideConfigValues( $configOverrides );
|
|
// Unfortunately the EmptyResourceLoader constructor doesn't pick up the overridden config
|
|
// values, we have to do that separately
|
|
$baseConfig = static::getSettings();
|
|
$rl = new EmptyResourceLoader( new HashConfig( $configOverrides + $baseConfig ) );
|
|
$lc = $rl->getLessCompiler();
|
|
|
|
if ( $exception !== null ) {
|
|
if ( isset( $exception['class'] ) ) {
|
|
$this->expectException( $exception['class'] );
|
|
}
|
|
if ( isset( $exception['message'] ) ) {
|
|
$this->expectExceptionMessage( $exception['message'] );
|
|
}
|
|
}
|
|
|
|
$css = $lc->parseFile( $input )->getCss();
|
|
if ( $expected !== null ) {
|
|
$this->assertStringEqualsFile( $expected, $css );
|
|
}
|
|
}
|
|
|
|
public static function provideMediaWikiVariablesCases() {
|
|
$basePath = __DIR__ . '/../../data/less';
|
|
return [
|
|
[
|
|
'config' => [],
|
|
'importPaths' => [],
|
|
'skin' => 'fallback',
|
|
'expected' => "$basePath/use-variables-default.css",
|
|
],
|
|
[
|
|
'config' => [
|
|
MainConfigNames::ValidSkinNames => [
|
|
// Required to make Context::getSkin work
|
|
'example' => 'Example',
|
|
],
|
|
],
|
|
'importPaths' => [
|
|
'example' => "$basePath/testvariables/",
|
|
],
|
|
'skin' => 'example',
|
|
'expected' => "$basePath/use-variables-test.css",
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMediaWikiVariablesCases
|
|
*/
|
|
public function testMediaWikiVariablesDefault( array $config, array $importPaths, $skin, $expectedFile ) {
|
|
$this->overrideConfigValues( $config );
|
|
$reset = ExtensionRegistry::getInstance()->setAttributeForTest( 'SkinLessImportPaths', $importPaths );
|
|
|
|
$context = $this->getResourceLoaderContext( [ 'skin' => $skin ] );
|
|
$module = new FileModule( [
|
|
'localBasePath' => __DIR__ . '/../../data/less',
|
|
'styles' => [ 'use-variables.less' ],
|
|
] );
|
|
$module->setConfig( $context->getResourceLoader()->getConfig() );
|
|
$module->setName( 'test.less' );
|
|
$styles = $module->getStyles( $context );
|
|
$this->assertStringEqualsFile( $expectedFile, $styles['all'] );
|
|
}
|
|
|
|
public static function providePackedModules() {
|
|
return [
|
|
[
|
|
'Example from makePackedModulesString doc comment',
|
|
[ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
|
|
'foo.bar,baz|bar.baz,quux',
|
|
],
|
|
[
|
|
'Example from expandModuleNames doc comment',
|
|
[ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
|
|
'jquery.foo,bar|jquery.ui.baz,quux',
|
|
],
|
|
[
|
|
'Regression fixed in r87497 (7fee86c38e) with dotless names',
|
|
[ 'foo', 'bar', 'baz' ],
|
|
'foo,bar,baz',
|
|
],
|
|
[
|
|
'Prefixless modules after a prefixed module',
|
|
[ 'single.module', 'foobar', 'foobaz' ],
|
|
'single.module|foobar,foobaz',
|
|
],
|
|
[
|
|
'Ordering',
|
|
[ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
|
|
'foo|foo.baz,bar|baz.quux',
|
|
[ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providePackedModules
|
|
*/
|
|
public function testMakePackedModulesString( $desc, $modules, $packed ) {
|
|
$this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providePackedModules
|
|
*/
|
|
public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
|
|
$this->assertEquals(
|
|
$unpacked ?: $modules,
|
|
ResourceLoader::expandModuleNames( $packed ),
|
|
$desc
|
|
);
|
|
}
|
|
|
|
public static function provideAddSource() {
|
|
return [
|
|
[ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
|
|
[ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
|
|
[
|
|
[
|
|
'foowiki' => 'https://example.org/w/load.php',
|
|
'bazwiki' => 'https://example.com/w/load.php',
|
|
],
|
|
null,
|
|
[ 'foowiki', 'bazwiki' ]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideAddSource
|
|
*/
|
|
public function testAddSource( $name, $info, $expected ) {
|
|
$rl = new EmptyResourceLoader;
|
|
$rl->addSource( $name, $info );
|
|
if ( is_array( $expected ) ) {
|
|
foreach ( $expected as $source ) {
|
|
$this->assertArrayHasKey( $source, $rl->getSources() );
|
|
}
|
|
} else {
|
|
$this->assertArrayHasKey( $expected, $rl->getSources() );
|
|
}
|
|
}
|
|
|
|
public function testAddSourceDupe() {
|
|
$rl = new EmptyResourceLoader;
|
|
$this->expectException( RuntimeException::class );
|
|
$this->expectExceptionMessage( 'Cannot register source' );
|
|
$rl->addSource( 'foo', 'https://example.org/w/load.php' );
|
|
$rl->addSource( 'foo', 'https://example.com/w/load.php' );
|
|
}
|
|
|
|
public function testAddSourceInvalid() {
|
|
$rl = new EmptyResourceLoader;
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$this->expectExceptionMessage( 'must have a "loadScript" key' );
|
|
$rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
|
|
}
|
|
|
|
public static function provideLoaderImplement() {
|
|
return [
|
|
'Implement scripts, styles and messages' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => 'mw.example();',
|
|
'styles' => [ 'css' => [ '.mw-example {}' ] ],
|
|
'messages' => [ 'example' => '' ],
|
|
'templates' => [],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
|
},{"css":[".mw-example {}"]},{"example":""}];});',
|
|
] ],
|
|
'Implement scripts' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => 'mw.example();',
|
|
'styles' => [],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
|
}];});',
|
|
] ],
|
|
'Implement scripts with newline at end' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => "mw.example();\n",
|
|
'styles' => [],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
|
}];});',
|
|
] ],
|
|
'Implement scripts with comment at end' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => "mw.example();//Foo",
|
|
'styles' => [],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();//Foo
|
|
}];});',
|
|
] ],
|
|
'Implement styles' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => [],
|
|
'styles' => [ 'css' => [ '.mw-example {}' ] ],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",[],{"css":[".mw-example {}"]}];});',
|
|
] ],
|
|
'Implement scripts and messages' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => 'mw.example();',
|
|
'messages' => [ 'example' => '' ],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
|
},{},{"example":""}];});',
|
|
] ],
|
|
'Implement scripts and templates' => [ [
|
|
'name' => 'test.example',
|
|
'scripts' => 'mw.example();',
|
|
'templates' => [ 'example.html' => '' ],
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
|
},{},{},{"example.html":""}];});',
|
|
] ],
|
|
'Implement unwrapped user script' => [ [
|
|
'name' => 'user',
|
|
'scripts' => 'mw.example( 1 );',
|
|
'wrap' => false,
|
|
|
|
'expected' => 'mw.loader.impl(function(){return["user@1","mw.example( 1 );"];});',
|
|
] ],
|
|
'Implement multi-file script' => [ [
|
|
'name' => 'test.multifile',
|
|
'scripts' => [
|
|
'files' => [
|
|
'one.js' => [
|
|
'type' => 'script',
|
|
'content' => 'mw.example( 1 );',
|
|
],
|
|
'two.json' => [
|
|
'type' => 'data',
|
|
'content' => [ 'n' => 2 ],
|
|
],
|
|
'three.js' => [
|
|
'type' => 'script',
|
|
'content' => 'mw.example( 3 ); // Comment'
|
|
],
|
|
'four.js' => [
|
|
'type' => 'script',
|
|
'content' => "mw.example( 4 );\n"
|
|
],
|
|
'five.js' => [
|
|
'type' => 'script',
|
|
'content' => 'mw.example( 5 );'
|
|
],
|
|
],
|
|
'main' => 'five.js',
|
|
],
|
|
|
|
'expected' => <<<END
|
|
mw.loader.impl(function(){return["test.multifile@1",{"main":"five.js","files":{"one.js":function(require,module,exports){mw.example( 1 );
|
|
},"two.json":{
|
|
"n": 2
|
|
},"three.js":function(require,module,exports){mw.example( 3 ); // Comment
|
|
},"four.js":function(require,module,exports){mw.example( 4 );
|
|
},"five.js":function(require,module,exports){mw.example( 5 );
|
|
}}}];});
|
|
END
|
|
] ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideLoaderImplement
|
|
*/
|
|
public function testAddImplementScript( $case ) {
|
|
$case += [
|
|
'version' => '1',
|
|
'wrap' => true,
|
|
'styles' => [],
|
|
'templates' => [],
|
|
'messages' => new HtmlJsCode( '{}' ),
|
|
'packageFiles' => [],
|
|
];
|
|
$rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
|
|
$minifier = new IdentityMinifierState;
|
|
$rl->addImplementScript(
|
|
$minifier,
|
|
$case['name'],
|
|
$case['version'],
|
|
( $case['wrap'] && is_string( $case['scripts'] ) )
|
|
? [ 'plainScripts' => [ [ 'content' => $case['scripts'] ] ] ]
|
|
: $case['scripts'],
|
|
$case['styles'],
|
|
$case['messages'],
|
|
$case['templates'],
|
|
$case['packageFiles']
|
|
);
|
|
$this->assertEquals( $case['expected'], $minifier->getMinifiedOutput() );
|
|
}
|
|
|
|
public function testAddImplementScriptInvalid() {
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$this->expectExceptionMessage( 'Script must be a' );
|
|
$minifier = new IdentityMinifierState;
|
|
$rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
|
|
$rl->addImplementScript(
|
|
$minifier,
|
|
'test', // name
|
|
'1', // version
|
|
123, // scripts
|
|
null, // styles
|
|
null, // messages
|
|
null, // templates
|
|
null // package files
|
|
);
|
|
}
|
|
|
|
public function testMakeLoaderRegisterScript() {
|
|
$context = new Context( new EmptyResourceLoader(), new FauxRequest( [
|
|
'debug' => 'true',
|
|
] ) );
|
|
$this->assertEquals(
|
|
'mw.loader.register([
|
|
[
|
|
"test.name",
|
|
"1234567"
|
|
]
|
|
]);',
|
|
ResourceLoader::makeLoaderRegisterScript( $context, [
|
|
[ 'test.name', '1234567' ],
|
|
] ),
|
|
'Nested array parameter'
|
|
);
|
|
|
|
$this->assertEquals(
|
|
'mw.loader.register([
|
|
[
|
|
"test.foo",
|
|
"100"
|
|
],
|
|
[
|
|
"test.bar",
|
|
"200",
|
|
[
|
|
"test.unknown"
|
|
]
|
|
],
|
|
[
|
|
"test.baz",
|
|
"300",
|
|
[
|
|
3,
|
|
0
|
|
]
|
|
],
|
|
[
|
|
"test.quux",
|
|
"400",
|
|
[],
|
|
null,
|
|
null,
|
|
"return true;"
|
|
]
|
|
]);',
|
|
ResourceLoader::makeLoaderRegisterScript( $context, [
|
|
[ 'test.foo', '100', [], null, null ],
|
|
[ 'test.bar', '200', [ 'test.unknown' ], null ],
|
|
[ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
|
|
[ 'test.quux', '400', [], null, null, 'return true;' ],
|
|
] ),
|
|
'Compact dependency indexes'
|
|
);
|
|
}
|
|
|
|
public function testMakeLoaderSourcesScript() {
|
|
$context = new Context( new EmptyResourceLoader(), new FauxRequest( [
|
|
'debug' => 'true',
|
|
] ) );
|
|
$this->assertEquals(
|
|
'mw.loader.addSource({
|
|
"local": "/w/load.php"
|
|
});',
|
|
ResourceLoader::makeLoaderSourcesScript( $context, [ 'local' => '/w/load.php' ] )
|
|
);
|
|
$this->assertEquals(
|
|
'mw.loader.addSource({
|
|
"local": "/w/load.php",
|
|
"example": "https://example.org/w/load.php"
|
|
});',
|
|
ResourceLoader::makeLoaderSourcesScript( $context, [
|
|
'local' => '/w/load.php',
|
|
'example' => 'https://example.org/w/load.php'
|
|
] )
|
|
);
|
|
$this->assertEquals(
|
|
'mw.loader.addSource([]);',
|
|
ResourceLoader::makeLoaderSourcesScript( $context, [] )
|
|
);
|
|
}
|
|
|
|
private static function fakeSources() {
|
|
return [
|
|
'examplewiki' => [
|
|
'loadScript' => '//example.org/w/load.php',
|
|
'apiScript' => '//example.org/w/api.php',
|
|
],
|
|
'example2wiki' => [
|
|
'loadScript' => '//example.com/w/load.php',
|
|
'apiScript' => '//example.com/w/api.php',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testGetLoadScript() {
|
|
$rl = new EmptyResourceLoader();
|
|
$sources = self::fakeSources();
|
|
$rl->addSource( $sources );
|
|
foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
|
|
$this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
|
|
}
|
|
|
|
$this->expectException( UnexpectedValueException::class );
|
|
$rl->getLoadScript( 'thiswasneverregistered' );
|
|
}
|
|
|
|
protected function getFailFerryMock( $getter = 'getScript' ) {
|
|
$mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ $getter, 'getName' ] )
|
|
->getMock();
|
|
$mock->method( $getter )->willThrowException(
|
|
new Exception( 'Ferry not found' )
|
|
);
|
|
$mock->method( 'getName' )->willReturn( __METHOD__ );
|
|
return $mock;
|
|
}
|
|
|
|
protected function getSimpleModuleMock( $script = '' ) {
|
|
$mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getScript', 'getName' ] )
|
|
->getMock();
|
|
$mock->method( 'getScript' )->willReturn( $script );
|
|
$mock->method( 'getName' )->willReturn( __METHOD__ );
|
|
return $mock;
|
|
}
|
|
|
|
protected function getSimpleStyleModuleMock( $styles = '' ) {
|
|
$mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getStyles', 'getName' ] )
|
|
->getMock();
|
|
$mock->method( 'getStyles' )->willReturn( [ '' => $styles ] );
|
|
$mock->method( 'getName' )->willReturn( __METHOD__ );
|
|
return $mock;
|
|
}
|
|
|
|
public function testGetCombinedVersion() {
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
// Disable log from outputErrorAndLog
|
|
->onlyMethods( [ 'outputErrorAndLog' ] )->getMock();
|
|
$rl->register( [
|
|
'foo' => [ 'class' => ResourceLoaderTestModule::class ],
|
|
'ferry' => [
|
|
'factory' => function () {
|
|
return $this->getFailFerryMock();
|
|
}
|
|
],
|
|
'bar' => [ 'class' => ResourceLoaderTestModule::class ],
|
|
] );
|
|
$context = $this->getResourceLoaderContext( [ 'debug' => 'false' ], $rl );
|
|
|
|
$this->assertSame(
|
|
'',
|
|
$rl->getCombinedVersion( $context, [] ),
|
|
'empty list'
|
|
);
|
|
|
|
$this->assertEquals(
|
|
self::BLANK_COMBI,
|
|
$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)'
|
|
);
|
|
}
|
|
|
|
public static function provideMakeModuleResponseConcat() {
|
|
$testcases = [
|
|
[
|
|
'modules' => [
|
|
'foo' => 'foo()',
|
|
],
|
|
'expected' => "foo()\n" . 'mw.loader.state({
|
|
"foo": "ready"
|
|
});',
|
|
'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
|
|
'message' => 'Script without semi-colon',
|
|
],
|
|
[
|
|
'modules' => [
|
|
'foo' => 'foo()',
|
|
'bar' => 'bar()',
|
|
],
|
|
'expected' => "foo()\nbar()\n" . 'mw.loader.state({
|
|
"foo": "ready",
|
|
"bar": "ready"
|
|
});',
|
|
'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
|
|
'message' => 'Two scripts without semi-colon',
|
|
],
|
|
[
|
|
'modules' => [
|
|
'foo' => "foo()\n// bar();"
|
|
],
|
|
'expected' => "foo()\n// bar();\n" . 'mw.loader.state({
|
|
"foo": "ready"
|
|
});',
|
|
'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
|
|
'message' => 'Script with semi-colon in comment (T162719)',
|
|
],
|
|
];
|
|
$ret = [];
|
|
foreach ( $testcases as $i => $case ) {
|
|
$ret["#$i"] = [
|
|
$case['modules'],
|
|
$case['expected'],
|
|
true, // debug
|
|
$case['message'],
|
|
];
|
|
$ret["#$i (minified)"] = [
|
|
$case['modules'],
|
|
$case['minified'],
|
|
false, // debug
|
|
$case['message'],
|
|
];
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Verify how multiple scripts and mw.loader.state() calls are concatenated.
|
|
*
|
|
* @dataProvider provideMakeModuleResponseConcat
|
|
*/
|
|
public function testMakeModuleResponseConcat( $scripts, $expected, $debug, $message = null ) {
|
|
$rl = new EmptyResourceLoader();
|
|
$modules = array_map( function ( $script ) {
|
|
return $this->getSimpleModuleMock( $script );
|
|
}, $scripts );
|
|
|
|
$context = $this->getResourceLoaderContext(
|
|
[
|
|
'modules' => implode( '|', array_keys( $modules ) ),
|
|
'only' => 'scripts',
|
|
'debug' => $debug ? 'true' : 'false',
|
|
],
|
|
$rl
|
|
);
|
|
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$this->assertSame( [], $rl->getErrors(), 'Errors' );
|
|
$this->assertEquals( $expected, $response, $message ?: 'Response' );
|
|
}
|
|
|
|
public function testMakeModuleResponseEmpty() {
|
|
$rl = new EmptyResourceLoader();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => '', 'only' => 'scripts' ],
|
|
$rl
|
|
);
|
|
|
|
$response = $rl->makeModuleResponse( $context, [] );
|
|
$this->assertSame( [], $rl->getErrors(), 'Errors' );
|
|
$this->assertMatchesRegularExpression( '/^\/\*.+no modules were requested.+\*\/$/ms', $response );
|
|
}
|
|
|
|
/**
|
|
* 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' => $this->getSimpleModuleMock( 'foo();' ),
|
|
'ferry' => $this->getFailFerryMock(),
|
|
'bar' => $this->getSimpleModuleMock( 'bar();' ),
|
|
];
|
|
$rl = new EmptyResourceLoader();
|
|
$context = $this->getResourceLoaderContext(
|
|
[
|
|
'modules' => 'foo|ferry|bar',
|
|
'only' => 'scripts',
|
|
],
|
|
$rl
|
|
);
|
|
|
|
// Disable log from makeModuleResponse via outputErrorAndLog
|
|
$this->setLogger( 'exception', new NullLogger() );
|
|
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$errors = $rl->getErrors();
|
|
|
|
$this->assertCount( 1, $errors );
|
|
$this->assertMatchesRegularExpression( '/Ferry not found/', $errors[0] );
|
|
$this->assertEquals(
|
|
"foo();\nbar();\n" . 'mw.loader.state({
|
|
"ferry": "error",
|
|
"foo": "ready",
|
|
"bar": "ready"
|
|
});',
|
|
$response
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify that exceptions in PHP for one module will not break others
|
|
* (stylesheet response).
|
|
*/
|
|
public function testMakeModuleResponseErrorCSS() {
|
|
$modules = [
|
|
'foo' => self::getSimpleStyleModuleMock( '.foo{}' ),
|
|
'ferry' => $this->getFailFerryMock( 'getStyles' ),
|
|
'bar' => self::getSimpleStyleModuleMock( '.bar{}' ),
|
|
];
|
|
$rl = new EmptyResourceLoader();
|
|
$context = $this->getResourceLoaderContext(
|
|
[
|
|
'modules' => 'foo|ferry|bar',
|
|
'only' => 'styles',
|
|
'debug' => 'false',
|
|
],
|
|
$rl
|
|
);
|
|
|
|
// Disable log from makeModuleResponse via outputErrorAndLog
|
|
$this->setLogger( 'exception', new NullLogger() );
|
|
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$errors = $rl->getErrors();
|
|
|
|
$this->assertCount( 2, $errors );
|
|
$this->assertMatchesRegularExpression( '/Ferry not found/', $errors[0] );
|
|
$this->assertMatchesRegularExpression( '/Problem.+"ferry":\s*"error"/ms', $errors[1] );
|
|
$this->assertEquals(
|
|
'.foo{}.bar{}',
|
|
$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() {
|
|
// This is an integration test that uses a lot of MediaWiki state,
|
|
// provide the full Config object here.
|
|
$rl = new EmptyResourceLoader( $this->getServiceContainer()->getMainConfig() );
|
|
$rl->register( [
|
|
'foo' => [ 'factory' => function () {
|
|
return $this->getSimpleModuleMock( 'foo();' );
|
|
} ],
|
|
'ferry' => [ 'factory' => function () {
|
|
return $this->getFailFerryMock();
|
|
} ],
|
|
'bar' => [ 'factory' => function () {
|
|
return $this->getSimpleModuleMock( 'bar();' );
|
|
} ],
|
|
] );
|
|
$context = $this->getResourceLoaderContext(
|
|
[
|
|
'modules' => 'startup',
|
|
'only' => 'scripts',
|
|
// No module build for version hash in debug mode
|
|
'debug' => 'false',
|
|
],
|
|
$rl
|
|
);
|
|
|
|
$this->assertEquals(
|
|
[ 'startup', 'foo', 'ferry', 'bar' ],
|
|
$rl->getModuleNames(),
|
|
'getModuleNames'
|
|
);
|
|
|
|
// Disable log from makeModuleResponse via outputErrorAndLog
|
|
$this->setLogger( 'exception', new NullLogger() );
|
|
|
|
$modules = [ 'startup' => $rl->getModule( 'startup' ) ];
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$errors = $rl->getErrors();
|
|
|
|
$this->assertMatchesRegularExpression( '/Ferry not found/', $errors[0] ?? '' );
|
|
$this->assertCount( 1, $errors );
|
|
$this->assertMatchesRegularExpression(
|
|
'/isCompatible.*window\.RLQ/s',
|
|
$response,
|
|
'startup response undisrupted (T152266)'
|
|
);
|
|
$this->assertMatchesRegularExpression(
|
|
'/register\([^)]+"ferry",\s*""/',
|
|
$response,
|
|
'startup response registers broken module'
|
|
);
|
|
$this->assertMatchesRegularExpression(
|
|
'/state\([^)]+"ferry":\s*"error"/',
|
|
$response,
|
|
'startup response sets state to error'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Integration test for modules sending extra HTTP response headers.
|
|
*/
|
|
public function testMakeModuleResponseExtraHeaders() {
|
|
$module = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getPreloadLinks', 'getName' ] )->getMock();
|
|
$module->method( 'getPreloadLinks' )->willReturn( [
|
|
'https://example.org/script.js' => [ 'as' => 'script' ],
|
|
] );
|
|
$module->method( 'getName' )->willReturn( __METHOD__ );
|
|
|
|
$rl = new EmptyResourceLoader();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'foo', 'only' => 'scripts' ],
|
|
$rl
|
|
);
|
|
|
|
$modules = [ 'foo' => $module ];
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
|
|
|
|
$this->assertEquals(
|
|
[
|
|
'Link: <https://example.org/script.js>;rel=preload;as=script'
|
|
],
|
|
$extraHeaders,
|
|
'Extra headers'
|
|
);
|
|
}
|
|
|
|
public function testMakeModuleResponseExtraHeadersMulti() {
|
|
$foo = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getPreloadLinks', 'getName' ] )->getMock();
|
|
$foo->method( 'getPreloadLinks' )->willReturn( [
|
|
'https://example.org/script.js' => [ 'as' => 'script' ],
|
|
] );
|
|
$foo->method( 'getName' )->willReturn( __METHOD__ );
|
|
|
|
$bar = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getPreloadLinks', 'getName' ] )->getMock();
|
|
$bar->method( 'getPreloadLinks' )->willReturn( [
|
|
'/example.png' => [ 'as' => 'image' ],
|
|
'/example.jpg' => [ 'as' => 'image' ],
|
|
] );
|
|
$bar->method( 'getName' )->willReturn( __METHOD__ );
|
|
|
|
$rl = new EmptyResourceLoader();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'foo|bar', 'only' => 'scripts' ],
|
|
$rl
|
|
);
|
|
|
|
$modules = [ 'foo' => $foo, 'bar' => $bar ];
|
|
$response = $rl->makeModuleResponse( $context, $modules );
|
|
$extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
|
|
$this->assertEquals(
|
|
[
|
|
'Link: <https://example.org/script.js>;rel=preload;as=script',
|
|
'Link: </example.png>;rel=preload;as=image,</example.jpg>;rel=preload;as=image'
|
|
],
|
|
$extraHeaders,
|
|
'Extra headers'
|
|
);
|
|
}
|
|
|
|
public function testRespondEmpty() {
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'tryRespondNotModified',
|
|
'sendResponseHeaders',
|
|
'measureResponseTime',
|
|
] )
|
|
->getMock();
|
|
$context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl );
|
|
|
|
$rl->expects( $this->once() )->method( 'measureResponseTime' );
|
|
$this->expectOutputRegex( '/no modules were requested/' );
|
|
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
public function testRespondSimple() {
|
|
$module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'measureResponseTime',
|
|
'tryRespondNotModified',
|
|
'sendResponseHeaders',
|
|
'makeModuleResponse',
|
|
] )
|
|
->getMock();
|
|
$rl->register( 'test', [
|
|
'factory' => static function () use ( $module ) {
|
|
return $module;
|
|
}
|
|
] );
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'test', 'only' => null ],
|
|
$rl
|
|
);
|
|
|
|
$rl->expects( $this->once() )->method( 'makeModuleResponse' )
|
|
->with( $context, [ 'test' => $module ] )
|
|
->willReturn( 'implement_foo;' );
|
|
$this->expectOutputRegex( '/^implement_foo;/' );
|
|
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
public function testRespondMissingModule() {
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'measureResponseTime',
|
|
'tryRespondNotModified',
|
|
'sendResponseHeaders',
|
|
] )
|
|
->getMock();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'unknown', 'only' => null ],
|
|
$rl
|
|
);
|
|
|
|
$this->expectOutputRegex( '/mw\.loader\.state.*"unknown": "missing"/s' );
|
|
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
/**
|
|
* Silently ignore invalid UTF-8 injected into random query parameters.
|
|
*
|
|
* @see https://phabricator.wikimedia.org/T331641
|
|
*/
|
|
public function testRespondInvalidMissingModule() {
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'measureResponseTime',
|
|
'tryRespondNotModified',
|
|
'sendResponseHeaders',
|
|
] )
|
|
->getMock();
|
|
|
|
// Cover the JS-response which formats via mw.loader.state()
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => "foo|bar\x80\xf0bara|quux", 'only' => null ],
|
|
$rl
|
|
);
|
|
$this->expectOutputRegex( '/mw\.loader\.state.*"foo": "missing"/s' );
|
|
$rl->respond( $context );
|
|
|
|
// Cover the CSS-response which formats via a block comment
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => "foo|bar\x80\xf0bara|quux", 'only' => 'styles' ],
|
|
$rl
|
|
);
|
|
$this->expectOutputRegex( '/Problematic modules.*"foo": "missing"/s' );
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
/**
|
|
* Refuse requests for private modules.
|
|
*/
|
|
public function testRespondErrorPrivate() {
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'measureResponseTime',
|
|
'tryRespondNotModified',
|
|
'sendResponseHeaders',
|
|
] )
|
|
->getMock();
|
|
$rl->register( [
|
|
'foo' => [ 'class' => ResourceLoaderTestModule::class ],
|
|
'bar' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'private' ],
|
|
] );
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'foo|bar', 'only' => null ],
|
|
$rl
|
|
);
|
|
|
|
$this->expectOutputRegex( '/\/\*.+Cannot build private module/s' );
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
public function testRespondInternalFailures() {
|
|
$module = $this->getMockBuilder( ResourceLoaderTestModule::class )
|
|
->onlyMethods( [ 'getDefinitionSummary', 'enableModuleContentVersion' ] )
|
|
->getMock();
|
|
$module->method( 'enableModuleContentVersion' )
|
|
->willReturn( false );
|
|
$module->method( 'getDefinitionSummary' )
|
|
->willThrowException( new Exception( 'Version error' ) );
|
|
$rl = $this->getMockBuilder( EmptyResourceLoader::class )
|
|
->onlyMethods( [
|
|
'measureResponseTime',
|
|
'preloadModuleInfo',
|
|
'tryRespondNotModified',
|
|
'makeModuleResponse',
|
|
'sendResponseHeaders',
|
|
] )
|
|
->getMock();
|
|
$rl->register( 'test', [
|
|
'factory' => static function () use ( $module ) {
|
|
return $module;
|
|
}
|
|
] );
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'test', 'debug' => 'false' ],
|
|
$rl
|
|
);
|
|
// Disable logging from outputErrorAndLog
|
|
$this->setLogger( 'exception', new NullLogger() );
|
|
|
|
$rl->expects( $this->once() )->method( 'preloadModuleInfo' )
|
|
->willThrowException( new Exception( 'Preload error' ) );
|
|
$rl->expects( $this->once() )->method( 'makeModuleResponse' )
|
|
->with( $context, [ 'test' => $module ] )
|
|
->willReturn( 'foo;' );
|
|
// Internal errors should be caught and logged without affecting module output
|
|
$this->expectOutputRegex( '/foo;.*\/\*.+Preload error.+Version error.+\*\//ms' );
|
|
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
private function getResourceLoaderWithTestModules( Config $config = null ) {
|
|
$localBasePath = __DIR__ . '/../../data/resourceloader';
|
|
$remoteBasePath = '/w';
|
|
$rl = new EmptyResourceLoader( $config );
|
|
$rl->register( 'test1', [
|
|
'localBasePath' => $localBasePath,
|
|
'remoteBasePath' => $remoteBasePath,
|
|
'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
|
|
] );
|
|
$rl->register( 'test2', [
|
|
'localBasePath' => $localBasePath,
|
|
'remoteBasePath' => $remoteBasePath,
|
|
'scripts' => [ 'script-nosemi-nonl.js' ],
|
|
] );
|
|
return $rl;
|
|
}
|
|
|
|
public function testRespondSourceMap() {
|
|
$rl = $this->getResourceLoaderWithTestModules();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'test1', 'sourcemap' => '1', 'debug' => '' ],
|
|
$rl
|
|
);
|
|
$this->expectOutputString( <<<JSON
|
|
{
|
|
"version": 3,
|
|
"file": "/load.php?lang=en&modules=test1&only=scripts",
|
|
"sources": ["/w/script-nosemi.js","/w/script-comment.js"],
|
|
"sourcesContent": ["/* eslint-disable */\\nmw.foo()\\n","/* eslint-disable */\\nmw.foo()\\n// mw.bar();\\n"],
|
|
"names": [],
|
|
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC;ACAP,EAAE,CAAC,GAAG,CAAC"
|
|
}
|
|
|
|
JSON
|
|
);
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
public function testRespondIndexMap() {
|
|
$rl = $this->getResourceLoaderWithTestModules();
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'test1|test2', 'sourcemap' => '1', 'debug' => '' ],
|
|
$rl
|
|
);
|
|
$this->expectOutputString( <<<JSON
|
|
{
|
|
"version": 3,
|
|
"sections": [
|
|
{"offset":{"line":0,"column":0},"map":{
|
|
"version": 3,
|
|
"file": "/load.php?lang=en&modules=test1%2Ctest2&only=scripts",
|
|
"sources": ["/w/script-nosemi.js","/w/script-comment.js"],
|
|
"sourcesContent": ["/* eslint-disable */\\nmw.foo()\\n","/* eslint-disable */\\nmw.foo()\\n// mw.bar();\\n"],
|
|
"names": [],
|
|
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC;ACAP,EAAE,CAAC,GAAG,CAAC"
|
|
}
|
|
},
|
|
{"offset":{"line":2,"column":0},"map":{
|
|
"version": 3,
|
|
"file": "/load.php?lang=en&modules=test1%2Ctest2&only=scripts",
|
|
"sources": ["/w/script-nosemi-nonl.js"],
|
|
"sourcesContent": ["/* eslint-disable */\\nmw.foo()"],
|
|
"names": [],
|
|
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
);
|
|
$rl->respond( $context );
|
|
}
|
|
|
|
public function testRespondSourceMapLink() {
|
|
$rl = $this->getResourceLoaderWithTestModules( new HashConfig(
|
|
[
|
|
MainConfigNames::ResourceLoaderEnableSourceMapLinks => true,
|
|
]
|
|
) );
|
|
$context = $this->getResourceLoaderContext(
|
|
[ 'modules' => 'test1|test2', 'debug' => '' ],
|
|
$rl
|
|
);
|
|
$this->expectOutputString( <<<JS
|
|
mw.foo()
|
|
mw.foo()
|
|
mw.foo()
|
|
mw.loader.state({"test1":"ready","test2":"ready"});
|
|
JS
|
|
);
|
|
$rl->respond( $context );
|
|
|
|
$extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
|
|
$this->assertEquals(
|
|
[
|
|
'SourceMap: /load.php?lang=en&modules=test1%2Ctest2&only=scripts&sourcemap=1&version=pq39u'
|
|
],
|
|
$extraHeaders,
|
|
'Extra headers'
|
|
);
|
|
}
|
|
|
|
public function testMeasureResponseTime() {
|
|
$stats = $this->getMockBuilder( NullStatsdDataFactory::class )
|
|
->onlyMethods( [ 'timing' ] )->getMock();
|
|
$this->setService( 'StatsdDataFactory', $stats );
|
|
|
|
$stats->expects( $this->once() )->method( 'timing' )
|
|
->with( 'resourceloader.responseTime', $this->anything() );
|
|
|
|
$rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
|
|
$rl->measureResponseTime();
|
|
}
|
|
|
|
public function testGetUserDefaults() {
|
|
$this->setService( 'UserOptionsLookup', new StaticUserOptionsLookup(
|
|
[],
|
|
[
|
|
'include' => 1,
|
|
'exclude' => 1,
|
|
]
|
|
) );
|
|
$ctx = $this->createStub( Context::class );
|
|
$this->setTemporaryHook( 'ResourceLoaderExcludeUserOptions', function (
|
|
array &$keysToExclude,
|
|
Context $context
|
|
) use ( $ctx ): void {
|
|
$this->assertSame( $ctx, $context );
|
|
$keysToExclude[] = 'exclude';
|
|
}, true );
|
|
|
|
$defaults = ResourceLoader::getUserDefaults(
|
|
$ctx,
|
|
$this->getServiceContainer()->getHookContainer(),
|
|
$this->getServiceContainer()->getUserOptionsLookup()
|
|
);
|
|
$this->assertSame( [ 'include' => 1 ], $defaults );
|
|
}
|
|
}
|