Merge "OutputPage: Set empty modules to state 'ready' instead of 'missing'."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 3 Apr 2013 23:49:03 +0000 (23:49 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 3 Apr 2013 23:49:03 +0000 (23:49 +0000)
22 files changed:
RELEASE-NOTES-1.22
includes/Exception.php
includes/api/ApiQueryIWLinks.php
includes/api/ApiQueryLangLinks.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/job/Job.php
includes/job/JobQueue.php
includes/job/JobQueueRedis.php
maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql [new file with mode: 0644]
maintenance/archives/patch-iwlinks-from-title-index.sql [new file with mode: 0644]
maintenance/archives/patch-kill-iwl_pft.sql [deleted file]
maintenance/postgres/archives/patch-kill-iwl_pft.sql [deleted file]
maintenance/postgres/archives/patch-rename-iwl_prefix.sql
maintenance/postgres/tables.sql
maintenance/sqlite/archives/patch-kill-iwl_pft.sql [deleted file]
maintenance/sqlite/archives/patch-rename-iwl_prefix.sql
maintenance/tables.sql
resources/jquery/jquery.makeCollapsible.js
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js [new file with mode: 0644]

index 9b562ae..4366d0d 100644 (file)
@@ -27,6 +27,8 @@ changes to languages because of Bugzilla reports.
 * redirect.php was removed. It was unused.
 * BREAKING CHANGE: Legacy skins Simple, MySkin and Standard were all removed.
   Nostalgia was moved to an extension.
+* Event namespace used by jquery.makeCollapsible has been changed from
+  'mw-collapse' to 'mw-collapsible' for consistency with the module name.
 
 == Compatibility ==
 
index dc34320..21952bb 100644 (file)
@@ -247,7 +247,7 @@ class MWException extends Exception {
         * It will be either HTML or plain text based on isCommandLine().
         */
        function report() {
-               global $wgLogExceptionBacktrace;
+               global $wgLogExceptionBacktrace, $wgMimeType;
                $log = $this->getLogMessage();
 
                if ( $log ) {
@@ -267,6 +267,7 @@ class MWException extends Exception {
                } else {
                        header( "HTTP/1.1 500 MediaWiki exception" );
                        header( "Status: 500 MediaWiki exception", true );
+                       header( "Content-Type: $wgMimeType; charset=utf-8", true );
 
                        $this->reportHTML();
                }
index fc77b4e..6f03bbe 100644 (file)
@@ -81,8 +81,8 @@ class ApiQueryIWLinks extends ApiQueryBase {
                                $this->addOption( 'ORDER BY', 'iwl_from' . $sort );
                        } else {
                                $this->addOption( 'ORDER BY', array(
-                                               'iwl_title' . $sort,
-                                               'iwl_from' . $sort
+                                               'iwl_from' . $sort,
+                                               'iwl_title' . $sort
                                ));
                        }
                } else {
@@ -92,7 +92,8 @@ class ApiQueryIWLinks extends ApiQueryBase {
                        } else {
                                $this->addOption( 'ORDER BY', array (
                                                'iwl_from' . $sort,
-                                               'iwl_prefix' . $sort
+                                               'iwl_prefix' . $sort,
+                                               'iwl_title' . $sort
                                ));
                        }
                }
index ac65d2d..e6b02d7 100644 (file)
@@ -67,18 +67,15 @@ class ApiQueryLangLinks extends ApiQueryBase {
                        );
                }
 
-                       $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
-                       if ( isset( $params['lang'] ) ) {
+               // Note that, since (ll_from, ll_lang) is a unique key, we don't need
+               // to sort by ll_title to ensure deterministic ordering.
+               $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
+               if ( isset( $params['lang'] ) ) {
                        $this->addWhereFld( 'll_lang', $params['lang'] );
                        if ( isset( $params['title'] ) ) {
                                $this->addWhereFld( 'll_title', $params['title'] );
-                               $this->addOption( 'ORDER BY', 'll_from' . $sort );
-                       } else {
-                               $this->addOption( 'ORDER BY', array(
-                                                       'll_title' . $sort,
-                                                       'll_from' . $sort
-                               ));
                        }
+                       $this->addOption( 'ORDER BY', 'll_from' . $sort );
                } else {
                        // Don't order by ll_from if it's constant in the WHERE clause
                        if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) {
index d8fa64e..6b74653 100644 (file)
@@ -179,7 +179,6 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'addField', 'updatelog',     'ul_value',         'patch-ul_value.sql' ),
                        array( 'addField', 'interwiki',     'iw_api',           'patch-iw_api_and_wikiid.sql' ),
                        array( 'dropIndex', 'iwlinks',      'iwl_prefix',       'patch-kill-iwl_prefix.sql' ),
-                       array( 'dropIndex', 'iwlinks',      'iwl_prefix_from_title', 'patch-kill-iwl_pft.sql' ),
                        array( 'addField', 'categorylinks', 'cl_collation',     'patch-categorylinks-better-collation.sql' ),
                        array( 'doClFieldsUpdate' ),
                        array( 'doCollationUpdate' ),
@@ -230,6 +229,10 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase-255.sql' ),
                        array( 'addIndex', 'page_props', 'pp_propname_page',  'patch-page_props-propname-page-index.sql' ),
                        array( 'addIndex', 'image', 'img_media_mime', 'patch-img_media_mime-index.sql' ),
+
+                       // 1.22
+                       array( 'doIwlinksIndexNonUnique' ),
+                       array( 'addIndex', 'iwlinks', 'iwl_prefix_from_title',  'patch-iwlinks-from-title-index.sql' ),
                );
        }
 
@@ -902,4 +905,18 @@ class MysqlUpdater extends DatabaseUpdater {
 
                $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql', false, "Making user_last_timestamp nullable" );
        }
+
+       protected function doIwlinksIndexNonUnique() {
+               $info = $this->db->indexInfo( 'iwlinks', 'iwl_prefix_title_from' );
+               if ( is_array( $info ) && $info[0]->Non_unique ) {
+                       $this->output( "...iwl_prefix_title_from index is already non-UNIQUE.\n" );
+                       return true;
+               }
+               if ( $this->skipSchema ) {
+                       $this->output( "...skipping schema change (making iwl_prefix_title_from index non-UNIQUE).\n" );
+                       return false;
+               }
+
+               return $this->applyPatch( 'patch-iwl_prefix_title_from-non-unique.sql', false, "Making iwl_prefix_title_from index non-UNIQUE" );
+       }
 }
index 17285c5..0a319ee 100644 (file)
@@ -232,6 +232,7 @@ class PostgresUpdater extends DatabaseUpdater {
                        array( 'addPgIndex', 'watchlist',     'wl_user',                '(wl_user)' ),
                        array( 'addPgIndex', 'logging',       'logging_user_type_time', '(log_user, log_type, log_timestamp)' ),
                        array( 'addPgIndex', 'logging',       'logging_page_id_time',   '(log_page,log_timestamp)' ),
+                       array( 'addPgIndex', 'iwlinks',       'iwl_prefix_from_title',  '(iwl_prefix, iwl_from, iwl_title)' ),
                        array( 'addPgIndex', 'iwlinks',       'iwl_prefix_title_from',  '(iwl_prefix, iwl_title, iwl_from)' ),
                        array( 'addPgIndex', 'job',           'job_timestamp_idx',      '(job_timestamp)' ),
                        array( 'addPgIndex', 'job',           'job_sha1',               '(job_sha1)' ),
@@ -251,6 +252,12 @@ class PostgresUpdater extends DatabaseUpdater {
                                array( 'cl_from', 'int4_ops', 'btree', 0 ),
                        ),
                        'CREATE INDEX cl_sortkey ON "categorylinks" USING "btree" ("cl_to", "cl_sortkey", "cl_from")' ),
+                       array( 'checkIndex', 'iwl_prefix_title_from', array(
+                               array('iwl_prefix', 'text_ops', 'btree', 0),
+                               array('iwl_title', 'text_ops', 'btree', 0),
+                               array('iwl_from', 'int4_ops', 'btree', 0),
+                       ),
+                       'CREATE INDEX iwl_prefix_title_from ON "iwlinks" USING "btree" ("iwl_prefix", "iwl_title", "iwl_from")' ),
                        array( 'checkIndex', 'logging_times', array(
                                array( 'log_timestamp', 'timestamptz_ops', 'btree', 0 ),
                        ),
@@ -770,7 +777,7 @@ END;
 
        protected function checkIwlPrefix() {
                if ( $this->db->indexExists( 'iwlinks', 'iwl_prefix' ) ) {
-                       $this->applyPatch( 'patch-rename-iwl_prefix.sql', false, "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'" );
+                       $this->applyPatch( 'patch-rename-iwl_prefix.sql', false, "Replacing index 'iwl_prefix' with 'iwl_prefix_title_from'" );
                }
        }
 
index 11e3445..54e115b 100644 (file)
@@ -62,7 +62,6 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'addField', 'updatelog', 'ul_value',             'patch-ul_value.sql' ),
                        array( 'addField', 'interwiki',     'iw_api',           'patch-iw_api_and_wikiid.sql' ),
                        array( 'dropIndex', 'iwlinks', 'iwl_prefix',            'patch-kill-iwl_prefix.sql' ),
-                       array( 'dropIndex', 'iwlinks', 'iwl_prefix_from_title', 'patch-kill-iwl_pft.sql' ),
                        array( 'addField', 'categorylinks', 'cl_collation',     'patch-categorylinks-better-collation.sql' ),
                        array( 'doCollationUpdate' ),
                        array( 'addTable', 'msg_resource',                      'patch-msg_resource.sql' ),
@@ -110,6 +109,7 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase-255.sql' ),
                        array( 'addIndex', 'page_props', 'pp_propname_page',  'patch-page_props-propname-page-index.sql' ),
                        array( 'addIndex', 'image', 'img_media_mime', 'patch-img_media_mime-index.sql' ),
+                       array( 'addIndex', 'iwlinks', 'iwl_prefix_from_title',  'patch-iwlinks-from-title-index.sql' ),
                );
        }
 
index c6787a0..64925f7 100644 (file)
@@ -202,7 +202,10 @@ abstract class Job {
        }
 
        /**
-        * Subclasses may need to override this to make duplication detection work
+        * Subclasses may need to override this to make duplication detection work.
+        * The resulting map conveys everything that makes the job unique. This is
+        * only checked if ignoreDuplicates() returns true, meaning that duplicate
+        * jobs are supposed to be ignored.
         *
         * @return Array Map of key/values
         * @since 1.21
@@ -225,6 +228,7 @@ abstract class Job {
        }
 
        /**
+        * @see JobQueue::deduplicateRootJob()
         * @param string $key A key that identifies the task
         * @return Array
         * @since 1.21
@@ -237,7 +241,9 @@ abstract class Job {
        }
 
        /**
+        * @see JobQueue::deduplicateRootJob()
         * @return Array
+        * @since 1.21
         */
        public function getRootJobParams() {
                return array(
@@ -250,6 +256,16 @@ abstract class Job {
                );
        }
 
+       /**
+        * @see JobQueue::deduplicateRootJob()
+        * @return bool
+        * @since 1.22
+        */
+       public function hasRootJobParams() {
+               return isset( $this->params['rootJobSignature'] )
+                       && isset( $this->params['rootJobTimestamp'] );
+       }
+
        /**
         * Insert a single job into the queue.
         * @return bool true on success
index 6533abd..17a1338 100644 (file)
@@ -427,12 +427,11 @@ abstract class JobQueue {
        protected function doDeduplicateRootJob( Job $job ) {
                global $wgMemc;
 
-               $params = $job->getParams();
-               if ( !isset( $params['rootJobSignature'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
-               } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
+               if ( !$job->hasRootJobParams() ) {
+                       throw new MWException( "Cannot register root job; missing parameters." );
                }
+               $params = $job->getRootJobParams();
+
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
                // Callers should call batchInsert() and then this function so that if the insert
                // fails, the de-duplication registration will be aborted. Since the insert is
@@ -473,13 +472,10 @@ abstract class JobQueue {
        protected function doIsRootJobOldDuplicate( Job $job ) {
                global $wgMemc;
 
-               $params = $job->getParams();
-               if ( !isset( $params['rootJobSignature'] ) ) {
+               if ( !$job->hasRootJobParams() ) {
                        return false; // job has no de-deplication info
-               } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
-                       trigger_error( "Cannot check root job; missing 'rootJobTimestamp'." );
-                       return false;
                }
+               $params = $job->getRootJobParams();
 
                // Get the last time this root job was enqueued
                $timestamp = $wgMemc->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
index c57081e..26a9b72 100644 (file)
@@ -441,12 +441,11 @@ LUA;
         * @throws MWException
         */
        protected function doDeduplicateRootJob( Job $job ) {
-               $params = $job->getParams();
-               if ( !isset( $params['rootJobSignature'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
-               } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
-                       throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
+               if ( !$job->hasRootJobParams() ) {
+                       throw new MWException( "Cannot register root job; missing parameters." );
                }
+               $params = $job->getRootJobParams();
+
                $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
 
                $conn = $this->getConnection();
diff --git a/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql b/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql
new file mode 100644 (file)
index 0000000..bff63c7
--- /dev/null
@@ -0,0 +1,5 @@
+--
+-- Makes the iwl_prefix_title_from index for the iwlinks table non-unique
+--
+DROP INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks;
+CREATE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from);
diff --git a/maintenance/archives/patch-iwlinks-from-title-index.sql b/maintenance/archives/patch-iwlinks-from-title-index.sql
new file mode 100644 (file)
index 0000000..8b73f9e
--- /dev/null
@@ -0,0 +1,4 @@
+--
+-- Recreates the iwl_prefix_from_title index for the iwlinks table
+--
+CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title);
diff --git a/maintenance/archives/patch-kill-iwl_pft.sql b/maintenance/archives/patch-kill-iwl_pft.sql
deleted file mode 100644 (file)
index 96e1435..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
---
--- Kill the old iwl_prefix_from_title index, which may be present on some
--- installs if they ran update.php between it being added and being renamed
---
-
-DROP INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks;
-
diff --git a/maintenance/postgres/archives/patch-kill-iwl_pft.sql b/maintenance/postgres/archives/patch-kill-iwl_pft.sql
deleted file mode 100644 (file)
index 4419d9e..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
---
--- Kill the old iwl_prefix_from_title index, which may be present on some
--- installs if they ran update.php between it being added and being renamed
---
-
-DROP INDEX iwl_prefix_from_title;
-
index a4bdb6a..0eb792e 100644 (file)
@@ -1,2 +1,2 @@
 DROP INDEX iwl_prefix;
-CREATE UNIQUE INDEX iwl_prefix_title_from ON iwlinks (iwl_prefix, iwl_from, iwl_title);
+CREATE UNIQUE INDEX iwl_prefix_title_from ON iwlinks (iwl_prefix, iwl_title, iwl_from);
index 9cbabfd..5ce1a17 100644 (file)
@@ -679,6 +679,7 @@ CREATE TABLE iwlinks (
 );
 CREATE UNIQUE INDEX iwl_from ON iwlinks (iwl_from, iwl_prefix, iwl_title);
 CREATE UNIQUE INDEX iwl_prefix_title_from ON iwlinks (iwl_prefix, iwl_title, iwl_from);
+CREATE UNIQUE INDEX iwl_prefix_from_title ON iwlinks (iwl_prefix, iwl_from, iwl_title);
 
 CREATE TABLE msg_resource (
   mr_resource   TEXT         NOT NULL,
diff --git a/maintenance/sqlite/archives/patch-kill-iwl_pft.sql b/maintenance/sqlite/archives/patch-kill-iwl_pft.sql
deleted file mode 100644 (file)
index 8fc4b5c..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
---
--- Kill the old iwl_prefix_from_title index, which may be present on some
--- installs if they ran update.php between it being added and being renamed
---
-
-DROP INDEX IF EXISTS /*i*/iwl_prefix;
-
index fd4c9ec..6d5b1bf 100644 (file)
@@ -2,4 +2,4 @@
 -- Recreates the iwl_prefix for the iwlinks table
 --
 DROP INDEX IF EXISTS /*i*/iwl_prefix;
-CREATE INDEX IF NOT EXISTS /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title);
+CREATE INDEX IF NOT EXISTS /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from);
index 72b4eb6..fb145d0 100644 (file)
@@ -674,7 +674,8 @@ CREATE TABLE /*_*/iwlinks (
 ) /*$wgDBTableOptions*/;
 
 CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title);
-CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from);
+CREATE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from);
+CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title);
 
 
 --
index 630002d..f07f1b7 100644 (file)
@@ -24,7 +24,7 @@
         * @param {Object|undefined} options
         */
        function toggleElement( $collapsible, action, $defaultToggle, options ) {
-               var $collapsibleContent, $containers;
+               var $collapsibleContent, $containers, hookCallback;
                options = options || {};
 
                // Validate parameters
                        return;
                }
 
+               // Trigger a custom event to allow callers to hook to the collapsing/expanding,
+               // allowing the module to be testable, and making it possible to
+               // e.g. implement persistence via cookies
+               $collapsible.trigger( action === 'expand' ? 'beforeExpand.mw-collapsible' : 'beforeCollapse.mw-collapsible' );
+               hookCallback = function () {
+                       $collapsible.trigger( action === 'expand' ? 'afterExpand.mw-collapsible' : 'afterCollapse.mw-collapsible' );
+               };
+
                // Handle different kinds of elements
 
                if ( !options.plainMode && $collapsible.is( 'table' ) ) {
                                // http://stackoverflow.com/questions/467336#920480
                                if ( options.instantHide ) {
                                        $containers.hide();
+                                       hookCallback();
                                } else {
-                                       $containers.stop( true, true ).fadeOut();
+                                       $containers.stop( true, true ).fadeOut( hookCallback );
                                }
                        } else {
-                               $containers.stop( true, true ).fadeIn();
+                               $containers.stop( true, true ).fadeIn( hookCallback );
                        }
 
                } else if ( !options.plainMode && ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) ) {
                        if ( action === 'collapse' ) {
                                if ( options.instantHide ) {
                                        $containers.hide();
+                                       hookCallback();
                                } else {
-                                       $containers.stop( true, true ).slideUp();
+                                       $containers.stop( true, true ).slideUp( hookCallback );
                                }
                        } else {
-                               $containers.stop( true, true ).slideDown();
+                               $containers.stop( true, true ).slideDown( hookCallback );
                        }
 
                } else {
                                if ( action === 'collapse' ) {
                                        if ( options.instantHide ) {
                                                $collapsibleContent.hide();
+                                               hookCallback();
                                        } else {
-                                               $collapsibleContent.slideUp();
+                                               $collapsibleContent.slideUp( hookCallback );
                                        }
                                } else {
-                                       $collapsibleContent.slideDown();
+                                       $collapsibleContent.slideDown( hookCallback );
                                }
 
                        // Otherwise assume this is a customcollapse with a remote toggle
                                if ( action === 'collapse' ) {
                                        if ( options.instantHide ) {
                                                $collapsible.hide();
+                                               hookCallback();
                                        } else {
                                                if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
-                                                       $collapsible.fadeOut();
+                                                       $collapsible.fadeOut( hookCallback );
                                                } else {
-                                                       $collapsible.slideUp();
+                                                       $collapsible.slideUp( hookCallback );
                                                }
                                        }
                                } else {
                                        if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
-                                               $collapsible.fadeIn();
+                                               $collapsible.fadeIn( hookCallback );
                                        } else {
-                                               $collapsible.slideDown();
+                                               $collapsible.slideDown( hookCallback );
                                        }
                                }
                        }
                                                .parent()
                                                .prepend( '&nbsp;[' )
                                                .append( ']&nbsp;' )
-                                               .on( 'click.mw-collapse', function ( e, opts ) {
+                                               .on( 'click.mw-collapsible', function ( e, opts ) {
                                                        opts = $.extend( { toggleText: { collapseText: collapsetext, expandText: expandtext } }, options, opts );
                                                        toggleLinkDefault( $(this), e, opts );
                                                } );
 
                        // Bind the custom togglers
                        if ( $customTogglers && $customTogglers.length ) {
-                               $customTogglers.on( 'click.mw-collapse', function ( e, opts ) {
+                               $customTogglers.on( 'click.mw-collapsible', function ( e, opts ) {
                                        opts = $.extend( {}, options, opts );
                                        toggleLinkCustom( $(this), e, opts, $collapsible );
                                } );
                                        if ( !$toggle.length ) {
                                                $firstItem.eq(-1).prepend( $toggleLink );
                                        } else {
-                                               $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) {
+                                               $toggleLink = $toggle.off( 'click.mw-collapsible' ).on( 'click.mw-collapsible', function ( e, opts ) {
                                                        opts = $.extend( {}, options, opts );
                                                        toggleLinkPremade( $toggle, e, opts );
                                                } );
                                                }
                                                $collapsible.prepend( $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent() );
                                        } else {
-                                               $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) {
+                                               $toggleLink = $toggle.off( 'click.mw-collapsible' ).on( 'click.mw-collapsible', function ( e, opts ) {
                                                        opts = $.extend( {}, options, opts );
                                                        toggleLinkPremade( $toggle, e, opts );
                                                } );
                                        if ( !$toggle.length ) {
                                                $collapsible.prepend( $toggleLink );
                                        } else {
-                                               $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) {
+                                               $toggleLink = $toggle.off( 'click.mw-collapsible' ).on( 'click.mw-collapsible', function ( e, opts ) {
                                                        opts = $.extend( {}, options, opts );
                                                        toggleLinkPremade( $toggle, e, opts );
                                                } );
index 36fdbcc..2e5cbd0 100644 (file)
@@ -16,6 +16,7 @@ return array(
                        'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.localize.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js',
@@ -45,6 +46,7 @@ return array(
                        'jquery.hidpi',
                        'jquery.highlightText',
                        'jquery.localize',
+                       'jquery.makeCollapsible',
                        'jquery.mwExtension',
                        'jquery.tabIndex',
                        'jquery.tablesorter',
diff --git a/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js b/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
new file mode 100644 (file)
index 0000000..9f34bee
--- /dev/null
@@ -0,0 +1,118 @@
+( function ( mw, $ ) {
+       var loremIpsum = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.';
+
+       QUnit.module( 'jquery.makeCollapsible', QUnit.newMwEnvironment() );
+
+       function prepareCollapsible( html, options ) {
+               return $( $.parseHTML( html ) )
+                       .appendTo( '#qunit-fixture' )
+                       // options might be undefined here - this is okay
+                       .makeCollapsible( options );
+       }
+
+       QUnit.asyncTest( 'testing hooks (triggers)', 4, function ( assert ) {
+               var $collapsible, $content, $toggle;
+               $collapsible = prepareCollapsible(
+                       '<div class="mw-collapsible">' + loremIpsum + '</div>'
+               );
+               $content = $collapsible.find( '.mw-collapsible-content' );
+               $toggle = $collapsible.find( '.mw-collapsible-toggle' );
+
+               // In one full collapse-expand cycle, each event will be fired once
+
+               // On collapse...
+               $collapsible.on( 'beforeCollapse.mw-collapsible', function () {
+                       assert.assertTrue( $content.is( ':visible' ), 'first beforeCollapseExpand: content is visible' );
+               } );
+               $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+                       assert.assertTrue( $content.is( ':hidden' ), 'first afterCollapseExpand: content is hidden' );
+
+                       // On expand...
+                       $collapsible.on( 'beforeExpand.mw-collapsible', function () {
+                               assert.assertTrue( $content.is( ':hidden' ), 'second beforeCollapseExpand: content is hidden' );
+                       } );
+                       $collapsible.on( 'afterExpand.mw-collapsible', function () {
+                               assert.assertTrue( $content.is( ':visible' ), 'second afterCollapseExpand: content is visible' );
+
+                               QUnit.start();
+                       } );
+
+                       // ...expanding happens here
+                       $toggle.trigger( 'click' );
+               } );
+
+               // ...collapsing happens here
+               $toggle.trigger( 'click' );
+       } );
+
+       QUnit.asyncTest( 'basic operation', 3, function ( assert ) {
+               var $collapsible, $content;
+               $collapsible = prepareCollapsible(
+                       '<div class="mw-collapsible">' + loremIpsum + '</div>'
+               );
+               $content = $collapsible.find( '.mw-collapsible-content' );
+
+               assert.equal( $content.length, 1, 'content is present' );
+               assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+               $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+                       assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+                       QUnit.start();
+               } );
+
+               $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+       } );
+
+       QUnit.test( 'basic operation with instantHide (synchronous test)', 2, function ( assert ) {
+               var $collapsible, $content;
+               $collapsible = prepareCollapsible(
+                       '<div class="mw-collapsible">' + loremIpsum + '</div>',
+                       { instantHide: true }
+               );
+               $content = $collapsible.find( '.mw-collapsible-content' );
+
+               assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+               $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+
+               assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+       } );
+
+       QUnit.asyncTest( 'initially collapsed - mw-collapsed class', 2, function ( assert ) {
+               var $collapsible, $content;
+               $collapsible = prepareCollapsible(
+                       '<div class="mw-collapsible mw-collapsed">' + loremIpsum + '</div>'
+               );
+               $content = $collapsible.find( '.mw-collapsible-content' );
+
+               // Synchronous - mw-collapsed should cause instantHide: true to be used on initial collapsing
+               assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+               $collapsible.on( 'afterExpand.mw-collapsible', function () {
+                       assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+                       QUnit.start();
+               } );
+
+               $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+       } );
+
+       QUnit.asyncTest( 'initially collapsed - options', 2, function ( assert ) {
+               var $collapsible, $content;
+               $collapsible = prepareCollapsible(
+                       '<div class="mw-collapsible">' + loremIpsum + '</div>',
+                       { collapsed: true }
+               );
+               $content = $collapsible.find( '.mw-collapsible-content' );
+
+               // Synchronous - collapsed: true should cause instantHide: true to be used on initial collapsing
+               assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+               $collapsible.on( 'afterExpand.mw-collapsible', function () {
+                       assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+                       QUnit.start();
+               } );
+
+               $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+       } );
+
+}( mediaWiki, jQuery ) );