From 23d027b486c46021359d2819f6e40a232dcb432c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Fri, 30 Oct 2026 11:34:51 +0100 Subject: [PATCH] =?UTF-8?q?fix(tabs):=20=F0=9F=90=9B=20honor=20first-tab?= =?UTF-8?q?=20hash=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/JS/jquery.smartTab.min.js | 2 +- .../Browser/Invoke-TabHashNavigationTest.ps1 | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 Tests/Browser/Invoke-TabHashNavigationTest.ps1 diff --git a/Resources/JS/jquery.smartTab.min.js b/Resources/JS/jquery.smartTab.min.js index 086d9a49..62558bb6 100644 --- a/Resources/JS/jquery.smartTab.min.js +++ b/Resources/JS/jquery.smartTab.min.js @@ -9,4 +9,4 @@ * Licensed under the terms of the MIT License * https://github.com/techlab/jquery-smarttab/blob/master/LICENSE */ -!function(i){"function"==typeof define&&define.amd?define(["jquery"],i):"object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=function(t,n){return void 0===n&&(n="undefined"!=typeof window?require("jquery"):require("jquery")(t)),i(n),n}:i(jQuery)}(function(u){var s={selected:0,theme:"basic",justified:!0,autoAdjustHeight:!0,backButtonSupport:!0,enableUrlHash:!0,transition:{animation:"none",speed:"400",easing:"",prefixCss:"",fwdShowCss:"",fwdHideCss:"",bckShowCss:"",bckHideCss:""},autoProgress:{enabled:!1,interval:3500,stopOnFocus:!0},keyboard:{keyNavigation:!0,keyLeft:[37,38],keyRight:[39,40],keyHome:[36],keyEnd:[35]},style:{mainCss:"st",navCss:"nav",navLinkCss:"nav-link",contentCss:"tab-content",contentPanelCss:"tab-pane",themePrefixCss:"st-theme-",justifiedCss:"st-justified",anchorDefaultCss:"default",anchorActiveCss:"active",loaderCss:"st-loading"},getContent:null},i=function(){function e(t,n){var i=this;_classCallCheck(this,e),this.options=u.extend(!0,{},s,n),this.main=u(t),this.nav=this._getFirstDescendant("."+this.options.style.navCss),this.container=this._getFirstDescendant("."+this.options.style.contentCss),this.tabs=this.nav.find("."+this.options.style.navLinkCss),this.pages=this.container.children("."+this.options.style.contentPanelCss),this.current_index=-1,this.autoProgressId=null,this.is_init=!1,this._init(),setTimeout(function(){i._load()},0)}return _createClass(e,[{key:"_init",value:function(){if(this._setElements(),!0===this.is_init)return!0;this._setEvents(),this.is_init=!0,this._triggerEvent("initialized")}},{key:"_load",value:function(){this.pages.hide(),this.tabs.removeClass(this.options.style.anchorActiveCss),this.current_index=-1;var t=(t=this._getURLHashIndex())||this.options.selected;this._showTab(t),this._triggerEvent("loaded")}},{key:"_getFirstDescendant",value:function(i){var e=this.main.children(i);return 0 +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $ReportDirectory, + [Parameter(Mandatory)][uri] $BaseUri, + [string] $CaptureDirectory +) + +$ErrorActionPreference = 'Stop' +if ($BaseUri.Scheme -ne 'http' -or $BaseUri.Host -ne '127.0.0.1') { + throw 'BaseUri must point to the loopback HTTP server serving ReportDirectory.' +} +Import-Module (Join-Path $PSScriptRoot '../../PSWriteHTML.psd1') -Force +Import-Module PSParseHTML -MinimumVersion 3.0.3 -ErrorAction Stop +$null = New-Item -ItemType Directory -Path $ReportDirectory -Force +$ReportPath = Join-Path $ReportDirectory 'tab-hash-navigation.html' +$IconPath = Join-Path $ReportDirectory 'tab-icon.svg' +[IO.File]::WriteAllText($IconPath, '') +$null = New-HTML -TitleText 'Nested tab navigation' -FavIcon $IconPath -FilePath $ReportPath { + New-HTMLSection -HeaderText 'Report navigation' { + New-HTMLText -Text 'Select either nested tab, then use history or a link to return to the first tab.' + New-HTMLAnchor -Href '#nested-first' -Text 'Open first nested tab' + } + New-HTMLTabPanel { + New-HTMLTab -Name 'Overview' -AnchorName overview { + New-HTMLText -Text 'Overview content' + } + New-HTMLTab -Name 'Recovery' -AnchorName recovery -Default { + New-HTMLTabPanel { + New-HTMLTab -Name 'Nested first' -AnchorName nested-first { + New-HTMLSection -HeaderText 'Recovery inventory' { + New-HTMLText -Text 'First nested tab is selected. Asset identifiers stay visible when navigation changes.' + New-HTMLTable -DataTable @( + [pscustomobject]@{ Asset = 'Directory server'; Identifier = '000123456789012345678'; State = 'Ready' } + [pscustomobject]@{ Asset = 'Recovery server'; Identifier = '000042'; State = 'Ready' } + ) -HideButtons -DataTableID nested-inventory + } + } + New-HTMLTab -Name 'Nested second' -AnchorName nested-second -Default { + New-HTMLText -Text 'Second nested tab is selected.' + } + } + } + } +} +$ReportUri = [uri]::new($BaseUri, 'tab-hash-navigation.html').AbsoluteUri +$Session = Start-HtmlBrowserSession -Url ($ReportUri + '#nested-second') -Browser Chromium -BrowserChannel msedge -Visible:$false -ViewportWidth 1440 -ViewportHeight 1000 -ErrorAction Stop +$Results = [System.Collections.Generic.List[string]]::new() +try { + $Session.Page.SetDefaultTimeout(10000) + function Assert-SelectedTab { + param([string] $Id, [string] $Case) + $Expression = '() => jQuery("#{0}").is(":visible") && jQuery(''a[href="#{0}"]'').hasClass("active")' -f $Id + try { + $null = $Session.Page.WaitForFunctionAsync($Expression).GetAwaiter().GetResult() + } catch { + throw "Navigation case '$Case' failed: $($_.Exception.Message)" + } + $Results.Add($Case) + } + Assert-SelectedTab nested-second 'Initial configured second tab' + + $null = Invoke-HtmlBrowserScript -Session $Session -Script "location.hash = '#nested-first'" + Assert-SelectedTab nested-first 'Hash edit selects first nested tab' + $null = Invoke-HtmlBrowserScript -Session $Session -Script "location.hash = '#nested-second'" + Assert-SelectedTab nested-second 'Hash edit selects second nested tab' + $null = $Session.Page.GoBackAsync().GetAwaiter().GetResult() + Assert-SelectedTab nested-first 'Back selects first nested tab' + $null = $Session.Page.GoForwardAsync().GetAwaiter().GetResult() + Assert-SelectedTab nested-second 'Forward restores second nested tab' + + $Session.Page.Locator('a[href="#nested-first"]').First.ClickAsync().GetAwaiter().GetResult() + Assert-SelectedTab nested-first 'In-document link selects first nested tab' + $null = Invoke-HtmlBrowserScript -Session $Session -Script "location.hash = '#nested-second'" + Assert-SelectedTab nested-second 'Select second tab before an unknown hash' + $null = Invoke-HtmlBrowserScript -Session $Session -Script @' +() => new Promise(resolve => { + window.addEventListener('hashchange', () => requestAnimationFrame(() => resolve(true)), { once: true }); + location.hash = '#missing-tab'; +}) +'@ + Assert-SelectedTab nested-second 'Unknown hash preserves second tab' + + $null = $Session.Page.GotoAsync('about:blank').GetAwaiter().GetResult() + $Response = $Session.Page.GotoAsync($ReportUri + '#nested-first').GetAwaiter().GetResult() + if ($null -eq $Response -or $Response.Status -ne 200) { throw 'Expected a fresh report document for the initial-link test.' } + Assert-SelectedTab nested-first 'Cold first-tab hash overrides configured second tab' + $null = $Session.Page.WaitForSelectorAsync('#nested-inventory_wrapper').GetAwaiter().GetResult() + if ($CaptureDirectory) { + $null = New-Item -ItemType Directory -Path $CaptureDirectory -Force + foreach ($Theme in @('light', 'dark')) { + $MediaType = $Session.Page.GetType().GetMethod('EmulateMediaAsync').GetParameters()[0].ParameterType + $Media = [Activator]::CreateInstance($MediaType) + $ColorType = [Nullable]::GetUnderlyingType($MediaType.GetProperty('ColorScheme').PropertyType) + $Media.ColorScheme = [Enum]::Parse($ColorType, $Theme, $true) + $Session.Page.EmulateMediaAsync($Media).GetAwaiter().GetResult() + foreach ($Width in @(390, 1024, 1440)) { + $Session.Page.SetViewportSizeAsync($Width, 1000).GetAwaiter().GetResult() + $null = Invoke-HtmlBrowserScript -Session $Session -Script "jQuery('#nested-inventory').DataTable().columns.adjust().responsive.recalc()" + Save-HtmlBrowserScreenshot -Session $Session -OutFile (Join-Path $CaptureDirectory "nested-first-$Width-$Theme.png") -Full -ErrorAction Stop | Out-Null + } + } + } + $ConsoleErrors = @(Get-HtmlBrowserConsoleLog -Session $Session -Severity Error) + if ($ConsoleErrors.Count) { throw ($ConsoleErrors | ConvertTo-Json -Depth 5) } + $Results | ForEach-Object { "PASS: $_" } +} finally { + Close-HtmlBrowserSession -Session $Session -ErrorAction Stop +}