Trevor Parscal has uploaded a new change for review.

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

Change subject: [WIP] Window refactor, introduction of Processes
......................................................................

[WIP] Window refactor, introduction of Processes

Processes collect and execute lists of functions. If a function returns boolean 
false, the process is halted. If the function returns a promise, the process 
waits for the promise. If the promise is rejected, the process is halted. If 
the promise is resolved the promise continues.

Changes
* Introduce OO.ui.Process
* Convert window and dialog to use processes
* Convert frame to return promises instead of taking callbacks
* Using super everywhere

Change-Id: I5f97185296e2c9a2d5fcdd9f2f7975e24995e49a
---
M build/modules.json
M src/Dialog.js
M src/Frame.js
A src/Process.js
M src/Window.js
M src/layouts/StackLayout.js
M src/toolgroups/PopupToolGroup.js
M src/widgets/ButtonOptionWidget.js
M src/widgets/GroupWidget.js
M src/widgets/InputWidget.js
M src/widgets/ItemWidget.js
M src/widgets/MenuWidget.js
M src/widgets/PopupButtonWidget.js
M src/widgets/SelectWidget.js
M src/widgets/TextInputMenuWidget.js
M src/widgets/TextInputWidget.js
M src/widgets/ToggleButtonWidget.js
A test/OO.ui.Process.test.js
M test/index.html
19 files changed, 466 insertions(+), 169 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/oojs/ui refs/changes/93/133193/1

diff --git a/build/modules.json b/build/modules.json
index 96739a8..2b083b5 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -10,6 +10,7 @@
                        "src/Dialog.js",
                        "src/Layout.js",
                        "src/Widget.js",
+                       "src/Process.js",
                        "src/elements/ButtonedElement.js",
                        "src/elements/ClippableElement.js",
                        "src/elements/FlaggableElement.js",
diff --git a/src/Dialog.js b/src/Dialog.js
index 3f4e116..2c19466 100644
--- a/src/Dialog.js
+++ b/src/Dialog.js
@@ -27,7 +27,7 @@
 
        // Events
        this.$element.on( 'mousedown', false );
-       this.connect( this, { 'opening': 'onOpening' } );
+       this.connect( this, { 'open': 'onOpen' } );
 
        // Initialization
        this.$element.addClass( 'oo-ui-dialog' );
@@ -113,8 +113,10 @@
        }
 };
 
-/** */
-OO.ui.Dialog.prototype.onOpening = function () {
+/**
+ * Handle window open events.
+ */
+OO.ui.Dialog.prototype.onOpen = function () {
        this.$element.addClass( 'oo-ui-dialog-open' );
 };
 
@@ -146,7 +148,7 @@
  */
 OO.ui.Dialog.prototype.initialize = function () {
        // Parent method
-       OO.ui.Window.prototype.initialize.call( this );
+       OO.ui.Dialog.super.prototype.initialize.call( this );
 
        // Properties
        this.closeButton = new OO.ui.ButtonWidget( {
@@ -172,41 +174,29 @@
 /**
  * @inheritdoc
  */
-OO.ui.Dialog.prototype.setup = function ( data ) {
-       // Parent method
-       OO.ui.Window.prototype.setup.call( this, data );
-
-       // Prevent scrolling in top-level window
-       this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
-       this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
+OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
+       return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
+               .next( function () {
+                       // Prevent scrolling in top-level window
+                       this.$( window ).on( 'mousewheel', 
this.onWindowMouseWheelHandler );
+                       this.$( document ).on( 'keydown', 
this.onDocumentKeyDownHandler );
+               }, this );
 };
 
 /**
  * @inheritdoc
  */
-OO.ui.Dialog.prototype.teardown = function ( data ) {
-       // Parent method
-       OO.ui.Window.prototype.teardown.call( this, data );
-
-       // Allow scrolling in top-level window
-       this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
-       this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.Dialog.prototype.close = function ( data ) {
-       var dialog = this;
-       if ( !dialog.opening && !dialog.closing && dialog.visible ) {
-               // Trigger transition
-               dialog.$element.removeClass( 'oo-ui-dialog-open' );
-               // Allow transition to complete before actually closing
-               setTimeout( function () {
-                       // Parent method
-                       OO.ui.Window.prototype.close.call( dialog, data );
-               }, 250 );
-       }
+OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
+       return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data 
)
+               .first( function () {
+                       this.$element.removeClass( 'oo-ui-dialog-open' );
+                       return OO.ui.Process.static.delay( 250 );
+               }, this )
+               .next( function () {
+                       // Allow scrolling in top-level window
+                       this.$( window ).off( 'mousewheel', 
this.onWindowMouseWheelHandler );
+                       this.$( document ).off( 'keydown', 
this.onDocumentKeyDownHandler );
+               }, this );
 };
 
 /**
diff --git a/src/Frame.js b/src/Frame.js
index 798ef35..9f3535e 100644
--- a/src/Frame.js
+++ b/src/Frame.js
@@ -16,7 +16,7 @@
        OO.EventEmitter.call( this );
 
        // Properties
-       this.loading = false;
+       this.loading = null;
        this.loaded = false;
        this.config = config;
 
@@ -53,10 +53,10 @@
  *
  * This loops over the style sheets in the parent document, and copies their 
nodes to the
  * frame's document. It then polls the document to see when all styles have 
loaded, and once they
- * have, invokes the callback.
+ * have, resolves the promise.
  *
  * If the styles still haven't loaded after a long time (5 seconds by 
default), we give up waiting
- * and invoke the callback anyway. This protects against cases like a display: 
none; iframe in
+ * and resolve the promise anyway. This protects against cases like a display: 
none; iframe in
  * Firefox, where the styles won't load until the iframe becomes visible.
  *
  * For details of how we arrived at the strategy used in this function, see 
#load.
@@ -65,18 +65,19 @@
  * @inheritable
  * @param {HTMLDocument} parentDoc Document to transplant styles from
  * @param {HTMLDocument} frameDoc Document to transplant styles to
- * @param {Function} [callback] Callback to execute once styles have loaded
  * @param {number} [timeout=5000] How long to wait before giving up (in ms). 
If 0, never give up.
+ * @return {jQuery.Promise} Promise resolved when styles have loaded
  */
-OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, 
callback, timeout ) {
+OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout 
) {
        var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, 
$pendingPollNodes,
                $pollNodes = $( [] ),
                // Fake font-family value
-               fontFamily = 'oo-ui-frame-transplantStyles-loaded';
+               fontFamily = 'oo-ui-frame-transplantStyles-loaded',
+               deferred = $.Deferred();
 
        for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; 
i++ ) {
                styleNode = parentDoc.styleSheets[i].ownerNode;
-               if ( callback && styleNode.nodeName.toLowerCase() === 'link' ) {
+               if ( styleNode.nodeName.toLowerCase() === 'link' ) {
                        // External stylesheet
                        // Create a node with a unique ID that we're going to 
monitor to see when the CSS
                        // has loaded
@@ -98,40 +99,40 @@
                frameDoc.head.appendChild( newNode );
        }
 
-       if ( callback ) {
-               // Poll every 100ms until all external stylesheets have loaded
-               $pendingPollNodes = $pollNodes;
-               timeoutID = setTimeout( function pollExternalStylesheets() {
-                       while (
-                               $pendingPollNodes.length > 0 &&
-                               $pendingPollNodes.eq( 0 ).css( 'font-family' ) 
=== fontFamily
-                       ) {
-                               $pendingPollNodes = $pendingPollNodes.slice( 1 
);
-                       }
-
-                       if ( $pendingPollNodes.length === 0 ) {
-                               // We're done!
-                               if ( timeoutID !== null ) {
-                                       timeoutID = null;
-                                       $pollNodes.remove();
-                                       callback();
-                               }
-                       } else {
-                               timeoutID = setTimeout( 
pollExternalStylesheets, 100 );
-                       }
-               }, 100 );
-               // ...but give up after a while
-               if ( timeout !== 0 ) {
-                       setTimeout( function () {
-                               if ( timeoutID ) {
-                                       clearTimeout( timeoutID );
-                                       timeoutID = null;
-                                       $pollNodes.remove();
-                                       callback();
-                               }
-                       }, timeout || 5000 );
+       // Poll every 100ms until all external stylesheets have loaded
+       $pendingPollNodes = $pollNodes;
+       timeoutID = setTimeout( function pollExternalStylesheets() {
+               while (
+                       $pendingPollNodes.length > 0 &&
+                       $pendingPollNodes.eq( 0 ).css( 'font-family' ) === 
fontFamily
+               ) {
+                       $pendingPollNodes = $pendingPollNodes.slice( 1 );
                }
+
+               if ( $pendingPollNodes.length === 0 ) {
+                       // We're done!
+                       if ( timeoutID !== null ) {
+                               timeoutID = null;
+                               $pollNodes.remove();
+                               deferred.resolve();
+                       }
+               } else {
+                       timeoutID = setTimeout( pollExternalStylesheets, 100 );
+               }
+       }, 100 );
+       // ...but give up after a while
+       if ( timeout !== 0 ) {
+               setTimeout( function () {
+                       if ( timeoutID ) {
+                               clearTimeout( timeoutID );
+                               timeoutID = null;
+                               $pollNodes.remove();
+                               deferred.reject();
+                       }
+               }, timeout || 5000 );
        }
+
+       return deferred.promise();
 };
 
 /* Methods */
@@ -139,7 +140,10 @@
 /**
  * Load the frame contents.
  *
- * Once the iframe's stylesheets are loaded, the `initialize` event will be 
emitted.
+ * Once the iframe's stylesheets are loaded, the `load` event will be emitted 
and the returned
+ * promise will be resolved. Calling while loading will return a promise but 
not trigger a new
+ * loading cycle. Calling after loading is complete will return a promise 
that's already been
+ * resolved.
  *
  * Sounds simple right? Read on...
  *
@@ -167,15 +171,27 @@
  *
  * All this stylesheet injection and polling magic is in #transplantStyles.
  *
- * @private
+ * @return {jQuery.promise} Promise resolved when loading is complete
  * @fires load
  */
 OO.ui.Frame.prototype.load = function () {
-       var win = this.$element.prop( 'contentWindow' ),
-               doc = win.document,
-               frame = this;
+       var win, doc;
 
-       this.loading = true;
+       // Return another promise if already loading
+       if ( this.loading ) {
+               return this.loading.promise();
+       }
+
+       // Return resolved promise if already loaded
+       if ( this.loaded ) {
+               return $.Deferred().resolve().promise();
+       }
+
+       // Load the frame
+       this.loading = $.Deferred();
+
+       win = this.$element.prop( 'contentWindow' );
+       doc = win.document;
 
        // Figure out directionality:
        this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr';
@@ -197,37 +213,16 @@
        this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 );
        this.$document = this.$( doc );
 
-       this.constructor.static.transplantStyles(
-               this.getElementDocument(),
-               this.$document[0],
-               function () {
-                       frame.loading = false;
-                       frame.loaded = true;
-                       frame.emit( 'load' );
-               }
-       );
-};
+       // Initialization
+       this.constructor.static.transplantStyles( this.getElementDocument(), 
this.$document[0] )
+               .always( OO.ui.bind( function () {
+                       this.emit( 'load' );
+                       this.loading.resolve();
+                       this.loading = null;
+                       this.loaded = true;
+               }, this ) );
 
-/**
- * Run a callback as soon as the frame has been loaded.
- *
- *
- * This will start loading if it hasn't already, and runs
- * immediately if the frame is already loaded.
- *
- * Don't call this until the element is attached.
- *
- * @param {Function} callback
- */
-OO.ui.Frame.prototype.run = function ( callback ) {
-       if ( this.loaded ) {
-               callback();
-       } else {
-               if ( !this.loading ) {
-                       this.load();
-               }
-               this.once( 'load', callback );
-       }
+       return this.loading.promise();
 };
 
 /**
diff --git a/src/Process.js b/src/Process.js
new file mode 100644
index 0000000..8d400be
--- /dev/null
+++ b/src/Process.js
@@ -0,0 +1,113 @@
+/**
+ * A list of functions, called in sequence.
+ *
+ * If a function returns a promise, the next step in the process will not be 
taken until the promise
+ * is resolved.
+ *
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.Process = function () {
+       // Properties
+       this.steps = [];
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Process );
+
+/* Static Methods */
+
+/**
+ * Generate a promise which is resolved after a set amount of time.
+ *
+ * @param {number} length Number of milliseconds before resolving the promise
+ * @return {jQuery.Promise} Promise that will be resolved after a set amount 
of time
+ */
+OO.ui.Process.static.delay = function ( length ) {
+       var deferred = $.Deferred();
+
+       setTimeout( function () {
+               deferred.resolve();
+       }, length );
+
+       return deferred.promise();
+};
+
+/* Methods */
+
+/**
+ * Start the process.
+ *
+ * @return {jQuery.Promise} Promise that is resolved when all steps have 
completed or rejected when
+ *   any of the steps return boolean false or a promise which gets rejected
+ */
+OO.ui.Process.prototype.execute = function () {
+       var i, len, promise;
+
+       /**
+        * Continue execution.
+        *
+        * @param {Array} step A function and the context it should be called in
+        * @return {Function} Function that continues the process
+        */
+       function proceed( step ) {
+               return function () {
+                       // Execute step in the correct context
+                       var result = step[0].call( step[1] );
+
+                       if ( result === false ) {
+                               // Use rejected promise for boolean false 
results
+                               return $.Deferred().reject().promise();
+                       } else if ( typeof result !== 'object' || 
!$.isFunction( result.promise ) ) {
+                               // Use resolved promise for other non-promise 
results
+                               return $.Deferred().resolve().promise();
+                       } else {
+                               // Use returned promise
+                               return result;
+                       }
+               };
+       }
+
+       if ( this.steps.length ) {
+               // Generate a chain reaction of promises
+               promise = proceed( this.steps[0] )();
+               for ( i = 1, len = this.steps.length; i < len; i++ ) {
+                       promise = promise
+                               .then( proceed( this.steps[i] ) );
+               }
+       } else {
+               promise = $.Deferred().resolve();
+       }
+
+       return promise;
+};
+
+/**
+ * Add step to the beginning of the process.
+ *
+ * @param {Function} step Function to execute; if it returns boolean false the 
process will stop; if
+ *   it returns a promise the process will continue to the next step when the 
promise is resolved or
+ *   will stop when the promise is rejected
+ * @param {Object} [context=null] Context to call the step function in
+ * @chainable
+ */
+OO.ui.Process.prototype.first = function ( step, context ) {
+       this.steps.unshift( [ step, context || null ] );
+       return this;
+};
+
+/**
+ * Add step to the end of the process.
+ *
+ * @param {Function} step Function to execute; if it returns boolean false the 
process will stop; if
+ *   it returns a promise the process will continue to the next step when the 
promise is resolved or
+ *   will stop when the promise is rejected
+ * @param {Object} [context=null] Context to call the step function in
+ * @chainable
+ */
+OO.ui.Process.prototype.next = function ( step, context ) {
+       this.steps.push( [ step, context || null ] );
+       return this;
+};
diff --git a/src/Window.js b/src/Window.js
index c6b7276..8b025ea 100644
--- a/src/Window.js
+++ b/src/Window.js
@@ -57,14 +57,6 @@
 /* Events */
 
 /**
- * Initialize contents.
- *
- * Fired asynchronously after construction when iframe is ready.
- *
- * @event initialize
- */
-
-/**
  * Open window.
  *
  * Fired after window has been opened.
@@ -268,7 +260,6 @@
  *
  * Once this method is called, this.$$ can be used to create elements within 
the frame.
  *
- * @fires initialize
  * @chainable
  */
 OO.ui.Window.prototype.initialize = function () {
@@ -295,97 +286,128 @@
        // We can do this safely now that the iframe has initialized
        this.$element.hide().css( 'visibility', '' );
 
-       this.emit( 'initialize' );
-
        return this;
 };
 
 /**
- * Setup window for use.
+ * Get a process for setting up a window for use.
  *
- * Each time the window is opened, once it's ready to be interacted with, this 
will set it up for
- * use in a particular context, based on the `data` argument.
+ * Each time the window is opened this process will set it up for use in a 
particular context, based
+ * on the `data` argument.
  *
- * When you override this method, you must call the parent method at the very 
beginning.
+ * When you override this method, you can add additional setup steps to the 
process the parent
+ * method provides using the 'first' and 'then' methods.
  *
  * @abstract
  * @param {Object} [data] Window opening data
+ * @return {OO.ui.Process} Setup process
  */
-OO.ui.Window.prototype.setup = function () {
-       // Override to do something
+OO.ui.Window.prototype.getSetupProcess = function () {
+       return new OO.ui.Process();
 };
 
 /**
- * Tear down window after use.
+ * Get a process for readying a window for use.
  *
- * Each time the window is closed, and it's done being interacted with, this 
will tear it down and
- * do something with the user's interactions within the window, based on the 
`data` argument.
+ * Each time the window is open and setup, this process will ready it up for 
use in a particular
+ * context, based on the `data` argument.
  *
- * When you override this method, you must call the parent method at the very 
end.
+ * When you override this method, you can add additional setup steps to the 
process the parent
+ * method provides using the 'first' and 'then' methods.
+ *
+ * @abstract
+ * @param {Object} [data] Window opening data
+ * @return {OO.ui.Process} Setup process
+ */
+OO.ui.Window.prototype.getReadyProcess = function () {
+       return new OO.ui.Process();
+};
+
+/**
+ * Get a process for tearing down a window after use.
+ *
+ * Each time the window is closed this process will tear it down and do 
something with the user's
+ * interactions within the window, based on the `data` argument.
+ *
+ * When you override this method, you can add additional teardown steps to the 
process the parent
+ * method provides using the 'first' and 'then' methods.
  *
  * @abstract
  * @param {Object} [data] Window closing data
+ * @return {OO.ui.Process} Teardown process
  */
-OO.ui.Window.prototype.teardown = function () {
-       // Override to do something
+OO.ui.Window.prototype.getTeardownProcess = function () {
+       return new OO.ui.Process();
 };
 
 /**
  * Open window.
  *
- * Do not override this method. See #setup for a way to make changes each time 
the window opens.
+ * Do not override this method. Use #geSetupProcess to do something each time 
the window closes.
  *
  * @param {Object} [data] Window opening data
+ * @fires initialize
  * @fires opening
  * @fires open
  * @fires ready
- * @chainable
+ * @return {jQuery.Promise} Promise resolved when window is opened
  */
 OO.ui.Window.prototype.open = function ( data ) {
+       var deferred = $.Deferred();
+
        if ( !this.opening && !this.closing && !this.visible ) {
                this.opening = true;
-               this.frame.run( OO.ui.bind( function () {
+               this.frame.load().done( OO.ui.bind( function () {
                        this.$element.show();
                        this.visible = true;
                        this.emit( 'opening', data );
-                       this.setup( data );
-                       this.emit( 'open', data );
-                       setTimeout( OO.ui.bind( function () {
-                               // Focus the content div (which has a tabIndex) 
to inactivate
-                               // (but not clear) selections in the parent 
frame.
-                               // Must happen after 'open' is emitted (to 
ensure it is visible)
-                               // but before 'ready' is emitted (so subclasses 
can give focus to something else)
-                               this.frame.$content.focus();
-                               this.emit( 'ready', data );
-                               this.opening = false;
+                       this.getSetupProcess( data ).execute().done( 
OO.ui.bind( function () {
+                               this.emit( 'open', data );
+                               setTimeout( OO.ui.bind( function () {
+                                       // Focus the content div (which has a 
tabIndex) to inactivate
+                                       // (but not clear) selections in the 
parent frame.
+                                       // Must happen after 'open' is emitted 
(to ensure it is visible)
+                                       // but before 'ready' is emitted (so 
subclasses can give focus to something
+                                       // else)
+                                       this.frame.$content.focus();
+                                       this.getReadyProcess( data 
).execute().done( OO.ui.bind( function () {
+                                               this.emit( 'ready', data );
+                                               this.opening = false;
+                                               deferred.resolve();
+                                       }, this ) );
+                               }, this ) );
                        }, this ) );
                }, this ) );
        }
 
-       return this;
+       return deferred;
 };
 
 /**
  * Close window.
  *
- * See #teardown for a way to do something each time the window closes.
+ * Do not override this method. Use #getTeardownProcess to do something each 
time the window closes.
  *
  * @param {Object} [data] Window closing data
  * @fires closing
  * @fires close
- * @chainable
+ * @return {jQuery.Promise} Promise resolved when window is closed
  */
 OO.ui.Window.prototype.close = function ( data ) {
+       var deferred = $.Deferred();
+
        if ( !this.opening && !this.closing && this.visible ) {
                this.frame.$content.find( ':focus' ).blur();
                this.closing = true;
-               this.$element.hide();
-               this.visible = false;
                this.emit( 'closing', data );
-               this.teardown( data );
-               this.emit( 'close', data );
-               this.closing = false;
+               this.getTeardownProcess( data ).execute().done( OO.ui.bind( 
function () {
+                       this.emit( 'close', data );
+                       this.$element.hide();
+                       this.visible = false;
+                       this.closing = false;
+                       deferred.resolve();
+               }, this ) );
        }
 
-       return this;
+       return deferred;
 };
diff --git a/src/layouts/StackLayout.js b/src/layouts/StackLayout.js
index 9a73bb6..03fc992 100644
--- a/src/layouts/StackLayout.js
+++ b/src/layouts/StackLayout.js
@@ -68,6 +68,7 @@
  * @chainable
  */
 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
+       // Mixin method
        OO.ui.GroupElement.prototype.addItems.call( this, items, index );
 
        if ( !this.currentItem && items.length ) {
@@ -86,6 +87,7 @@
  * @chainable
  */
 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
+       // Mixin method
        OO.ui.GroupElement.prototype.removeItems.call( this, items );
        if ( $.inArray( this.currentItem, items  ) !== -1 ) {
                this.currentItem = null;
@@ -106,6 +108,8 @@
  */
 OO.ui.StackLayout.prototype.clearItems = function () {
        this.currentItem = null;
+
+       // Mixin method
        OO.ui.GroupElement.prototype.clearItems.call( this );
 
        return this;
diff --git a/src/toolgroups/PopupToolGroup.js b/src/toolgroups/PopupToolGroup.js
index cf72bda..70a651e 100644
--- a/src/toolgroups/PopupToolGroup.js
+++ b/src/toolgroups/PopupToolGroup.js
@@ -95,7 +95,7 @@
        if ( !this.isDisabled() && e.which === 1 ) {
                this.setActive( false );
        }
-       return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e );
+       return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
 };
 
 /**
diff --git a/src/widgets/ButtonOptionWidget.js 
b/src/widgets/ButtonOptionWidget.js
index e949e08..936c01a 100644
--- a/src/widgets/ButtonOptionWidget.js
+++ b/src/widgets/ButtonOptionWidget.js
@@ -38,7 +38,7 @@
  * @inheritdoc
  */
 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
-       OO.ui.OptionWidget.prototype.setSelected.call( this, state );
+       OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state 
);
 
        this.setActive( state );
 
diff --git a/src/widgets/GroupWidget.js b/src/widgets/GroupWidget.js
index b3ff6d9..51eeed5 100644
--- a/src/widgets/GroupWidget.js
+++ b/src/widgets/GroupWidget.js
@@ -36,8 +36,8 @@
        var i, len;
 
        // Parent method
-       // Note this is calling OO.ui.Widget; we're assuming the class this is 
mixed into
-       // is a subclass of OO.ui.Widget.
+       // Note: We are calling this constructor assuming this class is mixed 
into a subclass of
+       // OO.ui.Widget
        OO.ui.Widget.prototype.setDisabled.call( this, disabled );
 
        // During construction, #setDisabled is called before the 
OO.ui.GroupElement constructor
diff --git a/src/widgets/InputWidget.js b/src/widgets/InputWidget.js
index aef779f..253390f 100644
--- a/src/widgets/InputWidget.js
+++ b/src/widgets/InputWidget.js
@@ -177,7 +177,7 @@
  * @inheritdoc
  */
 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
-       OO.ui.Widget.prototype.setDisabled.call( this, state );
+       OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
        if ( this.$input ) {
                this.$input.prop( 'disabled', this.isDisabled() );
        }
diff --git a/src/widgets/ItemWidget.js b/src/widgets/ItemWidget.js
index 220af27..73c44d3 100644
--- a/src/widgets/ItemWidget.js
+++ b/src/widgets/ItemWidget.js
@@ -34,6 +34,8 @@
  */
 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
        // Parent method
+       // Note: We are calling this constructor assuming this class is mixed 
into a subclass of
+       // OO.ui.Element
        OO.ui.Element.prototype.setElementGroup.call( this, group );
 
        // Initialize item disabled states
diff --git a/src/widgets/MenuWidget.js b/src/widgets/MenuWidget.js
index 86b10aa..c2a1739 100644
--- a/src/widgets/MenuWidget.js
+++ b/src/widgets/MenuWidget.js
@@ -160,7 +160,7 @@
        var i, len, item;
 
        // Parent method
-       OO.ui.SelectWidget.prototype.addItems.call( this, items, index );
+       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
 
        // Auto-initialize
        if ( !this.newItems ) {
diff --git a/src/widgets/PopupButtonWidget.js b/src/widgets/PopupButtonWidget.js
index f544a06..77ddfc7 100644
--- a/src/widgets/PopupButtonWidget.js
+++ b/src/widgets/PopupButtonWidget.js
@@ -45,7 +45,7 @@
                } else {
                        this.showPopup();
                }
-               OO.ui.ButtonWidget.prototype.onClick.call( this );
+               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
        }
        return false;
 };
diff --git a/src/widgets/SelectWidget.js b/src/widgets/SelectWidget.js
index e83b836..dda03cc 100644
--- a/src/widgets/SelectWidget.js
+++ b/src/widgets/SelectWidget.js
@@ -436,7 +436,8 @@
                this.removeItems( remove );
        }
 
-       OO.ui.GroupElement.prototype.addItems.call( this, items, index );
+       // Mixin method
+       OO.ui.GroupWidget.super.prototype.addItems.call( this, items, index );
 
        // Always provide an index, even if it was omitted
        this.emit( 'add', items, index === undefined ? this.items.length - 
items.length - 1 : index );
@@ -467,7 +468,9 @@
                        this.selectItem( null );
                }
        }
-       OO.ui.GroupElement.prototype.removeItems.call( this, items );
+
+       // Mixin method
+       OO.ui.GroupWidget.super.prototype.removeItems.call( this, items );
 
        this.emit( 'remove', items );
 
@@ -487,7 +490,8 @@
 
        // Clear all items
        this.hashes = {};
-       OO.ui.GroupElement.prototype.clearItems.call( this );
+       // Mixin method
+       OO.ui.GroupWidget.super.prototype.clearItems.call( this );
        this.selectItem( null );
 
        this.emit( 'remove', items );
diff --git a/src/widgets/TextInputMenuWidget.js 
b/src/widgets/TextInputMenuWidget.js
index 4558753..9b80cb6 100644
--- a/src/widgets/TextInputMenuWidget.js
+++ b/src/widgets/TextInputMenuWidget.js
@@ -44,7 +44,7 @@
  */
 OO.ui.TextInputMenuWidget.prototype.show = function () {
        // Parent method
-       OO.ui.MenuWidget.prototype.show.call( this );
+       OO.ui.TextInputMenuWidget.super.prototype.show.call( this );
 
        this.position();
        this.$( this.getElementWindow() ).on( 'resize', 
this.onWindowResizeHandler );
@@ -58,7 +58,7 @@
  */
 OO.ui.TextInputMenuWidget.prototype.hide = function () {
        // Parent method
-       OO.ui.MenuWidget.prototype.hide.call( this );
+       OO.ui.TextInputMenuWidget.super.prototype.hide.call( this );
 
        this.$( this.getElementWindow() ).off( 'resize', 
this.onWindowResizeHandler );
        return this;
diff --git a/src/widgets/TextInputWidget.js b/src/widgets/TextInputWidget.js
index 8053245..c6eb5dd 100644
--- a/src/widgets/TextInputWidget.js
+++ b/src/widgets/TextInputWidget.js
@@ -90,7 +90,7 @@
        this.adjustSize();
 
        // Parent method
-       return OO.ui.InputWidget.prototype.onEdit.call( this );
+       return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
 };
 
 /**
diff --git a/src/widgets/ToggleButtonWidget.js 
b/src/widgets/ToggleButtonWidget.js
index ee2fc8e..8bb2519 100644
--- a/src/widgets/ToggleButtonWidget.js
+++ b/src/widgets/ToggleButtonWidget.js
@@ -39,7 +39,7 @@
        }
 
        // Parent method
-       return OO.ui.ButtonWidget.prototype.onClick.call( this );
+       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
 };
 
 /**
@@ -52,7 +52,7 @@
        }
 
        // Parent method
-       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
+       OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value );
 
        return this;
 };
diff --git a/test/OO.ui.Process.test.js b/test/OO.ui.Process.test.js
new file mode 100644
index 0000000..63330ae
--- /dev/null
+++ b/test/OO.ui.Process.test.js
@@ -0,0 +1,165 @@
+QUnit.module( 'OO.ui.Process' );
+
+/* Tests */
+
+QUnit.test( 'next', 1, function ( assert ) {
+       var process = new OO.ui.Process(),
+               result = [];
+
+       process
+               .next( function () {
+                       result.push( 0 );
+               } )
+               .next( function () {
+                       result.push( 1 );
+               } )
+               .next( function () {
+                       result.push( 2 );
+               } )
+               .execute();
+
+       assert.deepEqual( result, [ 0, 1, 2 ], 'Steps can be added at the end' 
);
+} );
+
+QUnit.test( 'first', 1, function ( assert ) {
+       var process = new OO.ui.Process(),
+               result = [];
+
+       process
+               .first( function () {
+                       result.push( 0 );
+               } )
+               .first( function () {
+                       result.push( 1 );
+               } )
+               .first( function () {
+                       result.push( 2 );
+               } )
+               .execute();
+
+       assert.deepEqual( result, [ 2, 1, 0 ], 'Steps can be added at the 
beginning' );
+} );
+
+QUnit.asyncTest( 'execute (async)', 1, function ( assert ) {
+       window.console.log( 'async test' );
+       // Async
+       var process = new OO.ui.Process(),
+               result = [];
+
+       process
+               .next( function () {
+                       var deferred = $.Deferred();
+
+                       setTimeout( function () {
+                               result.push( 1 );
+                               deferred.resolve();
+                       }, 10 );
+
+                       return deferred.promise();
+               } )
+               .first( function () {
+                       result.push( 0 );
+               } )
+               .next( function () {
+                       var deferred = $.Deferred();
+
+                       setTimeout( function () {
+                               result.push( 2 );
+                               deferred.resolve();
+                       }, 10 );
+
+                       return deferred.promise();
+               } );
+
+       process.execute().done( function () {
+               assert.deepEqual(
+                       result,
+                       [ 0, 1, 2 ],
+                       'Synchronous and asynchronous steps are executed in the 
correct order'
+               );
+               QUnit.start();
+       } );
+} );
+
+QUnit.asyncTest( 'execute (return false)', 1, function ( assert ) {
+       var process = new OO.ui.Process(),
+               result = [];
+
+       process
+               .next( function () {
+                       var deferred = $.Deferred();
+
+                       setTimeout( function () {
+                               result.push( 0 );
+                               deferred.resolve();
+                       }, 10 );
+
+                       return deferred.promise();
+               } )
+               .next( function () {
+                       result.push( 1 );
+                       return false;
+               } )
+               .next( function () {
+                       // Should never be run because previous step is rejected
+                       result.push( 2 );
+               } );
+
+       process.execute().fail( function () {
+               assert.deepEqual(
+                       result,
+                       [ 0, 1 ],
+                       'Process is stopped when a step returns false'
+               );
+               QUnit.start();
+       } );
+} );
+
+QUnit.asyncTest( 'execute (async reject)', 1, function ( assert ) {
+       var process = new OO.ui.Process(),
+               result = [];
+
+       process
+               .next( function () {
+                       result.push( 0 );
+               } )
+               .next( function () {
+                       var deferred = $.Deferred();
+
+                       setTimeout( function () {
+                               result.push( 1 );
+                               deferred.reject();
+                       }, 10 );
+
+                       return deferred.promise();
+               } )
+               .next( function () {
+                       // Should never be run because previous step is rejected
+                       result.push( 2 );
+               } );
+
+       process.execute().fail( function () {
+               assert.deepEqual(
+                       result,
+                       [ 0, 1 ],
+                       'Process is stopped when a step returns a promise that 
is then rejected'
+               );
+               QUnit.start();
+       } );
+} );
+
+QUnit.asyncTest( 'delay', 2, function ( assert ) {
+       var result = [];
+
+       OO.ui.Process.static.delay( 10 ).done( function () {
+               result.push( 0 );
+       } );
+
+       // Will still be empty because delayed promise hasn't been resolved yet
+       assert.deepEqual( result, [], 'Delayed promises take time to resolve' );
+
+       setTimeout( function () {
+               assert.deepEqual( result, [ 0 ], 'Delayed promises resolve 
after a time' );
+               QUnit.start();
+       }, 20 );
+} );
diff --git a/test/index.html b/test/index.html
index abb89d9..a0d3645 100644
--- a/test/index.html
+++ b/test/index.html
@@ -15,6 +15,7 @@
        <script src="../dist/oojs-ui.js"></script>
        <!-- Test suites -->
        <script src="./OO.ui.Element.test.js"></script>
+       <script src="./OO.ui.Process.test.js"></script>
 </head>
 <body>
        <div id="qunit"></div>

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I5f97185296e2c9a2d5fcdd9f2f7975e24995e49a
Gerrit-PatchSet: 1
Gerrit-Project: oojs/ui
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to