Files
2026-05-18 15:53:59 +03:30

420 lines
13 KiB
C#

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Bunit;
using Bunit.JSInterop;
using SufiChain.SufiBlazor.Components.Overlays;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Overlays;
public class SbMenuTests : BunitContext
{
public SbMenuTests()
{
JSInterop.Mode = JSRuntimeMode.Loose;
}
private static RenderFragment DefaultMenuItems => (b) =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Item 1");
b.CloseComponent();
b.OpenComponent<SbMenuItem>(2);
b.AddAttribute(3, "Text", "Item 2");
b.CloseComponent();
};
private IRenderedComponent<SbMenu> RenderMenu(
Action<ComponentParameterCollectionBuilder<SbMenu>>? configure = null)
{
return Render<SbMenu>(p =>
{
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button class=\"anchor-btn\">Open</button>")));
p.AddChildContent(DefaultMenuItems);
configure?.Invoke(p);
});
}
[Fact]
public void RendersAnchorContent()
{
// Arrange & Act
var cut = RenderMenu();
// Assert
var anchor = cut.Find(".sb-menu-anchor");
Assert.NotNull(anchor);
Assert.Contains("Open", cut.Markup);
Assert.NotNull(cut.Find(".anchor-btn"));
}
[Fact]
public void DoesNotRenderMenuWhenClosed()
{
// Arrange & Act
var cut = RenderMenu();
// Assert
Assert.Empty(cut.FindAll(".sb-menu"));
}
[Fact]
public void RendersMenuWhenOpen()
{
// Arrange & Act
var cut = RenderMenu(p => p.Add(x => x.Open, true));
// Assert
var menu = cut.Find(".sb-menu");
Assert.NotNull(menu);
Assert.Equal("menu", menu.GetAttribute("role"));
}
[Fact]
public void RendersMenuItemsWhenOpen()
{
// Arrange & Act
var cut = RenderMenu(p => p.Add(x => x.Open, true));
// Assert
Assert.Contains("Item 1", cut.Markup);
Assert.Contains("Item 2", cut.Markup);
var items = cut.FindAll(".sb-menu-item");
Assert.Equal(2, items.Count);
}
[Fact]
public void MenuItemsHaveMenuitemRole()
{
// Arrange & Act
var cut = RenderMenu(p => p.Add(x => x.Open, true));
// Assert
var items = cut.FindAll("[role=\"menuitem\"]");
Assert.Equal(2, items.Count);
}
[Theory]
[InlineData(SbPlacement.BottomStart, "bottom-start")]
[InlineData(SbPlacement.Top, "top")]
[InlineData(SbPlacement.End, "end")]
[InlineData(SbPlacement.Start, "start")]
public void AppliesPlacementClass(SbPlacement placement, string expectedClass)
{
// Arrange & Act
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.Placement, placement));
// Assert
var menu = cut.Find(".sb-menu");
Assert.Contains($"sb-menu--{expectedClass}", menu.ClassList);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.Class, "my-menu"));
// Assert
var menu = cut.Find(".sb-menu");
Assert.Contains("my-menu", menu.ClassList);
}
[Fact]
public void MenuItemRendersText()
{
// Arrange & Act
var cut = RenderMenu(p => p
.AddChildContent(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Save File");
b.CloseComponent();
})
.Add(x => x.Open, true));
// Assert
Assert.Contains("Save File", cut.Markup);
var textSpan = cut.Find(".sb-menu-item__text");
Assert.NotNull(textSpan);
}
[Fact]
public void MenuItemRendersChildContentOverridesText()
{
// Arrange & Act
var cut = RenderMenu(p => p
.AddChildContent(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Ignore");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<span>Custom</span>")));
b.CloseComponent();
})
.Add(x => x.Open, true));
// Assert
Assert.Contains("Custom", cut.Markup);
}
[Fact]
public void MenuItemRendersShortcut()
{
// Arrange & Act
var cut = RenderMenu(p => p
.AddChildContent(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "New");
b.AddAttribute(2, "Shortcut", "Ctrl+N");
b.CloseComponent();
})
.Add(x => x.Open, true));
// Assert
var shortcut = cut.Find(".sb-menu-item__shortcut");
Assert.NotNull(shortcut);
Assert.Equal("Ctrl+N", shortcut.TextContent.Trim());
}
[Fact]
public void DisabledMenuItemHasDisabledClassAndAttribute()
{
// Arrange & Act
var cut = Render<SbMenu>(p =>
{
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
p.Add(x => x.Open, true);
p.Add(x => x.ChildContent, (RenderFragment)(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Disabled");
b.AddAttribute(2, "Disabled", true);
b.CloseComponent();
}));
});
// Assert
var item = cut.Find("button.sb-menu-item");
Assert.Contains("sb-menu-item--disabled", item.GetAttribute("class") ?? "");
Assert.NotNull(item.GetAttribute("disabled"));
}
[Fact]
public void MenuItemAppliesClassParameter()
{
// Arrange & Act
var cut = Render<SbMenu>(p =>
{
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
p.Add(x => x.Open, true);
p.Add(x => x.ChildContent, (RenderFragment)(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Item");
b.AddAttribute(2, "Class", "my-item");
b.CloseComponent();
}));
});
// Assert
var item = cut.Find("button.sb-menu-item");
Assert.Contains("my-item", item.GetAttribute("class") ?? "");
}
[Fact]
public async Task MenuItemClickInvokesOnClick()
{
// Arrange - use default items with OnClick on first item via markup in ChildContent
var clicked = false;
var cut = Render<SbMenu>(p =>
{
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
p.Add(x => x.Open, true);
p.Add(x => x.CloseOnItemClick, false);
p.Add(x => x.ChildContent, (RenderFragment)(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Click Me");
b.AddAttribute(2, "OnClick", EventCallback.Factory.Create(this, () => clicked = true));
b.CloseComponent();
}));
});
// Act
var item = cut.Find("button.sb-menu-item");
await cut.InvokeAsync(() => item.Click());
// Assert
Assert.True(clicked);
}
[Fact]
public async Task MenuItemClickClosesMenuWhenCloseOnItemClickTrue()
{
// Arrange
var openChangedValue = true;
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.CloseOnItemClick, true)
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
// Act
var item = cut.Find("button.sb-menu-item");
await cut.InvokeAsync(() => item.Click());
// Assert - OpenChanged invoked with false
Assert.False(openChangedValue);
}
[Fact]
public async Task MenuItemClickDoesNotCloseMenuWhenCloseOnItemClickFalse()
{
// Arrange
var openChangedValue = true;
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.CloseOnItemClick, false)
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
// Act
var item = cut.Find("button.sb-menu-item");
await cut.InvokeAsync(() => item.Click());
// Assert
Assert.True(openChangedValue);
Assert.NotNull(cut.Find(".sb-menu"));
}
[Fact]
public async Task DisabledMenuItemClickDoesNotInvokeOnClick()
{
// Arrange - disabled item should not fire OnClick
var clicked = false;
var cut = Render<SbMenu>(p =>
{
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
p.Add(x => x.Open, true);
p.Add(x => x.ChildContent, (RenderFragment)(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Disabled");
b.AddAttribute(2, "Disabled", true);
b.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, () => clicked = true));
b.CloseComponent();
}));
});
// Act
var item = cut.Find("button.sb-menu-item");
await cut.InvokeAsync(() => item.Click());
// Assert - disabled items don't respond to click
Assert.False(clicked);
}
[Fact]
public async Task AnchorKeyDownEnterOpensMenu()
{
// Arrange
var openChangedValue = false;
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
var anchorDiv = cut.Find(".sb-menu-anchor > div");
// Act
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Enter" }));
// Assert
Assert.True(openChangedValue);
}
[Fact]
public async Task AnchorKeyDownSpaceOpensMenu()
{
// Arrange
var openChangedValue = false;
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
var anchorDiv = cut.Find(".sb-menu-anchor > div");
// Act
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = " " }));
// Assert
Assert.True(openChangedValue);
}
[Fact]
public async Task AnchorKeyDownArrowDownOpensMenu()
{
// Arrange
var openChangedValue = false;
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
var anchorDiv = cut.Find(".sb-menu-anchor > div");
// Act
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "ArrowDown" }));
// Assert
Assert.True(openChangedValue);
}
[Fact]
public async Task MenuKeyDownEscapeClosesMenu()
{
// Arrange
var openChangedValue = true;
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
var menu = cut.Find(".sb-menu");
// Act
await cut.InvokeAsync(() => menu.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" }));
// Assert
Assert.False(openChangedValue);
}
[Fact]
public async Task MenuKeyDownTabClosesMenu()
{
// Arrange
var openChangedValue = true;
var cut = RenderMenu(p => p
.Add(x => x.Open, true)
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
var menu = cut.Find(".sb-menu");
// Act
await cut.InvokeAsync(() => menu.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Tab" }));
// Assert
Assert.False(openChangedValue);
}
[Fact]
public void MenuItemRendersIcon()
{
// Arrange & Act
var cut = RenderMenu(p => p
.AddChildContent(b =>
{
b.OpenComponent<SbMenuItem>(0);
b.AddAttribute(1, "Text", "Edit");
b.AddAttribute(2, "Icon", (RenderFragment)(c => c.AddMarkupContent(0, "<span class=\"icon\">✎</span>")));
b.CloseComponent();
})
.Add(x => x.Open, true));
// Assert
var icon = cut.Find(".sb-menu-item__icon");
Assert.NotNull(icon);
Assert.Contains("✎", cut.Markup);
}
}