diff options
Diffstat (limited to 'tests/qunit/suites')
15 files changed, 1922 insertions, 0 deletions
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' ); +}); |