File: Shared\TerminalSetupHelpers.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.Globalization;
using System.Threading.Channels;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Terminal;
using Aspire.Dashboard.Tests.Shared;
using Aspire.DashboardService.Proto.V1;
using Aspire.Tests.Shared.DashboardModel;
using Bunit;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.JSInterop;
using Assert = Xunit.Assert;
 
namespace Aspire.Dashboard.Components.Tests.Shared;
 
internal static class TerminalSetupHelpers
{
    public static TestDashboardClient CreateTerminalDashboardClient(
        Func<Channel<WatchTerminalsUpdate>>? terminalChannelProvider = null,
        Func<string, CancellationToken, Task>? closeTerminal = null)
        => new(isEnabled: true,
            resourceChannelProvider: () => Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>(),
            terminalChannelProvider: terminalChannelProvider,
            closeTerminal: closeTerminal);
 
    public static ResourceViewModel CreateTerminalResource(
        string resourceName,
        int replicaIndex = 0,
        int replicaCount = 1,
        string? displayName = null,
        KnownResourceState state = KnownResourceState.Running,
        bool hidden = false)
    {
        var properties = new Dictionary<string, string>
        {
            [KnownProperties.Terminal.Enabled] = "true",
            [KnownProperties.Terminal.ReplicaIndex] = replicaIndex.ToString(CultureInfo.InvariantCulture),
            [KnownProperties.Terminal.ReplicaCount] = replicaCount.ToString(CultureInfo.InvariantCulture)
        }.ToDictionary(pair => pair.Key, pair => new ResourcePropertyViewModel(
            pair.Key, Value.ForString(pair.Value), isValueSensitive: false, knownProperty: null,
            sortOrder: 0, displayName: null, isHighlighted: false));
 
        return ModelTestHelpers.CreateResource(resourceName, state, displayName, properties: properties, hidden: hidden);
    }
 
    public static void SetupTerminalComponents(BunitContext context, TestDashboardClient client, string pathBase = "")
    {
        FluentUISetupHelpers.AddCommonDashboardServices(context);
        FluentUISetupHelpers.SetupFluentUIComponents(context);
        FluentUISetupHelpers.SetupFluentButton(context);
        context.Services.AddSingleton<IDashboardClient>(client);
        context.JSInterop.Setup<string>("Blazor._internal.PageTitle.getAndRemoveExistingTitle", _ => true).SetResult(string.Empty);
        SetupTerminalView(context, pathBase);
        SetupTerminalDock(context, pathBase);
    }
 
    public static void SetupTerminalView(BunitContext context, string pathBase = "")
    {
        var module = SetupTerminalViewModule(context, $"{pathBase}/Components/Controls/TerminalView.razor.js");
        module.Setup<int>("initTerminal", _ => true).SetResult(1);
        module.SetupVoid("setReadOnly", _ => true).SetVoidResult();
    }
 
    public static BunitJSModuleInterop SetupTerminalViewModule(BunitContext context, string modulePath)
    {
        context.Services.TryAddSingleton<TerminalViewSessionRegistry>();
        FluentUISetupHelpers.SetupFluentList(context);
        var module = context.JSInterop.SetupModule(modulePath);
        module.Setup<int>("reconnectTerminal", _ => true).SetResult(2);
        module.SetupVoid("disposeTerminal", _ => true).SetVoidResult();
        module.SetupVoid("dismissError", _ => true).SetVoidResult();
        module.SetupVoid("refreshLayout", _ => true).SetVoidResult();
        module.SetupVoid("setAutoFit", _ => true).SetVoidResult();
        module.SetupVoid("fitToContainer", _ => true).SetVoidResult();
        module.Setup<TerminalSizePreset[]>("getSizePresets").SetResult(
            [new("80x24", "80×24", 80, 24)]);
        return module;
    }
 
    public static void SetupTerminalDock(BunitContext context, string pathBase = "")
    {
        var dock = context.JSInterop.SetupModule("./Components/Layout/TerminalDock.razor.js");
        dock.SetupVoid("registerResizeHandle", _ => true).SetVoidResult();
        dock.SetupVoid("unregisterResizeHandle", _ => true).SetVoidResult();
        dock.SetupVoid("registerTabNavigation", _ => true).SetVoidResult();
        dock.SetupVoid("unregisterTabNavigation", _ => true).SetVoidResult();
 
        SetupTerminalWindows(context, pathBase);
    }
 
    public static BunitJSModuleInterop SetupTerminalWindows(BunitContext context, string pathBase = "")
    {
        var windows = context.JSInterop.SetupModule($"{pathBase}/js/app-terminalwindow.js");
        windows.SetupVoid("registerTerminalWindowButton", _ => true).SetVoidResult();
        windows.SetupVoid("adoptTerminalWindows", _ => true).SetVoidResult();
        windows.Setup<bool>("focusTerminalWindow", _ => true).SetResult(true);
        windows.SetupVoid("closeTerminalWindow", _ => true).SetVoidResult();
        windows.SetupVoid("unregisterTerminalWindowButton", _ => true).SetVoidResult();
        windows.Setup<bool>("registerDetachedTerminalWindow", _ => true).SetResult(true);
        windows.SetupVoid("unregisterDetachedTerminalWindow", _ => true).SetVoidResult();
        windows.SetupVoid("releaseDetachedTerminalWindow", _ => true).SetVoidResult();
        return windows;
    }
 
    public static TerminalWindowLauncher GetWindowLauncher(BunitContext context, IRenderedComponent<IComponent> component)
    {
        // A terminal-watch update can render a child before that child's OnAfterRenderAsync registers its listener.
        component.WaitForAssertion(() => Assert.Single(context.JSInterop.Invocations, i => i.Identifier == "registerTerminalWindowButton"));
        var registration = Assert.Single(context.JSInterop.Invocations, i => i.Identifier == "registerTerminalWindowButton");
        return Assert.IsType<DotNetObjectReference<TerminalWindowLauncher>>(registration.Arguments[2]).Value;
    }
 
    public static void AssertSingleTerminalConnection(BunitContext context, string expectedWebSocketUrl)
    {
        var invocation = Assert.Single(context.JSInterop.Invocations, invocation => invocation.Identifier == "initTerminal");
        var options = Assert.IsType<TerminalViewOptions>(invocation.Arguments[3]);
        Assert.Equal($"{expectedWebSocketUrl}&viewId={options.ViewId}", invocation.Arguments[1]);
        Assert.True(context.Services.GetRequiredService<TerminalViewSessionRegistry>().TryGet(
            options.ViewId, new Uri(expectedWebSocketUrl).PathAndQuery, out _));
    }
 
    public static WatchTerminalsUpdate Snapshot(params string[] terminalIds) => new()
    {
        Snapshot = new TerminalDescriptorList
        {
            Terminals = { terminalIds.Select(id => new TerminalDescriptor { TerminalId = id, Title = id }) }
        }
    };
 
    public static WatchTerminalsUpdate Change(TerminalChangeType changeType, string terminalId, string? title = null) => new()
    {
        Change = new TerminalChangeNotification
        {
            ChangeType = changeType,
            Terminal = new TerminalDescriptor { TerminalId = terminalId, Title = title ?? terminalId }
        }
    };
}