summaryrefslogtreecommitdiff
path: root/www/wiki/tests/qunit/data
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/qunit/data')
-rw-r--r--www/wiki/tests/qunit/data/defineCallMwLoaderTestCallback.js1
-rw-r--r--www/wiki/tests/qunit/data/generateJqueryMsgData.php148
-rw-r--r--www/wiki/tests/qunit/data/load.mock.php107
-rw-r--r--www/wiki/tests/qunit/data/mediawiki.jqueryMsg.data.js492
-rw-r--r--www/wiki/tests/qunit/data/mwLoaderTestCallback.js1
-rw-r--r--www/wiki/tests/qunit/data/requireCallMwLoaderTestCallback.js6
-rw-r--r--www/wiki/tests/qunit/data/styleTest.css.php61
-rw-r--r--www/wiki/tests/qunit/data/testrunner.js652
8 files changed, 1468 insertions, 0 deletions
diff --git a/www/wiki/tests/qunit/data/defineCallMwLoaderTestCallback.js b/www/wiki/tests/qunit/data/defineCallMwLoaderTestCallback.js
new file mode 100644
index 00000000..641071a2
--- /dev/null
+++ b/www/wiki/tests/qunit/data/defineCallMwLoaderTestCallback.js
@@ -0,0 +1 @@
+module.exports = 'Defined.';
diff --git a/www/wiki/tests/qunit/data/generateJqueryMsgData.php b/www/wiki/tests/qunit/data/generateJqueryMsgData.php
new file mode 100644
index 00000000..e4f87f81
--- /dev/null
+++ b/www/wiki/tests/qunit/data/generateJqueryMsgData.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * This PHP script defines the spec that the mediawiki.jqueryMsg module should conform to.
+ *
+ * It does this by looking up the results of various kinds of string parsing, with various
+ * languages, in the current installation of MediaWiki. It then outputs a static specification,
+ * mapping expected inputs to outputs, which can be used fed into a unit test framework.
+ * (QUnit, Jasmine, anything, it just outputs an object with key/value pairs).
+ *
+ * This is similar to Michael Dale (mdale@mediawiki.org)'s parser tests, except that it doesn't
+ * look up the API results while doing the test, so the test run is much faster (at the cost
+ * of being out of date in rare circumstances. But mostly the parsing that we are doing in
+ * Javascript doesn't change much).
+ */
+
+/*
+ * @example QUnit
+ * <code>
+ QUnit.test( 'Output matches PHP parser', function ( assert ) {
+ mw.messages.set( mw.libs.phpParserData.messages );
+ $.each( mw.libs.phpParserData.tests, function ( i, test ) {
+ QUnit.stop();
+ getMwLanguage( test.lang, function ( langClass ) {
+ var parser = new mw.jqueryMsg.Parser( { language: langClass } );
+ assert.equal(
+ parser.parse( test.key, test.args ).html(),
+ test.result,
+ test.name
+ );
+ QUnit.start();
+ } );
+ } );
+ });
+ * </code>
+ *
+ * @example Jasmine
+ * <code>
+ describe( 'match output to output from PHP parser', function () {
+ mw.messages.set( mw.libs.phpParserData.messages );
+ $.each( mw.libs.phpParserData.tests, function ( i, test ) {
+ it( 'should parse ' + test.name, function () {
+ var langClass;
+ runs( function () {
+ getMwLanguage( test.lang, function ( gotIt ) {
+ langClass = gotIt;
+ });
+ });
+ waitsFor( function () {
+ return langClass !== undefined;
+ }, 'Language class should be loaded', 1000 );
+ runs( function () {
+ console.log( test.lang, 'running tests' );
+ var parser = new mw.jqueryMsg.Parser( { language: langClass } );
+ expect(
+ parser.parse( test.key, test.args ).html()
+ ).toEqual( test.result );
+ } );
+ } );
+ } );
+ } );
+ * </code>
+ */
+
+require __DIR__ . '/../../../maintenance/Maintenance.php';
+
+class GenerateJqueryMsgData extends Maintenance {
+
+ public static $keyToTestArgs = [
+ 'undelete_short' => [
+ [ 0 ],
+ [ 1 ],
+ [ 2 ],
+ [ 5 ],
+ [ 21 ],
+ [ 101 ]
+ ],
+ 'category-subcat-count' => [
+ [ 0, 10 ],
+ [ 1, 1 ],
+ [ 1, 2 ],
+ [ 3, 30 ]
+ ]
+ ];
+
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = 'Create a specification for message parsing ini JSON format';
+ // add any other options here
+ }
+
+ public function execute() {
+ list( $messages, $tests ) = $this->getMessagesAndTests();
+ $this->writeJavascriptFile( $messages, $tests, __DIR__ . '/mediawiki.jqueryMsg.data.js' );
+ }
+
+ private function getMessagesAndTests() {
+ $messages = [];
+ $tests = [];
+ foreach ( [ 'en', 'fr', 'ar', 'jp', 'zh' ] as $languageCode ) {
+ foreach ( self::$keyToTestArgs as $key => $testArgs ) {
+ foreach ( $testArgs as $args ) {
+ // Get the raw message, without any transformations.
+ $template = wfMessage( $key )->inLanguage( $languageCode )->plain();
+
+ // Get the magic-parsed version with args.
+ $result = wfMessage( $key, $args )->inLanguage( $languageCode )->text();
+
+ // Record the template, args, language, and expected result
+ // fake multiple languages by flattening them together.
+ $langKey = $languageCode . '_' . $key;
+ $messages[$langKey] = $template;
+ $tests[] = [
+ 'name' => $languageCode . ' ' . $key . ' ' . implode( ',', $args ),
+ 'key' => $langKey,
+ 'args' => $args,
+ 'result' => $result,
+ 'lang' => $languageCode
+ ];
+ }
+ }
+ }
+ return [ $messages, $tests ];
+ }
+
+ private function writeJavascriptFile( $messages, $tests, $dataSpecFile ) {
+ $phpParserData = [
+ 'messages' => $messages,
+ 'tests' => $tests,
+ ];
+
+ $output =
+ "// This file stores the output from the PHP parser for various messages, arguments,\n"
+ . "// languages, and parser modes. Intended for use by a unit test framework by looping\n"
+ . "// through the object and comparing its parser return value with the 'result' property.\n"
+ . '// Last generated with ' . basename( __FILE__ ) . ' at ' . gmdate( 'r' ) . "\n"
+ . "/* eslint-disable */\n"
+ . "\n"
+ . 'mediaWiki.libs.phpParserData = ' . FormatJson::encode( $phpParserData, true ) . ";\n";
+
+ $fp = file_put_contents( $dataSpecFile, $output );
+ if ( $fp === false ) {
+ die( "Couldn't write to $dataSpecFile." );
+ }
+ }
+}
+
+$maintClass = "GenerateJqueryMsgData";
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/www/wiki/tests/qunit/data/load.mock.php b/www/wiki/tests/qunit/data/load.mock.php
new file mode 100644
index 00000000..23009498
--- /dev/null
+++ b/www/wiki/tests/qunit/data/load.mock.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Mock load.php with pre-defined test modules.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @package MediaWiki
+ * @author Lupo
+ * @since 1.20
+ */
+header( 'Content-Type: text/javascript; charset=utf-8' );
+
+$moduleImplementations = [
+ 'testUsesMissing' => "
+mw.loader.implement( 'testUsesMissing', function () {
+ mw.loader.testFail( 'Module usesMissing script should not run.' );
+}, {}, {});
+",
+
+ 'testUsesNestedMissing' => "
+mw.loader.implement( 'testUsesNestedMissing', function () {
+ mw.loader.testFail('Module testUsesNestedMissing script should not run.' );
+}, {}, {});
+",
+
+ 'testSkipped' => "
+mw.loader.implement( 'testSkipped', function () {
+ mw.loader.testFail( false, 'Module testSkipped was supposed to be skipped.' );
+}, {}, {});
+",
+
+ 'testNotSkipped' => "
+mw.loader.implement( 'testNotSkipped', function () {}, {}, {});
+",
+
+ 'testUsesSkippable' => "
+mw.loader.implement( 'testUsesSkippable', function () {}, {}, {});
+",
+
+ 'testUrlInc' => "
+mw.loader.implement( 'testUrlInc', function () {} );
+",
+ 'testUrlInc.a' => "
+mw.loader.implement( 'testUrlInc.a', function () {} );
+",
+ 'testUrlInc.b' => "
+mw.loader.implement( 'testUrlInc.b', function () {} );
+",
+ 'testUrlOrder' => "
+mw.loader.implement( 'testUrlOrder', function () {} );
+",
+ 'testUrlOrder.a' => "
+mw.loader.implement( 'testUrlOrder.a', function () {} );
+",
+ 'testUrlOrder.b' => "
+mw.loader.implement( 'testUrlOrder.b', function () {} );
+",
+];
+
+$response = '';
+
+// Does not support the full behaviour of ResourceLoaderContext::expandModuleNames(),
+// Only supports dotless module names joined by comma,
+// with the exception of the hardcoded cases for testUrl*.
+if ( isset( $_GET['modules'] ) ) {
+ if ( $_GET['modules'] === 'testUrlInc,testUrlIncDump|testUrlInc.a,b' ) {
+ $modules = [ 'testUrlInc', 'testUrlIncDump', 'testUrlInc.a', 'testUrlInc.b' ];
+ } elseif ( $_GET['modules'] === 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b' ) {
+ $modules = [ 'testUrlOrder', 'testUrlOrderDump', 'testUrlOrder.a', 'testUrlOrder.b' ];
+ } else {
+ $modules = explode( ',', $_GET['modules'] );
+ }
+ foreach ( $modules as $module ) {
+ if ( isset( $moduleImplementations[$module] ) ) {
+ $response .= $moduleImplementations[$module];
+ } elseif ( preg_match( '/^test.*Dump$/', $module ) === 1 ) {
+ $queryModules = $_GET['modules'];
+ $queryVersion = isset( $_GET['version'] ) ? strval( $_GET['version'] ) : null;
+ $response .= 'mw.loader.implement( ' . json_encode( $module )
+ . ', function ( $, jQuery, require, module ) {'
+ . 'module.exports.query = { '
+ . 'modules: ' . json_encode( $queryModules ) . ','
+ . 'version: ' . json_encode( $queryVersion )
+ . ' };'
+ . '} );';
+ } else {
+ // Default
+ $response .= 'mw.loader.state(' . json_encode( $module ) . ', "missing" );' . "\n";
+ }
+ }
+}
+
+echo $response;
diff --git a/www/wiki/tests/qunit/data/mediawiki.jqueryMsg.data.js b/www/wiki/tests/qunit/data/mediawiki.jqueryMsg.data.js
new file mode 100644
index 00000000..90dc1b28
--- /dev/null
+++ b/www/wiki/tests/qunit/data/mediawiki.jqueryMsg.data.js
@@ -0,0 +1,492 @@
+// This file stores the output from the PHP parser for various messages, arguments,
+// languages, and parser modes. Intended for use by a unit test framework by looping
+// through the object and comparing its parser return value with the 'result' property.
+// Last generated with generateJqueryMsgData.php at Fri, 10 Jul 2015 11:44:08 +0000
+/* eslint-disable */
+
+mediaWiki.libs.phpParserData = {
+ "messages": {
+ "en_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
+ "en_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
+ "fr_undelete_short": "Restaurer $1 modification{{PLURAL:$1||s}}",
+ "fr_category-subcat-count": "Cette cat\u00e9gorie comprend {{PLURAL:$2|la sous-cat\u00e9gorie|$2 sous-cat\u00e9gories, dont {{PLURAL:$1|celle|les $1}}}} ci-dessous.",
+ "ar_undelete_short": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 {{PLURAL:$1||\u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f|\u062a\u0639\u062f\u064a\u0644\u064a\u0646|$1 \u062a\u0639\u062f\u064a\u0644\u0627\u062a|$1 \u062a\u0639\u062f\u064a\u0644\u0627\u064b|$1 \u062a\u0639\u062f\u064a\u0644}}",
+ "ar_category-subcat-count": "{{PLURAL:$2|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a \u0627\u0644\u062a\u0627\u0644\u064a|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a {{PLURAL:$1||\u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a|\u062a\u0635\u0646\u064a\u0641\u064a\u0646 \u0641\u0631\u0639\u064a\u064a\u0646|$1 \u062a\u0635\u0646\u064a\u0641\u0627\u062a \u0641\u0631\u0639\u064a\u0629}}\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a $2.}}",
+ "jp_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
+ "jp_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
+ "zh_undelete_short": "\u8fd8\u539f{{PLURAL:$1|$1\u4e2a\u7f16\u8f91}}",
+ "zh_category-subcat-count": "{{PLURAL:$2|\u672c\u5206\u7c7b\u53ea\u6709\u4ee5\u4e0b\u5b50\u5206\u7c7b\u3002|\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b$1\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u6709$2\u4e2a\u5b50\u5206\u7c7b\u3002}}"
+ },
+ "tests": [
+ {
+ "name": "en undelete_short 0",
+ "key": "en_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Undelete 0 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 1",
+ "key": "en_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Undelete one edit",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 2",
+ "key": "en_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Undelete 2 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 5",
+ "key": "en_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Undelete 5 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 21",
+ "key": "en_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Undelete 21 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 101",
+ "key": "en_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Undelete 101 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 0,10",
+ "key": "en_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "This category has the following 0 subcategories, out of 10 total.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 1,1",
+ "key": "en_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "This category has only the following subcategory.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 1,2",
+ "key": "en_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "This category has the following subcategory, out of 2 total.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 3,30",
+ "key": "en_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "This category has the following 3 subcategories, out of 30 total.",
+ "lang": "en"
+ },
+ {
+ "name": "fr undelete_short 0",
+ "key": "fr_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Restaurer 0 modification",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 1",
+ "key": "fr_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Restaurer 1 modification",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 2",
+ "key": "fr_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Restaurer 2 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 5",
+ "key": "fr_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Restaurer 5 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 21",
+ "key": "fr_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Restaurer 21 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 101",
+ "key": "fr_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Restaurer 101 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 0,10",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "Cette cat\u00e9gorie comprend 10 sous-cat\u00e9gories, dont celle ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 1,1",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "Cette cat\u00e9gorie comprend la sous-cat\u00e9gorie ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 1,2",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "Cette cat\u00e9gorie comprend 2 sous-cat\u00e9gories, dont celle ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 3,30",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "Cette cat\u00e9gorie comprend 30 sous-cat\u00e9gories, dont les 3 ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "ar undelete_short 0",
+ "key": "ar_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 ",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 1",
+ "key": "ar_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 2",
+ "key": "ar_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644\u064a\u0646",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 5",
+ "key": "ar_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 5 \u062a\u0639\u062f\u064a\u0644\u0627\u062a",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 21",
+ "key": "ar_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 21 \u062a\u0639\u062f\u064a\u0644\u0627\u064b",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 101",
+ "key": "ar_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 101 \u062a\u0639\u062f\u064a\u0644",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 0,10",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 10.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 1,1",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 1.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 1,2",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 2.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 3,30",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a 3 \u062a\u0635\u0646\u064a\u0641\u0627\u062a \u0641\u0631\u0639\u064a\u0629\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 30.",
+ "lang": "ar"
+ },
+ {
+ "name": "jp undelete_short 0",
+ "key": "jp_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Undelete 0 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 1",
+ "key": "jp_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Undelete one edit",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 2",
+ "key": "jp_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Undelete 2 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 5",
+ "key": "jp_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Undelete 5 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 21",
+ "key": "jp_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Undelete 21 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 101",
+ "key": "jp_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Undelete 101 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 0,10",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "This category has the following 0 subcategories, out of 10 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 1,1",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "This category has only the following subcategory.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 1,2",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "This category has the following subcategory, out of 2 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 3,30",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "This category has the following 3 subcategories, out of 30 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "zh undelete_short 0",
+ "key": "zh_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "\u8fd8\u539f0\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 1",
+ "key": "zh_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "\u8fd8\u539f1\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 2",
+ "key": "zh_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "\u8fd8\u539f2\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 5",
+ "key": "zh_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "\u8fd8\u539f5\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 21",
+ "key": "zh_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "\u8fd8\u539f21\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 101",
+ "key": "zh_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "\u8fd8\u539f101\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 0,10",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b0\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670910\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 1,1",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "\u672c\u5206\u7c7b\u53ea\u6709\u4ee5\u4e0b\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 1,2",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b1\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u67092\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 3,30",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b3\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670930\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ }
+ ]
+};
diff --git a/www/wiki/tests/qunit/data/mwLoaderTestCallback.js b/www/wiki/tests/qunit/data/mwLoaderTestCallback.js
new file mode 100644
index 00000000..dd034115
--- /dev/null
+++ b/www/wiki/tests/qunit/data/mwLoaderTestCallback.js
@@ -0,0 +1 @@
+mediaWiki.loader.testCallback();
diff --git a/www/wiki/tests/qunit/data/requireCallMwLoaderTestCallback.js b/www/wiki/tests/qunit/data/requireCallMwLoaderTestCallback.js
new file mode 100644
index 00000000..815a3b48
--- /dev/null
+++ b/www/wiki/tests/qunit/data/requireCallMwLoaderTestCallback.js
@@ -0,0 +1,6 @@
+module.exports = {
+ immediate: require( 'test.require.define' ),
+ later: function () {
+ return require( 'test.require.define' );
+ }
+};
diff --git a/www/wiki/tests/qunit/data/styleTest.css.php b/www/wiki/tests/qunit/data/styleTest.css.php
new file mode 100644
index 00000000..0e845811
--- /dev/null
+++ b/www/wiki/tests/qunit/data/styleTest.css.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Dynamically create a simple stylesheet for unit tests in MediaWiki.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @package MediaWiki
+ * @author Timo Tijhof
+ * @since 1.20
+ */
+header( 'Content-Type: text/css; charset=utf-8' );
+
+/**
+ * Allows characters in ranges [a-z], [A-Z] and [0-9],
+ * in addition to a dot ("."), dash ("-"), space (" ") and hash ("#").
+ * @since 1.20
+ *
+ * @param string $val
+ * @return string Value with any illegal characters removed.
+ */
+function cssfilter( $val ) {
+ return preg_replace( '/[^A-Za-z0-9\.\- #]/', '', $val );
+}
+
+// Do basic sanitization
+$params = array_map( 'cssfilter', $_GET );
+
+// Defaults
+$selector = isset( $params['selector'] ) ? $params['selector'] : '.mw-test-example';
+$property = isset( $params['prop'] ) ? $params['prop'] : 'float';
+$value = isset( $params['val'] ) ? $params['val'] : 'right';
+$wait = isset( $params['wait'] ) ? (int)$params['wait'] : 0; // seconds
+
+sleep( $wait );
+
+$css = "
+/**
+ * Generated " . gmdate( 'r' ) . ".
+ * Waited {$wait}s.
+ */
+
+$selector {
+ $property: $value;
+}
+";
+
+echo trim( $css ) . "\n";
diff --git a/www/wiki/tests/qunit/data/testrunner.js b/www/wiki/tests/qunit/data/testrunner.js
new file mode 100644
index 00000000..06c146c2
--- /dev/null
+++ b/www/wiki/tests/qunit/data/testrunner.js
@@ -0,0 +1,652 @@
+/* global sinon */
+( function ( $, mw, QUnit ) {
+ 'use strict';
+
+ var addons, nested;
+
+ /**
+ * Make a safe copy of localEnv:
+ * - Creates a new object that inherits, instead of modifying the original.
+ * This prevents recursion in the event that a test suite stores inherits
+ * hooks object statically and passes it to multiple QUnit.module() calls.
+ * - Supporting QUnit 1.x 'setup' and 'teardown' hooks
+ * (deprecated in QUnit 1.16, removed in QUnit 2).
+ */
+ function makeSafeEnv( localEnv ) {
+ var wrap = localEnv ? Object.create( localEnv ) : {};
+ if ( wrap.setup ) {
+ wrap.beforeEach = wrap.beforeEach || wrap.setup;
+ }
+ if ( wrap.teardown ) {
+ wrap.afterEach = wrap.afterEach || wrap.teardown;
+ }
+ return wrap;
+ }
+
+ /**
+ * Add bogus to url to prevent IE crazy caching
+ *
+ * @param {string} value a relative path (eg. 'data/foo.js'
+ * or 'data/test.php?foo=bar').
+ * @return {string} Such as 'data/foo.js?131031765087663960'
+ */
+ QUnit.fixurl = function ( value ) {
+ return value + ( /\?/.test( value ) ? '&' : '?' )
+ + String( new Date().getTime() )
+ + String( parseInt( Math.random() * 100000, 10 ) );
+ };
+
+ /**
+ * Configuration
+ */
+
+ // For each test() that is asynchronous, allow this time to pass before
+ // killing the test and assuming timeout failure.
+ QUnit.config.testTimeout = 60 * 1000;
+
+ // Reduce default animation duration from 400ms to 0ms for unit tests
+ // eslint-disable-next-line no-underscore-dangle
+ $.fx.speeds._default = 0;
+
+ // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
+ QUnit.config.urlConfig.push( {
+ id: 'debug',
+ label: 'Enable ResourceLoaderDebug',
+ tooltip: 'Enable debug mode in ResourceLoader',
+ value: 'true'
+ } );
+
+ /**
+ * SinonJS
+ *
+ * Glue code for nicer integration with QUnit setup/teardown
+ * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
+ */
+ sinon.assert.fail = function ( msg ) {
+ QUnit.assert.ok( false, msg );
+ };
+ sinon.assert.pass = function ( msg ) {
+ QUnit.assert.ok( true, msg );
+ };
+ sinon.config = {
+ injectIntoThis: true,
+ injectInto: null,
+ properties: [ 'spy', 'stub', 'mock', 'sandbox' ],
+ // Don't fake timers by default
+ useFakeTimers: false,
+ useFakeServer: false
+ };
+ // Extend QUnit.module with:
+ // - Add support for QUnit 1.x 'setup' and 'teardown' hooks
+ // - Add a Sinon sandbox to the test context.
+ // - Add a test fixture to the test context.
+ ( function () {
+ var orgModule = QUnit.module;
+ QUnit.module = function ( name, localEnv, executeNow ) {
+ var orgExecute, orgBeforeEach, orgAfterEach;
+ if ( nested ) {
+ // In a nested module, don't re-add our hooks, QUnit does that already.
+ return orgModule.apply( this, arguments );
+ }
+ if ( arguments.length === 2 && typeof localEnv === 'function' ) {
+ executeNow = localEnv;
+ localEnv = undefined;
+ }
+ if ( executeNow ) {
+ // Wrap executeNow() so that we can detect nested modules
+ orgExecute = executeNow;
+ executeNow = function () {
+ var ret;
+ nested = true;
+ ret = orgExecute.apply( this, arguments );
+ nested = false;
+ return ret;
+ };
+ }
+
+ localEnv = makeSafeEnv( localEnv );
+ orgBeforeEach = localEnv.beforeEach;
+ orgAfterEach = localEnv.afterEach;
+
+ localEnv.beforeEach = function () {
+ // Sinon sandbox
+ var config = sinon.getConfig( sinon.config );
+ config.injectInto = this;
+ sinon.sandbox.create( config );
+
+ // Fixture element
+ this.fixture = document.createElement( 'div' );
+ this.fixture.id = 'qunit-fixture';
+ document.body.appendChild( this.fixture );
+
+ if ( orgBeforeEach ) {
+ return orgBeforeEach.apply( this, arguments );
+ }
+ };
+ localEnv.afterEach = function () {
+ var ret;
+ if ( orgAfterEach ) {
+ ret = orgAfterEach.apply( this, arguments );
+ }
+ this.sandbox.verifyAndRestore();
+ this.fixture.parentNode.removeChild( this.fixture );
+ return ret;
+ };
+
+ return orgModule( name, localEnv, executeNow );
+ };
+ }() );
+
+ /**
+ * Reset mw.config and others to a fresh copy of the live config for each test(),
+ * and restore it back to the live one afterwards.
+ *
+ * @param {Object} [localEnv]
+ * @example (see test suite at the bottom of this file)
+ * </code>
+ */
+ QUnit.newMwEnvironment = ( function () {
+ var warn, error, liveConfig, liveMessages,
+ MwMap = mw.config.constructor, // internal use only
+ ajaxRequests = [];
+
+ liveConfig = mw.config;
+ liveMessages = mw.messages;
+
+ function suppressWarnings() {
+ if ( warn === undefined ) {
+ warn = mw.log.warn;
+ error = mw.log.error;
+ mw.log.warn = mw.log.error = $.noop;
+ }
+ }
+
+ function restoreWarnings() {
+ // Guard against calls not balanced with suppressWarnings()
+ if ( warn !== undefined ) {
+ mw.log.warn = warn;
+ mw.log.error = error;
+ warn = error = undefined;
+ }
+ }
+
+ function freshConfigCopy( custom ) {
+ var copy;
+ // Tests should mock all factors that directly influence the tested code.
+ // For backwards compatibility though we set mw.config to a fresh copy of the live
+ // config. This way any modifications made to mw.config during the test will not
+ // affect other tests, nor the global scope outside the test runner.
+ // This is a shallow copy, since overriding an array or object value via "custom"
+ // should replace it. Setting a config property means you override it, not extend it.
+ // NOTE: It is important that we suppress warnings because extend() will also access
+ // deprecated properties and trigger deprecation warnings from mw.log#deprecate.
+ suppressWarnings();
+ copy = $.extend( {}, liveConfig.get(), custom );
+ restoreWarnings();
+
+ return copy;
+ }
+
+ function freshMessagesCopy( custom ) {
+ return $.extend( /* deep */true, {}, liveMessages.get(), custom );
+ }
+
+ /**
+ * @param {jQuery.Event} event
+ * @param {jqXHR} jqXHR
+ * @param {Object} ajaxOptions
+ */
+ function trackAjax( event, jqXHR, ajaxOptions ) {
+ ajaxRequests.push( { xhr: jqXHR, options: ajaxOptions } );
+ }
+
+ return function ( orgEnv ) {
+ var localEnv, orgBeforeEach, orgAfterEach;
+
+ localEnv = makeSafeEnv( orgEnv );
+ // MediaWiki env testing
+ localEnv.config = localEnv.config || {};
+ localEnv.messages = localEnv.messages || {};
+
+ orgBeforeEach = localEnv.beforeEach;
+ orgAfterEach = localEnv.afterEach;
+
+ localEnv.beforeEach = function () {
+ // Greetings, mock environment!
+ mw.config = new MwMap();
+ mw.config.set( freshConfigCopy( localEnv.config ) );
+ mw.messages = new MwMap();
+ mw.messages.set( freshMessagesCopy( localEnv.messages ) );
+ // Update reference to mw.messages
+ mw.jqueryMsg.setParserDefaults( {
+ messages: mw.messages
+ } );
+
+ this.suppressWarnings = suppressWarnings;
+ this.restoreWarnings = restoreWarnings;
+
+ // Start tracking ajax requests
+ $( document ).on( 'ajaxSend', trackAjax );
+
+ if ( orgBeforeEach ) {
+ return orgBeforeEach.apply( this, arguments );
+ }
+ };
+ localEnv.afterEach = function () {
+ var timers, pending, $activeLen, ret;
+
+ if ( orgAfterEach ) {
+ ret = orgAfterEach.apply( this, arguments );
+ }
+
+ // Stop tracking ajax requests
+ $( document ).off( 'ajaxSend', trackAjax );
+
+ // As a convenience feature, automatically restore warnings if they're
+ // still suppressed by the end of the test.
+ restoreWarnings();
+
+ // Farewell, mock environment!
+ mw.config = liveConfig;
+ mw.messages = liveMessages;
+ // Restore reference to mw.messages
+ mw.jqueryMsg.setParserDefaults( {
+ messages: liveMessages
+ } );
+
+ // Tests should use fake timers or wait for animations to complete
+ // Check for incomplete animations/requests/etc and throw if there are any.
+ if ( $.timers && $.timers.length !== 0 ) {
+ timers = $.timers.length;
+ $.each( $.timers, function ( i, timer ) {
+ var node = timer.elem;
+ mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' +
+ mw.html.element( node.nodeName.toLowerCase(), $( node ).getAttrs() )
+ );
+ } );
+ // Force animations to stop to give the next test a clean start
+ $.timers = [];
+ $.fx.stop();
+
+ throw new Error( 'Unfinished animations: ' + timers );
+ }
+
+ // Test should use fake XHR, wait for requests, or call abort()
+ $activeLen = $.active;
+ if ( $activeLen !== undefined && $activeLen !== 0 ) {
+ pending = ajaxRequests.filter( function ( ajax ) {
+ return ajax.xhr.state() === 'pending';
+ } );
+ if ( pending.length !== $activeLen ) {
+ mw.log.warn( 'Pending requests does not match jQuery.active count' );
+ }
+ // Force requests to stop to give the next test a clean start
+ ajaxRequests.forEach( function ( ajax, i ) {
+ mw.log.warn(
+ 'AJAX request #' + i + ' (state: ' + ajax.xhr.state() + ')',
+ ajax.options
+ );
+ ajax.xhr.abort();
+ } );
+ ajaxRequests = [];
+
+ throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' );
+ }
+
+ return ret;
+ };
+ return localEnv;
+ };
+ }() );
+
+ // $.when stops as soon as one fails, which makes sense in most
+ // practical scenarios, but not in a unit test where we really do
+ // need to wait until all of them are finished.
+ QUnit.whenPromisesComplete = function () {
+ var altPromises = [];
+
+ $.each( arguments, function ( i, arg ) {
+ var alt = $.Deferred();
+ altPromises.push( alt );
+
+ // Whether this one fails or not, forwards it to
+ // the 'done' (resolve) callback of the alternative promise.
+ arg.always( alt.resolve );
+ } );
+
+ return $.when.apply( $, altPromises );
+ };
+
+ /**
+ * Recursively convert a node to a plain object representing its structure.
+ * Only considers attributes and contents (elements and text nodes).
+ * Attribute values are compared strictly and not normalised.
+ *
+ * @param {Node} node
+ * @return {Object|string} Plain JavaScript value representing the node.
+ */
+ function getDomStructure( node ) {
+ var $node, children, processedChildren, i, len, el;
+ $node = $( node );
+ if ( node.nodeType === Node.ELEMENT_NODE ) {
+ children = $node.contents();
+ processedChildren = [];
+ for ( i = 0, len = children.length; i < len; i++ ) {
+ el = children[ i ];
+ if ( el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.TEXT_NODE ) {
+ processedChildren.push( getDomStructure( el ) );
+ }
+ }
+
+ return {
+ tagName: node.tagName,
+ attributes: $node.getAttrs(),
+ contents: processedChildren
+ };
+ } else {
+ // Should be text node
+ return $node.text();
+ }
+ }
+
+ /**
+ * Gets structure of node for this HTML.
+ *
+ * @param {string} html HTML markup for one or more nodes.
+ */
+ function getHtmlStructure( html ) {
+ var el = $( '<div>' ).append( html )[ 0 ];
+ return getDomStructure( el );
+ }
+
+ /**
+ * Add-on assertion helpers
+ */
+ // Define the add-ons
+ addons = {
+
+ // Expect boolean true
+ assertTrue: function ( actual, message ) {
+ this.pushResult( {
+ result: actual === true,
+ actual: actual,
+ expected: true,
+ message: message
+ } );
+ },
+
+ // Expect boolean false
+ assertFalse: function ( actual, message ) {
+ this.pushResult( {
+ result: actual === false,
+ actual: actual,
+ expected: false,
+ message: message
+ } );
+ },
+
+ // Expect numerical value less than X
+ lt: function ( actual, expected, message ) {
+ this.pushResult( {
+ result: actual < expected,
+ actual: actual,
+ expected: 'less than ' + expected,
+ message: message
+ } );
+ },
+
+ // Expect numerical value less than or equal to X
+ ltOrEq: function ( actual, expected, message ) {
+ this.pushResult( {
+ result: actual <= expected,
+ actual: actual,
+ expected: 'less than or equal to ' + expected,
+ message: message
+ } );
+ },
+
+ // Expect numerical value greater than X
+ gt: function ( actual, expected, message ) {
+ this.pushResult( {
+ result: actual > expected,
+ actual: actual,
+ expected: 'greater than ' + expected,
+ message: message
+ } );
+ },
+
+ // Expect numerical value greater than or equal to X
+ gtOrEq: function ( actual, expected, message ) {
+ this.pushResult( {
+ result: actual >= true,
+ actual: actual,
+ expected: 'greater than or equal to ' + expected,
+ message: message
+ } );
+ },
+
+ /**
+ * Asserts that two HTML strings are structurally equivalent.
+ *
+ * @param {string} actualHtml Actual HTML markup.
+ * @param {string} expectedHtml Expected HTML markup
+ * @param {string} message Assertion message.
+ */
+ htmlEqual: function ( actualHtml, expectedHtml, message ) {
+ var actual = getHtmlStructure( actualHtml ),
+ expected = getHtmlStructure( expectedHtml );
+ this.pushResult( {
+ result: QUnit.equiv( actual, expected ),
+ actual: actual,
+ expected: expected,
+ message: message
+ } );
+ },
+
+ /**
+ * Asserts that two HTML strings are not structurally equivalent.
+ *
+ * @param {string} actualHtml Actual HTML markup.
+ * @param {string} expectedHtml Expected HTML markup.
+ * @param {string} message Assertion message.
+ */
+ notHtmlEqual: function ( actualHtml, expectedHtml, message ) {
+ var actual = getHtmlStructure( actualHtml ),
+ expected = getHtmlStructure( expectedHtml );
+
+ this.pushResult( {
+ result: !QUnit.equiv( actual, expected ),
+ actual: actual,
+ expected: expected,
+ message: message,
+ negative: true
+ } );
+ }
+ };
+
+ $.extend( QUnit.assert, addons );
+
+ /**
+ * Small test suite to confirm proper functionality of the utilities and
+ * initializations defined above in this file.
+ */
+ QUnit.module( 'testrunner', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.mwHtmlLive = mw.html;
+ mw.html = {
+ escape: function () {
+ return 'mocked';
+ }
+ };
+ },
+ teardown: function () {
+ mw.html = this.mwHtmlLive;
+ },
+ config: {
+ testVar: 'foo'
+ },
+ messages: {
+ testMsg: 'Foo.'
+ }
+ } ) );
+
+ QUnit.test( 'Setup', function ( assert ) {
+ assert.equal( mw.html.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
+ assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
+ assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
+
+ mw.config.set( 'testVar', 'bar' );
+ mw.messages.set( 'testMsg', 'Bar.' );
+ } );
+
+ QUnit.test( 'Teardown', function ( assert ) {
+ assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
+ assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
+ } );
+
+ QUnit.test( 'Loader status', function ( assert ) {
+ var i, len, state,
+ modules = mw.loader.getModuleNames(),
+ error = [],
+ missing = [];
+
+ for ( i = 0, len = modules.length; i < len; i++ ) {
+ state = mw.loader.getState( modules[ i ] );
+ if ( state === 'error' ) {
+ error.push( modules[ i ] );
+ } else if ( state === 'missing' ) {
+ missing.push( modules[ i ] );
+ }
+ }
+
+ assert.deepEqual( error, [], 'Modules in error state' );
+ assert.deepEqual( missing, [], 'Modules in missing state' );
+ } );
+
+ QUnit.test( 'assert.htmlEqual', function ( assert ) {
+ assert.htmlEqual(
+ '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
+ '<div><p data-length=\'10\' class=\'some classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
+ 'Attribute order, spacing and quotation marks (equal)'
+ );
+
+ assert.notHtmlEqual(
+ '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
+ '<div><p data-length=\'10\' class=\'some more classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
+ 'Attribute order, spacing and quotation marks (not equal)'
+ );
+
+ assert.htmlEqual(
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ 'Multiple root nodes (equal)'
+ );
+
+ assert.notHtmlEqual(
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
+ 'Multiple root nodes (not equal, last label node is different)'
+ );
+
+ assert.htmlEqual(
+ 'fo&quot;o<br/>b&gt;ar',
+ 'fo"o<br/>b>ar',
+ 'Extra escaping is equal'
+ );
+ assert.notHtmlEqual(
+ 'foo&lt;br/&gt;bar',
+ 'foo<br/>bar',
+ 'Text escaping (not equal)'
+ );
+
+ assert.htmlEqual(
+ 'foo<a href="http://example.com">example</a>bar',
+ 'foo<a href="http://example.com">example</a>bar',
+ 'Outer text nodes are compared (equal)'
+ );
+
+ assert.notHtmlEqual(
+ 'foo<a href="http://example.com">example</a>bar',
+ 'foo<a href="http://example.com">example</a>quux',
+ 'Outer text nodes are compared (last text node different)'
+ );
+ } );
+
+ QUnit.module( 'testrunner-after', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Teardown', function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '&lt;', 'teardown() callback was ran.' );
+ assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
+ assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
+ } );
+
+ QUnit.module( 'testrunner-each', {
+ beforeEach: function () {
+ this.mwHtmlLive = mw.html;
+ },
+ afterEach: function () {
+ mw.html = this.mwHtmlLive;
+ }
+ } );
+ QUnit.test( 'beforeEach', function ( assert ) {
+ assert.ok( this.mwHtmlLive, 'setup() ran' );
+ mw.html = null;
+ } );
+ QUnit.test( 'afterEach', function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '&lt;', 'afterEach() ran' );
+ } );
+
+ QUnit.module( 'testrunner-each-compat', {
+ setup: function () {
+ this.mwHtmlLive = mw.html;
+ },
+ teardown: function () {
+ mw.html = this.mwHtmlLive;
+ }
+ } );
+ QUnit.test( 'setup', function ( assert ) {
+ assert.ok( this.mwHtmlLive, 'setup() ran' );
+ mw.html = null;
+ } );
+ QUnit.test( 'teardown', function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '&lt;', 'teardown() ran' );
+ } );
+
+ // Regression test for 'this.sandbox undefined' error, fixed by
+ // ensuring Sinon setup/teardown is not re-run on inner module.
+ QUnit.module( 'testrunner-nested', function () {
+ QUnit.module( 'testrunner-nested-inner', function () {
+ QUnit.test( 'Dummy', function ( assert ) {
+ assert.ok( true, 'Nested modules supported' );
+ } );
+ } );
+ } );
+
+ QUnit.module( 'testrunner-hooks-outer', function () {
+ var beforeHookWasExecuted = false,
+ afterHookWasExecuted = false;
+ QUnit.module( 'testrunner-hooks', {
+ before: function () {
+ beforeHookWasExecuted = true;
+
+ // This way we can be sure that module `testrunner-hook-after` will always
+ // be executed after module `testrunner-hooks`
+ QUnit.module( 'testrunner-hooks-after' );
+ QUnit.test(
+ '`after` hook for module `testrunner-hooks` was executed',
+ function ( assert ) {
+ assert.ok( afterHookWasExecuted );
+ }
+ );
+ },
+ after: function () {
+ afterHookWasExecuted = true;
+ }
+ } );
+
+ QUnit.test( '`before` hook was executed', function ( assert ) {
+ assert.ok( beforeHookWasExecuted );
+ } );
+ } );
+
+}( jQuery, mediaWiki, QUnit ) );