Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/DashboardStateTests.cs
T
2026-11-13 18:23:12 -05:00

109 lines
6.9 KiB
C#

using Bunit;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
using DashboardPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Dashboard;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages;
/// <summary>Dashboard facts distinguish independent failures, loading and known zero.</summary>
public class DashboardStateTests : BunitContext
{
private readonly ISiteRepository _sites = Substitute.For<ISiteRepository>();
private readonly ITemplateEngineRepository _templates = Substitute.For<ITemplateEngineRepository>();
private readonly IInboundApiKeyAdmin _keys = Substitute.For<IInboundApiKeyAdmin>();
/// <summary>Registers deterministic read-only dependencies and a signed-in fixture.</summary>
public DashboardStateTests()
{
this.AddAuthorization().SetAuthorized("fixture");
_sites.GetAllSitesAsync().Returns(Task.FromResult<IReadOnlyList<Site>>([new("A", "a"), new("B", "b")]));
_sites.GetAllDataConnectionsAsync().Returns(Task.FromResult<IReadOnlyList<DataConnection>>([new("Link", "OpcUa", 1)]));
_templates.GetTemplateStubsAsync().Returns(Task.FromResult<IReadOnlyList<TemplateStub>>([
new(1,"A",null,null,false,null), new(2,"B",null,null,false,null), new(3,"C",null,null,false,null)]));
_keys.ListAsync().Returns(Task.FromResult<IReadOnlyList<InboundApiKeyInfo>>([]));
Services.AddSingleton(_sites); Services.AddSingleton(_templates); Services.AddSingleton(_keys);
}
/// <summary>Each read failure leaves the other facts intact and is never a false zero.</summary>
[Theory]
[InlineData("Sites configured")]
[InlineData("Data connections configured")]
[InlineData("Templates")]
[InlineData("API keys")]
public void CountFailure_DoesNotEraseOtherFactsOrRenderZero(string label)
{
if(label=="Sites configured")_sites.GetAllSitesAsync().Returns(Task.FromException<IReadOnlyList<Site>>(new Exception("local failure")));
if(label=="Data connections configured")_sites.GetAllDataConnectionsAsync().Returns(Task.FromException<IReadOnlyList<DataConnection>>(new Exception("local failure")));
if(label=="Templates")_templates.GetTemplateStubsAsync().Returns(Task.FromException<IReadOnlyList<TemplateStub>>(new Exception("local failure")));
if(label=="API keys")_keys.ListAsync().Returns(Task.FromException<IReadOnlyList<InboundApiKeyInfo>>(new Exception("local failure")));
var cut=Render<DashboardPage>();
var facts=new Dictionary<string,string>{{"Sites configured","2"},{"Data connections configured","1"},{"Templates","3"},{"API keys","0"}};
foreach(var (name,count) in facts)
{
var fact=cut.Find("[role=region][aria-label='"+name+"']");
Assert.Contains(name==label?"Unavailable":count,fact.TextContent);
if(name==label)
{
Assert.Contains("Retry "+name,fact.TextContent);
Assert.DoesNotContain("0",fact.TextContent);
}
}
}
/// <summary>Only a failed API count is retried, and a real zero is displayed after success.</summary>
[Fact]
public void RetryFailure_PreservesSuccessfulFactsAndCanRecoverToZero()
{
_keys.ListAsync().Returns(Task.FromException<IReadOnlyList<InboundApiKeyInfo>>(new Exception("local failure")));
var cut=Render<DashboardPage>();
Assert.Contains("Unavailable",cut.Find("[aria-label='API keys']").TextContent);
_keys.ListAsync().Returns(Task.FromResult<IReadOnlyList<InboundApiKeyInfo>>([]));
cut.Find("[aria-label='API keys'] button").Click();
Assert.Contains("0",cut.Find("[aria-label='API keys']").TextContent);
Assert.DoesNotContain("Unavailable",cut.Markup);
_sites.Received(1).GetAllSitesAsync(); _sites.Received(1).GetAllDataConnectionsAsync(); _templates.Received(1).GetTemplateStubsAsync();
}
/// <summary>Early retries share the initial-read gate and duplicate retries remain single-flight.</summary>
[Fact]
public async Task RetryDuringNextRead_WaitsWithoutOverlapAndCoalescesDuplicateTarget()
{
var connectionRead = new TaskCompletionSource<IReadOnlyList<DataConnection>>(TaskCreationOptions.RunContinuationsAsynchronously);
_sites.GetAllSitesAsync().Returns(Task.FromException<IReadOnlyList<Site>>(new Exception("local failure")),
Task.FromResult<IReadOnlyList<Site>>([new("Recovered", "recovered")]));
_sites.GetAllDataConnectionsAsync().Returns(connectionRead.Task);
var cut = Render<DashboardPage>();
Assert.Contains("Unavailable", cut.Find("[aria-label='Sites configured']").TextContent);
Assert.Contains("Loading", cut.Find("[aria-label='Data connections configured']").TextContent);
// Model two already-dispatched retries without relying on a stale DOM node.
var retry = typeof(DashboardPage).GetMethod("LoadSites", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
Task first = null!, second = null!;
await cut.InvokeAsync(() =>
{
first = (Task)retry.Invoke(cut.Instance, null)!;
second = (Task)retry.Invoke(cut.Instance, null)!;
});
Assert.Equal(1, _sites.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(ISiteRepository.GetAllSitesAsync)));
Assert.False(first.IsCompleted);
await cut.InvokeAsync(() => connectionRead.SetResult([new("Link", "OpcUa", 1)]));
await Task.WhenAll(first, second);
cut.WaitForAssertion(() => Assert.Contains("1", cut.Find("[aria-label='Sites configured']").TextContent));
Assert.Equal(2, _sites.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(ISiteRepository.GetAllSitesAsync)));
Assert.Equal(1, _sites.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(ISiteRepository.GetAllDataConnectionsAsync)));
}
/// <summary>An outstanding read remains loading while already loaded facts are available.</summary>
[Fact]
public async Task DelayedRead_RemainsLoadingUntilKnownZeroArrives()
{
var pending=new TaskCompletionSource<IReadOnlyList<InboundApiKeyInfo>>(TaskCreationOptions.RunContinuationsAsynchronously);
_keys.ListAsync().Returns(pending.Task);
var cut=Render<DashboardPage>();
Assert.Contains("Loading",cut.Find("[aria-label='API keys']").TextContent);
Assert.Contains("2",cut.Find("[aria-label='Sites configured']").TextContent);
await cut.InvokeAsync(()=>pending.SetResult([]));
cut.WaitForAssertion(()=>Assert.Contains("0",cut.Find("[aria-label='API keys']").TextContent));
Assert.DoesNotContain("Loading",cut.Find("[aria-label='API keys']").TextContent);
}
}