Use global variables directly as globals, except for the specific case of accessing it when it may not exist. In those cases we use the fact that undefined properties of an object yield the `undefined` value which we can cast to false. Accessing an undefined variable would yield a ReferenceError exception. Change-Id: I1d9e9aa5845ba3c756ad6e31358d8594e003b04b
434 lines
15 KiB
PHP
434 lines
15 KiB
PHP
<?php
|
|
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @group ResourceLoader
|
|
* @covers ResourceLoaderClientHtml
|
|
*/
|
|
class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
|
|
|
|
use MediaWikiCoversValidator;
|
|
|
|
public function testGetData() {
|
|
$context = self::makeContext();
|
|
$context->getResourceLoader()->register( self::makeSampleModules() );
|
|
|
|
$client = new ResourceLoaderClientHtml( $context );
|
|
$client->setModules( [
|
|
'test',
|
|
'test.private',
|
|
'test.shouldembed.empty',
|
|
'test.shouldembed',
|
|
'test.user',
|
|
'test.unregistered',
|
|
] );
|
|
$client->setModuleStyles( [
|
|
'test.styles.mixed',
|
|
'test.styles.user.empty',
|
|
'test.styles.private',
|
|
'test.styles.pure',
|
|
'test.styles.shouldembed',
|
|
'test.styles.deprecated',
|
|
'test.unregistered.styles',
|
|
] );
|
|
|
|
$expected = [
|
|
'states' => [
|
|
// The below are NOT queued for loading via `mw.loader.load(Array)`.
|
|
// Instead we tell the client to set their state to "loading" so that
|
|
// if they are needed as dependencies, the client will not try to
|
|
// load them on-demand, because the server is taking care of them already.
|
|
// Either:
|
|
// - Embedded as inline scripts in the HTML (e.g. user-private code, and
|
|
// previews). Once that script tag is reached, the state is "loaded".
|
|
// - Loaded directly from the HTML with a dedicated HTTP request (e.g.
|
|
// user scripts, which vary by a 'user' and 'version' parameter that
|
|
// the static user-agnostic startup module won't have).
|
|
'test.private' => 'loading',
|
|
'test.shouldembed' => 'loading',
|
|
'test.user' => 'loading',
|
|
// The below are known to the server to be empty scripts, or to be
|
|
// synchronously loaded stylesheets. These start in the "ready" state.
|
|
'test.shouldembed.empty' => 'ready',
|
|
'test.styles.pure' => 'ready',
|
|
'test.styles.user.empty' => 'ready',
|
|
'test.styles.private' => 'ready',
|
|
'test.styles.shouldembed' => 'ready',
|
|
'test.styles.deprecated' => 'ready',
|
|
],
|
|
'general' => [
|
|
'test',
|
|
],
|
|
'styles' => [
|
|
'test.styles.pure',
|
|
'test.styles.deprecated',
|
|
],
|
|
'embed' => [
|
|
'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
|
|
'general' => [
|
|
'test.private',
|
|
'test.shouldembed',
|
|
'test.user',
|
|
],
|
|
],
|
|
'styleDeprecations' => [
|
|
Xml::encodeJsCall(
|
|
'mw.log.warn',
|
|
[ 'This page is using the deprecated ResourceLoader module "test.styles.deprecated".
|
|
Deprecation message.' ]
|
|
)
|
|
],
|
|
];
|
|
|
|
$access = TestingAccessWrapper::newFromObject( $client );
|
|
$this->assertEquals( $expected, $access->getData() );
|
|
}
|
|
|
|
public function testGetHeadHtml() {
|
|
$context = self::makeContext();
|
|
$context->getResourceLoader()->register( self::makeSampleModules() );
|
|
|
|
$client = new ResourceLoaderClientHtml( $context, [
|
|
'nonce' => false,
|
|
] );
|
|
$client->setConfig( [ 'key' => 'value' ] );
|
|
$client->setModules( [
|
|
'test',
|
|
'test.private',
|
|
] );
|
|
$client->setModuleStyles( [
|
|
'test.styles.pure',
|
|
'test.styles.private',
|
|
'test.styles.deprecated',
|
|
] );
|
|
$client->setExemptStates( [
|
|
'test.exempt' => 'ready',
|
|
] );
|
|
|
|
// phpcs:disable Generic.Files.LineLength
|
|
$expected = '<script>'
|
|
. 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
|
|
. 'RLCONF={"key":"value"};'
|
|
. 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
|
|
. 'RLPAGEMODULES=["test"];'
|
|
. '</script>' . "\n"
|
|
. '<script>(RLQ=window.RLQ||[]).push(function(){'
|
|
. 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
|
|
. '});</script>' . "\n"
|
|
. '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.deprecated%2Cpure&only=styles&skin=fallback"/>' . "\n"
|
|
. '<style>.private{}</style>' . "\n"
|
|
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&skin=fallback"></script>';
|
|
// phpcs:enable
|
|
$expected = self::expandVariables( $expected );
|
|
|
|
$this->assertSame( $expected, (string)$client->getHeadHtml() );
|
|
}
|
|
|
|
/**
|
|
* Confirm that 'target' is passed down to the startup module's load url.
|
|
*/
|
|
public function testGetHeadHtmlWithTarget() {
|
|
$client = new ResourceLoaderClientHtml(
|
|
self::makeContext(),
|
|
[ 'target' => 'example' ]
|
|
);
|
|
|
|
// phpcs:disable Generic.Files.LineLength
|
|
$expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
|
|
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&skin=fallback&target=example"></script>';
|
|
// phpcs:enable
|
|
|
|
$this->assertSame( $expected, (string)$client->getHeadHtml() );
|
|
}
|
|
|
|
/**
|
|
* Confirm that 'safemode' is passed down to startup.
|
|
*/
|
|
public function testGetHeadHtmlWithSafemode() {
|
|
$client = new ResourceLoaderClientHtml(
|
|
self::makeContext(),
|
|
[ 'safemode' => '1' ]
|
|
);
|
|
|
|
// phpcs:disable Generic.Files.LineLength
|
|
$expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
|
|
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&safemode=1&skin=fallback"></script>';
|
|
// phpcs:enable
|
|
|
|
$this->assertSame( $expected, (string)$client->getHeadHtml() );
|
|
}
|
|
|
|
/**
|
|
* Confirm that a null 'target' is the same as no target.
|
|
*/
|
|
public function testGetHeadHtmlWithNullTarget() {
|
|
$client = new ResourceLoaderClientHtml(
|
|
self::makeContext(),
|
|
[ 'target' => null ]
|
|
);
|
|
|
|
// phpcs:disable Generic.Files.LineLength
|
|
$expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
|
|
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&skin=fallback"></script>';
|
|
// phpcs:enable
|
|
|
|
$this->assertSame( $expected, (string)$client->getHeadHtml() );
|
|
}
|
|
|
|
public function testGetBodyHtml() {
|
|
$context = self::makeContext();
|
|
$context->getResourceLoader()->register( self::makeSampleModules() );
|
|
|
|
$client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
|
|
$client->setConfig( [ 'key' => 'value' ] );
|
|
$client->setModules( [
|
|
'test',
|
|
'test.private.bottom',
|
|
] );
|
|
$client->setModuleStyles( [
|
|
'test.styles.deprecated',
|
|
] );
|
|
// phpcs:disable Generic.Files.LineLength
|
|
$expected = '<script>(RLQ=window.RLQ||[]).push(function(){'
|
|
. 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
|
|
. '});</script>';
|
|
// phpcs:enable
|
|
|
|
$this->assertSame( $expected, (string)$client->getBodyHtml() );
|
|
}
|
|
|
|
public static function provideMakeLoad() {
|
|
// phpcs:disable Generic.Files.LineLength
|
|
return [
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.unknown' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.styles.private' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '<style>.private{}</style>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.private' ],
|
|
'only' => ResourceLoaderModule::TYPE_COMBINED,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
// Eg. startup module
|
|
'modules' => [ 'test.scripts.raw' ],
|
|
'only' => ResourceLoaderModule::TYPE_SCRIPTS,
|
|
'extra' => [],
|
|
'output' => '<script async="" src="/w/load.php?lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback"></script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.scripts.raw' ],
|
|
'only' => ResourceLoaderModule::TYPE_SCRIPTS,
|
|
'extra' => [ 'sync' => '1' ],
|
|
'output' => '<script src="/w/load.php?lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback&sync=1"></script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.scripts.user' ],
|
|
'only' => ResourceLoaderModule::TYPE_SCRIPTS,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026skin=fallback\u0026user=Example\u0026version=0a56zyi");});</script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.user' ],
|
|
'only' => ResourceLoaderModule::TYPE_COMBINED,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026skin=fallback\u0026user=Example\u0026version=0a56zyi");});</script>',
|
|
],
|
|
[
|
|
'context' => [ 'debug' => 'true' ],
|
|
'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.mixed&only=styles&skin=fallback"/>' . "\n"
|
|
. '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>',
|
|
],
|
|
[
|
|
'context' => [ 'debug' => 'false' ],
|
|
'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.mixed%2Cpure&only=styles&skin=fallback"/>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.styles.noscript' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.noscript&only=styles&skin=fallback"/></noscript>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.shouldembed' ],
|
|
'only' => ResourceLoaderModule::TYPE_COMBINED,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.styles.shouldembed' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' => '<style>.shouldembed{}</style>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.scripts.shouldembed' ],
|
|
'only' => ResourceLoaderModule::TYPE_SCRIPTS,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test', 'test.shouldembed' ],
|
|
'only' => ResourceLoaderModule::TYPE_COMBINED,
|
|
'extra' => [],
|
|
'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test\u0026skin=fallback");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' =>
|
|
'<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>' . "\n"
|
|
. '<style>.shouldembed{}</style>'
|
|
],
|
|
[
|
|
'context' => [],
|
|
'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
|
|
'only' => ResourceLoaderModule::TYPE_STYLES,
|
|
'extra' => [],
|
|
'output' =>
|
|
'<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.a%2Cb&only=styles&skin=fallback"/>' . "\n"
|
|
. '<style>.orderingC{}.orderingD{}</style>' . "\n"
|
|
. '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.e&only=styles&skin=fallback"/>'
|
|
],
|
|
];
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMakeLoad
|
|
* @covers ResourceLoaderClientHtml
|
|
* @covers ResourceLoaderModule::getModuleContent
|
|
* @covers ResourceLoader
|
|
*/
|
|
public function testMakeLoad(
|
|
array $contextQuery,
|
|
array $modules,
|
|
$type,
|
|
array $extraQuery,
|
|
$expected
|
|
) {
|
|
$context = self::makeContext( $contextQuery );
|
|
$context->getResourceLoader()->register( self::makeSampleModules() );
|
|
$actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
|
|
$expected = self::expandVariables( $expected );
|
|
$this->assertSame( $expected, (string)$actual );
|
|
}
|
|
|
|
public function testGetDocumentAttributes() {
|
|
$client = new ResourceLoaderClientHtml( self::makeContext() );
|
|
$this->assertInternalType( 'array', $client->getDocumentAttributes() );
|
|
}
|
|
|
|
private static function expandVariables( $text ) {
|
|
return strtr( $text, [
|
|
'{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
|
|
] );
|
|
}
|
|
|
|
private static function makeContext( $extraQuery = [] ) {
|
|
$conf = new HashConfig( [
|
|
'ResourceModuleSkinStyles' => [],
|
|
'ResourceModules' => [],
|
|
'EnableJavaScriptTest' => false,
|
|
'LoadScript' => '/w/load.php',
|
|
] );
|
|
return new ResourceLoaderContext(
|
|
new ResourceLoader( $conf ),
|
|
new FauxRequest( array_merge( [
|
|
'lang' => 'nl',
|
|
'skin' => 'fallback',
|
|
'user' => 'Example',
|
|
'target' => 'phpunit',
|
|
], $extraQuery ) )
|
|
);
|
|
}
|
|
|
|
private static function makeModule( array $options = [] ) {
|
|
return new ResourceLoaderTestModule( $options );
|
|
}
|
|
|
|
private static function makeSampleModules() {
|
|
$modules = [
|
|
'test' => [],
|
|
'test.private' => [ 'group' => 'private' ],
|
|
'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
|
|
'test.shouldembed' => [ 'shouldEmbed' => true ],
|
|
'test.user' => [ 'group' => 'user' ],
|
|
|
|
'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
|
|
'test.styles.mixed' => [],
|
|
'test.styles.noscript' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'group' => 'noscript',
|
|
],
|
|
'test.styles.user' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'group' => 'user',
|
|
],
|
|
'test.styles.user.empty' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'group' => 'user',
|
|
'isKnownEmpty' => true,
|
|
],
|
|
'test.styles.private' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'group' => 'private',
|
|
'styles' => '.private{}',
|
|
],
|
|
'test.styles.shouldembed' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'shouldEmbed' => true,
|
|
'styles' => '.shouldembed{}',
|
|
],
|
|
'test.styles.deprecated' => [
|
|
'type' => ResourceLoaderModule::LOAD_STYLES,
|
|
'deprecated' => 'Deprecation message.',
|
|
],
|
|
|
|
'test.scripts' => [],
|
|
'test.scripts.user' => [ 'group' => 'user' ],
|
|
'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
|
|
'test.scripts.raw' => [ 'isRaw' => true ],
|
|
'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
|
|
|
|
'test.ordering.a' => [ 'shouldEmbed' => false ],
|
|
'test.ordering.b' => [ 'shouldEmbed' => false ],
|
|
'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
|
|
'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
|
|
'test.ordering.e' => [ 'shouldEmbed' => false ],
|
|
];
|
|
return array_map( function ( $options ) {
|
|
return self::makeModule( $options );
|
|
}, $modules );
|
|
}
|
|
}
|