diff options
Diffstat (limited to 'tests/qunit/data')
-rw-r--r-- | tests/qunit/data/callMwLoaderTestCallback.js | 1 | ||||
-rw-r--r-- | tests/qunit/data/generateJqueryMsgData.php | 149 | ||||
-rw-r--r-- | tests/qunit/data/load.mock.php | 74 | ||||
-rw-r--r-- | tests/qunit/data/mediawiki.jqueryMsg.data.js | 491 | ||||
-rw-r--r-- | tests/qunit/data/qunitOkCall.js | 2 | ||||
-rw-r--r-- | tests/qunit/data/styleTest.css.php | 61 | ||||
-rw-r--r-- | tests/qunit/data/testrunner.js | 547 |
7 files changed, 1325 insertions, 0 deletions
diff --git a/tests/qunit/data/callMwLoaderTestCallback.js b/tests/qunit/data/callMwLoaderTestCallback.js new file mode 100644 index 00000000..dd034115 --- /dev/null +++ b/tests/qunit/data/callMwLoaderTestCallback.js @@ -0,0 +1 @@ +mediaWiki.loader.testCallback(); diff --git a/tests/qunit/data/generateJqueryMsgData.php b/tests/qunit/data/generateJqueryMsgData.php new file mode 100644 index 00000000..61ebbf8f --- /dev/null +++ b/tests/qunit/data/generateJqueryMsgData.php @@ -0,0 +1,149 @@ +<?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', 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 ) { + 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 = array( + 'undelete_short' => array( + array( 0 ), + array( 1 ), + array( 2 ), + array( 5 ), + array( 21 ), + array( 101 ) + ), + 'category-subcat-count' => array( + array( 0, 10 ), + array( 1, 1 ), + array( 1, 2 ), + array( 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 = array(); + $tests = array(); + foreach ( array( '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[] = array( + 'name' => $languageCode . ' ' . $key . ' ' . join( ',', $args ), + 'key' => $langKey, + 'args' => $args, + 'result' => $result, + 'lang' => $languageCode + ); + } + } + } + return array( $messages, $tests ); + } + + private function writeJavascriptFile( $messages, $tests, $dataSpecFile ) { + $phpParserData = array( + '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" + // This file will contain unquoted JSON strings as javascript native object literals, + // flip the quotemark convention for this file. + . "\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/tests/qunit/data/load.mock.php b/tests/qunit/data/load.mock.php new file mode 100644 index 00000000..ee729e60 --- /dev/null +++ b/tests/qunit/data/load.mock.php @@ -0,0 +1,74 @@ +<?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' ); + +require_once __DIR__ . '/../../../includes/json/FormatJson.php'; +require_once __DIR__ . '/../../../includes/Xml.php'; + +$moduleImplementations = array( + 'testUsesMissing' => " +mw.loader.implement( 'testUsesMissing', function () { + QUnit.ok( false, 'Module usesMissing script should not run.' ); + QUnit.start(); +}, {}, {}); +", + + 'testUsesNestedMissing' => " +mw.loader.implement( 'testUsesNestedMissing', function () { + QUnit.ok( false, 'Module testUsesNestedMissing script should not run.' ); + QUnit.start(); +}, {}, {}); +", + + 'testSkipped' =>" +mw.loader.implement( 'testSkipped', function () { + QUnit.ok( false, 'Module testSkipped was supposed to be skipped.' ); +}, {}, {}); +", + + 'testNotSkipped' =>" +mw.loader.implement( 'testNotSkipped', function () {}, {}, {}); +", + + 'testUsesSkippable' =>" +mw.loader.implement( 'testUsesSkippable', function () {}, {}, {}); +", +); + +$response = ''; + +// Only support for non-encoded module names, full module names expected +if ( isset( $_GET['modules'] ) ) { + $modules = explode( ',', $_GET['modules'] ); + foreach ( $modules as $module ) { + if ( isset( $moduleImplementations[$module] ) ) { + $response .= $moduleImplementations[$module]; + } else { + $response .= Xml::encodeJsCall( 'mw.loader.state', array( $module, 'missing' ), true ); + } + } +} + +echo $response; diff --git a/tests/qunit/data/mediawiki.jqueryMsg.data.js b/tests/qunit/data/mediawiki.jqueryMsg.data.js new file mode 100644 index 00000000..4ab5f146 --- /dev/null +++ b/tests/qunit/data/mediawiki.jqueryMsg.data.js @@ -0,0 +1,491 @@ +// 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 Thu, 30 Jan 2014 04:04:41 +0000 + +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/tests/qunit/data/qunitOkCall.js b/tests/qunit/data/qunitOkCall.js new file mode 100644 index 00000000..3ed5514e --- /dev/null +++ b/tests/qunit/data/qunitOkCall.js @@ -0,0 +1,2 @@ +QUnit.start(); +QUnit.assert.ok( true, 'Successfully loaded!' ); diff --git a/tests/qunit/data/styleTest.css.php b/tests/qunit/data/styleTest.css.php new file mode 100644 index 00000000..0e845811 --- /dev/null +++ b/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/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js new file mode 100644 index 00000000..db312b21 --- /dev/null +++ b/tests/qunit/data/testrunner.js @@ -0,0 +1,547 @@ +/*global CompletenessTest, sinon */ +/*jshint evil: true */ +( function ( $, mw, QUnit ) { + 'use strict'; + + var mwTestIgnore, mwTester, + addons, + ELEMENT_NODE = 1, + TEXT_NODE = 3; + + /** + * Add bogus to url to prevent IE crazy caching + * + * @param value {String} 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 + */ + + // When a test() indicates asynchronicity with stop(), + // allow 10 seconds to pass before killing the test(), + // and assuming failure. + QUnit.config.testTimeout = 10 * 1000; + + // 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' + } ); + + QUnit.config.requireExpects = true; + + /** + * Load TestSwarm agent + */ + // Only if the current url indicates that there is a TestSwarm instance watching us + // (TestSwarm appends swarmURL to the test suites url it loads in iframes). + // Otherwise this is just a simple view of Special:JavaScriptTest/qunit directly, + // no point in loading inject.js in that case. Also, make sure that this instance + // of MediaWiki has actually been configured with the required url to that inject.js + // script. By default it is false. + if ( QUnit.urlParams.swarmURL && mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) { + jQuery.getScript( QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) ); + } + + /** + * CompletenessTest + * + * Adds toggle checkbox to header + */ + QUnit.config.urlConfig.push( { + id: 'completenesstest', + label: 'Run CompletenessTest', + tooltip: 'Run the completeness test' + } ); + + /** + * SinonJS + * + * Glue code for nicer integration with QUnit setup/teardown + * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js + * Fixes: + * - Work properly with asynchronous QUnit by using module setup/teardown + * instead of synchronously wrapping QUnit.test. + */ + 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 + }; + ( function () { + var orgModule = QUnit.module; + + QUnit.module = function ( name, localEnv ) { + localEnv = localEnv || {}; + orgModule( name, { + setup: function () { + var config = sinon.getConfig( sinon.config ); + config.injectInto = this; + sinon.sandbox.create( config ); + + if ( localEnv.setup ) { + localEnv.setup.call( this ); + } + }, + teardown: function () { + this.sandbox.verifyAndRestore(); + + if ( localEnv.teardown ) { + localEnv.teardown.call( this ); + } + } + } ); + }; + }() ); + + // 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 ) { + + // Return true to ignore + mwTestIgnore = function ( val, tester ) { + + // 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; + return true; + } + if ( val instanceof mw.Title ) { + tester.methodCallTracker.Title = true; + return true; + } + + // Don't record methods of the properties of a jQuery object + if ( val instanceof $ ) { + return true; + } + + // Don't iterate over the module registry (the 'script' references would + // be listed as untested methods otherwise) + if ( val === mw.loader.moduleRegistry ) { + return true; + } + + return false; + }; + + 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 and others to a fresh copy of the live config for each test(), + * and restore it back to the live one afterwards. + * @param localEnv {Object} [optional] + * @example (see test suite at the bottom of this file) + * </code> + */ + QUnit.newMwEnvironment = ( function () { + var warn, log, liveConfig, liveMessages; + + liveConfig = mw.config.values; + liveMessages = mw.messages.values; + + function suppressWarnings() { + warn = mw.log.warn; + mw.log.warn = $.noop; + } + + function restoreWarnings() { + if ( warn !== undefined ) { + mw.log.warn = warn; + warn = 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, custom ); + restoreWarnings(); + + return copy; + } + + function freshMessagesCopy( custom ) { + return $.extend( /*deep=*/true, {}, liveMessages, custom ); + } + + log = QUnit.urlParams.mwlogenv ? mw.log : function () {}; + + return function ( localEnv ) { + localEnv = $.extend( { + // QUnit + setup: $.noop, + teardown: $.noop, + // MediaWiki + config: {}, + messages: {} + }, localEnv ); + + return { + setup: function () { + log( 'MwEnvironment> SETUP for "' + QUnit.config.current.module + + ': ' + QUnit.config.current.testName + '"' ); + + // Greetings, mock environment! + mw.config.values = freshConfigCopy( localEnv.config ); + mw.messages.values = freshMessagesCopy( localEnv.messages ); + this.suppressWarnings = suppressWarnings; + this.restoreWarnings = restoreWarnings; + + localEnv.setup.call( this ); + }, + + teardown: function () { + log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module + + ': ' + QUnit.config.current.testName + '"' ); + + localEnv.teardown.call( this ); + + // Farewell, mock environment! + mw.config.values = liveConfig; + mw.messages.values = liveMessages; + + // As a convenience feature, automatically restore warnings if they're + // still suppressed by the end of the test. + restoreWarnings(); + + // Check for incomplete animations/requests/etc and throw + // error if there are any. + if ( $.timers && $.timers.length !== 0 ) { + // Test may need to use fake timers, wait for animations or + // call $.fx.stop(). + throw new Error( 'Unfinished animations: ' + $.timers.length ); + } + if ( $.active !== undefined && $.active !== 0 ) { + // Test may need to use fake XHR, wait for requests or + // call abort(). + throw new Error( 'Unfinished AJAX requests: ' + $.active ); + } + } + }; + }; + }() ); + + // $.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 === ELEMENT_NODE ) { + children = $node.contents(); + processedChildren = []; + for ( i = 0, len = children.length; i < len; i++ ) { + el = children[i]; + if ( el.nodeType === ELEMENT_NODE || el.nodeType === 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 ) { + QUnit.push( actual === true, actual, true, message ); + }, + + // Expect boolean false + assertFalse: function ( actual, message ) { + QUnit.push( actual === false, actual, false, message ); + }, + + // Expect numerical value less than X + 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 ) { + QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message ); + }, + + // Expect numerical value greater than X + 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 ) { + QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, 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 ); + + QUnit.push( + QUnit.equiv( + actual, + expected + ), + actual, + expected, + 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 ); + + QUnit.push( + !QUnit.equiv( + actual, + expected + ), + actual, + expected, + message + ); + } + }; + + $.extend( QUnit.assert, addons ); + + /** + * Small test suite to confirm proper functionality of the utilities and + * initializations defined above in this file. + */ + QUnit.module( 'test.mediawiki.qunit.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', 3, 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', 2, 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', 2, 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( 'htmlEqual', 8, 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"o<br/>b>ar', + 'fo"o<br/>b>ar', + 'Extra escaping is equal' + ); + assert.notHtmlEqual( + 'foo<br/>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( 'test.mediawiki.qunit.testrunner-after', QUnit.newMwEnvironment() ); + + QUnit.test( 'Teardown', 3, function ( assert ) { + assert.equal( mw.html.escape( '<' ), '<', '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()' ); + } ); + +}( jQuery, mediaWiki, QUnit ) ); |