File: Controls\AspireMenuTests.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 Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Controls;
 
public class AspireMenuTests : DashboardTestContext
{
    [Fact]
    public void UnanchoredAspireMenu_RendersFluentMenu()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
 
        var provider = RenderComponent<FluentMenuProvider>();
        var menuHost = RenderComponent<AspireMenu>(builder =>
        {
            builder.Add(p => p.Anchor, "menu-anchor");
            builder.Add(p => p.Anchored, false);
            builder.Add(p => p.Items, new[] { new MenuButtonItem { Text = "Item" } });
        });
 
        var menu = Assert.Single(menuHost.FindComponents<FluentMenu>()).Instance;
        Assert.False(menu.Anchored);
        Assert.Empty(provider.FindComponents<FluentMenu>());
    }
 
    [Fact]
    public async Task RemoveAspireMenu_UnregistersFluentMenuFromMenuProvider()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
 
        var provider = RenderComponent<FluentMenuProvider>();
        var menuHost = RenderComponent<CascadingValue<bool>>(builder =>
        {
            builder.Add(p => p.Value, false);
            builder.AddChildContent<AspireMenu>(menuBuilder =>
            {
                menuBuilder.Add(p => p.Anchor, "menu-anchor");
                menuBuilder.Add(p => p.Items, new[] { new MenuButtonItem { Text = "Item" } });
            });
        });
        Assert.Single(menuHost.FindComponents<FluentMenu>());
 
        await menuHost.InvokeAsync(() => menuHost.FindComponent<AspireMenu>().Instance.OpenAsync(1920, 1080, 10, 10));
 
        provider.WaitForAssertion(() => Assert.Single(provider.FindComponents<FluentMenu>()));
 
        menuHost.SetParametersAndRender(builder =>
        {
            builder.Add(p => p.Value, false);
            builder.Add(p => p.ChildContent, (RenderFragment)(_ => { }));
        });
 
        provider.WaitForAssertion(() => Assert.Empty(provider.FindComponents<FluentMenu>()));
    }
 
    [Fact]
    public void ClickItem_MenuButton_FocusesAnchorBeforeOnClick()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
        FluentUISetupHelpers.SetupFluentButton(this);
 
        var anchor = "view-options-button";
        var itemClicked = false;
        var focusElementInvocationHandler = JSInterop.SetupVoid("focusElement", anchor);
        focusElementInvocationHandler.SetVoidResult();
        var items = new List<MenuButtonItem>
        {
            new()
            {
                Text = "Show hidden resources",
                OnClick = () =>
                {
                    Assert.Single(focusElementInvocationHandler.Invocations);
                    itemClicked = true;
 
                    return Task.CompletedTask;
                }
            }
        };
 
        var provider = RenderComponent<FluentMenuProvider>();
        var menuButton = RenderComponent<AspireMenuButton>(builder =>
        {
            builder.Add(p => p.MenuButtonId, anchor);
            builder.Add(p => p.Title, "View options");
            builder.Add(p => p.ItemsProvider, () => items);
        });
 
        menuButton.Find($"#{anchor}").Click();
        provider.WaitForElement("fluent-menu-item").Click();
 
        Assert.True(itemClicked);
        var invocation = Assert.Single(focusElementInvocationHandler.Invocations);
        Assert.Collection(invocation.Arguments,
            argument => Assert.Equal(anchor, Assert.IsType<string>(argument)));
    }
 
    [Fact]
    public void ClickItem_MenuButtonWithFocusRestorationDisabled_DoesNotFocusAnchor()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
        FluentUISetupHelpers.SetupFluentButton(this);
 
        var anchor = "view-options-button";
        var itemClicked = false;
        var items = new List<MenuButtonItem>
        {
            new()
            {
                Text = "Show hidden resources",
                OnClick = () =>
                {
                    itemClicked = true;
                    return Task.CompletedTask;
                }
            }
        };
 
        var provider = RenderComponent<FluentMenuProvider>();
        var menuButton = RenderComponent<AspireMenuButton>(builder =>
        {
            builder.Add(p => p.MenuButtonId, anchor);
            builder.Add(p => p.Title, "View options");
            builder.Add(p => p.ItemsProvider, () => items);
            builder.Add(p => p.RestoreFocusOnItemClick, false);
        });
 
        menuButton.Find($"#{anchor}").Click();
        provider.WaitForElement("fluent-menu-item").Click();
 
        Assert.True(itemClicked);
        var focusElementInvocations = JSInterop.Invocations
            .Where(invocation => invocation.Identifier == "focusElement")
            .ToArray();
        Assert.Empty(focusElementInvocations);
    }
 
    [Fact]
    public void CheckableItems_RenderAccessibleRoleAndCheckedStateInDom()
    {
        FluentUISetupHelpers.AddCommonDashboardServices(this);
        FluentUISetupHelpers.SetupFluentUIComponents(this);
        FluentUISetupHelpers.SetupFluentMenu(this);
        FluentUISetupHelpers.SetupFluentAnchoredRegion(this);
        FluentUISetupHelpers.SetupFluentButton(this);
 
        var anchor = "view-options-button";
        var items = new List<MenuButtonItem>
        {
            new() { Text = "Console", Role = MenuItemRole.MenuItemCheckbox, Checked = false },
            new() { Text = "Terminal", Role = MenuItemRole.MenuItemCheckbox, Checked = true },
        };
 
        var provider = RenderComponent<FluentMenuProvider>();
        var menuButton = RenderComponent<AspireMenuButton>(builder =>
        {
            builder.Add(p => p.MenuButtonId, anchor);
            builder.Add(p => p.Title, "View options");
            builder.Add(p => p.ItemsProvider, () => items);
        });
 
        menuButton.Find($"#{anchor}").Click();
        provider.WaitForElement("fluent-menu-item");
 
        var menuItems = provider.FindAll("fluent-menu-item");
        Assert.Equal(2, menuItems.Count);
 
        // Both options must carry the checkable role so assistive technology announces
        // them as a selectable set. Asserting on the rendered element (not the backing
        // MenuButtonItem) guards the Role passthrough through AspireMenu -> FluentMenuItem:
        // the unchecked item only gets role="menuitemcheckbox" from an explicit Role, since
        // FluentMenuItem otherwise infers that role solely from a checked item.
        Assert.Equal("menuitemcheckbox", menuItems[0].GetAttribute("role"));
        Assert.Equal("menuitemcheckbox", menuItems[1].GetAttribute("role"));
 
        // Only the active option reflects the checked state in the DOM. This guards the
        // Checked passthrough; without it the rendered items would lose their checked state.
        Assert.False(menuItems[0].HasAttribute("checked"));
        Assert.True(menuItems[1].HasAttribute("checked"));
    }
}