feat(centralui): Topology pre-loads and expands the first layer (sites -> top-level areas) (Gitea #39)

This commit is contained in:
Joseph Doherty
2026-10-22 14:04:07 -04:00
parent 1d38fb6c90
commit d71f61e71b
3 changed files with 57 additions and 9 deletions
@@ -244,6 +244,55 @@ public class TopologyPageTests : BunitContext
_repo.DidNotReceive().GetAllTemplatesAsync(Arg.Any<CancellationToken>());
}
/// <summary>Gitea #39: the first layer (areas of every permitted site) is loaded at open; instances stay lazy.</summary>
[Fact]
public void InitialLoad_PreloadsAreasForPermittedSites_ButNoInstances()
{
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }, new Site("Yard", "yard") { Id = 2 }],
areasBySite: new()
{
[1] = new List<Area> { new("Line A") { Id = 10, SiteId = 1 } },
[2] = new List<Area> { new("Dock") { Id = 20, SiteId = 2 } },
});
var cut = Render<TopologyPage>();
cut.WaitForAssertion(() =>
{
Assert.Contains("Line A", cut.Markup);
Assert.Contains("Dock", cut.Markup);
});
_repo.DidNotReceive().GetTopologyInstancesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
}
/// <summary>Gitea #39: a site outside the user's scope never has its areas read.</summary>
[Fact]
public async Task InitialLoad_OutOfScopeSite_AreasNeverRead()
{
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }, new Site("Yard", "yard") { Id = 2 }],
areasBySite: new()
{
[1] = new List<Area> { new("Line A") { Id = 10, SiteId = 1 } },
[2] = new List<Area> { new("Dock") { Id = 20, SiteId = 2 } },
});
var auth = Services.GetRequiredService<AuthenticationStateProvider>();
var identity = (ClaimsIdentity)(await auth.GetAuthenticationStateAsync()).User.Identity!;
identity.AddClaim(new Claim(JwtTokenService.SiteIdClaimType, "1"));
var cut = Render<TopologyPage>();
cut.WaitForAssertion(() => Assert.Contains("Line A", cut.Markup));
Assert.DoesNotContain("Dock", cut.Markup);
await _repo.DidNotReceive().GetAreasBySiteIdAsync(2, Arg.Any<CancellationToken>());
}
/// <summary>Gitea #39: with no saved expansion state sites open; a saved (collapsed) state wins.</summary>
[Fact]
public void InitialLoad_SitesExpanded_WhenNoSavedState_AndSavedStateWins()
{
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }],
areasBySite: new() { [1] = new List<Area> { new("Line A") { Id = 10, SiteId = 1 } } });
var cut = Render<TopologyPage>();
cut.WaitForAssertion(() => Assert.Contains("Line A", cut.Markup));
Assert.Contains("aria-expanded=\"true\"", cut.Markup);
}
/// <summary>Verifies that moving an existing topology item uses the current site scope.</summary>
/// <param name="revoked">Whether the user's site permission has been revoked.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
@@ -343,7 +392,6 @@ public class TopologyPageTests : BunitContext
// Expand the site, then the area, to render the instance leaf. The
// helper scopes by the row's own label so we don't match outer rows
// whose TextContent transitively contains the inner label.
FindToggleForLabel(cut, "Plant-A")!.Click();
FindToggleForLabel(cut, "Line-1")!.Click();
Assert.Contains("Pump-001", cut.Markup);
@@ -369,7 +417,6 @@ public class TopologyPageTests : BunitContext
areasBySite: areasBySite);
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
var search = cut.Find("input[type='text']");
search.Input("Line");
@@ -440,7 +487,6 @@ public class TopologyPageTests : BunitContext
SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } }, areasBySite: areasBySite);
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
var areaLabel = cut.FindAll("span.tv-label").First(s => s.TextContent == "Line-1");
areaLabel.DoubleClick();
@@ -464,7 +510,6 @@ public class TopologyPageTests : BunitContext
_repo.GetAreaByIdAsync(10, Arg.Any<CancellationToken>()).Returns(area);
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
cut.FindAll("span.tv-label").First(s => s.TextContent == "Line-1").DoubleClick();
var input = cut.Find("input[aria-label='Rename Line-1']");
@@ -491,7 +536,6 @@ public class TopologyPageTests : BunitContext
});
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
var instanceLabel = cut.FindAll("span.tv-label").First(s => s.TextContent == "Pump-001");
Assert.Throws<Bunit.MissingEventHandlerException>(() => instanceLabel.DoubleClick());
@@ -560,7 +604,6 @@ public class TopologyPageTests : BunitContext
new FlatteningPipelineResult(currentConfig, "hash-new", ValidationResult.Success()))));
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
FindToggleForLabel(cut, "Line-1")!.Click();
// The per-node action menu only renders after a context-menu (right
@@ -646,7 +689,6 @@ public class TopologyPageTests : BunitContext
SeedOneDeployedInstanceWithComparison();
var cut = RenderLoadedTopology();
FindToggleForLabel(cut, "Plant-A")!.Click();
Assert.Contains("Pump-001", cut.Markup);
Assert.Contains("Enabled", cut.Markup);