refactor(ui): unify semantic presentation and table hierarchy

This commit is contained in:
Joseph Doherty
2026-11-13 19:06:18 -05:00
parent 2d30323ae8
commit 0c41b613ac
97 changed files with 13829 additions and 167 deletions
@@ -23,6 +23,39 @@ public class DataTablePagerTests : BunitContext
.Add(p => p.RowContent, (RenderFragment<int>)(item => b => b.AddMarkupContent(0, $"<tr><td>{item}</td></tr>"))));
}
/// <summary>True empty results show no contradictory one-to-zero range.</summary>
[Fact]
public void EmptyTable_ShowsZeroItems()
{
var cut = RenderTable(0);
Assert.Equal("0 items", cut.Find(".text-muted.small").TextContent.Trim());
}
/// <summary>Filtering to zero and clearing preserves the correct range and paging state.</summary>
[Fact]
public void FilterToEmpty_ThenClear_RestoresRealCount()
{
var cut = RenderTable(60);
cut.Render(p => p.Add(c => c.ShowSearch, true).Add(c => c.SearchFilter, (item, search) => item.ToString().Contains(search)));
cut.Find("input").Input("no-match");
Assert.Equal("0 items", cut.Find(".text-muted.small").TextContent.Trim());
cut.Find("button[aria-label='Clear search']").Click();
Assert.Contains("1–25 of 60 items", cut.Find(".text-muted.small").TextContent);
}
/// <summary>The reusable table has one named keyboard-accessible overflow region.</summary>
[Fact]
public void Table_HasNamedKeyboardScrollRegion()
{
var cut = RenderTable(4);
var region = cut.Find("[role=region]");
Assert.Equal("Data table", region.GetAttribute("aria-label"));
Assert.Equal("0", region.GetAttribute("tabindex"));
Assert.Single(region.QuerySelectorAll("table"));
cut.Render(p => p.Add(c => c.TableLabel, "Synthetic records"));
Assert.Equal("Synthetic records", cut.Find("[role=region]").GetAttribute("aria-label"));
}
private static int NumberedPageButtons(IRenderedComponent<DataTable<int>> cut)
=> cut.FindAll("ul.pagination li.page-item button")
.Count(b => int.TryParse(b.TextContent.Trim(), out _));
@@ -150,6 +150,26 @@ public class TopologyPageTests : BunitContext
});
}
/// <summary>Every load state keeps one semantic page title.</summary>
[Theory]
[InlineData("loading")]
[InlineData("loaded")]
[InlineData("error")]
public async Task PageTitle_RemainsOneH1AcrossLoadStates(string state)
{
SeedRepos();
var pending = new TaskCompletionSource<IReadOnlyList<Site>>(TaskCreationOptions.RunContinuationsAsynchronously);
if (state == "loading") _siteRepo.GetAllSitesAsync().Returns(pending.Task);
if (state == "error") _siteRepo.GetAllSitesAsync().Returns(Task.FromException<IReadOnlyList<Site>>(new Exception("local failure")));
var cut = Render<TopologyPage>();
try
{
Assert.Single(cut.FindAll("h1"));
Assert.Equal("Topology", cut.Find("h1").TextContent.Trim());
}
finally { await cut.InvokeAsync(() => pending.TrySetResult([])); }
}
private IRenderedComponent<TopologyPage> RenderLoadedTopology()
{
var cut = Render<TopologyPage>();