diff options
Diffstat (limited to 'www/wiki/tests/qunit/suites/resources/jquery')
13 files changed, 3350 insertions, 0 deletions
diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js new file mode 100644 index 00000000..e4b61572 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js @@ -0,0 +1,121 @@ +( function ( $ ) { + var getAccessKeyPrefixTestData, updateTooltipAccessKeysTestData; + + QUnit.module( 'jquery.accessKeyLabel', QUnit.newMwEnvironment( { + messages: { + brackets: '[$1]', + 'word-separator': ' ' + } + } ) ); + + getAccessKeyPrefixTestData = [ + // ua string, platform string, expected prefix + // Internet Explorer + [ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 'Win32', 'alt-' ], + [ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', 'Win32', 'alt-' ], + [ 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko', 'Win64', 'alt-' ], + [ 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136', 'Win64', 'alt-' ], + // Firefox + [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19', 'MacIntel', 'ctrl-' ], + [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17', 'Linux i686', 'alt-shift-' ], + [ 'Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', 'Win32', 'alt-shift-' ], + [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0', 'MacIntel', 'ctrl-option-' ], + [ 'Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20121202 Firefox/17.0 Iceweasel/17.0.1', 'Linux 1686', 'alt-shift-' ], + [ 'Mozilla/5.0 (Windows NT 5.2; U; de; rv:1.8.0) Gecko/20060728 Firefox/1.5.0', 'Win32', 'alt-' ], + // Safari / Konqueror + [ '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', 'MacIntel', 'ctrl-option-' ], + [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; de-de) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3', 'MacIntel', 'ctrl-' ], + [ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; cs-CZ) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.29', 'Win32', 'alt-' ], + [ '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', 'Win32', 'alt-' ], + [ 'Mozilla/5.0 (X11; Linux i686) KHTML/4.9.1 (like Gecko) Konqueror/4.9', 'Linux i686', 'ctrl-' ], + // Opera + [ 'Opera/9.80 (Windows NT 5.1)', 'Win32', 'shift-esc-' ], + [ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130', 'Win32', 'alt-shift-' ], + // Chrome + [ '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', 'MacIntel', 'ctrl-option-' ], + [ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30', 'Linux i686', 'alt-shift-' ], + // Unknown! Note: These aren't necessarily *right*, this is just + // testing that we're getting the expected output based on the + // platform. + [ 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:1.0.1) Gecko/20021111 Chimera/0.6', 'MacPPC', 'ctrl-' ], + [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.3a) Gecko/20021207 Phoenix/0.5', 'Linux i686', 'alt-' ] + ]; + // strings appended to title to make sure updateTooltipAccessKeys handles them correctly + updateTooltipAccessKeysTestData = [ '', ' [a]', ' [test-a]', ' [alt-b]' ]; + + function makeInput( title, accessKey ) { + // The properties aren't escaped, so make sure you don't call this function with values that need to be escaped! + return '<input title="' + title + '" ' + ( accessKey ? 'accessKey="' + accessKey + '" ' : '' ) + ' />'; + } + + QUnit.test( 'getAccessKeyPrefix', function ( assert ) { + var i; + for ( i = 0; i < getAccessKeyPrefixTestData.length; i++ ) { + assert.equal( $.fn.updateTooltipAccessKeys.getAccessKeyPrefix( { + userAgent: getAccessKeyPrefixTestData[ i ][ 0 ], + platform: getAccessKeyPrefixTestData[ i ][ 1 ] + } ), getAccessKeyPrefixTestData[ i ][ 2 ], 'Correct prefix for ' + getAccessKeyPrefixTestData[ i ][ 0 ] ); + } + } ); + + QUnit.test( 'updateTooltipAccessKeys - current browser', function ( assert ) { + var title = $( makeInput( 'Title', 'a' ) ).updateTooltipAccessKeys().prop( 'title' ), + // The new title should be something like "Title [alt-a]", but the exact label will depend on the browser. + // The "a" could be capitalized, and the prefix could be anything, e.g. a simple "^" for ctrl- + // (no browser is known using such a short prefix, though) or "Alt+Umschalt+" in German Firefox. + result = /^Title \[(.+)[aA]\]$/.exec( title ); + assert.ok( result, 'title should match expected structure.' ); + assert.notEqual( result[ 1 ], 'test-', 'Prefix used for testing shouldn\'t be used in production.' ); + } ); + + QUnit.test( 'updateTooltipAccessKeys - no access key', function ( assert ) { + var i, oldTitle, $input, newTitle; + for ( i = 0; i < updateTooltipAccessKeysTestData.length; i++ ) { + oldTitle = 'Title' + updateTooltipAccessKeysTestData[ i ]; + $input = $( makeInput( oldTitle ) ); + $( '#qunit-fixture' ).append( $input ); + newTitle = $input.updateTooltipAccessKeys().prop( 'title' ); + assert.equal( newTitle, 'Title', 'title="' + oldTitle + '"' ); + } + } ); + + QUnit.test( 'updateTooltipAccessKeys - with access key', function ( assert ) { + var i, oldTitle, $input, newTitle; + $.fn.updateTooltipAccessKeys.setTestMode( true ); + for ( i = 0; i < updateTooltipAccessKeysTestData.length; i++ ) { + oldTitle = 'Title' + updateTooltipAccessKeysTestData[ i ]; + $input = $( makeInput( oldTitle, 'a' ) ); + $( '#qunit-fixture' ).append( $input ); + newTitle = $input.updateTooltipAccessKeys().prop( 'title' ); + assert.equal( newTitle, 'Title [test-a]', 'title="' + oldTitle + '"' ); + } + $.fn.updateTooltipAccessKeys.setTestMode( false ); + } ); + + QUnit.test( 'updateTooltipAccessKeys with label element', function ( assert ) { + var html, $label, $input; + $.fn.updateTooltipAccessKeys.setTestMode( true ); + html = '<label for="testInput" title="Title">Label</label><input id="testInput" accessKey="a" />'; + $( '#qunit-fixture' ).html( html ); + $label = $( '#qunit-fixture label' ); + $input = $( '#qunit-fixture input' ); + $input.updateTooltipAccessKeys(); + assert.equal( $input.prop( 'title' ), '', 'No title attribute added to input element.' ); + assert.equal( $label.prop( 'title' ), 'Title [test-a]', 'title updated for associated label element.' ); + $.fn.updateTooltipAccessKeys.setTestMode( false ); + } ); + + QUnit.test( 'updateTooltipAccessKeys with label element as parent', function ( assert ) { + var html, $label, $input; + $.fn.updateTooltipAccessKeys.setTestMode( true ); + html = '<label title="Title">Label<input id="testInput" accessKey="a" /></label>'; + $( '#qunit-fixture' ).html( html ); + $label = $( '#qunit-fixture label' ); + $input = $( '#qunit-fixture input' ); + $input.updateTooltipAccessKeys(); + assert.equal( $input.prop( 'title' ), '', 'No title attribute added to input element.' ); + assert.equal( $label.prop( 'title' ), 'Title [test-a]', 'title updated for associated label element.' ); + $.fn.updateTooltipAccessKeys.setTestMode( false ); + } ); + +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.color.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.color.test.js new file mode 100644 index 00000000..ca6a512f --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.color.test.js @@ -0,0 +1,15 @@ +( function ( $ ) { + QUnit.module( 'jquery.color', QUnit.newMwEnvironment() ); + + QUnit.test( 'animate', function ( assert ) { + var done = assert.async(), + $canvas = $( '<div>' ).css( 'background-color', '#fff' ).appendTo( '#qunit-fixture' ); + + $canvas.animate( { 'background-color': '#000' }, 3 ).promise() + .done( function () { + var endColors = $.colorUtil.getRGB( $canvas.css( 'background-color' ) ); + assert.deepEqual( endColors, [ 0, 0, 0 ], 'end state' ); + } ) + .always( done ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js new file mode 100644 index 00000000..d6208e91 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js @@ -0,0 +1,63 @@ +( function ( $ ) { + QUnit.module( 'jquery.colorUtil', QUnit.newMwEnvironment() ); + + QUnit.test( 'getRGB', function ( assert ) { + assert.strictEqual( $.colorUtil.getRGB(), undefined, 'No arguments' ); + assert.strictEqual( $.colorUtil.getRGB( '' ), undefined, 'Empty string' ); + assert.deepEqual( $.colorUtil.getRGB( [ 0, 100, 255 ] ), [ 0, 100, 255 ], 'Parse array of rgb values' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0,100,255)' ), [ 0, 100, 255 ], 'Parse simple rgb string' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0, 100, 255)' ), [ 0, 100, 255 ], 'Parse simple rgb string with spaces' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%,20%,40%)' ), [ 0, 51, 102 ], 'Parse rgb string with percentages' ); + assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%, 20%, 40%)' ), [ 0, 51, 102 ], 'Parse rgb string with percentages and spaces' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2ddee' ), [ 242, 221, 238 ], 'Hex string: 6 char lowercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2DDEE' ), [ 242, 221, 238 ], 'Hex string: 6 char uppercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#f2DdEe' ), [ 242, 221, 238 ], 'Hex string: 6 char mixed' ); + assert.deepEqual( $.colorUtil.getRGB( '#eee' ), [ 238, 238, 238 ], 'Hex string: 3 char lowercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#EEE' ), [ 238, 238, 238 ], 'Hex string: 3 char uppercase' ); + assert.deepEqual( $.colorUtil.getRGB( '#eEe' ), [ 238, 238, 238 ], 'Hex string: 3 char mixed' ); + assert.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 behavior so, let's keep + // track of it, so we will know in case it would ever change. + assert.strictEqual( $.colorUtil.getRGB( 'rgba(0,0,0,0)' ), undefined, 'Zero rgba without whitespace' ); + + assert.deepEqual( $.colorUtil.getRGB( 'lightGreen' ), [ 144, 238, 144 ], 'Color names (lightGreen)' ); + assert.deepEqual( $.colorUtil.getRGB( 'transparent' ), [ 255, 255, 255 ], 'Color names (transparent)' ); + assert.strictEqual( $.colorUtil.getRGB( 'mediaWiki' ), undefined, 'Inexisting color name' ); + } ); + + QUnit.test( 'rgbToHsl', function ( assert ) { + var hsl, ret; + + // Cross-browser differences in decimals... + // Round to two decimals so they can be more reliably checked. + function dualDecimals( a ) { + return Math.round( a * 100 ) / 100; + } + + // Re-create the rgbToHsl return array items, limited to two decimals. + hsl = $.colorUtil.rgbToHsl( 144, 238, 144 ); + ret = [ dualDecimals( hsl[ 0 ] ), dualDecimals( hsl[ 1 ] ), dualDecimals( hsl[ 2 ] ) ]; + + assert.deepEqual( ret, [ 0.33, 0.73, 0.75 ], 'rgb(144, 238, 144): hsl(0.33, 0.73, 0.75)' ); + } ); + + QUnit.test( 'hslToRgb', function ( assert ) { + var rgb, ret; + rgb = $.colorUtil.hslToRgb( 0.3, 0.7, 0.8 ); + + // Re-create the hslToRgb return array items, rounded to whole numbers. + ret = [ Math.round( rgb[ 0 ] ), Math.round( rgb[ 1 ] ), Math.round( rgb[ 2 ] ) ]; + + assert.deepEqual( ret, [ 183, 240, 168 ], 'hsl(0.3, 0.7, 0.8): rgb(183, 240, 168)' ); + } ); + + QUnit.test( 'getColorBrightness', function ( assert ) { + var a, b; + a = $.colorUtil.getColorBrightness( 'red', +0.1 ); + assert.equal( a, 'rgb(255,50,50)', 'Start with named color "red", brighten 10%' ); + + b = $.colorUtil.getColorBrightness( 'rgb(200,50,50)', -0.2 ); + assert.equal( b, 'rgb(118,29,29)', 'Start with rgb string "rgb(200,50,50)", darken 20%' ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js new file mode 100644 index 00000000..74d85090 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js @@ -0,0 +1,14 @@ +( function ( $ ) { + QUnit.module( 'jquery.getAttrs', QUnit.newMwEnvironment() ); + + QUnit.test( 'getAttrs()', function ( assert ) { + var attrs = { + foo: 'bar', + class: 'lorem', + 'data-foo': 'data value' + }, + $el = $( '<div>' ).attr( attrs ); + + assert.propEqual( $el.getAttrs(), attrs, 'keys and values match' ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js new file mode 100644 index 00000000..6a265eb5 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js @@ -0,0 +1,38 @@ +( function ( $ ) { + QUnit.module( 'jquery.hidpi', QUnit.newMwEnvironment() ); + + QUnit.test( 'devicePixelRatio', function ( assert ) { + var devicePixelRatio = $.devicePixelRatio(); + assert.equal( typeof devicePixelRatio, 'number', '$.devicePixelRatio() returns a number' ); + } ); + + QUnit.test( 'bracketedDevicePixelRatio', function ( assert ) { + var ratio = $.bracketedDevicePixelRatio(); + assert.equal( typeof ratio, 'number', '$.bracketedDevicePixelRatio() returns a number' ); + } ); + + QUnit.test( 'bracketDevicePixelRatio', function ( assert ) { + assert.equal( $.bracketDevicePixelRatio( 0.75 ), 1, '0.75 gives 1' ); + assert.equal( $.bracketDevicePixelRatio( 1 ), 1, '1 gives 1' ); + assert.equal( $.bracketDevicePixelRatio( 1.25 ), 1.5, '1.25 gives 1.5' ); + assert.equal( $.bracketDevicePixelRatio( 1.5 ), 1.5, '1.5 gives 1.5' ); + assert.equal( $.bracketDevicePixelRatio( 1.75 ), 2, '1.75 gives 2' ); + assert.equal( $.bracketDevicePixelRatio( 2 ), 2, '2 gives 2' ); + assert.equal( $.bracketDevicePixelRatio( 2.5 ), 2, '2.5 gives 2' ); + assert.equal( $.bracketDevicePixelRatio( 3 ), 2, '3 gives 2' ); + } ); + + QUnit.test( 'matchSrcSet', function ( assert ) { + var srcset = 'onefive.png 1.5x, two.png 2x'; + + // Nice exact matches + assert.equal( $.matchSrcSet( 1, srcset ), null, '1.0 gives no match' ); + assert.equal( $.matchSrcSet( 1.5, srcset ), 'onefive.png', '1.5 gives match' ); + assert.equal( $.matchSrcSet( 2, srcset ), 'two.png', '2 gives match' ); + + // Non-exact matches; should return the next-biggest specified + assert.equal( $.matchSrcSet( 1.25, srcset ), null, '1.25 gives no match' ); + assert.equal( $.matchSrcSet( 1.75, srcset ), 'onefive.png', '1.75 gives match to 1.5' ); + assert.equal( $.matchSrcSet( 2.25, srcset ), 'two.png', '2.25 gives match to 2' ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js new file mode 100644 index 00000000..277ba3f2 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js @@ -0,0 +1,235 @@ +( function ( $ ) { + QUnit.module( 'jquery.highlightText', QUnit.newMwEnvironment() ); + + QUnit.test( 'Check', function ( assert ) { + var $fixture, + cases = [ + { + desc: 'Test 001', + text: 'Blue Öyster Cult', + highlight: 'Blue', + expected: '<span class="highlight">Blue</span> Öyster Cult' + }, + { + desc: 'Test 002', + text: 'Blue Öyster Cult', + highlight: 'Blue ', + expected: '<span class="highlight">Blue</span> Öyster Cult' + }, + { + desc: 'Test 003', + text: 'Blue Öyster Cult', + highlight: 'Blue Ö', + expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult' + }, + { + desc: 'Test 004', + text: 'Blue Öyster Cult', + highlight: 'Blue Öy', + expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult' + }, + { + desc: 'Test 005', + text: 'Blue Öyster Cult', + highlight: ' Blue', + expected: '<span class="highlight">Blue</span> Öyster Cult' + }, + { + desc: 'Test 006', + text: 'Blue Öyster Cult', + highlight: ' Blue ', + expected: '<span class="highlight">Blue</span> Öyster Cult' + }, + { + desc: 'Test 007', + text: 'Blue Öyster Cult', + highlight: ' Blue Ö', + expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult' + }, + { + desc: 'Test 008', + text: 'Blue Öyster Cult', + highlight: ' Blue Öy', + expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult' + }, + { + desc: 'Test 009: Highlighter broken on starting Umlaut?', + text: 'Österreich', + highlight: 'Österreich', + expected: '<span class="highlight">Österreich</span>' + }, + { + desc: 'Test 010: Highlighter broken on starting Umlaut?', + text: 'Österreich', + highlight: 'Ö', + expected: '<span class="highlight">Ö</span>sterreich' + }, + { + desc: 'Test 011: Highlighter broken on starting Umlaut?', + text: 'Österreich', + highlight: 'Öst', + expected: '<span class="highlight">Öst</span>erreich' + }, + { + desc: 'Test 012: Highlighter broken on starting Umlaut?', + text: 'Österreich', + highlight: 'Oe', + expected: 'Österreich' + }, + { + desc: 'Test 013: Highlighter broken on punctuation mark?', + text: 'So good. To be there', + highlight: 'good', + expected: 'So <span class="highlight">good</span>. To be there' + }, + { + desc: 'Test 014: Highlighter broken on space?', + text: 'So good. To be there', + highlight: 'be', + expected: 'So good. To <span class="highlight">be</span> there' + }, + { + desc: 'Test 015: Highlighter broken on space?', + text: 'So good. To be there', + highlight: ' be', + expected: 'So good. To <span class="highlight">be</span> there' + }, + { + desc: 'Test 016: Highlighter broken on space?', + text: 'So good. To be there', + highlight: 'be ', + expected: 'So good. To <span class="highlight">be</span> there' + }, + { + desc: 'Test 017: Highlighter broken on space?', + text: 'So good. To be there', + highlight: ' be ', + expected: 'So good. To <span class="highlight">be</span> there' + }, + { + desc: 'Test 018: en de Highlighter broken on special character at the end?', + text: 'So good. xbß', + highlight: 'xbß', + expected: 'So good. <span class="highlight">xbß</span>' + }, + { + desc: 'Test 019: en de Highlighter broken on special character at the end?', + text: 'So good. xbß.', + highlight: 'xbß.', + expected: 'So good. <span class="highlight">xbß.</span>' + }, + { + desc: 'Test 020: RTL he Hebrew', + text: 'חסיד אומות העולם', + highlight: 'חסיד אומות העולם', + expected: '<span class="highlight">חסיד</span> <span class="highlight">אומות</span> <span class="highlight">העולם</span>' + }, + { + desc: 'Test 021: RTL he Hebrew', + text: 'חסיד אומות העולם', + highlight: 'חסי', + expected: '<span class="highlight">חסי</span>ד אומות העולם' + }, + { + desc: 'Test 022: ja Japanese', + text: '諸国民の中の正義の人', + highlight: '諸国民の中の正義の人', + expected: '<span class="highlight">諸国民の中の正義の人</span>' + }, + { + desc: 'Test 023: ja Japanese', + text: '諸国民の中の正義の人', + highlight: '諸国', + expected: '<span class="highlight">諸国</span>民の中の正義の人' + }, + { + desc: 'Test 024: fr French text and « french quotes » (guillemets)', + text: '« L\'oiseau est sur l’île »', + highlight: '« L\'oiseau est sur l’île »', + expected: '<span class="highlight">«</span> <span class="highlight">L\'oiseau</span> <span class="highlight">est</span> <span class="highlight">sur</span> <span class="highlight">l’île</span> <span class="highlight">»</span>' + }, + { + desc: 'Test 025: fr French text and « french quotes » (guillemets)', + text: '« L\'oiseau est sur l’île »', + highlight: '« L\'oise', + expected: '<span class="highlight">«</span> <span class="highlight">L\'oise</span>au est sur l’île »' + }, + { + desc: 'Test 025a: fr French text and « french quotes » (guillemets) - does it match the single strings "«" and "L" separately?', + text: '« L\'oiseau est sur l’île »', + highlight: '« L', + expected: '<span class="highlight">«</span> <span class="highlight">L</span>\'oiseau est sur <span class="highlight">l</span>’île »' + }, + { + desc: 'Test 026: ru Russian', + text: 'Праведники мира', + highlight: 'Праведники мира', + expected: '<span class="highlight">Праведники</span> <span class="highlight">мира</span>' + }, + { + desc: 'Test 027: ru Russian', + text: 'Праведники мира', + highlight: 'Праве', + expected: '<span class="highlight">Праве</span>дники мира' + }, + { + desc: 'Test 028 ka Georgian', + text: 'მთავარი გვერდი', + highlight: 'მთავარი გვერდი', + expected: '<span class="highlight">მთავარი</span> <span class="highlight">გვერდი</span>' + }, + { + desc: 'Test 029 ka Georgian', + text: 'მთავარი გვერდი', + highlight: 'მთა', + expected: '<span class="highlight">მთა</span>ვარი გვერდი' + }, + { + desc: 'Test 030 hy Armenian', + text: 'Նոնա Գափրինդաշվիլի', + highlight: 'Նոնա Գափրինդաշվիլի', + expected: '<span class="highlight">Նոնա</span> <span class="highlight">Գափրինդաշվիլի</span>' + }, + { + desc: 'Test 031 hy Armenian', + text: 'Նոնա Գափրինդաշվիլի', + highlight: 'Նոն', + expected: '<span class="highlight">Նոն</span>ա Գափրինդաշվիլի' + }, + { + desc: 'Test 032: th Thai', + text: 'พอล แอร์ดิช', + highlight: 'พอล แอร์ดิช', + expected: '<span class="highlight">พอล</span> <span class="highlight">แอร์ดิช</span>' + }, + { + desc: 'Test 033: th Thai', + text: 'พอล แอร์ดิช', + highlight: 'พอ', + expected: '<span class="highlight">พอ</span>ล แอร์ดิช' + }, + { + desc: 'Test 034: RTL ar Arabic', + text: 'بول إيردوس', + highlight: 'بول إيردوس', + expected: '<span class="highlight">بول</span> <span class="highlight">إيردوس</span>' + }, + { + desc: 'Test 035: RTL ar Arabic', + text: 'بول إيردوس', + highlight: 'بو', + expected: '<span class="highlight">بو</span>ل إيردوس' + } + ]; + + cases.forEach( function ( item ) { + $fixture = $( '<p>' ).text( item.text ).highlightText( item.highlight ); + assert.equal( + $fixture.html(), + // Re-parse to normalize + $( '<p>' ).html( item.expected ).html(), + item.desc || undefined + ); + } ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js new file mode 100644 index 00000000..7117d1f4 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js @@ -0,0 +1,286 @@ +( function ( $, mw ) { + var simpleSample, U_20AC, poop, mbSample; + + QUnit.module( 'jquery.lengthLimit', QUnit.newMwEnvironment() ); + + // Simple sample (20 chars, 20 bytes) + simpleSample = '12345678901234567890'; + + // 3 bytes (euro-symbol) + U_20AC = '\u20AC'; + + // Outside of the BMP (pile of poo emoji) + poop = '\uD83D\uDCA9'; // "💩" + + // Multi-byte sample (22 chars, 26 bytes) + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; + + // Basic sendkey-implementation + function addChars( $input, charstr ) { + var c, len; + + function x( $input, i ) { + // Add character to the value + return $input.val() + charstr.charAt( i ); + } + + for ( c = 0, len = charstr.length; c < len; c += 1 ) { + $input + .val( x( $input, c ) ) + .trigger( 'change' ); + } + } + + /** + * Test factory for $.fn.byteLimit + * + * @param {Object} options + * @param {string} options.description Test name + * @param {jQuery} options.$input jQuery object in an input element + * @param {string} options.sample Sequence of characters to simulate being + * added one by one + * @param {string} options.expected Expected final value of `$input` + */ + function byteLimitTest( options ) { + var opt = $.extend( { + description: '', + $input: null, + sample: '', + expected: '' + }, options ); + + QUnit.test( opt.description, function ( assert ) { + opt.$input.appendTo( '#qunit-fixture' ); + + // Simulate pressing keys for each of the sample characters + addChars( opt.$input, opt.sample ); + + assert.equal( + opt.$input.val(), + opt.expected, + 'New value matches the expected string' + ); + } ); + } + + byteLimitTest( { + description: 'Plain text input', + $input: $( '<input>' ).attr( 'type', 'text' ), + sample: simpleSample, + expected: simpleSample + } ); + + byteLimitTest( { + description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (T38310)', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit(), + sample: simpleSample, + expected: simpleSample + } ); + + byteLimitTest( { + description: 'Limit using the maxlength attribute', + $input: $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '10' ) + .byteLimit(), + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 10 ), + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value, overriding maxlength attribute', + $input: $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '10' ) + .byteLimit( 15 ), + sample: simpleSample, + expected: '123456789012345' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte)', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 14 ), + sample: mbSample, + expected: '1234567890' + U_20AC + '1' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte, outside BMP)', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 3 ), + sample: poop, + expected: '' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte) overlapping a byte', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 12 ), + sample: mbSample, + expected: '123456789012' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 6, function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Limit using the maxlength attribute and pass a callback as input filter', + $input: $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '6' ) + .byteLimit( function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 6, function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + } ), + sample: 'User:Example', + // The callback alters the value to be used to calculeate + // the length. The altered value is "Exampl" which has + // a length of 6, the "e" would exceed the limit. + expected: 'User:Exampl' + } ); + + byteLimitTest( { + description: 'Input filter that increases the length', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 10, function ( text ) { + return 'prefix' + text; + } ), + sample: simpleSample, + // Prefix adds 6 characters, limit is reached after 4 + expected: '1234' + } ); + + // Regression tests for T43450 + byteLimitTest( { + description: 'Input filter of which the base exceeds the limit', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 3, function ( text ) { + return 'prefix' + text; + } ), + sample: simpleSample, + expected: '' + } ); + + QUnit.test( 'Confirm properties and attributes set', function ( assert ) { + var $el; + + $el = $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit(); + + assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' ); + + $el = $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ); + + assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' ); + + $el = $( '<input>' ).attr( 'type', 'text' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12, function ( val ) { + return val; + } ); + + assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' ); + + $( '<input>' ).attr( 'type', 'text' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '7' ) + .appendTo( '#qunit-fixture' ); + + $( '<input>' ).attr( 'type', 'text' ) + .addClass( 'mw-test-byteLimit-foo' ) + .attr( 'maxlength', '12' ) + .appendTo( '#qunit-fixture' ); + + $el = $( '.mw-test-byteLimit-foo' ); + + assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' ); + + $el.byteLimit(); + } ); + + QUnit.test( 'Trim from insertion when limit exceeded', function ( assert ) { + var $el; + + // Use a new <input> because the bug only occurs on the first time + // the limit it reached (T42850) + $el = $( '<input>' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 3 ) + .val( 'abc' ).trigger( 'change' ) + .val( 'zabc' ).trigger( 'change' ); + + assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 0), not the end' ); + + $el = $( '<input>' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 3 ) + .val( 'abc' ).trigger( 'change' ) + .val( 'azbc' ).trigger( 'change' ); + + assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' ); + } ); + + QUnit.test( 'Do not cut up false matching substrings in emoji insertions', function ( assert ) { + var $el, + oldVal = '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" + newVal = '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" + expected = '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9'; // "💩💹💩" + + // Possible bad results: + // * With no surrogate support: + // '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9' "💩💹🢩" + // * With correct trimming but bad detection of inserted text: + // '\uD83D\uDCA9\uD83D\uDCB9\uDCA9' "💩💹�" + + $el = $( '<input>' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ) + .val( oldVal ).trigger( 'change' ) + .val( newVal ).trigger( 'change' ); + + assert.strictEqual( $el.val(), expected, 'Pasted emoji correctly trimmed at the end' ); + } ); + + byteLimitTest( { + description: 'Unpaired surrogates do not crash', + $input: $( '<input>' ).attr( 'type', 'text' ).byteLimit( 4 ), + sample: '\uD800\uD800\uDFFF', + expected: '\uD800' + } ); + +}( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.localize.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.localize.test.js new file mode 100644 index 00000000..a3e46abe --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.localize.test.js @@ -0,0 +1,135 @@ +( function ( $, mw ) { + QUnit.module( 'jquery.localize', QUnit.newMwEnvironment() ); + + QUnit.test( 'Handle basic replacements', function ( assert ) { + 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' ); + + assert.strictEqual( $lc.text(), 'Basic stuff', 'Tag: html:msg' ); + + // Attribute: title-msg + html = '<div><span title-msg="basic"></span></div>'; + $lc = $( html ).localize().find( 'span' ); + + assert.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' ); + + assert.strictEqual( $lc.attr( 'alt' ), 'Basic stuff', 'Attribute: alt-msg' ); + + // Attribute: placeholder-msg + html = '<div><input placeholder-msg="basic" /></div>'; + $lc = $( html ).localize().find( 'input' ); + + assert.strictEqual( $lc.attr( 'placeholder' ), 'Basic stuff', 'Attribute: placeholder-msg' ); + } ); + + QUnit.test( 'Proper escaping', function ( assert ) { + 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' ); + + assert.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' ); + + assert.strictEqual( $lc.attr( 'title' ), mw.msg( 'properfoo' ), 'Attributes are not inserted raw.' ); + } ); + + QUnit.test( 'Options', function ( assert ) { + var html, $lc, x, sitename = 'Wikipedia'; + 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)' + } ); + + // Message key prefix + html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>'; + $lc = $( html ).localize( { + prefix: 'foo-' + } ).find( 'span' ); + + assert.strictEqual( $lc.attr( 'title' ), 'Lorem', 'Message key prefix - attr' ); + assert.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' ); + + assert.strictEqual( $lc.attr( 'title' ), 'Read more about bars', 'Variable keys mapping - attr' ); + assert.strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' ); + + // Passing parameteters to mw.msg + html = '<div><span><html:msg key="foo-welcome" /></span></div>'; + $lc = $( html ).localize( { + params: { + 'foo-welcome': [ sitename, 'yesterday' ] + } + } ).find( 'span' ); + + assert.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' ); + + assert.strictEqual( $lc.text(), 'The Bazz (Wikipedia)', 'Combination of options prefix, params and keys - text' ); + assert.strictEqual( $lc.attr( 'title' ), 'Read more about bazz at Wikipedia (last modified: 3 minutes ago)', 'Combination of options prefix, params and keys - attr' ); + } ); + + QUnit.test( 'Handle data text', function ( assert ) { + var html, $lc; + mw.messages.set( 'option-one', 'Item 1' ); + mw.messages.set( 'option-two', 'Item 2' ); + html = '<select><option data-msg-text="option-one"></option><option data-msg-text="option-two"></option></select>'; + $lc = $( html ).localize().find( 'option' ); + assert.strictEqual( $lc.eq( 0 ).text(), mw.msg( 'option-one' ), 'data-msg-text becomes text of options' ); + assert.strictEqual( $lc.eq( 1 ).text(), mw.msg( 'option-two' ), 'data-msg-text becomes text of options' ); + } ); + + QUnit.test( 'Handle data html', function ( assert ) { + var html, $lc; + mw.messages.set( 'html', 'behold... there is a <a>link</a> here!!' ); + html = '<div><div data-msg-html="html"></div></div>'; + $lc = $( html ).localize().find( 'a' ); + assert.strictEqual( $lc.length, 1, 'link is created' ); + assert.strictEqual( $lc.text(), 'link', 'the link text got added' ); + } ); +}( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js new file mode 100644 index 00000000..d51dc373 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js @@ -0,0 +1,377 @@ +( function ( $ ) { + var loremIpsum = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.'; + + QUnit.module( 'jquery.makeCollapsible', QUnit.newMwEnvironment() ); + + function prepareCollapsible( html, options ) { + return $( $.parseHTML( html ) ) + .appendTo( '#qunit-fixture' ) + // options might be undefined here - this is okay + .makeCollapsible( options ); + } + + // This test is first because if it fails, then almost all of the latter tests are meaningless. + QUnit.test( 'testing hooks/triggers', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>' + ), + $content = $collapsible.find( '.mw-collapsible-content' ), + $toggle = $collapsible.find( '.mw-collapsible-toggle' ); + + // In one full collapse-expand cycle, each event will be fired once + + // On collapse... + $collapsible.on( 'beforeCollapse.mw-collapsible', function () { + assert.assertTrue( $content.is( ':visible' ), 'first beforeCollapseExpand: content is visible' ); + } ); + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $content.is( ':hidden' ), 'first afterCollapseExpand: content is hidden' ); + + // On expand... + $collapsible.on( 'beforeExpand.mw-collapsible', function () { + assert.assertTrue( $content.is( ':hidden' ), 'second beforeCollapseExpand: content is hidden' ); + } ); + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $content.is( ':visible' ), 'second afterCollapseExpand: content is visible' ); + } ); + + // ...expanding happens here + $toggle.trigger( 'click' ); + } ); + + // ...collapsing happens here + $toggle.trigger( 'click' ); + } ); + + QUnit.test( 'basic operation (<div>)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>' + ), + $content = $collapsible.find( '.mw-collapsible-content' ), + $toggle = $collapsible.find( '.mw-collapsible-toggle' ); + + assert.equal( $content.length, 1, 'content is present' ); + assert.equal( $content.find( $toggle ).length, 0, 'toggle is not a descendant of content' ); + + assert.assertTrue( $content.is( ':visible' ), 'content is visible' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + QUnit.test( 'basic operation (<table>)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<table class="mw-collapsible">' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '</table>' + ), + $headerRow = $collapsible.find( 'tr:first' ), + $contentRow = $collapsible.find( 'tr:last' ), + $toggle = $headerRow.find( 'td:last .mw-collapsible-toggle' ); + + assert.equal( $toggle.length, 1, 'toggle is added to last cell of first row' ); + + assert.assertTrue( $headerRow.is( ':visible' ), 'headerRow is visible' ); + assert.assertTrue( $contentRow.is( ':visible' ), 'contentRow is visible' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $headerRow.is( ':visible' ), 'after collapsing: headerRow is still visible' ); + assert.assertTrue( $contentRow.is( ':hidden' ), 'after collapsing: contentRow is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $headerRow.is( ':visible' ), 'after expanding: headerRow is still visible' ); + assert.assertTrue( $contentRow.is( ':visible' ), 'after expanding: contentRow is visible' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + function tableWithCaptionTest( $collapsible, test, assert ) { + var $caption = $collapsible.find( 'caption' ), + $headerRow = $collapsible.find( 'tr:first' ), + $contentRow = $collapsible.find( 'tr:last' ), + $toggle = $caption.find( '.mw-collapsible-toggle' ); + + assert.equal( $toggle.length, 1, 'toggle is added to the end of the caption' ); + + assert.assertTrue( $caption.is( ':visible' ), 'caption is visible' ); + assert.assertTrue( $headerRow.is( ':visible' ), 'headerRow is visible' ); + assert.assertTrue( $contentRow.is( ':visible' ), 'contentRow is visible' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $caption.is( ':visible' ), 'after collapsing: caption is still visible' ); + assert.assertTrue( $headerRow.is( ':hidden' ), 'after collapsing: headerRow is hidden' ); + assert.assertTrue( $contentRow.is( ':hidden' ), 'after collapsing: contentRow is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $caption.is( ':visible' ), 'after expanding: caption is still visible' ); + assert.assertTrue( $headerRow.is( ':visible' ), 'after expanding: headerRow is visible' ); + assert.assertTrue( $contentRow.is( ':visible' ), 'after expanding: contentRow is visible' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + $toggle.trigger( 'click' ); + } + + QUnit.test( 'basic operation (<table> with caption)', function ( assert ) { + tableWithCaptionTest( prepareCollapsible( + '<table class="mw-collapsible">' + + '<caption>' + loremIpsum + '</caption>' + + '<tr><th>' + loremIpsum + '</th><th>' + loremIpsum + '</th></tr>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '</table>' + ), this, assert ); + } ); + + QUnit.test( 'basic operation (<table> with caption and <thead>)', function ( assert ) { + tableWithCaptionTest( prepareCollapsible( + '<table class="mw-collapsible">' + + '<caption>' + loremIpsum + '</caption>' + + '<thead><tr><th>' + loremIpsum + '</th><th>' + loremIpsum + '</th></tr></thead>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' + + '</table>' + ), this, assert ); + } ); + + function listTest( listType, test, assert ) { + var $collapsible = prepareCollapsible( + '<' + listType + ' class="mw-collapsible">' + + '<li>' + loremIpsum + '</li>' + + '<li>' + loremIpsum + '</li>' + + '</' + listType + '>' + ), + $toggleItem = $collapsible.find( 'li.mw-collapsible-toggle-li:first-child' ), + $contentItem = $collapsible.find( 'li:last' ), + $toggle = $toggleItem.find( '.mw-collapsible-toggle' ); + + assert.equal( $toggle.length, 1, 'toggle is present, added inside new zeroth list item' ); + + assert.assertTrue( $toggleItem.is( ':visible' ), 'toggleItem is visible' ); + assert.assertTrue( $contentItem.is( ':visible' ), 'contentItem is visible' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $toggleItem.is( ':visible' ), 'after collapsing: toggleItem is still visible' ); + assert.assertTrue( $contentItem.is( ':hidden' ), 'after collapsing: contentItem is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $toggleItem.is( ':visible' ), 'after expanding: toggleItem is still visible' ); + assert.assertTrue( $contentItem.is( ':visible' ), 'after expanding: contentItem is visible' ); + } ); + + $toggle.trigger( 'click' ); + } ); + + $toggle.trigger( 'click' ); + } + + QUnit.test( 'basic operation (<ul>)', function ( assert ) { + listTest( 'ul', this, assert ); + } ); + + QUnit.test( 'basic operation (<ol>)', function ( assert ) { + listTest( 'ol', this, assert ); + } ); + + QUnit.test( 'basic operation when synchronous (options.instantHide)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>', + { instantHide: true } + ), + $content = $collapsible.find( '.mw-collapsible-content' ); + + assert.assertTrue( $content.is( ':visible' ), 'content is visible' ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + + assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' ); + } ); + + QUnit.test( 'mw-made-collapsible data added', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div>' + loremIpsum + '</div>' + ); + + assert.equal( $collapsible.data( 'mw-made-collapsible' ), true, 'mw-made-collapsible data present' ); + } ); + + QUnit.test( 'mw-collapsible added when missing', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div>' + loremIpsum + '</div>' + ); + + assert.assertTrue( $collapsible.hasClass( 'mw-collapsible' ), 'mw-collapsible class present' ); + } ); + + QUnit.test( 'mw-collapsed added when missing', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div>' + loremIpsum + '</div>', + { collapsed: true } + ); + + assert.assertTrue( $collapsible.hasClass( 'mw-collapsed' ), 'mw-collapsed class present' ); + } ); + + QUnit.test( 'initial collapse (mw-collapsed class)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible mw-collapsed">' + loremIpsum + '</div>' + ), + $content = $collapsible.find( '.mw-collapsible-content' ); + + // Synchronous - mw-collapsed should cause instantHide: true to be used on initial collapsing + assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + QUnit.test( 'initial collapse (options.collapsed)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>', + { collapsed: true } + ), + $content = $collapsible.find( '.mw-collapsible-content' ); + + // Synchronous - collapsed: true should cause instantHide: true to be used on initial collapsing + assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + QUnit.test( 'clicks on links inside toggler pass through', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + + '<div class="mw-collapsible-toggle">' + + 'Toggle <a href="#top">toggle</a> toggle <b>toggle</b>' + + '</div>' + + '<div class="mw-collapsible-content">' + loremIpsum + '</div>' + + '</div>', + // Can't do asynchronous because we're testing that the event *doesn't* happen + { instantHide: true } + ), + $content = $collapsible.find( '.mw-collapsible-content' ); + + $collapsible.find( '.mw-collapsible-toggle a' ).trigger( 'click' ); + assert.assertTrue( $content.is( ':visible' ), 'click event on link inside toggle passes through (content not toggled)' ); + + $collapsible.find( '.mw-collapsible-toggle b' ).trigger( 'click' ); + assert.assertTrue( $content.is( ':hidden' ), 'click event on non-link inside toggle toggles content' ); + } ); + + QUnit.test( 'click on non-link inside toggler counts as trigger', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + + '<div class="mw-collapsible-toggle">' + + 'Toggle <a>toggle</a> toggle <b>toggle</b>' + + '</div>' + + '<div class="mw-collapsible-content">' + loremIpsum + '</div>' + + '</div>', + { instantHide: true } + ), + $content = $collapsible.find( '.mw-collapsible-content' ); + + $collapsible.find( '.mw-collapsible-toggle a' ).trigger( 'click' ); + assert.assertTrue( $content.is( ':hidden' ), 'click event on link (with no href) inside toggle toggles content' ); + } ); + + QUnit.test( 'collapse/expand text (data-collapsetext, data-expandtext)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible" data-collapsetext="Collapse me!" data-expandtext="Expand me!">' + + loremIpsum + + '</div>' + ), + $toggleText = $collapsible.find( '.mw-collapsible-text' ); + + assert.equal( $toggleText.text(), 'Collapse me!', 'data-collapsetext is respected' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.equal( $toggleText.text(), 'Expand me!', 'data-expandtext is respected' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + QUnit.test( 'collapse/expand text (options.collapseText, options.expandText)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>', + { collapseText: 'Collapse me!', expandText: 'Expand me!' } + ), + $toggleText = $collapsible.find( '.mw-collapsible-text' ); + + assert.equal( $toggleText.text(), 'Collapse me!', 'options.collapseText is respected' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.equal( $toggleText.text(), 'Expand me!', 'options.expandText is respected' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + QUnit.test( 'predefined toggle button and text (.mw-collapsible-toggle/.mw-collapsible-text)', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + + '<div class="mw-collapsible-toggle">' + + '<span>[</span><span class="mw-collapsible-text">Toggle</span><span>]</span>' + + '</div>' + + '<div class="mw-collapsible-content">' + loremIpsum + '</div>' + + '</div>', + { collapseText: 'Hide', expandText: 'Show' } + ), + $toggleText = $collapsible.find( '.mw-collapsible-text' ); + + assert.equal( $toggleText.text(), 'Toggle', 'predefined text remains' ); + + $collapsible.on( 'afterCollapse.mw-collapsible', function () { + assert.equal( $toggleText.text(), 'Show', 'predefined text is toggled' ); + + $collapsible.on( 'afterExpand.mw-collapsible', function () { + assert.equal( $toggleText.text(), 'Hide', 'predefined text is toggled back' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' ); + } ); + + QUnit.test( 'cloned collapsibles can be made collapsible again', function ( assert ) { + var $collapsible = prepareCollapsible( + '<div class="mw-collapsible">' + loremIpsum + '</div>' + ), + $clone = $collapsible.clone() // clone without data and events + .appendTo( '#qunit-fixture' ).makeCollapsible(), + $content = $clone.find( '.mw-collapsible-content' ); + + assert.assertTrue( $content.is( ':visible' ), 'content is visible' ); + + $clone.on( 'afterCollapse.mw-collapsible', function () { + assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' ); + } ); + + $clone.find( '.mw-collapsible-toggle a' ).trigger( 'click' ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js new file mode 100644 index 00000000..ec3539b9 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js @@ -0,0 +1,35 @@ +( function ( $ ) { + QUnit.module( 'jquery.tabIndex', QUnit.newMwEnvironment() ); + + QUnit.test( 'firstTabIndex', function ( assert ) { + var html, $testA, $testB; + html = '<form>' + + '<input tabindex="7" />' + + '<input tabindex="9" />' + + '<textarea tabindex="2">Foobar</textarea>' + + '<textarea tabindex="5">Foobar</textarea>' + + '</form>'; + + $testA = $( '<div>' ).html( html ).appendTo( '#qunit-fixture' ); + assert.strictEqual( $testA.firstTabIndex(), 2, 'First tabindex should be 2 within this context.' ); + + $testB = $( '<div>' ); + assert.strictEqual( $testB.firstTabIndex(), null, 'Return null if none available.' ); + } ); + + QUnit.test( 'lastTabIndex', function ( assert ) { + var html, $testA, $testB; + html = '<form>' + + '<input tabindex="7" />' + + '<input tabindex="9" />' + + '<textarea tabindex="2">Foobar</textarea>' + + '<textarea tabindex="5">Foobar</textarea>' + + '</form>'; + + $testA = $( '<div>' ).html( html ).appendTo( '#qunit-fixture' ); + assert.strictEqual( $testA.lastTabIndex(), 9, 'Last tabindex should be 9 within this context.' ); + + $testB = $( '<div>' ); + assert.strictEqual( $testB.lastTabIndex(), null, 'Return null if none available.' ); + } ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js new file mode 100644 index 00000000..2865cbba --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js @@ -0,0 +1,267 @@ +( function ( $, mw ) { + /** + * This module tests the input/output capabilities of the parsers of tablesorter. + * It does not test actual sorting. + */ + + var text, ipv4, + simpleMDYDatesInMDY, simpleMDYDatesInDMY, oldMDYDates, complexMDYDates, clobberedDates, MYDates, YDates, ISODates, + currencyData, transformedCurrencyData; + + QUnit.module( 'jquery.tablesorter.parsers', QUnit.newMwEnvironment( { + setup: function () { + this.liveMonths = mw.language.months; + mw.language.months = { + keys: { + names: [ 'january', 'february', 'march', 'april', 'may_long', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' ], + genitive: [ 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen', + 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen', 'december-gen' ], + abbrev: [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ] + }, + names: [ 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ], + genitive: [ 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ], + abbrev: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] + }; + }, + teardown: function () { + mw.language.months = this.liveMonths; + }, + config: { + wgPageContentLanguage: 'en', + /* default date format of the content language */ + wgDefaultDateFormat: 'dmy', + /* These two are important for numeric interpretations */ + wgSeparatorTransformTable: [ '', '' ], + wgDigitTransformTable: [ '', '' ] + } + } ) ); + + /** + * For a value, check if the parser recognizes it and how it transforms it + * + * @param {string} msg text to pass on to qunit describing the test case + * @param {string[]} parserId of the parser that will be tested + * @param {string[][]} data Array of testcases. Each testcase, array of + * inputValue: The string value that we want to test the parser for + * recognized: If we expect that this value's type is detectable by the parser + * outputValue: The value the parser has converted the input to + * msg: describing the testcase + * @param {function($table)} callback something to do before we start the testcase + */ + function parserTest( msg, parserId, data, callback ) { + QUnit.test( msg, function ( assert ) { + var extractedR, extractedF, parser; + + if ( callback !== undefined ) { + callback(); + } + + parser = $.tablesorter.getParser( parserId ); + data.forEach( function ( testcase ) { + extractedR = parser.is( testcase[ 0 ] ); + extractedF = parser.format( testcase[ 0 ] ); + + assert.strictEqual( extractedR, testcase[ 1 ], 'Detect: ' + testcase[ 3 ] ); + assert.strictEqual( extractedF, testcase[ 2 ], 'Sortkey: ' + testcase[ 3 ] ); + } ); + + } ); + } + + text = [ + [ 'Mars', true, 'mars', 'Simple text' ], + [ 'Mẘas', true, 'mẘas', 'Non ascii character' ], + [ 'A sentence', true, 'a sentence', 'A sentence with space chars' ] + ]; + parserTest( 'Textual keys', 'text', text ); + + ipv4 = [ + // Some randomly generated fake IPs + [ '0.0.0.0', true, 0, 'An IP address' ], + [ '255.255.255.255', true, 255255255255, 'An IP address' ], + [ '45.238.27.109', true, 45238027109, 'An IP address' ], + [ '1.238.27.1', true, 1238027001, 'An IP address with small numbers' ], + [ '238.27.1', false, 238027001, 'A malformed IP Address' ], + [ '1', false, 1, 'A super malformed IP Address' ], + [ 'Just text', false, -Infinity, 'A line with just text' ], + [ '45.238.27.109Postfix', false, 45238027109, 'An IP address with a connected postfix' ], + [ '45.238.27.109 postfix', false, 45238027109, 'An IP address with a seperated postfix' ] + ]; + parserTest( 'IPv4', 'IPAddress', ipv4 ); + + simpleMDYDatesInMDY = [ + [ 'January 17, 2010', true, 20100117, 'Long middle endian date' ], + [ 'Jan 17, 2010', true, 20100117, 'Short middle endian date' ], + [ '1/17/2010', true, 20100117, 'Numeric middle endian date' ], + [ '01/17/2010', true, 20100117, 'Numeric middle endian date with padding on month' ], + [ '01/07/2010', true, 20100107, 'Numeric middle endian date with padding on day' ], + [ '01/07/0010', true, 20100107, 'Numeric middle endian date with padding on year' ], + [ '5.12.1990', true, 19900512, 'Numeric middle endian date with . separator' ] + ]; + parserTest( 'MDY Dates using mdy content language', 'date', simpleMDYDatesInMDY ); + + simpleMDYDatesInDMY = [ + [ 'January 17, 2010', true, 20100117, 'Long middle endian date' ], + [ 'Jan 17, 2010', true, 20100117, 'Short middle endian date' ], + [ '1/17/2010', true, 20101701, 'Numeric middle endian date' ], + [ '01/17/2010', true, 20101701, 'Numeric middle endian date with padding on month' ], + [ '01/07/2010', true, 20100701, 'Numeric middle endian date with padding on day' ], + [ '01/07/0010', true, 20100701, 'Numeric middle endian date with padding on year' ], + [ '5.12.1990', true, 19901205, 'Numeric middle endian date with . separator' ] + ]; + parserTest( 'MDY Dates using dmy content language', 'date', simpleMDYDatesInDMY, function () { + mw.config.set( { + wgDefaultDateFormat: 'dmy', + wgPageContentLanguage: 'de' + } ); + } ); + + oldMDYDates = [ + [ 'January 19, 1400 BC', false, '99999999', 'BC' ], + [ 'January 19, 1400BC', false, '99999999', 'Connected BC' ], + [ 'January, 19 1400 B.C.', false, '99999999', 'B.C.' ], + [ 'January 19, 1400 AD', false, '99999999', 'AD' ], + [ 'January, 19 10', true, 20100119, 'AD' ], + [ 'January, 19 1', false, '99999999', 'AD' ] + ]; + parserTest( 'Very old MDY dates', 'date', oldMDYDates ); + + complexMDYDates = [ + [ 'January, 19 2010', true, 20100119, 'Comma after month' ], + [ 'January 19, 2010', true, 20100119, 'Comma after day' ], + [ 'January/19/2010', true, 20100119, 'Forward slash separator' ], + [ '04 22 1991', true, 19910422, 'Month with 0 padding' ], + [ 'April 21 1991', true, 19910421, 'Space separation' ], + [ '04 22 1991', true, 19910422, 'Month with 0 padding' ], + [ 'December 12 \'10', true, 20101212, '' ], + [ 'Dec 12 \'10', true, 20101212, '' ], + [ 'Dec. 12 \'10', true, 20101212, '' ] + ]; + parserTest( 'MDY Dates', 'date', complexMDYDates ); + + clobberedDates = [ + [ 'January, 19 2010 - January, 20 2010', false, '99999999', 'Date range with hyphen' ], + [ 'January, 19 2010 — January, 20 2010', false, '99999999', 'Date range with mdash' ], + [ 'prefixJanuary, 19 2010', false, '99999999', 'Connected prefix' ], + [ 'prefix January, 19 2010', false, '99999999', 'Prefix' ], + [ 'December 12 2010postfix', false, '99999999', 'ConnectedPostfix' ], + [ 'December 12 2010 postfix', false, '99999999', 'Postfix' ], + [ 'A simple text', false, '99999999', 'Plain text in date sort' ], + [ '04l22l1991', false, '99999999', 'l char as separator' ], + [ 'January\\19\\2010', false, '99999999', 'backslash as date separator' ] + ]; + parserTest( 'Clobbered Dates', 'date', clobberedDates ); + + MYDates = [ + [ 'December 2010', false, '99999999', 'Plain month year' ], + [ 'Dec 2010', false, '99999999', 'Abreviated month year' ], + [ '12 2010', false, '99999999', 'Numeric month year' ] + ]; + parserTest( 'MY Dates', 'date', MYDates ); + + YDates = [ + [ '2010', false, '99999999', 'Plain 4-digit year' ], + [ '876', false, '99999999', '3-digit year' ], + [ '76', false, '99999999', '2-digit year' ], + [ '\'76', false, '99999999', '2-digit millenium bug year' ], + [ '2010 BC', false, '99999999', '4-digit year BC' ] + ]; + parserTest( 'Y Dates', 'date', YDates ); + + ISODates = [ + [ '', false, -Infinity, 'Not a date' ], + [ '2000', false, 946684800000, 'Plain 4-digit year' ], + [ '2000-01', true, 946684800000, 'Year with month' ], + [ '2000-01-01', true, 946684800000, 'Year with month and day' ], + [ '2000-13-01', false, 978307200000, 'Non existant month' ], + [ '2000-01-32', true, 949363200000, 'Non existant day' ], + [ '2000-01-01T12:30:30', true, 946729830000, 'Date with a time' ], + [ '2000-01-01T12:30:30Z', true, 946729830000, 'Date with a UTC+0 time' ], + [ '2000-01-01T24:30:30Z', true, 946773030000, 'Date with invalid hours' ], + [ '2000-01-01T12:60:30Z', true, 946728000000, 'Date with invalid minutes' ], + [ '2000-01-01T12:30:61Z', true, 946729800000, 'Date with invalid amount of seconds, drops seconds' ], + [ '2000-01-01T23:59:59Z', true, 946771199000, 'Edges of time' ], + [ '2000-01-01T12:30:30.111Z', true, 946729830111, 'Date with milliseconds' ], + [ '2000-01-01T12:30:30.11111Z', true, 946729830111, 'Date with too high precision' ], + [ '2000-01-01T12:30:30,111Z', true, 946729830111, 'Date with milliseconds and , separator' ], + [ '2000-01-01T12:30:30+01:00', true, 946726230000, 'Date time in UTC+1' ], + [ '2000-01-01T12:30:30+01:30', true, 946724430000, 'Date time in UTC+1:30' ], + [ '2000-01-01T12:30:30-01:00', true, 946733430000, 'Date time in UTC-1' ], + [ '2000-01-01T12:30:30-01:30', true, 946735230000, 'Date time in UTC-1:30' ], + [ '2000-01-01T12:30:30.111+01:00', true, 946726230111, 'Date time and milliseconds in UTC+1' ], + [ '2000-01-01Postfix', true, 946684800000, 'Date with appended postfix' ], + [ '2000-01-01 Postfix', true, 946684800000, 'Date with separate postfix' ], + [ '2 Postfix', false, -62104060800000, 'One digit with separate postfix' ], + [ 'ca. 2', false, -62104060800000, 'Three digit with separate prefix' ], + [ '~200', false, -55855785600000, 'Three digit with appended prefix' ], + [ 'ca. 200[1]', false, -55855785600000, 'Three digit with separate prefix and postfix' ], + [ '2000-11-31', true, 975628800000, '31 days in 30 day month' ], + [ '50-01-01', true, -60589296000000, 'Year with just two digits' ], + [ '2', false, -62104060800000, 'Year with one digit' ], + [ '02-01', true, -62104060800000, 'Year with one digit and leading zero' ], + [ ' 2-01', true, -62104060800000, 'Year with one digit and leading space' ], + [ '-2-10', true, -62206704000000, 'Year BC with month' ], + [ '-9999', false, -377705116800000, 'max. Year BC' ], + [ '+9999-12', true, 253399622400000, 'max. Date with +sign' ], + [ '2000-01-01 12:30:30Z', true, 946729830000, 'Date and time with no T marker' ], + [ '2000-01-01T12:30:60Z', true, 946729860000, 'Date with leap second' ], + [ '2000-01-01T12:30:30-23:59', true, 946816170000, 'Date time in UTC-23:59' ], + [ '2000-01-01T12:30:30+23:59', true, 946643490000, 'Date time in UTC+23:59' ], + [ '2000-01-01T123030+0100', true, 946726230000, 'Time without separators' ], + [ '20000101T123030+0100', false, 946726230000, 'All without separators' ] + ]; + parserTest( 'ISO Dates', 'isoDate', ISODates ); + + currencyData = [ + [ '1.02 $', true, 1.02, '' ], + [ '$ 3.00', true, 3, '' ], + [ '€ 2,99', true, 299, '' ], + [ '$ 1.00', true, 1, '' ], + [ '$3.50', true, 3.50, '' ], + [ '$ 1.50', true, 1.50, '' ], + [ '€ 0.99', true, 0.99, '' ], + [ '$ 299.99', true, 299.99, '' ], + [ '$ 2,299.99', true, 2299.99, '' ], + [ '$ 2,989', true, 2989, '' ], + [ '$ 2 299.99', true, 2299.99, '' ], + [ '$ 2 989', true, 2989, '' ], + [ '$ 2.989', true, 2.989, '' ] + ]; + parserTest( 'Currency', 'currency', currencyData ); + + transformedCurrencyData = [ + [ '1.02 $', true, 102, '' ], + [ '$ 3.00', true, 300, '' ], + [ '€ 2,99', true, 2.99, '' ], + [ '$ 1.00', true, 100, '' ], + [ '$3.50', true, 350, '' ], + [ '$ 1.50', true, 150, '' ], + [ '€ 0.99', true, 99, '' ], + [ '$ 299.99', true, 29999, '' ], + [ '$ 2\'299,99', true, 2299.99, '' ], + [ '$ 2,989', true, 2.989, '' ], + [ '$ 2 299.99', true, 229999, '' ], + [ '2 989 $', true, 2989, '' ], + [ '299.99 $', true, 29999, '' ], + [ '2\'299,99 $', true, 2299.99, '' ], + [ '2,989 $', true, 2.989, '' ], + [ '2 299.99 $', true, 229999, '' ], + [ '2 989 $', true, 2989, '' ] + ]; + parserTest( 'Currency with european separators', 'currency', transformedCurrencyData, function () { + mw.config.set( { + // We expect 22'234.444,22 + // Map from ascii separators => localized separators + wgSeparatorTransformTable: [ ', . ,', '\' , .' ], + wgDigitTransformTable: [ '', '' ] + } ); + } ); + + // TODO add numbers sorting tests for T10115 with a different language + +}( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js new file mode 100644 index 00000000..23ef26f6 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -0,0 +1,1498 @@ +( function ( $, mw ) { + var header = [ 'Planet', 'Radius (km)' ], + + // Data set "planets" + mercury = [ 'Mercury', '2439.7' ], + venus = [ 'Venus', '6051.8' ], + earth = [ 'Earth', '6371.0' ], + mars = [ 'Mars', '3390.0' ], + jupiter = [ 'Jupiter', '69911' ], + saturn = [ 'Saturn', '58232' ], + planets = [ mercury, venus, earth, mars, jupiter, saturn ], + planetsAscName = [ earth, jupiter, mars, mercury, saturn, venus ], + planetsAscRadius = [ mercury, mars, venus, earth, saturn, jupiter ], + planetsRowspan, + planetsRowspanII, + planetsAscNameLegacy, + + // Data set "simple" + a1 = [ 'A', '1' ], + a2 = [ 'A', '2' ], + a3 = [ 'A', '3' ], + b1 = [ 'B', '1' ], + b2 = [ 'B', '2' ], + b3 = [ 'B', '3' ], + simple = [ a2, b3, a1, a3, b2, b1 ], + simpleAsc = [ a1, a2, a3, b1, b2, b3 ], + simpleDescasc = [ b1, b2, b3, a1, a2, a3 ], + + // Data set "colspan" + header4 = [ 'column1a', 'column1b', 'column1c', 'column2' ], + aaa1 = [ 'A', 'A', 'A', '1' ], + aab5 = [ 'A', 'A', 'B', '5' ], + abc3 = [ 'A', 'B', 'C', '3' ], + bbc2 = [ 'B', 'B', 'C', '2' ], + caa4 = [ 'C', 'A', 'A', '4' ], + colspanInitial = [ aab5, aaa1, abc3, bbc2, caa4 ], + + // Data set "ipv4" + 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' ] + ], + 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' ] + ], + + // Data set "umlaut" + umlautWords = [ + [ 'Günther' ], + [ 'Peter' ], + [ 'Björn' ], + [ 'Bjorn' ], + [ 'Apfel' ], + [ 'Äpfel' ], + [ 'Strasse' ], + [ 'Sträßschen' ] + ], + umlautWordsSorted = [ + [ 'Äpfel' ], + [ 'Apfel' ], + [ 'Björn' ], + [ 'Bjorn' ], + [ 'Günther' ], + [ 'Peter' ], + [ 'Sträßschen' ], + [ 'Strasse' ] + ], + + // Data set "digraph" + digraphWords = [ + [ 'London' ], + [ 'Ljubljana' ], + [ 'Luxembourg' ], + [ 'Njivice' ], + [ 'Norwich' ], + [ 'New York' ] + ], + digraphWordsSorted = [ + [ 'London' ], + [ 'Luxembourg' ], + [ 'Ljubljana' ], + [ 'New York' ], + [ 'Norwich' ], + [ 'Njivice' ] + ], + + complexMDYDates = [ + [ 'January, 19 2010' ], + [ 'April 21 1991' ], + [ '04 22 1991' ], + [ '5.12.1990' ], + [ 'December 12 \'10' ] + ], + complexMDYSorted = [ + [ '5.12.1990' ], + [ 'April 21 1991' ], + [ '04 22 1991' ], + [ 'January, 19 2010' ], + [ 'December 12 \'10' ] + ], + + currencyUnsorted = [ + [ '1.02 $' ], + [ '$ 3.00' ], + [ '€ 2,99' ], + [ '$ 1.00' ], + [ '$3.50' ], + [ '$ 1.50' ], + [ '€ 0.99' ] + ], + currencySorted = [ + [ '€ 0.99' ], + [ '$ 1.00' ], + [ '1.02 $' ], + [ '$ 1.50' ], + [ '$ 3.00' ], + [ '$3.50' ], + // Commas sort after dots + // Not intentional but test to detect changes + [ '€ 2,99' ] + ], + + numbers = [ + [ '12' ], + [ '7' ], + [ '13,000' ], + [ '9' ], + [ '14' ], + [ '8.0' ] + ], + numbersAsc = [ + [ '7' ], + [ '8.0' ], + [ '9' ], + [ '12' ], + [ '14' ], + [ '13,000' ] + ], + + correctDateSorting1 = [ + [ '01 January 2010' ], + [ '05 February 2010' ], + [ '16 January 2010' ] + ], + correctDateSortingSorted1 = [ + [ '01 January 2010' ], + [ '16 January 2010' ], + [ '05 February 2010' ] + ], + + correctDateSorting2 = [ + [ 'January 01 2010' ], + [ 'February 05 2010' ], + [ 'January 16 2010' ] + ], + correctDateSortingSorted2 = [ + [ 'January 01 2010' ], + [ 'January 16 2010' ], + [ 'February 05 2010' ] + ], + isoDateSorting = [ + [ '2010-02-01' ], + [ '2009-12-25T12:30:45.001Z' ], + [ '2010-01-31' ], + [ '2009' ], + [ '2009-12-25T12:30:45' ], + [ '2009-12-25T12:30:45.111' ], + [ '2009-12-25T12:30:45+01:00' ] + ], + isoDateSortingSorted = [ + [ '2009' ], + [ '2009-12-25T12:30:45+01:00' ], + [ '2009-12-25T12:30:45' ], + [ '2009-12-25T12:30:45.001Z' ], + [ '2009-12-25T12:30:45.111' ], + [ '2010-01-31' ], + [ '2010-02-01' ] + ]; + + QUnit.module( 'jquery.tablesorter', QUnit.newMwEnvironment( { + setup: function () { + this.liveMonths = mw.language.months; + mw.language.months = { + keys: { + names: [ 'january', 'february', 'march', 'april', 'may_long', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' ], + genitive: [ 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen', + 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen', 'december-gen' ], + abbrev: [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ] + }, + names: [ 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ], + genitive: [ 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ], + abbrev: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] + }; + }, + teardown: function () { + mw.language.months = this.liveMonths; + }, + config: { + wgDefaultDateFormat: 'dmy', + wgSeparatorTransformTable: [ '', '' ], + wgDigitTransformTable: [ '', '' ], + wgPageContentLanguage: 'en' + } + } ) ); + + /** + * 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} + */ + function tableCreate( header, data ) { + var i, + $table = $( '<table class="sortable"><thead></thead><tbody></tbody></table>' ), + $thead = $table.find( 'thead' ), + $tbody = $table.find( 'tbody' ), + $tr = $( '<tr>' ); + + header.forEach( function ( str ) { + var $th = $( '<th>' ); + $th.text( str ).appendTo( $tr ); + } ); + $tr.appendTo( $thead ); + + for ( i = 0; i < data.length; i++ ) { + $tr = $( '<tr>' ); + // eslint-disable-next-line no-loop-func + data[ i ].forEach( function ( str ) { + var $td = $( '<td>' ); + $td.text( str ).appendTo( $tr ); + } ); + $tr.appendTo( $tbody ); + } + return $table; + } + + /** + * Extract text from table. + * + * @param {jQuery} $table + * @return {string[][]} + */ + function tableExtract( $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 + */ + function tableTest( msg, header, data, expected, callback ) { + QUnit.test( msg, function ( assert ) { + var extracted, + $table = tableCreate( header, data ); + + // 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. + extracted = tableExtract( $table ); + assert.deepEqual( extracted, expected, msg ); + } ); + } + + /** + * Run a table test by building a table with the given HTML, + * running some callback on it, then checking the results. + * + * @param {string} msg text to pass on to qunit for the comparison + * @param {string} html HTML 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 + */ + function tableTestHTML( msg, html, expected, callback ) { + QUnit.test( msg, function ( assert ) { + var extracted, + $table = $( html ); + + // Give caller a chance to set up sorting and manipulate the table. + if ( callback ) { + callback( $table ); + } else { + $table.tablesorter(); + $table.find( '#sortme' ).click(); + } + + // Table sorting is done synchronously; if it ever needs to change back + // to asynchronous, we'll need a timeout or a callback here. + extracted = tableExtract( $table ); + assert.deepEqual( extracted, expected, msg ); + } ); + } + + function reversed( arr ) { + // Clone array + var arr2 = arr.slice( 0 ); + + arr2.reverse(); + + return arr2; + } + + // Sample data set using planets named and their radius + + tableTest( + 'Basic planet table: sorting initially - ascending by name', + header, + planets, + planetsAscName, + function ( $table ) { + $table.tablesorter( { sortList: [ + { 0: 'asc' } + ] } ); + } + ); + tableTest( + 'Basic planet table: sorting initially - descending by radius', + header, + planets, + reversed( planetsAscRadius ), + function ( $table ) { + $table.tablesorter( { sortList: [ + { 1: 'desc' } + ] } ); + } + ); + tableTest( + 'Basic planet table: ascending by name', + header, + planets, + planetsAscName, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'Basic planet table: ascending by name a second time', + header, + planets, + planetsAscName, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'Basic planet table: ascending by name (multiple clicks)', + header, + planets, + planetsAscName, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + $table.find( '.headerSort:eq(1)' ).click(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'Basic planet table: descending by name', + header, + planets, + reversed( planetsAscName ), + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click().click(); + } + ); + tableTest( + 'Basic planet table: ascending radius', + header, + planets, + planetsAscRadius, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(1)' ).click(); + } + ); + tableTest( + 'Basic planet table: descending radius', + header, + planets, + reversed( planetsAscRadius ), + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(1)' ).click().click(); + } + ); + tableTest( + 'Sorting multiple columns by passing sort list', + header, + simple, + simpleAsc, + function ( $table ) { + $table.tablesorter( + { sortList: [ + { 0: 'asc' }, + { 1: 'asc' } + ] } + ); + } + ); + tableTest( + 'Sorting multiple columns by programmatically triggering sort()', + header, + simple, + simpleDescasc, + function ( $table ) { + $table.tablesorter(); + $table.data( 'tablesorter' ).sort( + [ + { 0: 'desc' }, + { 1: 'asc' } + ] + ); + } + ); + tableTest( + 'Reset to initial sorting by triggering sort() without any parameters', + header, + simple, + simpleAsc, + function ( $table ) { + $table.tablesorter( + { sortList: [ + { 0: 'asc' }, + { 1: 'asc' } + ] } + ); + $table.data( 'tablesorter' ).sort( + [ + { 0: 'desc' }, + { 1: 'asc' } + ] + ); + $table.data( 'tablesorter' ).sort(); + } + ); + tableTest( + 'Sort via click event after having initialized the tablesorter with initial sorting', + header, + simple, + simpleDescasc, + function ( $table ) { + $table.tablesorter( + { sortList: [ { 0: 'asc' }, { 1: 'asc' } ] } + ); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'Multi-sort via click event after having initialized the tablesorter with initial sorting', + header, + simple, + simpleAsc, + function ( $table ) { + var event; + $table.tablesorter( + { sortList: [ { 0: 'desc' }, { 1: 'desc' } ] } + ); + $table.find( '.headerSort:eq(0)' ).click(); + + // Pretend to click while pressing the multi-sort key + event = $.Event( 'click' ); + event[ $table.data( 'tablesorter' ).config.sortMultiSortKey ] = true; + $table.find( '.headerSort:eq(1)' ).trigger( event ); + } + ); + QUnit.test( 'Reset sorting making table appear unsorted', function ( assert ) { + var $table = tableCreate( header, simple ); + $table.tablesorter( + { sortList: [ + { 0: 'desc' }, + { 1: 'asc' } + ] } + ); + $table.data( 'tablesorter' ).sort( [] ); + + assert.equal( + $table.find( 'th.headerSortUp' ).length + $table.find( 'th.headerSortDown' ).length, + 0, + 'No sort specific sort classes addign to header cells' + ); + + assert.equal( + $table.find( 'th' ).first().attr( 'title' ), + mw.msg( 'sort-ascending' ), + 'First header cell has default title' + ); + + assert.equal( + $table.find( 'th' ).first().attr( 'title' ), + $table.find( 'th' ).last().attr( 'title' ), + 'Both header cells\' titles match' + ); + } ); + + // Sorting with colspans + + tableTest( 'Sorting with colspanned headers: spanned column', + header4, + colspanInitial, + [ aaa1, aab5, abc3, bbc2, caa4 ], + function ( $table ) { + // Make colspanned header for test + $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove(); + $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( 'Sorting with colspanned headers: sort spanned column twice', + header4, + colspanInitial, + [ caa4, bbc2, abc3, aab5, aaa1 ], + function ( $table ) { + // Make colspanned header for test + $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove(); + $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( 'Sorting with colspanned headers: subsequent column', + header4, + colspanInitial, + [ aaa1, bbc2, abc3, caa4, aab5 ], + function ( $table ) { + // Make colspanned header for test + $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove(); + $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(1)' ).click(); + } + ); + tableTest( 'Sorting with colspanned headers: sort subsequent column twice', + header4, + colspanInitial, + [ aab5, caa4, abc3, bbc2, aaa1 ], + function ( $table ) { + // Make colspanned header for test + $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove(); + $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(1)' ).click(); + $table.find( '.headerSort:eq(1)' ).click(); + } + ); + + QUnit.test( 'Basic planet table: one unsortable column', function ( assert ) { + var $table = tableCreate( header, planets ), + $cell; + $table.find( 'tr:eq(0) > th:eq(0)' ).addClass( 'unsortable' ); + + $table.tablesorter(); + $table.find( 'tr:eq(0) > th:eq(0)' ).click(); + + assert.deepEqual( + tableExtract( $table ), + planets, + 'table not sorted' + ); + + $cell = $table.find( 'tr:eq(0) > th:eq(0)' ); + $table.find( 'tr:eq(0) > th:eq(1)' ).click(); + + assert.equal( + $cell.hasClass( 'headerSortUp' ) || $cell.hasClass( 'headerSortDown' ), + false, + 'after sort: no class headerSortUp or headerSortDown' + ); + + assert.equal( + $cell.attr( 'title' ), + undefined, + 'after sort: no title tag added' + ); + + } ); + + // Regression tests! + tableTest( + 'T30775: German-style (dmy) 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 ) { + mw.config.set( 'wgDefaultDateFormat', 'dmy' ); + mw.config.set( 'wgPageContentLanguage', 'de' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'T30775: American-style (mdy) 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 ) { + mw.config.set( 'wgDefaultDateFormat', 'mdy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'T19141: IPv4 address sorting', + [ 'IP' ], + ipv4, + ipv4Sorted, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'T19141: IPv4 address sorting (reverse)', + [ 'IP' ], + ipv4, + reversed( ipv4Sorted ), + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click().click(); + } + ); + + 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(); + } + ); + + tableTest( + 'Digraphs with custom collation', + [ 'City' ], + digraphWords, + digraphWordsSorted, + function ( $table ) { + mw.config.set( 'tableSorterCollation', { + lj: 'lzzzz', + nj: 'nzzzz' + } ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + QUnit.test( 'Rowspan not exploded on init', function ( assert ) { + var $table = tableCreate( header, planets ); + + // Modify the table to have a multiple-row-spanning cell: + // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. + $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); + // - Set rowspan for 2nd cell of 3rd row to 3. + // This covers the removed cell in the 4th and 5th row. + $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' ); + + $table.tablesorter(); + + assert.equal( + $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowSpan' ), + 3, + 'Rowspan not exploded' + ); + } ); + + planetsRowspan = [ + [ 'Earth', '6051.8' ], + jupiter, + [ 'Mars', '6051.8' ], + mercury, + saturn, + venus + ]; + planetsRowspanII = [ jupiter, mercury, saturn, venus, [ 'Venus', '6371.0' ], [ 'Venus', '3390.0' ] ]; + + tableTest( + 'Basic planet table: same value for multiple rows via rowspan', + header, + planets, + planetsRowspan, + function ( $table ) { + // Modify the table to have a multiple-row-spanning cell: + // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. + $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); + // - Set rowspan for 2nd cell of 3rd row to 3. + // This covers the removed cell in the 4th and 5th row. + $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + tableTest( + 'Basic planet table: same value for multiple rows via rowspan (sorting initially)', + header, + planets, + planetsRowspan, + function ( $table ) { + // Modify the table to have a multiple-row-spanning cell: + // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. + $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); + // - Set rowspan for 2nd cell of 3rd row to 3. + // This covers the removed cell in the 4th and 5th row. + $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' ); + + $table.tablesorter( { sortList: [ + { 0: 'asc' } + ] } ); + } + ); + tableTest( + 'Basic planet table: Same value for multiple rows via rowspan II', + header, + planets, + planetsRowspanII, + function ( $table ) { + // Modify the table to have a multiple-row-spanning cell: + // - Remove 1st cell of 4th row, and, 1st cell or 5th row. + $table.find( 'tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)' ).remove(); + // - Set rowspan for 1st cell of 3rd row to 3. + // This covers the removed cell in the 4th and 5th row. + $table.find( 'tr:eq(2) td:eq(0)' ).attr( 'rowspan', '3' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'Complex date parsing I', + [ 'date' ], + complexMDYDates, + complexMDYSorted, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'mdy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'Currency parsing I', + [ 'currency' ], + currencyUnsorted, + currencySorted, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + planetsAscNameLegacy = planetsAscName.slice( 0 ); + planetsAscNameLegacy[ 4 ] = planetsAscNameLegacy[ 5 ]; + planetsAscNameLegacy.pop(); + + tableTest( + 'Legacy compat with .sortbottom', + header, + planets, + planetsAscNameLegacy, + function ( $table ) { + $table.find( 'tr:last' ).addClass( 'sortbottom' ); + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + QUnit.test( 'Test detection routine', function ( assert ) { + var $table; + $table = $( + '<table class="sortable">' + + '<caption>CAPTION</caption>' + + '<tr><th>THEAD</th></tr>' + + '<tr><td>1</td></tr>' + + '<tr class="sortbottom"><td>text</td></tr>' + + '</table>' + ); + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + + assert.equal( + $table.data( 'tablesorter' ).config.parsers[ 0 ].id, + 'number', + 'Correctly detected column content skipping sortbottom' + ); + } ); + + /** FIXME: the diff output is not very readeable. */ + QUnit.test( 'T34047 - caption must be before thead', function ( assert ) { + 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(); + + assert.equal( + $table.children().get( 0 ).nodeName, + 'CAPTION', + 'First element after <thead> must be <caption> (T34047)' + ); + } ); + + QUnit.test( 'data-sort-value attribute, when available, should override sorting position', function ( assert ) { + var $table, data; + + // Example 1: All cells except one cell without data-sort-value, + // which should be sorted at it's text content value. + $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( 'sortValue' ), + text: $( td ).text() + } ); + } ); + } ); + + assert.deepEqual( data, [ + { + data: 'Apple', + text: 'Bird' + }, + { + data: 'Bananna', + text: 'Ferret' + }, + { + data: undefined, + text: 'Cheetah' + }, + { + data: 'Cherry', + text: 'Dolphin' + }, + { + data: 'Drupe', + text: 'Elephant' + } + ], 'Order matches expected order (based on data-sort-value attribute values)' ); + + // Example 2 + $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( 'sortValue' ), + text: $( td ).text() + } ); + } ); + } ); + + assert.deepEqual( data, [ + { + data: undefined, + text: 'B' + }, + { + data: undefined, + text: 'D' + }, + { + data: 'E', + text: 'A' + }, + { + data: 'F', + text: 'C' + }, + { + data: undefined, + text: 'G' + } + ], 'Order matches expected order (based on data-sort-value attribute values)' ); + + // Example 3: Test that live changes are used from data-sort-value, + // even if they change after the tablesorter is constructed (T40152). + $table = $( + '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' + + '<tbody>' + + '<tr><td>D</td></tr>' + + '<tr><td data-sort-value="1">A</td></tr>' + + '<tr><td>B</td></tr>' + + '<tr><td data-sort-value="2">G</td></tr>' + + '<tr><td>C</td></tr>' + + '</tbody></table>' + ); + // initialize table sorter and sort once + $table + .tablesorter() + .find( '.headerSort:eq(0)' ).click(); + + // Change the sortValue data properties (T40152) + // - change data + $table.find( 'td:contains(A)' ).data( 'sortValue', 3 ); + // - add data + $table.find( 'td:contains(B)' ).data( 'sortValue', 1 ); + // - remove data, bring back attribute: 2 + $table.find( 'td:contains(G)' ).removeData( 'sortValue' ); + + // Now sort again (twice, so it is back at Ascending) + $table.find( '.headerSort:eq(0)' ).click(); + $table.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( 'sortValue' ), + text: $( td ).text() + } ); + } ); + } ); + + assert.deepEqual( data, [ + { + data: 1, + text: 'B' + }, + { + data: 2, + text: 'G' + }, + { + data: 3, + text: 'A' + }, + { + data: undefined, + text: 'C' + }, + { + data: undefined, + text: 'D' + } + ], 'Order matches expected order, using the current sortValue in $.data()' ); + + } ); + + tableTest( 'T10115: sort numbers with commas (ascending)', + [ 'Numbers' ], numbers, numbersAsc, + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( 'T10115: sort numbers with commas (descending)', + [ 'Numbers' ], numbers, reversed( numbersAsc ), + function ( $table ) { + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click().click(); + } + ); + // TODO add numbers sorting tests for T10115 with a different language + + QUnit.test( 'T34888 - Tables inside a tableheader cell', function ( assert ) { + var $table; + $table = $( + '<table class="sortable" id="mw-bug-32888">' + + '<tr><th>header<table id="mw-bug-32888-2">' + + '<tr><th>1</th><th>2</th></tr>' + + '</table></th></tr>' + + '<tr><td>A</td></tr>' + + '<tr><td>B</td></tr>' + + '</table>' + ); + $table.tablesorter(); + + assert.equal( + $table.find( '> thead:eq(0) > tr > th.headerSort' ).length, + 1, + 'Child tables inside a headercell should not interfere with sortable headers (T34888)' + ); + assert.equal( + $( '#mw-bug-32888-2' ).find( 'th.headerSort' ).length, + 0, + 'The headers of child tables inside a headercell should not be sortable themselves (T34888)' + ); + } ); + + tableTest( + 'Correct date sorting I', + [ 'date' ], + correctDateSorting1, + correctDateSortingSorted1, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'mdy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'Correct date sorting II', + [ 'date' ], + correctDateSorting2, + correctDateSortingSorted2, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'dmy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + tableTest( + 'ISO date sorting', + [ 'isoDate' ], + isoDateSorting, + isoDateSortingSorted, + function ( $table ) { + mw.config.set( 'wgDefaultDateFormat', 'dmy' ); + + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + } + ); + + QUnit.test( 'Sorting images using alt text', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<tr><th>THEAD</th></tr>' + + '<tr><td><img alt="2"/></td></tr>' + + '<tr><td>1</td></tr>' + + '</table>' + ); + $table.tablesorter().find( '.headerSort:eq(0)' ).click(); + + assert.equal( + $table.find( 'td' ).first().text(), + '1', + 'Applied correct sorting order' + ); + } ); + + QUnit.test( 'Sorting images using alt text (complex)', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<tr><th>THEAD</th></tr>' + + '<tr><td><img alt="D" />A</td></tr>' + + '<tr><td>CC</td></tr>' + + '<tr><td><a><img alt="A" /></a>F</tr>' + + '<tr><td><img alt="A" /><strong>E</strong></tr>' + + '<tr><td><strong><img alt="A" />D</strong></tr>' + + '<tr><td><img alt="A" />C</tr>' + + '</table>' + ); + $table.tablesorter().find( '.headerSort:eq(0)' ).click(); + + assert.equal( + $table.find( 'td' ).text(), + 'CDEFCCA', + 'Applied correct sorting order' + ); + } ); + + QUnit.test( 'Sorting images using alt text (with format autodetection)', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<tr><th>THEAD</th></tr>' + + '<tr><td><img alt="1" />7</td></tr>' + + '<tr><td>1<img alt="6" /></td></tr>' + + '<tr><td>5</td></tr>' + + '<tr><td>4</td></tr>' + + '</table>' + ); + $table.tablesorter().find( '.headerSort:eq(0)' ).click(); + + assert.equal( + $table.find( 'td' ).text(), + '4517', + 'Applied correct sorting order' + ); + } ); + + QUnit.test( 'T40911 - The row with the largest amount of columns should receive the sort indicators', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<thead>' + + '<tr><th rowspan="2" id="A1">A1</th><th colspan="2">B2a</th></tr>' + + '<tr><th id="B2b">B2b</th><th id="C2b">C2b</th></tr>' + + '</thead>' + + '<tr><td>A</td><td>Aa</td><td>Ab</td></tr>' + + '<tr><td>B</td><td>Ba</td><td>Bb</td></tr>' + + '</table>' + ); + $table.tablesorter(); + + assert.equal( + $table.find( '#A1' ).attr( 'class' ), + 'headerSort', + 'The first column of the first row should be sortable' + ); + assert.equal( + $table.find( '#B2b' ).attr( 'class' ), + 'headerSort', + 'The th element of the 2nd row of the 2nd column should be sortable' + ); + assert.equal( + $table.find( '#C2b' ).attr( 'class' ), + 'headerSort', + 'The th element of the 2nd row of the 3rd column should be sortable' + ); + } ); + + QUnit.test( 'rowspans in table headers should prefer the last row when rows are equal in length', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<thead>' + + '<tr><th rowspan="2" id="A1">A1</th><th>B2a</th></tr>' + + '<tr><th id="B2b">B2b</th></tr>' + + '</thead>' + + '<tr><td>A</td><td>Aa</td></tr>' + + '<tr><td>B</td><td>Ba</td></tr>' + + '</table>' + ); + $table.tablesorter(); + + assert.equal( + $table.find( '#A1' ).attr( 'class' ), + 'headerSort', + 'The first column of the first row should be sortable' + ); + assert.equal( + $table.find( '#B2b' ).attr( 'class' ), + 'headerSort', + 'The th element of the 2nd row of the 2nd column should be sortable' + ); + } ); + + QUnit.test( 'holes in the table headers should not throw JS errors', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<thead>' + + '<tr><th id="A1">A1</th><th>B1</th><th id="C1" rowspan="2">C1</th></tr>' + + '<tr><th id="A2">A2</th></tr>' + + '</thead>' + + '<tr><td>A</td><td>Aa</td><td>Aaa</td></tr>' + + '<tr><td>B</td><td>Ba</td><td>Bbb</td></tr>' + + '</table>' + ); + $table.tablesorter(); + assert.equal( $table.find( '#A2' ).data( 'headerIndex' ), + undefined, + 'A2 should not be a sort header' + ); + assert.equal( $table.find( '#C1' ).data( 'headerIndex' ), + 2, + 'C1 should be a sort header' + ); + } ); + + // T55527 + QUnit.test( 'td cells in thead should not be taken into account for longest row calculation', function ( assert ) { + var $table = $( + '<table class="sortable">' + + '<thead>' + + '<tr><th id="A1">A1</th><th>B1</th><td id="C1">C1</td></tr>' + + '<tr><th id="A2">A2</th><th>B2</th><th id="C2">C2</th></tr>' + + '</thead>' + + '</table>' + ); + $table.tablesorter(); + assert.equal( $table.find( '#C2' ).data( 'headerIndex' ), + 2, + 'C2 should be a sort header' + ); + assert.equal( $table.find( '#C1' ).data( 'headerIndex' ), + undefined, + 'C1 should not be a sort header' + ); + } ); + + // T43889 - exploding rowspans in more complex cases + tableTestHTML( + 'Rowspan exploding with row headers', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><th rowspan="2">foo</th><td rowspan="2">bar</td><td>baz</td></tr>' + + '<tr><td>2</td><td>baz</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo', 'bar', 'baz' ], + [ '2', 'foo', 'bar', 'baz' ] + ] + ); + + // T55211 - exploding rowspans in more complex cases + QUnit.test( + 'Rowspan exploding with row headers and colspans', function ( assert ) { + var $table = $( '<table class="sortable">' + + '<thead><tr><th rowspan="2">n</th><th colspan="2">foo</th><th rowspan="2">baz</th></tr>' + + '<tr><th>foo</th><th>bar</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td>foo</td><td>bar</td><td>baz</td></tr>' + + '<tr><td>2</td><td>foo</td><td>bar</td><td>baz</td></tr>' + + '</tbody></table>' ); + + $table.tablesorter(); + assert.equal( $table.find( 'tr:eq(1) th:eq(1)' ).data( 'headerIndex' ), + 2, + 'Incorrect index of sort header' + ); + } + ); + + tableTestHTML( + 'Rowspan exploding with colspanned cells', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td></tr>' + + '<tr><td>2</td><td colspan="2">foobar</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo', 'bar', 'baz' ], + [ '2', 'foobar', 'baz' ] + ] + ); + + tableTestHTML( + 'Rowspan exploding with colspanned cells (2)', + '<table class="sortable">' + + '<thead><tr><th>n</th><th>foo</th><th>bar</th><th>baz</th><th id="sortme">n2</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td><td>2</td></tr>' + + '<tr><td>2</td><td colspan="2">foobar</td><td>1</td></tr>' + + '</tbody></table>', + [ + [ '2', 'foobar', 'baz', '1' ], + [ '1', 'foo', 'bar', 'baz', '2' ] + ] + ); + + tableTestHTML( + 'Rowspan exploding with rightmost rows spanning most', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td></tr>' + + '<tr><td>2</td></tr>' + + '<tr><td>3</td><td rowspan="2">foo</td></tr>' + + '<tr><td>4</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo', 'bar' ], + [ '2', 'foo', 'bar' ], + [ '3', 'foo', 'bar' ], + [ '4', 'foo', 'bar' ] + ] + ); + + tableTestHTML( + 'Rowspan exploding with rightmost rows spanning most (2)', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td><td>baz</td></tr>' + + '<tr><td>2</td><td>baz</td></tr>' + + '<tr><td>3</td><td rowspan="2">foo</td><td>baz</td></tr>' + + '<tr><td>4</td><td>baz</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo', 'bar', 'baz' ], + [ '2', 'foo', 'bar', 'baz' ], + [ '3', 'foo', 'bar', 'baz' ], + [ '4', 'foo', 'bar', 'baz' ] + ] + ); + + tableTestHTML( + 'Rowspan exploding with row-and-colspanned cells', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>bar</th><th>baz</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="4">bar</td><td>baz</td></tr>' + + '<tr><td>2</td><td>baz</td></tr>' + + '<tr><td>3</td><td colspan="2" rowspan="2">foo</td><td>baz</td></tr>' + + '<tr><td>4</td><td>baz</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo1', 'foo2', 'bar', 'baz' ], + [ '2', 'foo1', 'foo2', 'bar', 'baz' ], + [ '3', 'foo', 'bar', 'baz' ], + [ '4', 'foo', 'bar', 'baz' ] + ] + ); + + tableTestHTML( + 'Rowspan exploding with uneven rowspan layout', + '<table class="sortable">' + + '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>foo3</th><th>bar</th><th>baz</th></tr></thead>' + + '<tbody>' + + '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>bar</td><td>baz</td></tr>' + + '<tr><td>2</td><td rowspan="3">bar</td><td>baz</td></tr>' + + '<tr><td>3</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>baz</td></tr>' + + '<tr><td>4</td><td>baz</td></tr>' + + '</tbody></table>', + [ + [ '1', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ], + [ '2', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ], + [ '3', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ], + [ '4', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ] + ] + ); + + QUnit.test( 'T105731 - incomplete rows in table body', function ( assert ) { + var $table, parsers; + $table = $( + '<table class="sortable">' + + '<tr><th>A</th><th>B</th></tr>' + + '<tr><td>3</td></tr>' + + '<tr><td>1</td><td>2</td></tr>' + + '</table>' + ); + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + // now the first row have 2 columns + $table.find( '.headerSort:eq(1)' ).click(); + + parsers = $table.data( 'tablesorter' ).config.parsers; + + assert.equal( + parsers.length, + 2, + 'detectParserForColumn() detect 2 parsers' + ); + + assert.equal( + parsers[ 1 ].id, + 'number', + 'detectParserForColumn() detect parser.id "number" for second column' + ); + + assert.equal( + parsers[ 1 ].format( $table.find( 'tbody > tr > td:eq(1)' ).text() ), + -Infinity, + 'empty cell is sorted as number -Infinity' + ); + } ); + + QUnit.test( 'bug T114721 - use of expand-child class', function ( assert ) { + var $table, parsers; + $table = $( + '<table class="sortable">' + + '<tr><th>A</th><th>B</th></tr>' + + '<tr><td>b</td><td>4</td></tr>' + + '<tr class="expand-child"><td colspan="2">some text follow b</td></tr>' + + '<tr><td>a</td><td>2</td></tr>' + + '<tr class="expand-child"><td colspan="2">some text follow a</td></tr>' + + '<tr class="expand-child"><td colspan="2">more text</td></tr>' + + '</table>' + ); + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + + assert.deepEqual( + tableExtract( $table ), + [ + [ 'a', '2' ], + [ 'some text follow a' ], + [ 'more text' ], + [ 'b', '4' ], + [ 'some text follow b' ] + ], + 'row with expand-child class follow above row' + ); + + parsers = $table.data( 'tablesorter' ).config.parsers; + assert.equal( + parsers[ 1 ].id, + 'number', + 'detectParserForColumn() detect parser.id "number" for second column' + ); + } ); + +}( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js new file mode 100644 index 00000000..32cda7eb --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js @@ -0,0 +1,266 @@ +( function ( $ ) { + var caretSample, + sig = { + pre: '--~~~~' + }, + bold = { + pre: '\'\'\'', + peri: 'Bold text', + post: '\'\'\'' + }, + h2 = { + pre: '== ', + peri: 'Heading 2', + post: ' ==', + regex: /^(\s*)(={1,6})(.*?)\2(\s*)$/, + regexReplace: '$1==$3==$4', + ownline: true + }, + ulist = { + pre: '* ', + peri: 'Bulleted list item', + post: '', + ownline: true, + splitlines: true + }; + + QUnit.module( 'jquery.textSelection', QUnit.newMwEnvironment() ); + + /** + * Test factory for $.fn.textSelection( 'encapsulateText' ) + * + * @param {Object} options Associative configuration array + * @param {string} options.description Description + * @param {string} options.input Input + * @param {string} options.output Output + * @param {int} options.start Starting char for selection + * @param {int} options.end Ending char for selection + * @param {Object} options.params Additional parameters for $().textSelection( 'encapsulateText' ) + */ + function encapsulateTest( options ) { + var opt = $.extend( { + description: '', + before: {}, + after: {}, + replace: {} + }, options ); + + opt.before = $.extend( { + text: '', + start: 0, + end: 0 + }, opt.before ); + opt.after = $.extend( { + text: '', + selected: null + }, opt.after ); + + QUnit.test( opt.description, function ( assert ) { + var $textarea, start, end, options, text, selected; + + $textarea = $( '<textarea>' ); + + $( '#qunit-fixture' ).append( $textarea ); + + $textarea.textSelection( 'setContents', opt.before.text ); + + start = opt.before.start; + end = opt.before.end; + + // Clone opt.replace + options = $.extend( {}, opt.replace ); + options.selectionStart = start; + options.selectionEnd = end; + $textarea.textSelection( 'encapsulateSelection', options ); + + text = $textarea.textSelection( 'getContents' ).replace( /\r\n/g, '\n' ); + + assert.equal( text, opt.after.text, 'Checking full text after encapsulation' ); + + if ( opt.after.selected !== null ) { + selected = $textarea.textSelection( 'getSelection' ); + assert.equal( selected, opt.after.selected, 'Checking selected text after encapsulation.' ); + } + + } ); + } + + encapsulateTest( { + description: 'Adding sig to end of text', + before: { + text: 'Wikilove dude! ', + start: 15, + end: 15 + }, + after: { + text: 'Wikilove dude! --~~~~', + selected: '' + }, + replace: sig + } ); + + encapsulateTest( { + description: 'Adding bold to empty', + before: { + text: '', + start: 0, + end: 0 + }, + after: { + text: '\'\'\'Bold text\'\'\'', + selected: 'Bold text' // selected because it's the default + }, + replace: bold + } ); + + encapsulateTest( { + description: 'Adding bold to existing text', + before: { + text: 'Now is the time for all good men to come to the aid of their country', + start: 20, + end: 32 + }, + after: { + text: 'Now is the time for \'\'\'all good men\'\'\' to come to the aid of their country', + selected: '' // empty because it's not the default' + }, + replace: bold + } ); + + encapsulateTest( { + description: 'ownline option: adding new h2', + before: { + text: 'Before\nAfter', + start: 7, + end: 7 + }, + after: { + text: 'Before\n== Heading 2 ==\nAfter', + selected: 'Heading 2' + }, + replace: h2 + } ); + + encapsulateTest( { + description: 'ownline option: turn a whole line into new h2', + before: { + text: 'Before\nMy heading\nAfter', + start: 7, + end: 17 + }, + after: { + text: 'Before\n== My heading ==\nAfter', + selected: '' + }, + replace: h2 + } ); + + encapsulateTest( { + description: 'ownline option: turn a partial line into new h2', + before: { + text: 'BeforeMy headingAfter', + start: 6, + end: 16 + }, + after: { + text: 'Before\n== My heading ==\nAfter', + selected: '' + }, + replace: h2 + } ); + + encapsulateTest( { + description: 'splitlines option: no selection, insert new list item', + before: { + text: 'Before\nAfter', + start: 7, + end: 7 + }, + after: { + text: 'Before\n* Bulleted list item\nAfter' + }, + replace: ulist + } ); + + encapsulateTest( { + description: 'splitlines option: single partial line selection, insert new list item', + before: { + text: 'BeforeMy List ItemAfter', + start: 6, + end: 18 + }, + after: { + text: 'Before\n* My List Item\nAfter' + }, + replace: ulist + } ); + + encapsulateTest( { + description: 'splitlines option: multiple lines', + before: { + text: 'Before\nFirst\nSecond\nThird\nAfter', + start: 7, + end: 25 + }, + after: { + text: 'Before\n* First\n* Second\n* Third\nAfter' + }, + replace: ulist + } ); + + function caretTest( options ) { + QUnit.test( options.description, function ( assert ) { + var pos, + $textarea = $( '<textarea>' ).text( options.text ); + + $( '#qunit-fixture' ).append( $textarea ); + + if ( options.mode === 'set' ) { + $textarea.textSelection( 'setSelection', { + start: options.start, + end: options.end + } ); + } + + function among( actual, expected, message ) { + if ( Array.isArray( expected ) ) { + assert.ok( expected.indexOf( actual ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' ); + } else { + assert.equal( actual, expected, message ); + } + } + + pos = $textarea.textSelection( 'getCaretPosition', { startAndEnd: true } ); + among( pos[ 0 ], options.start, 'Caret start should be where we set it.' ); + among( pos[ 1 ], options.end, 'Caret end should be where we set it.' ); + } ); + } + + caretSample = 'Some big text that we like to work with. Nothing fancy... you know what I mean?'; + + /* @broken: Disabled per T36820 + caretTest({ + description: 'getCaretPosition with original/empty selection - T33847 with IE 6/7/8', + text: caretSample, + start: [0, caretSample.length], // Opera and Firefox (prior to FF 6.0) default caret to the end of the box (caretSample.length) + end: [0, caretSample.length], // Other browsers default it to the beginning (0), so check both. + mode: 'get' + }); + */ + + caretTest( { + description: 'set/getCaretPosition with forced empty selection', + text: caretSample, + start: 7, + end: 7, + mode: 'set' + } ); + + caretTest( { + description: 'set/getCaretPosition with small selection', + text: caretSample, + start: 6, + end: 11, + mode: 'set' + } ); +}( jQuery ) ); |