File: AppHostPresentationTests.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.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;
 
namespace Aspire.Tray.Tests;
 
public class AppHostPresentationTests
{
    [Theory]
    [InlineData("Stopped")]
    [InlineData("Stopping AppHost...")]
    [InlineData("Starting AppHost; waiting for discovery...")]
    [InlineData("Shop \u00b7 PID 42")]
    [InlineData("AppHost no longer available")]
    public void FullMenuDetailsCombineOriginalCasePathAndDetailsOnOneLine(string subtitle)
    {
        const string Path = "C:\\src\\A&B\\Shop.AppHost.cs";
 
        Assert.Equal($"{Path} \u00b7 {subtitle}", AppHostPresentation.GetMenuDetailsText(Path, subtitle));
    }
 
    [Fact]
    public void MenuDetailsSanitizeLineBreaksAndKeepLiteralAmpersands()
    {
        Assert.Equal("C:\\src\\A&B Shop\\AppHost.cs \u00b7 Stop failed. Try again. More details.",
            AppHostPresentation.GetMenuDetailsText("C:\\src\\A&B\tShop\\AppHost.cs", "Stop failed.\r\nTry again.\u2028More details."));
    }
 
    [Theory]
    [InlineData(44)]
    [InlineData(45)]
    [InlineData(46)]
    [InlineData(100)]
    public void MenuDetailsLimitTheEntirePathAndStatusTo45Characters(int length)
    {
        const string Suffix = " \u00b7 Stopped";
        var path = "C:\\" + new string('a', length - 3 - Suffix.Length);
        var full = AppHostPresentation.GetMenuDetailsText(path, "Stopped");
        var expected = length <= 45 ? full : full[..22] + "\u2026" + full[^22..];
        var label = AppHostPresentation.GetMenuDetailsLabel(full);
 
        Assert.Equal(path + Suffix, full);
        Assert.Equal(expected, label);
        Assert.Equal(Math.Min(length, 45), label.Length);
    }
 
    [Theory]
    [InlineData("e\u0301")]
    [InlineData("\U0001F600")]
    [InlineData("\U0001F469\u200D\U0001F4BB")]
    public void MenuDetailsPreserveUnicodeAtBoth45CharacterTruncationBoundaries(string grapheme)
    {
        var path = string.Concat(Enumerable.Repeat(grapheme, 50));
        var full = AppHostPresentation.GetMenuDetailsText(path, "Stopped");
        var expected = string.Concat(Enumerable.Repeat(grapheme, 22)) + "\u2026"
            + string.Concat(Enumerable.Repeat(grapheme, 12)) + " \u00b7 Stopped";
        var label = AppHostPresentation.GetMenuDetailsLabel(full);
 
        Assert.Equal(path + " \u00b7 Stopped", full);
        Assert.Equal(expected, label);
        Assert.Equal(45, StringInfo.ParseCombiningCharacters(label).Length);
    }
 
    [Theory]
    [InlineData(44)]
    [InlineData(45)]
    [InlineData(100)]
    public void PathLabelsLimitLengthAndKeepBothEnds(int length)
    {
        const string Prefix = "C:\\";
        const string Suffix = "\\apphost.cs";
        var path = Prefix + new string('a', length - Prefix.Length - Suffix.Length) + Suffix;
        var expected = length <= 44 ? path : path[..21] + "\u2026" + path[^22..];
 
        var label = AppHostPresentation.GetPathLabel(path);
 
        Assert.Equal(expected, label);
        Assert.Equal(Math.Min(length, 44), StringInfo.ParseCombiningCharacters(label).Length);
    }
 
    [Theory]
    [InlineData("e\u0301")]
    [InlineData("\U0001F600")]
    [InlineData("\U0001F469\u200D\U0001F4BB")]
    public void PathLabelsPreserveGraphemesAtBothTruncationBoundaries(string grapheme)
    {
        var path = "C:\\" + string.Concat(Enumerable.Repeat(grapheme, 60)) + "\\apphost.cs";
        var expected = "C:\\" + string.Concat(Enumerable.Repeat(grapheme, 18)) + "\u2026"
            + string.Concat(Enumerable.Repeat(grapheme, 11)) + "\\apphost.cs";
 
        var label = AppHostPresentation.GetPathLabel(path);
 
        Assert.Equal(expected, label);
        Assert.Equal(44, StringInfo.ParseCombiningCharacters(label).Length);
    }
 
    [Theory]
    [InlineData("C:\\src\\Shop\\AppHost.cs", "C:\\src\\Shop\\AppHost.cs")]
    [InlineData("/src/my\nproject\tname/apphost.cs", "/src/my project name/apphost.cs")]
    [InlineData("C:\\src\\A&B\\AppHost.cs", "C:\\src\\A&B\\AppHost.cs")]
    public void PathLabelsKeepShortPathsOnOneLine(string path, string expected)
        => Assert.Equal(expected, AppHostPresentation.GetPathLabel(path));
 
    [Theory]
    [InlineData(true, false, false, null, nameof(AppHostHealth.Healthy))]
    [InlineData(false, false, false, null, nameof(AppHostHealth.Unknown))]
    [InlineData(false, true, false, null, nameof(AppHostHealth.Unknown))]
    [InlineData(true, false, true, null, nameof(AppHostHealth.Healthy))]
    [InlineData(false, false, false, "Start failed.", nameof(AppHostHealth.Unknown))]
    [InlineData(true, false, false, null, nameof(AppHostHealth.Unhealthy))]
    [InlineData(true, false, false, null, nameof(AppHostHealth.Warning))]
    [InlineData(true, false, false, null, nameof(AppHostHealth.Unknown))]
    public void CompactMenuLabelsKeepOnlyTheNameForEveryState(bool running, bool starting, bool stopping,
        string? error, string health)
    {
        var host = new AppHostMenuItem(default, "Shop", "Long directory context and PID", "Shop",
            false, false, stopping, error)
        {
            IsRunning = running,
            IsStarting = starting,
            Health = Enum.Parse<AppHostHealth>(health)
        };
 
        Assert.Equal("Shop", AppHostPresentation.GetCompactMenuLabel(host));
    }
 
    [Theory]
    [InlineData(44)]
    [InlineData(45)]
    [InlineData(60)]
    public void CompactMenuLabelsLimitNameAndPreserveGraphemes(int count)
    {
        const string Grapheme = "e\u0301";
        var name = string.Concat(Enumerable.Repeat(Grapheme, count));
        var host = new AppHostMenuItem(default, name, "Context", name, false, false, false, null)
        {
            IsRunning = true
        };
        var expectedName = count <= 44 ? name
            : string.Concat(Enumerable.Repeat(Grapheme, 21)) + "\u2026" + string.Concat(Enumerable.Repeat(Grapheme, 22));
 
        var label = AppHostPresentation.GetCompactMenuLabel(host);
 
        Assert.Equal(expectedName, label);
        Assert.Equal(Math.Min(count, 44), StringInfo.ParseCombiningCharacters(label).Length);
    }
 
    [Fact]
    public void MenuSeparatesProjectNameFromDirectoryContextAndInstancePid()
    {
        var host = new AppHostInfo(Path.GetFullPath("worktree-a/apphost/apphost.cs"), 42, null);
 
        Assert.Equal("worktree-a", AppHostPresentation.GetTitle(host));
        Assert.Equal($"{Path.Combine("worktree-a", "apphost")} \u00b7 PID 42", AppHostPresentation.GetSubtitle(host));
        Assert.Equal($"apphost - {Path.Combine("worktree-a", "apphost")} (PID 42)", AppHostPresentation.GetLabel(host));
        Assert.Equal(Path.GetFullPath("worktree-a/apphost"), AppHostPresentation.GetDirectory(host));
    }
 
    [Theory]
    [InlineData("worktree-a/apphost.mts", "worktree-a")]
    [InlineData("worktree-a/AppHost/AppHost.csproj", "worktree-a")]
    [InlineData("worktree-a/Shop.AppHost/Shop.AppHost.csproj", "Shop")]
    [InlineData("worktree-a/Shop.AppHost/apphost.cs", "Shop")]
    [InlineData("worktree-a/custom-app.cs", "custom-app")]
    [InlineData("worktree\nbranch/AppHost/apphost.cs", "worktree branch")]
    [InlineData("worktree-a/my\nproject.AppHost/my\nproject.AppHost.csproj", "my project")]
    public void NamesPreferTheProjectOrWorktreeOverGenericAppHostNames(string path, string expected)
    {
        var host = new AppHostInfo(Path.GetFullPath(path), 42, null);
 
        Assert.Equal(expected, AppHostPresentation.GetDisplayName(host));
        Assert.Equal(expected, AppHostPresentation.GetTitle(host));
    }
 
    [Fact]
    public void SameProjectInDifferentWorktreesKeepsItsLocationVisible()
    {
        var first = new AppHostInfo(Path.GetFullPath("worktree-a/Shop.AppHost/Shop.AppHost.csproj"), 42, null);
        var second = new AppHostInfo(Path.GetFullPath("worktree-b/Shop.AppHost/Shop.AppHost.csproj"), 43, null);
 
        Assert.Equal("Shop", AppHostPresentation.GetTitle(first));
        Assert.Equal("Shop", AppHostPresentation.GetTitle(second));
        Assert.Equal($"{Path.Combine("worktree-a", "Shop.AppHost")} \u00b7 PID 42", AppHostPresentation.GetSubtitle(first));
        Assert.Equal($"{Path.Combine("worktree-b", "Shop.AppHost")} \u00b7 PID 43", AppHostPresentation.GetSubtitle(second));
    }
 
    [Theory]
    [InlineData("\U0001F469\u200D\U0001F4BB", 44)]
    [InlineData("\U0001F469\u200D\U0001F4BB", 45)]
    [InlineData("\U0001F469\u200D\U0001F4BB", 60)]
    [InlineData("e\u0301", 60)]
    [InlineData("\U0001F600", 60)]
    public void LongNamesKeepBothEndsAndPreserveGraphemes(string grapheme, int count)
    {
        var name = string.Concat(Enumerable.Repeat(grapheme, count));
        var host = new AppHostInfo(Path.GetFullPath($"{name}/apphost.cs"), 42, null);
        var expected = count <= 44
            ? name
            : string.Concat(Enumerable.Repeat(grapheme, 21)) + "\u2026" + string.Concat(Enumerable.Repeat(grapheme, 22));
 
        Assert.Equal(name, AppHostPresentation.GetDisplayName(host));
        Assert.Equal(expected, AppHostPresentation.GetTitle(host));
        Assert.Equal(Math.Min(count, 44), StringInfo.ParseCombiningCharacters(AppHostPresentation.GetTitle(host)).Length);
    }
 
    [Theory]
    [InlineData("my\nproject\tname", "my project name")]
    [InlineData("my\r\nproject\tname", "my project name")]
    [InlineData("my\u0085project\u2028name\u2029end", "my project name end")]
    public void MenuLabelsDoNotIntroduceExtraLines(string name, string expected)
    {
        var path = Path.GetFullPath($"worktree\nbranch/apphost/{name}.cs");
        var host = new AppHostInfo(path, 42, null);
        var location = Path.Combine("worktree branch", "apphost");
 
        Assert.Equal(expected, AppHostPresentation.GetDisplayName(host));
        Assert.Equal(expected, AppHostPresentation.GetTitle(host));
        Assert.Equal($"{location} \u00b7 PID 42", AppHostPresentation.GetSubtitle(host));
        Assert.Equal($"{expected} - {location} (PID 42)", AppHostPresentation.GetLabel(host));
        Assert.Equal(path, host.AppHostPath);
        Assert.Equal(Path.GetDirectoryName(path), AppHostPresentation.GetDirectory(host));
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("file:///tmp/example")]
    [InlineData("javascript:alert(1)")]
    [InlineData("https://user:password@localhost/")]
    [InlineData("/relative")]
    public void NonBrowserAndCredentialBearingUrlsAreDisabled(string? url)
    {
        Assert.Null(new AppHostInfo(Path.GetFullPath("apphost.cs"), 42, url).DashboardUri);
    }
 
    [Fact]
    public void DashboardLoginQueryIsPreservedForBrowserNavigation()
    {
        const string Url = "https://localhost:1234/login?t=example-test-token";
        Assert.Equal(Url, new AppHostInfo(Path.GetFullPath("apphost.cs"), 42, Url).DashboardUri?.AbsoluteUri);
    }
}