File: Layout\TerminalDockTelemetryTests.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.Threading.Channels;
using Aspire.Dashboard.Components.Layout;
using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Tests;
using Aspire.DashboardService.Proto.V1;
using Bunit;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Layout;
 
public partial class TerminalDockTests
{
    [Theory]
    [InlineData("button", "User", true)]
    [InlineData("keyboard", "User", true)]
    [InlineData("activation", "AppHost", true)]
    [InlineData("snapshot", "AppHost", true)]
    [InlineData("button", "User", false)]
    [InlineData("activation", "AppHost", false)]
    public async Task Telemetry_TracksVisibleIntervalsAndOpeningTrigger(string action, string trigger, bool telemetryEnabled)
    {
        var updates = Channel.CreateUnbounded<WatchTerminalsUpdate>();
        var client = TerminalSetupHelpers.CreateTerminalDashboardClient(terminalChannelProvider: () => updates);
        var processed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        client.OnTerminalUpdateProcessed = _ => processed.TrySetResult();
        TerminalSetupHelpers.SetupTerminalComponents(this, client);
        var sender = new TestDashboardTelemetrySender { IsTelemetryEnabled = telemetryEnabled };
        Services.AddSingleton<IDashboardTelemetrySender>(sender);
        var telemetryService = Services.GetRequiredService<DashboardTelemetryService>();
        await telemetryService.InitializeAsync();
        Assert.Equal(Enumerable.Repeat(TelemetryEndpoints.TelemetryPostProperty,
            telemetryEnabled ? telemetryService._defaultProperties.Count : 0), DrainTelemetryEvents(sender));
        var cut = Render<TerminalDock>();
        Assert.Null(cut.Instance.TelemetryContext);
 
        var renderCount = cut.RenderCount;
        await updates.Writer.WriteAsync(TerminalSetupHelpers.Snapshot("first", "second"));
        await processed.Task.DefaultTimeout();
        Assert.Equal(renderCount, cut.RenderCount);
        Assert.Null(cut.Instance.TelemetryContext);
        Assert.Empty(DrainTelemetryEvents(sender));
 
        switch (action)
        {
            case "button":
                await cut.InvokeAsync(cut.Instance.ToggleAsync);
                break;
            case "keyboard":
                await Services.GetRequiredService<ShortcutManager>().OnGlobalKeyDown(AspireKeyboardShortcut.ToggleTerminalDock);
                break;
            case "activation":
                await updates.Writer.WriteAsync(TerminalSetupHelpers.Change(TerminalChangeType.Activated, "first"));
                break;
            case "snapshot":
                var snapshot = TerminalSetupHelpers.Snapshot("first", "second");
                snapshot.Snapshot.ActivatedTerminalId = "first";
                await updates.Writer.WriteAsync(snapshot);
                break;
        }
 
        cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-dock:not(.collapsed)")));
        var firstContext = Assert.IsType<ComponentTelemetryContext>(cut.Instance.TelemetryContext);
        AssertDockTelemetryProperties(firstContext, trigger);
        Assert.Equal(telemetryEnabled ? new[]
        {
            "/telemetry/userTask - $aspire/dashboard/component/initialize",
            "/telemetry/operation - $aspire/dashboard/component/paramsSet"
        } : [], DrainTelemetryEvents(sender));
 
        await updates.Writer.WriteAsync(TerminalSetupHelpers.Change(TerminalChangeType.Activated, "second"));
        cut.WaitForAssertion(() => Assert.Equal("second", cut.Find("[role=tab][aria-selected=true]").TextContent.Trim()));
        await cut.FindAll("[role=tab]")[0].ClickAsync(new());
        Assert.Equal("first", cut.Find("[role=tab][aria-selected=true]").TextContent.Trim());
        var recovery = TerminalSetupHelpers.Snapshot("first", "second");
        recovery.Snapshot.ActivatedTerminalId = "second";
        await updates.Writer.WriteAsync(recovery);
        cut.WaitForAssertion(() => Assert.Equal("second", cut.Find("[role=tab][aria-selected=true]").TextContent.Trim()));
        Assert.Same(firstContext, cut.Instance.TelemetryContext);
        AssertDockTelemetryProperties(firstContext, trigger);
        Assert.Empty(DrainTelemetryEvents(sender));
 
        await cut.InvokeAsync(cut.Instance.ToggleAsync);
        Assert.Null(cut.Instance.TelemetryContext);
        Assert.Equal(telemetryEnabled ? new[] { "/telemetry/operation - $aspire/dashboard/component/dispose" } : [],
            DrainTelemetryEvents(sender));
 
        if (trigger == "User")
        {
            await updates.Writer.WriteAsync(recovery);
        }
        else
        {
            await cut.InvokeAsync(cut.Instance.ToggleAsync);
        }
        cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-dock:not(.collapsed)")));
        var secondContext = Assert.IsType<ComponentTelemetryContext>(cut.Instance.TelemetryContext);
        Assert.NotSame(firstContext, secondContext);
        AssertDockTelemetryProperties(secondContext, trigger == "User" ? "AppHost" : "User");
        await cut.InvokeAsync(() => cut.Instance.DisposeAsync().AsTask()).DefaultTimeout();
        await cut.InvokeAsync(() => cut.Instance.DisposeAsync().AsTask()).DefaultTimeout();
        await cut.InvokeAsync(cut.Instance.ToggleAsync);
        Assert.Null(cut.Instance.TelemetryContext);
        Assert.Equal(telemetryEnabled ? new[]
        {
            "/telemetry/userTask - $aspire/dashboard/component/initialize",
            "/telemetry/operation - $aspire/dashboard/component/paramsSet",
            "/telemetry/operation - $aspire/dashboard/component/dispose"
        } : [], DrainTelemetryEvents(sender));
    }
 
    private static void AssertDockTelemetryProperties(ComponentTelemetryContext context, string trigger)
    {
        (string, string)[] expected =
        [
            ("Aspire.Dashboard.ComponentId", "TerminalDock"),
            ("Aspire.Dashboard.ComponentType", "Control"),
            ("Aspire.Dashboard.TerminalDock.Trigger", trigger)
        ];
        Assert.Equal(expected, context.Properties.OrderBy(p => p.Key)
            .Select(p => (p.Key, Assert.IsType<string>(p.Value.Value))));
    }
 
    private static string[] DrainTelemetryEvents(TestDashboardTelemetrySender sender)
    {
        List<string> events = [];
        while (sender.ContextChannel.Reader.TryRead(out var operation))
        {
            events.Add(operation.Name);
        }
        return events.ToArray();
    }
}