first commit
Release NuGet Packages / pack-and-push (release) Failing after 10m17s

This commit is contained in:
2026-05-21 11:20:43 +03:30
commit 3ff35dedb5
536 changed files with 94352 additions and 0 deletions
@@ -0,0 +1,327 @@
using Microsoft.AspNetCore.Components;
using Bunit;
using SufiChain.SufiBlazor.Components.Navigation;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
public class SbAccordionTests : BunitContext
{
private IRenderedComponent<SbAccordion> RenderAccordion(
Action<ComponentParameterCollectionBuilder<SbAccordion>>? configure = null)
{
return Render<SbAccordion>(p => configure?.Invoke(p));
}
private IRenderedComponent<SbAccordionItem> RenderAccordionItem(
Action<ComponentParameterCollectionBuilder<SbAccordionItem>>? configure = null)
{
return Render<SbAccordionItem>(p => configure?.Invoke(p));
}
// --- SbAccordion tests ---
[Fact]
public void RendersAccordionStructure()
{
// Arrange & Act
var cut = RenderAccordion();
// Assert
var div = cut.Find(".sb-accordion");
Assert.NotNull(div);
Assert.Equal("div", div.TagName.ToLowerInvariant());
}
[Fact]
public void RendersChildContent()
{
// Arrange & Act
var cut = RenderAccordion(p => p.AddChildContent("<span>Accordion content</span>"));
// Assert
Assert.Contains("Accordion content", cut.Markup);
Assert.NotNull(cut.Find("span"));
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderAccordion(p => p.Add(x => x.Class, "my-accordion"));
// Assert
var div = cut.Find(".sb-accordion");
Assert.Contains("my-accordion", div.ClassList);
}
[Fact]
public void RendersAccordionItemChildren()
{
// Arrange & Act
var cut = Render<SbAccordion>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Section 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Content 1</p>")));
b.CloseComponent();
b.OpenComponent<SbAccordionItem>(3);
b.AddAttribute(4, "Title", "Section 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Content 2</p>")));
b.CloseComponent();
}));
// Assert
var accordion = cut.Find(".sb-accordion");
Assert.NotNull(accordion);
var items = cut.FindAll(".sb-accordion-item");
Assert.Equal(2, items.Count);
Assert.Contains("Section 1", cut.Markup);
Assert.Contains("Section 2", cut.Markup);
Assert.Contains("Content 1", cut.Markup);
Assert.Contains("Content 2", cut.Markup);
}
// --- SbAccordionItem tests ---
[Fact]
public void AccordionItem_RendersStructure()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p.Add(x => x.Title, "Item Title"));
// Assert
var div = cut.Find(".sb-accordion-item");
Assert.NotNull(div);
Assert.Equal("div", div.TagName.ToLowerInvariant());
var button = cut.Find(".sb-accordion-item__header");
Assert.NotNull(button);
Assert.Equal("button", button.TagName.ToLowerInvariant());
}
[Fact]
public void AccordionItem_RendersTitle()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p.Add(x => x.Title, "My Section Title"));
// Assert
var title = cut.Find(".sb-accordion-item__title");
Assert.NotNull(title);
Assert.Contains("My Section Title", title.TextContent);
}
[Fact]
public void AccordionItem_RendersChildContent()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p
.Add(x => x.Title, "Section")
.AddChildContent("<p>Body content</p>"));
// Assert
Assert.Contains("Body content", cut.Markup);
var body = cut.Find(".sb-accordion-item__body");
Assert.NotNull(body);
Assert.NotNull(cut.Find("p"));
}
[Fact]
public void AccordionItem_IsCollapsedByDefault()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p.Add(x => x.Title, "Section"));
// Assert
var item = cut.Find(".sb-accordion-item");
Assert.DoesNotContain("sb-accordion-item--expanded", item.ClassList);
var content = cut.Find(".sb-accordion-item__content");
Assert.NotNull(content.GetAttribute("hidden"));
}
[Fact]
public void AccordionItem_ShowsExpandedWhenDefaultExpandedTrue()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p
.Add(x => x.Title, "Section")
.Add(x => x.DefaultExpanded, true));
// Assert - standalone item uses its own _isExpanded
var item = cut.Find(".sb-accordion-item");
Assert.Contains("sb-accordion-item--expanded", item.ClassList);
var content = cut.Find(".sb-accordion-item__content");
Assert.Null(content.GetAttribute("hidden"));
}
[Fact]
public void AccordionItem_AddsDisabledClassWhenDisabledTrue()
{
// Arrange & Act
var cut = RenderAccordionItem(p => p
.Add(x => x.Title, "Section")
.Add(x => x.Disabled, true));
// Assert
var item = cut.Find(".sb-accordion-item");
Assert.Contains("sb-accordion-item--disabled", item.ClassList);
var button = cut.Find("button");
Assert.NotNull(button.GetAttribute("disabled"));
}
[Fact]
public async Task AccordionItem_Standalone_ClickTogglesExpansion()
{
// Arrange - standalone item (no Parent)
var cut = RenderAccordionItem(p => p
.Add(x => x.Title, "Section")
.AddChildContent("<p>Content</p>"));
// Assert initially collapsed
var item = cut.Find(".sb-accordion-item");
Assert.DoesNotContain("sb-accordion-item--expanded", item.ClassList);
// Act - click to expand
var button = cut.Find("button");
await cut.InvokeAsync(() => button.Click());
// Assert expanded
cut.Render();
item = cut.Find(".sb-accordion-item");
Assert.Contains("sb-accordion-item--expanded", item.ClassList);
// Act - click to collapse
button = cut.Find("button");
await cut.InvokeAsync(() => button.Click());
// Assert collapsed again
cut.Render();
item = cut.Find(".sb-accordion-item");
Assert.DoesNotContain("sb-accordion-item--expanded", item.ClassList);
}
[Fact]
public void AccordionItem_InsideAccordion_DefaultExpandedShowsExpanded()
{
// Arrange & Act
var cut = Render<SbAccordion>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Expanded Section");
b.AddAttribute(2, "DefaultExpanded", true);
b.AddAttribute(3, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Expanded content")));
b.CloseComponent();
}));
// Assert
var item = cut.Find(".sb-accordion-item");
Assert.Contains("sb-accordion-item--expanded", item.ClassList);
Assert.Contains("Expanded content", cut.Markup);
}
[Fact]
public async Task AccordionItem_InsideAccordion_ClickTogglesExpansion()
{
// Arrange
var cut = Render<SbAccordion>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Section A");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content A")));
b.CloseComponent();
}));
// Assert initially collapsed
var item = cut.Find(".sb-accordion-item");
Assert.DoesNotContain("sb-accordion-item--expanded", item.ClassList);
// Act - click to expand
var button = cut.Find("button.sb-accordion-item__header");
await cut.InvokeAsync(() => button.Click());
// Assert expanded
cut.Render();
item = cut.Find(".sb-accordion-item");
Assert.Contains("sb-accordion-item--expanded", item.ClassList);
}
[Fact]
public async Task AccordionItem_Disabled_DoesNotToggleOnClick()
{
// Arrange
var cut = Render<SbAccordion>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Disabled Section");
b.AddAttribute(2, "Disabled", true);
b.AddAttribute(3, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Hidden")));
b.CloseComponent();
}));
// Act - click disabled button
var button = cut.Find("button.sb-accordion-item__header");
await cut.InvokeAsync(() => button.Click());
// Assert - still collapsed (disabled prevents toggle)
var item = cut.Find(".sb-accordion-item");
Assert.DoesNotContain("sb-accordion-item--expanded", item.ClassList);
}
[Fact]
public async Task Accordion_Multiple_AllowsMultipleExpanded()
{
// Arrange
var cut = Render<SbAccordion>(p => p
.Add(x => x.Multiple, true)
.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Item 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "C1")));
b.CloseComponent();
b.OpenComponent<SbAccordionItem>(3);
b.AddAttribute(4, "Title", "Item 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "C2")));
b.CloseComponent();
}));
var items = cut.FindAll(".sb-accordion-item");
var buttons = cut.FindAll("button.sb-accordion-item__header");
Assert.Equal(2, buttons.Count);
// Act - expand first, then second
await cut.InvokeAsync(() => buttons[0].Click());
cut.Render();
await cut.InvokeAsync(() => buttons[1].Click());
cut.Render();
// Assert - both expanded
items = cut.FindAll(".sb-accordion-item");
Assert.Contains("sb-accordion-item--expanded", items[0].ClassList);
Assert.Contains("sb-accordion-item--expanded", items[1].ClassList);
}
[Fact]
public async Task Accordion_OnExpandedChanged_FiresWhenItemToggled()
{
// Arrange
HashSet<SbAccordionItem>? receivedItems = null;
var cut = Render<SbAccordion>(p => p
.Add(x => x.OnExpandedChanged, EventCallback.Factory.Create<HashSet<SbAccordionItem>>(this, items => receivedItems = items))
.AddChildContent(b =>
{
b.OpenComponent<SbAccordionItem>(0);
b.AddAttribute(1, "Title", "Section");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content")));
b.CloseComponent();
}));
// Act - expand
var button = cut.Find("button.sb-accordion-item__header");
await cut.InvokeAsync(() => button.Click());
// Assert
Assert.NotNull(receivedItems);
Assert.Single(receivedItems);
}
}
@@ -0,0 +1,189 @@
using Bunit;
using SufiChain.SufiBlazor.Components.Navigation;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
public class SbBreadcrumbTests : BunitContext
{
private IRenderedComponent<SbBreadcrumb> RenderBreadcrumb(
Action<ComponentParameterCollectionBuilder<SbBreadcrumb>>? configure = null)
{
return Render<SbBreadcrumb>(p => configure?.Invoke(p));
}
[Fact]
public void RendersBreadcrumbStructure()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem> { new("Home", "/") }));
// Assert
var nav = cut.Find("nav.sb-breadcrumb");
Assert.NotNull(nav);
Assert.Equal("Breadcrumb", nav.GetAttribute("aria-label"));
var list = cut.Find("ol.sb-breadcrumb__list");
Assert.NotNull(list);
}
[Fact]
public void RendersEmptyListWhenNoItems()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>()));
// Assert
var nav = cut.Find("nav.sb-breadcrumb");
Assert.NotNull(nav);
var items = cut.FindAll(".sb-breadcrumb__item");
Assert.Empty(items);
}
[Fact]
public void RendersSingleItemAsCurrent()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("Products")
}));
// Assert
var item = cut.Find(".sb-breadcrumb__item");
Assert.NotNull(item);
Assert.Contains("sb-breadcrumb__item--current", item.ClassList);
var text = cut.Find(".sb-breadcrumb__text");
Assert.NotNull(text);
Assert.Contains("Products", text.TextContent);
Assert.Equal("page", text.GetAttribute("aria-current"));
Assert.Empty(cut.FindAll(".sb-breadcrumb__separator"));
}
[Fact]
public void RendersMultipleItemsWithLinksAndSeparator()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("Home", "/"),
new("Products", "/products"),
new("Detail")
}));
// Assert
var items = cut.FindAll(".sb-breadcrumb__item");
Assert.Equal(3, items.Count);
var links = cut.FindAll("a.sb-breadcrumb__link");
Assert.Equal(2, links.Count);
Assert.Equal("/", links[0].GetAttribute("href"));
Assert.Contains("Home", links[0].TextContent);
Assert.Equal("/products", links[1].GetAttribute("href"));
Assert.Contains("Products", links[1].TextContent);
var current = cut.Find(".sb-breadcrumb__text");
Assert.NotNull(current);
Assert.Contains("Detail", current.TextContent);
var separators = cut.FindAll(".sb-breadcrumb__separator");
Assert.Equal(2, separators.Count);
Assert.All(separators, s => Assert.Equal("true", s.GetAttribute("aria-hidden")));
}
[Fact]
public void UsesDefaultSeparator()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("A", "/a"),
new("B")
}));
// Assert
var separators = cut.FindAll(".sb-breadcrumb__separator");
Assert.Single(separators);
Assert.Equal("/", separators[0].TextContent);
}
[Fact]
public void AppliesCustomSeparator()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p
.Add(x => x.Items, new List<SbBreadcrumbItem> { new("A", "/a"), new("B") })
.Add(x => x.Separator, " > "));
// Assert
var separator = cut.Find(".sb-breadcrumb__separator");
Assert.NotNull(separator);
Assert.Equal(" > ", separator.TextContent);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p
.Add(x => x.Items, new List<SbBreadcrumbItem> { new("Home") })
.Add(x => x.Class, "my-breadcrumb"));
// Assert
var nav = cut.Find("nav.sb-breadcrumb");
Assert.Contains("my-breadcrumb", nav.ClassList);
}
[Fact]
public void ItemWithEmptyHrefRendersAsSpan()
{
// Arrange - last item has no Href, penultimate has empty Href
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("Home", "/"),
new("Current") { Href = "" }
}));
// Assert - "Current" is last, so it renders as span (current page). Second item has Href="" so !string.IsNullOrEmpty("") is false - it goes to else branch and renders as span
var items = cut.FindAll(".sb-breadcrumb__item");
Assert.Equal(2, items.Count);
// First: Home has Href="/" -> link. Second: Current has Href="" and is last -> span
var spans = cut.FindAll(".sb-breadcrumb__text");
Assert.Single(spans); // Last item renders as span
var links = cut.FindAll("a.sb-breadcrumb__link");
Assert.Single(links); // Only Home is a link
}
[Fact]
public void ItemWithNullHrefRendersAsSpan()
{
// Arrange - not last item but Href is null
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("Home", "/"),
new("Disabled") { Href = null },
new("Current")
}));
// Assert - "Disabled" has null Href, so it goes to else branch (span)
var links = cut.FindAll("a.sb-breadcrumb__link");
Assert.Single(links); // Only Home
Assert.Contains("Disabled", cut.Markup);
Assert.Contains("Current", cut.Markup);
}
[Fact]
public void RendersIconWhenPresent()
{
// Arrange & Act
var cut = RenderBreadcrumb(p => p.Add(x => x.Items, new List<SbBreadcrumbItem>
{
new("Home", "/") { Icon = "home" },
new("Current")
}));
// Assert
var icon = cut.Find(".sb-breadcrumb__icon");
Assert.NotNull(icon);
Assert.Contains("home", icon.TextContent);
}
}
@@ -0,0 +1,340 @@
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using SufiChain.SufiBlazor.Components.Navigation;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
public class SbNavMenuTests : BunitContext
{
private IRenderedComponent<SbNavMenu> RenderNavMenu(
Action<ComponentParameterCollectionBuilder<SbNavMenu>>? configure = null)
{
return Render<SbNavMenu>(p => configure?.Invoke(p));
}
private IRenderedComponent<SbNavItem> RenderNavItem(
Action<ComponentParameterCollectionBuilder<SbNavItem>>? configure = null)
{
return Render<SbNavItem>(p => configure?.Invoke(p));
}
// --- SbNavMenu tests ---
[Fact]
public void RendersNavMenuStructure()
{
// Arrange & Act
var cut = RenderNavMenu(p => p.AddChildContent("<span>Nav content</span>"));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
Assert.NotNull(nav);
Assert.Equal("nav", nav.TagName.ToLowerInvariant());
Assert.Equal("Navigation", nav.GetAttribute("aria-label"));
}
[Fact]
public void RendersChildContent()
{
// Arrange & Act
var cut = RenderNavMenu(p => p.AddChildContent("<ul><li>Item</li></ul>"));
// Assert
Assert.Contains("Item", cut.Markup);
Assert.NotNull(cut.Find("ul"));
}
[Fact]
public void UsesCustomAriaLabel()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.AriaLabel, "Main menu")
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav");
Assert.Equal("Main menu", nav.GetAttribute("aria-label"));
}
[Fact]
public void AddsCollapsedClassWhenCollapsedTrue()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.Collapsed, true)
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
Assert.Contains("sb-nav-menu--collapsed", nav.ClassList);
}
[Fact]
public void DoesNotAddCollapsedClassWhenCollapsedFalse()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.Collapsed, false)
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
Assert.DoesNotContain("sb-nav-menu--collapsed", nav.ClassList);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.Class, "sidebar-menu")
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
Assert.Contains("sidebar-menu", nav.ClassList);
}
[Fact]
public void AppliesStyleParameter()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.Style, "width: 200px;")
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav");
var style = nav.GetAttribute("style");
Assert.Contains("width: 200px", style);
}
[Fact]
public async Task ToggleAsync_FlipsCollapsedAndInvokesCallback()
{
// Arrange
var received = false;
var cut = RenderNavMenu(p => p
.Add(x => x.Collapsed, false)
.Add(x => x.CollapsedChanged, EventCallback.Factory.Create<bool>(this, v => received = v))
.AddChildContent("<span></span>"));
// Act
await cut.InvokeAsync(() => cut.Instance.ToggleAsync());
// Assert
Assert.True(received);
cut.Render();
var nav = cut.Find("nav.sb-nav-menu");
Assert.Contains("sb-nav-menu--collapsed", nav.ClassList);
}
[Fact]
public void PassesAdditionalAttributes()
{
// Arrange & Act
var cut = RenderNavMenu(p => p
.Add(x => x.AdditionalAttributes, new Dictionary<string, object> { { "data-testid", "main-nav" } })
.AddChildContent("<span></span>"));
// Assert
var nav = cut.Find("nav");
Assert.Equal("main-nav", nav.GetAttribute("data-testid"));
}
[Fact]
public void RendersNavItemChildren()
{
// Arrange & Act
var cut = Render<SbNavMenu>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbNavItem>(0);
b.AddAttribute(1, "Href", "/");
b.AddAttribute(2, "Text", "Home");
b.CloseComponent();
b.OpenComponent<SbNavItem>(3);
b.AddAttribute(4, "Href", "/about");
b.AddAttribute(5, "Text", "About");
b.CloseComponent();
}));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
Assert.NotNull(nav);
var items = cut.FindAll("a.sb-nav-item");
Assert.Equal(2, items.Count);
Assert.Contains("Home", cut.Markup);
Assert.Contains("About", cut.Markup);
}
// --- SbNavItem tests ---
[Fact]
public void NavItem_RendersAsAnchor()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/dashboard")
.Add(x => x.Text, "Dashboard"));
// Assert
var anchor = cut.Find("a.sb-nav-item");
Assert.NotNull(anchor);
Assert.Equal("/dashboard", anchor.GetAttribute("href"));
}
[Fact]
public void NavItem_RendersText()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Home"));
// Assert
var text = cut.Find(".sb-nav-item__text");
Assert.NotNull(text);
Assert.Contains("Home", text.TextContent);
}
[Fact]
public void NavItem_RendersIconWhenProvided()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Home")
.Add(x => x.Icon, (RenderFragment)(b => b.AddMarkupContent(0, "<span>ico</span>"))));
// Assert
var icon = cut.Find(".sb-nav-item__icon");
Assert.NotNull(icon);
Assert.Contains("ico", icon.InnerHtml);
}
[Fact]
public void NavItem_RendersBadgeWhenProvided()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/inbox")
.Add(x => x.Text, "Inbox")
.Add(x => x.Badge, (RenderFragment)(b => b.AddMarkupContent(0, "3"))));
// Assert
var badge = cut.Find(".sb-nav-item__badge");
Assert.NotNull(badge);
Assert.Contains("3", badge.TextContent);
}
[Fact]
public void NavItem_AddsActiveClassWhenCurrentPage()
{
// Arrange - navigate to /products then render item with Href="/products"
var navMan = Services.GetRequiredService<BunitNavigationManager>();
navMan.NavigateTo("http://localhost/products");
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/products")
.Add(x => x.Text, "Products"));
// Assert
var anchor = cut.Find("a.sb-nav-item");
Assert.Contains("sb-nav-item--active", anchor.ClassList);
}
[Fact]
public void NavItem_DoesNotAddActiveClassWhenNotCurrentPage()
{
// Arrange - default URI is base, render item with Href="/other"
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/other-page")
.Add(x => x.Text, "Other"));
// Assert
var anchor = cut.Find("a.sb-nav-item");
Assert.DoesNotContain("sb-nav-item--active", anchor.ClassList);
}
[Fact]
public void NavItem_AddsDisabledClassWhenDisabledTrue()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Disabled")
.Add(x => x.Disabled, true));
// Assert
var anchor = cut.Find("a.sb-nav-item");
Assert.Contains("sb-nav-item--disabled", anchor.ClassList);
}
[Fact]
public void NavItem_AppliesClassParameter()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Item")
.Add(x => x.Class, "custom-item"));
// Assert
var anchor = cut.Find("a.sb-nav-item");
Assert.Contains("custom-item", anchor.ClassList);
}
[Fact]
public void NavItem_AppliesStyleParameter()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Item")
.Add(x => x.Style, "font-weight: bold;"));
// Assert
var anchor = cut.Find("a");
var style = anchor.GetAttribute("style");
Assert.Contains("font-weight: bold", style);
}
[Fact]
public void NavItem_PassesAdditionalAttributes()
{
// Arrange & Act
var cut = RenderNavItem(p => p
.Add(x => x.Href, "/")
.Add(x => x.Text, "Item")
.Add(x => x.AdditionalAttributes, new Dictionary<string, object> { { "data-testid", "nav-home" } }));
// Assert
var anchor = cut.Find("a");
Assert.Equal("nav-home", anchor.GetAttribute("data-testid"));
}
[Fact]
public void NavItem_WorksInsideSbNavMenu()
{
// Arrange & Act
var cut = Render<SbNavMenu>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbNavItem>(0);
b.AddAttribute(1, "Href", "/dashboard");
b.AddAttribute(2, "Text", "Dashboard");
b.CloseComponent();
}));
// Assert
var nav = cut.Find("nav.sb-nav-menu");
var item = cut.Find("a.sb-nav-item");
Assert.NotNull(nav);
Assert.NotNull(item);
Assert.Equal("/dashboard", item.GetAttribute("href"));
Assert.Contains("Dashboard", cut.Markup);
}
}
@@ -0,0 +1,493 @@
using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Bunit;
using SufiChain.SufiBlazor.Components.Navigation;
using SufiChain.SufiBlazor.Localization;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
file class StubStringLocalizer : IStringLocalizer<SufiBlazorResource>
{
public LocalizedString this[string name] => new(name, name);
public LocalizedString this[string name, params object[] arguments] => new(name, string.Format(CultureInfo.InvariantCulture, name, arguments));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => Array.Empty<LocalizedString>();
}
public class SbStepperTests : BunitContext
{
public SbStepperTests()
{
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
}
private IRenderedComponent<SbStepper> RenderStepper(
Action<ComponentParameterCollectionBuilder<SbStepper>>? configure = null)
{
return Render<SbStepper>(p => configure?.Invoke(p));
}
private IRenderedComponent<SbStep> RenderStep(
Action<ComponentParameterCollectionBuilder<SbStep>>? configure = null)
{
return Render<SbStep>(p => configure?.Invoke(p));
}
// --- SbStepper tests ---
[Fact]
public void RendersStepperStructure()
{
// Arrange & Act
var cut = RenderStepper(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Content 1</p>")));
b.CloseComponent();
}));
// Assert
var stepper = cut.Find(".sb-stepper");
Assert.NotNull(stepper);
Assert.NotNull(cut.Find(".sb-stepper__steps"));
Assert.NotNull(cut.Find(".sb-stepper__content"));
}
[Fact]
public void UsesHorizontalOrientationByDefault()
{
// Arrange & Act
var cut = RenderStepper(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
}));
// Assert
var stepper = cut.Find(".sb-stepper");
Assert.Contains("sb-stepper--horizontal", stepper.ClassList);
}
[Theory]
[InlineData(SbStepperOrientation.Horizontal, "horizontal")]
[InlineData(SbStepperOrientation.Vertical, "vertical")]
public void AppliesOrientationClass(SbStepperOrientation orientation, string expectedClass)
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.Orientation, orientation)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
}));
// Assert
var stepper = cut.Find(".sb-stepper");
Assert.Contains($"sb-stepper--{expectedClass}", stepper.ClassList);
}
[Fact]
public void RendersStepLabels()
{
// Arrange & Act
var cut = RenderStepper(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "First");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Second");
b.CloseComponent();
}));
// Assert
Assert.Contains("First", cut.Markup);
Assert.Contains("Second", cut.Markup);
var stepButtons = cut.FindAll(".sb-stepper__step-button");
Assert.Equal(2, stepButtons.Count);
}
[Fact]
public void ShowsNavigationByDefault()
{
// Arrange & Act - 2 steps so we get Next button (single step shows Finish)
var cut = RenderStepper(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert
var nav = cut.Find(".sb-stepper__navigation");
Assert.NotNull(nav);
Assert.NotNull(cut.Find("button.sb-stepper__back"));
Assert.NotNull(cut.Find("button.sb-stepper__next"));
}
[Fact]
public void HidesNavigationWhenShowNavigationFalse()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.ShowNavigation, false)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
}));
// Assert
Assert.Empty(cut.FindAll(".sb-stepper__navigation"));
}
[Fact]
public void BackButtonDisabledOnFirstStep()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 0)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert
var backBtn = cut.Find("button.sb-stepper__back");
Assert.NotNull(backBtn.GetAttribute("disabled"));
}
[Fact]
public void ShowsFinishButtonOnLastStep()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 1)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert
Assert.NotNull(cut.Find("button.sb-stepper__finish"));
Assert.Empty(cut.FindAll("button.sb-stepper__next"));
}
[Fact]
public void AppliesCustomButtonText()
{
// Arrange & Act - 2 steps at index 1 shows Back + Finish; at index 0 shows Back + Next
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 1)
.Add(x => x.BackText, "Go Back")
.Add(x => x.NextText, "Continue")
.Add(x => x.FinishText, "Done")
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert - on last step: Back + Finish visible
Assert.Contains("Go Back", cut.Markup);
Assert.Contains("Done", cut.Markup);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.Class, "my-stepper")
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
}));
// Assert
var stepper = cut.Find(".sb-stepper");
Assert.Contains("my-stepper", stepper.ClassList);
}
[Fact]
public void StepButtonsHaveCorrectStatusClasses()
{
// Arrange & Act - 3 steps, active at index 1
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 1)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
b.OpenComponent<SbStep>(4);
b.AddAttribute(5, "Label", "Step 3");
b.CloseComponent();
}));
// Assert - completed, active, pending
var steps = cut.FindAll(".sb-stepper__step");
Assert.Equal(3, steps.Count);
Assert.Contains("sb-stepper__step--completed", steps[0].ClassList);
Assert.Contains("sb-stepper__step--active", steps[1].ClassList);
Assert.Contains("sb-stepper__step--pending", steps[2].ClassList);
}
[Fact]
public async Task NextButtonAdvancesActiveIndex()
{
// Arrange
var received = -1;
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 0)
.Add(x => x.ActiveIndexChanged, EventCallback.Factory.Create<int>(this, i => received = i))
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Act
var nextBtn = cut.Find("button.sb-stepper__next");
await cut.InvokeAsync(() => nextBtn.Click());
// Assert
Assert.Equal(1, received);
}
[Fact]
public async Task BackButtonDecrementsActiveIndex()
{
// Arrange
var received = -1;
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 1)
.Add(x => x.ActiveIndexChanged, EventCallback.Factory.Create<int>(this, i => received = i))
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Act
var backBtn = cut.Find("button.sb-stepper__back");
await cut.InvokeAsync(() => backBtn.Click());
// Assert
Assert.Equal(0, received);
}
[Fact]
public async Task FinishButtonInvokesOnFinish()
{
// Arrange
var finished = false;
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 1)
.Add(x => x.OnFinish, EventCallback.Factory.Create(this, () => finished = true))
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Act
var finishBtn = cut.Find("button.sb-stepper__finish");
await cut.InvokeAsync(() => finishBtn.Click());
// Assert
Assert.True(finished);
}
[Fact]
public void LinearModeDisablesFutureStepButtons()
{
// Arrange & Act - Linear=true (default), ActiveIndex=0
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 0)
.Add(x => x.Linear, true)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert - first step button enabled, second disabled
var buttons = cut.FindAll(".sb-stepper__step-button");
Assert.Null(buttons[0].GetAttribute("disabled"));
Assert.NotNull(buttons[1].GetAttribute("disabled"));
}
[Fact]
public void NonLinearModeEnablesAllStepButtons()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.ActiveIndex, 0)
.Add(x => x.Linear, false)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Step 2");
b.CloseComponent();
}));
// Assert
var buttons = cut.FindAll(".sb-stepper__step-button");
Assert.Null(buttons[0].GetAttribute("disabled"));
Assert.Null(buttons[1].GetAttribute("disabled"));
}
[Fact]
public void RendersOptionalLabelWhenStepOptional()
{
// Arrange & Act
var cut = RenderStepper(p => p
.Add(x => x.OptionalText, "Optional")
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Required");
b.CloseComponent();
b.OpenComponent<SbStep>(2);
b.AddAttribute(3, "Label", "Optional step");
b.AddAttribute(4, "Optional", true);
b.CloseComponent();
}));
// Assert
Assert.Contains("Optional", cut.Markup);
var optionalStep = cut.FindAll(".sb-stepper__step--optional");
Assert.Single(optionalStep);
}
[Fact]
public void RendersStepDescription()
{
// Arrange & Act
var cut = RenderStepper(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.AddAttribute(2, "Description", "Enter your details");
b.CloseComponent();
}));
// Assert
Assert.Contains("Enter your details", cut.Markup);
}
// --- SbStep tests ---
[Fact]
public void Step_RendersContentWhenActive()
{
// Arrange & Act
var cut = Render<SbStepper>(p => p
.Add(x => x.ActiveIndex, 0)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Active content</p>")));
b.CloseComponent();
}));
// Assert
var content = cut.Find(".sb-step-content");
Assert.NotNull(content);
Assert.Contains("Active content", cut.Markup);
}
[Fact]
public void Step_DoesNotRenderContentWhenNotActive()
{
// Arrange & Act - ActiveIndex 1, so first step's content is hidden
var cut = Render<SbStepper>(p => p
.Add(x => x.ActiveIndex, 1)
.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "Step 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Hidden content</p>")));
b.CloseComponent();
b.OpenComponent<SbStep>(3);
b.AddAttribute(4, "Label", "Step 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Visible content</p>")));
b.CloseComponent();
}));
// Assert - only Step 2 content visible
Assert.Contains("Visible content", cut.Markup);
Assert.DoesNotContain("Hidden content", cut.Markup);
}
[Fact]
public void Step_WorksInsideSbStepper()
{
// Arrange & Act
var cut = Render<SbStepper>(p => p.AddChildContent(b =>
{
b.OpenComponent<SbStep>(0);
b.AddAttribute(1, "Label", "First Step");
b.AddAttribute(2, "Description", "Description text");
b.AddAttribute(3, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Step body")));
b.CloseComponent();
}));
// Assert
var stepper = cut.Find(".sb-stepper");
Assert.NotNull(stepper);
Assert.Contains("First Step", cut.Markup);
Assert.Contains("Description text", cut.Markup);
Assert.Contains("Step body", cut.Markup);
}
}
+434
View File
@@ -0,0 +1,434 @@
using Microsoft.AspNetCore.Components;
using Bunit;
using SufiChain.SufiBlazor.Components.Navigation;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
public class SbTabsTests : BunitContext
{
private IRenderedComponent<SbTabs> RenderTabs(
Action<ComponentParameterCollectionBuilder<SbTabs>>? configure = null)
{
return Render<SbTabs>(p => configure?.Invoke(p));
}
// --- SbTabs tests ---
[Fact]
public void RendersTabsStructure()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<p>Content 1</p>")));
b.CloseComponent();
}));
// Assert
var tabs = cut.Find(".sb-tabs");
Assert.NotNull(tabs);
Assert.NotNull(cut.Find(".sb-tabs__list"));
Assert.Equal("tablist", cut.Find(".sb-tabs__list").GetAttribute("role"));
Assert.NotNull(cut.Find(".sb-tabs__content"));
}
[Fact]
public void RendersTabLabels()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "First");
b.CloseComponent();
b.OpenComponent<SbTab>(2);
b.AddAttribute(3, "Label", "Second");
b.CloseComponent();
}));
// Assert
Assert.Contains("First", cut.Markup);
Assert.Contains("Second", cut.Markup);
var tabButtons = cut.FindAll("button[role=\"tab\"]");
Assert.Equal(2, tabButtons.Count);
}
[Fact]
public void FirstTabActiveByDefault()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content 1")));
b.CloseComponent();
b.OpenComponent<SbTab>(3);
b.AddAttribute(4, "Label", "Tab 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content 2")));
b.CloseComponent();
}));
// Assert
var firstButton = cut.FindAll("button.sb-tabs__tab")[0];
Assert.Contains("sb-tabs__tab--active", firstButton.ClassList);
Assert.Contains("Content 1", cut.Markup);
}
[Fact]
public void ActiveIndexSelectsCorrectTab()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.ActiveIndex, 1)
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content 1")));
b.CloseComponent();
b.OpenComponent<SbTab>(3);
b.AddAttribute(4, "Label", "Tab 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content 2")));
b.CloseComponent();
}));
// Assert
var buttons = cut.FindAll("button.sb-tabs__tab");
Assert.Contains("sb-tabs__tab--active", buttons[1].ClassList);
Assert.Contains("Content 2", cut.Markup);
}
[Fact]
public void ActiveTabSelectsById()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.ActiveTab, "tab-b")
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Id", "tab-a");
b.AddAttribute(2, "Label", "A");
b.AddAttribute(3, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "A content")));
b.CloseComponent();
b.OpenComponent<SbTab>(4);
b.AddAttribute(5, "Id", "tab-b");
b.AddAttribute(6, "Label", "B");
b.AddAttribute(7, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "B content")));
b.CloseComponent();
}));
// Assert
var buttons = cut.FindAll("button.sb-tabs__tab");
Assert.Contains("sb-tabs__tab--active", buttons[1].ClassList);
Assert.Contains("B content", cut.Markup);
}
[Fact]
public async Task ClickingTabInvokesActiveIndexChanged()
{
// Arrange
var received = -1;
var cut = RenderTabs(p => p
.Add(x => x.ActiveIndexChanged, EventCallback.Factory.Create<int>(this, i => received = i))
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.CloseComponent();
b.OpenComponent<SbTab>(2);
b.AddAttribute(3, "Label", "Tab 2");
b.CloseComponent();
}));
// Act - click second tab
var buttons = cut.FindAll("button.sb-tabs__tab");
await cut.InvokeAsync(() => buttons[1].Click());
// Assert
Assert.Equal(1, received);
}
[Fact]
public async Task ClickingTabInvokesActiveTabChanged()
{
// Arrange
string? receivedId = null;
var cut = RenderTabs(p => p
.Add(x => x.ActiveTabChanged, EventCallback.Factory.Create<string?>(this, id => receivedId = id))
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Id", "first");
b.AddAttribute(2, "Label", "First");
b.CloseComponent();
b.OpenComponent<SbTab>(3);
b.AddAttribute(4, "Id", "second");
b.AddAttribute(5, "Label", "Second");
b.CloseComponent();
}));
// Act
var buttons = cut.FindAll("button.sb-tabs__tab");
await cut.InvokeAsync(() => buttons[1].Click());
// Assert
Assert.Equal("second", receivedId);
}
[Fact]
public void DisabledTabHasDisabledClassAndAttribute()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Active");
b.CloseComponent();
b.OpenComponent<SbTab>(2);
b.AddAttribute(3, "Label", "Disabled");
b.AddAttribute(4, "Disabled", true);
b.CloseComponent();
}));
// Assert
var buttons = cut.FindAll("button.sb-tabs__tab");
Assert.Contains("sb-tabs__tab--disabled", buttons[1].ClassList);
Assert.NotNull(buttons[1].GetAttribute("disabled"));
}
[Fact]
public async Task ClickingDisabledTabDoesNotChangeSelection()
{
// Arrange
var received = -1;
var cut = RenderTabs(p => p
.Add(x => x.ActiveIndex, 0)
.Add(x => x.ActiveIndexChanged, EventCallback.Factory.Create<int>(this, i => received = i))
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Active");
b.CloseComponent();
b.OpenComponent<SbTab>(2);
b.AddAttribute(3, "Label", "Disabled");
b.AddAttribute(4, "Disabled", true);
b.CloseComponent();
}));
// Act - try to click disabled tab
var buttons = cut.FindAll("button.sb-tabs__tab");
await cut.InvokeAsync(() => buttons[1].Click());
// Assert - callback not invoked
Assert.Equal(-1, received);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.Class, "my-tabs")
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.CloseComponent();
}));
// Assert
var tabs = cut.Find(".sb-tabs");
Assert.Contains("my-tabs", tabs.ClassList);
}
[Fact]
public void VerticalAddsVerticalClass()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.Vertical, true)
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.CloseComponent();
}));
// Assert
var tabs = cut.Find(".sb-tabs");
Assert.Contains("sb-tabs--vertical", tabs.ClassList);
}
[Fact]
public void RendersTabIconWhenProvided()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "With Icon");
b.AddAttribute(2, "Icon", (RenderFragment)(c => c.AddMarkupContent(0, "<span>ico</span>")));
b.CloseComponent();
}));
// Assert
var icon = cut.Find(".sb-tabs__tab-icon");
Assert.NotNull(icon);
Assert.Contains("ico", icon.InnerHtml);
}
[Fact]
public void RendersTabBadgeWhenProvided()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Inbox");
b.AddAttribute(2, "Badge", (RenderFragment)(c => c.AddMarkupContent(0, "5")));
b.CloseComponent();
}));
// Assert
var badge = cut.Find(".sb-tabs__tab-badge");
Assert.NotNull(badge);
Assert.Contains("5", badge.TextContent);
}
[Fact]
public void TabPanelHasCorrectRoleAndId()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Content")));
b.CloseComponent();
}));
// Assert
var panel = cut.Find(".sb-tab-panel");
Assert.NotNull(panel);
Assert.Equal("tabpanel", panel.GetAttribute("role"));
Assert.NotNull(panel.GetAttribute("id"));
Assert.StartsWith("tab-panel-", panel.GetAttribute("id")!);
}
[Fact]
public void ActiveTabPanelNotHidden()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Visible")));
b.CloseComponent();
}));
// Assert
var panel = cut.Find(".sb-tab-panel--active");
Assert.NotNull(panel);
Assert.Null(panel.GetAttribute("hidden"));
}
[Fact]
public void LazyLoadOnlyRendersActiveTabContent()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.LazyLoad, true)
.Add(x => x.ActiveIndex, 0)
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Lazy 1")));
b.CloseComponent();
b.OpenComponent<SbTab>(3);
b.AddAttribute(4, "Label", "Tab 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Lazy 2")));
b.CloseComponent();
}));
// Assert - only first tab content rendered
Assert.Contains("Lazy 1", cut.Markup);
Assert.DoesNotContain("Lazy 2", cut.Markup);
}
[Fact]
public async Task LazyLoadRendersTabContentAfterActivation()
{
// Arrange
var cut = RenderTabs(p => p
.Add(x => x.LazyLoad, true)
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Lazy 1")));
b.CloseComponent();
b.OpenComponent<SbTab>(3);
b.AddAttribute(4, "Label", "Tab 2");
b.AddAttribute(5, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Lazy 2")));
b.CloseComponent();
}));
// Act - click second tab
var buttons = cut.FindAll("button.sb-tabs__tab");
await cut.InvokeAsync(() => buttons[1].Click());
cut.Render();
// Assert - now both visible (second was activated)
Assert.Contains("Lazy 1", cut.Markup);
Assert.Contains("Lazy 2", cut.Markup);
}
// --- SbTab tests ---
[Fact]
public void Tab_RendersContentWhenActive()
{
// Arrange & Act
var cut = RenderTabs(p => p
.Add(x => x.ActiveIndex, 0)
.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Label", "Tab 1");
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Tab content")));
b.CloseComponent();
}));
// Assert
var panel = cut.Find(".sb-tab-panel");
Assert.NotNull(panel);
Assert.Contains("Tab content", cut.Markup);
}
[Fact]
public void Tab_WorksInsideSbTabs()
{
// Arrange & Act
var cut = RenderTabs(p => p.AddChildContent(b =>
{
b.OpenComponent<SbTab>(0);
b.AddAttribute(1, "Id", "settings");
b.AddAttribute(2, "Label", "Settings");
b.AddAttribute(3, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "Settings panel")));
b.CloseComponent();
}));
// Assert
var tabs = cut.Find(".sb-tabs");
Assert.NotNull(tabs);
Assert.Contains("Settings", cut.Markup);
Assert.Contains("Settings panel", cut.Markup);
}
}
@@ -0,0 +1,342 @@
using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Bunit;
using SufiChain.SufiBlazor.Components.Navigation;
using SufiChain.SufiBlazor.Localization;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Navigation;
file class StubStringLocalizer : IStringLocalizer<SufiBlazorResource>
{
public LocalizedString this[string name] => new(name, name);
public LocalizedString this[string name, params object[] arguments] => new(name, string.Format(CultureInfo.InvariantCulture, name, arguments));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => Array.Empty<LocalizedString>();
}
public class SbTreeViewTests : BunitContext
{
private class TreeTestItem
{
public string Name { get; set; } = "";
public string? Icon { get; set; }
public List<TreeTestItem> Children { get; set; } = new();
}
public SbTreeViewTests()
{
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
}
private IRenderedComponent<SbTreeView<TreeTestItem>> RenderTreeView(
Action<ComponentParameterCollectionBuilder<SbTreeView<TreeTestItem>>>? configure = null)
{
return Render<SbTreeView<TreeTestItem>>(p => configure?.Invoke(p));
}
private static List<TreeTestItem> CreateSimpleTree() => new()
{
new TreeTestItem { Name = "Root 1" },
new TreeTestItem
{
Name = "Root 2",
Children = new()
{
new TreeTestItem { Name = "Child 2.1" },
new TreeTestItem { Name = "Child 2.2" }
}
}
};
// --- SbTreeView tests ---
[Fact]
public void RendersTreeViewStructure()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Item 1" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
var tree = cut.Find(".sb-treeview");
Assert.NotNull(tree);
Assert.Equal("tree", tree.GetAttribute("role"));
}
[Fact]
public void RendersEmptyWhenNoItems()
{
// Arrange & Act
var cut = RenderTreeView(p => p
.Add(x => x.Items, new List<TreeTestItem>())
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
var tree = cut.Find(".sb-treeview");
Assert.NotNull(tree);
Assert.Empty(cut.FindAll(".sb-treeview-node"));
}
[Fact]
public void RendersRootNodes()
{
// Arrange & Act
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
Assert.Contains("Root 1", cut.Markup);
Assert.Contains("Root 2", cut.Markup);
var nodes = cut.FindAll(".sb-treeview-node");
Assert.Equal(2, nodes.Count);
}
[Fact]
public void RendersChildrenWhenExpanded()
{
// Arrange - Root 2 has children, expand via IsExpandedSelector
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.IsExpandedSelector, i => i.Name == "Root 2"));
// Assert
Assert.Contains("Child 2.1", cut.Markup);
Assert.Contains("Child 2.2", cut.Markup);
}
[Fact]
public void DoesNotRenderChildrenWhenCollapsed()
{
// Arrange - all collapsed (default)
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert - children not visible when collapsed
Assert.DoesNotContain("Child 2.1", cut.Markup);
Assert.DoesNotContain("Child 2.2", cut.Markup);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Item" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.Class, "my-tree"));
// Assert
var tree = cut.Find(".sb-treeview");
Assert.Contains("my-tree", tree.ClassList);
}
[Fact]
public void RendersIconWhenIconSelectorProvided()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Folder", Icon = "folder" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.IconSelector, i => i.Icon));
// Assert
var icon = cut.Find(".sb-treeview-node__icon");
Assert.NotNull(icon);
}
[Fact]
public void AppliesSelectedClassWhenSelectedItemMatches()
{
// Arrange
var items = CreateSimpleTree();
var selected = items[0];
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.SelectedItem, selected));
// Assert
var selectedContent = cut.Find(".sb-treeview-node__content--selected");
Assert.NotNull(selectedContent);
Assert.Contains("Root 1", selectedContent.TextContent);
}
[Fact]
public async Task ClickingNodeInvokesSelectedItemChanged()
{
// Arrange
TreeTestItem? received = null;
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.SelectedItemChanged, EventCallback.Factory.Create<TreeTestItem>(this, item => received = item)));
// Act - click Root 1 content
var content = cut.Find(".sb-treeview-node__content");
await cut.InvokeAsync(() => content.Click());
// Assert
Assert.NotNull(received);
Assert.Equal("Root 1", received.Name);
}
[Fact]
public void ShowsCheckboxesWhenShowCheckboxesTrue()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Item" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.ShowCheckboxes, true));
// Assert
var checkbox = cut.Find("input.sb-treeview-node__checkbox");
Assert.NotNull(checkbox);
Assert.Equal("checkbox", checkbox.GetAttribute("type"));
}
[Fact]
public void DoesNotShowCheckboxesByDefault()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Item" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
Assert.Empty(cut.FindAll("input.sb-treeview-node__checkbox"));
}
[Fact]
public async Task ToggleButtonExpandsNode()
{
// Arrange
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Initially children not visible
Assert.DoesNotContain("Child 2.1", cut.Markup);
// Act - click toggle on Root 2
var toggle = cut.FindAll("button.sb-treeview-node__toggle").Last();
await cut.InvokeAsync(() => toggle.Click());
cut.Render();
// Assert - children now visible
Assert.Contains("Child 2.1", cut.Markup);
}
[Fact]
public async Task ToggleInvokesOnToggleCallback()
{
// Arrange
TreeTestItem? toggled = null;
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.OnToggle, EventCallback.Factory.Create<TreeTestItem>(this, item => toggled = item)));
// Act
var toggle = cut.FindAll("button.sb-treeview-node__toggle").Last();
await cut.InvokeAsync(() => toggle.Click());
// Assert
Assert.NotNull(toggled);
Assert.Equal("Root 2", toggled.Name);
}
[Fact]
public void NodeHasCorrectRoleAndLevel()
{
// Arrange & Act
var items = new List<TreeTestItem> { new() { Name = "Item" } };
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
var node = cut.Find(".sb-treeview-node");
Assert.Equal("treeitem", node.GetAttribute("role"));
Assert.Equal("1", node.GetAttribute("aria-level"));
}
// --- SbTreeViewNode tests (via SbTreeView) ---
[Fact]
public void Node_LeafHasSpacerInsteadOfToggle()
{
// Arrange - Root 1 has no children
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert - leaf has spacer
var spacer = cut.Find(".sb-treeview-node__spacer");
Assert.NotNull(spacer);
}
[Fact]
public void Node_WithChildrenHasToggleButton()
{
// Arrange & Act
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children));
// Assert
var toggles = cut.FindAll("button.sb-treeview-node__toggle");
Assert.Single(toggles); // Root 2 has children
}
[Fact]
public void Node_ExpandedHasExpandedClass()
{
// Arrange
var items = CreateSimpleTree();
var cut = RenderTreeView(p => p
.Add(x => x.Items, items)
.Add(x => x.TextSelector, i => i.Name)
.Add(x => x.ChildSelector, i => i.Children)
.Add(x => x.IsExpandedSelector, i => i.Name == "Root 2"));
// Assert
var expandedNode = cut.Find(".sb-treeview-node--expanded");
Assert.NotNull(expandedNode);
}
}