File: Model\DashboardCommandExecutorTests.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.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Dashboard.Utils;
using Aspire.Tests.Shared.DashboardModel;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Model;
 
public class DashboardCommandExecutorTests : DashboardTestContext
{
    [Theory]
    [InlineData("Playwright exited with code 1.\nExpected: <button>Retry</button>\nExpected: 1 > 0")]
    [InlineData("<div>Failure</div>")]
    [InlineData("<div>Failure</div><img src=\"x\" onerror=\"alert(1)\"><script>alert(1)</script>")]
    [InlineData("Already escaped: &lt;button&gt; & \"quoted\"")]
    [InlineData("Command failed")]
    [InlineData("Command failed \U0001F600")]
    [InlineData("")]
    [InlineData(null)]
    public async Task ExecuteAsyncCore_FailedCommand_RendersMessageAsPlainText(string? message)
    {
        await AssertFailureMessageAsync(message, message);
    }
 
    [Theory]
    [InlineData('&', 249)]
    [InlineData('&', 250)]
    [InlineData('&', 251)]
    [InlineData('&', 10000)]
    [InlineData('<', 249)]
    [InlineData('<', 250)]
    [InlineData('<', 251)]
    [InlineData('<', 10000)]
    public async Task ExecuteAsyncCore_FailedCommand_TruncatesPlainTextBeforeEncoding(char character, int messageLength)
    {
        var message = new string(character, messageLength);
        var expectedMessage = messageLength <= 250 ? message : message[..249] + FormatHelpers.Ellipsis;
 
        await AssertFailureMessageAsync(message, expectedMessage);
    }
 
    [Theory]
    [InlineData(247, true)]
    [InlineData(248, false)]
    [InlineData(249, false)]
    public async Task ExecuteAsyncCore_FailedCommand_TruncationPreservesSurrogatePairs(int prefixLength, bool includeEmoji)
    {
        var prefix = new string('a', prefixLength);
        var message = prefix + "\U0001F600" + " Additional diagnostic text";
        var expectedMessage = prefix + (includeEmoji ? "\U0001F600" : "") + FormatHelpers.Ellipsis;
 
        await AssertFailureMessageAsync(message, expectedMessage);
    }
 
    private async Task AssertFailureMessageAsync(string? message, string? expectedMessage)
    {
        var response = new ResourceCommandResponseViewModel
        {
            Kind = ResourceCommandResponseKind.Failed,
            Message = message
        };
        var dashboardClient = new TestDashboardClient(
            isEnabled: true,
            executeResourceCommand: (_, _, _, _, _) => Task.FromResult(response));
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        Services.AddSingleton<IDashboardClient>(dashboardClient);
        Services.AddScoped<DashboardCommandExecutor>();
 
        var toasts = Render<FluentToastProvider>();
        var executor = Services.GetRequiredService<DashboardCommandExecutor>();
        var command = new CommandViewModel(
            name: "test",
            state: CommandViewModelState.Enabled,
            displayName: "Run tests",
            displayDescription: "Run tests",
            confirmationMessage: "",
            argumentInputs: [],
            isHighlighted: false,
            iconName: "",
            iconVariant: IconVariant.Regular);
        var resource = ModelTestHelpers.CreateResource(resourceName: "api", commands: [command]);
 
        await toasts.InvokeAsync(() => executor.ExecuteAsyncCore(resource, command, r => r.DisplayName))
            .WaitAsync(TimeSpan.FromSeconds(10));
        toasts.Render();
 
        var toast = Assert.Single(toasts.FindComponents<FluentToast>());
        Assert.Equal(ToastIntent.Error, toast.Instance.Intent);
        if (string.IsNullOrEmpty(expectedMessage))
        {
            Assert.Null(toast.Instance.ChildContent);
        }
        else
        {
            var body = toast.Find($"[id=\"{toast.Instance.Id}-body\"]");
            Assert.Equal(expectedMessage, body.TextContent);
            Assert.Empty(body.Children);
        }
 
        var notifications = Services.GetRequiredService<Aspire.Dashboard.Model.INotificationService>();
        Assert.Equal(message, Assert.Single(notifications.GetNotifications()).Entry.Body);
        Assert.Equal(message, response.Message);
    }
}