File: Controls\TerminalTitleTests.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.Tests.Shared;
using Bunit;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Controls;
 
[UseCulture("en-US")]
public class TerminalTitleTests : DashboardTestContext
{
    public TerminalTitleTests()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentButton(this);
        TerminalSetupHelpers.SetupTerminalTitle(this);
    }
 
    [Fact]
    public void Metadata_IsRenderedAsTextAndClearsToFallback()
    {
        var cut = Render<TerminalTitle>(builder => builder
            .Add(p => p.FallbackTitle, "shell")
            .Add(p => p.State, new TerminalToolbarState
            {
                Title = "<script>title</script>",
                WorkingDirectory = "/work/<app>",
                WorkingDirectoryUri = "file://remote/work/%3Capp%3E"
            }));
 
        Assert.Equal("<script>title</script>", cut.Find(".terminal-title").TextContent);
        var button = cut.Find(".terminal-directory");
        Assert.Equal("/work/<app>", cut.Find(".terminal-directory-measure").TextContent);
        Assert.Equal("/work/<app>", button.GetAttribute("data-text"));
        Assert.Equal("true", button.GetAttribute("data-copybutton"));
        Assert.Equal(Resources.ControlsStrings.GridValueCopyToClipboard, button.GetAttribute("data-precopy"));
        Assert.Equal(Resources.ControlsStrings.GridValueCopied, button.GetAttribute("data-postcopy"));
        Assert.Equal(Resources.TerminalStrings.TerminalCopyFailed, button.GetAttribute("data-copyfailed"));
        Assert.Equal("polite", cut.Find(".terminal-directory-container [role=status]").GetAttribute("aria-live"));
        Assert.False(button.HasAttribute("disabled"));
        Assert.Equal("Copy working directory: /work/<app>", button.GetAttribute("aria-label"));
        Assert.Empty(cut.FindAll("fluent-tooltip"));
        Assert.False(button.HasAttribute("title"));
        Assert.Single(cut.FindAll(".terminal-directory .copy-icon"));
        Assert.Empty(cut.FindAll("script, a"));
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            WorkingDirectory = "/a longer path/with spaces/and Unicode \u03bb",
        }));
        Assert.Equal("/a longer path/with spaces/and Unicode \u03bb", cut.Find(".terminal-directory").GetAttribute("data-text"));
        Assert.Equal("/a longer path/with spaces/and Unicode \u03bb", cut.Find(".terminal-directory-measure").TextContent);
        Assert.Equal("Copy working directory: /a longer path/with spaces/and Unicode \u03bb", cut.Find(".terminal-directory").GetAttribute("aria-label"));
        Assert.Equal(button.Id, cut.Find(".terminal-directory").Id);
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState()));
        Assert.Equal("shell", cut.Find(".terminal-title").TextContent);
        Assert.Empty(cut.FindAll(".terminal-directory"));
    }
 
    [Fact]
    public void Title_CopiesFullLiveTitleAndFallbackWithIndependentFeedback()
    {
        var title = "build <worker> \u03bb with a long title";
        var cut = Render<TerminalTitle>(builder => builder
            .Add(p => p.FallbackTitle, "shell")
            .Add(p => p.State, new TerminalToolbarState
            {
                Title = title,
                WorkingDirectory = "/work/app"
            }));
 
        var button = cut.Find(".terminal-title-button");
        var id = button.Id;
        Assert.NotEqual(cut.Find(".terminal-directory").Id, id);
        Assert.Equal(title, cut.Find(".terminal-title").TextContent);
        Assert.Equal(title, button.GetAttribute("data-text"));
        Assert.Equal("true", button.GetAttribute("data-copybutton"));
        Assert.Equal($"Copy terminal title: {title}", button.GetAttribute("aria-label"));
        Assert.Equal(Resources.ControlsStrings.GridValueCopied, button.GetAttribute("data-postcopy"));
        Assert.Equal(Resources.TerminalStrings.TerminalCopyFailed, button.GetAttribute("data-copyfailed"));
        Assert.Equal("polite", cut.Find(".terminal-title-container [role=status]").GetAttribute("aria-live"));
        Assert.Single(button.QuerySelectorAll(".copy-icon"));
        Assert.Single(button.QuerySelectorAll(".checkmark-icon"));
        Assert.False(button.HasAttribute("title"));
        Assert.False(button.HasAttribute("disabled"));
        Assert.Empty(cut.FindAll("fluent-tooltip"));
 
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState { Title = "updated" }));
        Assert.Equal("updated", cut.Find(".terminal-title-button").GetAttribute("data-text"));
        Assert.Equal(id, cut.Find(".terminal-title-button").Id);
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState()));
        Assert.Equal("shell", cut.Find(".terminal-title-button").GetAttribute("data-text"));
        Assert.Equal("Copy terminal title: shell", cut.Find(".terminal-title-button").GetAttribute("aria-label"));
        cut.Render(builder => builder.Add(p => p.FallbackTitle, ""));
        Assert.Empty(cut.FindAll(".terminal-title-button"));
    }
 
    [Fact]
    public void PathObserverFailure_DoesNotPreventCopyControlsFromRendering()
    {
        var module = JSInterop.SetupModule("./Components/Controls/TerminalTitle.razor.js");
        module.SetupVoid("observePath", _ => true).SetException(new JSException("Canvas unavailable"));
 
        var cut = Render<TerminalTitle>(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            Title = "shell",
            WorkingDirectory = "/work/app"
        }));
 
        Assert.Equal("shell", cut.Find(".terminal-title").TextContent);
        Assert.Equal("/work/app", cut.Find(".terminal-directory").GetAttribute("data-text"));
        Assert.Single(module.Invocations, i => i.Identifier == "observePath");
    }
 
    [Theory]
    [InlineData("/work/app")]
    [InlineData("C:\\src\\app")]
    [InlineData("ab\U0001F680cd")]
    [InlineData("abe\u0301cd")]
    [InlineData("/")]
    public void Directory_PreservesFullMeasurementAndCopyValue(string path)
    {
        var cut = Render<TerminalTitle>(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            WorkingDirectory = path
        }));
 
        Assert.Equal(path, cut.Find(".terminal-metadata").GetAttribute("data-directory"));
        Assert.Equal(path, cut.Find(".terminal-directory-measure").TextContent);
        Assert.Empty(cut.Find(".terminal-directory-display").TextContent);
        Assert.Equal(path, cut.Find(".terminal-directory").GetAttribute("data-text"));
    }
 
    [Theory]
    [InlineData("normal", 0, "0", "0%", "TerminalProgress")]
    [InlineData("normal", 10, "10", "10%", "TerminalProgress")]
    [InlineData("normal", 100, "100", "100%", "TerminalProgress")]
    [InlineData("indeterminate", null, null, "", "TerminalProgress")]
    [InlineData("error", 25, "25", "25%", "TerminalProgressError")]
    [InlineData("warning", 75, "75", "75%", "TerminalProgressWarning")]
    public void Progress_ExposesAccessibleStateAndValue(string state, int? percentage, string? expectedValue, string expectedText, string label)
    {
        var cut = Render<TerminalTitle>(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            Title = "shell", Connected = true, ProgressState = state, ProgressPercentage = percentage
        }));
        var progress = cut.Find("[role=progressbar]");
        Assert.Equal(expectedValue, progress.GetAttribute("aria-valuenow"));
        Assert.Equal(Resources.TerminalStrings.ResourceManager.GetString(label), progress.GetAttribute("aria-label"));
        var indicator = cut.Find(".terminal-icon");
        Assert.Equal(state, indicator.GetAttribute("data-state"));
        Assert.Equal(expectedValue is null ? Resources.TerminalStrings.ResourceManager.GetString(label) : expectedText, indicator.GetAttribute("title"));
        Assert.Empty(indicator.TextContent.Trim());
        Assert.Equal("terminal-icon", cut.Find(".terminal-metadata").Children[0].ClassName);
        Assert.Equal("terminal-title-container", cut.Find(".terminal-metadata").Children[1].ClassName);
        Assert.Single(indicator.Children);
    }
 
    [Theory]
    [InlineData(true, "none")]
    [InlineData(false, "normal")]
    [InlineData(false, "indeterminate")]
    public void InactiveProgress_ShowsTerminalIcon(bool connected, string state)
    {
        var cut = Render<TerminalTitle>(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            Connected = connected, ProgressState = state, ProgressPercentage = 42
        }));
        Assert.Empty(cut.FindAll("[role=progressbar]"));
        Assert.Equal("none", cut.Find(".terminal-icon").GetAttribute("data-state"));
        Assert.IsType<Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.WindowConsole>(cut.FindComponent<FluentIcon<Icon>>().Instance.Value);
    }
 
    [Fact]
    public void ResourceIcon_ReturnsAfterProgressAndDisconnect()
    {
        var icon = new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Database();
        var cut = Render<TerminalTitle>(builder => builder.Add(p => p.IdleIcon, icon));
        Assert.Same(icon, cut.FindComponent<FluentIcon<Icon>>().Instance.Value);
 
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState
        {
            Connected = true, ProgressState = "normal", ProgressPercentage = 42
        }));
        Assert.Single(cut.FindAll("[role=progressbar]"));
        Assert.Equal("42%", cut.Find(".terminal-icon").GetAttribute("title"));
        Assert.Empty(cut.FindComponents<FluentIcon<Icon>>());
 
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState { Connected = true, ProgressState = "none" }));
        Assert.Same(icon, cut.FindComponent<FluentIcon<Icon>>().Instance.Value);
        cut.Render(builder => builder.Add(p => p.State, new TerminalToolbarState { Connected = false, ProgressState = "normal", ProgressPercentage = 42 }));
        Assert.Same(icon, cut.FindComponent<FluentIcon<Icon>>().Instance.Value);
        Assert.Null(cut.Find(".terminal-icon").GetAttribute("title"));
    }
}