Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / tests / phpunit / includes / ResourceLoader / ModuleTest.php
blob4f1c12736104bb5aaec732a507dea86bf7133f54
1 <?php
3 namespace MediaWiki\Tests\ResourceLoader;
5 use LogicException;
6 use MediaWiki\MainConfigNames;
7 use MediaWiki\ResourceLoader\FileModule;
8 use MediaWiki\ResourceLoader\MessageBlobStore;
9 use MediaWiki\ResourceLoader\Module;
10 use MediaWiki\ResourceLoader\ResourceLoader;
11 use ReflectionMethod;
13 /**
14 * @group ResourceLoader
15 * @covers \MediaWiki\ResourceLoader\Module
17 class ModuleTest extends ResourceLoaderTestCase {
19 public function testGetVersionHash() {
20 $context = $this->getResourceLoaderContext( [ 'debug' => 'false' ] );
21 $msgBlobStore = $this->createMock( MessageBlobStore::class );
22 $msgBlobStore->method( 'getBlob' )->willReturn( '{}' );
23 $context->getResourceLoader()->setMessageBlobStore( $msgBlobStore );
25 $baseParams = [
26 'scripts' => [ 'foo.js', 'bar.js' ],
27 'dependencies' => [ 'jquery', 'mediawiki' ],
28 'messages' => [ 'hello', 'world' ],
31 $module = new FileModule( $baseParams );
32 $module->setName( 'test' );
33 $version = json_encode( $module->getVersionHash( $context ) );
35 // Exactly the same
36 $module = new FileModule( $baseParams );
37 $module->setName( 'test' );
38 $this->assertEquals(
39 $version,
40 json_encode( $module->getVersionHash( $context ) ),
41 'Instance is insignificant'
44 // Re-order dependencies
45 $module = new FileModule( [
46 'dependencies' => [ 'mediawiki', 'jquery' ],
47 ] + $baseParams );
48 $module->setName( 'test' );
49 $this->assertEquals(
50 $version,
51 json_encode( $module->getVersionHash( $context ) ),
52 'Order of dependencies is insignificant'
55 // Re-order messages
56 $module = new FileModule( [
57 'messages' => [ 'world', 'hello' ],
58 ] + $baseParams );
59 $module->setName( 'test' );
60 $this->assertEquals(
61 $version,
62 json_encode( $module->getVersionHash( $context ) ),
63 'Order of messages is insignificant'
66 // Re-order scripts
67 $module = new FileModule( [
68 'scripts' => [ 'bar.js', 'foo.js' ],
69 ] + $baseParams );
70 $module->setName( 'test' );
71 $this->assertNotEquals(
72 $version,
73 json_encode( $module->getVersionHash( $context ) ),
74 'Order of scripts is significant'
77 // Subclass
78 $module = new ResourceLoaderFileModuleTestingSubclass( $baseParams );
79 $module->setName( 'test' );
80 $this->assertNotEquals(
81 $version,
82 json_encode( $module->getVersionHash( $context ) ),
83 'Class is significant'
87 public function testGetVersionHash_debug() {
88 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
89 $module->setName( 'test' );
90 $context = $this->getResourceLoaderContext( [ 'debug' => 'true' ] );
91 $this->assertSame( '', $module->getVersionHash( $context ) );
94 public function testGetVersionHash_length() {
95 $context = $this->getResourceLoaderContext( [ 'debug' => 'false' ] );
96 $module = new ResourceLoaderTestModule( [
97 'script' => 'foo();'
98 ] );
99 $module->setName( 'test' );
100 $version = $module->getVersionHash( $context );
101 $this->assertSame( ResourceLoader::HASH_LENGTH, strlen( $version ), 'Hash length' );
104 public function testGetVersionHash_parentDefinition() {
105 $context = $this->getResourceLoaderContext( [ 'debug' => 'false' ] );
106 $module = $this->getMockBuilder( Module::class )
107 ->onlyMethods( [ 'getDefinitionSummary' ] )->getMock();
108 $module->method( 'getDefinitionSummary' )->willReturn( [ 'a' => 'summary' ] );
109 $module->setName( 'test' );
111 $this->expectException( LogicException::class );
112 $this->expectExceptionMessage( 'must call parent' );
113 $module->getVersionHash( $context );
117 * @covers \MediaWiki\ResourceLoader\Module
118 * @covers \MediaWiki\ResourceLoader\ResourceLoader
120 public function testGetURLsForDebug() {
121 $module = new ResourceLoaderTestModule( [
122 'script' => 'foo();',
123 'styles' => '.foo { color: blue; }',
124 ] );
125 $context = $this->getResourceLoaderContext( [ 'debug' => 'true' ] );
126 $module->setConfig( $context->getResourceLoader()->getConfig() );
127 $module->setName( 'test' );
129 $this->assertEquals(
131 'https://example.org/w/load.php?debug=1&lang=en&modules=test&only=scripts'
133 $module->getScriptURLsForDebug( $context ),
134 'script urls debug=true'
136 $this->assertEquals(
137 [ 'all' => [
138 '/w/load.php?debug=1&lang=en&modules=test&only=styles'
139 ] ],
140 $module->getStyleURLsForDebug( $context ),
141 'style urls debug=true'
144 $context = $this->getResourceLoaderContext( [ 'debug' => '2' ] );
145 $this->assertEquals(
147 'https://example.org/w/load.php?debug=2&lang=en&modules=test&only=scripts'
149 $module->getScriptURLsForDebug( $context ),
150 'script urls debug=2'
152 $this->assertEquals(
153 [ 'all' => [
154 '/w/load.php?debug=2&lang=en&modules=test&only=styles'
155 ] ],
156 $module->getStyleURLsForDebug( $context ),
157 'style urls debug=2'
161 public static function provideValidateScripts() {
162 yield 'valid ES5' => [ "\n'valid';" ];
164 yield 'valid ES6/ES2015 for-of' => [
165 "var x = ['a', 'b']; for (var key of x) { console.log(key); }"
168 yield 'valid ES2016 exponentiation' => [
169 "var x = 2; var y = 3; console.log(x ** y);"
172 yield 'valid ES2017 async-await' => [
173 "var foo = async function(x) { return await x.fetch(); }",
174 'Parse error: Unexpected: function on line 1'
177 yield 'valid ES2018 spread in object literal' => [
178 "var x = {b: 2, c: 3}; var y = {a: 1, ...x};",
179 'Parse error: Unexpected: ... on line 1'
182 yield 'SyntaxError' => [
183 "var a = 'this is';\n {\ninvalid",
184 'Parse error: Unclosed { on line 3'
187 // If an implementation matches inputs using a regex with runaway backtracking,
188 // then inputs with more than ~3072 repetitions are likely to fail (T299537).
189 $input = '"' . str_repeat( 'x', 10000 ) . '";';
190 yield 'double quote string 10K' => [ $input, ];
191 $input = '\'' . str_repeat( 'x', 10000 ) . '\';';
192 yield 'single quote string 10K' => [ $input ];
193 $input = '"' . str_repeat( '\u0021', 100 ) . '";';
194 yield 'escaping string 100' => [ $input ];
195 $input = '"' . str_repeat( '\u0021', 10000 ) . '";';
196 yield 'escaping string 10K' => [ $input ];
197 $input = '/' . str_repeat( 'x', 1000 ) . '/;';
198 yield 'regex 1K' => [ $input ];
199 $input = '/' . str_repeat( 'x', 10000 ) . '/;';
200 yield 'regex 10K' => [ $input ];
201 $input = '/' . str_repeat( '\u0021', 100 ) . '/;';
202 yield 'escaping regex 100' => [ $input ];
203 $input = '/' . str_repeat( '\u0021', 10000 ) . '/;';
204 yield 'escaping regex 10K' => [ $input ];
208 * @dataProvider provideValidateScripts
210 public function testValidateScriptFile( $input, $error = null ) {
211 $this->overrideConfigValue( MainConfigNames::ResourceLoaderValidateJS, true );
213 $context = $this->getResourceLoaderContext();
215 $module = new ResourceLoaderTestModule( [
216 'mayValidateScript' => true,
217 'script' => $input
218 ] );
219 $module->setConfig( $context->getResourceLoader()->getConfig() );
221 $result = $module->getScript( $context );
222 if ( $error ) {
223 $this->assertStringContainsString( 'mw.log.error(', $result, 'log error' );
224 $this->assertStringContainsString( $error, $result, 'error message' );
225 } else {
226 $this->assertEquals(
227 $input,
228 $module->getScript( $context ),
229 'Leave valid scripts as-is'
234 public static function provideBuildContentScripts() {
235 return [
237 "mw.foo()",
240 "mw.foo();",
243 "mw.foo();\n",
246 "mw.foo()\n",
249 "mw.foo()\n// mw.bar();",
252 "mw.foo()\n// mw.bar()",
255 "mw.foo()// mw.bar();",
261 * @dataProvider provideBuildContentScripts
263 public function testBuildContentScripts( $raw, $message = '' ) {
264 $context = $this->getResourceLoaderContext();
265 $module = new ResourceLoaderTestModule( [
266 'script' => $raw
267 ] );
268 $module->setName( 'test' );
269 $this->assertEquals( $raw, $module->getScript( $context ), 'Raw script' );
270 $this->assertEquals(
271 [ 'plainScripts' => [ [ 'content' => $raw ] ] ],
272 $module->getModuleContent( $context )[ 'scripts' ],
273 $message
277 public function testPlaceholderize() {
278 $getRelativePaths = new ReflectionMethod( Module::class, 'getRelativePaths' );
279 $getRelativePaths->setAccessible( true );
280 $expandRelativePaths = new ReflectionMethod( Module::class, 'expandRelativePaths' );
281 $expandRelativePaths->setAccessible( true );
283 $this->setMwGlobals( [
284 'IP' => '/srv/example/mediawiki/core',
285 ] );
286 $raw = [
287 '/srv/example/mediawiki/core/resources/foo.js',
288 '/srv/example/mediawiki/core/extensions/Example/modules/bar.js',
289 '/srv/example/mediawiki/skins/Example/baz.css',
290 '/srv/example/mediawiki/skins/Example/images/quux.png',
292 $canonical = [
293 'resources/foo.js',
294 'extensions/Example/modules/bar.js',
295 '../skins/Example/baz.css',
296 '../skins/Example/images/quux.png',
298 $this->assertEquals(
299 $canonical,
300 $getRelativePaths->invoke( null, $raw ),
301 'Insert placeholders'
303 $this->assertEquals(
304 $raw,
305 $expandRelativePaths->invoke( null, $canonical ),
306 'Substitute placeholders'
310 public function testGetHeaders() {
311 $context = $this->getResourceLoaderContext();
313 $module = new ResourceLoaderTestModule();
314 $module->setName( 'test' );
315 $this->assertSame( [], $module->getHeaders( $context ), 'Default' );
317 $module = $this->getMockBuilder( ResourceLoaderTestModule::class )
318 ->onlyMethods( [ 'getPreloadLinks' ] )->getMock();
319 $module->method( 'getPreloadLinks' )->willReturn( [
320 'https://example.org/script.js' => [ 'as' => 'script' ],
321 ] );
322 $this->assertSame(
324 'Link: <https://example.org/script.js>;rel=preload;as=script'
326 $module->getHeaders( $context ),
327 'Preload one resource'
330 $module = $this->getMockBuilder( ResourceLoaderTestModule::class )
331 ->onlyMethods( [ 'getPreloadLinks' ] )->getMock();
332 $module->method( 'getPreloadLinks' )->willReturn( [
333 'https://example.org/script.js' => [ 'as' => 'script' ],
334 '/example.png' => [ 'as' => 'image' ],
335 ] );
336 $module->setName( 'test' );
337 $this->assertSame(
339 'Link: <https://example.org/script.js>;rel=preload;as=script,' .
340 '</example.png>;rel=preload;as=image'
342 $module->getHeaders( $context ),
343 'Preload two resources'
347 public static function provideGetDeprecationWarning() {
348 return [
350 null,
351 'normalModule',
352 null,
355 true,
356 'deprecatedModule',
357 'This page is using the deprecated ResourceLoader module "deprecatedModule".',
360 'Will be removed tomorrow.',
361 'deprecatedTomorrow',
362 "This page is using the deprecated ResourceLoader module \"deprecatedTomorrow\".\n" .
363 "Will be removed tomorrow.",
369 * @dataProvider provideGetDeprecationWarning
371 * @param string|bool|null $deprecated
372 * @param string $name
373 * @param string $expected
375 public function testGetDeprecationWarning( $deprecated, $name, $expected ) {
376 $module = new ResourceLoaderTestModule( [ 'deprecated' => $deprecated ] );
377 $module->setName( $name );
378 $this->assertSame( $expected, $module->getDeprecationWarning() );
380 $this->hideDeprecated( 'MediaWiki\ResourceLoader\Module::getDeprecationInformation' );
381 $info = $module->getDeprecationInformation( $this->getResourceLoaderContext() );
382 if ( !$expected ) {
383 $this->assertSame( '', $info );
384 } else {
385 $this->assertSame( 'mw.log.warn(' . json_encode( $expected ) . ');', $info );