diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
commit | 4ac9fa081a7c045f6a9f1cfc529d82423f485b2e (patch) | |
tree | af68743f2f4a47d13f2b0eb05f5c4aaf86d8ea37 /tests/qunit | |
parent | af4da56f1ad4d3ef7b06557bae365da2ea27a897 (diff) |
Update to MediaWiki 1.22.0
Diffstat (limited to 'tests/qunit')
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 é B', + //'A é B', + //'A é 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<</foo>z' ); assert.equal( - parser( 'plain-input', 'bar' ), + formatParse( 'plain-input', 'bar' ), '<foo foo="foo">xbary&lt;</foo>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">></bar>' ), + formatParse( 'plain-replace', '<bar bar="bar">></bar>' ), 'Foo <bar bar="bar">&gt;</bar>', 'Replacement is not considered html' ); @@ -109,71 +112,68 @@ mw.messages.set( 'object-replace', 'Foo $1' ); assert.equal( - parser( 'object-replace', $( '<div class="bar">></div>' ) ), + formatParse( 'object-replace', $( '<div class="bar">></div>' ) ), 'Foo <div class="bar">></div>', 'jQuery objects are preserved as raw html' ); assert.equal( - parser( 'object-replace', $( '<div class="bar">></div>' ).get( 0 ) ), + formatParse( 'object-replace', $( '<div class="bar">></div>' ).get( 0 ) ), 'Foo <div class="bar">></div>', 'HTMLElement objects are preserved as raw html' ); assert.equal( - parser( 'object-replace', $( '<div class="bar">></div>' ).toArray() ), + formatParse( 'object-replace', $( '<div class="bar">></div>' ).toArray() ), 'Foo <div class="bar">></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&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=""Who" wants to be a millionaire & live on 'Exotic Island'?" href="/wiki/%22Who%22_wants_to_be_a_millionaire_%26_live_on_%27Exotic_Island%27%3F">"Who" wants to be a millionaire & live on 'Exotic Island'?</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' ), + '<i onclick="alert(\'foo\')">Foo</i>', + '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' ), + '<script >alert( "Who put this tag here?" );</script>', + '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' ), + '<script><a title="Foo" href="' + mw.html.escape( mw.util.getUrl( 'Foo' ) ) + '">bar</a></script>', + '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' ), + '<i class="important">test</b>', + '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' ), + '<script>alert( "jquerymsg-script-and-external-link test" );</script> <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"><script>alert( "jquerymsg-link-script test" );</script></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:"Arial"">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:'Arial'\'>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"><script>Script inside</script></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<tag>bar', + 'Nonsupported unclosed tags are escaped' + ); + + mw.messages.set( 'jquerymsg-self-closing-tag', 'Foo<tag/>bar' ); + assert.htmlEqual( + formatParse( 'jquerymsg-self-closing-tag' ), + 'Foo<tag/>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 <b>awesome</b> 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]] & [[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>' + + ' & <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]] & [[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&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&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(), + '<i>Very</i> 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 <i>italicized [[link|wiki-link]]</i>', + '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(), + '<script >alert( "Who put this script here?" );</script>', + 'Script escaped when using escaped format' + ); + assert.htmlEqual( + mw.message( 'mediawiki-script-msg' ).parse(), + '<script >alert( "Who put this script here?" );</script>', + '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"> [<a href="#" class="internal" id="togglelink">Hide</a> ]</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 ) ); |