Krinkle has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/56637


Change subject: mw.loader: Fix regression that caused CSS load after scripts.
......................................................................

mw.loader: Fix regression that caused CSS load after scripts.

Follows-up 705d50c which introduced cssText buffer (yielding
one tick of the event queue to reduce the number of forced
repaints).

However since that made CSS loading asynchronoous (be it only for
a split second) it caused some nasty side-effects.

This was also reflected in the unit tests that had to resort
to doing setTimeout from within the mw.loader implemented script
to assert the styles. This is now sane again: Scripts execute
after styles are inserted.

Bug: 46401
Change-Id: I7b1562b12c8ed1a0286c19ef9db8f76870d4f69e
---
M resources/mediawiki/mediawiki.js
M tests/qunit/suites/resources/mediawiki/mediawiki.test.js
2 files changed, 113 insertions(+), 72 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/37/56637/1

diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js
index d1cb41d..433553a 100644
--- a/resources/mediawiki/mediawiki.js
+++ b/resources/mediawiki/mediawiki.js
@@ -363,7 +363,7 @@
                         *             'dependencies': ['required.foo', 
'bar.also', ...], (or) function () {}
                         *             'group': 'somegroup', (or) null,
                         *             'source': 'local', 'someforeignwiki', 
(or) null
-                        *             'state': 'registered', 'loading', 
'loaded', 'ready', 'error' or 'missing'
+                        *             'state': 'registered', 'loaded', 
'loading', 'ready', 'error' or 'missing'
                         *             'script': ...,
                         *             'style': ...,
                         *             'messages': { 'key': 'value' },
@@ -393,7 +393,9 @@
                                // Selector cache for the marker element. Use 
getMarker() to get/use the marker!
                                $marker = null,
                                // Buffer for addEmbeddedCSS.
-                               cssBuffer = '';
+                               cssBuffer = '',
+                               // Callbacks for addEmbeddedCSS.
+                               cssCallbacks = $.Callbacks();
 
                        /* Private methods */
 
@@ -470,9 +472,14 @@
                        /**
                         * @param {string} [cssText=cssBuffer] If called 
without cssText,
                         * the internal buffer will be inserted instead.
+                        * @param {Function} callback
                         */
-                       function addEmbeddedCSS( cssText ) {
+                       function addEmbeddedCSS( cssText, callback ) {
                                var $style, styleEl;
+
+                               if ( callback ) {
+                                       cssCallbacks.add( callback );
+                               }
 
                                // Yield once before inserting the <style> tag. 
There are likely
                                // more calls coming up which we can combine 
this way.
@@ -531,11 +538,14 @@
                                                } else {
                                                        styleEl.appendChild( 
document.createTextNode( String( cssText ) ) );
                                                }
+                                               cssCallbacks.fire().empty();
                                                return;
                                        }
                                }
 
                                $( addStyleTag( cssText, getMarker() ) ).data( 
'ResourceLoaderDynamicStyleTag', true );
+
+                               cssCallbacks.fire().empty();
                        }
 
                        /**
@@ -880,7 +890,8 @@
                         * @param {string} module Module name to execute
                         */
                        function execute( module ) {
-                               var key, value, media, i, urls, script, 
markModuleReady, nestedAddScript;
+                               var key, value, media, i, urls, cssHandle, 
checkCssHandles,
+                                       cssHandlesRegistered = false;
 
                                if ( registry[module] === undefined ) {
                                        throw new Error( 'Module has not been 
registered yet: ' + module );
@@ -889,7 +900,7 @@
                                } else if ( registry[module].state === 
'loading' ) {
                                        throw new Error( 'Module has not 
completed loading yet: ' + module );
                                } else if ( registry[module].state === 'ready' 
) {
-                                       throw new Error( 'Module has already 
been loaded: ' + module );
+                                       throw new Error( 'Module has already 
been executed: ' + module );
                                }
 
                                /**
@@ -906,6 +917,80 @@
                                        }
                                        el.href = url;
                                }
+
+                               function runScript() {
+                                       var script, markModuleReady, 
nestedAddScript;
+                                       try {
+                                               script = 
registry[module].script;
+                                               markModuleReady = function () {
+                                                       registry[module].state 
= 'ready';
+                                                       handlePending( module );
+                                               };
+                                               nestedAddScript = function ( 
arr, callback, async, i ) {
+                                                       // Recursively call 
addScript() in its own callback
+                                                       // for each element of 
arr.
+                                                       if ( i >= arr.length ) {
+                                                               // We're at the 
end of the array
+                                                               callback();
+                                                               return;
+                                                       }
+
+                                                       addScript( arr[i], 
function () {
+                                                               
nestedAddScript( arr, callback, async, i + 1 );
+                                                       }, async );
+                                               };
+
+                                               if ( $.isArray( script ) ) {
+                                                       nestedAddScript( 
script, markModuleReady, registry[module].async, 0 );
+                                               } else if ( $.isFunction( 
script ) ) {
+                                                       registry[module].state 
= 'ready';
+                                                       script( $ );
+                                                       handlePending( module );
+                                               }
+                                       } catch ( e ) {
+                                               // This needs to NOT use mw.log 
because these errors are common in production mode
+                                               // and not in debug mode, such 
as when a symbol that should be global isn't exported
+                                               log( 'Exception thrown by ' + 
module + ': ' + e.message, e );
+                                               registry[module].state = 
'error';
+                                               handlePending( module );
+                                       }
+                               }
+
+                               // This used to be inside runScript, but since 
that is now fired asychronously
+                               // (after CSS is loaded) we need to set it here 
right away. It is crucial that
+                               // when execute() is called this is set 
synchronously, otherwise modules will get
+                               // executed multiple times as the registry will 
state that it isn't loading yet.
+                               registry[module].state = 'loading';
+
+                               // Add localizations to message system
+                               if ( $.isPlainObject( registry[module].messages 
) ) {
+                                       mw.messages.set( 
registry[module].messages );
+                               }
+
+                               // Make sure we don't run the scripts until all 
(potentially asynchronous)
+                               // stylesheet insertions have completed.
+                               ( function () {
+                                       var pending = 0;
+                                       checkCssHandles = function () {
+                                               // cssHandlesRegistered ensures 
we don't take off too soon, e.g. when
+                                               // one of the cssHandles is 
fired while we're still creating more handles.
+                                               if ( cssHandlesRegistered && 
pending === 0 && runScript ) {
+                                                       runScript();
+                                                       runScript = undefined; 
// Revoke
+                                               }
+                                       };
+                                       cssHandle = function () {
+                                               var check = checkCssHandles;
+                                               pending++;
+                                               return function () {
+                                                       if (check) {
+                                                               pending--;
+                                                               check();
+                                                               check = 
undefined; // Revoke
+                                                       }
+                                               };
+                                       };
+                               }() );
 
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
@@ -925,7 +1010,7 @@
                                                                // Strings are 
pre-wrapped in "@media". The media-type was just ""
                                                                // (because it 
had to be set to something).
                                                                // This is one 
of the reasons why this format is no longer used.
-                                                               addEmbeddedCSS( 
value );
+                                                               addEmbeddedCSS( 
value, cssHandle() );
                                                        } else {
                                                                // back-compat: 
{ <media>: [url, ..] }
                                                                media = key;
@@ -942,7 +1027,7 @@
                                                                        
addLink( media, value[i] );
                                                                } else if ( key 
=== 'css' ) {
                                                                        // { 
"css": [css, ..] }
-                                                                       
addEmbeddedCSS( value[i] );
+                                                                       
addEmbeddedCSS( value[i], cssHandle() );
                                                                }
                                                        }
                                                // Not an array, but a regular 
object
@@ -959,47 +1044,9 @@
                                        }
                                }
 
-                               // Add localizations to message system
-                               if ( $.isPlainObject( registry[module].messages 
) ) {
-                                       mw.messages.set( 
registry[module].messages );
-                               }
-
-                               // Execute script
-                               try {
-                                       script = registry[module].script;
-                                       markModuleReady = function () {
-                                               registry[module].state = 
'ready';
-                                               handlePending( module );
-                                       };
-                                       nestedAddScript = function ( arr, 
callback, async, i ) {
-                                               // Recursively call addScript() 
in its own callback
-                                               // for each element of arr.
-                                               if ( i >= arr.length ) {
-                                                       // We're at the end of 
the array
-                                                       callback();
-                                                       return;
-                                               }
-
-                                               addScript( arr[i], function () {
-                                                       nestedAddScript( arr, 
callback, async, i + 1 );
-                                               }, async );
-                                       };
-
-                                       if ( $.isArray( script ) ) {
-                                               registry[module].state = 
'loading';
-                                               nestedAddScript( script, 
markModuleReady, registry[module].async, 0 );
-                                       } else if ( $.isFunction( script ) ) {
-                                               registry[module].state = 
'ready';
-                                               script( $ );
-                                               handlePending( module );
-                                       }
-                               } catch ( e ) {
-                                       // This needs to NOT use mw.log because 
these errors are common in production mode
-                                       // and not in debug mode, such as when 
a symbol that should be global isn't exported
-                                       log( 'Exception thrown by ' + module + 
': ' + e.message, e );
-                                       registry[module].state = 'error';
-                                       handlePending( module );
-                               }
+                               // Kick off.
+                               cssHandlesRegistered = true;
+                               checkCssHandles();
                        }
 
                        /**
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js 
b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
index e8663f8..01e78f6 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
@@ -250,7 +250,7 @@
        function assertStyleAsync( assert, $element, prop, val, fn ) {
                var styleTestStart,
                        el = $element.get( 0 ),
-                       styleTestTimeout = ( QUnit.config.testTimeout - 200 ) 
|| 5000;
+                       styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) 
- 200;
 
                function isCssImportApplied() {
                        // Trigger reflow, repaint, redraw, whatever 
(cross-browser)
@@ -321,7 +321,7 @@
                } );
        } );
 
-       QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, 
function ( assert ) {
+       QUnit.asyncTest( 'mw.loader.implement( styles={ "css": [text, ..] } )', 
2, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-a"></div>' 
).appendTo( '#qunit-fixture' );
 
                assert.notEqual(
@@ -333,15 +333,12 @@
                mw.loader.implement(
                        'test.implement.a',
                        function () {
-                               QUnit.stop();
-                               setTimeout(function () {
-                                       assert.equal(
-                                               $element.css( 'float' ),
-                                               'right',
-                                               'style is applied'
-                                       );
-                                       QUnit.start();
-                               });
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                               QUnit.start();
                        },
                        {
                                'all': '.mw-test-implement-a { float: right; }'
@@ -417,8 +414,8 @@
                ] );
        } );
 
-// Backwards compatibility
-       QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) 
(back-compat)', 2, function ( assert ) {
+       // Backwards compatibility
+       QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: text } ) 
(back-compat)', 2, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-c"></div>' 
).appendTo( '#qunit-fixture' );
 
                assert.notEqual(
@@ -430,15 +427,12 @@
                mw.loader.implement(
                        'test.implement.c',
                        function () {
-                               QUnit.stop();
-                               setTimeout(function () {
-                                       assert.equal(
-                                               $element.css( 'float' ),
-                                               'right',
-                                               'style is applied'
-                                       );
-                                       QUnit.start();
-                               });
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                               QUnit.start();
                        },
                        {
                                'all': '.mw-test-implement-c { float: right; }'
@@ -451,7 +445,7 @@
                ] );
        } );
 
-// Backwards compatibility
+       // Backwards compatibility
        QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: [url, ..] } ) 
(back-compat)', 4, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-d"></div>' 
).appendTo( '#qunit-fixture' ),
                        $element2 = $( '<div 
class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' );
@@ -489,7 +483,7 @@
                ] );
        } );
 
-// @import (bug 31676)
+       // @import (bug 31676)
        QUnit.asyncTest( 'mw.loader.implement( styles has @import)', 5, 
function ( assert ) {
                var isJsExecuted, $element;
 

-- 
To view, visit https://gerrit.wikimedia.org/r/56637
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I7b1562b12c8ed1a0286c19ef9db8f76870d4f69e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to