dashboard: lazy-load BrowsePage via DashboardBrowseService
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for <see cref="DashboardBrowseService"/> — the in-process facade the
|
||||
/// Blazor BrowsePage uses to walk the Galaxy hierarchy one level at a time. The
|
||||
/// service must surface the projector's <c>child_has_children</c> hint, expose the
|
||||
/// current cache sequence, and translate the projector's
|
||||
/// <see cref="Grpc.Core.RpcException"/> on unknown parent into a friendly error
|
||||
/// rather than propagating it into the Blazor circuit.
|
||||
/// </summary>
|
||||
public sealed class DashboardBrowseServiceTests
|
||||
{
|
||||
/// <summary>Verifies that <see cref="DashboardBrowseService.GetRoots"/> returns root-level
|
||||
/// objects with the <c>HasChildrenHint</c> projector bit set, and reports the cache
|
||||
/// sequence of the entry it projected from.</summary>
|
||||
[Fact]
|
||||
public void GetRoots_ReturnsRootObjects_WithHasChildrenHint()
|
||||
{
|
||||
StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
|
||||
DashboardBrowseService service = new(cache);
|
||||
|
||||
BrowseLevelResult result = service.GetRoots(new BrowseFilterArgs());
|
||||
|
||||
Assert.Single(result.Nodes);
|
||||
Assert.Equal("Plant", result.Nodes[0].Object.TagName);
|
||||
Assert.True(result.Nodes[0].HasChildrenHint);
|
||||
Assert.Equal(11UL, result.CacheSequence);
|
||||
Assert.Null(result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <see cref="DashboardBrowseService.GetChildren"/> returns the
|
||||
/// direct children of the requested parent and that leaf nodes report
|
||||
/// <c>HasChildrenHint == false</c>.</summary>
|
||||
[Fact]
|
||||
public void GetChildren_ByParentGobjectId_ReturnsDirectChildren()
|
||||
{
|
||||
StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
|
||||
DashboardBrowseService service = new(cache);
|
||||
|
||||
BrowseLevelResult result = service.GetChildren(parentGobjectId: 1, new BrowseFilterArgs());
|
||||
|
||||
Assert.Single(result.Nodes);
|
||||
Assert.Equal("Mixer_001", result.Nodes[0].Object.TagName);
|
||||
Assert.False(result.Nodes[0].HasChildrenHint);
|
||||
Assert.Null(result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown parent id does not surface the projector's
|
||||
/// <see cref="Grpc.Core.RpcException"/> — the service catches the NotFound and
|
||||
/// returns an empty <see cref="BrowseLevelResult"/> with the error string set.</summary>
|
||||
[Fact]
|
||||
public void GetChildren_UnknownParent_ReturnsEmptyResultWithErrorFlag()
|
||||
{
|
||||
StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
|
||||
DashboardBrowseService service = new(cache);
|
||||
|
||||
BrowseLevelResult result = service.GetChildren(parentGobjectId: 999, new BrowseFilterArgs());
|
||||
|
||||
Assert.Empty(result.Nodes);
|
||||
Assert.NotNull(result.Error);
|
||||
Assert.False(string.IsNullOrEmpty(result.Error));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that swapping the cache's <c>Current</c> entry (as the refresh loop
|
||||
/// does after a deploy bump) causes subsequent queries to observe the new sequence.</summary>
|
||||
[Fact]
|
||||
public void CacheSequence_AdvancesAfterRefresh_NewQueriesReflectIt()
|
||||
{
|
||||
StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
|
||||
DashboardBrowseService service = new(cache);
|
||||
|
||||
BrowseLevelResult before = service.GetRoots(new BrowseFilterArgs());
|
||||
Assert.Equal(11UL, before.CacheSequence);
|
||||
|
||||
cache.Current = CreateEntry(CreateObjects(), sequence: 12);
|
||||
|
||||
BrowseLevelResult after = service.GetRoots(new BrowseFilterArgs());
|
||||
Assert.Equal(12UL, after.CacheSequence);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GalaxyObject> CreateObjects()
|
||||
{
|
||||
// Fixture: an Area "Plant" (id 1, parent 0, IsArea=true) containing one
|
||||
// Instance "Mixer_001" (id 2, parent 1). Both with no attributes — the
|
||||
// service is exercised through the projector, which only needs id /
|
||||
// parent / IsArea / display name to build the level slice.
|
||||
return
|
||||
[
|
||||
new GalaxyObject
|
||||
{
|
||||
GobjectId = 1,
|
||||
ParentGobjectId = 0,
|
||||
TagName = "Plant",
|
||||
BrowseName = "Plant",
|
||||
IsArea = true,
|
||||
},
|
||||
new GalaxyObject
|
||||
{
|
||||
GobjectId = 2,
|
||||
ParentGobjectId = 1,
|
||||
TagName = "Mixer_001",
|
||||
BrowseName = "Mixer_001",
|
||||
IsArea = false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private static GalaxyHierarchyCacheEntry CreateEntry(IReadOnlyList<GalaxyObject> objects, long sequence)
|
||||
{
|
||||
return GalaxyHierarchyCacheEntry.Empty with
|
||||
{
|
||||
Status = GalaxyCacheStatus.Healthy,
|
||||
Sequence = sequence,
|
||||
LastSuccessAt = DateTimeOffset.UtcNow,
|
||||
Objects = objects,
|
||||
Index = GalaxyHierarchyIndex.Build(objects),
|
||||
DashboardSummary = DashboardGalaxySummary.Unknown with
|
||||
{
|
||||
Status = DashboardGalaxyStatus.Healthy,
|
||||
ObjectCount = objects.Count,
|
||||
},
|
||||
ObjectCount = objects.Count,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry initial) : IGalaxyHierarchyCache
|
||||
{
|
||||
/// <summary>Mutable so a single test can swap the entry mid-flight.</summary>
|
||||
public GalaxyHierarchyCacheEntry Current { get; set; } = initial;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user