From 80e5b160e0985304a540a32002ff356aae886e8f Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Fri, 15 Jul 2016 15:13:09 +0100 Subject: [PATCH] resourceloader: Move queue formatting out of OutputPage HTML formatting of the queue was distributed over several OutputPage methods. Each method demanding a snippet of HTML by calling makeResourceLoaderLink() with a limited amount of information. As such, makeResourceLoaderLink() was unable to provide the client with the proper state information. Centralising it also allows it to better reduce duplication in HTML output and maintain a more accurate state. Problems fixed by centralising: 1. The 'user' module is special (due to per-user 'version' and 'user' params). It is manually requested via script-src. To avoid a separate (and wrong) request from something that requires it, we set state=loading directly. However, because the module is in the bottom, the old HTML formatter could only put state=loading in the bottom also. This sometimes caused a wrong request to be fired for modules=user if something in the top queue triggered a requirement for it. 2. Since a464d1d4 (T87871) we track states of page-style modules, with purpose of allowing dependencies on style modules without risking duplicate loading on pages where the styles are loaded already. This didn't work, because the state information about page-style modules is output near the stylesheet, which is after the script tag with mw.loader.load(). That runs first, and mw.loader would still make a duplicate request before it learns the state. Changes: * Document reasons for style/script tag order in getHeadHtml (per 09537e83). * Pass $type from getModuleStyles() to getAllowedModules(). This wasn't needed before since a duplicate check in makeResourceLoaderLink() verified the origin a second time. * Declare explicit position 'top' on 'user.options' and 'user.tokens' module. Previously, OutputPage hardcoded them in the top. The new formatter doesn't. * Remove getHeadScripts(). * Remove getInlineHeadScripts(). * Remove getExternalHeadScripts(). * Remove buildCssLinks(). * Remove getScriptsForBottomQueue(). * Change where Skin::setupSkinUserCss() is called. This methods lets the skin add modules to the queue. Previously it was called from buildCssLinks(), via headElement(), via prepareQuickTemplate(), via OutputPage::output(). It's now in OutputPage::output() directly (slightly earlier). This is needed because prepareQuickTemplate() calls bottomScripts() before headElement(). And bottomScript() would lazy-initialise the queue and lock it before setupSkinUserCss() is called from headElement(). This makes execution order more predictable instead of being dependent on the arbitrary order of data extraction in prepareQuickTemplate (which varies from one skin to another). * Compute isUserModulePreview() and isKnownEmpty() for the 'user' module early on so. This avoids wrongful loading and fixes problem 1. Effective changes in output: * mw.loader.state() is now before mw.loader.load(). This fixes problem 2. * mw.loader.state() now sets 'user.options' and 'user.tokens' to "loading". * mw.loader.state() now sets 'user' (as "loading" or "ready"). Fixes problem 1. * The + $data['scripts'][$position][] = $name; + + // Avoid duplicate request from mw.loader + $data['states'][$name] = 'loading'; + } + } + + return $data; + } + + /** + * @return array Attribute key-value pairs for the HTML document element + */ + public function getDocumentAttributes() { + return [ 'class' => 'client-nojs' ]; + } + + /** + * The order of elements in the head is as follows: + * - Inline scripts. + * - Stylesheets. + * - Async external script-src. + * + * Reasons: + * - Script execution may be blocked on preceeding stylesheets. + * - Async scripts are not blocked on stylesheets. + * - Inline scripts can't be asynchronous. + * - For styles, earlier is better. + * + * @return string|WrappedStringList HTML + */ + public function getHeadHtml() { + $data = $this->getData(); + $chunks = []; + + // Change "client-nojs" class to client-js. This allows easy toggling of UI components. + // This happens synchronously on every page view to avoid flashes of wrong content. + // See also #getDocumentAttributes() and /resources/src/startup.js. + $chunks[] = Html::inlineScript( + 'document.documentElement.className = document.documentElement.className' + . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );' + ); + + // Inline RLQ: Set page variables + if ( $this->config ) { + $chunks[] = ResourceLoader::makeInlineScript( + ResourceLoader::makeConfigSetScript( $this->config ) + ); + } + + // Inline RLQ: Initial module states + $states = array_merge( $this->exemptStates, $data['states'] ); + if ( $states ) { + $chunks[] = ResourceLoader::makeInlineScript( + ResourceLoader::makeLoaderStateScript( $states ) + ); + } + + // Inline RLQ: Embedded modules + if ( $data['embed']['general']['top'] ) { + $chunks[] = $this->getLoad( + $data['embed']['general']['top'], + ResourceLoaderModule::TYPE_COMBINED + ); + } + + // Inline RLQ: Load general modules + if ( $data['general']['top'] ) { + $chunks[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['top'] ] ) + ); + } + + // Inline RLQ: Load only=scripts + if ( $data['scripts']['top'] ) { + $chunks[] = $this->getLoad( + $data['scripts']['top'], + ResourceLoaderModule::TYPE_SCRIPTS + ); + } + + // External stylesheets + if ( $data['styles'] ) { + $chunks[] = $this->getLoad( + $data['styles'], + ResourceLoaderModule::TYPE_STYLES + ); + } + + // Inline stylesheets (embedded only=styles) + if ( $data['embed']['styles'] ) { + $chunks[] = $this->getLoad( + $data['embed']['styles'], + ResourceLoaderModule::TYPE_STYLES + ); + } + + // Async scripts. Once the startup is loaded, inline RLQ scripts will run. + $chunks[] = $this->getLoad( 'startup', ResourceLoaderModule::TYPE_SCRIPTS ); + + return WrappedStringList::join( "\n", $chunks ); + } + + /** + * @return string|WrappedStringList HTML + */ + public function getBodyHtml() { + $data = $this->getData(); + $chunks = []; + + // Inline RLQ: Embedded modules + if ( $data['embed']['general']['bottom'] ) { + $chunks[] = $this->getLoad( + $data['embed']['general']['bottom'], + ResourceLoaderModule::TYPE_COMBINED + ); + } + + // Inline RLQ: Load only=scripts + if ( $data['scripts']['bottom'] ) { + $chunks[] = $this->getLoad( + $data['scripts']['bottom'], + ResourceLoaderModule::TYPE_SCRIPTS + ); + } + + // Inline RLQ: Load general modules + if ( $data['general']['bottom'] ) { + $chunks[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['bottom'] ] ) + ); + } + + return WrappedStringList::join( "\n", $chunks ); + } + + private function getContext( $group, $type ) { + return self::makeContext( $this->context, $group, $type ); + } + + private function getLoad( $modules, $only ) { + return self::makeLoad( $this->context, (array)$modules, $only ); + } + + private static function makeContext( ResourceLoaderContext $mainContext, $group, $type, + array $extraQuery = [] + ) { + // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw(). + $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) ); + // Set 'only' if not combined + $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type ); + // Remove user parameter in most cases + if ( $group !== 'user' && $group !== 'private' ) { + $req->setVal( 'user', null ); + } + $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req ); + // Allow caller to setVersion() and setModules() + return new DerivativeResourceLoaderContext( $context ); + } + + /** + * Explicily load or embed modules on a page. + * + * @param ResourceLoaderContext $mainContext + * @param array $modules One or more module names + * @param string $only ResourceLoaderModule TYPE_ class constant + * @param array $extraQuery [optional] Array with extra query parameters for the request + * @return string|WrappedStringList HTML + */ + public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only, + array $extraQuery = [] + ) { + $rl = $mainContext->getResourceLoader(); + $chunks = []; + + if ( $mainContext->getDebug() && count( $modules ) > 1 ) { + $chunks = []; + // Recursively call us for every item + foreach ( $modules as $name ) { + $chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery ); + } + return new WrappedStringList( "\n", $chunks ); + } + + // Sort module names so requests are more uniform + sort( $modules ); + // Create keyed-by-source and then keyed-by-group list of module objects from modules list + $sortedModules = []; + foreach ( $modules as $name ) { + $module = $rl->getModule( $name ); + if ( !$module ) { + $rl->getLogger()->warning( 'Unknown module "{module}"', [ 'module' => $name ] ); + continue; + } + $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module; + } + + foreach ( $sortedModules as $source => $groups ) { + foreach ( $groups as $group => $grpModules ) { + $context = self::makeContext( $mainContext, $group, $only, $extraQuery ); + + if ( $group === 'private' ) { + // Decide whether to use style or script element + if ( $only == ResourceLoaderModule::TYPE_STYLES ) { + $chunks[] = Html::inlineStyle( + $rl->makeModuleResponse( $context, $grpModules ) + ); + } else { + $chunks[] = ResourceLoader::makeInlineScript( + $rl->makeModuleResponse( $context, $grpModules ) + ); + } + continue; + } + + // See if we have one or more raw modules + $isRaw = false; + foreach ( $grpModules as $key => $module ) { + $isRaw |= $module->isRaw(); + } + + // Special handling for the user group; because users might change their stuff + // on-wiki like user pages, or user preferences; we need to find the highest + // timestamp of these user-changeable modules so we can ensure cache misses on change + // This should NOT be done for the site group (bug 27564) because anons get that too + // and we shouldn't be putting timestamps in CDN-cached HTML + if ( $group === 'user' ) { + $version = $rl->getCombinedVersion( $context, array_keys( $grpModules ) ); + $context->setVersion( $version ); + } + + $context->setModules( array_keys( $grpModules ) ); + $url = $rl->createLoaderURL( $source, $context, $extraQuery ); + + // Decide whether to use 'style' or 'script' element + if ( $only === ResourceLoaderModule::TYPE_STYLES ) { + $chunk = Html::linkedStyle( $url ); + } else { + if ( $context->getRaw() || $isRaw ) { + $chunk = Html::element( 'script', [ + // In SpecialJavaScriptTest, QUnit must load synchronous + 'async' => !isset( $extraQuery['sync'] ), + 'src' => $url + ] ); + } else { + $chunk = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', [ $url ] ) + ); + } + } + + if ( $group == 'noscript' ) { + $chunks[] = Html::rawElement( 'noscript', [], $chunk ); + } else { + $chunks[] = $chunk; + } + } + } + + return new WrappedStringList( "\n", $chunks ); + } +} diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 85fc53d600..8fa04118c0 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -260,7 +260,7 @@ class ResourceLoaderContext { /** * @see ResourceLoaderModule::getVersionHash - * @see OutputPage::makeResourceLoaderLink + * @see ResourceLoaderClientHtml::makeLoad * @return string|null */ public function getVersion() { diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index b3b3f16832..c1b47bf51d 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -64,6 +64,13 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { return false; } + /** + * @return string + */ + public function getPosition() { + return 'top'; + } + /** * @return string */ diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php index cea1f3940b..c8a0ff131c 100644 --- a/includes/resourceloader/ResourceLoaderUserTokensModule.php +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -74,6 +74,13 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { return false; } + /** + * @return string + */ + public function getPosition() { + return 'top'; + } + /** * @return string */ diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index 5d36a3c676..0e2e7db046 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -118,7 +118,7 @@ class SpecialJavaScriptTest extends SpecialPage { . 'window.__karma__.loaded = function () {};' . '}'; - // The below is essentially a pure-javascript version of OutputPage::getHeadScripts. + // The below is essentially a pure-javascript version of OutputPage::headElement(). $startup = $rl->makeModuleResponse( $startupContext, [ 'startup' => $rl->getModule( 'startup' ), ] ); @@ -166,7 +166,7 @@ class SpecialJavaScriptTest extends SpecialPage { [ 'raw' => true, 'sync' => true ] ); - $head = implode( "\n", array_merge( $styles['html'], $scripts['html'] ) ); + $head = implode( "\n", [ $styles, $scripts ] ); $summary = $this->getSummaryHtml(); $html = << diff --git a/resources/src/startup.js b/resources/src/startup.js index 62ee94ea16..d026cb01ab 100644 --- a/resources/src/startup.js +++ b/resources/src/startup.js @@ -77,7 +77,7 @@ function isCompatible( str ) { var NORLQ, script; if ( !isCompatible() ) { // Undo class swapping in case of an unsupported browser. - // See OutputPage::getHeadScripts(). + // See ResourceLoaderClientHtml::getDocumentAttributes(). document.documentElement.className = document.documentElement.className .replace( /(^|\s)client-js(\s|$)/, '$1client-nojs$2' ); diff --git a/tests/phpunit/ResourceLoaderTestCase.php b/tests/phpunit/ResourceLoaderTestCase.php index 85bf9549cc..84bf2fdc12 100644 --- a/tests/phpunit/ResourceLoaderTestCase.php +++ b/tests/phpunit/ResourceLoaderTestCase.php @@ -64,10 +64,13 @@ class ResourceLoaderTestModule extends ResourceLoaderModule { protected $dependencies = []; protected $group = null; protected $source = 'local'; + protected $position = 'bottom'; protected $script = ''; protected $styles = ''; protected $skipFunction = null; protected $isRaw = false; + protected $isKnownEmpty = false; + protected $type = ResourceLoaderModule::LOAD_GENERAL; protected $targets = [ 'phpunit' ]; public function __construct( $options = [] ) { @@ -99,6 +102,13 @@ class ResourceLoaderTestModule extends ResourceLoaderModule { public function getSource() { return $this->source; } + public function getPosition() { + return $this->position; + } + + public function getType() { + return $this->type; + } public function getSkipFunction() { return $this->skipFunction; @@ -107,6 +117,9 @@ class ResourceLoaderTestModule extends ResourceLoaderModule { public function isRaw() { return $this->isRaw; } + public function isKnownEmpty( ResourceLoaderContext $context ) { + return $this->isKnownEmpty; + } public function enableModuleContentVersion() { return true; diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php index 9934749730..c637d34a5a 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -139,72 +139,35 @@ class OutputPageTest extends MediaWikiTestCase { public static function provideMakeResourceLoaderLink() { // @codingStandardsIgnoreStart Generic.Files.LineLength return [ - // Load module script only + // Single only=scripts load [ [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ], "" ], - [ - // Don't condition wrap raw modules (like the startup module) - [ 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ], - '' - ], - // Load module styles only - // This also tests the order the modules are put into the url + // Multiple only=styles load [ [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ], '' ], - // Load private module (only=scripts) + // Private embed (only=scripts) [ [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ], "" ], - // Load private module (combined) - [ - [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ], - "" - ], - // Load no modules - [ - [ [], ResourceLoaderModule::TYPE_COMBINED ], - '', - ], - // noscript group - [ - [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ], - '' - ], - // Load two modules in separate groups - [ - [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ], - "\n" - . "" - ], ]; // @codingStandardsIgnoreEnd } /** + * See ResourceLoaderClientHtmlTest for full coverage. + * * @dataProvider provideMakeResourceLoaderLink * @covers OutputPage::makeResourceLoaderLink - * @covers ResourceLoader::makeLoaderImplementScript - * @covers ResourceLoader::makeModuleResponse - * @covers ResourceLoader::makeInlineScript - * @covers ResourceLoader::makeLoaderStateScript - * @covers ResourceLoader::createLoaderURL */ public function testMakeResourceLoaderLink( $args, $expectedHtml ) { $this->setMwGlobals( [ @@ -238,25 +201,9 @@ class OutputPageTest extends MediaWikiTestCase { 'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }', 'group' => 'private', ] ), - 'test.raw' => new ResourceLoaderTestModule( [ - 'script' => 'mw.test.baz( { token: 123 } );', - 'isRaw' => true, - ] ), - 'test.noscript' => new ResourceLoaderTestModule( [ - 'styles' => '.mw-test-noscript { content: "style"; }', - 'group' => 'noscript', - ] ), - 'test.group.bar' => new ResourceLoaderTestModule( [ - 'styles' => '.mw-group-bar { content: "style"; }', - 'group' => 'bar', - ] ), - 'test.group.foo' => new ResourceLoaderTestModule( [ - 'styles' => '.mw-group-foo { content: "style"; }', - 'group' => 'foo', - ] ), ] ); $links = $method->invokeArgs( $out, $args ); - $actualHtml = implode( "\n", $links['html'] ); + $actualHtml = strval( $links ); $this->assertEquals( $expectedHtml, $actualHtml ); } diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php new file mode 100644 index 0000000000..0965b9f914 --- /dev/null +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php @@ -0,0 +1,278 @@ + [], + 'ResourceModuleSkinStyles' => [], + 'ResourceModules' => [], + 'EnableJavaScriptTest' => false, + 'ResourceLoaderDebug' => false, + 'LoadScript' => '/w/load.php', + ] ); + return new ResourceLoaderContext( + new ResourceLoader( $conf ), + new FauxRequest( array_merge( [ + 'lang' => 'nl', + 'skin' => 'fallback', + 'user' => 'Example', + 'target' => 'phpunit', + ], $extraQuery ) ) + ); + } + + protected static function makeModule( array $options = [] ) { + return new ResourceLoaderTestModule( $options ); + } + + protected static function makeSampleModules() { + $modules = [ + 'test' => [], + 'test.top' => [ 'position' => 'top' ], + 'test.private.top' => [ 'group' => 'private', 'position' => 'top' ], + 'test.private.bottom' => [ 'group' => 'private', 'position' => 'bottom' ], + + 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ], + 'test.styles.mixed' => [], + 'test.styles.noscript' => [ 'group' => 'noscript', 'type' => ResourceLoaderModule::LOAD_STYLES ], + 'test.styles.mixed.user' => [ 'group' => 'user' ], + 'test.styles.mixed.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ], + 'test.styles.private' => [ 'group' => 'private', 'styles' => '.private{}' ], + + 'test.scripts' => [], + 'test.scripts.top' => [ 'position' => 'top' ], + 'test.scripts.mixed.user' => [ 'group' => 'user' ], + 'test.scripts.mixed.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ], + 'test.scripts.raw' => [ 'isRaw' => true ], + ]; + return array_map( function ( $options ) { + return self::makeModule( $options ); + }, $modules ); + } + + /** + * @covers ResourceLoaderClientHtml::getDocumentAttributes + */ + public function testGetDocumentAttributes() { + $client = new ResourceLoaderClientHtml( self::makeContext() ); + $this->assertInternalType( 'array', $client->getDocumentAttributes() ); + } + + /** + * @covers ResourceLoaderClientHtml::__construct + * @covers ResourceLoaderClientHtml::setModules + * @covers ResourceLoaderClientHtml::setModuleStyles + * @covers ResourceLoaderClientHtml::setModuleScripts + * @covers ResourceLoaderClientHtml::getData + * @covers ResourceLoaderClientHtml::getContext + */ + public function testGetData() { + $context = self::makeContext(); + $context->getResourceLoader()->register( self::makeSampleModules() ); + + $client = new ResourceLoaderClientHtml( $context ); + $client->setModules( [ + 'test', + 'test.private.bottom', + 'test.private.top', + 'test.top', + 'test.unregistered', + ] ); + $client->setModuleStyles( [ + 'test.styles.mixed', + 'test.styles.mixed.user.empty', + 'test.styles.private', + 'test.styles.pure', + 'test.unregistered.styles', + ] ); + $client->setModuleScripts( [ + 'test.scripts', + 'test.scripts.mixed.user.empty', + 'test.scripts.top', + 'test.unregistered.scripts', + ] ); + + $expected = [ + 'states' => [ + 'test.private.top' => 'loading', + 'test.private.bottom' => 'loading', + 'test.styles.pure' => 'ready', + 'test.styles.mixed.user.empty' => 'ready', + 'test.styles.private' => 'ready', + 'test.scripts' => 'loading', + 'test.scripts.top' => 'loading', + 'test.scripts.mixed.user.empty' => 'ready', + ], + 'general' => [ + 'top' => [ 'test.top' ], + 'bottom' => [ 'test' ], + ], + 'styles' => [ + 'test.styles.mixed', + 'test.styles.pure', + ], + 'scripts' => [ + 'top' => [ 'test.scripts.top' ], + 'bottom' => [ 'test.scripts' ], + ], + 'embed' => [ + 'styles' => [ 'test.styles.private' ], + 'general' => [ + 'top' => [ 'test.private.top' ], + 'bottom' => [ 'test.private.bottom' ], + ], + ], + ]; + + $access = TestingAccessWrapper::newFromObject( $client ); + $this->assertEquals( $expected, $access->getData() ); + } + + /** + * @covers ResourceLoaderClientHtml::setConfig + * @covers ResourceLoaderClientHtml::setExemptStates + * @covers ResourceLoaderClientHtml::getHeadHtml + * @covers ResourceLoaderClientHtml::getLoad + * @covers ResourceLoader::makeLoaderStateScript + */ + public function testGetHeadHtml() { + $context = self::makeContext(); + $context->getResourceLoader()->register( self::makeSampleModules() ); + + $client = new ResourceLoaderClientHtml( $context ); + $client->setConfig( [ 'key' => 'value' ] ); + $client->setModules( [ + 'test.top', + 'test.private.top', + ] ); + $client->setModuleStyles( [ + 'test.styles.pure', + 'test.styles.private', + ] ); + $client->setModuleScripts( [ + 'test.scripts.top', + ] ); + $client->setExemptStates( [ + 'test.exempt' => 'ready', + ] ); + + // @codingStandardsIgnoreStart Generic.Files.LineLength + $expected = '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . ''; + // @codingStandardsIgnoreEnd + + $this->assertEquals( $expected, $client->getHeadHtml() ); + } + + /** + * @covers ResourceLoaderClientHtml::getBodyHtml + * @covers ResourceLoaderClientHtml::getLoad + */ + public function testGetBodyHtml() { + $context = self::makeContext(); + $context->getResourceLoader()->register( self::makeSampleModules() ); + + $client = new ResourceLoaderClientHtml( $context ); + $client->setConfig( [ 'key' => 'value' ] ); + $client->setModules( [ + 'test', + 'test.private.bottom', + ] ); + $client->setModuleScripts( [ + 'test.scripts', + ] ); + + // @codingStandardsIgnoreStart Generic.Files.LineLength + $expected = ''; + // @codingStandardsIgnoreEnd + + $this->assertEquals( $expected, $client->getBodyHtml() ); + } + + public static function provideMakeLoad() { + return [ + // @codingStandardsIgnoreStart Generic.Files.LineLength + [ + 'context' => [], + 'modules' => [ 'test.unknown' ], + 'only' => ResourceLoaderModule::TYPE_STYLES, + 'output' => '', + ], + [ + 'context' => [], + 'modules' => [ 'test.styles.private' ], + 'only' => ResourceLoaderModule::TYPE_STYLES, + 'output' => '', + ], + [ + 'context' => [], + 'modules' => [ 'test.private.top' ], + 'only' => ResourceLoaderModule::TYPE_COMBINED, + 'output' => '', + ], + [ + 'context' => [], + // Eg. startup module + 'modules' => [ 'test.scripts.raw' ], + 'only' => ResourceLoaderModule::TYPE_SCRIPTS, + 'output' => '', + ], + [ + 'context' => [], + 'modules' => [ 'test.scripts.mixed.user' ], + 'only' => ResourceLoaderModule::TYPE_SCRIPTS, + 'output' => '', + ], + [ + 'context' => [ 'debug' => true ], + 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ], + 'only' => ResourceLoaderModule::TYPE_STYLES, + 'output' => '' . "\n" + . '', + ], + [ + 'context' => [], + 'modules' => [ 'test.styles.noscript' ], + 'only' => ResourceLoaderModule::TYPE_STYLES, + 'output' => '', + ], + // @codingStandardsIgnoreEnd + ]; + } + + /** + * @dataProvider provideMakeLoad + * @covers ResourceLoaderClientHtml::makeLoad + * @covers ResourceLoaderClientHtml::makeContext + * @covers ResourceLoader::makeModuleResponse + * @covers ResourceLoaderModule::getModuleContent + * @covers ResourceLoader::getCombinedVersion + * @covers ResourceLoader::createLoaderURL + * @covers ResourceLoader::createLoaderQuery + * @covers ResourceLoader::makeLoaderQuery + * @covers ResourceLoader::makeInlineScript + */ + public function testMakeLoad( array $extraQuery, array $modules, $type, $expected ) { + $context = self::makeContext( $extraQuery ); + $context->getResourceLoader()->register( self::makeSampleModules() ); + $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type ); + $this->assertEquals( $expected, (string)$actual ); + } +} -- 2.20.1