summaryrefslogtreecommitdiff
path: root/tests/qunit
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2013-12-08 09:55:49 +0100
committerPierre Schmitz <pierre@archlinux.de>2013-12-08 09:55:49 +0100
commit4ac9fa081a7c045f6a9f1cfc529d82423f485b2e (patch)
treeaf68743f2f4a47d13f2b0eb05f5c4aaf86d8ea37 /tests/qunit
parentaf4da56f1ad4d3ef7b06557bae365da2ea27a897 (diff)
Update to MediaWiki 1.22.0
Diffstat (limited to 'tests/qunit')
-rw-r--r--tests/qunit/QUnitTestResources.php4
-rw-r--r--tests/qunit/data/generateJqueryMsgData.php4
-rw-r--r--tests/qunit/data/load.mock.php3
-rw-r--r--tests/qunit/data/testrunner.js29
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.byteLength.test.js24
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js120
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.client.test.js283
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.localize.test.js10
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js287
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js166
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.textSelection.test.js7
-rw-r--r--tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js2
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js268
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js337
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.test.js171
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js6
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js91
-rw-r--r--tests/qunit/suites/resources/startup.test.js129
18 files changed, 1609 insertions, 332 deletions
diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php
index 01072d83..c8743750 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -6,6 +6,7 @@ return array(
'mediawiki.tests.qunit.suites' => array(
'scripts' => array(
+ 'tests/qunit/suites/resources/startup.test.js',
'tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js',
'tests/qunit/suites/resources/jquery/jquery.byteLength.test.js',
'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js',
@@ -16,6 +17,7 @@ return array(
'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js',
'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js',
'tests/qunit/suites/resources/jquery/jquery.localize.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js',
'tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js',
'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js',
'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js',
@@ -45,6 +47,7 @@ return array(
'jquery.hidpi',
'jquery.highlightText',
'jquery.localize',
+ 'jquery.makeCollapsible',
'jquery.mwExtension',
'jquery.tabIndex',
'jquery.tablesorter',
@@ -61,6 +64,5 @@ return array(
'mediawiki.language',
'mediawiki.cldr',
),
- 'position' => 'top',
)
);
diff --git a/tests/qunit/data/generateJqueryMsgData.php b/tests/qunit/data/generateJqueryMsgData.php
index 604ede81..12e5a2dc 100644
--- a/tests/qunit/data/generateJqueryMsgData.php
+++ b/tests/qunit/data/generateJqueryMsgData.php
@@ -61,7 +61,7 @@
* </code>
*/
-require( __DIR__ . '/../../../maintenance/Maintenance.php' );
+require __DIR__ . '/../../../maintenance/Maintenance.php';
class GenerateJqueryMsgData extends Maintenance {
@@ -147,4 +147,4 @@ class GenerateJqueryMsgData extends Maintenance {
}
$maintClass = "GenerateJqueryMsgData";
-require_once( RUN_MAINTENANCE_IF_MAIN );
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/tests/qunit/data/load.mock.php b/tests/qunit/data/load.mock.php
index 7ff392ab..f6eff77a 100644
--- a/tests/qunit/data/load.mock.php
+++ b/tests/qunit/data/load.mock.php
@@ -24,6 +24,7 @@
*/
header( 'Content-Type: text/javascript; charset=utf-8' );
+require_once __DIR__ . '/../../../includes/json/FormatJson.php';
require_once __DIR__ . '/../../../includes/Xml.php';
$moduleImplementations = array(
@@ -50,7 +51,7 @@ if ( isset( $_GET['modules'] ) ) {
if ( isset( $moduleImplementations[$module] ) ) {
$response .= $moduleImplementations[$module];
} else {
- $response .= Xml::encodeJsCall( 'mw.loader.state', array( $module, 'missing' ) );
+ $response .= Xml::encodeJsCall( 'mw.loader.state', array( $module, 'missing' ), true );
}
}
}
diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js
index 62dd81ac..1a2bfa10 100644
--- a/tests/qunit/data/testrunner.js
+++ b/tests/qunit/data/testrunner.js
@@ -38,6 +38,8 @@
tooltip: 'Enable debug mode in ResourceLoader'
} );
+ QUnit.config.requireExpects = true;
+
/**
* Load TestSwarm agent
*/
@@ -48,13 +50,14 @@
// 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' ) ) {
- document.write( '<scr' + 'ipt src="' + QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) + '"></scr' + 'ipt>' );
+ jQuery.getScript( QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) );
}
/**
* CompletenessTest
+ *
+ * Adds toggle checkbox to header
*/
- // Adds toggle checkbox to header
QUnit.config.urlConfig.push( {
id: 'completenesstest',
label: 'Run CompletenessTest',
@@ -93,8 +96,9 @@
/**
* Test environment recommended for all QUnit test modules
+ *
+ * Whether to log environment changes to the console
*/
- // Whether to log environment changes to the console
QUnit.config.urlConfig.push( 'mwlogenv' );
/**
@@ -347,6 +351,25 @@
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>',
diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
index e4e579b0..e6aa3aa8 100644
--- a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
@@ -16,20 +16,22 @@
} );
- QUnit.test( 'Special text', 5, function ( assert ) {
- // http://en.wikipedia.org/wiki/UTF-8
+ QUnit.test( 'Special text', 4, function ( assert ) {
+ // https://en.wikipedia.org/wiki/UTF-8
var u0024 = '$',
+ // Cent symbol
u00A2 = '\u00A2',
+ // Euro symbol
u20AC = '\u20AC',
- u024B62 = '\u024B62',
- // The normal one doesn't display properly, try the below which is the same
- // according to http://www.fileformat.info/info/unicode/char/24B62/index.htm
- u024B62alt = '\uD852\uDF62';
+ // Character \U00024B62 (Han script) can't be represented in javascript as a single
+ // code point, instead it is composed as a surrogate pair of two separate code units.
+ // http://codepoints.net/U+24B62
+ // http://www.fileformat.info/info/unicode/char/24B62/index.htm
+ u024B62 = '\uD852\uDF62';
- assert.strictEqual( $.byteLength( u0024 ), 1, 'U+0024: 1 byte. $ (dollar sign)' );
- assert.strictEqual( $.byteLength( u00A2 ), 2, 'U+00A2: 2 bytes. \u00A2 (cent sign)' );
- assert.strictEqual( $.byteLength( u20AC ), 3, 'U+20AC: 3 bytes. \u20AC (euro sign)' );
- assert.strictEqual( $.byteLength( u024B62 ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character)' );
- assert.strictEqual( $.byteLength( u024B62alt ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character) - alternative method' );
+ assert.strictEqual( $.byteLength( u0024 ), 1, 'U+0024' );
+ assert.strictEqual( $.byteLength( u00A2 ), 2, 'U+00A2' );
+ assert.strictEqual( $.byteLength( u20AC ), 3, 'U+20AC' );
+ assert.strictEqual( $.byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' );
} );
}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
index c21844eb..22d2af19 100644
--- a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
@@ -31,55 +31,34 @@
/**
* Test factory for $.fn.byteLimit
*
- * @param $input {jQuery} jQuery object in an input element
- * @param hasLimit {Boolean} Wether a limit should apply at all
- * @param limit {Number} Limit (if used) otherwise undefined
- * The limit should be less than 20 (the sample data's length)
+ * @param {Object} options
+ * @param {string} options.description Test name
+ * @param {jQuery} options.$input jQuery object in an input element
+ * @param {string} options.sample Sequence of characters to simulate being
+ * added one by one
+ * @param {string} options.expected Expected final value of `$input`
*/
function byteLimitTest( options ) {
var opt = $.extend( {
description: '',
$input: null,
sample: '',
- hasLimit: false,
- expected: '',
- limit: null
+ expected: ''
}, options );
- QUnit.asyncTest( opt.description, opt.hasLimit ? 3 : 2, function ( assert ) {
+ QUnit.asyncTest( opt.description, 1, function ( assert ) {
setTimeout( function () {
- var rawVal, fn, effectiveVal;
-
opt.$input.appendTo( '#qunit-fixture' );
// Simulate pressing keys for each of the sample characters
addChars( opt.$input, opt.sample );
- rawVal = opt.$input.val();
- fn = opt.$input.data( 'byteLimit.callback' );
- effectiveVal = fn ? fn( rawVal ) : rawVal;
-
- if ( opt.hasLimit ) {
- assert.ltOrEq(
- $.byteLength( effectiveVal ),
- opt.limit,
- 'Prevent keypresses after byteLimit was reached, length never exceeded the limit'
- );
- assert.equal(
- $.byteLength( rawVal ),
- $.byteLength( opt.expected ),
- 'Not preventing keypresses too early, length has reached the expected length'
- );
- assert.equal( rawVal, opt.expected, 'New value matches the expected string' );
-
- } else {
- assert.equal(
- $.byteLength( effectiveVal ),
- $.byteLength( opt.expected ),
- 'Unlimited scenarios are not affected, expected length reached'
- );
- assert.equal( rawVal, opt.expected, 'New value matches the expected string' );
- }
+ assert.equal(
+ opt.$input.val(),
+ opt.expected,
+ 'New value matches the expected string'
+ );
+
QUnit.start();
}, 10 );
} );
@@ -89,7 +68,6 @@
description: 'Plain text input',
$input: $( '<input type="text"/>' ),
sample: simpleSample,
- hasLimit: false,
expected: simpleSample
} );
@@ -98,7 +76,6 @@
$input: $( '<input type="text"/>' )
.byteLimit(),
sample: simpleSample,
- hasLimit: false,
expected: simpleSample
} );
@@ -108,8 +85,6 @@
.attr( 'maxlength', '10' )
.byteLimit(),
sample: simpleSample,
- hasLimit: true,
- limit: 10,
expected: '1234567890'
} );
@@ -118,8 +93,6 @@
$input: $( '<input type="text"/>' )
.byteLimit( 10 ),
sample: simpleSample,
- hasLimit: true,
- limit: 10,
expected: '1234567890'
} );
@@ -129,8 +102,6 @@
.attr( 'maxlength', '10' )
.byteLimit( 15 ),
sample: simpleSample,
- hasLimit: true,
- limit: 15,
expected: '123456789012345'
} );
@@ -139,8 +110,6 @@
$input: $( '<input type="text"/>' )
.byteLimit( 14 ),
sample: mbSample,
- hasLimit: true,
- limit: 14,
expected: '1234567890' + U_20AC + '1'
} );
@@ -149,8 +118,6 @@
$input: $( '<input type="text"/>' )
.byteLimit( 12 ),
sample: mbSample,
- hasLimit: true,
- limit: 12,
expected: '1234567890' + '12'
} );
@@ -158,17 +125,11 @@
description: 'Pass the limit and a callback as input filter',
$input: $( '<input type="text"/>' )
.byteLimit( 6, function ( val ) {
- // Invalid title
- if ( val === '' ) {
- return '';
- }
-
+ var title = mw.Title.newFromText( String( val ) );
// Return without namespace prefix
- return new mw.Title( String( val ) ).getMain();
+ return title ? title.getMain() : '';
} ),
sample: 'User:Sample',
- hasLimit: true,
- limit: 6, // 'Sample' length
expected: 'User:Sample'
} );
@@ -177,20 +138,53 @@
$input: $( '<input type="text"/>' )
.attr( 'maxlength', '6' )
.byteLimit( function ( val ) {
- // Invalid title
- if ( val === '' ) {
- return '';
- }
-
+ var title = mw.Title.newFromText( String( val ) );
// Return without namespace prefix
- return new mw.Title( String( val ) ).getMain();
+ return title ? title.getMain() : '';
} ),
sample: 'User:Sample',
- hasLimit: true,
- limit: 6, // 'Sample' length
expected: 'User:Sample'
} );
+ byteLimitTest( {
+ description: 'Pass the limit and a callback as input filter',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 6, function ( val ) {
+ var title = mw.Title.newFromText( String( val ) );
+ // Return without namespace prefix
+ return title ? title.getMain() : '';
+ } ),
+ sample: 'User:Example',
+ // The callback alters the value to be used to calculeate
+ // the length. The altered value is "Exampl" which has
+ // a length of 6, the "e" would exceed the limit.
+ expected: 'User:Exampl'
+ } );
+
+ byteLimitTest( {
+ description: 'Input filter that increases the length',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 10, function ( text ) {
+ return 'prefix' + text;
+ } ),
+ sample: simpleSample,
+ // Prefix adds 6 characters, limit is reached after 4
+ expected: '1234'
+ } );
+
+ // Regression tests for bug 41450
+ byteLimitTest( {
+ description: 'Input filter of which the base exceeds the limit',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 3, function ( text ) {
+ return 'prefix' + text;
+ } ),
+ sample: simpleSample,
+ hasLimit: true,
+ limit: 6, // 'prefix' length
+ expected: ''
+ } );
+
QUnit.test( 'Confirm properties and attributes set', 4, function ( assert ) {
var $el, $elA, $elB;
diff --git a/tests/qunit/suites/resources/jquery/jquery.client.test.js b/tests/qunit/suites/resources/jquery/jquery.client.test.js
index 88bbf5c4..4c7c3022 100644
--- a/tests/qunit/suites/resources/jquery/jquery.client.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.client.test.js
@@ -1,16 +1,11 @@
( function ( $ ) {
- var uacount, uas, testMap;
QUnit.module( 'jquery.client', QUnit.newMwEnvironment() );
- /** Number of user-agent defined */
- uacount = 0;
-
- uas = ( function () {
-
+ var uacount = 0,
// Object keyed by userAgent. Value is an array (human-readable name, client-profile object, navigator.platform value)
// Info based on results from http://toolserver.org/~krinkle/testswarm/job/174/
- var uas = {
+ uas = {
// Internet Explorer 6
// Internet Explorer 7
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)': {
@@ -39,7 +34,7 @@
profile: {
name: 'msie',
layout: 'trident',
- layoutVersion: 'unknown', // should be able to report 6?
+ layoutVersion: 6,
platform: 'win',
version: '10.0',
versionBase: '10',
@@ -50,6 +45,60 @@
rtl: true
}
},
+ // Internet Explorer 11
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'Win32',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Internet Explorer 11 - Windows 8.1 x64 Modern UI
+ 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'Win64',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Internet Explorer 11 - Windows 8.1 x64 desktop UI
+ 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'WOW64',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
// Firefox 2
// Firefox 3.5
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19': {
@@ -141,6 +190,24 @@
rtl: true
}
},
+ // Iceweasel 15.0.1
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 Iceweasel/15.0.1': {
+ title: 'Iceweasel 15.0.1',
+ platform: 'Linux',
+ profile: {
+ name: 'iceweasel',
+ layout: 'gecko',
+ layoutVersion: 20100101,
+ platform: 'linux',
+ version: '15.0.1',
+ versionBase: '15',
+ versionNumber: 15
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
// Firefox 5
// Safari 3
// Safari 4
@@ -179,6 +246,42 @@
}
},
// Safari 5
+ // Safari 6
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.29.13 (KHTML, like Gecko) Version/6.0.4 Safari/536.29.13': {
+ title: 'Safari 6',
+ platform: 'MacIntel',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 536,
+ platform: 'mac',
+ version: '6.0.4',
+ versionBase: '6',
+ versionNumber: 6
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Safari 6.0.5+ (doesn't have the comma in "KHTML, like Gecko")
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1': {
+ title: 'Safari 6',
+ platform: 'MacIntel',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 536,
+ platform: 'mac',
+ version: '6.0.5',
+ versionBase: '6',
+ versionNumber: 6
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
// Opera 10+
'Opera/9.80 (Windows NT 5.1)': {
title: 'Opera 10+ (exact version unspecified)',
@@ -215,6 +318,24 @@
rtl: true
}
},
+ // Opera 15 (WebKit-based)
+ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130': {
+ title: 'Opera 15',
+ platform: 'Win32',
+ profile: {
+ name: 'opera',
+ layout: 'webkit',
+ layoutVersion: 537,
+ platform: 'win',
+ version: '15.0.1147.130',
+ versionBase: '15',
+ versionNumber: 15
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
// Chrome 5
// Chrome 6
// Chrome 7
@@ -257,6 +378,24 @@
rtl: true
}
},
+ // Android WebKit Browser 2.3
+ 'Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1': {
+ title: 'Android WebKit Browser 2.3',
+ platform: 'Linux armv7l',
+ profile: {
+ name: 'android',
+ layout: 'webkit',
+ layoutVersion: 533,
+ platform: 'linux',
+ version: '2.3.5',
+ versionBase: '2',
+ versionNumber: 2.3
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
// Bug #34924
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) rekonq Safari/534.34': {
title: 'Rekonq',
@@ -275,28 +414,42 @@
rtl: true
}
}
- };
- $.each( uas, function () {
- uacount++;
- } );
- return uas;
- }() );
-
- QUnit.test( 'profile userAgent support', uacount, function ( assert ) {
- // Generate a client profile object and compare recursively
- var uaTest = function ( rawUserAgent, data ) {
- var ret = $.client.profile( {
- userAgent: rawUserAgent,
- platform: data.platform
- } );
- assert.deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent );
- };
+ },
+ testMap = {
+ // Example from WikiEditor
+ // Make sure to use raw numbers, a string like "7.0" would fail on a
+ // version 10 browser since in string comparaison "10" is before "7.0" :)
+ 'ltr': {
+ 'msie': [['>=', 7.0]],
+ 'firefox': [['>=', 2]],
+ 'opera': [['>=', 9.6]],
+ 'safari': [['>=', 3]],
+ 'chrome': [['>=', 3]],
+ 'netscape': [['>=', 9]],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ },
+ 'rtl': {
+ 'msie': [['>=', 8]],
+ 'firefox': [['>=', 2]],
+ 'opera': [['>=', 9.6]],
+ 'safari': [['>=', 3]],
+ 'chrome': [['>=', 3]],
+ 'netscape': [['>=', 9]],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ }
+ }
+ ;
- // Loop through and run tests
- $.each( uas, uaTest );
+ // Count test cases
+ $.each( uas, function () {
+ uacount++;
} );
- QUnit.test( 'profile return validation for current user agent', 7, function ( assert ) {
+ QUnit.test( 'profile( navObject )', 7, function ( assert ) {
var p = $.client.profile();
function unknownOrType( val, type, summary ) {
@@ -312,44 +465,58 @@
assert.equal( typeof p.versionNumber, 'number', 'p.versionNumber is a number' );
} );
- // Example from WikiEditor
- // Make sure to use raw numbers, a string like "7.0" would fail on a
- // version 10 browser since in string comparaison "10" is before "7.0" :)
- testMap = {
- 'ltr': {
- 'msie': [['>=', 7.0]],
- 'firefox': [['>=', 2]],
- 'opera': [['>=', 9.6]],
- 'safari': [['>=', 3]],
- 'chrome': [['>=', 3]],
- 'netscape': [['>=', 9]],
- 'blackberry': false,
- 'ipod': false,
- 'iphone': false
- },
- 'rtl': {
- 'msie': [['>=', 8]],
- 'firefox': [['>=', 2]],
- 'opera': [['>=', 9.6]],
- 'safari': [['>=', 3]],
- 'chrome': [['>=', 3]],
- 'netscape': [['>=', 9]],
- 'blackberry': false,
- 'ipod': false,
- 'iphone': false
- }
- };
+ QUnit.test( 'profile( navObject ) - samples', uacount, function ( assert ) {
+ // Loop through and run tests
+ $.each( uas, function ( rawUserAgent, data ) {
+ // Generate a client profile object and compare recursively
+ var ret = $.client.profile( {
+ userAgent: rawUserAgent,
+ platform: data.platform
+ } );
+ assert.deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent );
+ } );
+ } );
- QUnit.test( 'test', 1, function ( assert ) {
+ QUnit.test( 'test( testMap )', 4, function ( assert ) {
// .test() uses eval, make sure no exceptions are thrown
// then do a basic return value type check
- var testMatch = $.client.test( testMap );
+ var testMatch = $.client.test( testMap ),
+ ie7Profile = $.client.profile( {
+ 'userAgent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
+ 'platform': ''
+ } );
+
+ assert.equal( typeof testMatch, 'boolean', 'map with ltr/rtl split returns a boolean value' );
+
+ testMatch = $.client.test( testMap.ltr );
+
+ assert.equal( typeof testMatch, 'boolean', 'simple map (without ltr/rtl split) returns a boolean value' );
+
+ assert.equal( $.client.test( {
+ 'msie': null
+ }, ie7Profile ), true, 'returns true if any version of a browser are allowed (null)' );
+
+ assert.equal( $.client.test( {
+ 'msie': false
+ }, ie7Profile ), false, 'returns false if all versions of a browser are not allowed (false)' );
+ } );
+
+ QUnit.test( 'test( testMap, exactMatchOnly )', 2, function ( assert ) {
+ var ie7Profile = $.client.profile( {
+ 'userAgent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
+ 'platform': ''
+ } );
- assert.equal( typeof testMatch, 'boolean', 'test returns a boolean value' );
+ assert.equal( $.client.test( {
+ 'firefox': [['>=', 2]]
+ }, ie7Profile, false ), true, 'returns true if browser not found and exactMatchOnly not set' );
+ assert.equal( $.client.test( {
+ 'firefox': [['>=', 2]]
+ }, ie7Profile, true ), false, 'returns false if browser not found and exactMatchOnly is set' );
} );
- QUnit.test( 'User-agent matches against WikiEditor\'s compatibility map', uacount * 2, function ( assert ) {
+ QUnit.test( 'test( testMap) - WikiEditor sample', uacount * 2, function ( assert ) {
var $body = $( 'body' ),
bodyClasses = $body.attr( 'class' );
diff --git a/tests/qunit/suites/resources/jquery/jquery.localize.test.js b/tests/qunit/suites/resources/jquery/jquery.localize.test.js
index d3877e05..3ef27903 100644
--- a/tests/qunit/suites/resources/jquery/jquery.localize.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.localize.test.js
@@ -38,7 +38,7 @@
// making sure it is actually using text() and attr() (or something with the same effect)
// Text escaping
- html = '<div><span><html:msg key="properfoo"></span></div>';
+ html = '<div><span><html:msg key="properfoo" /></span></div>';
$lc = $( html ).localize().find( 'span' );
assert.strictEqual( $lc.text(), mw.msg( 'properfoo' ), 'Content is inserted as text, not as html.' );
@@ -63,7 +63,7 @@
var html, $lc, x, sitename = 'Wikipedia';
// Message key prefix
- html = '<div><span title-msg="lorem"><html:msg key="ipsum"></span></div>';
+ html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>';
$lc = $( html ).localize( {
prefix: 'foo-'
} ).find( 'span' );
@@ -73,7 +73,7 @@
// Variable keys mapping
x = 'bar';
- html = '<div><span title-msg="title"><html:msg key="label"></span></div>';
+ html = '<div><span title-msg="title"><html:msg key="label" /></span></div>';
$lc = $( html ).localize( {
keys: {
'title': 'foo-' + x + '-title',
@@ -85,7 +85,7 @@
assert.strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' );
// Passing parameteters to mw.msg
- html = '<div><span><html:msg key="foo-welcome"></span></div>';
+ html = '<div><span><html:msg key="foo-welcome" /></span></div>';
$lc = $( html ).localize( {
params: {
'foo-welcome': [sitename, 'yesterday']
@@ -96,7 +96,7 @@
// Combination of options prefix, params and keys
x = 'bazz';
- html = '<div><span title-msg="title"><html:msg key="label"></span></div>';
+ html = '<div><span title-msg="title"><html:msg key="label" /></span></div>';
$lc = $( html ).localize( {
prefix: 'foo-',
keys: {
diff --git a/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js b/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
new file mode 100644
index 00000000..6da56ed2
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
@@ -0,0 +1,287 @@
+( function ( mw, $ ) {
+ var loremIpsum = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.';
+
+ QUnit.module( 'jquery.makeCollapsible', QUnit.newMwEnvironment() );
+
+ function prepareCollapsible( html, options ) {
+ return $( $.parseHTML( html ) )
+ .appendTo( '#qunit-fixture' )
+ // options might be undefined here - this is okay
+ .makeCollapsible( options );
+ }
+
+ // This test is first because if it fails, then almost all of the latter tests are meaningless.
+ QUnit.asyncTest( 'testing hooks/triggers', 4, function ( assert ) {
+ var $collapsible, $content, $toggle;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>'
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+ $toggle = $collapsible.find( '.mw-collapsible-toggle' );
+
+ // In one full collapse-expand cycle, each event will be fired once
+
+ // On collapse...
+ $collapsible.on( 'beforeCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'first beforeCollapseExpand: content is visible' );
+ } );
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'first afterCollapseExpand: content is hidden' );
+
+ // On expand...
+ $collapsible.on( 'beforeExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'second beforeCollapseExpand: content is hidden' );
+ } );
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'second afterCollapseExpand: content is visible' );
+
+ QUnit.start();
+ } );
+
+ // ...expanding happens here
+ $toggle.trigger( 'click' );
+ } );
+
+ // ...collapsing happens here
+ $toggle.trigger( 'click' );
+ } );
+
+ QUnit.asyncTest( 'basic operation (<div>)', 5, function ( assert ) {
+ var $collapsible, $content, $toggle;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>'
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+ $toggle = $collapsible.find( '.mw-collapsible-toggle' );
+
+ assert.equal( $content.length, 1, 'content is present' );
+ assert.equal( $content.find( $toggle ).length, 0, 'toggle is not a descendant of content' );
+
+ assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ QUnit.start();
+ } );
+
+ $toggle.trigger( 'click' );
+ } );
+
+ $toggle.trigger( 'click' );
+ } );
+
+ QUnit.asyncTest( 'basic operation (<table>)', 7, function ( assert ) {
+ var $collapsible, $headerRow, $contentRow, $toggle;
+ $collapsible = prepareCollapsible(
+ '<table class="mw-collapsible">' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '</table>'
+ );
+ $headerRow = $collapsible.find( 'tr:first' );
+ $contentRow = $collapsible.find( 'tr:last' );
+
+ $toggle = $headerRow.find( 'td:last .mw-collapsible-toggle' );
+ assert.equal( $toggle.length, 1, 'toggle is added to last cell of first row' );
+
+ assert.assertTrue( $headerRow.is( ':visible' ), 'headerRow is visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'contentRow is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $headerRow.is( ':visible' ), 'after collapsing: headerRow is still visible' );
+ assert.assertTrue( $contentRow.is( ':hidden' ), 'after collapsing: contentRow is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $headerRow.is( ':visible' ), 'after expanding: headerRow is still visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'after expanding: contentRow is visible' );
+ QUnit.start();
+ } );
+
+ $toggle.trigger( 'click' );
+ } );
+
+ $toggle.trigger( 'click' );
+ } );
+
+ function listTest( listType, assert ) {
+ var $collapsible, $toggleItem, $contentItem, $toggle;
+ $collapsible = prepareCollapsible(
+ '<' + listType + ' class="mw-collapsible">' +
+ '<li>' + loremIpsum + '</li>' +
+ '<li>' + loremIpsum + '</li>' +
+ '</' + listType + '>'
+ );
+ $toggleItem = $collapsible.find( 'li.mw-collapsible-toggle-li:first-child' );
+ $contentItem = $collapsible.find( 'li:last' );
+
+ $toggle = $toggleItem.find( '.mw-collapsible-toggle' );
+ assert.equal( $toggle.length, 1, 'toggle is present, added inside new zeroth list item' );
+
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'toggleItem is visible' );
+ assert.assertTrue( $contentItem.is( ':visible' ), 'contentItem is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'after collapsing: toggleItem is still visible' );
+ assert.assertTrue( $contentItem.is( ':hidden' ), 'after collapsing: contentItem is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'after expanding: toggleItem is still visible' );
+ assert.assertTrue( $contentItem.is( ':visible' ), 'after expanding: contentItem is visible' );
+ QUnit.start();
+ } );
+
+ $toggle.trigger( 'click' );
+ } );
+
+ $toggle.trigger( 'click' );
+ }
+
+ QUnit.asyncTest( 'basic operation (<ul>)', 7, function ( assert ) {
+ listTest( 'ul', assert );
+ } );
+
+ QUnit.asyncTest( 'basic operation (<ol>)', 7, function ( assert ) {
+ listTest( 'ol', assert );
+ } );
+
+ QUnit.test( 'basic operation when synchronous (options.instantHide)', 2, function ( assert ) {
+ var $collapsible, $content;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { instantHide: true }
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+
+ assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+ } );
+
+ QUnit.test( 'mw-made-collapsible data added', 1, function ( assert ) {
+ var $collapsible;
+ $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>'
+ );
+ assert.equal( $collapsible.data( 'mw-made-collapsible' ), true, 'mw-made-collapsible data present' );
+ } );
+
+ QUnit.test( 'mw-collapsible added when missing', 1, function ( assert ) {
+ var $collapsible;
+ $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>'
+ );
+ assert.assertTrue( $collapsible.hasClass( 'mw-collapsible' ), 'mw-collapsible class present' );
+ } );
+
+ QUnit.test( 'mw-collapsed added when missing', 1, function ( assert ) {
+ var $collapsible;
+ $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>',
+ { collapsed: true }
+ );
+ assert.assertTrue( $collapsible.hasClass( 'mw-collapsed' ), 'mw-collapsed class present' );
+ } );
+
+ QUnit.asyncTest( 'initial collapse (mw-collapsed class)', 2, function ( assert ) {
+ var $collapsible, $content;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible mw-collapsed">' + loremIpsum + '</div>'
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ // Synchronous - mw-collapsed should cause instantHide: true to be used on initial collapsing
+ assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ QUnit.start();
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ } );
+
+ QUnit.asyncTest( 'initial collapse (options.collapsed)', 2, function ( assert ) {
+ var $collapsible, $content;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { collapsed: true }
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ // Synchronous - collapsed: true should cause instantHide: true to be used on initial collapsing
+ assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ QUnit.start();
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ } );
+
+ QUnit.test( 'clicks on links inside toggler pass through (options.linksPassthru)' , 2, function ( assert ) {
+ var $collapsible, $content;
+
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' +
+ '<div class="mw-collapsible-toggle">' +
+ 'Toggle <a href="#top">toggle</a> toggle <b>toggle</b>' +
+ '</div>' +
+ '<div class="mw-collapsible-content">' + loremIpsum + '</div>' +
+ '</div>',
+ // Can't do asynchronous because we're testing that the event *doesn't* happen
+ { instantHide: true }
+ );
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ $collapsible.find( '.mw-collapsible-toggle a' ).trigger( 'click' );
+ assert.assertTrue( $content.is( ':visible' ), 'click event on link inside toggle passes through (content not toggled)' );
+
+ $collapsible.find( '.mw-collapsible-toggle b' ).trigger( 'click' );
+ assert.assertTrue( $content.is( ':hidden' ), 'click event on non-link inside toggle toggles content' );
+ } );
+
+ QUnit.asyncTest( 'collapse/expand text (data-collapsetext, data-expandtext)', 2, function ( assert ) {
+ var $collapsible, $toggleLink;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible" data-collapsetext="Collapse me!" data-expandtext="Expand me!">' +
+ loremIpsum +
+ '</div>'
+ );
+ $toggleLink = $collapsible.find( '.mw-collapsible-toggle a' );
+
+ assert.equal( $toggleLink.text(), 'Collapse me!', 'data-collapsetext is respected' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.equal( $toggleLink.text(), 'Expand me!', 'data-expandtext is respected' );
+ QUnit.start();
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ } );
+
+ QUnit.asyncTest( 'collapse/expand text (options.collapseText, options.expandText)', 2, function ( assert ) {
+ var $collapsible, $toggleLink;
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { collapseText: 'Collapse me!', expandText: 'Expand me!' }
+ );
+ $toggleLink = $collapsible.find( '.mw-collapsible-toggle a' );
+
+ assert.equal( $toggleLink.text(), 'Collapse me!', 'options.collapseText is respected' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.equal( $toggleLink.text(), 'Expand me!', 'options.expandText is respected' );
+ QUnit.start();
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
index 307b0440..f73fd7bf 100644
--- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
@@ -5,6 +5,8 @@
wgMonthNames: ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
wgMonthNamesShort: ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
wgDefaultDateFormat: 'dmy',
+ wgSeparatorTransformTable: ['', ''],
+ wgDigitTransformTable: ['', ''],
wgContentLanguage: 'en'
};
@@ -180,6 +182,18 @@
}
);
tableTest(
+ 'Basic planet table: ascending by name (multiple clicks)',
+ header,
+ planets,
+ ascendingName,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(1)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
'Basic planet table: descending by name',
header,
planets,
@@ -273,6 +287,35 @@
$table.data( 'tablesorter' ).sort();
}
);
+ tableTest(
+ 'Sort via click event after having initialized the tablesorter with initial sorting',
+ header,
+ initial,
+ descasc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [ { 0: 'asc' }, { 1: 'asc' } ] }
+ );
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Multi-sort via click event after having initialized the tablesorter with initial sorting',
+ header,
+ initial,
+ asc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [ { 0: 'desc' }, { 1: 'desc' } ] }
+ );
+ $table.find( '.headerSort:eq(0)' ).click();
+
+ // Pretend to click while pressing the multi-sort key
+ var event = $.Event( 'click' );
+ event[$table.data( 'tablesorter' ).config.sortMultiSortKey] = true;
+ $table.find( '.headerSort:eq(1)' ).trigger( event );
+ }
+ );
QUnit.test( 'Reset sorting making table appear unsorted', 3, function ( assert ) {
var $table = tableCreate( header, initial );
$table.tablesorter(
@@ -319,12 +362,26 @@
function ( $table ) {
// Make colspanned header for test
$table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
- $table.find( 'tr:eq(0) th:eq(0)' ).prop( 'colspan', '3' );
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
$table.tablesorter();
$table.find( '.headerSort:eq(0)' ).click();
}
);
+ tableTest( 'Sorting with colspanned headers: sort spanned column twice',
+ header,
+ initial,
+ [ caa4, bbc2, abc3, aab5, aaa1 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
tableTest( 'Sorting with colspanned headers: subsequent column',
header,
initial,
@@ -332,12 +389,40 @@
function ( $table ) {
// Make colspanned header for test
$table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
- $table.find( 'tr:eq(0) th:eq(0)' ).prop( 'colspan', '3' );
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
$table.tablesorter();
$table.find( '.headerSort:eq(1)' ).click();
}
);
+ tableTest( 'Sorting with colspanned headers: sort subsequent column twice',
+ header,
+ initial,
+ [ aab5, caa4, abc3, bbc2, aaa1 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(1)' ).click();
+ $table.find( '.headerSort:eq(1)' ).click();
+ }
+ );
+
+
+ tableTest(
+ 'Basic planet table: one unsortable column',
+ header,
+ planets,
+ planets,
+ function ( $table ) {
+ $table.find( 'tr:eq(0) > th:eq(0)' ).addClass( 'unsortable' );
+
+ $table.tablesorter();
+ $table.find( 'tr:eq(0) > th:eq(0)' ).click();
+ }
+ );
// Regression tests!
tableTest(
@@ -489,12 +574,12 @@
$table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
// - Set rowspan for 2nd cell of 3rd row to 3.
// This covers the removed cell in the 4th and 5th row.
- $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowspan', '3' );
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
$table.tablesorter();
assert.equal(
- $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowspan' ),
+ $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowSpan' ),
3,
'Rowspan not exploded'
);
@@ -521,7 +606,7 @@
$table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
// - Set rowspan for 2nd cell of 3rd row to 3.
// This covers the removed cell in the 4th and 5th row.
- $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowspan', '3' );
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
$table.tablesorter();
$table.find( '.headerSort:eq(0)' ).click();
@@ -538,7 +623,7 @@
$table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
// - Set rowspan for 2nd cell of 3rd row to 3.
// This covers the removed cell in the 4th and 5th row.
- $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowspan', '3' );
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
$table.tablesorter( { sortList: [
{ 0: 'asc' }
@@ -556,7 +641,7 @@
$table.find( 'tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)' ).remove();
// - Set rowspan for 1st cell of 3rd row to 3.
// This covers the removed cell in the 4th and 5th row.
- $table.find( 'tr:eq(2) td:eq(0)' ).prop( 'rowspan', '3' );
+ $table.find( 'tr:eq(2) td:eq(0)' ).attr( 'rowspan', '3' );
$table.tablesorter();
$table.find( '.headerSort:eq(0)' ).click();
@@ -642,7 +727,7 @@
}
);
- QUnit.test( 'Test detection routine', function ( assert ) {
+ QUnit.test( 'Test detection routine', 1, function ( assert ) {
var $table;
$table = $(
'<table class="sortable">' +
@@ -663,7 +748,7 @@
} );
/** FIXME: the diff output is not very readeable. */
- QUnit.test( 'bug 32047 - caption must be before thead', function ( assert ) {
+ QUnit.test( 'bug 32047 - caption must be before thead', 1, function ( assert ) {
var $table;
$table = $(
'<table class="sortable">' +
@@ -683,7 +768,7 @@
);
} );
- QUnit.test( 'data-sort-value attribute, when available, should override sorting position', function ( assert ) {
+ QUnit.test( 'data-sort-value attribute, when available, should override sorting position', 3, function ( assert ) {
var $table, data;
// Example 1: All cells except one cell without data-sort-value,
@@ -953,7 +1038,7 @@
}
);
- QUnit.test( 'Sorting images using alt text', function ( assert ) {
+ QUnit.test( 'Sorting images using alt text', 1, function ( assert ) {
var $table = $(
'<table class="sortable">' +
'<tr><th>THEAD</th></tr>' +
@@ -970,7 +1055,7 @@
);
} );
- QUnit.test( 'Sorting images using alt text (complex)', function ( assert ) {
+ QUnit.test( 'Sorting images using alt text (complex)', 1, function ( assert ) {
var $table = $(
'<table class="sortable">' +
'<tr><th>THEAD</th></tr>' +
@@ -991,7 +1076,7 @@
);
} );
- QUnit.test( 'Sorting images using alt text (with format autodetection)', function ( assert ) {
+ QUnit.test( 'Sorting images using alt text (with format autodetection)', 1, function ( assert ) {
var $table = $(
'<table class="sortable">' +
'<tr><th>THEAD</th></tr>' +
@@ -1010,6 +1095,61 @@
);
} );
+ QUnit.test( 'bug 38911 - The row with the largest amount of columns should receive the sort indicators', 3, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th rowspan="2" id="A1">A1</th><th colspan="2">B2a</th></tr>' +
+ '<tr><th id="B2b">B2b</th><th id="C2b">C2b</th></tr>' +
+ '</thead>' +
+ '<tr><td>A</td><td>Aa</td><td>Ab</td></tr>' +
+ '<tr><td>B</td><td>Ba</td><td>Bb</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( '#A1' ).attr( 'class' ),
+ 'headerSort',
+ 'The first column of the first row should be sortable'
+ );
+ assert.equal(
+ $table.find( '#B2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 2nd column should be sortable'
+ );
+ assert.equal(
+ $table.find( '#C2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 3rd column should be sortable'
+ );
+ } );
+
+ QUnit.test( 'rowspans in table headers should prefer the last row when rows are equal in length', 2, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th rowspan="2" id="A1">A1</th><th>B2a</th></tr>' +
+ '<tr><th id="B2b">B2b</th></tr>' +
+ '</thead>' +
+ '<tr><td>A</td><td>Aa</td></tr>' +
+ '<tr><td>B</td><td>Ba</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( '#A1' ).attr( 'class' ),
+ 'headerSort',
+ 'The first column of the first row should be sortable'
+ );
+ assert.equal(
+ $table.find( '#B2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 2nd column should be sortable'
+ );
+ } );
+
// bug 41889 - exploding rowspans in more complex cases
tableTestHTML(
'Rowspan exploding with row headers',
diff --git a/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
index ce03b697..5fe23944 100644
--- a/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
@@ -48,13 +48,6 @@
var start = opt.before.start,
end = opt.before.end;
- if ( window.opera ) {
- // Compensate for Opera's craziness converting \n to \r\n and counting that as two chars
- var newLinesBefore = opt.before.text.substring( 0, start ).split( '\n' ).length - 1,
- newLinesInside = opt.before.text.substring( start, end ).split( '\n' ).length - 1;
- start += newLinesBefore;
- end += newLinesBefore + newLinesInside;
- }
var options = $.extend( {}, opt.replace ); // Clone opt.replace
options.selectionStart = start;
diff --git a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
index 9389651f..ee854aef 100644
--- a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
+++ b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
@@ -3,7 +3,7 @@
// TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ]
- QUnit.test( '"all" namespace disable checkboxes', function ( assert ) {
+ QUnit.test( '"all" namespace disable checkboxes', 8, function ( assert ) {
var selectHtml, $env, $options;
// from Special:Recentchanges
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
index 30a31ef7..ab96f753 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
@@ -1,4 +1,4 @@
-( function ( mw ) {
+( function ( mw, $ ) {
// mw.Title relies on these three config vars
// Restore them after each test run
var config = {
@@ -53,49 +53,184 @@
antarctic_waterfowl: 100
},
wgCaseSensitiveNamespaces: []
+ },
+ repeat = function ( input, multiplier ) {
+ return new Array( multiplier + 1 ).join( input );
+ },
+ cases = {
+ // See also TitleTest.php#testSecureAndSplit
+ valid: [
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ // Length is 256 total, but only title part matters
+ 'Category:' + repeat( 'x', 248 ),
+ repeat( 'x', 252 )
+ ],
+ invalid: [
+ '',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: The ones with # are commented out as those are interpreted as fragment and
+ // as such end up being valid.
+ 'A &eacute; B',
+ //'A &#233; B',
+ //'A &#x00E9; B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ repeat( 'x', 256 ),
+ // Extension separation is a js invention, for length
+ // purposes it is part of the title
+ repeat( 'x', 252 ) + '.json',
+ // Namespace prefix without actual title
+ // ':', // bug 54044
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ]
};
QUnit.module( 'mediawiki.Title', QUnit.newMwEnvironment( { config: config } ) );
- QUnit.test( 'Transformation', 8, function ( assert ) {
+ QUnit.test( 'constructor', cases.invalid.length, function ( assert ) {
+ var i, title;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ title = new mw.Title( cases.valid[i] );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ /*jshint loopfunc:true */
+ title = cases.invalid[i];
+ assert.throws( function () {
+ return new mw.Title( title );
+ }, cases.invalid[i] );
+ }
+ } );
+
+ QUnit.test( 'newFromText', cases.valid.length + cases.invalid.length, function ( assert ) {
+ var i;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.valid[i] ) ),
+ 'object',
+ cases.valid[i]
+ );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.invalid[i] ) ),
+ 'null',
+ cases.invalid[i]
+ );
+ }
+ } );
+
+ QUnit.test( 'Basic parsing', 12, function ( assert ) {
+ var title;
+ title = new mw.Title( 'File:Foo_bar.JPG' );
+
+ assert.equal( title.getNamespaceId(), 6 );
+ assert.equal( title.getNamespacePrefix(), 'File:' );
+ assert.equal( title.getName(), 'Foo_bar' );
+ assert.equal( title.getNameText(), 'Foo bar' );
+ assert.equal( title.getExtension(), 'JPG' );
+ assert.equal( title.getDotExtension(), '.JPG' );
+ assert.equal( title.getMain(), 'Foo_bar.JPG' );
+ assert.equal( title.getMainText(), 'Foo bar.JPG' );
+ assert.equal( title.getPrefixedDb(), 'File:Foo_bar.JPG' );
+ assert.equal( title.getPrefixedText(), 'File:Foo bar.JPG' );
+
+ title = new mw.Title( 'Foo#bar' );
+ assert.equal( title.getPrefixedText(), 'Foo' );
+ assert.equal( title.getFragment(), 'bar' );
+ } );
+
+ QUnit.test( 'Transformation', 11, function ( assert ) {
var title;
title = new mw.Title( 'File:quux pif.jpg' );
- assert.equal( title.getName(), 'Quux_pif' );
+ assert.equal( title.getNameText(), 'Quux pif', 'First character of title' );
title = new mw.Title( 'File:Glarg_foo_glang.jpg' );
- assert.equal( title.getNameText(), 'Glarg foo glang' );
+ assert.equal( title.getNameText(), 'Glarg foo glang', 'Underscores' );
title = new mw.Title( 'User:ABC.DEF' );
- assert.equal( title.toText(), 'User:ABC.DEF' );
- assert.equal( title.getNamespaceId(), 2 );
- assert.equal( title.getNamespacePrefix(), 'User:' );
+ assert.equal( title.toText(), 'User:ABC.DEF', 'Round trip text' );
+ assert.equal( title.getNamespaceId(), 2, 'Parse canonical namespace prefix' );
+
+ title = new mw.Title( 'Image:quux pix.jpg' );
+ assert.equal( title.getNamespacePrefix(), 'File:', 'Transform alias to canonical namespace' );
title = new mw.Title( 'uSEr:hAshAr' );
assert.equal( title.toText(), 'User:HAshAr' );
- assert.equal( title.getNamespaceId(), 2 );
+ assert.equal( title.getNamespaceId(), 2, 'Case-insensitive namespace prefix' );
- title = new mw.Title( ' MediaWiki: Foo bar .js ' );
- // Don't ask why, it's the way the backend works. One space is kept of each set
- assert.equal( title.getName(), 'Foo_bar_.js', 'Merge multiple spaces to a single space.' );
- } );
+ // Don't ask why, it's the way the backend works. One space is kept of each set.
+ title = new mw.Title( 'Foo __ \t __ bar' );
+ assert.equal( title.getMain(), 'Foo_bar', 'Merge multiple types of whitespace/underscores into a single underscore' );
- QUnit.test( 'Main text for filename', 8, function ( assert ) {
- var title = new mw.Title( 'File:foo_bar.JPG' );
+ // Regression test: Previously it would only detect an extension if there is no space after it
+ title = new mw.Title( 'Example.js ' );
+ assert.equal( title.getExtension(), 'js', 'Space after an extension is stripped' );
- assert.equal( title.getNamespaceId(), 6 );
- assert.equal( title.getNamespacePrefix(), 'File:' );
- assert.equal( title.getName(), 'Foo_bar' );
- assert.equal( title.getNameText(), 'Foo bar' );
- assert.equal( title.getMain(), 'Foo_bar.JPG' );
- assert.equal( title.getMainText(), 'Foo bar.JPG' );
- assert.equal( title.getExtension(), 'JPG' );
- assert.equal( title.getDotExtension(), '.JPG' );
+ title = new mw.Title( 'Example#foo' );
+ assert.equal( title.getFragment(), 'foo', 'Fragment' );
+
+ title = new mw.Title( 'Example#_foo_bar baz_' );
+ assert.equal( title.getFragment(), ' foo bar baz', 'Fragment' );
} );
- QUnit.test( 'Namespace detection and conversion', 6, function ( assert ) {
+ QUnit.test( 'Namespace detection and conversion', 10, function ( assert ) {
var title;
+ title = new mw.Title( 'File:User:Example' );
+ assert.equal( title.getNamespaceId(), 6, 'Titles can contain namespace prefixes, which are otherwise ignored' );
+
+ title = new mw.Title( 'Example', 6 );
+ assert.equal( title.getNamespaceId(), 6, 'Default namespace passed is used' );
+
+ title = new mw.Title( 'User:Example', 6 );
+ assert.equal( title.getNamespaceId(), 2, 'Included namespace prefix overrides the given default' );
+
+ title = new mw.Title( ':Example', 6 );
+ assert.equal( title.getNamespaceId(), 0, 'Colon forces main namespace' );
+
title = new mw.Title( 'something.PDF', 6 );
assert.equal( title.toString(), 'File:Something.PDF' );
@@ -189,10 +324,93 @@
mw.config.set( 'wgArticlePath', '/wiki/$1' );
title = new mw.Title( 'Foobar' );
- assert.equal( title.getUrl(), '/wiki/Foobar', 'Basic functionally, toString passing to wikiGetlink' );
+ assert.equal( title.getUrl(), '/wiki/Foobar', 'Basic functionally, getUrl uses mw.util.getUrl' );
title = new mw.Title( 'John Doe', 3 );
assert.equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' );
} );
-}( mediaWiki ) );
+ QUnit.test( 'newFromImg', 28, function ( assert ) {
+ var title, i, thisCase, prefix,
+ cases = [
+ {
+ url: '/wiki/images/thumb/9/91/Anticlockwise_heliotrope%27s.jpg/99px-Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Normal hashed directory thumbnail',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: '//upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Commons thumbnail',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '/wiki/images/9/91/Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Full image',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: 'http://localhost/thumb.php?f=Stuffless_Figaro%27s.jpg&width=180',
+ typeOfUrl: 'thumb.php-based thumbnail',
+ nameText: 'Stuffless Figaro\'s',
+ prefixedText: 'File:Stuffless Figaro\'s.jpg'
+ },
+
+ {
+ url: '/wikipedia/commons/thumb/Wikipedia-logo-v2.svg/150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Commons unhashed thumbnail',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '/wiki/images/Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Unhashed local file',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: '',
+ typeOfUrl: 'Empty string'
+ },
+
+ {
+ url: 'foo',
+ typeOfUrl: 'String with only alphabet characters'
+ },
+
+ {
+ url: 'foobar.foobar',
+ typeOfUrl: 'Not a file path'
+ },
+
+ {
+ url: '/a/a0/blah blah blah',
+ typeOfUrl: 'Space characters'
+ }
+ ];
+
+ for ( i = 0; i < cases.length; i++ ) {
+ thisCase = cases[i];
+ title = mw.Title.newFromImg( { src: thisCase.url } );
+
+ if ( thisCase.nameText !== undefined ) {
+ prefix = '[' + thisCase.typeOfUrl + ' URL' + '] ';
+
+ assert.notStrictEqual( title, null, prefix + 'Parses successfully' );
+ assert.equal( title.getNameText(), thisCase.nameText, prefix + 'Filename matches original' );
+ assert.equal( title.getPrefixedText(), thisCase.prefixedText, prefix + 'File page title matches original' );
+ assert.equal( title.getNamespaceId(), 6, prefix + 'Namespace ID matches File namespace' );
+ } else {
+ assert.strictEqual( title, null, thisCase.typeOfUrl + ', should not produce an mw.Title object' );
+ }
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
index 0a9df966..be362e22 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
@@ -1,20 +1,17 @@
( function ( mw, $ ) {
- var mwLanguageCache = {}, oldGetOuterHtml, formatnumTests, specialCharactersPageName,
+ var mwLanguageCache = {}, formatText, formatParse, formatnumTests, specialCharactersPageName,
expectedListUsers, expectedEntrypoints;
+ // When the expected result is the same in both modes
+ function assertBothModes( assert, parserArguments, expectedResult, assertMessage ) {
+ assert.equal( formatText.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'text\'' );
+ assert.equal( formatParse.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'parse\'' );
+ }
+
QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( {
setup: function () {
this.orgMwLangauge = mw.language;
mw.language = $.extend( true, {}, this.orgMwLangauge );
- oldGetOuterHtml = $.fn.getOuterHtml;
- $.fn.getOuterHtml = function () {
- var $div = $( '<div>' ), html;
- $div.append( $( this ).eq( 0 ).clone() );
- html = $div.html();
- $div.empty();
- $div = undefined;
- return html;
- };
// Messages that are reused in multiple tests
mw.messages.set( {
@@ -39,18 +36,26 @@
'external-link-replace': 'Foo [$1 bar]'
} );
+ mw.config.set( {
+ wgArticlePath: '/wiki/$1'
+ } );
+
specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
- expectedListUsers = '注册' + $( '<a>' ).attr( {
- title: 'Special:ListUsers',
- href: mw.util.wikiGetlink( 'Special:ListUsers' )
- } ).text( '用户' ).getOuterHtml();
+ expectedListUsers = '注册<a title="Special:ListUsers" href="/wiki/Special:ListUsers">用户</a>';
expectedEntrypoints = '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>';
+
+ formatText = mw.jqueryMsg.getMessageFunction( {
+ format: 'text'
+ } );
+
+ formatParse = mw.jqueryMsg.getMessageFunction( {
+ format: 'parse'
+ } );
},
teardown: function () {
mw.language = this.orgMwLangauge;
- $.fn.getOuterHtml = oldGetOuterHtml;
}
} ) );
@@ -82,18 +87,16 @@
}
QUnit.test( 'Replace', 9, function ( assert ) {
- var parser = mw.jqueryMsg.getMessageFunction();
-
mw.messages.set( 'simple', 'Foo $1 baz $2' );
- assert.equal( parser( 'simple' ), 'Foo $1 baz $2', 'Replacements with no substitutes' );
- assert.equal( parser( 'simple', 'bar' ), 'Foo bar baz $2', 'Replacements with less substitutes' );
- assert.equal( parser( 'simple', 'bar', 'quux' ), 'Foo bar baz quux', 'Replacements with all substitutes' );
+ assert.equal( formatParse( 'simple' ), 'Foo $1 baz $2', 'Replacements with no substitutes' );
+ assert.equal( formatParse( 'simple', 'bar' ), 'Foo bar baz $2', 'Replacements with less substitutes' );
+ assert.equal( formatParse( 'simple', 'bar', 'quux' ), 'Foo bar baz quux', 'Replacements with all substitutes' );
mw.messages.set( 'plain-input', '<foo foo="foo">x$1y&lt;</foo>z' );
assert.equal(
- parser( 'plain-input', 'bar' ),
+ formatParse( 'plain-input', 'bar' ),
'&lt;foo foo="foo"&gt;xbary&amp;lt;&lt;/foo&gt;z',
'Input is not considered html'
);
@@ -101,7 +104,7 @@
mw.messages.set( 'plain-replace', 'Foo $1' );
assert.equal(
- parser( 'plain-replace', '<bar bar="bar">&gt;</bar>' ),
+ formatParse( 'plain-replace', '<bar bar="bar">&gt;</bar>' ),
'Foo &lt;bar bar="bar"&gt;&amp;gt;&lt;/bar&gt;',
'Replacement is not considered html'
);
@@ -109,71 +112,68 @@
mw.messages.set( 'object-replace', 'Foo $1' );
assert.equal(
- parser( 'object-replace', $( '<div class="bar">&gt;</div>' ) ),
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ) ),
'Foo <div class="bar">&gt;</div>',
'jQuery objects are preserved as raw html'
);
assert.equal(
- parser( 'object-replace', $( '<div class="bar">&gt;</div>' ).get( 0 ) ),
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ).get( 0 ) ),
'Foo <div class="bar">&gt;</div>',
'HTMLElement objects are preserved as raw html'
);
assert.equal(
- parser( 'object-replace', $( '<div class="bar">&gt;</div>' ).toArray() ),
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ).toArray() ),
'Foo <div class="bar">&gt;</div>',
'HTMLElement[] arrays are preserved as raw html'
);
assert.equal(
- parser( 'external-link-replace', 'http://example.org/?x=y&z' ),
+ formatParse( 'external-link-replace', 'http://example.org/?x=y&z' ),
'Foo <a href="http://example.org/?x=y&amp;z">bar</a>',
'Href is not double-escaped in wikilink function'
);
} );
QUnit.test( 'Plural', 3, function ( assert ) {
- var parser = mw.jqueryMsg.getMessageFunction();
-
- assert.equal( parser( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' );
- assert.equal( parser( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' );
- assert.equal( parser( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' );
+ assert.equal( formatParse( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' );
+ assert.equal( formatParse( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' );
+ assert.equal( formatParse( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' );
} );
QUnit.test( 'Gender', 11, function ( assert ) {
// TODO: These tests should be for mw.msg once mw.msg integrated with mw.jqueryMsg
// TODO: English may not be the best language for these tests. Use a language like Arabic or Russian
- var user = mw.user,
- parser = mw.jqueryMsg.getMessageFunction();
+ var user = mw.user;
user.options.set( 'gender', 'male' );
assert.equal(
- parser( 'gender-msg', 'Bob', 'male' ),
+ formatParse( 'gender-msg', 'Bob', 'male' ),
'Bob: blue',
'Masculine from string "male"'
);
assert.equal(
- parser( 'gender-msg', 'Bob', user ),
+ formatParse( 'gender-msg', 'Bob', user ),
'Bob: blue',
'Masculine from mw.user object'
);
user.options.set( 'gender', 'unknown' );
assert.equal(
- parser( 'gender-msg', 'Foo', user ),
+ formatParse( 'gender-msg', 'Foo', user ),
'Foo: green',
'Neutral from mw.user object' );
assert.equal(
- parser( 'gender-msg', 'Alice', 'female' ),
+ formatParse( 'gender-msg', 'Alice', 'female' ),
'Alice: pink',
'Feminine from string "female"' );
assert.equal(
- parser( 'gender-msg', 'User' ),
+ formatParse( 'gender-msg', 'User' ),
'User: green',
'Neutral when no parameter given' );
assert.equal(
- parser( 'gender-msg', 'User', 'unknown' ),
+ formatParse( 'gender-msg', 'User', 'unknown' ),
'User: green',
'Neutral from string "unknown"'
);
@@ -181,43 +181,41 @@
mw.messages.set( 'gender-msg-one-form', '{{GENDER:$1|User}}: $2 {{PLURAL:$2|edit|edits}}' );
assert.equal(
- parser( 'gender-msg-one-form', 'male', 10 ),
+ formatParse( 'gender-msg-one-form', 'male', 10 ),
'User: 10 edits',
'Gender neutral and plural form'
);
assert.equal(
- parser( 'gender-msg-one-form', 'female', 1 ),
+ formatParse( 'gender-msg-one-form', 'female', 1 ),
'User: 1 edit',
'Gender neutral and singular form'
);
mw.messages.set( 'gender-msg-lowercase', '{{gender:$1|he|she}} is awesome' );
assert.equal(
- parser( 'gender-msg-lowercase', 'male' ),
+ formatParse( 'gender-msg-lowercase', 'male' ),
'he is awesome',
'Gender masculine'
);
assert.equal(
- parser( 'gender-msg-lowercase', 'female' ),
+ formatParse( 'gender-msg-lowercase', 'female' ),
'she is awesome',
'Gender feminine'
);
mw.messages.set( 'gender-msg-wrong', '{{gender}} test' );
assert.equal(
- parser( 'gender-msg-wrong', 'female' ),
+ formatParse( 'gender-msg-wrong', 'female' ),
' test',
'Invalid syntax should result in {{gender}} simply being stripped away'
);
} );
QUnit.test( 'Grammar', 2, function ( assert ) {
- var parser = mw.jqueryMsg.getMessageFunction();
-
- assert.equal( parser( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' );
+ assert.equal( formatParse( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' );
mw.messages.set( 'grammar-msg-wrong-syntax', 'Przeszukaj {{GRAMMAR:grammar_case_xyz}}' );
- assert.equal( parser( 'grammar-msg-wrong-syntax' ), 'Przeszukaj ', 'Grammar Test with wrong grammar template syntax' );
+ assert.equal( formatParse( 'grammar-msg-wrong-syntax' ), 'Przeszukaj ', 'Grammar Test with wrong grammar template syntax' );
} );
QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( assert ) {
@@ -242,8 +240,7 @@
} );
QUnit.test( 'Links', 6, function ( assert ) {
- var parser = mw.jqueryMsg.getMessageFunction(),
- expectedDisambiguationsText,
+ var expectedDisambiguationsText,
expectedMultipleBars,
expectedSpecialCharacters;
@@ -252,26 +249,24 @@
the bold was removed because it is not yet implemented.
*/
- assert.equal(
- parser( 'jquerymsg-test-statistics-users' ),
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-statistics-users' ),
expectedListUsers,
'Piped wikilink'
);
expectedDisambiguationsText = 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.\nA page is treated as a disambiguation page if it uses a template that is linked from ' +
- $( '<a>' ).attr( {
- title: 'MediaWiki:Disambiguationspage',
- href: mw.util.wikiGetlink( 'MediaWiki:Disambiguationspage' )
- } ).text( 'MediaWiki:Disambiguationspage' ).getOuterHtml() + '.';
+ '<a title="MediaWiki:Disambiguationspage" href="/wiki/MediaWiki:Disambiguationspage">MediaWiki:Disambiguationspage</a>.';
+
mw.messages.set( 'disambiguations-text', 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.\nA page is treated as a disambiguation page if it uses a template that is linked from [[MediaWiki:Disambiguationspage]].' );
- assert.equal(
- parser( 'disambiguations-text' ),
+ assert.htmlEqual(
+ formatParse( 'disambiguations-text' ),
expectedDisambiguationsText,
'Wikilink without pipe'
);
- assert.equal(
- parser( 'jquerymsg-test-version-entrypoints-index-php' ),
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-version-entrypoints-index-php' ),
expectedEntrypoints,
'External link'
);
@@ -279,30 +274,24 @@
// Pipe trick is not supported currently, but should not parse as text either.
mw.messages.set( 'pipe-trick', '[[Tampa, Florida|]]' );
assert.equal(
- parser( 'pipe-trick' ),
+ formatParse( 'pipe-trick' ),
'pipe-trick: Parse error at position 0 in input: [[Tampa, Florida|]]',
'Pipe trick should return error string.'
);
- expectedMultipleBars = $( '<a>' ).attr( {
- title: 'Main Page',
- href: mw.util.wikiGetlink( 'Main Page' )
- } ).text( 'Main|Page' ).getOuterHtml();
+ expectedMultipleBars = '<a title="Main Page" href="/wiki/Main_Page">Main|Page</a>';
mw.messages.set( 'multiple-bars', '[[Main Page|Main|Page]]' );
- assert.equal(
- parser( 'multiple-bars' ),
+ assert.htmlEqual(
+ formatParse( 'multiple-bars' ),
expectedMultipleBars,
'Bar in anchor'
);
- expectedSpecialCharacters = $( '<a>' ).attr( {
- title: specialCharactersPageName,
- href: mw.util.wikiGetlink( specialCharactersPageName )
- } ).text( specialCharactersPageName ).getOuterHtml();
+ expectedSpecialCharacters = '<a title="&quot;Who&quot; wants to be a millionaire &amp; live on &#039;Exotic Island&#039;?" href="/wiki/%22Who%22_wants_to_be_a_millionaire_%26_live_on_%27Exotic_Island%27%3F">&quot;Who&quot; wants to be a millionaire &amp; live on &#039;Exotic Island&#039;?</a>';
mw.messages.set( 'special-characters', '[[' + specialCharactersPageName + ']]' );
- assert.equal(
- parser( 'special-characters' ),
+ assert.htmlEqual(
+ formatParse( 'special-characters' ),
expectedSpecialCharacters,
'Special characters'
);
@@ -310,32 +299,16 @@
// Tests that {{-transformation vs. general parsing are done as requested
QUnit.test( 'Curly brace transformation', 14, function ( assert ) {
- var formatText, formatParse, oldUserLang;
-
- oldUserLang = mw.config.get( 'wgUserLanguage' );
-
- formatText = mw.jqueryMsg.getMessageFunction( {
- format: 'text'
- } );
-
- formatParse = mw.jqueryMsg.getMessageFunction( {
- format: 'parse'
- } );
-
- // When the expected result is the same in both modes
- function assertBothModes( parserArguments, expectedResult, assertMessage ) {
- assert.equal( formatText.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'text\'' );
- assert.equal( formatParse.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'parse\'' );
- }
+ var oldUserLang = mw.config.get( 'wgUserLanguage' );
- assertBothModes( ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' );
+ assertBothModes( assert, ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' );
- assertBothModes( ['plural-msg', 5], 'Found 5 items', 'plural is resolved' );
+ assertBothModes( assert, ['plural-msg', 5], 'Found 5 items', 'plural is resolved' );
- assertBothModes( ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' );
+ assertBothModes( assert, ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' );
mw.config.set( 'wgUserLanguage', 'en' );
- assertBothModes( ['formatnum-msg', '987654321.654321'], '987,654,321.654', 'formatnum is resolved' );
+ assertBothModes( assert, ['formatnum-msg', '987654321.654321'], '987,654,321.654', 'formatnum is resolved' );
// Test non-{{ wikitext, where behavior differs
@@ -345,7 +318,7 @@
mw.messages.get( 'jquerymsg-test-statistics-users' ),
'Internal link message unchanged when format is \'text\''
);
- assert.equal(
+ assert.htmlEqual(
formatParse( 'jquerymsg-test-statistics-users' ),
expectedListUsers,
'Internal link message parsed when format is \'parse\''
@@ -357,7 +330,7 @@
mw.messages.get( 'jquerymsg-test-version-entrypoints-index-php' ),
'External link message unchanged when format is \'text\''
);
- assert.equal(
+ assert.htmlEqual(
formatParse( 'jquerymsg-test-version-entrypoints-index-php' ),
expectedEntrypoints,
'External link message processed when format is \'parse\''
@@ -369,7 +342,7 @@
'Foo [http://example.com bar]',
'External link message only substitutes parameter when format is \'text\''
);
- assert.equal(
+ assert.htmlEqual(
formatParse( 'external-link-replace', 'http://example.com' ),
'Foo <a href="http://example.com">bar</a>',
'External link message processed when format is \'parse\''
@@ -379,28 +352,25 @@
} );
QUnit.test( 'Int', 4, function ( assert ) {
- var parser = mw.jqueryMsg.getMessageFunction(),
- newarticletextSource = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the [[{{Int:Helppage}}|help page]] for more info). If you are here by mistake, click your browser\'s back button.',
- expectedNewarticletext;
+ var newarticletextSource = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the [[{{Int:Helppage}}|help page]] for more info). If you are here by mistake, click your browser\'s back button.',
+ expectedNewarticletext,
+ helpPageTitle = 'Help:Contents';
- mw.messages.set( 'helppage', 'Help:Contents' );
+ mw.messages.set( 'helppage', helpPageTitle );
expectedNewarticletext = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the ' +
- $( '<a>' ).attr( {
- title: mw.msg( 'helppage' ),
- href: mw.util.wikiGetlink( mw.msg( 'helppage' ) )
- } ).text( 'help page' ).getOuterHtml() + ' for more info). If you are here by mistake, click your browser\'s back button.';
+ '<a title="Help:Contents" href="/wiki/Help:Contents">help page</a> for more info). If you are here by mistake, click your browser\'s back button.';
mw.messages.set( 'newarticletext', newarticletextSource );
- assert.equal(
- parser( 'newarticletext' ),
+ assert.htmlEqual(
+ formatParse( 'newarticletext' ),
expectedNewarticletext,
'Link with nested message'
);
assert.equal(
- parser( 'see-portal-url' ),
+ formatParse( 'see-portal-url' ),
'Project:Community portal is an important community page.',
'Nested message'
);
@@ -408,8 +378,8 @@
mw.messages.set( 'newarticletext-lowercase',
newarticletextSource.replace( 'Int:Helppage', 'int:helppage' ) );
- assert.equal(
- parser( 'newarticletext-lowercase' ),
+ assert.htmlEqual(
+ formatParse( 'newarticletext-lowercase' ),
expectedNewarticletext,
'Link with nested message, lowercase include'
);
@@ -417,7 +387,7 @@
mw.messages.set( 'uses-missing-int', '{{int:doesnt-exist}}' );
assert.equal(
- parser( 'uses-missing-int' ),
+ formatParse( 'uses-missing-int' ),
'[doesnt-exist]',
'int: where nested message does not exist'
);
@@ -596,4 +566,149 @@ QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) {
} );
} );
+// HTML in wikitext
+QUnit.test( 'HTML', 26, function ( assert ) {
+ mw.messages.set( 'jquerymsg-italics-msg', '<i>Very</i> important' );
+
+ assertBothModes( assert, ['jquerymsg-italics-msg'], mw.messages.get( 'jquerymsg-italics-msg' ), 'Simple italics unchanged' );
+
+ mw.messages.set( 'jquerymsg-bold-msg', '<b>Strong</b> speaker' );
+ assertBothModes( assert, ['jquerymsg-bold-msg'], mw.messages.get( 'jquerymsg-bold-msg' ), 'Simple bold unchanged' );
+
+ mw.messages.set( 'jquerymsg-bold-italics-msg', 'It is <b><i>key</i></b>' );
+ assertBothModes( assert, ['jquerymsg-bold-italics-msg'], mw.messages.get( 'jquerymsg-bold-italics-msg' ), 'Bold and italics nesting order preserved' );
+
+ mw.messages.set( 'jquerymsg-italics-bold-msg', 'It is <i><b>vital</b></i>' );
+ assertBothModes( assert, ['jquerymsg-italics-bold-msg'], mw.messages.get( 'jquerymsg-italics-bold-msg' ), 'Italics and bold nesting order preserved' );
+
+ mw.messages.set( 'jquerymsg-italics-with-link', 'An <i>italicized [[link|wiki-link]]</i>' );
+
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-with-link' ),
+ 'An <i>italicized <a title="link" href="' + mw.html.escape( mw.util.getUrl( 'link' ) ) + '">wiki-link</i>',
+ 'Italics with link inside in parse mode'
+ );
+
+ assert.equal(
+ formatText( 'jquerymsg-italics-with-link' ),
+ mw.messages.get( 'jquerymsg-italics-with-link' ),
+ 'Italics with link unchanged in text mode'
+ );
+
+ mw.messages.set( 'jquerymsg-italics-id-class', '<i id="foo" class="bar">Foo</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-id-class' ),
+ mw.messages.get( 'jquerymsg-italics-id-class' ),
+ 'ID and class are allowed'
+ );
+
+ mw.messages.set( 'jquerymsg-italics-onclick', '<i onclick="alert(\'foo\')">Foo</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-onclick' ),
+ '&lt;i onclick=&quot;alert(\'foo\')&quot;&gt;Foo&lt;/i&gt;',
+ 'element with onclick is escaped because it is not allowed'
+ );
+
+ mw.messages.set( 'jquerymsg-script-msg', '<script >alert( "Who put this tag here?" );</script>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-msg' ),
+ '&lt;script &gt;alert( &quot;Who put this tag here?&quot; );&lt;/script&gt;',
+ 'Tag outside whitelist escaped in parse mode'
+ );
+
+ assert.equal(
+ formatText( 'jquerymsg-script-msg' ),
+ mw.messages.get( 'jquerymsg-script-msg' ),
+ 'Tag outside whitelist unchanged in text mode'
+ );
+
+ mw.messages.set( 'jquerymsg-script-link-msg', '<script>[[Foo|bar]]</script>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-link-msg' ),
+ '&lt;script&gt;<a title="Foo" href="' + mw.html.escape( mw.util.getUrl( 'Foo' ) ) + '">bar</a>&lt;/script&gt;',
+ 'Script tag text is escaped because that element is not allowed, but link inside is still HTML'
+ );
+
+ mw.messages.set( 'jquerymsg-mismatched-html', '<i class="important">test</b>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-mismatched-html' ),
+ '&lt;i class=&quot;important&quot;&gt;test&lt;/b&gt;',
+ 'Mismatched HTML start and end tag treated as text'
+ );
+
+ // TODO (mattflaschen, 2013-03-18): It's not a security issue, but there's no real
+ // reason the htmlEmitter span needs to be here. It's an artifact of how emitting works.
+ mw.messages.set( 'jquerymsg-script-and-external-link', '<script>alert( "jquerymsg-script-and-external-link test" );</script> [http://example.com <i>Foo</i> bar]' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-and-external-link' ),
+ '&lt;script&gt;alert( "jquerymsg-script-and-external-link test" );&lt;/script&gt; <a href="http://example.com"><span class="mediaWiki_htmlEmitter"><i>Foo</i> bar</span></a>',
+ 'HTML tags in external links not interfering with escaping of other tags'
+ );
+
+ mw.messages.set( 'jquerymsg-link-script', '[http://example.com <script>alert( "jquerymsg-link-script test" );</script>]' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-link-script' ),
+ '<a href="http://example.com"><span class="mediaWiki_htmlEmitter">&lt;script&gt;alert( "jquerymsg-link-script test" );&lt;/script&gt;</span></a>',
+ 'Non-whitelisted HTML tag in external link anchor treated as text'
+ );
+
+ // Intentionally not using htmlEqual for the quote tests
+ mw.messages.set( 'jquerymsg-double-quotes-preserved', '<i id="double">Double</i>' );
+ assert.equal(
+ formatParse( 'jquerymsg-double-quotes-preserved' ),
+ mw.messages.get( 'jquerymsg-double-quotes-preserved' ),
+ 'Attributes with double quotes are preserved as such'
+ );
+
+ mw.messages.set( 'jquerymsg-single-quotes-normalized-to-double', '<i id=\'single\'>Single</i>' );
+ assert.equal(
+ formatParse( 'jquerymsg-single-quotes-normalized-to-double' ),
+ '<i id="single">Single</i>',
+ 'Attributes with single quotes are normalized to double'
+ );
+
+ mw.messages.set( 'jquerymsg-escaped-double-quotes-attribute', '<i style="font-family:&quot;Arial&quot;">Styled</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-escaped-double-quotes-attribute' ),
+ mw.messages.get( 'jquerymsg-escaped-double-quotes-attribute' ),
+ 'Escaped attributes are parsed correctly'
+ );
+
+ mw.messages.set( 'jquerymsg-escaped-single-quotes-attribute', '<i style=\'font-family:&#039;Arial&#039;\'>Styled</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-escaped-single-quotes-attribute' ),
+ mw.messages.get( 'jquerymsg-escaped-single-quotes-attribute' ),
+ 'Escaped attributes are parsed correctly'
+ );
+
+
+ mw.messages.set( 'jquerymsg-wikitext-contents-parsed', '<i>[http://example.com Example]</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-wikitext-contents-parsed' ),
+ '<i><a href="http://example.com">Example</a></i>',
+ 'Contents of valid tag are treated as wikitext, so external link is parsed'
+ );
+
+ mw.messages.set( 'jquerymsg-wikitext-contents-script', '<i><script>Script inside</script></i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-wikitext-contents-script' ),
+ '<i><span class="mediaWiki_htmlEmitter">&lt;script&gt;Script inside&lt;/script&gt;</span></i>',
+ 'Contents of valid tag are treated as wikitext, so invalid HTML element is treated as text'
+ );
+
+ mw.messages.set( 'jquerymsg-unclosed-tag', 'Foo<tag>bar' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-unclosed-tag' ),
+ 'Foo&lt;tag&gt;bar',
+ 'Nonsupported unclosed tags are escaped'
+ );
+
+ mw.messages.set( 'jquerymsg-self-closing-tag', 'Foo<tag/>bar' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-self-closing-tag' ),
+ 'Foo&lt;tag/&gt;bar',
+ 'Self-closing tags don\'t cause a parse error'
+ );
+} );
+
}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
index 01e78f61..bd4d1d21 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
@@ -15,11 +15,17 @@
'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome',
'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
'formatnum-msg': '{{formatnum:$1}}',
- 'int-msg': 'Some {{int:other-message}}'
+ 'int-msg': 'Some {{int:other-message}}',
+ 'mediawiki-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+ 'external-link-replace': 'Foo [$1 bar]'
} );
- // For formatnum tests
- mw.config.set( 'wgUserLanguage', 'en' );
+ mw.config.set( {
+ wgArticlePath: '/wiki/$1',
+
+ // For formatnum tests
+ wgUserLanguage: 'en'
+ } );
specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
}
@@ -124,7 +130,7 @@
assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
} );
- QUnit.test( 'mw.message & mw.messages', 54, function ( assert ) {
+ QUnit.test( 'mw.message & mw.messages', 100, function ( assert ) {
var goodbye, hello;
// Convenience method for asserting the same result for multiple formats
@@ -158,11 +164,24 @@
assert.equal( hello.escaped(), 'Hello &lt;b&gt;awesome&lt;/b&gt; world', 'Message.escaped returns the escaped message' );
assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
- assert.ok( mw.messages.set( 'escaped-with-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ) );
- assert.equal( mw.message( 'escaped-with-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName' ) + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+ assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+
+ assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
+ assert.equal( mw.message( 'multiple-square-brackets-and-ampersand' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+ assert.htmlEqual( mw.message( 'multiple-square-brackets-and-ampersand' ).parse(), 'Visit the ' +
+ '<a title="Project:Community portal" href="/wiki/Project:Community_portal">community portal</a>' +
+ ' &amp; <a title="Project:Help desk" href="/wiki/Project:Help_desk">help desk</a>', 'Internal links work with parse' );
- assert.ok( mw.messages.set( 'escaped-with-square-brackets', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ) );
- assert.equal( mw.message( 'escaped-with-square-brackets' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+ assertMultipleFormats( ['mediawiki-test-version-entrypoints-index-php'], ['plain', 'text', 'escaped'], mw.messages.get( 'mediawiki-test-version-entrypoints-index-php' ), 'External link markup is unprocessed' );
+ assert.htmlEqual( mw.message( 'mediawiki-test-version-entrypoints-index-php' ).parse(), '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>', 'External link works correctly in parse mode' );
+
+ assertMultipleFormats( ['external-link-replace', 'http://example.org/?x=y&z'], ['plain', 'text'] , 'Foo [http://example.org/?x=y&z bar]', 'Parameters are substituted but external link is not processed' );
+ assert.equal( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).escaped(), 'Foo [http://example.org/?x=y&amp;z bar]', 'In escaped mode, parameters are substituted and ampersand is escaped, but external link is not processed' );
+ assert.htmlEqual( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).parse(), 'Foo <a href="http://example.org/?x=y&amp;z">bar</a>', 'External link with replacement works in parse mode without double-escaping' );
hello.parse();
assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
@@ -186,6 +205,16 @@
assertMultipleFormats( ['plural-test-msg', 6], ['text', 'parse', 'escaped'], 'There are 6 results', 'plural get resolved' );
assert.equal( mw.message( 'plural-test-msg', 6 ).plain(), 'There {{PLURAL:6|is|are}} 6 {{PLURAL:6|result|results}}', 'Parameter is substituted but plural is not resolved in plain' );
+ assert.ok( mw.messages.set( 'plural-test-msg-explicit', 'There {{plural:$1|is one car|are $1 cars|0=are no cars|12=are a dozen cars}}' ), 'mw.messages.set: Register message with explicit plural forms' );
+ assertMultipleFormats( ['plural-test-msg-explicit', 12], ['text', 'parse', 'escaped'], 'There are a dozen cars', 'explicit plural get resolved' );
+
+ assert.ok( mw.messages.set( 'plural-test-msg-explicit-beginning', 'Basket has {{plural:$1|0=no eggs|12=a dozen eggs|6=half a dozen eggs|one egg|$1 eggs}}' ), 'mw.messages.set: Register message with explicit plural forms' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 1], ['text', 'parse', 'escaped'], 'Basket has one egg', 'explicit plural given at beginning get resolved for singular' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 4], ['text', 'parse', 'escaped'], 'Basket has 4 eggs', 'explicit plural given at beginning get resolved for plural' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 6], ['text', 'parse', 'escaped'], 'Basket has half a dozen eggs', 'explicit plural given at beginning get resolved for 6' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 0], ['text', 'parse', 'escaped'], 'Basket has no eggs', 'explicit plural given at beginning get resolved for 0' );
+
+
assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary'], ['plain', 'text'], mw.messages.get( 'mediawiki-test-pagetriage-del-talk-page-notify-summary' ), 'Double square brackets with no parameters unchanged' );
assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName], ['plain', 'text'], 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets with one parameter' );
@@ -196,7 +225,7 @@
assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[<b>−</b>]' ), 'mw.messages.set: Register' );
assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' );
- assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ) );
+ assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ), 'mw.messages.set: Register' );
assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' );
assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' );
@@ -211,6 +240,42 @@
assertMultipleFormats( ['int-msg'], ['text', 'parse', 'escaped'], 'Some Other Message', 'int is resolved' );
assert.equal( mw.message( 'int-msg' ).plain(), mw.messages.get( 'int-msg' ), 'int is not resolved in plain mode' );
+
+ assert.ok( mw.messages.set( 'mediawiki-italics-msg', '<i>Very</i> important' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-italics-msg'], ['plain', 'text', 'parse'], mw.messages.get( 'mediawiki-italics-msg' ), 'Simple italics unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-msg' ).escaped(),
+ '&lt;i&gt;Very&lt;/i&gt; important',
+ 'Italics are escaped in escaped mode'
+ );
+
+ assert.ok( mw.messages.set( 'mediawiki-italics-with-link', 'An <i>italicized [[link|wiki-link]]</i>' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-italics-with-link'], ['plain', 'text'], mw.messages.get( 'mediawiki-italics-with-link' ), 'Italics with link unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-with-link' ).escaped(),
+ 'An &lt;i&gt;italicized [[link|wiki-link]]&lt;/i&gt;',
+ 'Italics and link unchanged except for escaping in escaped mode'
+ );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-with-link' ).parse(),
+ 'An <i>italicized <a title="link" href="' + mw.util.getUrl( 'link' ) + '">wiki-link</i>',
+ 'Italics with link inside in parse mode'
+ );
+
+ assert.ok( mw.messages.set( 'mediawiki-script-msg', '<script >alert( "Who put this script here?" );</script>' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-script-msg'], ['plain', 'text'], mw.messages.get( 'mediawiki-script-msg' ), 'Script unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-script-msg' ).escaped(),
+ '&lt;script &gt;alert( "Who put this script here?" );&lt;/script&gt;',
+ 'Script escaped when using escaped format'
+ );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-script-msg' ).parse(),
+ '&lt;script &gt;alert( "Who put this script here?" );&lt;/script&gt;',
+ 'Script escaped when using parse format'
+ );
+
+
} );
QUnit.test( 'mw.msg', 14, function ( assert ) {
@@ -218,7 +283,7 @@
assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
- assert.ok( mw.messages.set( 'plural-item', 'Found $1 {{PLURAL:$1|item|items}}' ) );
+ assert.ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ), 'mw.messages.set: Register' );
assert.equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' );
assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
@@ -762,4 +827,90 @@
} );
+ QUnit.test( 'mw.hook', 10, function ( assert ) {
+ var hook, add, fire, chars, callback;
+
+ mw.hook( 'test.hook.unfired' ).add( function () {
+ assert.ok( false, 'Unfired hook' );
+ } );
+
+ mw.hook( 'test.hook.basic' ).add( function () {
+ assert.ok( true, 'Basic callback' );
+ } );
+ mw.hook( 'test.hook.basic' ).fire();
+
+ mw.hook( 'test.hook.data' ).add( function ( data1, data2 ) {
+ assert.equal( data1, 'example', 'Fire with data (string param)' );
+ assert.deepEqual( data2, ['two'], 'Fire with data (array param)' );
+ } );
+ mw.hook( 'test.hook.data' ).fire( 'example', ['two'] );
+
+ mw.hook( 'test.hook.chainable' ).add( function () {
+ assert.ok( true, 'Chainable' );
+ } ).fire();
+
+ hook = mw.hook( 'test.hook.detach' );
+ add = hook.add;
+ fire = hook.fire;
+ add( function ( x, y ) {
+ assert.deepEqual( [x, y], ['x', 'y'], 'Detached (contextless) with data' );
+ } );
+ fire( 'x', 'y' );
+
+ mw.hook( 'test.hook.fireBefore' ).fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before' );
+ } );
+
+ mw.hook( 'test.hook.fireTwiceBefore' ).fire().fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before (only last one)' );
+ } );
+
+ chars = [];
+
+ mw.hook( 'test.hook.many' )
+ .add( function ( chr ) {
+ chars.push( chr );
+ } )
+ .fire( 'x' ).fire( 'y' ).fire( 'z' )
+ .add( function ( chr ) {
+ assert.equal( chr, 'z', 'Adding callback later invokes right away with last data' );
+ } );
+
+ assert.deepEqual( chars, ['x', 'y', 'z'], 'Multiple callbacks with multiple fires' );
+
+ chars = [];
+ callback = function ( chr ) {
+ chars.push( chr );
+ };
+
+ mw.hook( 'test.hook.variadic' )
+ .add(
+ callback,
+ callback,
+ function ( chr ) {
+ chars.push( chr );
+ },
+ callback
+ )
+ .fire( 'x' )
+ .remove(
+ function () {
+ 'not-added';
+ },
+ callback
+ )
+ .fire( 'y' )
+ .remove( callback )
+ .fire( 'z' );
+
+ assert.deepEqual(
+ chars,
+ ['x', 'x', 'x', 'x', 'y', 'z'],
+ '"add" and "remove" support variadic arguments. ' +
+ '"add" does not filter unique. ' +
+ '"remove" removes all equal by reference. ' +
+ '"remove" is silent if the function is not found'
+ );
+ } );
+
}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
index 875ab91a..96be3d1f 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
@@ -5,7 +5,7 @@
assert.ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' );
} );
- QUnit.test( 'user status', 9, function ( assert ) {
+ QUnit.test( 'user status', 11, function ( assert ) {
/**
* Tests can be run under three different conditions:
* 1) From tests/qunit/index.html, user will be anonymous.
@@ -15,19 +15,23 @@
// Forge an anonymous user:
mw.config.set( 'wgUserName', null );
+ delete mw.config.values.wgUserId;
assert.strictEqual( mw.user.getName(), null, 'user.getName() returns null when anonymous' );
assert.strictEqual( mw.user.name(), null, 'user.name() compatibility' );
assert.assertTrue( mw.user.isAnon(), 'user.isAnon() returns true when anonymous' );
assert.assertTrue( mw.user.anonymous(), 'user.anonymous() compatibility' );
+ assert.strictEqual( mw.user.getId(), 0, 'user.getId() returns 0 when anonymous' );
// Not part of startUp module
mw.config.set( 'wgUserName', 'John' );
+ mw.config.set( 'wgUserId', 123 );
assert.equal( mw.user.getName(), 'John', 'user.getName() returns username when logged-in' );
assert.equal( mw.user.name(), 'John', 'user.name() compatibility' );
assert.assertFalse( mw.user.isAnon(), 'user.isAnon() returns false when logged-in' );
assert.assertFalse( mw.user.anonymous(), 'user.anonymous() compatibility' );
+ assert.strictEqual( mw.user.getId(), 123, 'user.getId() returns correct ID when logged-in' );
assert.equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' );
} );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
index 6fc0731c..9216f0af 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
@@ -1,5 +1,13 @@
( function ( mw, $ ) {
- QUnit.module( 'mediawiki.util', QUnit.newMwEnvironment() );
+ QUnit.module( 'mediawiki.util', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.taPrefix = mw.util.tooltipAccessKeyPrefix;
+ mw.util.tooltipAccessKeyPrefix = 'ctrl-alt-';
+ },
+ teardown: function () {
+ mw.util.tooltipAccessKeyPrefix = this.taPrefix;
+ }
+ } ) );
QUnit.test( 'rawurlencode', 1, function ( assert ) {
assert.equal( mw.util.rawurlencode( 'Test:A & B/Here' ), 'Test%3AA%20%26%20B%2FHere' );
@@ -9,20 +17,24 @@
assert.equal( mw.util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' );
} );
- QUnit.test( 'wikiGetlink', 3, function ( assert ) {
+ QUnit.test( 'getUrl', 4, function ( assert ) {
// Not part of startUp module
mw.config.set( 'wgArticlePath', '/wiki/$1' );
mw.config.set( 'wgPageName', 'Foobar' );
- var href = mw.util.wikiGetlink( 'Sandbox' );
+ var href = mw.util.getUrl( 'Sandbox' );
assert.equal( href, '/wiki/Sandbox', 'Simple title; Get link for "Sandbox"' );
- href = mw.util.wikiGetlink( 'Foo:Sandbox ? 5+5=10 ! (test)/subpage' );
+ href = mw.util.getUrl( 'Foo:Sandbox ? 5+5=10 ! (test)/subpage' );
assert.equal( href, '/wiki/Foo:Sandbox_%3F_5%2B5%3D10_%21_%28test%29/subpage',
'Advanced title; Get link for "Foo:Sandbox ? 5+5=10 ! (test)/subpage"' );
- href = mw.util.wikiGetlink();
+ href = mw.util.getUrl();
assert.equal( href, '/wiki/Foobar', 'Default title; Get link for current page ("Foobar")' );
+
+ href = mw.util.getUrl( 'Sandbox', { action: 'edit' } );
+ assert.equal( href, '/wiki/Sandbox?action=edit',
+ 'Simple title with query string; Get link for "Sandbox" with action=edit' );
} );
QUnit.test( 'wikiScript', 4, function ( assert ) {
@@ -76,13 +88,13 @@
assert.strictEqual( mw.util.toggleToc(), null, 'Return null if there is no table of contents on the page.' );
- tocHtml = '<table id="toc" class="toc"><tr><td>' +
+ tocHtml = '<div id="toc" class="toc">' +
'<div id="toctitle">' +
'<h2>Contents</h2>' +
'<span class="toctoggle">&nbsp;[<a href="#" class="internal" id="togglelink">Hide</a>&nbsp;]</span>' +
'</div>' +
'<ul><li></li></ul>' +
- '</td></tr></table>';
+ '</div>';
$( tocHtml ).appendTo( '#qunit-fixture' );
$toggleLink = $( '#togglelink' );
@@ -108,10 +120,14 @@
assert.strictEqual( mw.util.getParamValue( 'TEST', url ), 'a b+c d', 'Bug 30441: getParamValue must understand "+" encoding of space (multiple spaces)' );
} );
- QUnit.test( 'tooltipAccessKey', 3, function ( assert ) {
- assert.equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'mw.util.tooltipAccessKeyPrefix must be a string' );
- assert.ok( mw.util.tooltipAccessKeyRegexp instanceof RegExp, 'mw.util.tooltipAccessKeyRegexp instance of RegExp' );
- assert.ok( mw.util.updateTooltipAccessKeys, 'mw.util.updateTooltipAccessKeys' );
+ QUnit.test( 'tooltipAccessKey', 4, function ( assert ) {
+ assert.equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'tooltipAccessKeyPrefix must be a string' );
+ assert.equal( $.type( mw.util.tooltipAccessKeyRegexp ), 'regexp', 'tooltipAccessKeyRegexp is a regexp' );
+ assert.ok( mw.util.updateTooltipAccessKeys, 'updateTooltipAccessKeys is non-empty' );
+
+ 'Example [a]'.replace( mw.util.tooltipAccessKeyRegexp, function ( sub, m1, m2, m3, m4, m5, m6 ) {
+ assert.equal( m6, 'a', 'tooltipAccessKeyRegexp finds the accesskey hint' );
+ } );
} );
QUnit.test( '$content', 2, function ( assert ) {
@@ -125,17 +141,18 @@
* Previously, test elements where invisible to the selector since only
* one element can have a given id.
*/
- QUnit.test( 'addPortletLink', 8, function ( assert ) {
- var pTestTb, pCustom, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo;
+ QUnit.test( 'addPortletLink', 13, function ( assert ) {
+ var pTestTb, pCustom, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo,
+ addedAfter, tbRLDMnonexistentid, tbRLDMemptyjquery;
pTestTb = '\
<div class="portlet" id="p-test-tb">\
- <h5>Toolbox</h5>\
+ <h3>Toolbox</h3>\
<ul class="body"></ul>\
</div>';
pCustom = '\
<div class="portlet" id="p-test-custom">\
- <h5>Views</h5>\
+ <h3>Views</h3>\
<ul class="body">\
<li id="c-foo"><a href="#">Foo</a></li>\
<li id="c-barmenu">\
@@ -147,14 +164,15 @@
</div>';
vectorTabs = '\
<div id="p-test-views" class="vectorTabs">\
- <h5>Views</h5>\
+ <h3>Views</h3>\
<ul></ul>\
</div>';
$( '#qunit-fixture' ).append( pTestTb, pCustom, vectorTabs );
tbRL = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/ResourceLoader',
- 'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l' );
+ 'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l'
+ );
assert.ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' );
@@ -162,14 +180,32 @@
'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', tbRL );
$tbMW = $( tbMW );
+ assert.propEqual(
+ $tbMW.getAttrs(),
+ {
+ id: 't-mworg'
+ },
+ 'Validate attributes of created element'
+ );
+
+ assert.propEqual(
+ $tbMW.find( 'a' ).getAttrs(),
+ {
+ href: '//mediawiki.org/',
+ title: 'Go to MediaWiki.org [ctrl-alt-m]',
+ accesskey: 'm'
+ },
+ 'Validate attributes of anchor tag in created element'
+ );
- assert.equal( $tbMW.attr( 'id' ), 't-mworg', 'Link has correct ID set' );
assert.equal( $tbMW.closest( '.portlet' ).attr( 'id' ), 'p-test-tb', 'Link was inserted within correct portlet' );
- assert.equal( $tbMW.next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing nextnode)' );
+ assert.strictEqual( $tbMW.next()[0], tbRL, 'Link is in the correct position (by passing nextnode)' );
- cuQuux = mw.util.addPortletLink( 'p-test-custom', '#', 'Quux' );
+ cuQuux = mw.util.addPortletLink( 'p-test-custom', '#', 'Quux', null, 'Example [shift-x]', 'q' );
$cuQuux = $( cuQuux );
+ assert.equal( $cuQuux.find( 'a' ).attr( 'title' ), 'Example [ctrl-alt-q]', 'Existing accesskey is stripped and updated' );
+
assert.equal(
$( '#p-test-custom #c-barmenu ul li' ).length,
1,
@@ -185,6 +221,21 @@
assert.strictEqual( $tbMW.find( 'span' ).length, 0, 'No <span> element should be added for porlets without vectorTabs class.' );
assert.strictEqual( $( caFoo ).find( 'span' ).length, 1, 'A <span> element should be added for porlets with vectorTabs class.' );
+
+ addedAfter = mw.util.addPortletLink( 'p-test-tb', '#', 'After foo', 'post-foo', 'After foo', null, $( tbRL ) );
+ assert.strictEqual( $( addedAfter ).next()[0], tbRL, 'Link is in the correct position (by passing a jQuery object as nextnode)' );
+
+ // test case - nonexistent id as next node
+ tbRLDMnonexistentid = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM',
+ 'Default modules', 't-rldm-nonexistent', 'List of all default modules ', 'd', '#t-rl-nonexistent' );
+
+ assert.equal( tbRLDMnonexistentid, $( '#p-test-tb li:last' )[0], 'Nonexistent id as nextnode adds the portlet at end' );
+
+ // test case - empty jquery object as next node
+ tbRLDMemptyjquery = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM',
+ 'Default modules', 't-rldm-empty-jquery', 'List of all default modules ', 'd', $( '#t-rl-nonexistent' ) );
+
+ assert.equal( tbRLDMemptyjquery, $( '#p-test-tb li:last' )[0], 'Empty jquery as nextnode adds the portlet at end' );
} );
QUnit.test( 'jsMessage', 1, function ( assert ) {
diff --git a/tests/qunit/suites/resources/startup.test.js b/tests/qunit/suites/resources/startup.test.js
new file mode 100644
index 00000000..76f32f7e
--- /dev/null
+++ b/tests/qunit/suites/resources/startup.test.js
@@ -0,0 +1,129 @@
+/*global isCompatible: true */
+( function ( $ ) {
+ var testcases = {
+ // Supported: Compatible
+ gradeA: [
+ // Chrome
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16',
+ // Firefox 4+
+ 'Mozilla/5.0 (Windows NT 6.1.1; rv:5.0) Gecko/20100101 Firefox/5.0',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0',
+ 'Mozilla/5.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4',
+ 'Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0',
+ 'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1',
+ // Kindle Fire
+ 'Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Kindle Fire Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Safari/533.1',
+ // Safari 5.0+
+ 'Mozilla/5.0 (Macintosh; I; Intel Mac OS X 10_6_7; ru-ru) AppleWebKit/534.31+ (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
+ // Opera 11+
+ 'Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.8.131 Version/11.10',
+ // Internet Explorer 6+
+ 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)',
+ 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)',
+ 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)',
+ 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)',
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
+ // IE Mobile
+ 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 800)',
+ // BlackBerry 6+
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9300; en) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.570 Mobile Safari/534.8+',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+',
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.3+ (KHTML, like Gecko) Version/10.0.9.386 Mobile Safari/537.3+',
+ // Open WebOS 1.4+ (HP Veer 4G)
+ 'Mozilla/5.0 (webOS/2.1.2; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 P160UNA/1.0',
+ // Firefox Mobile
+ 'Mozilla/5.0 (Mobile; rv:14.0) Gecko/14.0 Firefox/14.0',
+ // iOS
+ 'Mozilla/5.0 (ipod: U;CPU iPhone OS 2_2 like Mac OS X: es_es) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
+ 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
+ // Android
+ 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17'
+ ],
+ // Supported: Uncompatible, serve basic content
+ gradeB: [
+ // Internet Explorer < 6
+ 'Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)',
+ 'Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)',
+ 'Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;)',
+ 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
+ // Firefox < 3.6
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2',
+ 'Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1.1) Gecko/20070311 Firefox/2.0.0.1',
+ // BlackBerry < 6
+ 'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133',
+ 'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1',
+ // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
+ 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
+ 'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
+ // SymbianOS
+ 'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
+ 'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
+ 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
+ 'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
+ // NetFront
+ 'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)',
+ 'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)',
+ 'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2',
+ // Opera Mini
+ 'Opera/9.80 (J2ME/MIDP; Opera Mini/3.1.10423/22.387; U; en) Presto/2.5.25 Version/10.54',
+ 'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)',
+ 'Opera/9.80 (J2ME/MIDP; Opera Mini/6.24093/26.1305; U; en) Presto/2.8.119 Version/10.54',
+ 'Opera/9.80 (Android; Opera Mini/7.29530/27.1407; U; en) Presto/2.8.119 Version/11.10',
+ // Ovi Browser
+ 'Mozilla/5.0 (Series40; NokiaX3-02/05.60; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.2.0.0.6',
+ 'Mozilla/5.0 (Series40; Nokia305/05.92; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.7.0.0.11'
+ ],
+ // No explicit support for or against these browsers, they're
+ // given a shot at Grade A at their own risk.
+ gradeX: [
+ // Firefox 3.6
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3',
+ // Gecko
+ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060928 (Debian|Debian-1.8.0.7-1) Epiphany/2.14',
+ 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.6) Gecko/20070817 IceWeasel/2.0.0.6-g2',
+ // KHTML
+ 'Mozilla/5.0 (compatible; Konqueror/4.3; Linux) KHTML/4.3.5 (like Gecko)',
+ // Text browsers
+ 'Links (2.1pre33; Darwin 8.11.0 Power Macintosh; x)',
+ 'Links (6.9; Unix 6.9-astral sparc; 80x25)',
+ 'Lynx/2.8.6rel.4 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8g',
+ 'w3m/0.5.1',
+ // Bots
+ 'Googlebot/2.1 (+http://www.google.com/bot.html)',
+ 'Mozilla/5.0 (compatible; googlebot/2.1; +http://www.google.com/bot.html)',
+ 'Mozilla/5.0 (compatible; YandexBot/3.0)',
+ // Scripts
+ 'curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5',
+ 'Wget/1.9',
+ 'Wget/1.10.1 (Red Hat modified)',
+ // Unknown
+ 'I\'m an unknown browser',
+ // Empty
+ ''
+ ]
+ };
+
+ QUnit.module( 'startup', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'isCompatible( Grade A )', testcases.gradeA.length, function ( assert ) {
+ $.each( testcases.gradeA, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), true, ua );
+ }
+ );
+ } );
+
+ QUnit.test( 'isCompatible( Grade B )', testcases.gradeB.length, function ( assert ) {
+ $.each( testcases.gradeB, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), false, ua );
+ }
+ );
+ } );
+
+ QUnit.test( 'isCompatible( Grade X )', testcases.gradeX.length, function ( assert ) {
+ $.each( testcases.gradeX, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), true, ua );
+ }
+ );
+ } );
+
+}( jQuery ) );