diff --git a/tests/unit/tabs/core.js b/tests/unit/tabs/core.js index c2fd890488..1eac3c2683 100644 --- a/tests/unit/tabs/core.js +++ b/tests/unit/tabs/core.js @@ -747,4 +747,81 @@ QUnit.test( "extra listeners created when tabs are added/removed (trac-15136)", "No extra listeners after removing all the extra tabs" ); } ); +QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) { + assert.expect( 1 ); + + var origAjax = $.ajax, + element = $( "#tabs1" ), + anchor = element.find( "a[href='#fragment-3']" ), + url = new URL( anchor.prop( "href" ) ); + + try { + $.ajax = function() { + throw new Error( "Unexpected AJAX call; all tabs are local!" ); + }; + + anchor.attr( "href", url.protocol + "//username:password@" + url.host + + url.pathname + url.search + url.hash ); + + element.tabs(); + anchor.trigger( "click" ); + + assert.strictEqual( element.tabs( "option", "active" ), 2, + "should set the active option" ); + } finally { + $.ajax = origAjax; + } +} ); + +( function() { + function getVerifyTab( assert, element ) { + return function verifyTab( index ) { + assert.strictEqual( + element.tabs( "option", "active" ), + index, + "should set the active option to " + index ); + assert.strictEqual( + element.find( "[role='tabpanel']:visible" ).text().trim(), + "Tab " + ( index + 1 ), + "should set the panel to 'Tab " + ( index + 1 ) + "'" ); + }; + } + + QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) { + assert.expect( 12 ); + + location.hash = "#tabs-2"; + + var i, + element = $( "#tabs10" ).tabs(), + tabLinks = element.find( "> ul a" ), + verifyTab = getVerifyTab( assert, element ); + + for ( i = 0; i < tabLinks.length; i++ ) { + tabLinks.eq( i ).trigger( "click" ); + verifyTab( i ); + } + + location.hash = ""; + } ); + + QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) { + assert.expect( 12 ); + + var i, + element = $( "#tabs10" ), + tabLinks = element.find( "> ul a" ), + verifyTab = getVerifyTab( assert, element ); + + for ( i = 0; i < tabLinks.length; i++ ) { + location.hash = tabLinks.eq( i ).attr( "href" ); + element.tabs(); + verifyTab( i ); + element.tabs( "destroy" ); + } + + location.hash = ""; + } ); +} )(); + } ); diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html index cb4e5389f6..3f18fa015f 100644 --- a/tests/unit/tabs/tabs.html +++ b/tests/unit/tabs/tabs.html @@ -125,6 +125,35 @@
+
+ +
+

Tab 1

+
+
+

Tab 2

+
+
+

Tab 3

+
+
+

Tab 4

+
+
+

Tab 5

+
+
+

Tab 6

+
+
+ diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js index 49468feb39..494e54f224 100644 --- a/ui/widgets/tabs.js +++ b/ui/widgets/tabs.js @@ -61,26 +61,19 @@ $.widget( "ui.tabs", { load: null }, - _isLocal: ( function() { - var rhash = /#.*$/; - - return function( anchor ) { - var anchorUrl, locationUrl; - - anchorUrl = anchor.href.replace( rhash, "" ); - locationUrl = location.href.replace( rhash, "" ); - - // Decoding may throw an error if the URL isn't UTF-8 (#9518) - try { - anchorUrl = decodeURIComponent( anchorUrl ); - } catch ( _error ) {} - try { - locationUrl = decodeURIComponent( locationUrl ); - } catch ( _error ) {} - - return anchor.hash.length > 1 && anchorUrl === locationUrl; - }; - } )(), + _isLocal: function( anchor ) { + var anchorUrl = new URL( anchor.href ), + locationUrl = new URL( location.href ); + + return anchor.hash.length > 1 && + + // `href` may contain a hash but also username & password; + // we want to ignore them, so we check the three fields + // below instead. + anchorUrl.origin === locationUrl.origin && + anchorUrl.pathname === locationUrl.pathname && + anchorUrl.search === locationUrl.search; + }, _create: function() { var that = this, @@ -121,18 +114,31 @@ $.widget( "ui.tabs", { _initialActive: function() { var active = this.options.active, collapsible = this.options.collapsible, - locationHashDecoded = decodeURIComponent( location.hash.substring( 1 ) ); + locationHash = location.hash.substring( 1 ), + locationHashDecoded = decodeURIComponent( locationHash ); if ( active === null ) { // check the fragment identifier in the URL - if ( locationHashDecoded ) { + if ( locationHash ) { this.tabs.each( function( i, tab ) { - if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { active = i; return false; } } ); + + // If not found, decode the hash & try again. + // See the comment in `_processTabs` under the `_isLocal` check + // for more information. + if ( active === null ) { + this.tabs.each( function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) { + active = i; + return false; + } + } ); + } } // Check for a tab marked active via a class @@ -430,9 +436,24 @@ $.widget( "ui.tabs", { // Inline tab if ( that._isLocal( anchor ) ) { - selector = decodeURIComponent( anchor.hash ); + + // The "scrolling to a fragment" section of the HTML spec: + // https://html.spec.whatwg.org/#scrolling-to-a-fragment + // uses a concept of document's indicated part: + // https://html.spec.whatwg.org/#the-indicated-part-of-the-document + // Slightly below there's an algorithm to compute the indicated + // part: + // https://html.spec.whatwg.org/#the-indicated-part-of-the-document + // First, the algorithm tries the hash as-is, without decoding. + // Then, if one is not found, the same is attempted with a decoded + // hash. Replicate this logic. + selector = anchor.hash; panelId = selector.substring( 1 ); panel = that.element.find( "#" + CSS.escape( panelId ) ); + if ( !panel.length ) { + panelId = decodeURIComponent( panelId ); + panel = that.element.find( "#" + CSS.escape( panelId ) ); + } // remote tab } else {