This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user