File: Shared\FluentUISetupHelpers.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 Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.BrowserStorage;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.ServiceClient;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Tests;
using Aspire.Dashboard.Utils;
using Aspire.Tests.Utils;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.FluentUI.AspNetCore.Components;
 
namespace Aspire.Dashboard.Components.Tests.Shared;
 
internal static class FluentUISetupHelpers
{
    private static readonly Version s_fluentUIVersion = typeof(FluentButton).Assembly.GetName().Version!;
 
    private static string GetFluentFile(string filePath)
    {
        return $"{filePath}?v={s_fluentUIVersion}";
    }
 
    public static void SetupFluentDialogProvider(TestContext context)
    {
        var dialogProviderModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Dialog/FluentDialogProvider.razor.js"));
        dialogProviderModule.SetupModule("getActiveElement", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Dialog.Show", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Dialog.Hide", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Dialog.FocusOnPreviousActiveElement", _ => true).SetVoidResult();
    }
 
    public static void SetupFluentMenu(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Menu.Initialize", _ => true).SetVoidResult();
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Menu.OpenMenu", _ => true).SetVoidResult();
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Menu.CloseMenu", _ => true).SetVoidResult();
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Menu.Dispose", _ => true).SetVoidResult();
    }
 
    public static void SetupFluentOverflow(TestContext context)
    {
        var overflowModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Overflow/FluentOverflow.razor.js"));
        overflowModule.SetupVoid("fluentOverflowInitialize", _ => true);
        overflowModule.SetupVoid("fluentOverflowDispose", _ => true);
    }
 
    public static void SetupFluentAnchor(TestContext context)
    {
        context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Anchor/FluentAnchor.razor.js"));
    }
 
    public static void SetupFluentAnchoredRegion(TestContext context)
    {
        var module = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/AnchoredRegion/FluentAnchoredRegion.razor.js"));
        module.SetupVoid("goToNextFocusableElement", _ => true);
        module.SetupVoid("initializeKeyboardNavigation", _ => true);
        module.SetupVoid("removeKeyboardNavigation", _ => true);
    }
 
    public static void SetupFluentDivider(TestContext context)
    {
        var dividerModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Divider/FluentDivider.razor.js"));
        dividerModule.SetupVoid("setDividerAriaOrientation");
    }
 
    public static void SetupFluentDataGrid(TestContext context)
    {
        var dataGridModule = context.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js");
        dataGridModule.SetupVoid("Microsoft.FluentUI.Blazor.DataGrid.EnableColumnResizing", _ => true);
 
        var gridReference = dataGridModule.SetupModule("Microsoft.FluentUI.Blazor.DataGrid.Initialize", _ => true);
        gridReference.SetupVoid("stop", _ => true);
    }
 
    public static void SetupFluentSearch(TestContext context)
    {
        var searchModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Search/FluentSearch.razor.js"));
        searchModule.SetupVoid("addAriaHidden", _ => true);
    }
 
    public static void SetupFluentKeyCode(TestContext context)
    {
        var keycodeModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/KeyCode/FluentKeyCode.razor.js"));
        keycodeModule.Setup<string>("RegisterKeyCode", _ => true);
    }
 
    public static void SetupFluentToolbar(TestContext context)
    {
        var toolbarModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Toolbar/FluentToolbar.razor.js"));
        toolbarModule.SetupVoid("removePreventArrowKeyNavigation", _ => true);
    }
 
    public static void SetupFluentInputLabel(TestContext context)
    {
        var inputLabelModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Label/FluentInputLabel.razor.js"));
        inputLabelModule.SetupVoid("setInputAriaLabel", _ => true);
    }
 
    public static void SetupFluentList(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Select.Initialize", _ => true);
    }
 
    public static void SetupFluentTab(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Tabs.ObserveTabsChanged", _ => true);
    }
 
    public static void SetupFluentCheckbox(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", _ => true);
        var checkboxModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Checkbox/FluentCheckbox.razor.js"));
        checkboxModule.SetupVoid("setFluentCheckBoxIndeterminate", _ => true);
        checkboxModule.SetupVoid("stop", _ => true);
    }
 
    public static void SetupFluentTextField(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.applyShadowStyle", _ => true);
    }
 
    public static void SetupFluentButton(TestContext context)
    {
        var buttonModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Button/FluentButton.razor.js"));
        buttonModule.SetupVoid("updateProxy", _ => true);
    }
 
    public static void SetupFluentInputFile(TestContext context)
    {
        var inputFileModule = context.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/InputFile/FluentInputFile.razor.js");
        inputFileModule.SetupVoid("Microsoft.FluentUI.Blazor.InputFile.AttachClickHandler", _ => true);
        var dropZoneReference = inputFileModule.SetupModule("Microsoft.FluentUI.Blazor.InputFile.InitializeFileDropZone", _ => true);
        dropZoneReference.SetupVoid("dispose", _ => true);
    }
 
    public static void SetupFluentCombobox(TestContext context)
    {
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.copyToShadow", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Select.UpdateValue", _ => true);
    }
 
    public static async Task ConfigureTelemetryRepository(
        TestContext context,
        bool readOnly,
        Func<ITelemetryRepositoryWriter, Task> seed)
    {
        context.Services.AddSingleton(new TelemetryRepositoryConfiguration(readOnly));
 
        var databasePath = Path.Combine(context.Services.GetRequiredService<TemporaryWorkspace>().Path, "dashboard.db");
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        var options = context.Services.GetRequiredService<IOptions<DashboardOptions>>();
        var outgoingPeerResolvers = context.Services.GetServices<IOutgoingPeerResolver>();
 
        using var database = new DashboardSqliteDatabase(databasePath, pooling: false);
        await database.InitializeSchemaAsync(CancellationToken.None);
        using var writer = new SqliteTelemetryRepository(
            database,
            loggerFactory,
            options,
            new PauseManager(),
            context.Services.GetRequiredService<TimeProvider>(),
            outgoingPeerResolvers);
        await seed(writer);
    }
 
    public static void AddCommonDashboardServices(
        TestContext context,
        ILocalStorage? localStorage = null,
        ISessionStorage? sessionStorage = null,
        ThemeManager? themeManager = null,
        BrowserTimeProvider? browserTimeProvider = null,
        IDashboardRunStore? dashboardRunStore = null)
    {
        context.Services.AddFluentUIComponents();
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", _ => true);
        context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Utilities.Attributes.copyToShadow", _ => true);
        context.JSInterop.SetupVoid("Blazor.theme.setThemeMode", _ => true);
        var tooltipModule = context.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Tooltip/FluentTooltip.razor.js");
        tooltipModule.SetupVoid("Microsoft.FluentUI.Blazor.Tooltip.FluentTooltipInitialize", _ => true);
        context.Services.AddLocalization();
        context.Services.AddSingleton<BrowserTimeProvider>(browserTimeProvider ?? new TestTimeProvider());
        context.Services.AddSingleton(TimeProvider.System);
        context.Services.AddSingleton(_ => TemporaryWorkspace.Create(
            global::Xunit.TestContext.Current.TestOutputHelper ?? throw new InvalidOperationException("An active test output helper is required.")));
        context.Services.AddSingleton(services =>
        {
            var databasePath = Path.Combine(services.GetRequiredService<TemporaryWorkspace>().Path, "dashboard.db");
            var configuration = services.GetService<TelemetryRepositoryConfiguration>();
            var database = new DashboardSqliteDatabase(databasePath, readOnly: configuration?.ReadOnly == true, pooling: false);
            if (!database.IsReadOnly)
            {
                database.InitializeSchemaAsync(CancellationToken.None).GetAwaiter().GetResult();
            }
            return database;
        });
        context.Services.AddSingleton<SqliteTelemetryRepository>(services =>
        {
            var loggerFactory = services.GetRequiredService<ILoggerFactory>();
            var options = services.GetRequiredService<IOptions<DashboardOptions>>();
            var pauseManager = services.GetRequiredService<PauseManager>();
            var outgoingPeerResolvers = services.GetServices<IOutgoingPeerResolver>();
 
            return new SqliteTelemetryRepository(
                services.GetRequiredService<DashboardSqliteDatabase>(),
                loggerFactory,
                options,
                pauseManager,
                services.GetRequiredService<TimeProvider>(),
                outgoingPeerResolvers);
        });
        context.Services.AddSingleton<ITelemetryRepository>(services => services.GetRequiredService<SqliteTelemetryRepository>());
        context.Services.AddSingleton<ITelemetryRepositoryWriter>(services => services.GetRequiredService<SqliteTelemetryRepository>());
        context.Services.AddSingleton<PauseManager>();
        context.Services.AddScoped<NavigationDialogService>();
        context.Services.AddScoped<IDialogService>(services => services.GetRequiredService<NavigationDialogService>());
        context.Services.AddSingleton<ILocalStorage>(localStorage ?? new TestLocalStorage());
        context.Services.AddSingleton<ISessionStorage>(sessionStorage ?? new TestSessionStorage());
        context.Services.AddSingleton<IDashboardRunStore>(services => dashboardRunStore ?? new TestDashboardRunStore(
            databasePath: Path.Combine(services.GetRequiredService<TemporaryWorkspace>().Path, "dashboard.db")));
        context.Services.AddSingleton<IDashboardRunSelection, TestDashboardRunSelection>();
        context.Services.AddSingleton<IDashboardClient, TestDashboardClient>();
        context.Services.AddSingleton<IResourceRepository>(services => services.GetRequiredService<IDashboardClient>());
        context.Services.AddSingleton<IRepositoryFactory, TestRepositoryFactory>();
        context.Services.AddSingleton<DashboardDataSourcePool>();
        context.Services.AddScoped<DashboardDataSource>();
        context.Services.AddSingleton<ShortcutManager>();
        context.Services.AddSingleton<LibraryConfiguration>();
        context.Services.AddSingleton<IKeyCodeService, KeyCodeService>();
        context.Services.AddSingleton<DashboardTelemetryService>();
        context.Services.AddSingleton<DashboardActivitySource>();
        context.Services.AddSingleton<IDashboardTelemetrySender, TestDashboardTelemetrySender>();
        context.Services.AddSingleton<ComponentTelemetryContextProvider>();
        context.Services.AddSingleton<ITelemetryErrorRecorder, TestTelemetryErrorRecorder>();
        context.Services.AddSingleton<ThemeManager>(themeManager ?? new ThemeManager(new TestThemeResolver()));
        context.Services.AddSingleton<DimensionManager>();
        context.Services.AddSingleton(TimeProvider.System);
        context.Services.AddSingleton<Aspire.Dashboard.Model.INotificationService, Aspire.Dashboard.Model.NotificationService>();
        context.Services.AddScoped<DashboardDialogService>();
        context.Services.AddScoped<DashboardMessageBarService>();
        context.Services.AddScoped<ResourceMenuBuilder>();
        context.Services.AddScoped<StructuredLogMenuBuilder>();
        context.Services.AddScoped<SpanMenuBuilder>();
        context.Services.AddScoped<TraceMenuBuilder>();
        context.Services.AddSingleton<IOptions<DashboardOptions>>(Options.Create(new DashboardOptions()));
    }
 
    internal sealed class TestDashboardRunStore(
        IEnumerable<DashboardRunDescriptor>? runs = null,
        bool supportsRunSelection = true,
        string? databasePath = null) : IDashboardRunStore
    {
        private readonly IReadOnlyList<DashboardRunDescriptor> _runs = (runs ??
            [
                new(
                RunId: "current",
                SchemaVersion: DashboardRunStore.SchemaVersion,
                StartedAtUtc: DateTimeOffset.UnixEpoch,
                EndedAtUtc: null,
                CleanShutdown: false,
                ApplicationName: "TestApp",
                DatabasePath: databasePath ?? string.Empty,
                IsCurrent: true)
            ]).ToArray();
 
        public int GetRunsCallCount { get; private set; }
 
        public Action<DashboardRunDescriptor, bool>? OnSetRunPinned { get; set; }
 
        public IReadOnlyList<DashboardRunDescriptor> GetRuns()
        {
            GetRunsCallCount++;
            return _runs;
        }
 
        public DashboardRunDescriptor GetCurrentRun() => _runs.Single(run => run.IsCurrent);
 
        public DashboardRunDescriptor? GetRunById(string runId) =>
            _runs.SingleOrDefault(run => string.Equals(run.RunId, runId, StringComparison.Ordinal));
 
        public void SetRunPinned(DashboardRunDescriptor run, bool isPinned)
        {
            OnSetRunPinned?.Invoke(run, isPinned);
            _runs.Single(candidate => string.Equals(candidate.RunId, run.RunId, StringComparison.Ordinal)).IsPinned = isPinned;
        }
 
        public IDisposable? TryAcquireRunLease(DashboardRunDescriptor run) => null;
 
        public void PublishRun()
        {
        }
 
        public void PruneExpiredRuns()
        {
        }
 
        public bool SupportsRunSelection => supportsRunSelection;
    }
 
    internal sealed class TestDashboardRunSelection(IDashboardRunStore runStore) : IDashboardRunSelection
    {
        public DashboardRunDescriptor SelectedRun { get; private set; } = runStore.GetCurrentRun();
 
        public string? SelectedRunId { get; private set; }
 
        public Action<string?>? OnSelectRun { get; set; }
 
        public void SelectRun(string? runId)
        {
            OnSelectRun?.Invoke(runId);
            SelectedRun = runId is not null ? runStore.GetRunById(runId) ?? runStore.GetCurrentRun() : runStore.GetCurrentRun();
            SelectedRunId = SelectedRun.IsCurrent ? null : SelectedRun.RunId;
        }
    }
 
    private sealed class TestRepositoryFactory(
        ITelemetryRepository telemetryRepository,
        IDashboardClient dashboardClient) : IRepositoryFactory
    {
        public ITelemetryRepository CreateTelemetryRepository(DashboardSqliteDatabase database) => telemetryRepository;
        public IResourceRepository CreateResourceRepository(DashboardSqliteDatabase database) => dashboardClient;
    }
 
    public static void SetupFluentUIComponents(TestContext context, bool setupAspireMenuButtonModule = true)
    {
        context.Services.AddFluentUIComponents();
        context.Services.AddScoped<NavigationDialogService>();
        context.Services.AddScoped<IDialogService>(services => services.GetRequiredService<NavigationDialogService>());
 
        if (setupAspireMenuButtonModule)
        {
            SetupAspireMenuButtonModule(context);
        }
    }
 
    /// <summary>
    /// Registers the JS module <see cref="AspireMenuButton"/> imports when its menu is first opened.
    /// Tests that click a menu button need this even when they configure FluentUI piecemeal.
    /// </summary>
    public static void SetupAspireMenuButtonModule(TestContext context)
    {
        var menuButtonModule = context.JSInterop.SetupModule("./Components/Controls/AspireMenuButton.razor.js");
        menuButtonModule.SetupVoid("prepareForFluentMenuInitialization", _ => true).SetVoidResult();
        menuButtonModule.SetupVoid("waitForFluentMenuInitialization", _ => true).SetVoidResult();
        menuButtonModule.SetupVoid("cancelFluentMenuInitialization", _ => true).SetVoidResult();
    }
 
    public static void SetupDialogInfrastructure(
        TestContext context,
        ThemeManager? themeManager = null,
        ILocalStorage? localStorage = null)
    {
        AddCommonDashboardServices(context, localStorage: localStorage, themeManager: themeManager);
        SetupFluentUIComponents(context);
        SetupFluentDialogProvider(context);
    }
 
    public static IRenderedFragment RenderDialogProvider(TestContext context)
    {
        return context.Render(builder =>
        {
            builder.OpenComponent<FluentDialogProvider>(0);
            builder.CloseComponent();
        });
    }
 
    public static IRenderedFragment RenderMessageBarProviderWithPage<TPage>(TestContext context, ViewportInformation viewport)
        where TPage : IComponent
    {
        // Keep the provider and page in the same render tree, matching MainLayout so message bars
        // raised while rendering the page are delivered to the provider under test.
        return context.Render(builder =>
        {
            builder.OpenComponent<FluentMessageBarProvider>(0);
            builder.AddComponentParameter(1, nameof(FluentMessageBarProvider.Section), DashboardUIHelpers.MessageBarSection);
            builder.CloseComponent();
            builder.OpenComponent<CascadingValue<ViewportInformation>>(2);
            builder.AddComponentParameter(3, nameof(CascadingValue<ViewportInformation>.Value), viewport);
            builder.AddComponentParameter(4, nameof(CascadingValue<ViewportInformation>.ChildContent), (RenderFragment)(contentBuilder =>
            {
                contentBuilder.OpenComponent<TPage>(5);
                contentBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
    }
 
    private sealed record TelemetryRepositoryConfiguration(bool ReadOnly);
}