File: Controls\ResourceActionsTests.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.Collections.Concurrent;
using System.Threading.Channels;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Tests.Shared.DashboardModel;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
using DashboardResources = Aspire.Dashboard.Resources.Resources;
 
namespace Aspire.Dashboard.Components.Tests.Controls;
 
[UseCulture("en-US")]
public class ResourceActionsTests : DashboardTestContext
{
    [Theory]
    [InlineData(true, true, false, true)]
    [InlineData(false, true, false, false)]
    [InlineData(true, true, true, false)]
    [InlineData(true, false, false, false)]
    public async Task OutputShortcut_PrefersLiveTerminalAndRetainsConsoleLogsInMenu(
        bool hasTerminal, bool enabled, bool readOnly, bool expectTerminal)
    {
        var resource = hasTerminal
            ? TerminalSetupHelpers.CreateTerminalResource("shell")
            : ModelTestHelpers.CreateResource("shell");
        var client = new TestDashboardClient(isEnabled: enabled, initialResources: [resource],
            resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>,
            isReadOnly: readOnly);
        var cut = RenderActions(resource, client, isDesktop: true);
        var outputButton = cut.FindComponent<FluentButton>();
        Assert.Equal(expectTerminal ? TerminalStrings.TerminalTitle : DashboardResources.ResourceActionConsoleLogsText,
            outputButton.Instance.Title);
 
        await cut.InvokeAsync(outputButton.Instance.OnClick.InvokeAsync);
        var navigation = Services.GetRequiredService<NavigationManager>();
        var expectedPath = expectTerminal ? "/terminals/resource/shell" : "/consolelogs/resource/shell";
        Assert.Equal(navigation.ToAbsoluteUri(expectedPath).AbsoluteUri, navigation.Uri);
 
        var menuItems = cut.FindComponent<AspireMenuButton>().Instance.ItemsProvider();
        var expectedItems = new List<string> { ControlsStrings.ActionViewDetailsText };
        if (expectTerminal)
        {
            expectedItems.Add(TerminalStrings.TerminalTitle);
        }
        expectedItems.Add(DashboardResources.ResourceActionConsoleLogsText);
        expectedItems.Add(ControlsStrings.ViewJson);
        Assert.Equal(expectedItems, menuItems.Select(item => item.Text));
 
        var consoleLogs = Assert.Single(menuItems, item => item.Text == DashboardResources.ResourceActionConsoleLogsText);
        await cut.InvokeAsync(consoleLogs.OnClick!);
        Assert.Equal(navigation.ToAbsoluteUri("/consolelogs/resource/shell").AbsoluteUri, navigation.Uri);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task OutputShortcutAndMenu_PreserveReplicaSelection(bool multipleReplicas)
    {
        var resource = TerminalSetupHelpers.CreateTerminalResource("shell-abc123", displayName: "shell");
        var resources = new ConcurrentDictionary<string, ResourceViewModel>(StringComparers.ResourceName);
        resources[resource.Name] = resource;
        if (multipleReplicas)
        {
            var otherReplica = TerminalSetupHelpers.CreateTerminalResource("shell-def456", displayName: "shell");
            resources[otherReplica.Name] = otherReplica;
        }
        var client = new TestDashboardClient(isEnabled: true, initialResources: resources.Values.ToList(),
            resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>);
        var cut = RenderActions(resource, client, isDesktop: true, resources);
        var expectedName = multipleReplicas ? "shell-abc123" : "shell";
        var navigation = Services.GetRequiredService<NavigationManager>();
 
        await cut.InvokeAsync(() => cut.FindComponent<FluentButton>().Instance.OnClick.InvokeAsync());
        Assert.Equal(navigation.ToAbsoluteUri($"/terminals/resource/{expectedName}").AbsoluteUri, navigation.Uri);
 
        var menuItems = cut.FindComponent<AspireMenuButton>().Instance.ItemsProvider();
        var consoleLogs = Assert.Single(menuItems, item => item.Text == DashboardResources.ResourceActionConsoleLogsText);
        await cut.InvokeAsync(consoleLogs.OnClick!);
        Assert.Equal(navigation.ToAbsoluteUri($"/consolelogs/resource/{expectedName}").AbsoluteUri, navigation.Uri);
 
        var terminal = Assert.Single(menuItems, item => item.Text == TerminalStrings.TerminalTitle);
        await cut.InvokeAsync(terminal.OnClick!);
        Assert.Equal(navigation.ToAbsoluteUri($"/terminals/resource/{expectedName}").AbsoluteUri, navigation.Uri);
    }
 
    [Fact]
    public void OutputShortcut_UpdatesWithTerminalCapability()
    {
        var resource = ModelTestHelpers.CreateResource("shell");
        var client = new TestDashboardClient(isEnabled: true, initialResources: [resource],
            resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>);
        var cut = RenderActions(resource, client, isDesktop: true);
        Assert.Equal(DashboardResources.ResourceActionConsoleLogsText, cut.FindComponent<FluentButton>().Instance.Title);
 
        cut.Render(builder => builder.Add(component => component.Resource, TerminalSetupHelpers.CreateTerminalResource("shell")));
        Assert.Equal(TerminalStrings.TerminalTitle, cut.FindComponent<FluentButton>().Instance.Title);
 
        cut.Render(builder => builder.Add(component => component.Resource, resource));
        Assert.Equal(DashboardResources.ResourceActionConsoleLogsText, cut.FindComponent<FluentButton>().Instance.Title);
    }
 
    [Fact]
    public async Task MobileMenu_OffersTerminalAndConsoleLogs()
    {
        var resource = TerminalSetupHelpers.CreateTerminalResource("shell");
        var client = new TestDashboardClient(isEnabled: true, initialResources: [resource],
            resourceChannelProvider: Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>);
        var cut = RenderActions(resource, client, isDesktop: false);
        Assert.Single(cut.FindComponents<FluentButton>());
        var menuItems = cut.FindComponent<AspireMenuButton>().Instance.ItemsProvider();
        Assert.Equal(
            new[] { ControlsStrings.ActionViewDetailsText, TerminalStrings.TerminalTitle, DashboardResources.ResourceActionConsoleLogsText, ControlsStrings.ViewJson },
            menuItems.Select(item => item.Text));
 
        var terminal = Assert.Single(menuItems, item => item.Text == TerminalStrings.TerminalTitle);
        await cut.InvokeAsync(terminal.OnClick!);
        var navigation = Services.GetRequiredService<NavigationManager>();
        Assert.Equal(navigation.ToAbsoluteUri("/terminals/resource/shell").AbsoluteUri, navigation.Uri);
    }
 
    private IRenderedComponent<ResourceActions> RenderActions(
        ResourceViewModel resource, TestDashboardClient client, bool isDesktop,
        ConcurrentDictionary<string, ResourceViewModel>? resources = null)
    {
        var viewport = new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: !isDesktop);
        ResourceSetupHelpers.SetupResourcesPage(this, viewport, client);
 
        return Render<ResourceActions>(builder => builder
            .AddCascadingValue(viewport)
            .Add(component => component.Resource, resource)
            .Add(component => component.ResourceByName, resources ?? new ConcurrentDictionary<string, ResourceViewModel>(StringComparers.ResourceName))
            .Add(component => component.MaxHighlightedCount, 0)
            .Add(component => component.CommandSelected, EventCallback<CommandViewModel>.Empty)
            .Add(component => component.OnViewDetails, EventCallback<string?>.Empty)
            .Add(component => component.IsCommandExecuting, (_, _) => false));
    }
}