Krinkle has uploaded a new change for review.
https://gerrit.wikimedia.org/r/198288
Change subject: build: Add Karma task for automated QUnit testing in browsers
......................................................................
build: Add Karma task for automated QUnit testing in browsers
To use, first run 'npm install'. Then run 'grunt qunit' to start
the test suite in Chrome.
Squashed cherry-picks from master:
* 7605f112e4: jquery.mwExtension.test: Fix qunit-fixture conflict
* 365b6f3af9: mediawiki.jqueryMsg.test: Fix crazy concurrency
* 945c1efe37: build: Add Karma task
* 8d92aaf83e: build: Clean up Gruntfile
* 0ad7553228: build: Don't run jshint on empty skins dir
* 2258f25053: build: Add assert-mw-env task
* dcbbc0489c: build: Increase browserNoActivityTimeout to 60s
* fa4ba8dbd7: build: Declare grunt-cli dependency
Change-Id: I4e96da137340a28789b38940e75d4b6b8bc5d76a
---
M Gruntfile.js
M includes/specials/SpecialJavaScriptTest.php
M package.json
M tests/qunit/data/testrunner.js
M tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
M tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
6 files changed, 190 insertions(+), 83 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/88/198288/1
diff --git a/Gruntfile.js b/Gruntfile.js
index 9badf03..467a172 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -4,6 +4,13 @@
grunt.loadNpmTasks( 'grunt-contrib-jshint' );
grunt.loadNpmTasks( 'grunt-contrib-watch' );
grunt.loadNpmTasks( 'grunt-jscs-checker' );
+ grunt.loadNpmTasks( 'grunt-karma' );
+
+ var wgServer = process.env.MW_SERVER,
+ wgScriptPath = process.env.MW_SCRIPT_PATH,
+ karmaProxy = {};
+
+ karmaProxy[wgScriptPath] = wgServer + wgScriptPath;
grunt.initConfig( {
pkg: grunt.file.readJSON( 'package.json' ),
@@ -11,7 +18,10 @@
options: {
jshintrc: '.jshintrc'
},
- all: [ '*.js',
'{includes,languages,resources,skins,tests}/**/*.js' ]
+ all: [
+ '*.js',
+ '{includes,languages,resources,tests}/**/*.js'
+ ]
},
jscs: {
// Known issues:
@@ -38,6 +48,29 @@
],
tasks: 'test'
},
+ karma: {
+ options: {
+ proxies: karmaProxy,
+ files: [ {
+ pattern: wgServer + wgScriptPath +
'/index.php?title=Special:JavaScriptTest/qunit/export',
+ watched: false,
+ included: true,
+ served: false
+ } ],
+ frameworks: [ 'qunit' ],
+ reporters: [ 'dots' ],
+ singleRun: true,
+ autoWatch: false,
+ // Some tests in extensions don't yield for
more than the default 10s (T89075)
+ browserNoActivityTimeout: 60 * 1000
+ },
+ main: {
+ browsers: [ 'Chrome' ]
+ },
+ more: {
+ browsers: [ 'Chrome', 'Firefox' ]
+ }
+ },
copy: {
jsduck: {
src: 'resources/**/*',
@@ -50,7 +83,22 @@
}
} );
+ grunt.registerTask( 'assert-mw-env', function () {
+ if ( !process.env.MW_SERVER ) {
+ grunt.log.error( 'Environment variable MW_SERVER must
be set.\n' +
+ 'Set this like $wgServer, e.g.
"http://localhost"'
+ );
+ }
+ if ( !process.env.MW_SCRIPT_PATH ) {
+ grunt.log.error( 'Environment variable MW_SCRIPT_PATH
must be set.\n' +
+ 'Set this like $wgScriptPath, e.g. "/w"');
+ }
+ return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH
);
+ } );
+
grunt.registerTask( 'lint', ['jshint', 'jscs'] );
+ grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] );
+
grunt.registerTask( 'test', ['lint'] );
- grunt.registerTask( 'default', ['test'] );
+ grunt.registerTask( 'default', 'test' );
};
diff --git a/includes/specials/SpecialJavaScriptTest.php
b/includes/specials/SpecialJavaScriptTest.php
index a5984fe..c852a06 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -164,7 +164,6 @@
$baseHtml = <<<HTML
<div class="mw-content-ltr">
<div id="qunit"></div>
-<div id="qunit-fixture"></div>
</div>
HTML;
@@ -265,7 +264,6 @@
<title>QUnit</title>
$head
<div id="qunit"></div>
-<div id="qunit-fixture"></div>
HTML;
$html .= "\n" . Html::linkedScript( $url );
diff --git a/package.json b/package.json
index a9b9148..9f27d5f 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,17 @@
"postdoc": "grunt copy:jsduck"
},
"devDependencies": {
- "grunt": "0.4.2",
+ "grunt": "0.4.5",
+ "grunt-cli": "0.1.13",
"grunt-contrib-jshint": "0.9.2",
"grunt-contrib-copy": "0.8.0",
"grunt-contrib-watch": "0.6.1",
- "grunt-jscs-checker": "0.4.1"
+ "grunt-jscs-checker": "0.4.1",
+ "grunt-karma": "0.10.1",
+ "karma": "0.12.31",
+ "karma-chrome-launcher": "0.1.7",
+ "karma-firefox-launcher": "0.1.4",
+ "karma-qunit": "0.1.4",
+ "qunitjs": "1.16.0"
}
}
diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js
index ab9aab1..aca8433e 100644
--- a/tests/qunit/data/testrunner.js
+++ b/tests/qunit/data/testrunner.js
@@ -112,6 +112,34 @@
};
}() );
+ // Extend QUnit.module to provide a fixture element.
+ ( function () {
+ var orgModule = QUnit.module;
+
+ QUnit.module = function ( name, localEnv ) {
+ var fixture;
+ localEnv = localEnv || {};
+ orgModule( name, {
+ setup: function () {
+ fixture = document.createElement( 'div'
);
+ fixture.id = 'qunit-fixture';
+ document.body.appendChild( fixture );
+
+ if ( localEnv.setup ) {
+ localEnv.setup.call( this );
+ }
+ },
+ teardown: function () {
+ if ( localEnv.teardown ) {
+ localEnv.teardown.call( this );
+ }
+
+ fixture.parentNode.removeChild( fixture
);
+ }
+ } );
+ };
+ }() );
+
// Initiate when enabled
if ( QUnit.urlParams.completenesstest ) {
diff --git a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
index 7571b92..795c2bb 100644
--- a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
@@ -15,24 +15,22 @@
assert.equal( $.escapeRE( '0123456789' ), '0123456789',
'escapeRE - Leave numbers alone' );
} );
- QUnit.test( 'Is functions', 15, function ( assert ) {
- assert.strictEqual( $.isDomElement( document.getElementById(
'qunit-header' ) ), true,
- 'isDomElement: #qunit-header Node' );
- assert.strictEqual( $.isDomElement( document.getElementById(
'random-name' ) ), false,
- 'isDomElement: #random-name (null)' );
- assert.strictEqual( $.isDomElement(
document.getElementsByTagName( 'div' ) ), false,
- 'isDomElement: getElementsByTagName Array' );
- assert.strictEqual( $.isDomElement(
document.getElementsByTagName( 'div' )[0] ), true,
- 'isDomElement: getElementsByTagName(..)[0] Node' );
- assert.strictEqual( $.isDomElement( $( 'div' ) ), false,
- 'isDomElement: jQuery object' );
- assert.strictEqual( $.isDomElement( $( 'div' ).get( 0 ) ), true,
- 'isDomElement: jQuery object > Get node' );
+ QUnit.test( 'isDomElement', 6, function ( assert ) {
assert.strictEqual( $.isDomElement( document.createElement(
'div' ) ), true,
- 'isDomElement: createElement' );
+ 'isDomElement: HTMLElement' );
+ assert.strictEqual( $.isDomElement( document.createTextNode( ''
) ), true,
+ 'isDomElement: TextNode' );
+ assert.strictEqual( $.isDomElement( null ), false,
+ 'isDomElement: null' );
+ assert.strictEqual( $.isDomElement(
document.getElementsByTagName( 'div' ) ), false,
+ 'isDomElement: NodeList' );
+ assert.strictEqual( $.isDomElement( $( 'div' ) ), false,
+ 'isDomElement: jQuery' );
assert.strictEqual( $.isDomElement( { foo: 1 } ), false,
- 'isDomElement: Object' );
+ 'isDomElement: Plain Object' );
+ } );
+ QUnit.test( 'isEmpty', 7, function ( assert ) {
assert.strictEqual( $.isEmpty( 'string' ), false, 'isEmpty:
"string"' );
assert.strictEqual( $.isEmpty( '0' ), true, 'isEmpty: "0"' );
assert.strictEqual( $.isEmpty( '' ), true, 'isEmpty: ""' );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
index 995c1ed..2638ed0 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
@@ -1,6 +1,7 @@
( function ( mw, $ ) {
- var mwLanguageCache = {}, formatText, formatParse, formatnumTests,
specialCharactersPageName,
- expectedListUsers, expectedEntrypoints;
+ var formatText, formatParse, formatnumTests, specialCharactersPageName,
expectedListUsers, expectedEntrypoints,
+ mwLanguageCache = {},
+ hasOwn = Object.hasOwnProperty;
// When the expected result is the same in both modes
function assertBothModes( assert, parserArguments, expectedResult,
assertMessage ) {
@@ -59,31 +60,52 @@
}
} ) );
- function getMwLanguage( langCode, cb ) {
- if ( mwLanguageCache[langCode] !== undefined ) {
- mwLanguageCache[langCode].add( cb );
- return;
- }
- mwLanguageCache[langCode] = $.Callbacks( 'once memory' );
- mwLanguageCache[langCode].add( cb );
- $.ajax( {
- url: mw.util.wikiScript( 'load' ),
- data: {
- skin: mw.config.get( 'skin' ),
- lang: langCode,
- debug: mw.config.get( 'debug' ),
- modules: [
- 'mediawiki.language.data',
- 'mediawiki.language'
- ].join( '|' ),
- only: 'scripts'
- },
- dataType: 'script'
- } ).done(function () {
- mwLanguageCache[langCode].fire( mw.language );
- } ).fail( function () {
- mwLanguageCache[langCode].fire( false );
+ /**
+ * Be careful to no run this in parallel as it uses a global identifier
(mw.language)
+ * to transport the module back to the test. It musn't be overwritten
concurrentely.
+ *
+ * This function caches the mw.language data to avoid having to request
the same module
+ * multiple times. There is more than one test case for any given
language.
+ */
+ function getMwLanguage( langCode ) {
+ if ( !hasOwn.call( mwLanguageCache, langCode ) ) {
+ mwLanguageCache[langCode] = $.ajax( {
+ url: mw.util.wikiScript( 'load' ),
+ data: {
+ skin: mw.config.get( 'skin' ),
+ lang: langCode,
+ debug: mw.config.get( 'debug' ),
+ modules: [
+ 'mediawiki.language.data',
+ 'mediawiki.language'
+ ].join( '|' ),
+ only: 'scripts'
+ },
+ dataType: 'script',
+ cache: true
+ } ).then( function () {
+ return mw.language;
} );
+ }
+ return mwLanguageCache[langCode];
+ }
+
+ /**
+ * @param {Function[]} tasks List of functions that perform tasks
+ * that may be asynchronous. Invoke the callback parameter when done.
+ * @param {Function} done When all tasks are done.
+ * @return
+ */
+ function process( tasks, done ) {
+ function run() {
+ var task = tasks.shift();
+ if ( task ) {
+ task( run );
+ } else {
+ done();
+ }
+ }
+ run();
}
QUnit.test( 'Replace', 9, function ( assert ) {
@@ -244,23 +266,27 @@
QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length,
function ( assert ) {
mw.messages.set( mw.libs.phpParserData.messages );
- $.each( mw.libs.phpParserData.tests, function ( i, test ) {
- QUnit.stop();
- getMwLanguage( test.lang, function ( langClass ) {
- QUnit.start();
- if ( !langClass ) {
- assert.ok( false, 'Language "' +
test.lang + '" failed to load' );
- return;
- }
- mw.config.set( 'wgUserLanguage', test.lang );
- var parser = new mw.jqueryMsg.parser( {
language: langClass } );
- assert.equal(
- parser.parse( test.key, test.args
).html(),
- test.result,
- test.name
- );
- } );
+ var tasks = $.map( mw.libs.phpParserData.tests, function ( test
) {
+ return function ( next ) {
+ getMwLanguage( test.lang )
+ .done( function ( langClass ) {
+ mw.config.set(
'wgUserLanguage', test.lang );
+ var parser = new
mw.jqueryMsg.parser( { language: langClass } );
+ assert.equal(
+ parser.parse( test.key,
test.args ).html(),
+ test.result,
+ test.name
+ );
+ } )
+ .fail( function () {
+ assert.ok( false, 'Language "'
+ test.lang + '" failed to load.' );
+ } )
+ .always( next );
+ };
} );
+
+ QUnit.stop();
+ process( tasks, QUnit.start );
} );
QUnit.test( 'Links', 6, function ( assert ) {
@@ -419,8 +445,8 @@
);
} );
-// Tests that getMessageFunction is used for non-plain messages with curly
braces or
-// square brackets, but not otherwise.
+ // Tests that getMessageFunction is used for non-plain messages with
curly braces or
+ // square brackets, but not otherwise.
QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function (
assert ) {
var oldGMF, outerCalled, innerCalled;
@@ -571,25 +597,27 @@
QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) {
mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' );
mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' );
- $.each( formatnumTests, function ( i, test ) {
- QUnit.stop();
- getMwLanguage( test.lang, function ( langClass ) {
- QUnit.start();
- if ( !langClass ) {
- assert.ok( false, 'Language "' + test.lang + '"
failed to load' );
- return;
- }
- mw.messages.set(test.message );
- mw.config.set( 'wgUserLanguage', test.lang );
- var parser = new mw.jqueryMsg.parser( { language:
langClass } );
- assert.equal(
- parser.parse( test.integer ?
'formatnum-msg-int' : 'formatnum-msg',
- [ test.number ] ).html(),
- test.result,
- test.description
- );
- } );
+ var queue = $.map( formatnumTests, function ( test ) {
+ return function ( next ) {
+ getMwLanguage( test.lang )
+ .done( function ( langClass ) {
+ mw.config.set( 'wgUserLanguage',
test.lang );
+ var parser = new mw.jqueryMsg.parser( {
language: langClass } );
+ assert.equal(
+ parser.parse( test.integer ?
'formatnum-msg-int' : 'formatnum-msg',
+ [ test.number ]
).html(),
+ test.result,
+ test.description
+ );
+ } )
+ .fail( function () {
+ assert.ok( false, 'Language "' +
test.lang + '" failed to load' );
+ } )
+ .always( next );
+ };
} );
+ QUnit.stop();
+ process( queue, QUnit.start );
} );
// HTML in wikitext
--
To view, visit https://gerrit.wikimedia.org/r/198288
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I4e96da137340a28789b38940e75d4b6b8bc5d76a
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: REL1_23
Gerrit-Owner: Krinkle <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits