diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2011-12-03 13:29:22 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2011-12-03 13:29:22 +0100 |
commit | ca32f08966f1b51fcb19460f0996bb0c4048e6fe (patch) | |
tree | ec04cc15b867bc21eedca904cea9af0254531a11 /tests/qunit | |
parent | a22fbfc60f36f5f7ee10d5ae6fe347340c2ee67c (diff) |
Update to MediaWiki 1.18.0
* also update ArchLinux skin to chagnes in MonoBook
* Use only css to hide our menu bar when printing
Diffstat (limited to 'tests/qunit')
20 files changed, 2458 insertions, 0 deletions
diff --git a/tests/qunit/.htaccess b/tests/qunit/.htaccess new file mode 100644 index 00000000..605d2f4c --- /dev/null +++ b/tests/qunit/.htaccess @@ -0,0 +1 @@ +Allow from all diff --git a/tests/qunit/data/defineTestCallback.js b/tests/qunit/data/defineTestCallback.js new file mode 100644 index 00000000..6fcd4595 --- /dev/null +++ b/tests/qunit/data/defineTestCallback.js @@ -0,0 +1,4 @@ +window.mw.loader.testCallback = function() { + start(); + ok( true, 'Implementing a module, is the callback timed properly ?'); +}; diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js new file mode 100644 index 00000000..dbfe9fad --- /dev/null +++ b/tests/qunit/data/testrunner.js @@ -0,0 +1,92 @@ +( function( $ ) { + +/** + * Add bogus to url to prevent IE crazy caching + * + * @param value {String} a relative path (eg. 'data/defineTestCallback.js' or 'data/test.php?foo=bar') + * @return {String} Such as 'data/defineTestCallback.js?131031765087663960' + */ +QUnit.fixurl = function(value) { + return value + (/\?/.test(value) ? "&" : "?") + new Date().getTime() + "" + parseInt(Math.random()*100000); +}; + +/** + * Load TestSwarm agent + */ +if ( QUnit.urlParams.swarmURL ) { + document.write("<scr" + "ipt src='" + QUnit.fixurl( 'data/testwarm.inject.js' ) + "'></scr" + "ipt>"); +} + +/** + * Load completenesstest + */ +if ( QUnit.urlParams.completenesstest ) { + + // Return true to ignore + var mwTestIgnore = function( val, tester, funcPath ) { + + // Don't record methods of the properties of constructors, + // to avoid getting into a loop (prototype.constructor.prototype..). + // Since we're therefor skipping any injection for + // "new mw.Foo()", manually set it to true here. + if ( val instanceof mw.Map ) { + tester.methodCallTracker['Map'] = true; + return true; + } + + // Don't record methods of the properties of a jQuery object + if ( val instanceof $ ) { + return true; + } + + return false; + }; + + var mwTester = new CompletenessTest( mw, mwTestIgnore ); +} + +/** + * Add-on assertion helpers + */ +// Define the add-ons +var addons = { + + // Expect boolean true + assertTrue: function( actual, message ) { + strictEqual( actual, true, message ); + }, + + // Expect boolean false + assertFalse: function( actual, message ) { + strictEqual( actual, false, message ); + }, + + // Expect numerical value less than X + lt: function( actual, expected, message ) { + QUnit.push( actual < expected, actual, 'less than ' + expected, message ); + }, + + // Expect numerical value less than or equal to X + ltOrEq: function( actual, expected, message ) { + QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message ); + }, + + // Expect numerical value greater than X + gt: function( actual, expected, message ) { + QUnit.push( actual > expected, actual, 'greater than ' + expected, message ); + }, + + // Expect numerical value greater than or equal to X + gtOrEq: function( actual, expected, message ) { + QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message ); + }, + + // Backwards compatible with new verions of QUnit + equals: window.equal, + same: window.deepEqual +}; + +$.extend( QUnit, addons ); +$.extend( window, addons ); + +})( jQuery ); diff --git a/tests/qunit/data/testwarm.inject.js b/tests/qunit/data/testwarm.inject.js new file mode 100644 index 00000000..14ee8f93 --- /dev/null +++ b/tests/qunit/data/testwarm.inject.js @@ -0,0 +1,349 @@ +/* + Copyright (c) 2009 John Resig + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ +(function(){ + + var DEBUG = false; + + var doPost = false; + + try { + doPost = !!window.top.postMessage; + } catch(e){} + + var search = window.location.search, + url, index; + if( ( index = search.indexOf( "swarmURL=" ) ) != -1 ) + url = decodeURIComponent( search.slice( index + 9 ) ); + + if ( !DEBUG && (!url || url.indexOf("http") !== 0) ) { + return; + } + + var submitTimeout = 5; + + var curHeartbeat; + var beatRate = 20; + + // Expose the TestSwarm API + window.TestSwarm = { + submit: submit, + heartbeat: function(){ + if ( curHeartbeat ) { + clearTimeout( curHeartbeat ); + } + + curHeartbeat = setTimeout(function(){ + submit({ fail: -1, total: -1 }); + }, beatRate * 1000); + }, + serialize: function(){ + return trimSerialize(); + } + }; + + // Prevent careless things from executing + window.print = window.confirm = window.alert = window.open = function(){}; + + window.onerror = function(e){ + document.body.appendChild( document.createTextNode( "ERROR: " + e )); + submit({ fail: 0, error: 1, total: 1 }); + return false; + }; + + // QUnit (jQuery) + // http://docs.jquery.com/QUnit + if ( typeof QUnit !== "undefined" ) { + QUnit.done = function(results){ + submit({ + fail: results.failed, + error: 0, + total: results.total + }); + }; + + QUnit.log = window.TestSwarm.heartbeat; + window.TestSwarm.heartbeat(); + + window.TestSwarm.serialize = function(){ + // Clean up the HTML (remove any un-needed test markup) + remove("nothiddendiv"); + remove("loadediframe"); + remove("dl"); + remove("main"); + + // Show any collapsed results + var ol = document.getElementsByTagName("ol"); + for ( var i = 0; i < ol.length; i++ ) { + ol[i].style.display = "block"; + } + + return trimSerialize(); + }; + + // UnitTestJS (Prototype, Scriptaculous) + // http://github.com/tobie/unittest_js/tree/master + } else if ( typeof Test !== "undefined" && Test && Test.Unit && Test.Unit.runners ) { + var total_runners = Test.Unit.runners.length, cur_runners = 0; + var total = 0, fail = 0, error = 0; + + for (var i = 0; i < Test.Unit.runners.length; i++) (function(i){ + var finish = Test.Unit.runners[i].finish; + Test.Unit.runners[i].finish = function(){ + finish.call( this ); + + var results = this.getResult(); + total += results.assertions; + fail += results.failures; + error += results.errors; + + if ( ++cur_runners === total_runners ) { + submit({ + fail: fail, + error: error, + total: total + }); + } + }; + })(i); + + // JSSpec (MooTools) + // http://jania.pe.kr/aw/moin.cgi/JSSpec + } else if ( typeof JSSpec !== "undefined" && JSSpec && JSSpec.Logger ) { + var onRunnerEnd = JSSpec.Logger.prototype.onRunnerEnd; + JSSpec.Logger.prototype.onRunnerEnd = function(){ + onRunnerEnd.call(this); + + // Show any collapsed results + var ul = document.getElementsByTagName("ul"); + for ( var i = 0; i < ul.length; i++ ) { + ul[i].style.display = "block"; + } + + submit({ + fail: JSSpec.runner.getTotalFailures(), + error: JSSpec.runner.getTotalErrors(), + total: JSSpec.runner.totalExamples + }); + }; + + window.TestSwarm.serialize = function(){ + // Show any collapsed results + var ul = document.getElementsByTagName("ul"); + for ( var i = 0; i < ul.length; i++ ) { + ul[i].style.display = "block"; + } + + return trimSerialize(); + }; + + // JSUnit + // http://www.jsunit.net/ + // Note: Injection file must be included before the frames + // are document.write()d into the page. + } else if ( typeof JsUnitTestManager !== "undefined" ) { + var _done = JsUnitTestManager.prototype._done; + JsUnitTestManager.prototype._done = function(){ + _done.call(this); + + submit({ + fail: this.failureCount, + error: this.errorCount, + total: this.totalCount + }); + }; + + window.TestSwarm.serialize = function(){ + return "<pre>" + this.log.join("\n") + "</pre>"; + }; + + // Selenium Core + // http://seleniumhq.org/projects/core/ + } else if ( typeof SeleniumTestResult !== "undefined" && typeof LOG !== "undefined" ) { + // Completely overwrite the postback + SeleniumTestResult.prototype.post = function(){ + submit({ + fail: this.metrics.numCommandFailures, + error: this.metrics.numCommandErrors, + total: this.metrics.numCommandPasses + this.metrics.numCommandFailures + this.metrics.numCommandErrors + }); + }; + + window.TestSwarm.serialize = function(){ + var results = []; + while ( LOG.pendingMessages.length ) { + var msg = LOG.pendingMessages.shift(); + results.push( msg.type + ": " + msg.msg ); + } + + return "<pre>" + results.join("\n") + "</pre>"; + }; + + // Dojo Objective Harness + // http://docs.dojocampus.org/quickstart/doh + } else if ( typeof doh !== "undefined" && doh._report ) { + var _report = doh._report; + doh._report = function(){ + _report.apply(this, arguments); + + submit({ + fail: doh._failureCount, + error: doh._errorCount, + total: doh._testCount + }); + }; + + window.TestSwarm.serialize = function(){ + return "<pre>" + document.getElementById("logBody").innerHTML + "</pre>"; + }; + // Screw.Unit + // git://github.com/nathansobo/screw-unit.git + } else if ( typeof Screw !== "undefined" && typeof jQuery !== 'undefined' && Screw && Screw.Unit ) { + $(Screw).bind("after", function() { + var passed = $('.passed').length; + var failed = $('.failed').length; + submit({ + fail: failed, + error: 0, + total: failed + passed + }); + }); + + $(Screw).bind("loaded", function() { + $('.it') + .bind("passed", window.TestSwarm.heartbeat) + .bind("failed", window.TestSwarm.heartbeat); + window.TestSwarm.heartbeat(); + }); + + window.TestSwarm.serialize = function(){ + return trimSerialize(); + }; + } + + function trimSerialize(doc) { + doc = doc || document; + + var scripts = doc.getElementsByTagName("script"); + while ( scripts.length ) { + remove( scripts[0] ); + } + + var root = window.location.href.replace(/(https?:\/\/.*?)\/.*/, "$1"); + var cur = window.location.href.replace(/[^\/]*$/, ""); + + var links = doc.getElementsByTagName("link"); + for ( var i = 0; i < links.length; i++ ) { + var href = links[i].href; + if ( href.indexOf("/") === 0 ) { + href = root + href; + } else if ( !/^https?:\/\//.test( href ) ) { + href = cur + href; + } + links[i].href = href; + } + + return ("<html>" + doc.documentElement.innerHTML + "</html>") + .replace(/\s+/g, " "); + } + + function remove(elem){ + if ( typeof elem === "string" ) { + elem = document.getElementById( elem ); + } + + if ( elem ) { + elem.parentNode.removeChild( elem ); + } + } + + function submit(params){ + if ( curHeartbeat ) { + clearTimeout( curHeartbeat ); + } + + var paramItems = (url.split("?")[1] || "").split("&"); + + for ( var i = 0; i < paramItems.length; i++ ) { + if ( paramItems[i] ) { + var parts = paramItems[i].split("="); + if ( !params[ parts[0] ] ) { + params[ parts[0] ] = parts[1]; + } + } + } + + if ( !params.state ) { + params.state = "saverun"; + } + + if ( !params.results ) { + params.results = window.TestSwarm.serialize(); + } + + if ( doPost ) { + // Build Query String + var query = ""; + + for ( var i in params ) { + query += ( query ? "&" : "" ) + i + "=" + + encodeURIComponent(params[i]); + } + + if ( DEBUG ) { + alert( query ); + } else { + window.top.postMessage( query, "*" ); + } + + } else { + var form = document.createElement("form"); + form.action = url; + form.method = "POST"; + + for ( var i in params ) { + var input = document.createElement("input"); + input.type = "hidden"; + input.name = i; + input.value = params[i]; + form.appendChild( input ); + } + + if ( DEBUG ) { + alert( form.innerHTML ); + } else { + + // Watch for the result submission timing out + setTimeout(function(){ + submit( params ); + }, submitTimeout * 1000); + + document.body.appendChild( form ); + form.submit(); + } + } + } + +})(); diff --git a/tests/qunit/index.html b/tests/qunit/index.html new file mode 100644 index 00000000..d4bbe7e2 --- /dev/null +++ b/tests/qunit/index.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<html> +<head> + <title>MediaWiki JavaScript Test Suite</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <!-- MediaWiki Modules --> + + <!-- MW: startup --> + <script> + function startUp(){ + mw.config = new mw.Map( false ); + } + </script> + + <!-- MW: jquery|mediawiki --> + <script src="../../resources/jquery/jquery.js"></script> + <script src="../../resources/mediawiki/mediawiki.js"></script> + + <!-- MW: mediawiki.page.startup --> + <script src="../../resources/jquery/jquery.client.js"></script> + <script src="../../resources/mediawiki.page/mediawiki.page.startup.js"></script> + + <!-- MW: mediawiki.user|mediawiki.util|mediawiki.page.ready --> + <script src="../../resources/jquery/jquery.cookie.js"></script> + <script src="../../resources/mediawiki/mediawiki.user.js"></script> + + <script src="../../resources/jquery/jquery.messageBox.js"></script> + <script src="../../resources/jquery/jquery.mwPrototypes.js"></script> + <script src="../../resources/mediawiki/mediawiki.util.js"></script> + + <script src="../../resources/jquery/jquery.checkboxShiftClick.js"></script> + <script src="../../resources/jquery/jquery.makeCollapsible.js"></script> + <script src="../../resources/jquery/jquery.placeholder.js"></script> + <script src="../../resources/mediawiki.page/mediawiki.page.ready.js"></script> + + <!-- MW: user.options --> + <script> + mw.user.options.set({"skin": "vector"}); + </script> + + <!-- MW: Non-default modules --> + <script src="../../resources/jquery/jquery.autoEllipsis.js"></script> + <script src="../../resources/jquery/jquery.byteLength.js"></script> + <script src="../../resources/jquery/jquery.byteLimit.js"></script> + <script src="../../resources/jquery/jquery.colorUtil.js"></script> + <script src="../../resources/jquery/jquery.getAttrs.js"></script> + <script src="../../resources/jquery/jquery.localize.js"></script> + <script src="../../resources/jquery/jquery.tabIndex.js"></script> + <script src="../../resources/jquery/jquery.tablesorter.js"></script> + <script src="../../resources/mediawiki.special/mediawiki.special.js"></script> + <script src="../../resources/mediawiki.special/mediawiki.special.recentchanges.js"></script> + + <!-- QUnit: Load framework --> + <link rel="stylesheet" href="../../resources/jquery/jquery.qunit.css" /> + <script src="../../resources/jquery/jquery.qunit.js"></script> + <script src="../../resources/jquery/jquery.qunit.completenessTest.js"></script> + <script src="data/testrunner.js"></script> + + <!-- QUnit: Load test suites (maintain the same order as above please) --> + <script src="suites/resources/mediawiki/mediawiki.jscompat.test.js"></script> + <script src="suites/resources/mediawiki/mediawiki.js"></script> + <script src="suites/resources/mediawiki/mediawiki.user.js"></script> + + <script src="suites/resources/jquery/jquery.client.js"></script> + <script src="suites/resources/jquery/jquery.mwPrototypes.js"></script> + <script src="suites/resources/mediawiki/mediawiki.util.js"></script> + + <script src="suites/resources/jquery/jquery.autoEllipsis.js"></script> + <script src="suites/resources/jquery/jquery.byteLength.js"></script> + <script src="suites/resources/jquery/jquery.byteLimit.js"></script> + <script src="suites/resources/jquery/jquery.colorUtil.js"></script> + <script src="suites/resources/jquery/jquery.getAttrs.js"></script> + <script src="suites/resources/jquery/jquery.localize.js"></script> + <script src="suites/resources/jquery/jquery.tabIndex.js"></script> + <script src="suites/resources/jquery/jquery.tablesorter.test.js" charset="UTF-8"></script> + <script src="suites/resources/mediawiki.special/mediawiki.special.recentchanges.js"></script> +</head> +<body> + <h1 id="qunit-header">MediaWiki JavaScript Test Suite</h1> + <h2 id="qunit-banner"></h2> + <div id="qunit-testrunner-toolbar"> + <p><a href="http://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing">See testing documentation on mediawiki.org</a></p> + </div> + <h2 id="qunit-userAgent"></h2> + <ol id="qunit-tests"></ol> + +<!-- Scripts inserting stuff here shall remove it themselfs! --> +<div id="content"></div> +</body> +</html> diff --git a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.js b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.js new file mode 100644 index 00000000..caf5a6f1 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.js @@ -0,0 +1,58 @@ +module( 'jquery.autoEllipsis.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.fn.autoEllipsis, 'jQuery.fn.autoEllipsis defined' ); +}); + +function createWrappedDiv( text, width ) { + var $wrapper = $( '<div />' ).css( 'width', width ); + var $div = $( '<div />' ).text( text ); + $wrapper.append( $div ); + return $wrapper; +} + +function findDivergenceIndex( a, b ) { + var i = 0; + while ( i < a.length && i < b.length && a[i] == b[i] ) { + i++; + } + return i; +} + +test( 'Position right', function() { + expect(4); + + // We need this thing to be visible, so append it to the DOM + var origText = 'This is a really long random string and there is no way it fits in 100 pixels.'; + var $wrapper = createWrappedDiv( origText, '100px' ); + $( 'body' ).append( $wrapper ); + $wrapper.autoEllipsis( { position: 'right' } ); + + // Verify that, and only one, span element was created + var $span = $wrapper.find( '> span' ); + strictEqual( $span.length, 1, 'autoEllipsis wrapped the contents in a span element' ); + + // Check that the text fits by turning on word wrapping + $span.css( 'whiteSpace', 'nowrap' ); + ltOrEq( $span.width(), $span.parent().width(), "Text fits (making the span 'white-space:nowrap' does not make it wider than its parent)" ); + + // Add two characters using scary black magic + var spanText = $span.text(); + var d = findDivergenceIndex( origText, spanText ); + var spanTextNew = spanText.substr( 0, d ) + origText[d] + origText[d] + '...'; + + gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' ); + + // Put this text in the span and verify it doesn't fit + $span.text( spanTextNew ); + // In IE6 width works like min-width, allow IE6's width to be "equal to" + if ( $.browser.msie && Number( $.browser.version ) == 6 ) { + gtOrEq( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more) - IE6: Maybe equal to as well due to width behaving like min-width in IE6' ); + } else { + gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); + } + + // Clean up + $wrapper.remove(); +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.js new file mode 100644 index 00000000..f82fda27 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.byteLength.js @@ -0,0 +1,42 @@ +module( 'jquery.byteLength.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.byteLength, 'jQuery.byteLength defined' ); +} ); + +test( 'Simple text', function() { + expect(5); + + var azLc = 'abcdefghijklmnopqrstuvwxyz', + azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + num = '0123456789', + x = '*', + space = ' '; + + equal( $.byteLength( azLc ), 26, 'Lowercase a-z' ); + equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' ); + equal( $.byteLength( num ), 10, 'Numbers 0-9' ); + equal( $.byteLength( x ), 1, 'An asterisk' ); + equal( $.byteLength( space ), 3, '3 spaces' ); + +} ); + +test( 'Special text', window.foo = function() { + expect(5); + + // http://en.wikipedia.org/wiki/UTF-8 + var U_0024 = '\u0024', + U_00A2 = '\u00A2', + U_20AC = '\u20AC', + U_024B62 = '\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 + U_024B62_alt = '\uD852\uDF62'; + + strictEqual( $.byteLength( U_0024 ), 1, 'U+0024: 1 byte. \u0024 (dollar sign)' ); + strictEqual( $.byteLength( U_00A2 ), 2, 'U+00A2: 2 bytes. \u00A2 (cent sign)' ); + strictEqual( $.byteLength( U_20AC ), 3, 'U+20AC: 3 bytes. \u20AC (euro sign)' ); + strictEqual( $.byteLength( U_024B62 ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character)' ); + strictEqual( $.byteLength( U_024B62_alt ), 4, 'U+024B62: 4 bytes. \uD852\uDF62 (a Han character) - alternative method' ); +} ); diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.js new file mode 100644 index 00000000..461ea49b --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.byteLimit.js @@ -0,0 +1,155 @@ +module( 'jquery.byteLimit.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.fn.byteLimit, 'jQuery.fn.byteLimit defined' ); +} ); + +// Basic sendkey-implementation +$.addChars = function( $input, charstr ) { + var len = charstr.length; + for ( var i = 0; i < len; i++ ) { + // Keep track of the previous value + var prevVal = $input.val(); + + // Get the key code + var code = charstr.charCodeAt(i); + + // Trigger event and undo if prevented + var event = new jQuery.Event( 'keypress', { keyCode: code, which: code, charCode: code } ); + $input.trigger( event ); + if ( !event.isDefaultPrevented() ) { + $input.val( prevVal + charstr.charAt(i) ); + } + } +}; +var blti = 0; +/** + * Test factory for $.fn.byteLimit + * + * @param $input {jQuery} jQuery object in an input element + * @param useLimit {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) + */ +var byteLimitTest = function( options ) { + var opt = $.extend({ + description: '', + $input: null, + sample: '', + useLimit: false, + expected: 0, + limit: null + }, options); + var i = blti++; + + test( opt.description, function() { + + opt.$input.appendTo( 'body' ); + + // Simulate pressing keys for each of the sample characters + $.addChars( opt.$input, opt.sample ); + var newVal = opt.$input.val(); + + if ( opt.useLimit ) { + expect(2); + + ltOrEq( $.byteLength( newVal ), opt.limit, 'Prevent keypresses after byteLimit was reached, length never exceeded the limit' ); + equal( $.byteLength( newVal ), opt.expected, 'Not preventing keypresses too early, length has reached the expected length' ); + + } else { + expect(1); + equal( $.byteLength( newVal ), opt.expected, 'Unlimited scenarios are not affected, expected length reached' ); + } + + opt.$input.remove(); + } ); +}; + +var + // Simple sample (20 chars, 20 bytes) + simpleSample = '12345678901234567890', + + // 3 bytes (euro-symbol) + U_20AC = '\u20AC', + + // Multi-byte sample (22 chars, 26 bytes) + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; + +byteLimitTest({ + description: 'Plain text input', + $input: $( '<input>' ) + .attr( { + 'type': 'text' + }), + sample: simpleSample, + useLimit: false, + expected: $.byteLength( simpleSample ) +}); + +byteLimitTest({ + description: 'Limit using the maxlength attribute', + $input: $( '<input>' ) + .attr( { + 'type': 'text', + 'maxlength': '10' + }) + .byteLimit(), + sample: simpleSample, + useLimit: true, + limit: 10, + expected: 10 +}); + +byteLimitTest({ + description: 'Limit using a custom value', + $input: $( '<input>' ) + .attr( { + 'type': 'text' + }) + .byteLimit( 10 ), + sample: simpleSample, + useLimit: true, + limit: 10, + expected: 10 +}); + +byteLimitTest({ + description: 'Limit using a custom value, overriding maxlength attribute', + $input: $( '<input>' ) + .attr( { + 'type': 'text', + 'maxLength': '10' + }) + .byteLimit( 15 ), + sample: simpleSample, + useLimit: true, + limit: 15, + expected: 15 +}); + +byteLimitTest({ + description: 'Limit using a custom value (multibyte)', + $input: $( '<input>' ) + .attr( { + 'type': 'text' + }) + .byteLimit( 14 ), + sample: mbSample, + useLimit: true, + limit: 14, + expected: 14 // (10 x 1-byte char) + (1 x 3-byte char) + (1 x 1-byte char) +}); + +byteLimitTest({ + description: 'Limit using a custom value (multibyte) overlapping a byte', + $input: $( '<input>' ) + .attr( { + 'type': 'text' + }) + .byteLimit( 12 ), + sample: mbSample, + useLimit: true, + limit: 12, + expected: 12 // 10 x 1-byte char. The next 3-byte char exceeds limit of 12, but 2 more 1-byte chars come in after. +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.client.js b/tests/qunit/suites/resources/jquery/jquery.client.js new file mode 100644 index 00000000..50df2928 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.client.js @@ -0,0 +1,205 @@ +module( 'jquery.client.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( jQuery.client, 'jQuery.client defined' ); +}); + +test( 'profile userAgent support', function() { + expect(8); + + // 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 = { + // Internet Explorer 6 + // Internet Explorer 7 + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)': { + title: 'Internet Explorer 7', + platform: 'Win32', + profile: { + "name": "msie", + "layout": "trident", + "layoutVersion": "unknown", + "platform": "win", + "version": "7.0", + "versionBase": "7", + "versionNumber": 7 + } + }, + // Internet Explorer 8 + // Internet Explorer 9 + // Internet Explorer 10 + // 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': { + title: 'Firefox 3.5', + platform: 'MacIntel', + profile: { + "name": "firefox", + "layout": "gecko", + "layoutVersion": 20110420, + "platform": "mac", + "version": "3.5.19", + "versionBase": "3", + "versionNumber": 3.5 + } + }, + // Firefox 3.6 + 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17': { + title: 'Firefox 3.6', + platform: 'Linux i686', + profile: { + "name": "firefox", + "layout": "gecko", + "layoutVersion": 20110422, + "platform": "linux", + "version": "3.6.17", + "versionBase": "3", + "versionNumber": 3.6 + } + }, + // Firefox 4 + 'Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1': { + title: 'Firefox 4', + platform: 'Win32', + profile: { + "name": "firefox", + "layout": "gecko", + "layoutVersion": 20100101, + "platform": "win", + "version": "4.0.1", + "versionBase": "4", + "versionNumber": 4 + } + }, + // Firefox 5 + // Safari 3 + // Safari 4 + 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7': { + title: 'Safari 4', + platform: 'MacIntel', + profile: { + "name": "safari", + "layout": "webkit", + "layoutVersion": 531, + "platform": "mac", + "version": "4.0.5", + "versionBase": "4", + "versionNumber": 4 + } + }, + 'Mozilla/5.0 (Windows; U; Windows NT 6.0; cs-CZ) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7': { + title: 'Safari 4', + platform: 'Win32', + profile: { + "name": "safari", + "layout": "webkit", + "layoutVersion": 533, + "platform": "win", + "version": "4.0.5", + "versionBase": "4", + "versionNumber": 4 + } + }, + // Safari 5 + // Opera 10 + // Chrome 5 + // Chrome 6 + // Chrome 7 + // Chrome 8 + // Chrome 9 + // Chrome 10 + // Chrome 11 + // Chrome 12 + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30': { + title: 'Chrome 12', + platform: 'MacIntel', + profile: { + "name": "chrome", + "layout": "webkit", + "layoutVersion": 534, + "platform": "mac", + "version": "12.0.742.112", + "versionBase": "12", + "versionNumber": 12 + } + }, + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30': { + title: 'Chrome 12', + platform: 'Linux i686', + profile: { + "name": "chrome", + "layout": "webkit", + "layoutVersion": 534, + "platform": "linux", + "version": "12.0.742.68", + "versionBase": "12", + "versionNumber": 12 + } + } + }; + + // Generate a client profile object and compare recursively + var uaTest = function( rawUserAgent, data ) { + var ret = $.client.profile( { + userAgent: rawUserAgent, + platform: data.platform + } ); + deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent ); + }; + + // Loop through and run tests + $.each( uas, uaTest ); +} ); + +test( 'profile return validation for current user agent', function() { + expect(7); + var p = $.client.profile(); + var unknownOrType = function( val, type, summary ) { + return ok( typeof val === type || val === 'unknown', summary ); + }; + + equal( typeof p, 'object', 'profile returns an object' ); + unknownOrType( p.layout, 'string', 'p.layout is a string (or "unknown")' ); + unknownOrType( p.layoutVersion, 'number', 'p.layoutVersion is a number (or "unknown")' ); + unknownOrType( p.platform, 'string', 'p.platform is a string (or "unknown")' ); + unknownOrType( p.version, 'string', 'p.version is a string (or "unknown")' ); + unknownOrType( p.versionBase, 'string', 'p.versionBase is a string (or "unknown")' ); + equal( typeof p.versionNumber, 'number', 'p.versionNumber is a number' ); +}); + +test( 'test', function() { + expect(1); + + // Example from WikiEditor + var testMap = { + 'ltr': { + 'msie': [['>=', 7]], + '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 + } + }; + // .test() uses eval, make sure no exceptions are thrown + // then do a basic return value type check + var testMatch = $.client.test( testMap ); + + equal( typeof testMatch, 'boolean', 'test returns a boolean value' ); + +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.colorUtil.js b/tests/qunit/suites/resources/jquery/jquery.colorUtil.js new file mode 100644 index 00000000..93f12b82 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.colorUtil.js @@ -0,0 +1,71 @@ +module( 'jquery.colorUtil.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.colorUtil, '$.colorUtil defined' ); +}); + +test( 'getRGB', function() { + expect(18); + + strictEqual( $.colorUtil.getRGB(), undefined, 'No arguments' ); + strictEqual( $.colorUtil.getRGB( '' ), undefined, 'Empty string' ); + deepEqual( $.colorUtil.getRGB( [0, 100, 255] ), [0, 100, 255], 'Parse array of rgb values' ); + deepEqual( $.colorUtil.getRGB( 'rgb(0,100,255)' ), [0, 100, 255], 'Parse simple rgb string' ); + deepEqual( $.colorUtil.getRGB( 'rgb(0, 100, 255)' ), [0, 100, 255], 'Parse simple rgb string with spaces' ); + deepEqual( $.colorUtil.getRGB( 'rgb(0%,20%,40%)' ), [0, 51, 102], 'Parse rgb string with percentages' ); + deepEqual( $.colorUtil.getRGB( 'rgb(0%, 20%, 40%)' ), [0, 51, 102], 'Parse rgb string with percentages and spaces' ); + deepEqual( $.colorUtil.getRGB( '#f2ddee' ), [242, 221, 238], 'Hex string: 6 char lowercase' ); + deepEqual( $.colorUtil.getRGB( '#f2DDEE' ), [242, 221, 238], 'Hex string: 6 char uppercase' ); + deepEqual( $.colorUtil.getRGB( '#f2DdEe' ), [242, 221, 238], 'Hex string: 6 char mixed' ); + deepEqual( $.colorUtil.getRGB( '#eee' ), [238, 238, 238], 'Hex string: 3 char lowercase' ); + deepEqual( $.colorUtil.getRGB( '#EEE' ), [238, 238, 238], 'Hex string: 3 char uppercase' ); + deepEqual( $.colorUtil.getRGB( '#eEe' ), [238, 238, 238], 'Hex string: 3 char mixed' ); + deepEqual( $.colorUtil.getRGB( 'rgba(0, 0, 0, 0)' ), [255, 255, 255], 'Zero rgba for Safari 3; Transparent (whitespace)' ); + + // Perhaps this is a bug in colorUtil, but it is the current behaviour so, let's keep + // track of it, so we will know in case it would ever change. + strictEqual( $.colorUtil.getRGB( 'rgba(0,0,0,0)' ), undefined, 'Zero rgba without whitespace' ); + + deepEqual( $.colorUtil.getRGB( 'lightGreen' ), [144, 238, 144], 'Color names (lightGreen)' ); + deepEqual( $.colorUtil.getRGB( 'transparent' ), [255, 255, 255], 'Color names (transparent)' ); + strictEqual( $.colorUtil.getRGB( 'mediaWiki' ), undefined, 'Inexisting color name' ); +}); + +test( 'rgbToHsl', function() { + expect(1); + + var hsl = $.colorUtil.rgbToHsl( 144, 238, 144 ); + + // Cross-browser differences in decimals... + // Round to two decimals so they can be more reliably checked. + var dualDecimals = function(a,b){ + return Math.round(a*100)/100; + }; + // Re-create the rgbToHsl return array items, limited to two decimals. + var ret = [dualDecimals(hsl[0]), dualDecimals(hsl[1]), dualDecimals(hsl[2])]; + + deepEqual( ret, [0.33, 0.73, 0.75], 'rgb(144, 238, 144): hsl(0.33, 0.73, 0.75)' ); +}); + +test( 'hslToRgb', function() { + expect(1); + + var rgb = $.colorUtil.hslToRgb( 0.3, 0.7, 0.8 ); + + // Cross-browser differences in decimals... + // Re-create the hslToRgb return array items, rounded to whole numbers. + var ret = [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]; + + deepEqual( ret ,[183, 240, 168], 'hsl(0.3, 0.7, 0.8): rgb(183, 240, 168)' ); +}); + +test( 'getColorBrightness', function() { + expect(2); + + var a = $.colorUtil.getColorBrightness( 'red', +0.1 ); + equal( a, 'rgb(255,50,50)', 'Start with named color "red", brighten 10%' ); + + var b = $.colorUtil.getColorBrightness( 'rgb(200,50,50)', -0.2 ); + equal( b, 'rgb(118,29,29)', 'Start with rgb string "rgb(200,50,50)", darken 20%' ); +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.getAttrs.js b/tests/qunit/suites/resources/jquery/jquery.getAttrs.js new file mode 100644 index 00000000..3d3d01e1 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.getAttrs.js @@ -0,0 +1,17 @@ +module( 'jquery.getAttrs.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.fn.getAttrs, 'jQuery.fn.getAttrs defined' ); +} ); + +test( 'Check', function() { + expect(1); + var attrs = { + foo: 'bar', + 'class': 'lorem' + }, + $el = $( '<div>', attrs ); + + deepEqual( $el.getAttrs(), attrs, 'getAttrs() return object should match the attributes set, no more, no less' ); +} ); diff --git a/tests/qunit/suites/resources/jquery/jquery.localize.js b/tests/qunit/suites/resources/jquery/jquery.localize.js new file mode 100644 index 00000000..40b58687 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.localize.js @@ -0,0 +1,119 @@ +module( 'jquery.localize.js' ); + +test( '-- Initial check', function() { + expect(1); + ok( $.fn.localize, 'jQuery.fn.localize defined' ); +} ); + +test( 'Handle basic replacements', function() { + expect(3); + + var html, $lc; + mw.messages.set( 'basic', 'Basic stuff' ); + + // Tag: html:msg + html = '<div><span><html:msg key="basic" /></span></div>'; + $lc = $( html ).localize().find( 'span' ); + + strictEqual( $lc.text(), 'Basic stuff', 'Tag: html:msg' ); + + // Attribute: title-msg + html = '<div><span title-msg="basic" /></span></div>'; + $lc = $( html ).localize().find( 'span' ); + + strictEqual( $lc.attr( 'title' ), 'Basic stuff', 'Attribute: title-msg' ); + + // Attribute: alt-msg + html = '<div><span alt-msg="basic" /></span></div>'; + $lc = $( html ).localize().find( 'span' ); + + strictEqual( $lc.attr( 'alt' ), 'Basic stuff', 'Attribute: alt-msg' ); +} ); + +test( 'Proper escaping', function() { + expect(2); + + var html, $lc; + mw.messages.set( 'properfoo', '<proper esc="test">' ); + + // This is handled by jQuery inside $.fn.localize, just a simple sanity checked + // 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>'; + $lc = $( html ).localize().find( 'span' ); + + strictEqual( $lc.text(), mw.msg( 'properfoo' ), 'Content is inserted as text, not as html.' ); + + // Attribute escaping + html = '<div><span title-msg="properfoo" /></span></div>'; + $lc = $( html ).localize().find( 'span' ); + + strictEqual( $lc.attr( 'title' ), mw.msg( 'properfoo' ), 'Attributes are not inserted raw.' ); +} ); + +test( 'Options', function() { + expect(7); + + mw.messages.set( { + 'foo-lorem': 'Lorem', + 'foo-ipsum': 'Ipsum', + 'foo-bar-title': 'Read more about bars', + 'foo-bar-label': 'The Bars', + 'foo-bazz-title': 'Read more about bazz at $1 (last modified: $2)', + 'foo-bazz-label': 'The Bazz ($1)', + 'foo-welcome': 'Welcome to $1! (last visit: $2)' + } ); + var html, $lc, attrs, x, sitename = 'Wikipedia'; + + // Message key prefix + html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>'; + $lc = $( html ).localize( { + prefix: 'foo-' + } ).find( 'span' ); + + strictEqual( $lc.attr( 'title' ), 'Lorem', 'Message key prefix - attr' ); + strictEqual( $lc.text(), 'Ipsum', 'Message key prefix - text' ); + + // Variable keys mapping + x = 'bar'; + html = '<div><span title-msg="title"><html:msg key="label" /></span></div>'; + $lc = $( html ).localize( { + keys: { + 'title': 'foo-' + x + '-title', + 'label': 'foo-' + x + '-label' + } + } ).find( 'span' ); + + strictEqual( $lc.attr( 'title' ), 'Read more about bars', 'Variable keys mapping - attr' ); + strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' ); + + // Passing parameteters to mw.msg + html = '<div><span><html:msg key="foo-welcome" /></span></div>'; + $lc = $( html ).localize( { + params: { + 'foo-welcome': [sitename, 'yesterday'] + } + } ).find( 'span' ); + + strictEqual( $lc.text(), 'Welcome to Wikipedia! (last visit: yesterday)', 'Passing parameteters to mw.msg' ); + + // Combination of options prefix, params and keys + x = 'bazz'; + html = '<div><span title-msg="title"><html:msg key="label" /></span></div>'; + $lc = $( html ).localize( { + prefix: 'foo-', + keys: { + 'title': x + '-title', + 'label': x + '-label' + }, + params: { + 'title': [sitename, '3 minutes ago'], + 'label': [sitename, '3 minutes ago'] + + } + } ).find( 'span' ); + + strictEqual( $lc.text(), 'The Bazz (Wikipedia)', 'Combination of options prefix, params and keys - text' ); + strictEqual( $lc.attr( 'title' ), 'Read more about bazz at Wikipedia (last modified: 3 minutes ago)', 'Combination of options prefix, params and keys - attr' ); +} ); diff --git a/tests/qunit/suites/resources/jquery/jquery.mwPrototypes.js b/tests/qunit/suites/resources/jquery/jquery.mwPrototypes.js new file mode 100644 index 00000000..bb6d2a1b --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.mwPrototypes.js @@ -0,0 +1,56 @@ +module( 'jquery.mwPrototypes.js' ); + +test( 'String functions', function() { + + equal( $.trimLeft( ' foo bar ' ), 'foo bar ', 'trimLeft' ); + equal( $.trimRight( ' foo bar ' ), ' foo bar', 'trimRight' ); + equal( $.ucFirst( 'foo'), 'Foo', 'ucFirst' ); + + equal( $.escapeRE( '<!-- ([{+mW+}]) $^|?>' ), + '<!\\-\\- \\(\\[\\{\\+mW\\+\\}\\]\\) \\$\\^\\|\\?>', 'escapeRE - Escape specials' ); + equal( $.escapeRE( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ), + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'escapeRE - Leave uppercase alone' ); + equal( $.escapeRE( 'abcdefghijklmnopqrstuvwxyz' ), + 'abcdefghijklmnopqrstuvwxyz', 'escapeRE - Leave lowercase alone' ); + equal( $.escapeRE( '0123456789' ), '0123456789', 'escapeRE - Leave numbers alone' ); +}); + +test( 'Is functions', function() { + + strictEqual( $.isDomElement( document.getElementById( 'qunit-header' ) ), true, + 'isDomElement: #qunit-header Node' ); + strictEqual( $.isDomElement( document.getElementById( 'random-name' ) ), false, + 'isDomElement: #random-name (null)' ); + strictEqual( $.isDomElement( document.getElementsByTagName( 'div' ) ), false, + 'isDomElement: getElementsByTagName Array' ); + strictEqual( $.isDomElement( document.getElementsByTagName( 'div' )[0] ), true, + 'isDomElement: getElementsByTagName(..)[0] Node' ); + strictEqual( $.isDomElement( $( 'div' ) ), false, + 'isDomElement: jQuery object' ); + strictEqual( $.isDomElement( $( 'div' ).get(0) ), true, + 'isDomElement: jQuery object > Get node' ); + strictEqual( $.isDomElement( document.createElement( 'div' ) ), true, + 'isDomElement: createElement' ); + strictEqual( $.isDomElement( { foo: 1 } ), false, + 'isDomElement: Object' ); + + strictEqual( $.isEmpty( 'string' ), false, 'isEmptry: "string"' ); + strictEqual( $.isEmpty( '0' ), true, 'isEmptry: "0"' ); + strictEqual( $.isEmpty( [] ), true, 'isEmptry: []' ); + strictEqual( $.isEmpty( {} ), true, 'isEmptry: {}' ); + + // Documented behaviour + strictEqual( $.isEmpty( { length: 0 } ), true, 'isEmptry: { length: 0 }' ); +}); + +test( 'Comparison functions', function() { + + ok( $.compareArray( [0, 'a', [], [2, 'b'] ], [0, "a", [], [2, "b"] ] ), + 'compareArray: Two deep arrays that are excactly the same' ); + ok( !$.compareArray( [1], [2] ), 'compareArray: Two different arrays (false)' ); + + ok( $.compareObject( {}, {} ), 'compareObject: Two empty objects' ); + ok( $.compareObject( { foo: 1 }, { foo: 1 } ), 'compareObject: Two the same objects' ); + ok( !$.compareObject( { bar: true }, { baz: false } ), + 'compareObject: Two different objects (false)' ); +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.tabIndex.js b/tests/qunit/suites/resources/jquery/jquery.tabIndex.js new file mode 100644 index 00000000..1ff81e58 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.tabIndex.js @@ -0,0 +1,50 @@ +module( 'jquery.tabIndex.js' ); + +test( '-- Initial check', function() { + expect(2); + + ok( $.fn.firstTabIndex, '$.fn.firstTabIndex defined' ); + ok( $.fn.lastTabIndex, '$.fn.lastTabIndex defined' ); +}); + +test( 'firstTabIndex', function() { + expect(2); + + var testEnvironment = +'<form>' + + '<input tabindex="7" />' + + '<input tabindex="9" />' + + '<textarea tabindex="2">Foobar</textarea>' + + '<textarea tabindex="5">Foobar</textarea>' + +'</form>'; + + var $testA = $( '<div>' ).html( testEnvironment ).appendTo( 'body' ); + strictEqual( $testA.firstTabIndex(), 2, 'First tabindex should be 2 within this context.' ); + + var $testB = $( '<div>' ); + strictEqual( $testB.firstTabIndex(), null, 'Return null if none available.' ); + + // Clean up + $testA.add( $testB ).remove(); +}); + +test( 'lastTabIndex', function() { + expect(2); + + var testEnvironment = +'<form>' + + '<input tabindex="7" />' + + '<input tabindex="9" />' + + '<textarea tabindex="2">Foobar</textarea>' + + '<textarea tabindex="5">Foobar</textarea>' + +'</form>'; + + var $testA = $( '<div>' ).html( testEnvironment ).appendTo( 'body' ); + strictEqual( $testA.lastTabIndex(), 9, 'Last tabindex should be 9 within this context.' ); + + var $testB = $( '<div>' ); + strictEqual( $testB.lastTabIndex(), null, 'Return null if none available.' ); + + // Clean up + $testA.add( $testB ).remove(); +}); diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js new file mode 100644 index 00000000..f47b7f40 --- /dev/null +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -0,0 +1,475 @@ +(function() { + +module( 'jquery.tablesorter.test.js' ); + +// setup hack +mw.config.set('wgMonthNames', window.wgMonthNames = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']); +mw.config.set('wgMonthNamesShort', window.wgMonthNamesShort = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']); +mw.config.set('wgDefaultDateFormat', window.wgDefaultDateFormat = 'dmy'); + +test( '-- Initial check', function() { + expect(1); + ok( $.tablesorter, '$.tablesorter defined' ); +}); + +/** + * Create an HTML table from an array of row arrays containing text strings. + * First row will be header row. No fancy rowspan/colspan stuff. + * + * @param {String[]} header + * @param {String[][]} data + * @return jQuery + */ +var tableCreate = function( header, data ) { + var $table = $('<table class="sortable"><thead></thead><tbody></tbody></table>'), + $thead = $table.find('thead'), + $tbody = $table.find('tbody'); + var $tr = $('<tr>'); + $.each(header, function(i, str) { + var $th = $('<th>'); + $th.text(str).appendTo($tr); + }); + $tr.appendTo($thead); + + for (var i = 0; i < data.length; i++) { + $tr = $('<tr>'); + $.each(data[i], function(j, str) { + var $td = $('<td>'); + $td.text(str).appendTo($tr); + }); + $tr.appendTo($tbody); + } + return $table; +}; + +/** + * Extract text from table. + * + * @param {jQuery} $table + * @return String[][] + */ +var tableExtract = function( $table ) { + var data = []; + $table.find('tbody').find('tr').each(function(i, tr) { + var row = []; + $(tr).find('td,th').each(function(i, td) { + row.push($(td).text()); + }); + data.push(row); + }); + return data; +}; + +/** + * Run a table test by building a table with the given data, + * running some callback on it, then checking the results. + * + * @param {String} msg text to pass on to qunit for the comparison + * @param {String[]} header cols to make the table + * @param {String[][]} data rows/cols to make the table + * @param {String[][]} expected rows/cols to compare against at end + * @param {function($table)} callback something to do with the table before we compare + */ +var tableTest = function( msg, header, data, expected, callback ) { + test( msg, function() { + expect(1); + + var $table = tableCreate( header, data ); + //$('body').append($table); + + // Give caller a chance to set up sorting and manipulate the table. + callback( $table ); + + // Table sorting is done synchronously; if it ever needs to change back + // to asynchronous, we'll need a timeout or a callback here. + var extracted = tableExtract( $table ); + deepEqual( extracted, expected, msg ); + }); +}; + +var reversed = function(arr) { + var arr2 = arr.slice(0); + arr2.reverse(); + return arr2; +}; + +// Sample data set: some planets! +var header = ['Planet', 'Radius (km)'], + mercury = ['Mercury', '2439.7'], + venus = ['Venus', '6051.8'], + earth = ['Earth', '6371.0'], + mars = ['Mars', '3390.0'], + jupiter = ['Jupiter', '69911'], + saturn = ['Saturn', '58232']; + +// Initial data set +var planets = [mercury, venus, earth, mars, jupiter, saturn]; +var ascendingName = [earth, jupiter, mars, mercury, saturn, venus]; +var ascendingRadius = [mercury, mars, venus, earth, saturn, jupiter]; + +tableTest( + 'Basic planet table: ascending by name', + header, + planets, + ascendingName, + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); +tableTest( + 'Basic planet table: ascending by name a second time', + header, + planets, + ascendingName, + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); +tableTest( + 'Basic planet table: descending by name', + header, + planets, + reversed(ascendingName), + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click().click(); + } +); +tableTest( + 'Basic planet table: ascending radius', + header, + planets, + ascendingRadius, + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(1)').click(); + } +); +tableTest( + 'Basic planet table: descending radius', + header, + planets, + reversed(ascendingRadius), + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(1)').click().click(); + } +); + + +// Regression tests! +tableTest( + 'Bug 28775: German-style short numeric dates', + ['Date'], + [ + // German-style dates are day-month-year + ['11.11.2011'], + ['01.11.2011'], + ['02.10.2011'], + ['03.08.2011'], + ['09.11.2011'] + ], + [ + // Sorted by ascending date + ['03.08.2011'], + ['02.10.2011'], + ['01.11.2011'], + ['09.11.2011'], + ['11.11.2011'] + ], + function( $table ) { + // @fixme reset it at end or change module to allow us to override it + mw.config.set('wgDefaultDateFormat', window.wgDefaultDateFormat = 'dmy'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); +tableTest( + 'Bug 28775: American-style short numeric dates', + ['Date'], + [ + // American-style dates are month-day-year + ['11.11.2011'], + ['01.11.2011'], + ['02.10.2011'], + ['03.08.2011'], + ['09.11.2011'] + ], + [ + // Sorted by ascending date + ['01.11.2011'], + ['02.10.2011'], + ['03.08.2011'], + ['09.11.2011'], + ['11.11.2011'] + ], + function( $table ) { + // @fixme reset it at end or change module to allow us to override it + mw.config.set('wgDefaultDateFormat', window.wgDefaultDateFormat = 'mdy'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); + +var ipv4 = [ + // Some randomly generated fake IPs + ['45.238.27.109'], + ['44.172.9.22'], + ['247.240.82.209'], + ['204.204.132.158'], + ['170.38.91.162'], + ['197.219.164.9'], + ['45.68.154.72'], + ['182.195.149.80'] +]; +var ipv4Sorted = [ + // Sort order should go octet by octet + ['44.172.9.22'], + ['45.68.154.72'], + ['45.238.27.109'], + ['170.38.91.162'], + ['182.195.149.80'], + ['197.219.164.9'], + ['204.204.132.158'], + ['247.240.82.209'] +]; +tableTest( + 'Bug 17141: IPv4 address sorting', + ['IP'], + ipv4, + ipv4Sorted, + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); +tableTest( + 'Bug 17141: IPv4 address sorting (reverse)', + ['IP'], + ipv4, + reversed(ipv4Sorted), + function( $table ) { + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click().click(); + } +); + +var umlautWords = [ + // Some words with Umlauts + ['Günther'], + ['Peter'], + ['Björn'], + ['Bjorn'], + ['Apfel'], + ['Äpfel'], + ['Strasse'], + ['Sträßschen'] +]; + +var umlautWordsSorted = [ + // Some words with Umlauts + ['Äpfel'], + ['Apfel'], + ['Björn'], + ['Bjorn'], + ['Günther'], + ['Peter'], + ['Sträßschen'], + ['Strasse'] +]; + +tableTest( + 'Accented Characters with custom collation', + ['Name'], + umlautWords, + umlautWordsSorted, + function( $table ) { + mw.config.set('tableSorterCollation', {'ä':'ae', 'ö' : 'oe', 'ß': 'ss', 'ü':'ue'}); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + mw.config.set('tableSorterCollation', {}); + } +); + +var planetsRowspan =[["Earth","6051.8"], jupiter, ["Mars","6051.8"], mercury, saturn, venus]; +var planetsRowspanII =[jupiter, mercury, saturn, ['Venus', '6371.0'], venus, ['Venus', '3390.0']]; + +tableTest( + 'Basic planet table: Same value for multiple rows via rowspan', + header, + planets, + planetsRowspan, + function( $table ) { + //Quick&Dirty mod + $table.find('tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)').remove(); + $table.find('tr:eq(2) td:eq(1)').attr('rowspan', '3'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); +tableTest( + 'Basic planet table: Same value for multiple rows via rowspan II', + header, + planets, + planetsRowspanII, + function( $table ) { + //Quick&Dirty mod + $table.find('tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)').remove(); + $table.find('tr:eq(2) td:eq(0)').attr('rowspan', '3'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); + +var complexMDYDates = [ + // Some words with Umlauts + ['January, 19 2010'], + ['April 21 1991'], + ['04 22 1991'], + ['5.12.1990'], + ['December 12 \'10'] +]; + +var complexMDYSorted = [ + ["5.12.1990"], + ["April 21 1991"], + ["04 22 1991"], + ["January, 19 2010"], + ["December 12 '10"] +]; + +tableTest( + 'Complex date parsing I', + ['date'], + complexMDYDates, + complexMDYSorted, + function( $table ) { + mw.config.set('wgDefaultDateFormat', window.wgDefaultDateFormat = 'mdy'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); + +var ascendingNameLegacy = ascendingName.slice(0); +ascendingNameLegacy[4] = ascendingNameLegacy[5]; +ascendingNameLegacy.pop(); + +tableTest( + 'Legacy compat with .sortbottom', + header, + planets, + ascendingNameLegacy, + function( $table ) { + $table.find('tr:last').addClass('sortbottom'); + $table.tablesorter(); + $table.find('.headerSort:eq(0)').click(); + } +); + +/** FIXME: the diff output is not very readeable. */ +test( 'bug 32047 - caption must be before thead', function() { + var $table; + $table = $( + '<table class="sortable">' + + '<caption>CAPTION</caption>' + + '<tr><th>THEAD</th></tr>' + + '<tr><td>A</td></tr>' + + '<tr><td>B</td></tr>' + + '<tr class="sortbottom"><td>TFOOT</td></tr>' + + '</table>' + ); + $table.tablesorter(); + + equals( + $table.children( ).get( 0 ).nodeName, + 'CAPTION', + 'First element after <thead> must be <caption> (bug 32047)' + ); +}); + +test( 'data-sort-value attribute, when available, should override sorting position', function() { + var $table, data; + + // Simple example, one without data-sort-value which should be sorted at it's text. + $table = $( + '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + + '<tbody>' + + '<tr><td>Cheetah</td></tr>' + + '<tr><td data-sort-value="Apple">Bird</td></tr>' + + '<tr><td data-sort-value="Bananna">Ferret</td></tr>' + + '<tr><td data-sort-value="Drupe">Elephant</td></tr>' + + '<tr><td data-sort-value="Cherry">Dolphin</td></tr>' + + '</tbody></table>' + ); + $table.tablesorter().find( '.headerSort:eq(0)' ).click(); + + data = []; + $table.find( 'tbody > tr' ).each( function( i, tr ) { + $( tr ).find( 'td' ).each( function( i, td ) { + data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } ); + }); + }); + + deepEqual( data, [ + { + "data": "Apple", + "text": "Bird" + }, { + "data": "Bananna", + "text": "Ferret" + }, { + "data": undefined, + "text": "Cheetah" + }, { + "data": "Cherry", + "text": "Dolphin" + }, { + "data": "Drupe", + "text": "Elephant" + } + ] ); + + // Another example + $table = $( + '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + + '<tbody>' + + '<tr><td>D</td></tr>' + + '<tr><td data-sort-value="E">A</td></tr>' + + '<tr><td>B</td></tr>' + + '<tr><td>G</td></tr>' + + '<tr><td data-sort-value="F">C</td></tr>' + + '</tbody></table>' + ); + $table.tablesorter().find( '.headerSort:eq(0)' ).click(); + + data = []; + $table.find( 'tbody > tr' ).each( function( i, tr ) { + $( tr ).find( 'td' ).each( function( i, td ) { + data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } ); + }); + }); + + deepEqual( data, [ + { + "data": undefined, + "text": "B" + }, { + "data": undefined, + "text": "D" + }, { + "data": "E", + "text": "A" + }, { + "data": "F", + "text": "C" + }, { + "data": undefined, + "text": "G" + } + ] ); + +}); + +})(); diff --git a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.js b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.js new file mode 100644 index 00000000..bcc9b96b --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.js @@ -0,0 +1,71 @@ +module( 'mediawiki.special.recentchanges.js' ); + +test( '-- Initial check', function() { + expect( 2 ); + ok( mw.special.recentchanges.init, + 'mw.special.recentchanges.init defined' + ); + ok( mw.special.recentchanges.updateCheckboxes, + 'mw.special.recentchanges.updateCheckboxes defined' + ); + // TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ] +}); + +test( '"all" namespace disable checkboxes', function() { + + // from Special:Recentchanges + var select = + '<select id="namespace" name="namespace" class="namespaceselector">' + + '<option value="" selected="selected">all</option>' + + '<option value="0">(Main)</option>' + + '<option value="1">Talk</option>' + + '<option value="2">User</option>' + + '<option value="3">User talk</option>' + + '<option value="4">ProjectName</option>' + + '<option value="5">ProjectName talk</option>' + + '</select>' + + '<input name="invert" type="checkbox" value="1" id="nsinvert" title="no title" />' + + '<label for="nsinvert" title="no title">Invert selection</label>' + + '<input name="associated" type="checkbox" value="1" id="nsassociated" title="no title" />' + + '<label for="nsassociated" title="no title">Associated namespace</label>' + + '<input type="submit" value="Go" />' + + '<input type="hidden" value="Special:RecentChanges" name="title" />' + ; + + var $env = $( '<div>' ).html( select ).appendTo( 'body' ); + + // TODO abstract the double strictEquals + + // At first checkboxes are enabled + strictEqual( $( '#nsinvert' ).attr( 'disabled' ), undefined ); + strictEqual( $( '#nsassociated' ).attr( 'disabled' ), undefined ); + + // Initiate the recentchanges module + mw.special.recentchanges.init(); + + // By default + strictEqual( $( '#nsinvert' ).attr( 'disabled' ), 'disabled' ); + strictEqual( $( '#nsassociated' ).attr( 'disabled' ), 'disabled' ); + + // select second option... + var $options = $( '#namespace' ).find( 'option' ); + $options.eq(0).removeAttr( 'selected' ); + $options.eq(1).attr( 'selected', 'selected' ); + $( '#namespace' ).change(); + + // ... and checkboxes should be enabled again + strictEqual( $( '#nsinvert' ).attr( 'disabled' ), undefined ); + strictEqual( $( '#nsassociated' ).attr( 'disabled' ), undefined ); + + // select first option ( 'all' namespace)... + $options.eq(1).removeAttr( 'selected' ); + $options.eq(0).attr( 'selected', 'selected' );; + $( '#namespace' ).change(); + + // ... and checkboxes should now be disabled + strictEqual( $( '#nsinvert' ).attr( 'disabled' ), 'disabled' ); + strictEqual( $( '#nsassociated' ).attr( 'disabled' ), 'disabled' ); + + // DOM cleanup + $env.remove(); +}); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.js b/tests/qunit/suites/resources/mediawiki/mediawiki.js new file mode 100644 index 00000000..4beed881 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.js @@ -0,0 +1,232 @@ +module( 'mediawiki.js' ); + +test( '-- Initial check', function() { + expect(8); + + ok( window.jQuery, 'jQuery defined' ); + ok( window.$, '$j defined' ); + ok( window.$j, '$j defined' ); + strictEqual( window.$, window.jQuery, '$ alias to jQuery' ); + strictEqual( window.$j, window.jQuery, '$j alias to jQuery' ); + + ok( window.mediaWiki, 'mediaWiki defined' ); + ok( window.mw, 'mw defined' ); + strictEqual( window.mw, window.mediaWiki, 'mw alias to mediaWiki' ); +}); + +test( 'mw.Map', function() { + expect(17); + + ok( mw.Map, 'mw.Map defined' ); + + var conf = new mw.Map(), + // Dummy variables + funky = function() {}, + arry = [], + nummy = 7; + + // Tests for input validation + strictEqual( conf.get( 'inexistantKey' ), null, 'Map.get returns null if selection was a string and the key was not found' ); + strictEqual( conf.set( 'myKey', 'myValue' ), true, 'Map.set returns boolean true if a value was set for a valid key string' ); + strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' ); + strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' ); + strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' ); + equal( conf.get( 'myKey' ), 'myValue', 'Map.get returns a single value value correctly' ); + strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' ); + strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' ); + + // Multiple values at once + var someValues = { + 'foo': 'bar', + 'lorem': 'ipsum', + 'MediaWiki': true + }; + strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' ); + deepEqual( conf.get( ['foo', 'lorem'] ), { + 'foo': 'bar', + 'lorem': 'ipsum' + }, 'Map.get returns multiple values correctly as an object' ); + + deepEqual( conf.get( ['foo', 'notExist'] ), { + 'foo': 'bar', + 'notExist': null + }, 'Map.get return includes keys that were not found as null values' ); + + strictEqual( conf.exists( 'foo' ), true, 'Map.exists returns boolean true if a key exists' ); + strictEqual( conf.exists( 'notExist' ), false, 'Map.exists returns boolean false if a key does not exists' ); + + // Interacting with globals and accessing the values object + strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' ); + + conf.set( 'globalMapChecker', 'Hi' ); + + ok( false === 'globalMapChecker' in window, 'new mw.Map did not store its values in the global window object by default' ); + + var globalConf = new mw.Map( true ); + globalConf.set( 'anotherGlobalMapChecker', 'Hello' ); + + ok( 'anotherGlobalMapChecker' in window, 'new mw.Map( true ) did store its values in the global window object' ); + + // Whitelist this global variable for QUnit's 'noglobal' mode + if ( QUnit.config.noglobals ) { + QUnit.config.pollution.push( 'anotherGlobalMapChecker' ); + } +}); + +test( 'mw.config', function() { + expect(1); + + ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' ); +}); + +test( 'mw.message & mw.messages', function() { + expect(17); + + ok( mw.messages, 'messages defined' ); + ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' ); + ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); + + var hello = mw.message( 'hello' ); + + equal( hello.format, 'parse', 'Message property "format" defaults to "parse"' ); + strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' ); + equal( hello.key, 'hello', 'Message property "key" (currect key)' ); + deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' ); + + // Todo + ok( hello.params, 'Message prototype "params"' ); + + hello.format = 'plain'; + equal( hello.toString(), 'Hello <b>awesome</b> world', 'Message.toString returns the message as a string with the current "format"' ); + + equal( hello.escaped(), 'Hello <b>awesome</b> world', 'Message.escaped returns the escaped message' ); + equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' ); + + hello.parse(); + equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' ); + + hello.plain(); + equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' ); + + strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' ); + + var goodbye = mw.message( 'goodbye' ); + strictEqual( goodbye.exists(), false, 'Message.exists returns false for inexisting messages' ); + + equal( goodbye.plain(), '<goodbye>', 'Message.toString returns plain <key> if format is "plain" and key does not exist' ); + // bug 30684 + equal( goodbye.escaped(), '<goodbye>', 'Message.toString returns properly escaped <key> if format is "escaped" and key does not exist' ); +}); + +test( 'mw.msg', function() { + expect(3); + + ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' ); + + equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' ); + equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (inexisting message)' ); +}); + +test( 'mw.loader', function() { + expect(5); + + // Regular expression to extract the path for the QUnit tests + // Takes into account that tests could be run from a file:// URL + // by excluding the 'index.html' part from the URL + var rePath = /(?:[^#\?](?!index.html))*\/?/; + + // Four assertions to test the above regular expression: + equal( + rePath.exec( 'http://path/to/tests/?foobar' )[0], + "http://path/to/tests/", + "Extracting path from http URL with query" + ); + equal( + rePath.exec( 'http://path/to/tests/#frag' )[0], + "http://path/to/tests/", + "Extracting path from http URL with fragment" + ); + equal( + rePath.exec( 'file://path/to/tests/index.html?foobar' )[0], + "file://path/to/tests/", + "Extracting path from local URL (file://) with query" + ); + equal( + rePath.exec( 'file://path/to/tests/index.html#frag' )[0], + "file://path/to/tests/", + "Extracting path from local URL (file://) with fragment" + ); + + // Asynchronous ahead + stop(5000); + + // Extract path + var tests_path = rePath.exec( location.href ); + + mw.loader.implement( 'is.awesome', [QUnit.fixurl( tests_path + 'data/defineTestCallback.js')], {}, {} ); + + mw.loader.using( 'is.awesome', function() { + + // /sample/awesome.js declares the "mw.loader.testCallback" function + // which contains a call to start() and ok() + mw.loader.testCallback(); + mw.loader.testCallback = undefined; + + }, function() { + start(); + ok( false, 'Error callback fired while implementing "is.awesome" module' ); + }); + +}); + +test( 'mw.loader.bug29107' , function() { + expect(2); + + // Message doesn't exist already + ok( !mw.messages.exists( 'bug29107' ) ); + + // Async! Include a timeout, as failure in this test leads to neither the + // success nor failure callbacks getting called. + stop(5000); + + mw.loader.implement( 'bug29107.messages-only', [], {}, {'bug29107': 'loaded'} ); + mw.loader.using( 'bug29107.messages-only', function() { + start(); + ok( mw.messages.exists( 'bug29107' ), 'Bug 29107: messages-only module should implement ok' ); + }, function() { + start(); + ok( false, 'Error callback fired while implementing "bug29107.messages-only" module' ); + }); +}); + +test( 'mw.html', function() { + expect(7); + + raises( function(){ + mw.html.escape(); + }, TypeError, 'html.escape throws a TypeError if argument given is not a string' ); + + equal( mw.html.escape( '<mw awesome="awesome" value=\'test\' />' ), + '<mw awesome="awesome" value='test' />', 'html.escape escapes html snippet' ); + + equal( mw.html.element(), + '<undefined/>', 'html.element Always return a valid html string (even without arguments)' ); + + equal( mw.html.element( 'div' ), '<div/>', 'html.element DIV (simple)' ); + + equal( mw.html.element( 'div', + { id: 'foobar' } ), + '<div id="foobar"/>', + 'html.element DIV (attribs)' ); + + equal( mw.html.element( 'div', + null, 'a' ), + '<div>a</div>', + 'html.element DIV (content)' ); + + equal( mw.html.element( 'a', + { href: 'http://mediawiki.org/w/index.php?title=RL&action=history' }, 'a' ), + '<a href="http://mediawiki.org/w/index.php?title=RL&action=history">a</a>', + 'html.element DIV (attribs + content)' ); + +}); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js new file mode 100644 index 00000000..52cd32c8 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js @@ -0,0 +1,35 @@ +/* Some misc JavaScript compatibility tests, just to make sure the environments we run in are consistent */ + +module( 'mediawiki.jscompat' ); + +test( 'Variable with Unicode letter in name', function() { + expect(3); + var orig = "some token"; + var ŝablono = orig; + deepEqual( ŝablono, orig, 'ŝablono' ); + deepEqual( \u015dablono, orig, '\\u015dablono' ); + deepEqual( \u015Dablono, orig, '\\u015Dablono' ); +}); + +/* +// Not that we need this. ;) +// This fails on IE 6-8 +// Works on IE 9, Firefox 6, Chrome 14 +test( 'Keyword workaround: "if" as variable name using Unicode escapes', function() { + var orig = "another token"; + \u0069\u0066 = orig; + deepEqual( \u0069\u0066, orig, '\\u0069\\u0066' ); +}); +*/ + +/* +// Not that we need this. ;) +// This fails on IE 6-9 +// Works on Firefox 6, Chrome 14 +test( 'Keyword workaround: "if" as member variable name using Unicode escapes', function() { + var orig = "another token"; + var foo = {}; + foo.\u0069\u0066 = orig; + deepEqual( foo.\u0069\u0066, orig, 'foo.\\u0069\\u0066' ); +}); +*/ diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.user.js b/tests/qunit/suites/resources/mediawiki/mediawiki.user.js new file mode 100644 index 00000000..d5c6baad --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.user.js @@ -0,0 +1,29 @@ +module( 'mediawiki.user.js' ); + +test( '-- Initial check', function() { + expect(1); + + ok( mw.user, 'mw.user defined' ); +}); + + +test( 'options', function() { + expect(1); + + ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' ); +}); + +test( 'User login status', function() { + expect(5); + + strictEqual( mw.user.name(), null, 'user.name should return null when anonymous' ); + ok( mw.user.anonymous(), 'user.anonymous should reutrn true when anonymous' ); + + // Not part of startUp module + mw.config.set( 'wgUserName', 'John' ); + + equal( mw.user.name(), 'John', 'user.name returns username when logged-in' ); + ok( !mw.user.anonymous(), 'user.anonymous returns false when logged-in' ); + + equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' ); +}); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.js new file mode 100644 index 00000000..9c05d9b2 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.js @@ -0,0 +1,307 @@ +module( 'mediawiki.util.js' ); + +test( '-- Initial check', function() { + expect(1); + + ok( mw.util, 'mw.util defined' ); +}); + +test( 'rawurlencode', function() { + expect(1); + + equal( mw.util.rawurlencode( 'Test:A & B/Here' ), 'Test%3AA%20%26%20B%2FHere' ); +}); + +test( 'wikiUrlencode', function() { + expect(1); + + equal( mw.util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' ); +}); + +test( 'wikiGetlink', function() { + expect(3); + + // Not part of startUp module + mw.config.set( 'wgArticlePath', '/wiki/$1' ); + mw.config.set( 'wgPageName', 'Foobar' ); + + var hrefA = mw.util.wikiGetlink( 'Sandbox' ); + equal( hrefA, '/wiki/Sandbox', 'Simple title; Get link for "Sandbox"' ); + + var hrefB = mw.util.wikiGetlink( 'Foo:Sandbox ? 5+5=10 ! (test)/subpage' ); + equal( hrefB, '/wiki/Foo:Sandbox_%3F_5%2B5%3D10_%21_%28test%29/subpage', + 'Advanced title; Get link for "Foo:Sandbox ? 5+5=10 ! (test)/subpage"' ); + + var hrefC = mw.util.wikiGetlink(); + equal( hrefC, '/wiki/Foobar', 'Default title; Get link for current page ("Foobar")' ); +}); + +test( 'wikiScript', function() { + expect(2); + + mw.config.set({ + 'wgScript': '/w/index.php', + 'wgScriptPath': '/w', + 'wgScriptExtension': '.php' + }); + + equal( mw.util.wikiScript(), mw.config.get( 'wgScript' ), 'Defaults to index.php and is equal to wgScript' ); + equal( mw.util.wikiScript( 'api' ), '/w/api.php', 'API path' ); + +}); + +test( 'addCSS', function() { + expect(3); + + var $testEl = $( '<div>' ).attr( 'id', 'mw-addcsstest' ).appendTo( 'body' ); + + var style = mw.util.addCSS( '#mw-addcsstest { visibility: hidden; }' ); + equal( typeof style, 'object', 'addCSS returned an object' ); + strictEqual( style.disabled, false, 'property "disabled" is available and set to false' ); + + equal( $testEl.css( 'visibility' ), 'hidden', 'Added style properties are in effect' ); + + // Clean up + $( style.ownerNode ) + .add( $testEl ) + .remove(); +}); + +test( 'toggleToc', function() { + expect(4); + + strictEqual( mw.util.toggleToc(), null, 'Return null if there is no table of contents on the page.' ); + + var tocHtml = + '<table id="toc" class="toc"><tr><td>' + + '<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>', + $toc = $(tocHtml).appendTo( 'body' ), + $toggleLink = $( '#togglelink' ); + + strictEqual( $toggleLink.length, 1, 'Toggle link is appended to the page.' ); + + // Toggle animation is asynchronous + // QUnit should not finish this test() untill they are all done + stop(); + + var actionC = function() { + start(); + + // Clean up + $toc.remove(); + }; + var actionB = function() { + start(); stop(); + strictEqual( mw.util.toggleToc( $toggleLink, actionC ), true, 'Return boolean true if the TOC is now visible.' ); + }; + var actionA = function() { + strictEqual( mw.util.toggleToc( $toggleLink, actionB ), false, 'Return boolean false if the TOC is now hidden.' ); + }; + + actionA(); +}); + +test( 'getParamValue', function() { + expect(5); + + var url1 = 'http://mediawiki.org/?foo=wrong&foo=right#&foo=bad'; + + equal( mw.util.getParamValue( 'foo', url1 ), 'right', 'Use latest one, ignore hash' ); + strictEqual( mw.util.getParamValue( 'bar', url1 ), null, 'Return null when not found' ); + + var url2 = 'http://mediawiki.org/#&foo=bad'; + strictEqual( mw.util.getParamValue( 'foo', url2 ), null, 'Ignore hash if param is not in querystring but in hash (bug 27427)' ); + + var url3 = 'example.com?' + $.param({ 'TEST': 'a b+c' }); + strictEqual( mw.util.getParamValue( 'TEST', url3 ), 'a b+c', 'Bug 30441: getParamValue must understand "+" encoding of space' ); + + var url4 = 'example.com?' + $.param({ 'TEST': 'a b+c d' }); // check for sloppy code from r95332 :) + strictEqual( mw.util.getParamValue( 'TEST', url4 ), 'a b+c d', 'Bug 30441: getParamValue must understand "+" encoding of space (multiple spaces)' ); +}); + +test( 'tooltipAccessKey', function() { + expect(3); + + equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'mw.util.tooltipAccessKeyPrefix must be a string' ); + ok( mw.util.tooltipAccessKeyRegexp instanceof RegExp, 'mw.util.tooltipAccessKeyRegexp instance of RegExp' ); + ok( mw.util.updateTooltipAccessKeys, 'mw.util.updateTooltipAccessKeys' ); +}); + +test( '$content', function() { + expect(2); + + ok( mw.util.$content instanceof jQuery, 'mw.util.$content instance of jQuery' ); + strictEqual( mw.util.$content.length, 1, 'mw.util.$content must have length of 1' ); +}); + +test( 'addPortletLink', function() { + expect(7); + + var mwPanel = '<div id="mw-panel" class="noprint">\ + <h5>Toolbox</h5>\ + <div class="portlet" id="p-tb">\ + <ul class="body"></ul>\ + </div>\ +</div>', + vectorTabs = '<div id="p-views" class="vectorTabs">\ + <h5>Views</h5>\ + <ul></ul>\ +</div>', + $mwPanel = $(mwPanel).appendTo( 'body' ), + $vectorTabs = $(vectorTabs).appendTo( 'body' ); + + var tbRL = mw.util.addPortletLink( 'p-tb', 'http://mediawiki.org/wiki/ResourceLoader', + 'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l' ); + + ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' ); + + var tbMW = mw.util.addPortletLink( 'p-tb', 'http://mediawiki.org/', + 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', tbRL ), + $tbMW = $( tbMW ); + + + equal( $tbMW.attr( 'id' ), 't-mworg', 'Link has correct ID set' ); + equal( $tbMW.closest( '.portlet' ).attr( 'id' ), 'p-tb', 'Link was inserted within correct portlet' ); + equal( $tbMW.next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing nextnode)' ); + + var tbRLDM = mw.util.addPortletLink( 'p-tb', 'http://mediawiki.org/wiki/RL/DM', + 'Default modules', 't-rldm', 'List of all default modules ', 'd', '#t-rl' ); + + equal( $( tbRLDM ).next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing CSS selector)' ); + + var caFoo = mw.util.addPortletLink( 'p-views', '#', 'Foo' ); + + strictEqual( $tbMW.find( 'span').length, 0, 'No <span> element should be added for porlets without vectorTabs class.' ); + strictEqual( $( caFoo ).find( 'span').length, 1, 'A <span> element should be added for porlets with vectorTabs class.' ); + + // Clean up + $( [tbRL, tbMW, tbRLDM, caFoo] ) + .add( $mwPanel ) + .add( $vectorTabs ) + .remove(); +}); + +test( 'jsMessage', function() { + expect(1); + + var a = mw.util.jsMessage( "MediaWiki is <b>Awesome</b>." ); + ok( a, 'Basic checking of return value' ); + + // Clean up + $( '#mw-js-message' ).remove(); +}); + +test( 'validateEmail', function() { + expect(6); + + strictEqual( mw.util.validateEmail( "" ), null, 'Should return null for empty string ' ); + strictEqual( mw.util.validateEmail( "user@localhost" ), true, 'Return true for a valid e-mail address' ); + + // testEmailWithCommasAreInvalids + strictEqual( mw.util.validateEmail( "user,foo@example.org" ), false, 'Emails with commas are invalid' ); + strictEqual( mw.util.validateEmail( "userfoo@ex,ample.org" ), false, 'Emails with commas are invalid' ); + + // testEmailWithHyphens + strictEqual( mw.util.validateEmail( "user-foo@example.org" ), true, 'Emails may contain a hyphen' ); + strictEqual( mw.util.validateEmail( "userfoo@ex-ample.org" ), true, 'Emails may contain a hyphen' ); +}); + +test( 'isIPv6Address', function() { + expect(40); + + // Shortcuts + var assertFalseIPv6 = function( addy, summary ) { + return strictEqual( mw.util.isIPv6Address( addy ), false, summary ); + }, + assertTrueIPv6 = function( addy, summary ) { + return strictEqual( mw.util.isIPv6Address( addy ), true, summary ); + }; + + // Based on IPTest.php > testisIPv6 + assertFalseIPv6( ':fc:100::', 'IPv6 starting with lone ":"' ); + assertFalseIPv6( 'fc:100:::', 'IPv6 ending with a ":::"' ); + assertFalseIPv6( 'fc:300', 'IPv6 with only 2 words' ); + assertFalseIPv6( 'fc:100:300', 'IPv6 with only 3 words' ); + + $.each( + ['fc:100::', + 'fc:100:a::', + 'fc:100:a:d::', + 'fc:100:a:d:1::', + 'fc:100:a:d:1:e::', + 'fc:100:a:d:1:e:ac::'], function( i, addy ){ + assertTrueIPv6( addy, addy + ' is a valid IP' ); + }); + + assertFalseIPv6( 'fc:100:a:d:1:e:ac:0::', 'IPv6 with 8 words ending with "::"' ); + assertFalseIPv6( 'fc:100:a:d:1:e:ac:0:1::', 'IPv6 with 9 words ending with "::"' ); + + assertFalseIPv6( ':::' ); + assertFalseIPv6( '::0:', 'IPv6 ending in a lone ":"' ); + + assertTrueIPv6( '::', 'IPv6 zero address' ); + $.each( + ['::0', + '::fc', + '::fc:100', + '::fc:100:a', + '::fc:100:a:d', + '::fc:100:a:d:1', + '::fc:100:a:d:1:e', + '::fc:100:a:d:1:e:ac', + + 'fc:100:a:d:1:e:ac:0'], function( i, addy ){ + assertTrueIPv6( addy, addy + ' is a valid IP' ); + }); + + assertFalseIPv6( '::fc:100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' ); + assertFalseIPv6( '::fc:100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' ); + + assertFalseIPv6( ':fc::100', 'IPv6 starting with lone ":"' ); + assertFalseIPv6( 'fc::100:', 'IPv6 ending with lone ":"' ); + assertFalseIPv6( 'fc:::100', 'IPv6 with ":::" in the middle' ); + + assertTrueIPv6( 'fc::100', 'IPv6 with "::" and 2 words' ); + assertTrueIPv6( 'fc::100:a', 'IPv6 with "::" and 3 words' ); + assertTrueIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ); + assertTrueIPv6( 'fc::100:a:d:1', 'IPv6 with "::" and 5 words' ); + assertTrueIPv6( 'fc::100:a:d:1:e', 'IPv6 with "::" and 6 words' ); + assertTrueIPv6( 'fc::100:a:d:1:e:ac', 'IPv6 with "::" and 7 words' ); + assertTrueIPv6( '2001::df', 'IPv6 with "::" and 2 words' ); + assertTrueIPv6( '2001:5c0:1400:a::df', 'IPv6 with "::" and 5 words' ); + assertTrueIPv6( '2001:5c0:1400:a::df:2', 'IPv6 with "::" and 6 words' ); + + assertFalseIPv6( 'fc::100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' ); + assertFalseIPv6( 'fc::100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' ); +}); + +test( 'isIPv4Address', function() { + expect(11); + + // Shortcuts + var assertFalseIPv4 = function( addy, summary ) { + return strictEqual( mw.util.isIPv4Address( addy ), false, summary ); + }, + assertTrueIPv4 = function( addy, summary ) { + return strictEqual( mw.util.isIPv4Address( addy ), true, summary ); + }; + + // Based on IPTest.php > testisIPv4 + assertFalseIPv4( false, 'Boolean false is not an IP' ); + assertFalseIPv4( true, 'Boolean true is not an IP' ); + assertFalseIPv4( '', 'Empty string is not an IP' ); + assertFalseIPv4( 'abc', '"abc" is not an IP' ); + assertFalseIPv4( ':', 'Colon is not an IP' ); + assertFalseIPv4( '124.24.52', 'IPv4 not enough quads' ); + assertFalseIPv4( '24.324.52.13', 'IPv4 out of range' ); + assertFalseIPv4( '.24.52.13', 'IPv4 starts with period' ); + + assertTrueIPv4( '124.24.52.13', '124.24.52.134 is a valid IP' ); + assertTrueIPv4( '1.24.52.13', '1.24.52.13 is a valid IP' ); + assertFalseIPv4( '74.24.52.13/20', 'IPv4 ranges are not recogzized as valid IPs' ); +}); |