File: Layout\MainLayoutTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj (Aspire.Dashboard.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Threading.Channels;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Layout;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.BrowserStorage;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Dashboard.Utils;
using Aspire.Tests.Shared;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Layout;
 
[UseCulture("en-US")]
public partial class MainLayoutTests : DashboardTestContext
{
    private IRenderedComponent<FluentMessageBarProvider>? _messageBarProvider;
 
    [Fact]
    public async Task HeaderHelp_IsSingleActionThatOpensHelpDialog()
    {
        DialogParameters? capturedParameters = null;
        var dialogService = new TestDialogService(onShowDialog: (_, parameters) =>
        {
            capturedParameters = parameters;
            return Task.CompletedTask;
        });
        SetupMainLayoutServices(dialogService: dialogService);
        var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
            new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
 
        var helpButton = cut.Find($"#{MainLayout.HelpButtonId}");
        Assert.Equal(Resources.Layout.MainLayoutHelp, helpButton.GetAttribute("aria-label"));
        Assert.Equal("?", helpButton.GetAttribute("aria-keyshortcuts"));
        Assert.Empty(cut.FindAll($"header a[aria-label='{Resources.Layout.MainLayoutAspireDashboardHelpLink}']"));
 
        await cut.InvokeAsync(() => helpButton.Click());
 
        Assert.NotNull(capturedParameters);
        Assert.Equal("HelpDialog", capturedParameters.Id);
        Assert.Equal(Resources.Layout.MainLayoutHelp, capturedParameters.Title);
    }
 
    [Theory]
    [InlineData(".logo")]
    [InlineData(".application-name")]
    public async Task Logo_OpensAllResourcesOverviewWithoutRestoringSelection(string selector)
    {
        var stored = new Dictionary<string, object?>();
        SetupMainLayoutServices(
            sessionStorage: new TestSessionStorage { OnSetAsync = (key, value) => stored[key] = value },
            dashboardClient: new TestDashboardClient(isEnabled: true, resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>));
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
            new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
 
        await cut.InvokeAsync(() => cut.Find($"header {selector}").Click());
 
        Assert.Equal(DashboardUrls.ResourceListUrl(), "/" + navigationManager.ToBaseRelativePath(navigationManager.Uri));
        Assert.Empty(Assert.IsType<List<string>>(stored[BrowserStorageKeys.LastSelectedResources]));
    }
 
    [Theory]
    [InlineData(AspireKeyboardShortcut.GoToResources, "/resources")]
    [InlineData(AspireKeyboardShortcut.GoToParameters, "/parameters")]
    [InlineData(AspireKeyboardShortcut.GoToGraph, "/resources")]
    [InlineData(AspireKeyboardShortcut.GoToExtensions, "/extensions")]
    [InlineData(AspireKeyboardShortcut.GoToTelemetrySources, "/structuredlogs?pane=telemetry")]
    [InlineData(AspireKeyboardShortcut.GoToConsoleLogs, "/consolelogs")]
    public async Task NavigationShortcut_OpensDestination(AspireKeyboardShortcut shortcut, string expectedUrl)
    {
        SetupMainLayoutServices(dashboardClient: new TestDashboardClient(isEnabled: true,
            resourceChannelProvider: () => Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>()));
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        navigationManager.NavigateTo("/consolelogs/resource/api?resource=api&resource=worker");
        var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
            new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
        var shortcuts = Services.GetRequiredService<ShortcutManager>();
        Assert.True(shortcuts.IsShortcutAvailable(shortcut));
 
        await cut.InvokeAsync(() => shortcuts.OnGlobalKeyDown(shortcut));
 
        Assert.Equal(navigationManager.ToAbsoluteUri(expectedUrl).AbsoluteUri, navigationManager.Uri);
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task NavigationShortcuts_RespectResourceServiceAndGraphAvailability(bool resourceServiceEnabled, bool graphDisabled)
    {
        SetupMainLayoutServices(dashboardClient: new TestDashboardClient(isEnabled: resourceServiceEnabled,
            resourceChannelProvider: () => Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>()),
            configureOptions: options => options.UI.DisableResourceGraph = graphDisabled);
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
            new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
        var shortcuts = Services.GetRequiredService<ShortcutManager>();
 
        foreach (var shortcut in new[]
        {
            AspireKeyboardShortcut.GoToResources,
            AspireKeyboardShortcut.GoToParameters,
            AspireKeyboardShortcut.GoToGraph,
            AspireKeyboardShortcut.GoToExtensions,
            AspireKeyboardShortcut.GoToConsoleLogs
        })
        {
            var expectedAvailable = resourceServiceEnabled && (shortcut != AspireKeyboardShortcut.GoToGraph || !graphDisabled);
            Assert.Equal(expectedAvailable, shortcuts.IsShortcutAvailable(shortcut));
            if (!expectedAvailable)
            {
                var initialUri = navigationManager.Uri;
                await cut.InvokeAsync(() => cut.Instance.OnPageKeyDownAsync(shortcut));
                Assert.Equal(initialUri, navigationManager.Uri);
            }
        }
 
        Assert.True(shortcuts.IsShortcutAvailable(AspireKeyboardShortcut.GoToTelemetrySources));
        await cut.InvokeAsync(() => shortcuts.OnGlobalKeyDown(AspireKeyboardShortcut.GoToTelemetrySources));
        Assert.Equal(navigationManager.ToAbsoluteUri(DashboardUrls.TelemetrySourcesUrl()).AbsoluteUri, navigationManager.Uri);
    }
 
    [Fact]
    public void OnInitialize_BrowserInfoCircuitDisconnected_DoesNotThrow()
    {
        SetupMainLayoutServices();
        JSInterop.Setup<BrowserInfo>("window.getBrowserInfo").SetException(new JSDisconnectedException("The circuit disconnected."));
 
        var cut = Render<MainLayout>(builder => builder
            .Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
 
        cut.WaitForAssertion(() => Assert.Single(JSInterop.Invocations["window.getBrowserInfo"]));
        Assert.False(Renderer.UnhandledException.IsCompleted);
    }
 
    [Fact]
    public async Task OnInitialize_UnsecuredOtlp_NotDismissed_DisplayMessageBar()
    {
        // Arrange
        var testLocalStorage = new TestLocalStorage();
        SetupMainLayoutServices(localStorage: testLocalStorage);
 
        testLocalStorage.OnGetUnprotectedAsync = key =>
        {
            switch (key)
            {
                case BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey:
                case BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey:
                    return (false, false);
                case BrowserStorageKeys.DashboardFeedbackPromptDismissed:
                    return (true, true);
                // The view preferences are read with the layout. None are stored here.
                case BrowserStorageKeys.ResourceListDensity:
                case BrowserStorageKeys.GroupOverview:
                case BrowserStorageKeys.RecordingsStyle:
                    return (false, null);
                default:
                    throw new InvalidOperationException("Unexpected key.");
            }
        };
 
        var dismissedSettingSetTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        testLocalStorage.OnSetUnprotectedAsync = (key, value) =>
        {
            switch (key)
            {
                case BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey:
                case BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey:
                    dismissedSettingSetTcs.TrySetResult((bool)value!);
                    break;
                default:
                    throw new InvalidOperationException("Unexpected key.");
            }
        };
 
        // Act
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        // Assert
        var dismissButton = _messageBarProvider!.WaitForElement($"fluent-button[aria-label='{Aspire.Dashboard.Resources.Dialogs.NotificationEntryDismiss}']");
        dismissButton.Click();
 
        Assert.True(await dismissedSettingSetTcs.Task.DefaultTimeout());
    }
 
    [Theory]
    [InlineData(true, false)]
    [InlineData(false, true)]
    public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar(bool unsecuredTelemetryMessageDismissedKey, bool unsecuredEndpointMessageDismissedKey)
    {
        // Arrange
        var testLocalStorage = new TestLocalStorage();
        SetupMainLayoutServices(localStorage: testLocalStorage);
 
        testLocalStorage.OnGetUnprotectedAsync = key =>
        {
            switch (key)
            {
                case BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey:
                    return (unsecuredTelemetryMessageDismissedKey, unsecuredTelemetryMessageDismissedKey);
                case BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey:
                    return (unsecuredEndpointMessageDismissedKey, unsecuredEndpointMessageDismissedKey);
                case BrowserStorageKeys.DashboardFeedbackPromptDismissed:
                    return (true, true);
                // The view preferences are read with the layout. None are stored here.
                case BrowserStorageKeys.ResourceListDensity:
                case BrowserStorageKeys.GroupOverview:
                case BrowserStorageKeys.RecordingsStyle:
                    return (false, null);
                default:
                    throw new InvalidOperationException("Unexpected key.");
            }
        };
 
        // Act
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        // Assert
        Assert.Empty(_messageBarProvider!.FindComponents<DashboardMessageBar>());
    }
 
    [Theory]
    [InlineData(true, false)]
    [InlineData(false, true)]
    public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar(bool expectMessageBar, bool telemetrySuppressUnsecuredMessage)
    {
        // Arrange
        var testLocalStorage = new TestLocalStorage();
        SetupMainLayoutServices(localStorage: testLocalStorage, configureOptions: o =>
        {
            o.Otlp.SuppressUnsecuredMessage = telemetrySuppressUnsecuredMessage;
        });
 
        testLocalStorage.OnGetUnprotectedAsync = key =>
        {
            switch (key)
            {
                case BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey:
                case BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey:
                    return (false, false); // Message not dismissed, but should be suppressed by config if suppressUnsecuredMessage is true
                case BrowserStorageKeys.DashboardFeedbackPromptDismissed:
                    return (true, true);
                // The view preferences are read with the layout. None are stored here.
                case BrowserStorageKeys.ResourceListDensity:
                case BrowserStorageKeys.GroupOverview:
                case BrowserStorageKeys.RecordingsStyle:
                    return (false, null);
                default:
                    throw new InvalidOperationException("Unexpected key.");
            }
        };
 
        // Act
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        // Assert
        if (!expectMessageBar)
        {
            Assert.Empty(_messageBarProvider!.FindComponents<DashboardMessageBar>());
        }
        else
        {
            var messageBarProvider = _messageBarProvider!;
            messageBarProvider.WaitForAssertion(() =>
            {
                var messageBar = Assert.Single(messageBarProvider.FindComponents<DashboardMessageBar>());
                Assert.Contains("intent-warning", messageBar.Find("[role='alert']").ClassList);
            });
        }
    }
 
    [Theory]
    [InlineData(BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey)]
    [InlineData(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)]
    public void OnInitialize_BrowserStorageReadFails_ContinuesInitialization(string failedKey)
    {
        var readKeys = new List<string>();
        var localStorage = new TestLocalStorage
        {
            OnGetUnprotectedAsync = key =>
            {
                // The view preferences are read with the layout too, and aren't what this test is about.
                if (key == BrowserStorageKeys.DashboardFeedbackPromptDismissed)
                {
                    return (true, true);
                }
 
                if (key is BrowserStorageKeys.ResourceListDensity or BrowserStorageKeys.GroupOverview or BrowserStorageKeys.RecordingsStyle)
                {
                    return (false, null);
                }
 
                readKeys.Add(key);
                return key == failedKey ? (false, false) : (true, false);
            }
        };
        SetupMainLayoutServices(localStorage: localStorage);
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        Assert.Equal(
            [BrowserStorageKeys.UnsecuredEndpointMessageDismissedKey, BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey],
            readKeys);
        var messageBarProvider = _messageBarProvider!;
        messageBarProvider.WaitForAssertion(() => Assert.Single(messageBarProvider.FindComponents<DashboardMessageBar>()));
    }
 
    [Theory]
    [InlineData(true, "dashboard-help-button", "HelpDialog", "dashboard-help-button")]
    [InlineData(true, "dashboard-settings-button", "SettingsDialog", "dashboard-settings-button")]
    [InlineData(false, "dashboard-navigation-button", "HelpDialog", "dashboard-navigation-button")]
    [InlineData(false, "dashboard-navigation-button", "SettingsDialog", "dashboard-navigation-button")]
    public async Task HeaderDialogClose_RestoresFocusToLaunchButton(bool isDesktop, string launchButtonId, string expectedDialogId, string expectedFocusId)
    {
        DialogParameters? capturedParameters = null;
        TestDialogService? dialogService = null;
        dialogService = new TestDialogService(onShowDialog: (_, parameters) =>
        {
            capturedParameters = parameters;
            return Task.CompletedTask;
        });
 
        SetupMainLayoutServices(dialogService: dialogService);
        JSInterop.SetupVoid("focusElement", _ => true);
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        if (isDesktop)
        {
            await cut.InvokeAsync(() => cut.Find($"#{launchButtonId}").Click());
        }
        else
        {
            var menuItemName = expectedDialogId == "HelpDialog"
                ? Resources.Layout.MainLayoutKeyboardShortcuts
                : "Settings";
 
            await cut.InvokeAsync(() => cut.Find("#dashboard-navigation-button").Click());
            var menuItem = cut.FindAll("fluent-menu-item").Single(item => item.TextContent.Contains(menuItemName, StringComparison.OrdinalIgnoreCase));
            await cut.InvokeAsync(() => menuItem.TriggerEvent("onmenuitemchange", new MenuItemEventArgs { Id = menuItem.Id, Text = menuItem.TextContent }));
        }
 
        Assert.NotNull(capturedParameters);
        Assert.Equal(expectedDialogId, capturedParameters.Id);
 
        await cut.InvokeAsync(() => capturedParameters.OnDialogClosing.InvokeAsync(null!));
 
        cut.WaitForAssertion(() =>
        {
            Assert.Contains(JSInterop.Invocations, invocation =>
                invocation.Identifier == "focusElement" &&
                invocation.Arguments.Count == 1 &&
                string.Equals((string?)invocation.Arguments[0], expectedFocusId, StringComparison.Ordinal));
        });
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void DashboardRunSelect_Supported_IsDisplayedWithTheOtherHeaderButtons(bool isDesktop)
    {
        SetupMainLayoutServices();
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        Assert.Single(cut.FindComponents<DashboardRunSelect>());
        var runSelectIndex = cut.Markup.IndexOf($"id=\"{MainLayout.RunSelectButtonId}\"", StringComparison.Ordinal);
        Assert.True(runSelectIndex >= 0);
        if (isDesktop)
        {
            // Run selection is the first app-wide action after navigation.
            Assert.True(runSelectIndex < cut.Markup.IndexOf($"id=\"{MainLayout.HelpButtonId}\"", StringComparison.Ordinal));
        }
        else
        {
            Assert.True(runSelectIndex < cut.Markup.IndexOf($"id=\"{MainLayout.NavigationButtonId}\"", StringComparison.Ordinal));
        }
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void RecordingsMenuSetting_ShowsTheLiveRunMenuInsteadOfTheRecordingsButton(bool isDesktop)
    {
        var localStorage = new TestLocalStorage
        {
            OnGetUnprotectedAsync = key => key == BrowserStorageKeys.RecordingsStyle ? (true, "Menu") : (false, null)
        };
        SetupMainLayoutServices(localStorage: localStorage);
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        cut.WaitForAssertion(() => Assert.Single(cut.FindComponents<DashboardRunMenu>()));
        Assert.Empty(cut.FindComponents<DashboardRunSelect>());
        Assert.Empty(cut.FindComponents<DashboardRunPill>());
        var menuButton = Assert.Single(cut.FindComponent<DashboardRunMenu>().FindComponents<AspireMenuButton>());
        Assert.Equal("Live run", menuButton.Instance.Text);
        Assert.Equal(["Live run", "Learn about runs"], menuButton.Instance.ItemsProvider().Where(item => !item.IsDivider).Select(item => item.Text));
        if (isDesktop)
        {
            // Where it was before recordings had their own panel: with the header's buttons, ahead of the help button.
            var menuIndex = cut.Markup.IndexOf("application-run-select", StringComparison.Ordinal);
            Assert.True(menuIndex < cut.Markup.IndexOf($"id=\"{MainLayout.HelpButtonId}\"", StringComparison.Ordinal));
        }
    }
 
    [Fact]
    public async Task DashboardRunSelect_Click_OpensRecordingsPanel()
    {
        object? capturedContent = null;
        DialogParameters? capturedParameters = null;
        var dialogService = new TestDialogService(onShowDialog: (content, parameters) =>
        {
            capturedContent = content;
            capturedParameters = parameters;
            return Task.CompletedTask;
        });
        SetupMainLayoutServices(dialogService: dialogService);
        var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
            new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
 
        var button = cut.Find($"#{MainLayout.RunSelectButtonId}");
        Assert.Equal("Recordings, viewing Live view", button.GetAttribute("aria-label"));
        Assert.Equal("Recordings", button.GetAttribute("data-tooltip"));
        Assert.Equal("dialog", button.GetAttribute("aria-haspopup"));
        Assert.Single(cut.FindAll($"#{MainLayout.RunSelectButtonId} .dashboard-run-indicator.live"));
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
 
        await cut.InvokeAsync(() => button.Click());
 
        Assert.NotNull(capturedParameters);
        Assert.Equal("RecordingsDialog", capturedParameters.Id);
        Assert.Equal("Recordings", capturedParameters.Title);
        Assert.Equal(HorizontalAlignment.Right, capturedParameters.Alignment);
        var content = Assert.IsType<DashboardRunsDialogContent>(capturedContent);
        Assert.Equal(Services.GetRequiredService<IDashboardRunSelection>().SelectedRun.RunId, content.SelectedRunId);
    }
 
    [Fact]
    public async Task DashboardRunSelect_SwitchingRunsStoresAndAppliesSelectionWithoutNavigation()
    {
        var historicalRun = new DashboardRunDescriptor(
            RunId: "historical",
            SchemaVersion: DashboardRunStore.SchemaVersion,
            StartedAtUtc: new DateTimeOffset(2025, 1, 2, 12, 30, 0, TimeSpan.Zero),
            EndedAtUtc: new DateTimeOffset(2025, 1, 2, 13, 30, 0, TimeSpan.Zero),
            CleanShutdown: true,
            ApplicationName: "TestApp",
            DatabasePath: string.Empty,
            IsCurrent: false);
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true),
            historicalRun
        ]);
        string? storedRunId = null;
        var sessionStorage = new TestSessionStorage
        {
            OnSetAsync = (_, value) => storedRunId = Assert.IsType<string>(value)
        };
        DashboardRunsDialogContent? content = null;
        var dialogService = new TestDialogService(onShowDialog: (dialogContent, _) =>
        {
            content = Assert.IsType<DashboardRunsDialogContent>(dialogContent);
            return Task.CompletedTask;
        });
        var localStorage = new TestLocalStorage
        {
            OnGetUnprotectedAsync = key => key == BrowserStorageKeys.RecordingsStyle ? (true, "Panel") : (false, null)
        };
        SetupMainLayoutServices(localStorage: localStorage, dialogService: dialogService, dashboardRunStore: runStore, sessionStorage: sessionStorage);
        await Services.GetRequiredService<DashboardViewPreferences>().EnsureLoadedAsync();
        var expectedHistoricalRunText = FormatHelpers.FormatDateTime(
            Services.GetRequiredService<BrowserTimeProvider>(),
            historicalRun.StartedAtUtc.UtcDateTime);
        var initializedCount = 0;
        var disposedCount = 0;
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(component => component.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.Add(component => component.Body, bodyBuilder =>
            {
                bodyBuilder.OpenComponent<LifecycleTestComponent>(0);
                bodyBuilder.AddComponentParameter(1, nameof(LifecycleTestComponent.Initialized), (Action)(() => initializedCount++));
                bodyBuilder.AddComponentParameter(2, nameof(LifecycleTestComponent.Disposed), (Action)(() => disposedCount++));
                bodyBuilder.CloseComponent();
            });
        });
        var navigationOccurred = false;
        Services.GetRequiredService<NavigationManager>().LocationChanged += (_, _) => navigationOccurred = true;
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
 
        await cut.InvokeAsync(() => cut.Find($"#{MainLayout.RunSelectButtonId}").Click());
        Assert.NotNull(content);
        await cut.InvokeAsync(() => content.SelectRunAsync("historical"));
 
        Assert.Equal("historical", storedRunId);
        Assert.Equal("historical", runSelection.SelectedRunId);
        Assert.False(navigationOccurred);
        Assert.Equal(2, initializedCount);
        Assert.Equal(1, disposedCount);
        var pill = cut.Find(".dashboard-run-pill");
        Assert.Equal(expectedHistoricalRunText, pill.QuerySelector(".dashboard-run-pill-text")!.TextContent);
        Assert.Equal($"Recordings, viewing {expectedHistoricalRunText}", pill.GetAttribute("aria-label"));
        Assert.Single(pill.QuerySelectorAll(".dashboard-run-indicator.recording"));
        var historicalRunButton = cut.Find($"#{MainLayout.RunSelectButtonId}");
        Assert.Equal($"Recordings, viewing {expectedHistoricalRunText}", historicalRunButton.GetAttribute("aria-label"));
        Assert.Single(historicalRunButton.QuerySelectorAll(".dashboard-run-indicator.recording"));
 
        // The pill opens the same recordings panel as the header button. The panel closes itself when a run is
        // selected; the test dialog service doesn't render it, so close it here.
        await cut.InvokeAsync(() => dialogService.LastInstance!.CloseAsync());
        content = null;
        // The layout sees the first panel as closed once its result completes asynchronously.
        cut.WaitForAssertion(() =>
        {
            cut.Find(".dashboard-run-pill").Click();
            Assert.NotNull(content);
        });
        Assert.NotNull(content);
 
        await cut.InvokeAsync(() => content.SelectRunAsync(null));
 
        Assert.Equal(string.Empty, storedRunId);
        Assert.Null(runSelection.SelectedRunId);
        Assert.False(navigationOccurred);
        Assert.Equal(3, initializedCount);
        Assert.Equal(2, disposedCount);
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
        Assert.Single(cut.FindAll($"#{MainLayout.RunSelectButtonId} .dashboard-run-indicator.live"));
    }
 
    [Fact]
    public async Task DashboardRunSelect_SelectionFailure_KeepsCurrentRunAndCircuitActive()
    {
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true),
            new(
                RunId: "historical",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: DateTimeOffset.UnixEpoch,
                CleanShutdown: true,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: false)
        ]);
        string? storedRunId = null;
        var sessionStorage = new TestSessionStorage
        {
            OnSetAsync = (_, value) => storedRunId = Assert.IsType<string>(value)
        };
        var testSink = new TestSink();
        Services.AddSingleton<ILogger<MainLayout>>(new TestLogger<MainLayout>(new TestLoggerFactory(testSink, enabled: true)));
        SetupMainLayoutServices(dashboardRunStore: runStore, sessionStorage: sessionStorage);
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
        var exception = new InvalidOperationException("The historical database could not be opened.");
        runSelection.OnSelectRun = runId =>
        {
            if (runId == "historical")
            {
                throw exception;
            }
        };
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(component => component.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.Add(component => component.Body, bodyBuilder => bodyBuilder.AddMarkupContent(0, "<div id=\"body-content\"></div>"));
        });
 
        await cut.InvokeAsync(() => cut.Instance.SwitchDashboardRunAsync("historical"));
 
        Assert.NotNull(cut.Find("#body-content"));
        Assert.True(runSelection.SelectedRun.IsCurrent);
        Assert.Equal(string.Empty, storedRunId);
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
        var errorLog = Assert.Single(testSink.Writes);
        Assert.Equal(LogLevel.Error, errorLog.LogLevel);
        Assert.Equal("Failed to switch to dashboard run 'historical'. Keeping dashboard run 'current' selected.", errorLog.Message);
        Assert.Same(exception, errorLog.Exception);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void DashboardRunSelect_Unsupported_IsHiddenAndStaleSelectionIsIgnored(bool isDesktop)
    {
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true)
        ],
        supportsRunSelection: false);
        var sessionStorage = new TestSessionStorage
        {
            OnGetAsync = key => key == BrowserStorageKeys.DashboardFeedbackPromptDismissedForSession
                ? (false, false)
                : throw new InvalidOperationException("Run selection should not be read.")
        };
        SetupMainLayoutServices(dashboardRunStore: runStore, sessionStorage: sessionStorage);
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
        var getRunsCallCount = runStore.GetRunsCallCount;
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(
                component => component.ViewportInformation,
                new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        Assert.Empty(cut.FindComponents<DashboardRunSelect>());
        Assert.Equal(getRunsCallCount, runStore.GetRunsCallCount);
        Assert.Null(runSelection.SelectedRunId);
    }
 
    [Fact]
    public void DashboardRunSelect_GetSortedRuns_FiltersAndSortsRuns()
    {
        var startedAtUtc = new DateTimeOffset(2025, 1, 2, 12, 0, 0, TimeSpan.Zero);
        var currentRun = new DashboardRunDescriptor("current", DashboardRunStore.SchemaVersion, DateTimeOffset.UnixEpoch, null, false, "TestApp", string.Empty, IsCurrent: true);
        var pinnedRunB = new DashboardRunDescriptor("pinned-b", DashboardRunStore.SchemaVersion, startedAtUtc, null, true, "TestApp", string.Empty, IsCurrent: false) { IsPinned = true };
        var pinnedRunA = new DashboardRunDescriptor("pinned-a", DashboardRunStore.SchemaVersion, startedAtUtc, null, true, "TestApp", string.Empty, IsCurrent: false) { IsPinned = true };
        var olderPinnedRun = new DashboardRunDescriptor("pinned-older", DashboardRunStore.SchemaVersion, startedAtUtc.AddDays(-1), null, true, "TestApp", string.Empty, IsCurrent: false) { IsPinned = true };
        var unpinnedRun = new DashboardRunDescriptor("unpinned", DashboardRunStore.SchemaVersion, startedAtUtc.AddDays(1), null, true, "TestApp", string.Empty, IsCurrent: false);
        var incompatibleRun = new DashboardRunDescriptor("incompatible", DashboardRunStore.SchemaVersion - 1, startedAtUtc.AddDays(2), null, true, "TestApp", string.Empty, IsCurrent: false) { IsSelectable = false };
        var unavailableRun = new DashboardRunDescriptor("unavailable", DashboardRunStore.SchemaVersion, startedAtUtc.AddDays(3), null, true, "TestApp", string.Empty, IsCurrent: false) { IsSelectable = false };
        var prunedRun = new DashboardRunDescriptor("pruned", DashboardRunStore.SchemaVersion, startedAtUtc.AddDays(4), null, true, "TestApp", string.Empty, IsCurrent: false) { IsPruned = true };
 
        var runs = DashboardRunSelect.GetSortedRuns(
        [
            unavailableRun,
            pinnedRunB,
            unpinnedRun,
            prunedRun,
            olderPinnedRun,
            incompatibleRun,
            currentRun,
            pinnedRunA
        ]);
 
        Assert.Equal(
            ["current", "pinned-a", "pinned-b", "pinned-older", "incompatible", "unpinned"],
            runs.Select(run => run.RunId));
    }
 
    [Fact]
    public void DashboardRunSelect_FormatRunAge_UsesRelativeTime()
    {
        SetupMainLayoutServices();
        var loc = Services.GetRequiredService<IStringLocalizer<Aspire.Dashboard.Resources.Layout>>();
        var now = new DateTimeOffset(2025, 1, 10, 12, 0, 0, TimeSpan.Zero);
 
        Assert.Equal("a moment ago", DashboardRunSelect.FormatRunAge(now.AddSeconds(-59), now, loc));
        Assert.Equal("a moment ago", DashboardRunSelect.FormatRunAge(now.AddSeconds(5), now, loc));
        Assert.Equal("1 minute ago", DashboardRunSelect.FormatRunAge(now.AddMinutes(-1), now, loc));
        Assert.Equal("45 minutes ago", DashboardRunSelect.FormatRunAge(now.AddMinutes(-45).AddSeconds(-30), now, loc));
        Assert.Equal("1h ago", DashboardRunSelect.FormatRunAge(now.AddMinutes(-60), now, loc));
        Assert.Equal("23h ago", DashboardRunSelect.FormatRunAge(now.AddHours(-23).AddMinutes(-59), now, loc));
        Assert.Equal("1 day ago", DashboardRunSelect.FormatRunAge(now.AddDays(-1), now, loc));
        Assert.Equal("2 days ago", DashboardRunSelect.FormatRunAge(now.AddDays(-2).AddHours(-5), now, loc));
    }
 
    [Fact]
    public void DashboardRunSelect_FormatRunDuration_UsesRecordedEndTime()
    {
        var startedAtUtc = new DateTimeOffset(2025, 1, 2, 12, 0, 0, TimeSpan.Zero);
        DashboardRunDescriptor CreateRun(DateTimeOffset? endedAtUtc, bool isCurrent = false) =>
            new("run", DashboardRunStore.SchemaVersion, startedAtUtc, endedAtUtc, CleanShutdown: endedAtUtc is not null, "TestApp", string.Empty, isCurrent);
 
        Assert.Equal("(26m 6s)", DashboardRunSelect.FormatRunDuration(CreateRun(startedAtUtc.AddMinutes(26).AddSeconds(6.4))));
        Assert.Null(DashboardRunSelect.FormatRunDuration(CreateRun(endedAtUtc: null)));
        Assert.Null(DashboardRunSelect.FormatRunDuration(CreateRun(startedAtUtc.AddMinutes(-1))));
        Assert.Null(DashboardRunSelect.FormatRunDuration(CreateRun(startedAtUtc.AddMinutes(5), isCurrent: true)));
    }
 
    [Fact]
    public async Task RunSelectionPending_RendersCurrentRunWithoutLoadingRuns()
    {
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true)
        ]);
        var runSelectionSource = new TaskCompletionSource<(bool Success, object? Value)>(TaskCreationOptions.RunContinuationsAsynchronously);
        var sessionStorage = new TestSessionStorage
        {
            OnGetTaskAsync = key => key == BrowserStorageKeys.DashboardFeedbackPromptDismissedForSession
                ? Task.FromResult<(bool Success, object? Value)>((false, false))
                : runSelectionSource.Task
        };
        var localStorage = new TestLocalStorage
        {
            OnGetUnprotectedAsync = key => key == BrowserStorageKeys.RecordingsStyle ? (true, "Panel") : (false, null)
        };
        SetupMainLayoutServices(localStorage: localStorage, dashboardRunStore: runStore, sessionStorage: sessionStorage);
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
        var getRunsCallCount = runStore.GetRunsCallCount;
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.Add(p => p.Body, bodyBuilder => bodyBuilder.AddMarkupContent(0, "<div id=\"body-content\"></div>"));
        });
 
        Assert.NotNull(cut.Find("#body-content"));
        Assert.True(runSelection.SelectedRun.IsCurrent);
        Assert.Single(cut.FindComponents<DashboardRunSelect>());
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
        // Runs are listed by the recordings panel, so rendering the header button doesn't load them.
        Assert.Equal(getRunsCallCount, runStore.GetRunsCallCount);
 
        await cut.InvokeAsync(() => runSelectionSource.SetResult((false, null)));
    }
 
    [Fact]
    public async Task RunSelectionPending_StoredHistoricalRunReplacesCurrentRun()
    {
        var historicalRun = new DashboardRunDescriptor(
            RunId: "historical",
            SchemaVersion: DashboardRunStore.SchemaVersion,
            StartedAtUtc: new DateTimeOffset(2025, 1, 2, 12, 30, 0, TimeSpan.Zero),
            EndedAtUtc: new DateTimeOffset(2025, 1, 2, 13, 30, 0, TimeSpan.Zero),
            CleanShutdown: true,
            ApplicationName: "TestApp",
            DatabasePath: string.Empty,
            IsCurrent: false);
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true),
            historicalRun
        ]);
        var runSelectionSource = new TaskCompletionSource<(bool Success, object? Value)>(TaskCreationOptions.RunContinuationsAsynchronously);
        var sessionStorage = new TestSessionStorage
        {
            OnGetTaskAsync = key => key == BrowserStorageKeys.DashboardFeedbackPromptDismissedForSession
                ? Task.FromResult<(bool Success, object? Value)>((false, false))
                : runSelectionSource.Task
        };
        SetupMainLayoutServices(dashboardRunStore: runStore, sessionStorage: sessionStorage);
        await Services.GetRequiredService<DashboardViewPreferences>().EnsureLoadedAsync();
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.Add(p => p.Body, bodyBuilder => bodyBuilder.AddMarkupContent(0, "<div id=\"body-content\"></div>"));
        });
 
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
 
        await cut.InvokeAsync(() => runSelectionSource.SetResult((true, "historical")));
 
        var expectedHistoricalRunDate = FormatHelpers.FormatDateTime(
            Services.GetRequiredService<BrowserTimeProvider>(),
            historicalRun.StartedAtUtc.UtcDateTime);
        cut.WaitForAssertion(() =>
        {
            Assert.Equal(expectedHistoricalRunDate, cut.Find(".dashboard-run-pill-text").TextContent);
        });
    }
 
    [Fact]
    public void StoredHistoricalRunFailure_FallsBackToCurrentRunAndClearsSelection()
    {
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true),
            new(
                RunId: "historical",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: DateTimeOffset.UnixEpoch,
                CleanShutdown: true,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: false)
        ]);
        string? storedRunId = null;
        var sessionStorage = new TestSessionStorage
        {
            OnGetAsync = key => key == BrowserStorageKeys.DashboardFeedbackPromptDismissedForSession
                ? (false, false)
                : (true, "historical"),
            OnSetAsync = (_, value) => storedRunId = Assert.IsType<string>(value)
        };
        var testSink = new TestSink();
        Services.AddSingleton<ILogger<MainLayout>>(new TestLogger<MainLayout>(new TestLoggerFactory(testSink, enabled: true)));
        SetupMainLayoutServices(dashboardRunStore: runStore, sessionStorage: sessionStorage);
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
        var selectedRunIds = new List<string?>();
        var exception = new InvalidOperationException("The historical database could not be opened.");
        runSelection.OnSelectRun = runId =>
        {
            selectedRunIds.Add(runId);
            if (runId == "historical")
            {
                throw exception;
            }
        };
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        Assert.Equal(["historical", null], selectedRunIds);
        Assert.True(runSelection.SelectedRun.IsCurrent);
        Assert.Null(runSelection.SelectedRunId);
        Assert.Equal(string.Empty, storedRunId);
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
        var errorLog = Assert.Single(testSink.Writes);
        Assert.Equal(LogLevel.Error, errorLog.LogLevel);
        Assert.Equal("Failed to restore dashboard run 'historical'. Falling back to the current run.", errorLog.Message);
        Assert.Same(exception, errorLog.Exception);
    }
 
    [Fact]
    public async Task RunSelectionPending_UserSelectionWinsOverStoredSelection()
    {
        var historicalRun = new DashboardRunDescriptor(
            RunId: "historical",
            SchemaVersion: DashboardRunStore.SchemaVersion,
            StartedAtUtc: new DateTimeOffset(2025, 1, 2, 12, 30, 0, TimeSpan.Zero),
            EndedAtUtc: new DateTimeOffset(2025, 1, 2, 13, 30, 0, TimeSpan.Zero),
            CleanShutdown: true,
            ApplicationName: "TestApp",
            DatabasePath: string.Empty,
            IsCurrent: false);
        var runStore = new FluentUISetupHelpers.TestDashboardRunStore(
        [
            new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: string.Empty,
                IsCurrent: true),
            historicalRun
        ]);
        var runSelectionSource = new TaskCompletionSource<(bool Success, object? Value)>(TaskCreationOptions.RunContinuationsAsynchronously);
        string? storedRunId = null;
        var sessionStorage = new TestSessionStorage
        {
            OnGetTaskAsync = key => key == BrowserStorageKeys.DashboardFeedbackPromptDismissedForSession
                ? Task.FromResult<(bool Success, object? Value)>((false, false))
                : runSelectionSource.Task,
            OnSetAsync = (_, value) => storedRunId = Assert.IsType<string>(value)
        };
        SetupMainLayoutServices(dashboardRunStore: runStore, sessionStorage: sessionStorage);
        JSInterop.SetupVoid("focusElement", _ => true).SetVoidResult();
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
        await cut.InvokeAsync(() => cut.Instance.SwitchDashboardRunAsync(null));
 
        Assert.Equal(string.Empty, storedRunId);
        await cut.InvokeAsync(() => runSelectionSource.SetResult((true, "historical")));
 
        var runSelection = Assert.IsType<FluentUISetupHelpers.TestDashboardRunSelection>(Services.GetRequiredService<IDashboardRunSelection>());
        Assert.True(runSelection.SelectedRun.IsCurrent);
        Assert.Empty(cut.FindAll(".dashboard-run-pill"));
    }
 
    [Theory]
    [InlineData(true, false, "dashboard-help-button", "HelpDialog", "dashboard-navigation-button")]
    [InlineData(true, false, "dashboard-settings-button", "SettingsDialog", "dashboard-navigation-button")]
    [InlineData(false, true, "dashboard-navigation-button", "HelpDialog", "dashboard-help-button")]
    [InlineData(false, true, "dashboard-navigation-button", "SettingsDialog", "dashboard-settings-button")]
    public async Task HeaderDialogClose_AfterViewportChange_RestoresFocusToVisibleLaunchButton(
        bool initialIsDesktop,
        bool closingIsDesktop,
        string launchButtonId,
        string expectedDialogId,
        string expectedFocusId)
    {
        DialogParameters? capturedParameters = null;
        TestDialogService? dialogService = null;
        dialogService = new TestDialogService(onShowDialog: (_, parameters) =>
        {
            capturedParameters = parameters;
            return Task.CompletedTask;
        });
 
        SetupMainLayoutServices(dialogService: dialogService);
        JSInterop.SetupVoid("focusElement", _ => true);
 
        var cut = Render<CascadingValue<ViewportInformation>>(builder =>
        {
            builder.Add(p => p.Value, new ViewportInformation(IsDesktop: initialIsDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.AddChildContent<MainLayout>();
        });
 
        if (initialIsDesktop)
        {
            await cut.InvokeAsync(() => cut.Find($"#{launchButtonId}").Click());
        }
        else
        {
            var menuItemName = expectedDialogId == "HelpDialog"
                ? Resources.Layout.MainLayoutKeyboardShortcuts
                : "Settings";
 
            await cut.InvokeAsync(() => cut.Find("#dashboard-navigation-button").Click());
            var menuItem = cut.FindAll("fluent-menu-item").Single(item => item.TextContent.Contains(menuItemName, StringComparison.OrdinalIgnoreCase));
            await cut.InvokeAsync(() => menuItem.TriggerEvent("onmenuitemchange", new MenuItemEventArgs { Id = menuItem.Id, Text = menuItem.TextContent }));
        }
 
        Assert.NotNull(capturedParameters);
        Assert.Equal(expectedDialogId, capturedParameters.Id);
 
        cut.Render(parameters =>
        {
            parameters.Add(p => p.Value, new ViewportInformation(IsDesktop: closingIsDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false));
            parameters.AddChildContent<MainLayout>();
        });
 
        await cut.InvokeAsync(() => capturedParameters.OnDialogClosing.InvokeAsync(null!));
 
        cut.WaitForAssertion(() =>
        {
            Assert.Contains(JSInterop.Invocations, invocation =>
                invocation.Identifier == "focusElement" &&
                invocation.Arguments.Count == 1 &&
                string.Equals((string?)invocation.Arguments[0], expectedFocusId, StringComparison.Ordinal));
        });
    }
 
    [Theory]
    [InlineData(AspireKeyboardShortcut.Help, "dashboard-help-button", "HelpDialog")]
    [InlineData(AspireKeyboardShortcut.Settings, "dashboard-settings-button", "SettingsDialog")]
    public async Task HeaderDialogShortcutClose_RestoresFocusToLaunchButton(AspireKeyboardShortcut shortcut, string launchButtonId, string expectedDialogId)
    {
        DialogParameters? capturedParameters = null;
        TestDialogService? dialogService = null;
        dialogService = new TestDialogService(onShowDialog: (_, parameters) =>
        {
            capturedParameters = parameters;
            return Task.CompletedTask;
        });
 
        SetupMainLayoutServices(dialogService: dialogService);
        JSInterop.SetupVoid("focusElement", _ => true);
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        await cut.InvokeAsync(() => cut.Instance.OnPageKeyDownAsync(shortcut));
 
        Assert.NotNull(capturedParameters);
        Assert.Equal(expectedDialogId, capturedParameters.Id);
 
        await cut.InvokeAsync(() => capturedParameters.OnDialogClosing.InvokeAsync(null!));
 
        cut.WaitForAssertion(() =>
        {
            Assert.Contains(JSInterop.Invocations, invocation =>
                invocation.Identifier == "focusElement" &&
                invocation.Arguments.Count == 1 &&
                string.Equals((string?)invocation.Arguments[0], launchButtonId, StringComparison.Ordinal));
        });
    }
 
    [Theory]
    [InlineData(AspireKeyboardShortcut.GoToResources)]
    [InlineData(AspireKeyboardShortcut.GoToConsoleLogs)]
    public async Task ResourceServiceShortcut_WhenResourceServiceIsDisabled_DoesNotNavigate(AspireKeyboardShortcut shortcut)
    {
        SetupMainLayoutServices(dashboardClient: new TestDashboardClient(isEnabled: false));
 
        var cut = Render<MainLayout>(builder =>
        {
            builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        var initialUri = navigationManager.Uri;
 
        await cut.InvokeAsync(() => cut.Instance.OnPageKeyDownAsync(shortcut));
 
        Assert.False(cut.Instance.SubscribedShortcuts.Contains(shortcut));
        Assert.Equal(initialUri, navigationManager.Uri);
    }
 
    private void SetupMainLayoutServices(
        TestLocalStorage? localStorage = null,
        Action<DashboardOptions>? configureOptions = null,
        IDialogService? dialogService = null,
        BrowserTimeProvider? browserTimeProvider = null,
        IDashboardRunStore? dashboardRunStore = null,
        ISessionStorage? sessionStorage = null,
        TestDashboardClient? dashboardClient = null)
    {
        FluentUISetupHelpers.AddCommonDashboardServices(
            this,
            localStorage: localStorage,
            browserTimeProvider: browserTimeProvider,
            dashboardRunStore: dashboardRunStore,
            sessionStorage: sessionStorage);
 
        if (dialogService is not null)
        {
            Services.AddSingleton(dialogService);
        }
 
        Services.AddOptions();
        Services.AddSingleton<IThemeResolver, TestThemeResolver>();
        dashboardClient ??= new TestDashboardClient();
        Services.AddSingleton<IDashboardClient>(dashboardClient);
        Services.AddKeyedSingleton<IDashboardClient>(DashboardClient.LiveAppHostServiceKey, dashboardClient);
        Services.AddSingleton<ITooltipService, TooltipService>();
        Services.Configure<DashboardOptions>(o =>
        {
            // Configure OTLP endpoint URLs so they can be parsed
            o.Otlp.GrpcEndpointUrl = "http://localhost:4317";
            o.Otlp.AuthMode = OtlpAuthMode.Unsecured;
            configureOptions?.Invoke(o);
            // Call TryParseOptions to populate parsed endpoint addresses
            o.Otlp.TryParseOptions(out _);
        });
 
        FluentUISetupHelpers.SetupFluentDialogProvider(this);
        FluentUISetupHelpers.SetupFluentOverflow(this);
        FluentUISetupHelpers.SetupFluentAnchor(this);
        FluentUISetupHelpers.SetupFluentButton(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
        FluentUISetupHelpers.SetupFluentDivider(this);
        FluentUISetupHelpers.SetupFluentKeyCode(this);
 
        _messageBarProvider = Render<FluentMessageBarProvider>(builder =>
        {
            builder.Add(p => p.Section, DashboardUIHelpers.MessageBarSection);
        });
        FluentUISetupHelpers.SetupFluentInputLabel(this);
        FluentUISetupHelpers.SetupFluentList(this);
        FluentUISetupHelpers.SetupFluentCombobox(this);
        FluentUISetupHelpers.SetupAspireMenuButtonModule(this);
 
        var themeModule = JSInterop.SetupModule("/js/app-theme.js");
        themeModule.Setup<string>("updateTheme", _ => true).SetResult(ThemeManager.ThemeSettingSystem);
        JSInterop.SetupVoid("Blazor.theme.setThemeMode", _ => true).SetVoidResult();
 
        JSInterop.SetupModule("window.registerGlobalKeydownListener", _ => true);
        JSInterop.SetupModule("window.registerOpenTextVisualizerOnClick", _ => true);
        JSInterop.SetupVoid("registerResourceServiceConnectionProvider", _ => true).SetVoidResult();
        JSInterop.SetupVoid("updateResourceServiceConnectionState", _ => true).SetVoidResult();
        LayoutSetupHelpers.SetupMobileNavMenuKeyboardNavigation(this);
 
        JSInterop.Setup<BrowserInfo>("window.getBrowserInfo").SetResult(new BrowserInfo { TimeZone = "abc", UserAgent = "mozilla" });
    }
 
    private sealed class RecordingJSRuntime : IJSRuntime
    {
        public List<Invocation> Invocations { get; } = [];
 
        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
        {
            Invocations.Add(new Invocation(identifier, args ?? []));
            return ValueTask.FromResult(default(TValue)!);
        }
 
        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
        {
            Invocations.Add(new Invocation(identifier, args ?? []));
            return ValueTask.FromResult(default(TValue)!);
        }
 
        public sealed record Invocation(string Identifier, object?[] Arguments);
    }
}