File: Dialogs\InteractionsTerminalDialogTests.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.Controls;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Interaction;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Dialogs;
 
[UseCulture("en-US")]
public sealed class InteractionsTerminalDialogTests : DashboardTestContext
{
    [Theory]
    [InlineData("", "terminal", "terminal")]
    [InlineData("/aspire/nested", "terminal", "terminal")]
    [InlineData("", "terminal #1/?%+", "terminal%20%231%2F%3F%25%2B")]
    [InlineData("/aspire/nested", "terminal #1/?%+", "terminal%20%231%2F%3F%25%2B")]
    public async Task AppHostTerminalEndpoint_UsesDashboardBaseUri(string pathBase, string terminalId, string escapedTerminalId)
    {
        Services.AddSingleton<NavigationManager>(new TestNavigationManager($"https://dashboard.example{pathBase}/"));
        TerminalSetupHelpers.SetupTerminalView(this, pathBase);
        var getCut = InteractionsSetupHelpers.SetupDialog<InteractionsTerminalDialog, InteractionsTerminalDialogViewModel>(this, p => p.Content, out var dialogService);
        Services.GetRequiredService<NavigationManager>().NavigateTo("consolelogs/resource/other");
        var viewModel = new InteractionsTerminalDialogViewModel { TerminalId = terminalId, Message = "Message" };
 
        await dialogService.ShowDialogAsync<InteractionsTerminalDialog>(viewModel, new DialogParameters { Title = "Shell" });
 
        var cut = getCut();
        cut.WaitForAssertion(() => TerminalSetupHelpers.AssertSingleTerminalConnection(this,
            $"wss://dashboard.example{pathBase}/api/apphost-terminal?terminalId={escapedTerminalId}"));
        var terminal = cut.FindComponent<TerminalView>().Instance;
        Assert.True(terminal.Chromeless);
        Assert.True(terminal.AutoFit);
        Assert.False(terminal.ShowDimensionsPicker);
        Assert.False(terminal.ReadOnly);
        Assert.Empty(cut.FindComponents<TerminalWindowButton>());
        Assert.Equal("Message", cut.Find(".interaction-message").TextContent.Trim());
 
        await cut.InvokeAsync(() => viewModel.UpdateMessageAsync("<strong>Updated</strong>"));
        cut.WaitForAssertion(() => Assert.Equal("Updated", cut.Find(".interaction-message strong").TextContent));
        Assert.Same(terminal, cut.FindComponent<TerminalView>().Instance);
        TerminalSetupHelpers.AssertSingleTerminalConnection(this,
            $"wss://dashboard.example{pathBase}/api/apphost-terminal?terminalId={escapedTerminalId}");
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("Cancel work")]
    public async Task PrimaryAction_IsOptionalAndAlwaysCancels(string? buttonText)
    {
        TerminalSetupHelpers.SetupTerminalView(this);
        var getCut = InteractionsSetupHelpers.SetupDialog<InteractionsTerminalDialog, InteractionsTerminalDialogViewModel>(this, p => p.Content, out var dialogService);
        var viewModel = new InteractionsTerminalDialogViewModel { TerminalId = "terminal", Message = string.Empty };
        var dialog = await dialogService.ShowDialogAsync<InteractionsTerminalDialog>(viewModel,
            new DialogParameters { PrimaryAction = buttonText, UseCustomFooter = true });
        var cut = getCut();
        var action = cut.FindComponents<FluentButton>().Where(b => b.Instance.Class == "aspire-button aspire-neutral-button").ToList();
        if (string.IsNullOrEmpty(buttonText))
        {
            Assert.Empty(action);
            Assert.False(dialog.Result.IsCompleted);
            await dialog.CloseAsync().DefaultTimeout();
        }
        else
        {
            var button = Assert.Single(action);
            Assert.Equal(buttonText, button.Find("fluent-button").TextContent.Trim());
            await cut.InvokeAsync(button.Instance.OnClick.InvokeAsync);
            var result = await dialog.Result.DefaultTimeout();
            Assert.False(result.Cancelled);
            Assert.Equal("cancel", result.Value);
        }
    }
 
    [Fact]
    public async Task Dispose_DisconnectsOnlyTheViewerAndUnsubscribesUpdates()
    {
        TerminalSetupHelpers.SetupTerminalView(this);
        var getCut = InteractionsSetupHelpers.SetupDialog<InteractionsTerminalDialog, InteractionsTerminalDialogViewModel>(this, p => p.Content, out var dialogService);
        var viewModel = new InteractionsTerminalDialogViewModel { TerminalId = "terminal", Message = string.Empty };
        var dialog = await dialogService.ShowDialogAsync<InteractionsTerminalDialog>(viewModel, new DialogParameters());
        var cut = getCut();
        cut.WaitForAssertion(() => Assert.Single(JSInterop.Invocations, i => i.Identifier == "initTerminal"));
 
        // Remove the dialog through the renderer so Blazor disposes its entire component subtree.
        var host = Assert.IsAssignableFrom<IRenderedComponent<CascadingValue<IDialogInstance>>>(cut);
        host.Render(parameters => parameters
            .Add(p => p.Value, host.Instance.Value)
            .AddChildContent(string.Empty));
 
        cut.WaitForAssertion(() => Assert.Single(JSInterop.Invocations, i => i.Identifier == "disposeTerminal"));
        Assert.Null(viewModel.OnInteractionUpdated);
        Assert.False(dialog.Result.IsCompleted);
        await viewModel.UpdateMessageAsync("After disposal");
        await dialog.CloseAsync().DefaultTimeout();
    }
}