first commit

This commit is contained in:
2026-05-18 15:53:59 +03:30
commit 2c100028a1
534 changed files with 94240 additions and 0 deletions
+847
View File
@@ -0,0 +1,847 @@
using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Bunit;
using SufiChain.SufiBlazor.Components.Data;
using SufiChain.SufiBlazor.Contracts.Data;
using SufiChain.SufiBlazor.Localization;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Data;
/// <summary>
/// Stub localizer that returns the key as the value (for testing).
/// </summary>
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 SbDataGridTests : BunitContext
{
public SbDataGridTests()
{
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
}
private static List<TestItem> CreateTestItems(int count)
{
return Enumerable.Range(1, count)
.Select(i => new TestItem { Id = i, Name = $"Item {i}", Value = i * 10 })
.ToList();
}
private static RenderFragment ColumnsTemplate => builder =>
{
builder.OpenComponent<SbColumn<TestItem>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "Title", "Name");
builder.CloseComponent();
builder.OpenComponent<SbColumn<TestItem>>(3);
builder.AddAttribute(4, "Field", "Value");
builder.AddAttribute(5, "Title", "Value");
builder.AddAttribute(6, "FieldType", typeof(int));
builder.CloseComponent();
};
private static RenderFragment SortableColumnsTemplate => builder =>
{
builder.OpenComponent<SbColumn<TestItem>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "Title", "Name");
builder.AddAttribute(3, "Sortable", true);
builder.CloseComponent();
builder.OpenComponent<SbColumn<TestItem>>(4);
builder.AddAttribute(5, "Field", "Value");
builder.AddAttribute(6, "Title", "Value");
builder.AddAttribute(7, "FieldType", typeof(int));
builder.AddAttribute(8, "Sortable", true);
builder.CloseComponent();
};
private IRenderedComponent<SbDataGrid<TestItem>> RenderDataGrid(
List<TestItem>? items = null,
Action<ComponentParameterCollectionBuilder<SbDataGrid<TestItem>>>? configure = null)
{
items ??= CreateTestItems(3);
return Render<SbDataGrid<TestItem>>(p =>
{
p.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate);
configure?.Invoke(p);
});
}
[Fact]
public void RendersGridWithTableStructure()
{
// Arrange & Act
var cut = RenderDataGrid();
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.NotNull(grid);
Assert.NotNull(cut.Find("table.sb-datagrid__table"));
Assert.NotNull(cut.Find("thead.sb-datagrid__header"));
Assert.NotNull(cut.Find("tbody.sb-datagrid__body"));
}
[Fact]
public void RendersColumnHeaders()
{
// Arrange & Act
var cut = RenderDataGrid();
// Assert
Assert.Contains("Name", cut.Markup);
Assert.Contains("Value", cut.Markup);
}
[Fact]
public void RendersDataRows()
{
// Arrange
var items = CreateTestItems(2);
// Act
var cut = RenderDataGrid(items);
// Assert
Assert.Contains("Item 1", cut.Markup);
Assert.Contains("Item 2", cut.Markup);
Assert.Contains("10", cut.Markup);
Assert.Contains("20", cut.Markup);
}
[Fact]
public void RendersEmptyStateWhenNoItems()
{
// Arrange & Act
var cut = RenderDataGrid(new List<TestItem>());
// Assert
Assert.Contains("No data available", cut.Markup);
}
[Fact]
public void RendersEmptyStateWhenItemsIsNull()
{
// Arrange & Act
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, (IEnumerable<TestItem>?)null)
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate));
// Assert
Assert.Contains("No data available", cut.Markup);
}
[Fact]
public void RendersCustomEmptyTemplate()
{
// Arrange & Act
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, new List<TestItem>())
.Add(x => x.ShowPagination, false)
.Add(x => x.EmptyTemplate, ec => ec.AddMarkupContent(0, "<span class=\"custom-empty\">No items found</span>"))
.AddChildContent(ColumnsTemplate));
// Assert
var empty = cut.Find(".custom-empty");
Assert.NotNull(empty);
Assert.Contains("No items found", empty.TextContent);
}
[Fact]
public void ShowsLoadingState()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Loading, true));
// Assert
var loading = cut.Find(".sb-datagrid__loading");
Assert.NotNull(loading);
Assert.Contains("Loading", cut.Markup);
}
[Fact]
public void AppliesCustomClass()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Class, "my-grid"));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("my-grid", grid.ClassList);
}
[Fact]
public void AppliesStripedClass()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Striped, true));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("sb-datagrid--striped", grid.ClassList);
}
[Fact]
public void AppliesBorderedClass()
{
// Arrange & Act
var cut = RenderDataGrid();
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("sb-datagrid--bordered", grid.ClassList);
}
[Fact]
public void AppliesBorderlessWhenBorderedFalse()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Bordered, false));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("sb-datagrid--borderless", grid.ClassList);
}
[Fact]
public void AppliesDensityCompact()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Density, SbDataGridDensity.Compact));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("sb-datagrid--density-compact", grid.ClassList);
}
[Fact]
public void AppliesDensityComfortable()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.Density, SbDataGridDensity.Comfortable));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Contains("sb-datagrid--density-comfortable", grid.ClassList);
}
[Fact]
public void RendersSelectionColumnWhenSelectionModeMultiple()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.SelectionMode, SbSelectionMode.MultipleRows));
// Assert
var header = cut.Find(".sb-datagrid__cell--selection");
Assert.NotNull(header);
Assert.NotNull(cut.Find("input[type=\"checkbox\"]"));
}
[Fact]
public void RendersSelectionColumnWhenSelectionModeSingle()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.SelectionMode, SbSelectionMode.SingleRow));
// Assert
Assert.NotNull(cut.Find(".sb-datagrid__cell--selection"));
}
[Fact]
public void DoesNotRenderSelectionColumnWhenSelectionModeNone()
{
// Arrange & Act
var cut = RenderDataGrid();
// Assert
Assert.Empty(cut.FindAll(".sb-datagrid__cell--selection"));
}
[Fact]
public void RendersPaginationWhenEnabledAndHasData()
{
// Arrange & Act - use separate render to avoid duplicate ShowPagination (base sets false)
var items = CreateTestItems(5);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, true)
.Add(x => x.TotalCount, 10)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate));
// Assert
Assert.NotNull(cut.Find(".sb-datagrid__pagination"));
}
[Fact]
public void HasAriaLabelWhenSet()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.Add(x => x.AriaLabel, "Products grid"));
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Equal("Products grid", grid.GetAttribute("aria-label"));
}
[Fact]
public void HasRoleApplication()
{
// Arrange & Act
var cut = RenderDataGrid();
// Assert
var grid = cut.Find(".sb-datagrid");
Assert.Equal("application", grid.GetAttribute("role"));
}
#region Sorting
[Fact]
public void RendersSortableHeaderWhenColumnSortable()
{
// Arrange & Act
var cut = RenderDataGrid(configure: p => p.AddChildContent(SortableColumnsTemplate));
// Assert
var sortableHeader = cut.Find(".sb-datagrid__cell--sortable");
Assert.NotNull(sortableHeader);
}
[Fact]
public async Task InvokesOnSortChangedWhenSortableHeaderClicked()
{
// Arrange
SbSort? capturedSort = null;
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.OnSortChanged, EventCallback.Factory.Create<SbSort?>(this, s => capturedSort = s))
.AddChildContent(SortableColumnsTemplate));
// Act - click Name column header (first sortable column)
var nameHeader = cut.Find(".sb-datagrid__cell--sortable");
await cut.InvokeAsync(() => nameHeader.Click());
// Assert
Assert.NotNull(capturedSort);
Assert.Equal("Name", capturedSort!.Field);
Assert.Equal(SbSortDirection.Ascending, capturedSort.Direction);
}
[Fact]
public async Task SortsDataAscendingWhenHeaderClicked()
{
// Arrange - use items that have different sort orders: Zeta, Alpha, Beta
var items = new List<TestItem>
{
new() { Id = 1, Name = "Zeta", Value = 10 },
new() { Id = 2, Name = "Alpha", Value = 20 },
new() { Id = 3, Name = "Beta", Value = 30 }
};
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(SortableColumnsTemplate));
// Wait for initial render
cut.WaitForState(() => cut.Markup.Contains("Zeta"));
// Act - click Name header to sort ascending
var nameHeader = cut.Find(".sb-datagrid__cell--sortable");
await cut.InvokeAsync(() => nameHeader.Click());
cut.WaitForState(() =>
{
var rows = cut.FindAll(".sb-datagrid__row:not(.sb-datagrid__row--empty) .sb-datagrid__cell");
return rows.Count >= 2;
});
// Assert - first data row should be Alpha (ascending: Alpha, Beta, Zeta)
var firstDataRow = cut.FindAll("tbody .sb-datagrid__row").FirstOrDefault();
Assert.NotNull(firstDataRow);
Assert.Contains("Alpha", firstDataRow.TextContent);
}
[Fact]
public async Task SortsDataDescendingWhenHeaderClickedTwice()
{
// Arrange
var items = new List<TestItem>
{
new() { Id = 1, Name = "Alpha", Value = 10 },
new() { Id = 2, Name = "Beta", Value = 20 },
new() { Id = 3, Name = "Zeta", Value = 30 }
};
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(SortableColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Alpha"));
var nameHeader = cut.Find(".sb-datagrid__cell--sortable");
// Act - click once (ascending), then twice (descending)
await cut.InvokeAsync(() => nameHeader.Click());
await cut.InvokeAsync(() => nameHeader.Click());
cut.WaitForState(() =>
{
var rows = cut.FindAll(".sb-datagrid__row:not(.sb-datagrid__row--empty)");
return rows.Any(r => r.TextContent.Contains("Zeta"));
});
// Assert - first data row should be Zeta (descending: Zeta, Beta, Alpha)
var firstDataRow = cut.FindAll("tbody .sb-datagrid__row").FirstOrDefault();
Assert.NotNull(firstDataRow);
Assert.Contains("Zeta", firstDataRow.TextContent);
}
#endregion
#region Filtering
[Fact]
public async Task FiltersDataWhenSetFilterAsyncCalled()
{
// Arrange
var items = CreateTestItems(5);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.ShowFilterBar, true)
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
// Act - filter by Name equals "Item 2"
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Equals, "Item 2");
});
cut.WaitForState(() => cut.Markup.Contains("Item 2") && !cut.Markup.Contains("Item 1"));
// Assert
Assert.Contains("Item 2", cut.Markup);
Assert.DoesNotContain("Item 1", cut.Markup);
Assert.DoesNotContain("Item 3", cut.Markup);
}
[Fact]
public async Task ShowsFilterBarWhenFilterActive()
{
// Arrange
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.ShowFilterBar, true)
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
// Act
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Contains, "Item");
});
cut.WaitForState(() => cut.FindAll(".sb-datagrid__filter-bar").Count > 0);
// Assert
Assert.NotNull(cut.Find(".sb-datagrid__filter-bar"));
Assert.Contains("Filters:", cut.Markup);
}
[Fact]
public async Task ClearsFiltersWhenClearAllFiltersAsyncCalled()
{
// Arrange
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.ShowFilterBar, true)
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Equals, "Item 2");
});
cut.WaitForState(() => !cut.Markup.Contains("Item 1"));
// Act
await cut.InvokeAsync(async () =>
{
await cut.Instance.ClearAllFiltersAsync();
});
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
// Assert
Assert.Contains("Item 1", cut.Markup);
Assert.Contains("Item 2", cut.Markup);
Assert.Contains("Item 3", cut.Markup);
}
[Fact]
public async Task InvokesOnFiltersChangedWhenFilterSet()
{
// Arrange
IReadOnlyList<SbFilter>? capturedFilters = null;
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.OnFiltersChanged, EventCallback.Factory.Create<IReadOnlyList<SbFilter>>(this, f => capturedFilters = f))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
// Act
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Equals, "Item 2");
});
cut.WaitForState(() => capturedFilters != null);
// Assert
Assert.NotNull(capturedFilters);
Assert.Single(capturedFilters);
Assert.Equal("Name", capturedFilters![0].Field);
Assert.Equal(SbFilterOperator.Equals, capturedFilters[0].Operator);
Assert.Equal("Item 2", capturedFilters[0].Value);
}
[Fact]
public async Task RemovesFilterWhenRemoveFilterAsyncCalled()
{
// Arrange
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Equals, "Item 2");
});
cut.WaitForState(() => !cut.Markup.Contains("Item 1"));
// Act
await cut.InvokeAsync(async () =>
{
await cut.Instance.RemoveFilterAsync("Name");
});
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
// Assert - all items visible again
Assert.Contains("Item 1", cut.Markup);
Assert.Contains("Item 2", cut.Markup);
Assert.Contains("Item 3", cut.Markup);
}
[Fact]
public async Task FiltersWithSetOnlyFilterAsyncReplacesAllFilters()
{
// Arrange - set one filter, then use SetOnlyFilterAsync to replace with another
var items = CreateTestItems(5);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 1"));
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Equals, "Item 1");
});
cut.WaitForState(() => cut.Markup.Contains("Item 1") && !cut.Markup.Contains("Item 2"));
// Act - SetOnlyFilterAsync should clear Item 1 filter and set Value filter
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetOnlyFilterAsync("Value", SbFilterOperator.Equals, 30);
});
cut.WaitForState(() => cut.Markup.Contains("Item 3") && !cut.Markup.Contains("Item 1"));
// Assert - only Item 3 has Value=30
Assert.Contains("Item 3", cut.Markup);
Assert.Contains("30", cut.Markup);
Assert.DoesNotContain("Item 1", cut.Markup);
Assert.DoesNotContain("Item 2", cut.Markup);
}
#endregion
#region Selection
[Fact]
public void AppliesSelectedRowClassWhenRowSelected()
{
// Arrange
var items = CreateTestItems(2);
var selectedKeys = new HashSet<string> { "2" };
var cut = RenderDataGrid(items, p => p
.Add(x => x.SelectionMode, SbSelectionMode.SingleRow)
.Add(x => x.SelectedKeys, selectedKeys));
// Assert - row 2 should have selected class
cut.WaitForState(() => cut.FindAll(".sb-datagrid__row--selected").Count > 0);
var selectedRow = cut.Find(".sb-datagrid__row--selected");
Assert.NotNull(selectedRow);
Assert.Contains("Item 2", selectedRow.TextContent);
}
[Fact]
public async Task InvokesSelectedKeysChangedWhenRowClickedInSingleMode()
{
// Arrange
IReadOnlySet<string>? capturedKeys = null;
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.SelectionMode, SbSelectionMode.SingleRow)
.Add(x => x.SelectedKeysChanged, EventCallback.Factory.Create<IReadOnlySet<string>>(this, k => capturedKeys = k))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 2"));
// Act - click row 2 (second data row)
var rows = cut.FindAll("tbody .sb-datagrid__row");
var row2 = rows.FirstOrDefault(r => r.TextContent.Contains("Item 2"));
Assert.NotNull(row2);
await cut.InvokeAsync(() => row2!.Click());
cut.WaitForState(() => capturedKeys != null);
// Assert
Assert.NotNull(capturedKeys);
Assert.Single(capturedKeys);
Assert.Contains("2", capturedKeys!);
}
[Fact]
public async Task InvokesSelectedKeysChangedWhenCheckboxClickedInMultipleMode()
{
// Arrange
IReadOnlySet<string>? capturedKeys = null;
var items = CreateTestItems(2);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.SelectionMode, SbSelectionMode.MultipleRows)
.Add(x => x.SelectedKeysChanged, EventCallback.Factory.Create<IReadOnlySet<string>>(this, k => capturedKeys = k))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.FindAll("input[type=\"checkbox\"]").Count > 0);
// Act - change first row checkbox (SbCheckbox uses @onchange, not @onclick)
var checkboxes = cut.FindAll("tbody input[type=\"checkbox\"]");
var firstRowCheckbox = checkboxes.FirstOrDefault();
Assert.NotNull(firstRowCheckbox);
await cut.InvokeAsync(() => firstRowCheckbox!.Change(true));
cut.WaitForState(() => capturedKeys != null && capturedKeys.Count > 0);
// Assert
Assert.NotNull(capturedKeys);
Assert.Single(capturedKeys);
}
[Fact]
public async Task SelectsAllRowsWhenHeaderCheckboxClickedInMultipleMode()
{
// Arrange
IReadOnlySet<string>? capturedKeys = null;
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.SelectionMode, SbSelectionMode.MultipleRows)
.Add(x => x.SelectedKeysChanged, EventCallback.Factory.Create<IReadOnlySet<string>>(this, k => capturedKeys = k))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Find("thead input[type=\"checkbox\"]") != null);
// Act - change header "select all" checkbox (SbCheckbox uses @onchange, not @onclick)
var selectAllCheckbox = cut.Find("thead input[type=\"checkbox\"]");
await cut.InvokeAsync(() => selectAllCheckbox!.Change(true));
cut.WaitForState(() => capturedKeys != null && capturedKeys.Count == 3);
// Assert
Assert.NotNull(capturedKeys);
Assert.Equal(3, capturedKeys!.Count);
Assert.Contains("1", capturedKeys);
Assert.Contains("2", capturedKeys);
Assert.Contains("3", capturedKeys);
}
[Fact]
public async Task InvokesOnRowClickedWhenRowClicked()
{
// Arrange
TestItem? capturedItem = null;
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.OnRowClicked, EventCallback.Factory.Create<TestItem>(this, item => capturedItem = item))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.Markup.Contains("Item 2"));
// Act
var rows = cut.FindAll("tbody .sb-datagrid__row");
var row2 = rows.FirstOrDefault(r => r.TextContent.Contains("Item 2"));
await cut.InvokeAsync(() => row2!.Click());
cut.WaitForState(() => capturedItem != null);
// Assert
Assert.NotNull(capturedItem);
Assert.Equal(2, capturedItem!.Id);
Assert.Equal("Item 2", capturedItem.Name);
}
[Fact]
public async Task DeselectsRowWhenCheckboxUncheckedInMultipleMode()
{
// Arrange - start with row 1 selected
IReadOnlySet<string>? capturedKeys = null;
var items = CreateTestItems(2);
var selectedKeys = new HashSet<string> { "1" };
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.Add(x => x.SelectionMode, SbSelectionMode.MultipleRows)
.Add(x => x.SelectedKeys, selectedKeys)
.Add(x => x.SelectedKeysChanged, EventCallback.Factory.Create<IReadOnlySet<string>>(this, k => capturedKeys = k))
.AddChildContent(ColumnsTemplate));
cut.WaitForState(() => cut.FindAll("tbody input[type=\"checkbox\"]").Count > 0);
// Act - uncheck first row (Item 1) by triggering change to false
var checkboxes = cut.FindAll("tbody input[type=\"checkbox\"]");
var firstRowCheckbox = checkboxes.FirstOrDefault();
Assert.NotNull(firstRowCheckbox);
await cut.InvokeAsync(() => firstRowCheckbox!.Change(false));
cut.WaitForState(() => capturedKeys != null && capturedKeys.Count == 0);
// Assert
Assert.NotNull(capturedKeys);
Assert.Empty(capturedKeys!);
}
[Fact]
public async Task ActiveFiltersReflectsSetFilterAsync()
{
// Arrange
var items = CreateTestItems(3);
var cut = Render<SbDataGrid<TestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.KeySelector, (Func<TestItem, string>)(item => item.Id.ToString()))
.Add(x => x.ShowPagination, false)
.Add(x => x.ShowColumnFilters, false)
.AddChildContent(ColumnsTemplate));
// Act
await cut.InvokeAsync(async () =>
{
await cut.Instance.SetFilterAsync("Name", SbFilterOperator.Contains, "Item");
});
cut.WaitForState(() => cut.Instance.ActiveFilters.Count == 1);
// Assert
var filters = cut.Instance.ActiveFilters;
Assert.Single(filters);
Assert.Equal("Name", filters[0].Field);
Assert.Equal(SbFilterOperator.Contains, filters[0].Operator);
Assert.Equal("Item", filters[0].Value);
}
#endregion
private class TestItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Value { get; set; }
}
}
+281
View File
@@ -0,0 +1,281 @@
using Microsoft.AspNetCore.Components;
using Bunit;
using SufiChain.SufiBlazor.Components.Data;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Data;
public class SbStatCardTests : BunitContext
{
private IRenderedComponent<SbStatCard> RenderStatCard(
Action<ComponentParameterCollectionBuilder<SbStatCard>>? configure = null)
{
return Render<SbStatCard>(p =>
{
p.Add(x => x.Label, "Total Revenue")
.Add(x => x.Value, "1,234");
configure?.Invoke(p);
});
}
[Fact]
public void RendersCardStructure()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
var card = cut.Find(".sb-stat-card");
Assert.NotNull(card);
Assert.NotNull(cut.Find(".sb-stat-card__content"));
Assert.NotNull(cut.Find(".sb-stat-card__label"));
Assert.NotNull(cut.Find(".sb-stat-card__value"));
}
[Fact]
public void RendersLabelAndValue()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Contains("Total Revenue", cut.Markup);
Assert.Contains("1,234", cut.Markup);
}
[Fact]
public void RendersPrefixWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Prefix, "$"));
// Assert
var prefix = cut.Find(".sb-stat-card__prefix");
Assert.NotNull(prefix);
Assert.Contains("$", prefix.TextContent);
}
[Fact]
public void DoesNotRenderPrefixWhenEmpty()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__prefix"));
}
[Fact]
public void RendersSuffixWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Suffix, "K"));
// Assert
var suffix = cut.Find(".sb-stat-card__suffix");
Assert.NotNull(suffix);
Assert.Contains("K", suffix.TextContent);
}
[Fact]
public void DoesNotRenderSuffixWhenEmpty()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__suffix"));
}
[Fact]
public void RendersIconWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p
.Add(x => x.Icon, i => i.AddMarkupContent(0, "<span class=\"custom-icon\">📊</span>")));
// Assert
var iconContainer = cut.Find(".sb-stat-card__icon");
Assert.NotNull(iconContainer);
var customIcon = cut.Find(".custom-icon");
Assert.NotNull(customIcon);
}
[Fact]
public void DoesNotRenderIconWhenNull()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__icon"));
}
[Fact]
public void RendersPositiveTrendWithArrowUp()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Trend, 12.5));
// Assert
var trend = cut.Find(".sb-stat-card__trend");
Assert.NotNull(trend);
Assert.Contains("sb-stat-card__trend--positive", trend.ClassList);
Assert.Contains("12.5%", cut.Markup);
Assert.NotNull(cut.Find(".sb-stat-card__trend-icon"));
}
[Fact]
public void RendersNegativeTrendWithArrowDown()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Trend, -8.3));
// Assert
var trend = cut.Find(".sb-stat-card__trend");
Assert.NotNull(trend);
Assert.Contains("sb-stat-card__trend--negative", trend.ClassList);
Assert.Contains("8.3%", cut.Markup);
}
[Fact]
public void RendersZeroTrendAsPositive()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Trend, 0.0));
// Assert
var trend = cut.Find(".sb-stat-card__trend");
Assert.NotNull(trend);
Assert.Contains("sb-stat-card__trend--positive", trend.ClassList);
Assert.Contains("0%", cut.Markup);
}
[Fact]
public void DoesNotRenderTrendWhenTrendNull()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__trend"));
}
[Fact]
public void DoesNotRenderTrendWhenShowTrendFalse()
{
// Arrange & Act
var cut = RenderStatCard(p => p
.Add(x => x.Trend, 10.0)
.Add(x => x.ShowTrend, false));
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__trend"));
}
[Fact]
public void RendersTrendLabelWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p
.Add(x => x.Trend, 5.0)
.Add(x => x.TrendLabel, "vs last month"));
// Assert
var trendLabel = cut.Find(".sb-stat-card__trend-label");
Assert.NotNull(trendLabel);
Assert.Contains("vs last month", trendLabel.TextContent);
}
[Fact]
public void DoesNotRenderTrendLabelWhenEmpty()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Trend, 5.0));
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__trend-label"));
}
[Fact]
public void RendersDescriptionWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Description, "Year over year growth"));
// Assert
var description = cut.Find(".sb-stat-card__description");
Assert.NotNull(description);
Assert.Contains("Year over year growth", description.TextContent);
}
[Fact]
public void DoesNotRenderDescriptionWhenEmpty()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__description"));
}
[Fact]
public void RendersActionsWhenProvided()
{
// Arrange & Act
var cut = RenderStatCard(p => p
.Add(x => x.Actions, a => a.AddMarkupContent(0, "<button class=\"action-btn\">View</button>")));
// Assert
var actionsContainer = cut.Find(".sb-stat-card__actions");
Assert.NotNull(actionsContainer);
var actionBtn = cut.Find(".action-btn");
Assert.NotNull(actionBtn);
Assert.Contains("View", actionBtn.TextContent);
}
[Fact]
public void DoesNotRenderActionsWhenNull()
{
// Arrange & Act
var cut = RenderStatCard();
// Assert
Assert.Empty(cut.FindAll(".sb-stat-card__actions"));
}
[Fact]
public void AppliesCustomClass()
{
// Arrange & Act
var cut = RenderStatCard(p => p.Add(x => x.Class, "my-stat-card"));
// Assert
var card = cut.Find(".sb-stat-card");
Assert.Contains("my-stat-card", card.ClassList);
}
[Fact]
public void RendersCompleteCardWithAllOptionalParts()
{
// Arrange & Act
var cut = Render<SbStatCard>(p => p
.Add(x => x.Label, "Users")
.Add(x => x.Value, "2,500")
.Add(x => x.Suffix, " active")
.Add(x => x.Icon, i => i.AddMarkupContent(0, "<span>👥</span>"))
.Add(x => x.Trend, 15.2)
.Add(x => x.TrendLabel, "vs last week")
.Add(x => x.Description, "Monthly active users")
.Add(x => x.Actions, a => a.AddMarkupContent(0, "<a href=\"#\">Details</a>")));
// Assert
Assert.Contains("Users", cut.Markup);
Assert.Contains("2,500", cut.Markup);
Assert.Contains("active", cut.Markup);
Assert.Contains("15.2%", cut.Markup);
Assert.Contains("vs last week", cut.Markup);
Assert.Contains("Monthly active users", cut.Markup);
Assert.Contains("Details", cut.Markup);
}
}
+280
View File
@@ -0,0 +1,280 @@
using Microsoft.AspNetCore.Components;
using Bunit;
using SufiChain.SufiBlazor.Components.Data;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Data;
public class SbTableTests : BunitContext
{
private static List<TableTestItem> CreateTestItems(int count)
{
return Enumerable.Range(1, count)
.Select(i => new TableTestItem { Id = i, Name = $"Item {i}", Value = i * 10 })
.ToList();
}
private static RenderFragment<TableTestItem> RowTemplate => item => builder =>
{
builder.OpenElement(0, "td");
builder.AddContent(1, item.Name);
builder.CloseElement();
builder.OpenElement(2, "td");
builder.AddContent(3, item.Value.ToString());
builder.CloseElement();
};
private IRenderedComponent<SbTable<TableTestItem>> RenderTable(
List<TableTestItem>? items = null,
Action<ComponentParameterCollectionBuilder<SbTable<TableTestItem>>>? configure = null)
{
items ??= CreateTestItems(3);
return Render<SbTable<TableTestItem>>(p =>
{
p.Add(x => x.Items, items)
.Add(x => x.RowTemplate, RowTemplate);
configure?.Invoke(p);
});
}
[Fact]
public void RendersTableStructure()
{
// Arrange & Act
var cut = RenderTable();
// Assert
var container = cut.Find(".sb-table-container");
Assert.NotNull(container);
var table = cut.Find("table.sb-table");
Assert.NotNull(table);
Assert.NotNull(cut.Find("tbody.sb-table__body"));
}
[Fact]
public void RendersDataRows()
{
// Arrange
var items = CreateTestItems(2);
// Act
var cut = RenderTable(items);
// Assert
Assert.Contains("Item 1", cut.Markup);
Assert.Contains("Item 2", cut.Markup);
Assert.Contains("10", cut.Markup);
Assert.Contains("20", cut.Markup);
var rows = cut.FindAll(".sb-table__row");
Assert.Equal(2, rows.Count);
}
[Fact]
public void RendersHeaderWhenHeaderContentProvided()
{
// Arrange & Act
var cut = RenderTable(configure: p => p
.Add(x => x.HeaderContent, h => h.AddMarkupContent(0, "<tr><th>Name</th><th>Value</th></tr>")));
// Assert
var thead = cut.Find("thead.sb-table__head");
Assert.NotNull(thead);
Assert.Contains("Name", cut.Markup);
Assert.Contains("Value", cut.Markup);
}
[Fact]
public void DoesNotRenderHeaderWhenHeaderContentNull()
{
// Arrange & Act
var cut = RenderTable();
// Assert
Assert.Empty(cut.FindAll("thead"));
}
[Fact]
public void RendersFooterWhenFooterContentProvided()
{
// Arrange & Act
var cut = RenderTable(configure: p => p
.Add(x => x.FooterContent, f => f.AddMarkupContent(0, "<tr><td colspan=\"2\">Total: 3 items</td></tr>")));
// Assert
var tfoot = cut.Find("tfoot.sb-table__foot");
Assert.NotNull(tfoot);
Assert.Contains("Total: 3 items", cut.Markup);
}
[Fact]
public void DoesNotRenderFooterWhenFooterContentNull()
{
// Arrange & Act
var cut = RenderTable();
// Assert
Assert.Empty(cut.FindAll("tfoot"));
}
[Fact]
public void RendersEmptyContentWhenNoItems()
{
// Arrange & Act
var cut = Render<SbTable<TableTestItem>>(p => p
.Add(x => x.Items, new List<TableTestItem>())
.Add(x => x.RowTemplate, RowTemplate)
.Add(x => x.EmptyContent, e => e.AddMarkupContent(0, "<span class=\"custom-empty\">No data</span>")));
// Assert
var emptyCell = cut.Find(".sb-table__empty");
Assert.NotNull(emptyCell);
var customEmpty = cut.Find(".custom-empty");
Assert.NotNull(customEmpty);
Assert.Contains("No data", customEmpty.TextContent);
}
[Fact]
public void RendersEmptyContentWhenItemsNull()
{
// Arrange & Act
var cut = Render<SbTable<TableTestItem>>(p => p
.Add(x => x.Items, (IEnumerable<TableTestItem>?)null)
.Add(x => x.RowTemplate, RowTemplate)
.Add(x => x.EmptyContent, e => e.AddMarkupContent(0, "Nothing here")));
// Assert
Assert.Contains("Nothing here", cut.Markup);
}
[Fact]
public void RendersEmptyBodyWhenNoItemsAndNoEmptyContent()
{
// Arrange & Act
var cut = Render<SbTable<TableTestItem>>(p => p
.Add(x => x.Items, new List<TableTestItem>())
.Add(x => x.RowTemplate, RowTemplate));
// Assert - tbody exists but has no data rows
var tbody = cut.Find("tbody.sb-table__body");
Assert.NotNull(tbody);
Assert.Empty(cut.FindAll(".sb-table__row"));
}
[Fact]
public void AppliesStripedClass()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Striped, true));
// Assert
var table = cut.Find("table.sb-table");
Assert.Contains("sb-table--striped", table.ClassList);
}
[Fact]
public void AppliesHoverClass()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Hover, true));
// Assert - Hover defaults to true
var table = cut.Find("table.sb-table");
Assert.Contains("sb-table--hover", table.ClassList);
}
[Fact]
public void DoesNotApplyHoverClassWhenHoverFalse()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Hover, false));
// Assert
var table = cut.Find("table.sb-table");
Assert.DoesNotContain("sb-table--hover", table.ClassList);
}
[Fact]
public void AppliesBorderedClass()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Bordered, true));
// Assert
var table = cut.Find("table.sb-table");
Assert.Contains("sb-table--bordered", table.ClassList);
}
[Fact]
public void AppliesCompactClass()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Compact, true));
// Assert
var table = cut.Find("table.sb-table");
Assert.Contains("sb-table--compact", table.ClassList);
}
[Fact]
public void AppliesCustomClassToContainer()
{
// Arrange & Act
var cut = RenderTable(configure: p => p.Add(x => x.Class, "my-table"));
// Assert
var container = cut.Find(".sb-table-container");
Assert.Contains("my-table", container.ClassList);
}
[Fact]
public async Task InvokesOnRowClickWhenRowClicked()
{
// Arrange
TableTestItem? capturedItem = null;
var items = CreateTestItems(3);
var cut = Render<SbTable<TableTestItem>>(p => p
.Add(x => x.Items, items)
.Add(x => x.RowTemplate, RowTemplate)
.Add(x => x.OnRowClick, EventCallback.Factory.Create<TableTestItem>(this, item => capturedItem = item)));
// Act - click second row (Item 2)
var rows = cut.FindAll(".sb-table__row");
var row2 = rows.FirstOrDefault(r => r.TextContent.Contains("Item 2"));
Assert.NotNull(row2);
await cut.InvokeAsync(() => row2!.Click());
cut.WaitForState(() => capturedItem != null);
// Assert
Assert.NotNull(capturedItem);
Assert.Equal(2, capturedItem!.Id);
Assert.Equal("Item 2", capturedItem.Name);
Assert.Equal(20, capturedItem.Value);
}
[Fact]
public void RendersAllStyleVariantsTogether()
{
// Arrange & Act
var cut = RenderTable(configure: p => p
.Add(x => x.Striped, true)
.Add(x => x.Hover, true)
.Add(x => x.Bordered, true)
.Add(x => x.Compact, true));
// Assert
var table = cut.Find("table.sb-table");
Assert.Contains("sb-table--striped", table.ClassList);
Assert.Contains("sb-table--hover", table.ClassList);
Assert.Contains("sb-table--bordered", table.ClassList);
Assert.Contains("sb-table--compact", table.ClassList);
}
private class TableTestItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Value { get; set; }
}
}