summaryrefslogtreecommitdiff
path: root/www/wiki/tests/qunit/suites/resources/jquery
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/qunit/suites/resources/jquery')
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js121
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.color.test.js15
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js63
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js14
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js38
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js235
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js286
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.localize.test.js135
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js377
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js35
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js267
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js1498
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js266
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 ) );