File: Dialogs\ManageDataDialogTests.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 AngleSharp.Dom;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.ManageData;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Tests.Shared.DashboardModel;
using Bunit;
using Google.Protobuf.Collections;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.FluentUI.AspNetCore.Components;
using OpenTelemetry.Proto.Logs.V1;
using OpenTelemetry.Proto.Trace.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
 
namespace Aspire.Dashboard.Components.Tests.Dialogs;
 
[UseCulture("en-US")]
public sealed class ManageDataDialogTests : DashboardTestContext
{
    [Fact]
    public async Task Render_SelectionControlsExposeAccessibleNamesCheckboxRoleAndState()
    {
        var basketResource = ModelTestHelpers.CreateResource(
            resourceName: "basket",
            displayName: "Basket service",
            state: KnownResourceState.Running);
        var catalogResource = ModelTestHelpers.CreateResource(
            resourceName: "catalog",
            displayName: "Catalog service",
            state: KnownResourceState.Running);
        var resourcesChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
        var dashboardClient = new TestDashboardClient(
            isEnabled: true,
            initialResources: [basketResource, catalogResource],
            resourceChannelProvider: () => resourcesChannel);
        SetupManageDataDialogServices(dashboardClient);
 
        var cut = RenderComponent<ManageDataDialog>();
 
        cut.WaitForAssertion(() =>
        {
            Assert.Contains(JSInterop.Invocations, invocation => invocation.Identifier == "initializeIconCheckboxKeyboard");
            AssertSelectionCheckboxCount(cut, 3);
            AssertSelectionCheckbox(cut, "All data", "true");
            AssertSelectionCheckbox(cut, "Basket service", "true");
            AssertSelectionCheckbox(cut, "Catalog service", "true");
            AssertSelectionCheckboxesHaveNoActionLabels(cut);
            AssertNoButtonHasAccessibleName(cut, "Name");
        });
 
        await ClickSelectionCheckboxAsync(cut, "Basket service", "true");
 
        cut.WaitForAssertion(() =>
        {
            AssertSelectionCheckboxCount(cut, 3);
            AssertSelectionCheckbox(cut, "All data", "mixed");
            AssertSelectionCheckbox(cut, "Basket service", "false");
            AssertSelectionCheckbox(cut, "Catalog service", "true");
        });
 
        await ClickSelectionCheckboxAsync(cut, "Basket service", "false");
 
        cut.WaitForAssertion(() =>
        {
            AssertSelectionCheckboxCount(cut, 3);
            AssertSelectionCheckbox(cut, "All data", "true");
            AssertSelectionCheckbox(cut, "Basket service", "true");
            AssertSelectionCheckbox(cut, "Catalog service", "true");
        });
 
        await ExpandResourceRowsAsync(cut, expectedCount: 2);
 
        cut.WaitForAssertion(() =>
        {
            AssertAllSelectionControlsSelected(cut);
            AssertNoSelectionCheckboxHasAccessibleName(cut, "Console logs");
            AssertNoButtonHasAccessibleName(cut, "Name");
        });
 
        await ClickSelectionCheckboxAsync(cut, "Console logs for Basket service", "true");
 
        cut.WaitForAssertion(() =>
        {
            AssertSelectionCheckboxCount(cut, 7);
            AssertSelectionCheckbox(cut, "All data", "mixed");
            AssertSelectionCheckbox(cut, "Basket service", "mixed");
            AssertSelectionCheckbox(cut, "Catalog service", "true");
            AssertSelectionCheckbox(cut, "Resource for Basket service", "true");
            AssertSelectionCheckbox(cut, "Console logs for Basket service", "false");
            AssertSelectionCheckbox(cut, "Resource for Catalog service", "true");
            AssertSelectionCheckbox(cut, "Console logs for Catalog service", "true");
            AssertSelectionCheckboxesHaveNoActionLabels(cut);
            AssertNoSelectionCheckboxHasAccessibleName(cut, "Console logs");
            AssertNoButtonHasAccessibleName(cut, "Name");
        });
 
        await ClickSelectionCheckboxAsync(cut, "All data", "mixed");
 
        cut.WaitForAssertion(() =>
        {
            AssertAllSelectionControlsSelected(cut);
            AssertNoSelectionCheckboxHasAccessibleName(cut, "Console logs");
            AssertNoButtonHasAccessibleName(cut, "Name");
        });
 
        await ClickSelectionCheckboxAsync(cut, "All data", "true");
 
        cut.WaitForAssertion(() =>
        {
            AssertSelectionCheckboxCount(cut, 7);
            AssertSelectionCheckbox(cut, "All data", "false");
            AssertSelectionCheckbox(cut, "Basket service", "false");
            AssertSelectionCheckbox(cut, "Catalog service", "false");
            AssertSelectionCheckbox(cut, "Resource for Basket service", "false");
            AssertSelectionCheckbox(cut, "Console logs for Basket service", "false");
            AssertSelectionCheckbox(cut, "Resource for Catalog service", "false");
            AssertSelectionCheckbox(cut, "Console logs for Catalog service", "false");
            AssertSelectionCheckboxesHaveNoActionLabels(cut);
            AssertNoSelectionCheckboxHasAccessibleName(cut, "Console logs");
            AssertNoButtonHasAccessibleName(cut, "Name");
        });
    }
 
    [Fact]
    public async Task IconCheckbox_DoesNotInvokeClickWhenDisabled()
    {
        var clickCount = 0;
        SetupIconCheckboxJs();
 
        var cut = RenderComponent<IconCheckbox>(parameters => parameters
            .Add(p => p.CheckState, IconCheckboxState.Checked)
            .Add(p => p.Disabled, true)
            .Add(p => p.AccessibleLabel, "All data")
            .Add(p => p.OnClick, () => clickCount++));
 
        var checkbox = cut.Find("[role='checkbox']");
 
        Assert.Equal("true", checkbox.GetAttribute("aria-disabled"));
        await cut.InvokeAsync(() => checkbox.Click());
 
        Assert.Equal(0, clickCount);
    }
 
    [Fact]
    public async Task Render_ClearedSignals_PrunesSelectionsAndSupportsRemovingEmptyResource()
    {
        var dashboardClient = new TestDashboardClient(isEnabled: false, initialResources: []);
        SetupManageDataDialogServices(dashboardClient);
 
        var repository = Services.GetRequiredService<TelemetryRepository>();
        var resourceKey = new ResourceKey("orphan", "instance");
        repository.AddLogs(new AddContext(), new RepeatedField<ResourceLogs>
        {
            new ResourceLogs
            {
                Resource = CreateResource(name: resourceKey.Name, instanceId: resourceKey.InstanceId),
                ScopeLogs =
                {
                    new ScopeLogs
                    {
                        Scope = CreateScope("test-scope"),
                        LogRecords = { CreateLogRecord() }
                    }
                }
            }
        });
        var timestamp = DateTime.UnixEpoch;
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: resourceKey.Name, instanceId: resourceKey.InstanceId),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope("test-scope"),
                        Spans = { CreateSpan(traceId: "trace", spanId: "span", startTime: timestamp, endTime: timestamp.AddSeconds(1)) }
                    }
                }
            }
        });
 
        var cut = RenderComponent<ManageDataDialog>();
 
        cut.WaitForAssertion(() =>
        {
            Assert.Single(repository.GetResources());
            AssertSelectionCheckboxCount(cut, 2);
            AssertSelectionCheckbox(cut, "orphan", "true");
        });
 
        await ExpandResourceRowsAsync(cut, expectedCount: 1);
        cut.WaitForAssertion(() => AssertSelectionCheckbox(cut, "Structured logs for orphan", "true"));
        await ClickSelectionCheckboxAsync(cut, "Structured logs for orphan", "true");
 
        repository.ClearTraces(resourceKey);
 
        cut.WaitForAssertion(() =>
        {
            AssertNoButtonHasAccessibleName(cut, "Traces");
            AssertSelectionCheckbox(cut, "orphan", "false");
            AssertButtonDisabled(cut, "Export selected", expectedDisabled: true);
            AssertButtonDisabled(cut, "Remove selected", expectedDisabled: true);
        });
 
        repository.ClearStructuredLogs(resourceKey);
 
        cut.WaitForAssertion(() =>
        {
            Assert.Single(repository.GetResources());
            AssertSelectionCheckboxCount(cut, 2);
            AssertSelectionCheckbox(cut, "orphan", "false");
            AssertNoButtonHasAccessibleName(cut, "Structured logs");
            Assert.Single(cut.FindAll(".empty-data"));
            AssertButtonDisabled(cut, "Export selected", expectedDisabled: true);
            AssertButtonDisabled(cut, "Remove selected", expectedDisabled: true);
        });
 
        await cut.InvokeAsync(() => cut.Find(".empty-data").Click());
        cut.WaitForAssertion(() =>
        {
            AssertSelectionCheckbox(cut, "orphan", "true");
            AssertButtonDisabled(cut, "Export selected", expectedDisabled: true);
            AssertButtonDisabled(cut, "Remove selected", expectedDisabled: false);
        });
 
        cut.Find("fluent-button[aria-label='Remove selected']").Click();
 
        cut.WaitForAssertion(() => Assert.Empty(repository.GetResources()));
    }
 
    private void SetupManageDataDialogServices(TestDashboardClient dashboardClient)
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        Services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
        Services.AddOptions<DashboardOptions>().Configure(options => options.UI.DisableImport = true);
        Services.AddSingleton<IDashboardClient>(dashboardClient);
        Services.AddSingleton<IconResolver>();
        Services.AddSingleton<ConsoleLogsManager>();
        Services.AddSingleton<ConsoleLogsFetcher>();
        Services.AddSingleton<TelemetryExportService>();
        Services.AddSingleton<TelemetryImportService>();
 
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentButton(this);
        FluentUISetupHelpers.SetupFluentDataGrid(this);
 
        SetupIconCheckboxJs();
    }
 
    private void SetupIconCheckboxJs()
    {
        var module = JSInterop.SetupModule("./Components/Controls/IconCheckbox.razor.js");
        module.SetupVoid("initializeIconCheckboxKeyboard", _ => true);
        module.SetupVoid("disposeIconCheckboxKeyboard", _ => true);
    }
 
    private static async Task ExpandResourceRowsAsync(IRenderedComponent<ManageDataDialog> cut, int expectedCount)
    {
        for (var i = 0; i < expectedCount; i++)
        {
            await cut.InvokeAsync(() =>
            {
                var toggleButtons = cut.FindAll("fluent-button[aria-label='Toggle nesting']");
 
                Assert.Equal(expectedCount, toggleButtons.Count);
 
                toggleButtons[i].Click();
            });
        }
    }
 
    private static void AssertAllSelectionControlsSelected(IRenderedComponent<ManageDataDialog> cut)
    {
        AssertSelectionCheckboxCount(cut, 7);
        AssertSelectionCheckbox(cut, "All data", "true");
        AssertSelectionCheckbox(cut, "Basket service", "true");
        AssertSelectionCheckbox(cut, "Catalog service", "true");
        AssertSelectionCheckbox(cut, "Resource for Basket service", "true");
        AssertSelectionCheckbox(cut, "Console logs for Basket service", "true");
        AssertSelectionCheckbox(cut, "Resource for Catalog service", "true");
        AssertSelectionCheckbox(cut, "Console logs for Catalog service", "true");
        AssertSelectionCheckboxesHaveNoActionLabels(cut);
    }
 
    private static IElement AssertSelectionCheckbox(IRenderedComponent<ManageDataDialog> cut, string accessibleName, string ariaChecked)
    {
        var checkbox = Assert.Single(GetSelectionCheckboxes(cut), checkbox => ElementHasAccessibleName(checkbox, accessibleName));
 
        Assert.Equal("span", checkbox.LocalName);
        Assert.Equal("checkbox", checkbox.GetAttribute("role"));
        Assert.Equal(ariaChecked, checkbox.GetAttribute("aria-checked"));
        Assert.Equal("0", checkbox.GetAttribute("tabindex"));
        Assert.Empty(checkbox.QuerySelectorAll("fluent-button"));
 
        return checkbox;
    }
 
    private static void AssertSelectionCheckboxCount(IRenderedComponent<ManageDataDialog> cut, int expectedCount)
    {
        Assert.Empty(cut.FindAll("fluent-button[role='checkbox']"));
        Assert.Equal(expectedCount, GetSelectionCheckboxes(cut).Count);
    }
 
    private static void AssertSelectionCheckboxesHaveNoActionLabels(IRenderedComponent<ManageDataDialog> cut)
    {
        Assert.DoesNotContain(GetSelectionCheckboxes(cut), checkbox =>
        {
            var accessibleName = checkbox.GetAttribute("aria-label");
 
            return accessibleName is not null &&
                (accessibleName.StartsWith("Select ", StringComparison.Ordinal) ||
                 accessibleName.StartsWith("Deselect ", StringComparison.Ordinal));
        });
    }
 
    private static void AssertNoSelectionCheckboxHasAccessibleName(IRenderedComponent<ManageDataDialog> cut, string accessibleName) =>
        Assert.DoesNotContain(GetSelectionCheckboxes(cut), checkbox => ElementHasAccessibleName(checkbox, accessibleName));
 
    private static void AssertNoButtonHasAccessibleName(IRenderedComponent<ManageDataDialog> cut, string accessibleName) =>
        Assert.DoesNotContain(
            cut.FindAll("fluent-button"),
            element =>
                string.Equals(element.GetAttribute("title"), accessibleName, StringComparison.Ordinal) ||
                string.Equals(element.GetAttribute("aria-label"), accessibleName, StringComparison.Ordinal));
 
    private static void AssertButtonDisabled(IRenderedComponent<ManageDataDialog> cut, string accessibleName, bool expectedDisabled)
    {
        var button = cut.Find($"fluent-button[aria-label='{accessibleName}']");
 
        Assert.Equal(expectedDisabled, button.HasAttribute("disabled"));
    }
 
    private static bool ElementHasAccessibleName(IElement element, string accessibleName) =>
        string.Equals(element.GetAttribute("title"), accessibleName, StringComparison.Ordinal) &&
        string.Equals(element.GetAttribute("aria-label"), accessibleName, StringComparison.Ordinal);
 
    private static Task ClickSelectionCheckboxAsync(IRenderedComponent<ManageDataDialog> cut, string accessibleName, string ariaChecked) =>
        cut.InvokeAsync(() => AssertSelectionCheckbox(cut, accessibleName, ariaChecked).Click());
 
    private static IReadOnlyList<IElement> GetSelectionCheckboxes(IRenderedComponent<ManageDataDialog> cut)
    {
        return cut.FindComponent<FluentDataGrid<ManageDataGridItem>>().FindAll("[role='checkbox']");
    }
}