merge JSTesting branch into trunk
authorAntoine Musso <hashar@users.mediawiki.org>
Tue, 3 Jan 2012 18:33:26 +0000 (18:33 +0000)
committerAntoine Musso <hashar@users.mediawiki.org>
Tue, 3 Jan 2012 18:33:26 +0000 (18:33 +0000)
Changed written by Timo and reviewed by Hashar. This should be harmless.

To enable the feature:
  $wgEnableJavaScriptTest = true;

Then head to:
  [[Special:JavaScriptTest/qunit]]

32 files changed:
RELEASE-NOTES-1.19
docs/hooks.txt
includes/AutoLoader.php
includes/DefaultSettings.php
includes/Skin.php
includes/SpecialPageFactory.php
includes/resourceloader/ResourceLoader.php
includes/specials/SpecialJavaScriptTest.php [new file with mode: 0644]
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
resources/Resources.php
resources/mediawiki.special/mediawiki.special.javaScriptTest.js [new file with mode: 0644]
tests/qunit/QUnitTestResources.php [new file with mode: 0644]
tests/qunit/data/testrunner.js
tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js
tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
tests/qunit/suites/resources/jquery/jquery.client.test.js
tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js
tests/qunit/suites/resources/jquery/jquery.getAttrs.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.mwExtension.test.js
tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js
tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js

index 60fdd82..b2c6d82 100644 (file)
@@ -112,6 +112,9 @@ production.
 * (bug 23427) Introduced {{PAGEID}} variable to expose page.page_id.
 * (bug 33447) Link to the broken image tracking category from Special:Wantedfiles.
 * (bug 27724) Add timestamp to job queue.
+* (bug 30339) Implement SpecialPage for running javascript tests. Disabled by default, due to
+  tests potentially being harmful, not to be run on a production wiki.
+  Enable by setting $wgEnableJavaScriptTest to true.
 
 === Bug fixes in 1.19 ===
 * $wgUploadNavigationUrl should be used for file redlinks if.
index 4ca46e2..c7bd7dd 100644 (file)
@@ -1579,6 +1579,16 @@ scripts.
 loader request or generating HTML output.
 &$resourceLoader: ResourceLoader object
 
+'ResourceLoaderTestModules': let you add new javascript testing modules. This is called after the addition of 'qunit' and MediaWiki testing ressources.
+&testModules: array of javascript testing modules. 'qunit' is feed using tests/qunit/QUnitTestResources.php.
+&RessourceLoader object
+To add a new qunit module named 'myext.tests':
+testModules['qunit']['myext.tests'] = array(
+       'script' => 'extension/myext/tests.js',
+       'dependencies' => <any module dependency you might have>
+);
+For qunit framework, the mediawiki.tests.qunit.testrunner dependency will be added to any module.
+
 'RevisionInsertComplete': called after a revision is inserted into the DB
 &$revision: the Revision
 $data: the data stored in old_text.  The meaning depends on $flags: if external
index c269a70..e805c8c 100644 (file)
@@ -812,6 +812,7 @@ $wgAutoloadLocalClasses = array(
        'SpecialExport' => 'includes/specials/SpecialExport.php',
        'SpecialFilepath' => 'includes/specials/SpecialFilepath.php',
        'SpecialImport' => 'includes/specials/SpecialImport.php',
+       'SpecialJavaScriptTest' => 'includes/specials/SpecialJavaScriptTest.php',
        'SpecialListFiles' => 'includes/specials/SpecialListfiles.php',
        'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php',
        'SpecialListUsers' => 'includes/specials/SpecialListusers.php',
index 0f6dd0e..545a780 100644 (file)
@@ -4194,6 +4194,20 @@ $wgParserTestFiles = array(
  * );
  */
 $wgParserTestRemote = false;
+/**
+ * Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit).
+ */
+$wgEnableJavaScriptTest = false;
+
+/**
+ * Configuration for javascript testing.
+ */
+$wgJavaScriptTestConfig = array(
+       'qunit' => array(
+               'documentation' => '//www.mediawiki.org/wiki/Manual:JavaScript_unit_testing',
+       ),
+);
 
 
 /**
@@ -5248,6 +5262,7 @@ $wgSpecialPageGroups = array(
        'Specialpages'              => 'other',
        'Blockme'                   => 'other',
        'Booksources'               => 'other',
+       'JavaScriptTest'            => 'other',
 );
 
 /** Whether or not to sort special pages in Special:Specialpages */
index 9ecb615..1841f8a 100644 (file)
@@ -22,7 +22,7 @@ abstract class Skin extends ContextSource {
 
        /**
         * Fetch the set of available skins.
-        * @return array of strings
+        * @return associative array of strings
         */
        static function getSkinNames() {
                global $wgValidSkinNames;
@@ -55,6 +55,18 @@ abstract class Skin extends ContextSource {
                }
                return $wgValidSkinNames;
        }
+       /**
+        * Fetch the skinname messages for available skins.
+        * @return array of strings
+        */
+       static function getSkinNameMessages() {
+               $messages = array();
+               foreach( self::getSkinNames() as $skinKey => $skinName ) {
+                       $messages[] = "skinname-$skinKey";
+               }
+               return $messages;
+       }
 
        /**
         * Fetch the list of usable skins in regards to $wgSkipSkins.
index a307575..0a1631b 100644 (file)
@@ -138,6 +138,7 @@ class SpecialPageFactory {
                'Blankpage'                 => 'SpecialBlankpage',
                'Blockme'                   => 'SpecialBlockme',
                'Emailuser'                 => 'SpecialEmailUser',
+               'JavaScriptTest'            => 'SpecialJavaScriptTest',
                'Movepage'                  => 'MovePageForm',
                'Mycontributions'           => 'SpecialMycontributions',
                'Mypage'                    => 'SpecialMypage',
index cfc6494..0d1eef0 100644 (file)
@@ -37,6 +37,10 @@ class ResourceLoader {
 
        /** Associative array mapping module name to info associative array */
        protected $moduleInfos = array();
+       /** Associative array mapping framework ids to a list of names of test suite modules */
+       /** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
+       protected $testModuleNames = array();
 
        /** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
        protected $sources = array();
@@ -183,7 +187,7 @@ class ResourceLoader {
         * Registers core modules and runs registration hooks.
         */
        public function __construct() {
-               global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript;
+               global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
 
                wfProfileIn( __METHOD__ );
 
@@ -199,6 +203,11 @@ class ResourceLoader {
                wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
                $this->register( $wgResourceModules );
 
+               if ( $wgEnableJavaScriptTest === true ) {
+                       $this->registerTestModules();
+               }
+
+
                wfProfileOut( __METHOD__ );
        }
 
@@ -256,6 +265,40 @@ class ResourceLoader {
                wfProfileOut( __METHOD__ );
        }
 
+       /**
+        */
+       public function registerTestModules() {
+               global $IP, $wgEnableJavaScriptTest;
+
+               if ( $wgEnableJavaScriptTest !== true ) {
+                       throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
+               }
+
+               wfProfileIn( __METHOD__ );
+
+               // Get core test suites
+               $testModules = array();
+               $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
+               // Get other test suites (e.g. from extensions)
+               wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
+
+               // Add the testrunner (which configures QUnit) to the dependencies.
+               // Since it must be ready before any of the test suites are executed.
+               foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
+                       $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+               }
+
+               foreach( $testModules as $id => $names ) {
+                       // Register test modules
+                       $this->register( $testModules[$id] );
+
+                       // Keep track of their names so that they can be loaded together
+                       $this->testModuleNames[$id] = array_keys( $testModules[$id] );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
        /**
         * Add a foreign source of modules.
         *
@@ -300,6 +343,25 @@ class ResourceLoader {
        public function getModuleNames() {
                return array_keys( $this->moduleInfos );
        }
+       /**
+        * Get a list of test module names for one (or all) frameworks.
+        * If the given framework id is unknkown, or if the in-object variable is not an array,
+        * then it will return an empty array.
+        *
+        * @param $framework String: Optional. Get only the test module names for one
+        * particular framework.
+        * @return Array
+        */
+       public function getTestModuleNames( $framework = 'all' ) {
+               if ( $framework == 'all' ) {
+                       return $this->testModuleNames;
+               } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
+                       return $this->testModuleNames[$framework];
+               } else {
+                       return array();
+               }
+       }
 
        /**
         * Get the ResourceLoaderModule object for a given module name.
diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php
new file mode 100644 (file)
index 0000000..d19eb8e
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+
+class SpecialJavaScriptTest extends SpecialPage {
+
+       /**
+        * @var $frameworks Array: Mapping of framework ids and their initilizer methods
+        * in this class. If a framework is requested but not in this array,
+        * the 'unknownframework' error is served.
+        */
+       static $frameworks = array(
+               'qunit' => 'initQUnitTesting',
+       );
+
+       public function __construct() {
+               parent::__construct( 'JavaScriptTest' );
+       }
+
+       public function execute( $par ) {
+               global $wgEnableJavaScriptTest;
+
+               $out = $this->getOutput();
+
+               $this->setHeaders();
+               $out->disallowUserJs();
+
+               // Abort early if we're disabled
+               if ( $wgEnableJavaScriptTest !== true ) {
+                       $out->addWikiMsg( 'javascripttest-disabled' );
+                       return;
+               }
+
+               $out->addModules( 'mediawiki.special.javaScriptTest' );
+
+               // Determine framework
+               $pars = explode( '/', $par );
+               $framework = strtolower( $pars[0] );
+
+               // No framework specified
+               if ( $par == '' ) {
+                       $out->setPagetitle( wfMsg( 'javascripttest' ) );
+                       $summary = $this->wrapSummaryHtml(
+                               wfMsg( 'javascripttest-pagetext-noframework' ) . $this->getFrameworkListHtml(),
+                               'noframework'
+                       );
+                       $out->addHtml( $summary );
+
+               // Matched! Display proper title and initialize the framework
+               } elseif ( isset( self::$frameworks[$framework] ) ) {
+                       $out->setPagetitle( wfMsg( 'javascripttest-title', wfMsg( "javascripttest-$framework-name" ) ) );
+                       $out->setSubtitle(
+                               wfMessage( 'javascripttest-backlink' )->rawParams( Linker::linkKnown( $this->getTitle() ) )->escaped()
+                       );
+                       $this->{self::$frameworks[$framework]}();
+
+               // Framework not found, display error
+               } else {
+                       $out->setPagetitle( wfMsg( 'javascripttest' ) );
+                       $summary = $this->wrapSummaryHtml( '<p class="error">'
+                               . wfMsg( 'javascripttest-pagetext-unknownframework', $par )
+                               . '</p>'
+                               . $this->getFrameworkListHtml() );
+                       $out->addHtml( $summary, 'unknownframework' );
+               }
+       }
+
+       /**
+        * Get a list of frameworks (including introduction paragraph and links to the framework run pages)
+        * @return String: HTML
+        */
+       private function getFrameworkListHtml() {
+               $list = '<ul>';
+               foreach( self::$frameworks as $framework => $initFn ) {
+                       $list .= Html::rawElement(
+                               'li',
+                               array(),
+                               Linker::link( $this->getTitle( $framework ), wfMsg( "javascripttest-$framework-name" ) )
+                       );
+               }
+               $list .= '</ul>';
+               $msg = wfMessage( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock();
+
+               return $msg;
+       }
+
+       /**
+        * Function to wrap the summary.
+        * @param $html String: The raw HTML.
+        * @param $state String: State, one of 'noframework', 'unknownframework' or 'frameworkfound'
+        */
+       private function wrapSummaryHtml( $html = '', $state ) {
+               return "<div id=\"mw-javascripttest-summary\" class=\"mw-javascripttest-$state\">$html</div>";
+       }
+
+       /**
+        * Initialize the page for QUnit.
+        */
+       private function initQUnitTesting() {
+               global $wgJavaScriptTestConfig;
+
+               $out = $this->getOutput();
+
+               $out->addModules( 'mediawiki.tests.qunit.testrunner' );
+               $qunitTestModules = $out->getResourceLoader()->getTestModuleNames( 'qunit' );
+               $out->addModules( $qunitTestModules );
+
+               $summary = wfMessage( 'javascripttest-qunit-intro' )
+                       ->params( $wgJavaScriptTestConfig['qunit']['documentation'] )
+                       ->parseAsBlock();
+               $header = wfMessage( 'javascripttest-qunit-heading' )->escaped();
+
+               $baseHtml = <<<HTML
+<div id="qunit-header">$header</div>
+<div id="qunit-banner"></div>
+<div id="qunit-testrunner-toolbar"></div>
+<div id="qunit-userAgent"></div>
+<ol id="qunit-tests"></ol>
+HTML;
+               $out->addHtml( $this->wrapSummaryHtml( $summary, 'frameworkfound' ) . $baseHtml );
+
+       }
+
+       public function isListed(){
+               global $wgEnableJavaScriptTest;
+               return $wgEnableJavaScriptTest === true;
+       }
+
+}
index 8bdc59f..cd32d8d 100644 (file)
@@ -393,6 +393,7 @@ $specialPageAliases = array(
        'Filepath'                  => array( 'FilePath' ),
        'Import'                    => array( 'Import' ),
        'Invalidateemail'           => array( 'InvalidateEmail' ),
+       'JavaScriptTest'            => array( 'JavaScriptTest' ),
        'BlockList'                 => array( 'BlockList', 'ListBlocks', 'IPBlockList' ),
        'LinkSearch'                => array( 'LinkSearch' ),
        'Listadmins'                => array( 'ListAdmins' ),
@@ -3405,6 +3406,19 @@ Please try again.',
 'import-logentry-upload-detail'    => '$1 {{PLURAL:$1|revision|revisions}}',
 'import-logentry-interwiki'        => 'transwikied $1',
 'import-logentry-interwiki-detail' => '$1 {{PLURAL:$1|revision|revisions}} from $2',
+# JavaScriptTest
+'javascripttest'                           => 'JavaScript Test',
+'javascripttest-backlink'                  => '< $1',
+'javascripttest-disabled'                  => 'This function is disabled.',
+'javascripttest-title'                     => 'Running $1 tests',
+'javascripttest-pagetext-noframework'      => 'This page is reserved for running javascript tests.',
+'javascripttest-pagetext-unknownframework' => 'Unknown framework "$1".',
+'javascripttest-pagetext-frameworks'       => 'Please choose one of the following frameworks: $1',
+'javascripttest-pagetext-skins'            => 'Available skins',
+'javascripttest-qunit-name'                => 'QUnit', // Ignore, do not translate
+'javascripttest-qunit-intro'               => 'See [$1 testing documentation] on mediawiki.org.',
+'javascripttest-qunit-heading'             => 'MediaWiki JavaScript QUnit Test Suite', // Optional, only translate if needed
 
 # Keyboard access keys for power users
 'accesskey-pt-userpage'             => '.', # do not translate or duplicate this message to other languages
index b0f41bb..6e3ad16 100644 (file)
@@ -3059,6 +3059,17 @@ See also:
 'import-logentry-upload' => 'This is the text of an entry in the Import log (and Recent Changes), after hour (and date, only in the Import log) and sysop name:
 * $1 is the name of the imported file',
 
+# JavaScriptTest
+'javascripttest'                           => 'Title of the special page',
+'javascripttest-backlink'                  => '{{optional}}',
+'javascripttest-disabled'                  => '{{Identical|Function disabled}}.',
+'javascripttest-title'                     => 'Title of the special page when running a test suite. $1 is the name of the framework.',
+'javascripttest-pagetext-unknownframework' => 'Error message when given framework id is not found. $1 is the if of the framework.',
+'javascripttest-pagetext-frameworks'       => '$1 is the if of the framework.',
+'javascripttest-qunit-name'                => '{{Ignore}}',
+'javascripttest-qunit-intro'               => '$1 is the configured url to the documentation.',
+'javascripttest-qunit-heading'             => '{{Optional}}',
+
 # Tooltip help for the actions
 'tooltip-pt-userpage'             => 'Tooltip shown when hovering the mouse over the link to your own User page in the upper-side personal toolbox.',
 'tooltip-pt-mytalk'               => 'Tooltip shown when hovering over the "my talk" link in your personal toolbox (upper right side).',
index 8d73540..463c85c 100644 (file)
@@ -166,15 +166,15 @@ return array(
        'jquery.placeholder' => array(
                'scripts' => 'resources/jquery/jquery.placeholder.js',
        ),
-       'jquery.qunit.completenessTest' => array(
-               'scripts' => 'resources/jquery/jquery.qunit.completenessTest.js',
-               'dependencies' => 'jquery.qunit',
-       ),
        'jquery.qunit' => array(
                'scripts' => 'resources/jquery/jquery.qunit.js',
                'styles' => 'resources/jquery/jquery.qunit.css',
                'position' => 'top',
        ),
+       'jquery.qunit.completenessTest' => array(
+               'scripts' => 'resources/jquery/jquery.qunit.completenessTest.js',
+               'dependencies' => 'jquery.qunit',
+       ),
        'jquery.spinner' => array(
                'scripts' => 'resources/jquery/jquery.spinner.js',
                'styles' => 'resources/jquery/jquery.spinner.css',
@@ -767,6 +767,28 @@ return array(
                ),
                'dependencies' => array( 'mediawiki.libs.jpegmeta', 'mediawiki.util' ),
        ),
+       'mediawiki.special.javaScriptTest' => array(
+               'scripts' => 'resources/mediawiki.special/mediawiki.special.javaScriptTest.js',
+               'messages' => array_merge( Skin::getSkinNameMessages(), array(
+                       'colon-separator',
+                       'javascripttest-pagetext-skins',
+               ) ),
+               'dependencies' => array( 'jquery.qunit' ),
+               'position' => 'top',
+       ),
+
+       /* MediaWiki Tests */
+
+       'mediawiki.tests.qunit.testrunner' => array(
+               'scripts' => 'tests/qunit/data/testrunner.js',
+               'dependencies' => array(
+                       'jquery.qunit',
+                       'jquery.qunit.completenessTest',
+                       'mediawiki.page.startup',
+                       'mediawiki.page.ready',
+               ),
+               'position' => 'top',
+       ),
 
        /* MediaWiki Legacy */
 
diff --git a/resources/mediawiki.special/mediawiki.special.javaScriptTest.js b/resources/mediawiki.special/mediawiki.special.javaScriptTest.js
new file mode 100644 (file)
index 0000000..a342989
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * JavaScript for Special:JavaScriptTest
+ */
+jQuery( document ).ready( function( $ ) {
+
+       // Create useskin dropdown menu and reload onchange to the selected skin
+       // (only if a framework was found, not on error pages).
+       $( '#mw-javascripttest-summary.mw-javascripttest-frameworkfound' ).append( function() {
+
+               var     $html = $( '<p><label for="useskin">'
+                               + mw.message( 'javascripttest-pagetext-skins' ).escaped()
+                               + mw.message( 'colon-separator' ).plain()
+                               + '</label></p>' ),
+                       select = '<select name="useskin" id="useskin">';
+
+               // Build <select> further
+               $.each( mw.config.get( 'wgAvailableSkins' ), function( id ) {
+                       select += '<option value="' + id + '"'
+                               + ( mw.config.get( 'skin' ) === id ? ' selected="selected"' : '' )
+                               + '>' + mw.message( 'skinname-' + id ).escaped() + '</option>';
+               } );
+               select += '</select>';
+
+               // Bind onchange event handler and append to form
+               $html.append(
+                       $( select ).change( function() {
+                               window.location = QUnit.url( { useskin: $(this).val() } );
+                       } )
+               );
+
+               return $html;
+       } );
+} );
diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php
new file mode 100644 (file)
index 0000000..a744009
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+return array(
+
+       /* Test suites for MediaWiki core modules */
+
+       'mediawiki.tests.qunit.suites' => array(
+               'scripts' => array(
+                       'tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.byteLength.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.client.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.getAttrs.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.mwExtension.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js',
+                       'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.title.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js',
+                       'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
+
+                       // *has mw-config def:
+                       // This means the module overwrites/sets mw.config variables, reason being that
+                       // the static /qunit/index.html has an empty mw.config since it's static.
+                       // Until /qunit/index.html is fully replaceable and WMF's TestSwarm is up and running
+                       // with Special:JavaScriptTest - untill then, it is important that tests do not depend
+                       // on anything being in mw.config (not even wgServer).
+               ),
+               'dependencies' => array(
+                       'jquery.autoEllipsis',
+                       'jquery.byteLength',
+                       'jquery.byteLimit',
+                       'jquery.client',
+                       'jquery.colorUtil',
+                       'jquery.getAttrs',
+                       'jquery.highlightText',
+                       'jquery.localize',
+                       'jquery.mwExtension',
+                       'jquery.tabIndex',
+                       'jquery.tablesorter',
+                       'jquery.textSelection',
+                       'mediawiki',
+                       'mediawiki.Title',
+                       'mediawiki.user',
+                       'mediawiki.util',
+                       'mediawiki.special.recentchanges',
+               ),
+       )
+);
index 8e6671e..fdd3116 100644 (file)
@@ -1,13 +1,19 @@
-( function( $ ) {
+( function ( $, mw, QUnit, undefined ) {
+"use strict";
+
+var mwTestIgnore, mwTester, addons;
 
 /**
  * Add bogus to url to prevent IE crazy caching
  *
- * @param value {String} a relative path (eg. 'data/defineTestCallback.js' or 'data/test.php?foo=bar')
+ * @param value {String} a relative path (eg. 'data/defineTestCallback.js'
+ * or 'data/test.php?foo=bar').
  * @return {String} Such as 'data/defineTestCallback.js?131031765087663960'
  */
-QUnit.fixurl = function(value) {
-       return value + (/\?/.test(value) ? "&" : "?") + new Date().getTime() + "" + parseInt(Math.random()*100000);
+QUnit.fixurl = function (value) {
+       return value + (/\?/.test( value ) ? '&' : '?')
+               + String( new Date().getTime() )
+               + String( parseInt( Math.random()*100000, 10 ) );
 };
 
 /**
@@ -15,31 +21,41 @@ QUnit.fixurl = function(value) {
  */
 QUnit.config.testTimeout = 5000;
 
+/**
+ * MediaWiki debug mode
+ */
+QUnit.config.urlConfig.push( 'debug' );
+
 /**
  *  Load TestSwarm agent
  */
 if ( QUnit.urlParams.swarmURL  ) {
-       document.write("<scr" + "ipt src='" + QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/testwarm.inject.js' ) + "'></scr" + "ipt>");
+       document.write( "<scr" + "ipt src='" + QUnit.fixurl( mw.config.get( 'wgScriptPath' )
+               + '/tests/qunit/data/testwarm.inject.js' ) + "'></scr" + "ipt>" );
 }
 
 /**
- * Load completenesstest
+ * CompletenessTest
  */
+// Adds toggle checkbox to header
+QUnit.config.urlConfig.push( 'completenesstest' );
+
+// Initiate when enabled
 if ( QUnit.urlParams.completenesstest ) {
 
        // Return true to ignore
-       var mwTestIgnore = function( val, tester, funcPath ) {
+       mwTestIgnore = function ( val, tester, funcPath ) {
 
                // Don't record methods of the properties of constructors,
                // to avoid getting into a loop (prototype.constructor.prototype..).
                // Since we're therefor skipping any injection for
                // "new mw.Foo()", manually set it to true here.
                if ( val instanceof mw.Map ) {
-                       tester.methodCallTracker['Map'] = true;
+                       tester.methodCallTracker.Map = true;
                        return true;
                }
                if ( val instanceof mw.Title ) {
-                       tester.methodCallTracker['Title'] = true;
+                       tester.methodCallTracker.Title = true;
                        return true;
                }
 
@@ -51,42 +67,113 @@ if ( QUnit.urlParams.completenesstest ) {
                return false;
        };
 
-       var mwTester = new CompletenessTest( mw, mwTestIgnore );
+       mwTester = new CompletenessTest( mw, mwTestIgnore );
 }
 
+/**
+ * Test environment recommended for all QUnit test modules
+ */
+// Whether to log environment changes to the console
+QUnit.config.urlConfig.push( 'mwlogenv' );
+
+/**
+ * Reset mw.config to a fresh copy of the live config for each test();
+ * @param override {Object} [optional]
+ * @example:
+ * <code>
+ * module( .., newMwEnvironment() );
+ *
+ * test( .., function () {
+ *     mw.config.set( 'foo', 'bar' ); // just for this test
+ * } );
+ *
+ * test( .., function () {
+ *     mw.config.get( 'foo' ); // doesn't exist
+ * } );
+ *
+ *
+ * module( .., newMwEnvironment({ quux: 'corge' }) );
+ *
+ * test( .., function () {
+ *     mw.config.get( 'quux' ); // "corge"
+ *     mw.config.set( 'quux', "grault" );
+ * } );
+ *
+ * test( .., function () {
+ *     mw.config.get( 'quux' ); // "corge"
+ * } );
+ * </code>
+ */
+QUnit.newMwEnvironment = ( function () {
+       var liveConfig, freshConfigCopy, log;
+
+       liveConfig = mw.config.values;
+
+       freshConfigCopy = function ( custom ) {
+               // "deep=true" is important here.
+               // Otherwise we just create a new object with values referring to live config.
+               // e.g. mw.config.set( 'wgFileExtensions', [] ) would not effect liveConfig,
+               // but mw.config.get( 'wgFileExtensions' ).push( 'png' ) would as the array
+               // was passed by reference in $.extend's loop.
+               return $.extend({}, liveConfig, custom, /*deep=*/true );
+       };
+
+       log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
+
+       return function ( override ) {
+               override = override || {};
+
+               return {
+                       setup: function () {
+                               log( 'MwEnvironment> SETUP    for "' + QUnit.config.current.module
+                                       + ': ' + QUnit.config.current.testName + '"' );
+                               // Greetings, mock configuration!
+                               mw.config.values = freshConfigCopy( override );
+                       },
+
+                       teardown: function () {
+                               log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
+                                       + ': ' + QUnit.config.current.testName + '"' );
+                               // Farewell, mock configuration!
+                               mw.config.values = liveConfig;
+                       }
+               };
+       };
+}() );
+
 /**
  * Add-on assertion helpers
  */
 // Define the add-ons
-var addons = {
+addons = {
 
        // Expect boolean true
-       assertTrue: function( actual, message ) {
+       assertTrue: function ( actual, message ) {
                strictEqual( actual, true, message );
        },
 
        // Expect boolean false
-       assertFalse: function( actual, message ) {
+       assertFalse: function ( actual, message ) {
                strictEqual( actual, false, message );
        },
 
        // Expect numerical value less than X
-       lt: function( actual, expected, message ) {
+       lt: function ( actual, expected, message ) {
                QUnit.push( actual < expected, actual, 'less than ' + expected, message );
        },
 
        // Expect numerical value less than or equal to X
-       ltOrEq: function( actual, expected, message ) {
+       ltOrEq: function ( actual, expected, message ) {
                QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
        },
 
        // Expect numerical value greater than X
-       gt: function( actual, expected, message ) {
+       gt: function ( actual, expected, message ) {
                QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
        },
 
        // Expect numerical value greater than or equal to X
-       gtOrEq: function( actual, expected, message ) {
+       gtOrEq: function ( actual, expected, message ) {
                QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
        },
 
@@ -98,4 +185,4 @@ var addons = {
 $.extend( QUnit, addons );
 $.extend( window, addons );
 
-})( jQuery );
+})( jQuery, mediaWiki, QUnit );
index 690ffb2..ba03f2b 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.autoEllipsis' );
+module( 'jquery.autoEllipsis', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 68a08b9..15fac69 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.byteLength' );
+module( 'jquery.byteLength', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 7e28c72..2c4dda6 100644 (file)
@@ -1,4 +1,6 @@
-module( 'jquery.byteLimit' );
+( function () {
+
+module( 'jquery.byteLimit', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
@@ -150,8 +152,6 @@ byteLimitTest({
        $input: $( '<input>' )
                .attr( 'type', 'text' )
                .byteLimit( 6, function( val ) {
-                       _titleConfig();
-
                        // Invalid title
                        if ( val == '' ) {
                                return '';
@@ -172,8 +172,6 @@ byteLimitTest({
                .attr( 'type', 'text' )
                .prop( 'maxLength', '6' )
                .byteLimit( function( val ) {
-                       _titleConfig();
-
                        // Invalid title
                        if ( val === '' ) {
                                return '';
@@ -187,3 +185,5 @@ byteLimitTest({
        limit: 6, // 'Sample' length
        expected: 'User:Sample'
 });
+
+}() );
\ No newline at end of file
index 99c3070..f6eb700 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.client' );
+module( 'jquery.client', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 88791ca..655ee56 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.colorUtil' );
+module( 'jquery.colorUtil', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 5685756..9377a2f 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.getAttrs' );
+module( 'jquery.getAttrs', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 1d506da..4750d2b 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.highlightText' );
+module( 'jquery.highlightText', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index c5d8c4d..cd82863 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.localize' );
+module( 'jquery.localize', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index 7a1281d..3a2d0d8 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.mwExtension' );
+module( 'jquery.mwExtension', QUnit.newMwEnvironment() );
 
 test( 'String functions', function() {
 
index 7d014f3..f26ba7b 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.tabIndex' );
+module( 'jquery.tabIndex', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(2);
index 61f1408..31415d6 100644 (file)
@@ -1,12 +1,13 @@
-(function() {
+( function () {
 
-module( 'jquery.tablesorter' );
-
-// setup hack
-mw.config.set( 'wgMonthNames', [ '', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] );
-mw.config.set( 'wgMonthNamesShort', ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] );
-mw.config.set( 'wgDefaultDateFormat', 'dmy' );
+var config = {
+       wgMonthNames: ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+       wgMonthNamesShort: ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+       wgDefaultDateFormat: 'dmy',
+       wgContentLanguage: 'en'
+};
 
+module( 'jquery.tablesorter', QUnit.newMwEnvironment( config ) );
 
 test( '-- Initial check', function() {
        expect(1);
@@ -181,8 +182,9 @@ tableTest(
                ['11.11.2011']
        ],
        function( $table ) {
-               // @fixme reset it at end or change module to allow us to override it
                mw.config.set( 'wgDefaultDateFormat', 'dmy' );
+               mw.config.set( 'wgContentLanguage', 'de' );
+
                $table.tablesorter();
                $table.find( '.headerSort:eq(0)' ).click();
        }
@@ -206,8 +208,8 @@ tableTest(
                ['11.11.2011']
        ],
        function( $table ) {
-               // @fixme reset it at end or change module to allow us to override it
                mw.config.set( 'wgDefaultDateFormat', 'mdy' );
+
                $table.tablesorter();
                $table.find( '.headerSort:eq(0)' ).click();
        }
@@ -293,9 +295,9 @@ tableTest(
                        'ß': 'ss',
                        'ü':'ue'
                } );
+
                $table.tablesorter();
                $table.find( '.headerSort:eq(0)' ).click();
-               mw.config.set( 'tableSorterCollation', {} );
        }
 );
 
@@ -361,6 +363,7 @@ tableTest(
        complexMDYSorted,
        function( $table ) {
                mw.config.set( 'wgDefaultDateFormat', 'mdy' );
+
                $table.tablesorter();
                $table.find( '.headerSort:eq(0)' ).click();
        }
index a493bbe..a9a2446 100644 (file)
@@ -1,4 +1,4 @@
-module( 'jquery.textSelection' );
+module( 'jquery.textSelection', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
index e959d4b..d73fe5a 100644 (file)
@@ -1,4 +1,4 @@
-module( 'mediawiki.special.recentchanges' );
+module( 'mediawiki.special.recentchanges', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect( 2 );
index 4d1b50e..e04111f 100644 (file)
@@ -1,71 +1,69 @@
-module( 'mediawiki.Title' );
+( function () {
 
 // mw.Title relies on these three config vars
 // Restore them after each test run
-var _titleConfig = function() {
-
-       mw.config.set({
-               "wgFormattedNamespaces": {
-                       "-2": "Media",
-                       "-1": "Special",
-                       "0": "",
-                       "1": "Talk",
-                       "2": "User",
-                       "3": "User talk",
-                       "4": "Wikipedia",
-                       "5": "Wikipedia talk",
-                       "6": "File",
-                       "7": "File talk",
-                       "8": "MediaWiki",
-                       "9": "MediaWiki talk",
-                       "10": "Template",
-                       "11": "Template talk",
-                       "12": "Help",
-                       "13": "Help talk",
-                       "14": "Category",
-                       "15": "Category talk",
-                       /* testing custom / localized */
-                       "100": "Penguins"
-               },
-               "wgNamespaceIds": {
-                       "media": -2,
-                       "special": -1,
-                       "": 0,
-                       "talk": 1,
-                       "user": 2,
-                       "user_talk": 3,
-                       "wikipedia": 4,
-                       "wikipedia_talk": 5,
-                       "file": 6,
-                       "file_talk": 7,
-                       "mediawiki": 8,
-                       "mediawiki_talk": 9,
-                       "template": 10,
-                       "template_talk": 11,
-                       "help": 12,
-                       "help_talk": 13,
-                       "category": 14,
-                       "category_talk": 15,
-                       "image": 6,
-                       "image_talk": 7,
-                       "project": 4,
-                       "project_talk": 5,
-                       /* testing custom / alias */
-                       "penguins": 100,
-                       "antarctic_waterfowl": 100
-               },
-               "wgCaseSensitiveNamespaces": []
-       });
+var config = {
+       "wgFormattedNamespaces": {
+               "-2": "Media",
+               "-1": "Special",
+               "0": "",
+               "1": "Talk",
+               "2": "User",
+               "3": "User talk",
+               "4": "Wikipedia",
+               "5": "Wikipedia talk",
+               "6": "File",
+               "7": "File talk",
+               "8": "MediaWiki",
+               "9": "MediaWiki talk",
+               "10": "Template",
+               "11": "Template talk",
+               "12": "Help",
+               "13": "Help talk",
+               "14": "Category",
+               "15": "Category talk",
+               // testing custom / localized namespace
+               "100": "Penguins"
+       },
+       "wgNamespaceIds": {
+               "media": -2,
+               "special": -1,
+               "": 0,
+               "talk": 1,
+               "user": 2,
+               "user_talk": 3,
+               "wikipedia": 4,
+               "wikipedia_talk": 5,
+               "file": 6,
+               "file_talk": 7,
+               "mediawiki": 8,
+               "mediawiki_talk": 9,
+               "template": 10,
+               "template_talk": 11,
+               "help": 12,
+               "help_talk": 13,
+               "category": 14,
+               "category_talk": 15,
+               "image": 6,
+               "image_talk": 7,
+               "project": 4,
+               "project_talk": 5,
+               /* testing custom / alias */
+               "penguins": 100,
+               "antarctic_waterfowl": 100
+       },
+       "wgCaseSensitiveNamespaces": []
 };
 
-test( '-- Initial check', function() {
+module( 'mediawiki.Title', QUnit.newMwEnvironment( config ) );
+
+test( '-- Initial check', function () {
        expect(1);
        ok( mw.Title, 'mw.Title defined' );
 });
 
-test( 'Transformation', function() {
+test( 'Transformation', function () {
        expect(8);
-       _titleConfig();
 
        var title;
 
@@ -89,9 +87,8 @@ test( 'Transformation', function() {
        equal( title.getName(), 'Foo_bar_.js', "Merge multiple spaces to a single space." );
 });
 
-test( 'Main text for filename', function() {
+test( 'Main text for filename', function () {
        expect(8);
-       _titleConfig();
 
        var title = new mw.Title( 'File:foo_bar.JPG' );
 
@@ -105,9 +102,8 @@ test( 'Main text for filename', function() {
        equal( title.getDotExtension(), '.JPG' );
 });
 
-test( 'Namespace detection and conversion', function() {
+test( 'Namespace detection and conversion', function () {
        expect(6);
-       _titleConfig();
 
        var title;
 
@@ -128,18 +124,16 @@ test( 'Namespace detection and conversion', function() {
        equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' );
 });
 
-test( 'Throw error on invalid title', function() {
+test( 'Throw error on invalid title', function () {
        expect(1);
-       _titleConfig();
 
-       raises(function() {
+       raises(function () {
                var title = new mw.Title( '' );
        }, 'Throw error on empty string' );
 });
 
-test( 'Case-sensivity', function() {
+test( 'Case-sensivity', function () {
        expect(3);
-       _titleConfig();
 
        var title;
 
@@ -159,9 +153,8 @@ test( 'Case-sensivity', function() {
        equal( title.toString(), 'User:John', '$wgCapitalLinks=false: User namespace is insensitive, first-letter becomes uppercase' );
 });
 
-test( 'toString / toText', function() {
+test( 'toString / toText', function () {
        expect(2);
-       _titleConfig();
 
        var title = new mw.Title( 'Some random page' );
 
@@ -169,9 +162,8 @@ test( 'toString / toText', function() {
        equal( title.toText(), title.getPrefixedText() );
 });
 
-test( 'Exists', function() {
+test( 'Exists', function () {
        expect(3);
-       _titleConfig();
 
        var title;
 
@@ -191,9 +183,8 @@ test( 'Exists', function() {
 
 });
 
-test( 'Url', function() {
+test( 'Url', function () {
        expect(2);
-       _titleConfig();
 
        var title;
 
@@ -206,3 +197,5 @@ test( 'Url', function() {
        title = new mw.Title( 'John Doe', 3 );
        equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' );
 });
+
+}() );
\ No newline at end of file
index adaf5f9..24005b6 100644 (file)
@@ -1,6 +1,6 @@
 /* Some misc JavaScript compatibility tests, just to make sure the environments we run in are consistent */
 
-module( 'mediawiki.jscompat' );
+module( 'mediawiki.jscompat', QUnit.newMwEnvironment() );
 
 test( 'Variable with Unicode letter in name', function() {
        expect(3);
index 85c2472..9214131 100644 (file)
@@ -1,4 +1,4 @@
-module( 'mediawiki' );
+module( 'mediawiki', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(8);
@@ -173,12 +173,29 @@ test( 'mw.loader.bug30825', function() {
        // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
        // Test is for regressions!
 
-       expect(1);
+       expect(2);
+
+       var server = mw.config.get( 'wgServer' ),
+           basePath = mw.config.get( 'wgScriptPath' );
+
+       // From [[Special:JavaScriptTest]] we need to preprend the script path
+       // with the actual server (http://localhost/).
+       // Running from file tests/qunit/index.html, wgScriptPath is already
+       // including the wgServer part
+       if( server !== null ) {
+               basePath = server + basePath;
+       }
+       // Forge an URL to the test callback script
+       var target = QUnit.fixurl(
+               basePath + '/tests/qunit/data/qunitOkCall.js'
+       );
 
        // Confirm that mw.loader.load() works with protocol-relative URLs
-       var loc = window.location,
-               base = ('//' + loc.hostname + loc.pathname).replace(/\/[^\/]*$/, ''),
-               target = base + '/data/qunitOkCall.js?' + (new Date()).getTime();
+       target = target.replace( /https?:/, '' );
+
+       equal( target.substr( 0, 2 ), '//',
+               'URL must be relative to test relative URLs!'
+       );
 
        // Async!
        stop();
index 1aa4698..15265db 100644 (file)
@@ -1,4 +1,4 @@
-module( 'mediawiki.user' );
+module( 'mediawiki.user', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
@@ -16,6 +16,16 @@ test( 'options', function() {
 test( 'User login status', function() {
        expect(5);
 
+       /**
+        * Tests can be run under three different conditions:
+        *   1) From tests/qunit/index.html, user will be anonymous.
+        *   2) Logged in on [[Special:JavaScriptTest/qunit]]
+        *   3) Anonymously at the same special page.
+        */
+
+       // Forge an anonymous user:
+       mw.config.set( 'wgUserName', null);
+
        strictEqual( mw.user.name(), null, 'user.name should return null when anonymous' );
        ok( mw.user.anonymous(), 'user.anonymous should reutrn true when anonymous' );
 
index da18f02..445f82c 100644 (file)
@@ -1,4 +1,4 @@
-module( 'mediawiki.util' );
+module( 'mediawiki.util', QUnit.newMwEnvironment() );
 
 test( '-- Initial check', function() {
        expect(1);
@@ -39,7 +39,6 @@ test( 'wikiGetlink', function() {
 test( 'wikiScript', function() {
        expect(2);
 
-       var prevConfig = mw.config.get([ 'wgScript', 'wgScriptPath', 'wgScriptExtension' ]);
        mw.config.set({
                'wgScript': '/w/index.php',
                'wgScriptPath': '/w',
@@ -48,9 +47,6 @@ test( 'wikiScript', function() {
 
        equal( mw.util.wikiScript(), mw.config.get( 'wgScript' ), 'Defaults to index.php and is equal to wgScript' );
        equal( mw.util.wikiScript( 'api' ), '/w/api.php', 'API path' );
-
-       // Restore mw.config
-       mw.config.set( prevConfig );
 });
 
 test( 'addCSS', function() {