docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
@@ -14,6 +14,7 @@ public class DebugTreeBuilderTests
|
||||
private static AttributeValueChanged Attr(string name, object? value = null, string quality = "Good")
|
||||
=> new("Inst", name, name, value, quality, DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>Verifies that a root-level attribute becomes a single leaf.</summary>
|
||||
[Fact]
|
||||
public void RootLevelAttribute_BecomesSingleLeaf()
|
||||
{
|
||||
@@ -27,6 +28,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Speed", node.Attribute!.AttributeName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies two sibling leaves share single branch sorted by segment.</summary>
|
||||
[Fact]
|
||||
public void TwoSiblingLeaves_ShareSingleBranch_SortedBySegment()
|
||||
{
|
||||
@@ -47,6 +49,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Temp", branch.Children[1].Segment);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deep path nests branches by accumulated prefix.</summary>
|
||||
[Fact]
|
||||
public void DeepPath_NestsBranchesByAccumulatedPrefix()
|
||||
{
|
||||
@@ -70,6 +73,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.NotNull(pump.Attribute);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that branches are deduplicated across deep paths sharing a prefix.</summary>
|
||||
[Fact]
|
||||
public void BranchDedupe_SharedPrefixAcrossDeepPaths()
|
||||
{
|
||||
@@ -95,6 +99,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor1.Compressor.Valve", compressor.Children[1].Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a bad-quality descendant marks all ancestor branches as bad.</summary>
|
||||
[Fact]
|
||||
public void RollUp_BadQualityDescendant_MarksAllAncestorBranches()
|
||||
{
|
||||
@@ -113,6 +118,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.True(compressor.HasBadQuality);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a branch whose leaves are all good is not marked as bad quality.</summary>
|
||||
[Fact]
|
||||
public void RollUp_AllGood_LeavesBranchHasBadQualityFalse()
|
||||
{
|
||||
@@ -123,6 +129,8 @@ public class DebugTreeBuilderTests
|
||||
Assert.False(motor.HasBadQuality);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that any non-good quality counts as bad when rolling up.</summary>
|
||||
/// <param name="quality">The test input.</param>
|
||||
[Theory]
|
||||
[InlineData("Uncertain")]
|
||||
[InlineData("Bad")]
|
||||
@@ -134,6 +142,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.True(Assert.Single(tree).HasBadQuality);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering keeps only the matching leaves and their ancestors.</summary>
|
||||
[Fact]
|
||||
public void Filter_KeepsOnlyMatchingLeaves_AndTheirAncestors()
|
||||
{
|
||||
@@ -153,6 +162,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor1.Temp", leaf.Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering is case insensitive.</summary>
|
||||
[Fact]
|
||||
public void Filter_IsCaseInsensitive()
|
||||
{
|
||||
@@ -163,6 +173,8 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor1.Temp", Assert.Single(motor.Children).Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty or whitespace filter returns the full forest.</summary>
|
||||
/// <param name="filter">The test input.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -177,6 +189,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor2", tree[1].Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a filter with no matches returns an empty forest.</summary>
|
||||
[Fact]
|
||||
public void Filter_NoMatches_ReturnsEmptyForest()
|
||||
{
|
||||
@@ -186,6 +199,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Empty(tree);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the top-level forest is sorted by segment.</summary>
|
||||
[Fact]
|
||||
public void TopLevelForest_SortedBySegment()
|
||||
{
|
||||
@@ -198,6 +212,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Zeta", tree[2].Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty input returns an empty forest.</summary>
|
||||
[Fact]
|
||||
public void EmptyInput_ReturnsEmptyForest()
|
||||
{
|
||||
@@ -205,6 +220,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Empty(tree);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bad quality four levels deep rolls up to the root.</summary>
|
||||
[Fact]
|
||||
public void RollUp_FourLevelDeepBadQuality_ReachesRoot()
|
||||
{
|
||||
@@ -221,6 +237,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.True(d.HasBadQuality);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a filter matching a deep leaf retains all its ancestor branches.</summary>
|
||||
[Fact]
|
||||
public void Filter_DeepLeafMatch_RetainsAllAncestorBranches()
|
||||
{
|
||||
@@ -235,7 +252,7 @@ public class DebugTreeBuilderTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// DV-4: BuildAlarmTree — computed leaves, native condition groupings,
|
||||
// BuildAlarmTree — computed leaves, native condition groupings,
|
||||
// configured-placeholder binding nodes, roll-up, filter.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -261,6 +278,7 @@ public class DebugTreeBuilderTests
|
||||
IsConfiguredPlaceholder = placeholder,
|
||||
};
|
||||
|
||||
/// <summary>Verifies that a computed alarm with a path-qualified name becomes a leaf under a branch.</summary>
|
||||
[Fact]
|
||||
public void Computed_PathQualifiedName_BecomesLeafUnderBranch()
|
||||
{
|
||||
@@ -282,6 +300,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor1.HighTemp", leaf.Alarm!.AlarmName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a computed alarm with a root-level name becomes a single leaf.</summary>
|
||||
[Fact]
|
||||
public void Computed_RootLevelName_BecomesSingleLeaf()
|
||||
{
|
||||
@@ -294,6 +313,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.NotNull(node.Alarm);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native condition nests under its binding node with the source reference segment.</summary>
|
||||
[Fact]
|
||||
public void Native_Condition_NestsUnderBindingNode_WithSourceReferenceSegment()
|
||||
{
|
||||
@@ -318,6 +338,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1.Level.HiHi", condition.Alarm!.SourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two native conditions share the same binding node.</summary>
|
||||
[Fact]
|
||||
public void Native_TwoConditions_ShareSameBindingNode()
|
||||
{
|
||||
@@ -340,6 +361,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1.Levels::Tank1.Level.LoLo", binding.Children[1].Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native placeholder yields a binding node without children.</summary>
|
||||
[Fact]
|
||||
public void Native_Placeholder_YieldsChildlessBindingNode()
|
||||
{
|
||||
@@ -354,10 +376,11 @@ public class DebugTreeBuilderTests
|
||||
Assert.Empty(binding.Children);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that real conditions win over a placeholder under the same native binding.</summary>
|
||||
[Fact]
|
||||
public void Native_PlaceholderAndRealConditions_RealConditionsWin()
|
||||
{
|
||||
// DV-2 shouldn't emit both, but be safe: placeholder adds nothing.
|
||||
// Shouldn't emit both, but be safe: placeholder adds nothing.
|
||||
var tree = DebugTreeBuilder.BuildAlarmTree(
|
||||
new[]
|
||||
{
|
||||
@@ -373,6 +396,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", condition.Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an active descendant sets the worst state to active and is counted when rolling up.</summary>
|
||||
[Fact]
|
||||
public void RollUp_ActiveDescendant_SetsWorstStateActiveAndCounts()
|
||||
{
|
||||
@@ -394,6 +418,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal(1, binding.ActiveCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when every alarm is normal the worst state is normal with a zero active count.</summary>
|
||||
[Fact]
|
||||
public void RollUp_AllNormal_WorstStateNormalAndZeroCount()
|
||||
{
|
||||
@@ -405,6 +430,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal(0, tank.ActiveCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a placeholder never counts as active when rolling up.</summary>
|
||||
[Fact]
|
||||
public void RollUp_Placeholder_NeverCountsAsActive()
|
||||
{
|
||||
@@ -423,6 +449,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal(0, binding.ActiveCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering on the source reference keeps the condition under its binding and ancestors.</summary>
|
||||
[Fact]
|
||||
public void Filter_OnSourceReference_KeepsConditionUnderBindingAndAncestors()
|
||||
{
|
||||
@@ -443,6 +470,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", condition.Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering on a binding canonical name keeps the binding node.</summary>
|
||||
[Fact]
|
||||
public void Filter_OnBindingCanonicalName_KeepsBindingNode()
|
||||
{
|
||||
@@ -463,6 +491,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Single(binding.Children);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering on a binding name that matches a placeholder keeps the binding node.</summary>
|
||||
[Fact]
|
||||
public void Filter_OnBindingName_MatchingPlaceholder_KeepsBindingNode()
|
||||
{
|
||||
@@ -477,6 +506,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.False(binding.HasChildren);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that filtering on a computed alarm name is case insensitive.</summary>
|
||||
[Fact]
|
||||
public void Filter_OnComputedAlarmName_IsCaseInsensitive()
|
||||
{
|
||||
@@ -488,6 +518,8 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Motor1.HighTemp", Assert.Single(motor.Children).Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty or whitespace filter returns the full alarm forest.</summary>
|
||||
/// <param name="filter">The test input.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -503,6 +535,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1", tree[1].Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a filter with no matches returns an empty alarm forest.</summary>
|
||||
[Fact]
|
||||
public void Filter_NoMatches_ReturnsEmptyAlarmForest()
|
||||
{
|
||||
@@ -512,6 +545,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Empty(tree);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that mixed computed and native alarms produce both subtrees.</summary>
|
||||
[Fact]
|
||||
public void MixedComputedAndNative_ProduceBothSubtrees()
|
||||
{
|
||||
@@ -536,6 +570,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Equal("Tank1.Levels::Tank1.Level.HiHi", Assert.Single(binding.Children).Key);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty alarm input returns an empty forest.</summary>
|
||||
[Fact]
|
||||
public void EmptyAlarmInput_ReturnsEmptyForest()
|
||||
{
|
||||
@@ -543,6 +578,7 @@ public class DebugTreeBuilderTests
|
||||
Assert.Empty(tree);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that computed and native alarms sharing a prefix keep distinct binding and leaf nodes.</summary>
|
||||
[Fact]
|
||||
public void Native_ComputedAndNativeSharePrefix_DistinctBindingAndLeaf()
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploymen
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// DV-5: the Debug View renders a tabbed (Attributes / Alarms) layout, each tab
|
||||
/// The Debug View renders a tabbed (Attributes / Alarms) layout, each tab
|
||||
/// hosting a composition <c>TreeView</c>. Computed alarms and attributes nest as
|
||||
/// leaves under their composition-member branches; native conditions nest under
|
||||
/// their source-binding node. Branch nodes surface a roll-up (active-count /
|
||||
@@ -62,7 +62,10 @@ public class DebugViewAlarmTableTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the fixed authentication state to return.</summary>
|
||||
/// <param name="state">The authentication state returned to every caller.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync() => Task.FromResult(_state);
|
||||
}
|
||||
|
||||
@@ -98,6 +101,7 @@ public class DebugViewAlarmTableTests : BunitContext
|
||||
cut.Render();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that both tabs are present, the Attributes tab is the default, and it shows the attribute tree.</summary>
|
||||
[Fact]
|
||||
public void BothTabsPresent_AttributesTabIsDefault_AndShowsAttributeTree()
|
||||
{
|
||||
@@ -132,6 +136,7 @@ public class DebugViewAlarmTableTests : BunitContext
|
||||
Assert.NotNull(motor1.QuerySelector("ul[role='group'] li[role='treeitem']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Alarms tab groups a computed alarm under its branch with an active roll-up badge.</summary>
|
||||
[Fact]
|
||||
public void AlarmsTab_GroupsComputedAlarmUnderBranch_WithActiveRollUpBadge()
|
||||
{
|
||||
@@ -168,6 +173,7 @@ public class DebugViewAlarmTableTests : BunitContext
|
||||
Assert.NotNull(highTemp.QuerySelector(".badge.bg-danger")); // Active state badge
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Alarms tab renders a native condition with enriched badges under its binding node.</summary>
|
||||
[Fact]
|
||||
public void AlarmsTab_RendersNativeCondition_WithEnrichedBadgesUnderBindingNode()
|
||||
{
|
||||
|
||||
+18
-1
@@ -18,7 +18,7 @@ using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploymen
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for arch-review 2026-08-19 P6-6. <c>Connect()</c> awaits
|
||||
/// Regression tests for a dispose race in the debug view. <c>Connect()</c> awaits
|
||||
/// <c>StartStreamAsync</c> — which can take up to 30s — and then publishes the result into
|
||||
/// <c>_session</c>. If the circuit was torn down inside that window, <c>Dispose()</c> had
|
||||
/// already run, found <c>_session</c> still null and stopped nothing, so the just-created
|
||||
@@ -40,12 +40,17 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
private readonly TaskCompletionSource _entered = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly List<string> _stopped = new();
|
||||
|
||||
/// <summary>Creates a debug stream service whose stream start is held until released.</summary>
|
||||
/// <param name="comms">The communication service.</param>
|
||||
/// <param name="provider">The service provider.</param>
|
||||
/// <param name="factory">The gRPC client factory.</param>
|
||||
public GatedDebugStreamService(
|
||||
CommunicationService comms, IServiceProvider provider, SiteStreamGrpcClientFactory factory)
|
||||
: base(comms, provider, factory, NullLogger<DebugStreamService>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>The id of the session this service hands out.</summary>
|
||||
public string SessionId => "session-under-test";
|
||||
|
||||
/// <summary>Completes once <see cref="StartStreamAsync"/> has been entered.</summary>
|
||||
@@ -57,8 +62,10 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
get { lock (_stopped) { return _stopped.ToList(); } }
|
||||
}
|
||||
|
||||
/// <summary>Releases the held stream start so it can complete.</summary>
|
||||
public void ReleaseStart() => _gate.TrySetResult();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<DebugStreamSession> StartStreamAsync(
|
||||
int instanceId, Action<object> onEvent, Action onTerminated, CancellationToken ct = default)
|
||||
{
|
||||
@@ -73,6 +80,7 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void StopStream(string sessionId)
|
||||
{
|
||||
lock (_stopped) { _stopped.Add(sessionId); }
|
||||
@@ -82,7 +90,10 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Creates a provider that always returns the given authentication state.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -141,6 +152,8 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
return task!;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing during a pending connect stops the just-created session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Connect_DisposedMidStart_StopsTheJustCreatedSession()
|
||||
{
|
||||
@@ -162,6 +175,8 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
Assert.Null(GetField(cut.Instance, "_session"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connecting and then disposing stops the session exactly once.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Connect_ThenDispose_StopsTheSessionExactlyOnce()
|
||||
{
|
||||
@@ -186,6 +201,8 @@ public class DebugViewConnectDisposeRaceTests : BunitContext
|
||||
Assert.Equal(new[] { stream.SessionId }, stream.Stopped);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disconnecting after a connect stops the session once and clears the session slot.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Disconnect_AfterConnect_StopsTheSessionOnceAndClearsTheSlot()
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploymen
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CentralUI-009. The <c>DebugView</c> stream callbacks
|
||||
/// Regression tests for the debug view disposal guard. The <c>DebugView</c> stream callbacks
|
||||
/// (<c>onEvent</c>/<c>onTerminated</c>) run on an Akka/gRPC thread and capture
|
||||
/// <c>this</c> and <c>_toast</c>. If the user navigates away, an in-flight
|
||||
/// callback could still call <c>_toast.ShowError(...)</c> /
|
||||
@@ -69,11 +69,15 @@ public class DebugViewDisposalTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Creates a provider that always returns the given authentication state.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the debug view exposes a boolean disposal guard field.</summary>
|
||||
[Fact]
|
||||
public void DebugView_HasDisposalGuardField()
|
||||
{
|
||||
@@ -86,6 +90,7 @@ public class DebugViewDisposalTests : BunitContext
|
||||
Assert.Equal(typeof(bool), field!.FieldType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the debug view sets the disposal guard and that disposing twice does not throw.</summary>
|
||||
[Fact]
|
||||
public void DebugView_Dispose_SetsDisposedFlag_AndIsIdempotent()
|
||||
{
|
||||
|
||||
@@ -81,7 +81,10 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with a fixed authentication state.</summary>
|
||||
/// <param name="state">The state.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -101,6 +104,7 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
.First(r => r.TextContent.Trim() == text)
|
||||
.Click();
|
||||
|
||||
/// <summary>Verifies that the instance selector is the picker component rather than a flat select.</summary>
|
||||
[Fact]
|
||||
public void InstanceSelector_IsThePicker_NotAFlatSelect()
|
||||
{
|
||||
@@ -111,6 +115,7 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("select[data-test='debug-instance-select']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that opening the picker shows only the root area.</summary>
|
||||
[Fact]
|
||||
public void OpeningThePicker_ShowsOnlyTheRootArea()
|
||||
{
|
||||
@@ -122,6 +127,7 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
Assert.False(rows[0].HasAttribute("data-instance-id"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that drilling down reveals only the enabled instance.</summary>
|
||||
[Fact]
|
||||
public void DrillingDown_RevealsTheEnabledInstanceOnly()
|
||||
{
|
||||
@@ -139,6 +145,7 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
r => r.TextContent.Contains("Off01"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that picking an instance fills the field and enables the Connect button.</summary>
|
||||
[Fact]
|
||||
public void PickingAnInstance_FillsTheField_AndEnablesConnect()
|
||||
{
|
||||
@@ -211,6 +218,7 @@ public class DebugViewInstancePickerTests : BunitContext
|
||||
JSInterop.VerifyInvoke("localStorage.removeItem");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the picker is disabled while the view is connected.</summary>
|
||||
[Fact]
|
||||
public void WhileConnected_ThePickerIsDisabled()
|
||||
{
|
||||
|
||||
+8
-1
@@ -18,7 +18,7 @@ using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploymen
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the Debug View render coalescing (arch-review WP2.4). Every streamed
|
||||
/// Regression tests for the Debug View render coalescing. Every streamed
|
||||
/// event used to marshal onto the circuit dispatcher and call <c>StateHasChanged</c> on its
|
||||
/// own, so a chatty instance drove one full render — and two full composition-tree rebuilds —
|
||||
/// per value change. Events are now buffered and applied once per ~250 ms window: a burst
|
||||
@@ -60,7 +60,10 @@ public class DebugViewRenderCoalescingTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the authentication state to return.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -78,6 +81,7 @@ public class DebugViewRenderCoalescingTests : BunitContext
|
||||
new AttributeValueChanged("Inst-1", name, name, value, "Good", DateTimeOffset.UtcNow),
|
||||
});
|
||||
|
||||
/// <summary>Verifies that a burst of events triggers a single render while every event is still applied.</summary>
|
||||
[Fact]
|
||||
public void BurstOfEvents_RendersOnce_ButAppliesEveryEvent()
|
||||
{
|
||||
@@ -99,6 +103,7 @@ public class DebugViewRenderCoalescingTests : BunitContext
|
||||
Assert.InRange(renders, 1, 5);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated updates to one tag collapse to the latest value.</summary>
|
||||
[Fact]
|
||||
public void RepeatedUpdatesToOneTag_CollapseToTheLatestValue()
|
||||
{
|
||||
@@ -115,6 +120,7 @@ public class DebugViewRenderCoalescingTests : BunitContext
|
||||
Assert.Equal(49, applied.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an event arriving after a flush still arms a new coalescing window.</summary>
|
||||
[Fact]
|
||||
public void EventArrivingAfterAFlush_StillArmsANewWindow()
|
||||
{
|
||||
@@ -130,6 +136,7 @@ public class DebugViewRenderCoalescingTests : BunitContext
|
||||
cut.WaitForState(() => dict.Count == 2, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that events arriving after disposal are dropped without throwing.</summary>
|
||||
[Fact]
|
||||
public void EventsAfterDispose_AreDroppedWithoutThrowing()
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploymen
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CentralUI-021. The <c>DebugView</c> stream callback runs
|
||||
/// Regression tests for the DebugView stream race. The <c>DebugView</c> stream callback runs
|
||||
/// on an Akka/gRPC thread; it used to call <c>UpsertWithCap</c> directly on that
|
||||
/// thread, mutating the <c>_attributeValues</c>/<c>_alarmStates</c>
|
||||
/// <see cref="Dictionary{TKey,TValue}"/> while the render thread enumerated the
|
||||
@@ -67,7 +67,10 @@ public class DebugViewStreamRaceTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes a new instance of the <c>StubAuthStateProvider</c> class.</summary>
|
||||
/// <param name="state">The authentication state the provider returns.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -83,6 +86,7 @@ public class DebugViewStreamRaceTests : BunitContext
|
||||
typeof(DebugViewPage).GetProperty("FilteredAttributeValues",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(c)!;
|
||||
|
||||
/// <summary>Verifies that a stream event is applied once the dispatcher runs its queued work.</summary>
|
||||
[Fact]
|
||||
public void HandleStreamEvent_AppliesUpdate_OnceDispatcherRuns()
|
||||
{
|
||||
@@ -98,10 +102,12 @@ public class DebugViewStreamRaceTests : BunitContext
|
||||
Assert.True(dict.Contains("Pump.Speed"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stream events raised from background threads do not fault concurrent dispatcher reads.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleStreamEvent_OffThreadEvents_DoNotFaultDispatcherReads()
|
||||
{
|
||||
// CentralUI-021 reproduction. Writers fire stream events from background
|
||||
// Stream race reproduction. Writers fire stream events from background
|
||||
// threads (the Akka/gRPC callback threads). The reader enumerates
|
||||
// FilteredAttributeValues *through the renderer's dispatcher* — exactly
|
||||
// as the real render thread does. Pre-fix the writers mutated the
|
||||
|
||||
+9
-2
@@ -15,7 +15,7 @@ using DeploymentsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploym
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CentralUI-006. Component-CentralUI "Real-Time Updates"
|
||||
/// Regression tests for deployment status push updates. Component-CentralUI "Real-Time Updates"
|
||||
/// states deployment status transitions push to the UI immediately via SignalR
|
||||
/// with no polling. The page previously ran a 10-second <c>Timer</c> that
|
||||
/// reloaded every deployment record + instance map per tick. The fix removes
|
||||
@@ -61,11 +61,15 @@ public class DeploymentsPushUpdateTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the authentication state it returns.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Deployments page does not poll and has no refresh timer.</summary>
|
||||
[Fact]
|
||||
public void Deployments_DoesNotPoll_HasNoRefreshTimer()
|
||||
{
|
||||
@@ -76,6 +80,7 @@ public class DeploymentsPushUpdateTests : BunitContext
|
||||
Assert.Null(timerField);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deployment status change notification triggers a reload of the page.</summary>
|
||||
[Fact]
|
||||
public void Deployments_StatusChange_TriggersReload()
|
||||
{
|
||||
@@ -95,6 +100,7 @@ public class DeploymentsPushUpdateTests : BunitContext
|
||||
Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the page unsubscribes it from the status notifier.</summary>
|
||||
[Fact]
|
||||
public void Deployments_Dispose_UnsubscribesFromNotifier()
|
||||
{
|
||||
@@ -225,7 +231,7 @@ public class DeploymentsPushUpdateTests : BunitContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for CentralUI-022. The notifier is a process singleton:
|
||||
/// Regression test for the dispose race. The notifier is a process singleton:
|
||||
/// it can read its subscriber list and begin invoking
|
||||
/// <c>OnDeploymentStatusChanged</c> on the DeploymentManager thread an
|
||||
/// instant before the component is disposed. The handler must no-op against
|
||||
@@ -242,6 +248,7 @@ public class DeploymentsPushUpdateTests : BunitContext
|
||||
Assert.Equal(typeof(bool), field!.FieldType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a status change delivered after disposal neither throws nor reloads the page.</summary>
|
||||
[Fact]
|
||||
public void Deployments_StatusChangeAfterDispose_DoesNotThrowOrReload()
|
||||
{
|
||||
|
||||
+7
-1
@@ -14,7 +14,7 @@ using DeploymentsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploym
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the Deployment Status push coalescing (arch-review WP2.4). The
|
||||
/// Regression tests for the Deployment Status push coalescing. The
|
||||
/// notifier fires per status write, so a multi-instance deploy produced a stampede of
|
||||
/// reloads per circuit. The reload is leading-edge debounced: the first push after an
|
||||
/// idle gap is still immediate, and a burst behind it collapses into one trailing reload.
|
||||
@@ -57,7 +57,10 @@ public class DeploymentsReloadDebounceTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes a new instance of the <see cref="StubAuthStateProvider"/> class with a fixed authentication state.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -67,6 +70,7 @@ public class DeploymentsReloadDebounceTests : BunitContext
|
||||
_deployRepo.Received().QueryDeploymentListPageAsync(
|
||||
Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>()));
|
||||
|
||||
/// <summary>Verifies that a burst of status writes collapses into far fewer page reloads.</summary>
|
||||
[Fact]
|
||||
public void BurstOfStatusWrites_CollapsesIntoFarFewerReloads()
|
||||
{
|
||||
@@ -93,6 +97,7 @@ public class DeploymentsReloadDebounceTests : BunitContext
|
||||
Assert.InRange(reloads, 1, 4);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the first push after an idle period reloads the page immediately.</summary>
|
||||
[Fact]
|
||||
public void FirstPushAfterIdle_ReloadsImmediately()
|
||||
{
|
||||
@@ -112,6 +117,7 @@ public class DeploymentsReloadDebounceTests : BunitContext
|
||||
TimeSpan.FromMilliseconds(400));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the page during a coalesce window does not trigger a reload.</summary>
|
||||
[Fact]
|
||||
public void DisposeDuringACoalesceWindow_DoesNotReload()
|
||||
{
|
||||
|
||||
+16
-10
@@ -15,7 +15,7 @@ using DeploymentsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deploym
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for arch-review 2026-08-19 P6-11. Three independent initiators —
|
||||
/// Regression tests for stale request generations on the Deployment Status page. Three independent initiators —
|
||||
/// search, paging, and the push/coalesce reload — can each have a
|
||||
/// <c>LoadDataAsync</c> flight in flight on the Deployment Status page at once. Every
|
||||
/// flight awaits its own round-trip to central MS SQL, so COMPLETION order is not
|
||||
@@ -92,7 +92,10 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the authentication state it will return.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -146,18 +149,20 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
.GetField("_currentPage", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(page)!;
|
||||
|
||||
/// <summary>Verifies that a stale page 2 flight arriving after a newer flight does not overwrite the grid.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StaleFlightArrivingAfterANewerOne_DoesNotOverwriteTheGrid()
|
||||
{
|
||||
RegisterServices();
|
||||
|
||||
var initialRows = Enumerable.Range(1, 25).Select(i => Row(i, $"Init-{i}")).ToList();
|
||||
_pageResponses.Add(CompletedGate(new DeploymentListPage(initialRows, TotalCount: 100))); // call #1: initial render
|
||||
_pageResponses.Add(CompletedGate(new DeploymentListPage(initialRows, TotalCount: 100))); // first call: initial render
|
||||
|
||||
var stale = Gate(); // call #2: page-2 flight — held open, the SLOW, STALE flight
|
||||
var stale = Gate(); // second call: page 2 flight — held open, the SLOW, STALE flight
|
||||
_pageResponses.Add(stale);
|
||||
|
||||
// call #3: status-filter flight — resolves immediately, the FAST, NEWER flight
|
||||
// third call: status-filter flight — resolves immediately, the FAST, NEWER flight
|
||||
_pageResponses.Add(CompletedGate(
|
||||
new DeploymentListPage(new[] { Row(999, "TileFilteredFreshRow") }, TotalCount: 1)));
|
||||
|
||||
@@ -182,7 +187,7 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
stale.SetResult(new DeploymentListPage(new[] { Row(2, "PageTwoStaleRow") }, TotalCount: 100));
|
||||
await flightA.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Pre-fix, flight A's late arrival clobbers _rows with the stale page-2 data.
|
||||
// Pre-fix, flight A's late arrival clobbers _rows with the stale page 2 data.
|
||||
// Post-fix, its generation is no longer current, so it discards its own
|
||||
// results and the newer grid survives untouched.
|
||||
Assert.DoesNotContain(GetRows(cut.Instance), r => r.InstanceUniqueName == "PageTwoStaleRow");
|
||||
@@ -190,7 +195,7 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The "does the timer refresh reset the page mid-search" half of P6-11. The
|
||||
/// The "does the timer refresh reset the page mid-search" half of that regression. The
|
||||
/// filter-debounce ("timer") reload fires first, as if 500ms had elapsed after a
|
||||
/// keystroke, and is held open. While it is still in flight the operator
|
||||
/// navigates to page 2 by other means (a fresh, faster flight). The debounce
|
||||
@@ -204,18 +209,19 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
/// deterministically instead of racing bUnit's render pump.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StaleFilterDebounceFlight_DoesNotResetThePageAfterANewerNavigation()
|
||||
{
|
||||
RegisterServices();
|
||||
|
||||
var initialRows = Enumerable.Range(1, 25).Select(i => Row(i, $"Init-{i}")).ToList();
|
||||
_pageResponses.Add(CompletedGate(new DeploymentListPage(initialRows, TotalCount: 100))); // call #1: initial render
|
||||
_pageResponses.Add(CompletedGate(new DeploymentListPage(initialRows, TotalCount: 100))); // first call: initial render
|
||||
|
||||
var staleSearch = Gate(); // call #2: the debounce-triggered ("timer") flight — held open, STALE
|
||||
var staleSearch = Gate(); // second call: the debounce-triggered ("timer") flight — held open, STALE
|
||||
_pageResponses.Add(staleSearch);
|
||||
|
||||
// call #3: page-2 click — resolves immediately, NEWER.
|
||||
// third call: page 2 click — resolves immediately, NEWER.
|
||||
_pageResponses.Add(CompletedGate(
|
||||
new DeploymentListPage(new[] { Row(2, "PageTwoFreshRow") }, TotalCount: 100)));
|
||||
|
||||
@@ -243,7 +249,7 @@ public class DeploymentsRequestGenerationTests : BunitContext
|
||||
await staleFlight.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Pre-fix, the stale flight unconditionally wrote _currentPage = 1 up front
|
||||
// and then overwrote the grid with page-1 data once it resolved — resetting
|
||||
// and then overwrote the grid with page 1 data once it resolved — resetting
|
||||
// the operator's page mid an already-completed navigation. Post-fix, its
|
||||
// generation is no longer current, so both the page and the grid are left
|
||||
// untouched.
|
||||
|
||||
+17
@@ -79,7 +79,10 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the authentication state it will return.</summary>
|
||||
/// <param name="state">The authentication state returned to callers.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> Task.FromResult(_state);
|
||||
}
|
||||
@@ -100,6 +103,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
|
||||
// ── Paging ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the initial load requests page one from the server and never reads every instance.</summary>
|
||||
[Fact]
|
||||
public void InitialLoad_RequestsPageOneFromTheServer_AndNeverReadsEveryInstance()
|
||||
{
|
||||
@@ -115,6 +119,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
_templateRepo.DidNotReceiveWithAnyArgs().GetAllInstancesAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the server-reported total count drives the pager rather than the rows on screen.</summary>
|
||||
[Fact]
|
||||
public void TotalCountFromServer_DrivesThePager_NotTheRowsOnScreen()
|
||||
{
|
||||
@@ -133,6 +138,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Contains("6", cut.Find("ul.pagination").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving to the next page re-queries the server for page two.</summary>
|
||||
[Fact]
|
||||
public void NextPage_RequeriesTheServerForPageTwo()
|
||||
{
|
||||
@@ -148,6 +154,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Equal(2, call.Page);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single-page result renders no pager.</summary>
|
||||
[Fact]
|
||||
public void SinglePageResult_RendersNoPager()
|
||||
{
|
||||
@@ -160,6 +167,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
|
||||
// ── Status tiles ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the status tiles show server-computed counts rather than counts of the visible page.</summary>
|
||||
[Fact]
|
||||
public void Tiles_RenderServerComputedCounts_NotCountsOfTheVisiblePage()
|
||||
{
|
||||
@@ -179,6 +187,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Contains("400", cut.Find("[data-test=\"deploy-tile-all\"]").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clicking a status tile applies the filter on the server and resets to page one.</summary>
|
||||
[Fact]
|
||||
public void ClickingAStatusTile_AppliesTheFilterServerSide_AndResetsToPageOne()
|
||||
{
|
||||
@@ -197,6 +206,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Equal("true", cut.Find("[data-test=\"deploy-tile-failed\"]").GetAttribute("aria-pressed"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clicking the active status tile again clears the status filter.</summary>
|
||||
[Fact]
|
||||
public void ClickingTheActiveTileAgain_ClearsTheStatusFilter()
|
||||
{
|
||||
@@ -214,6 +224,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
|
||||
// ── Site scoping ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that a system-wide user queries with no site scope.</summary>
|
||||
[Fact]
|
||||
public void SystemWideUser_QueriesWithNoSiteScope()
|
||||
{
|
||||
@@ -224,6 +235,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Null(Assert.Single(_pageCalls).Filter.SiteIdScope);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site-scoped user's permitted site ids are pushed into the query.</summary>
|
||||
[Fact]
|
||||
public void SiteScopedUser_PushesThePermittedSiteIdsIntoTheQuery()
|
||||
{
|
||||
@@ -238,6 +250,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
|
||||
// ── Filter debounce ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that search input is debounced so a burst of keystrokes issues a single query.</summary>
|
||||
[Fact]
|
||||
public void SearchInput_IsDebounced_SoABurstOfKeystrokesIssuesOneQuery()
|
||||
{
|
||||
@@ -263,6 +276,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Equal(1, call.Page);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clearing the filters resets the search text and status and re-queries the server.</summary>
|
||||
[Fact]
|
||||
public void ClearFilters_ResetsSearchAndStatus_AndRequeries()
|
||||
{
|
||||
@@ -280,6 +294,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.Equal(1, call.Page);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the clear-filters button is enabled as soon as the operator types, not only once the debounce fires.</summary>
|
||||
[Fact]
|
||||
public void ClearFilters_IsEnabledAsSoonAsTheOperatorTypes_NotOnlyOnceTheDebounceFires()
|
||||
{
|
||||
@@ -294,6 +309,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
Assert.False(cut.Find("[data-test=\"deploy-clear-filters\"]").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the page with an armed filter timer does not issue a query.</summary>
|
||||
[Fact]
|
||||
public void DisposeWithAnArmedFilterTimer_DoesNotQuery()
|
||||
{
|
||||
@@ -311,6 +327,7 @@ public class DeploymentsServerPagingTests : BunitContext
|
||||
|
||||
// ── Rendering ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the instance name comes from the joined server row rather than a client-side lookup table.</summary>
|
||||
[Fact]
|
||||
public void InstanceName_ComesFromTheJoinedRow_NotAClientSideLookupTable()
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// #37: the MesMoveOut shape — a REQUIRED array of objects — which the old flat text-box pane
|
||||
/// The MesMoveOut shape — a REQUIRED array of objects — which the old flat text-box pane
|
||||
/// could not take at all (the array reached the site empty).
|
||||
/// </summary>
|
||||
private static readonly ApiMethod MesMoveOut = new("MesMoveOut", "return 1;")
|
||||
@@ -69,6 +69,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
],
|
||||
[new("n1", "n2", false), new("n2", "n3", false)]);
|
||||
|
||||
/// <summary>Verifies that a refresh merge keeps the replay row order and then appends rows that exist only in the live run.</summary>
|
||||
[Fact]
|
||||
public void RefreshMerge_UsesReplayOrderThenLiveOnlyRows()
|
||||
{
|
||||
@@ -140,10 +141,14 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly AuthenticationState _state;
|
||||
/// <summary>Initializes the provider with the authentication state to return.</summary>
|
||||
/// <param name="state">The authentication state to return.</param>
|
||||
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync() => Task.FromResult(_state);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the page renders the toolbar and an empty flow for a deployer.</summary>
|
||||
[Fact]
|
||||
public void Renders_ForDeployer_WithToolbarAndEmptyFlow()
|
||||
{
|
||||
@@ -164,6 +169,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
// ---- F3: Replay opens to the OperationalAudit roles; Live stays Deployer-only ----------------
|
||||
|
||||
/// <summary>Verifies that the page renders replay-only for a viewer, with no live run controls.</summary>
|
||||
[Fact]
|
||||
public void Renders_ForViewer_ReplayOnly_WithNoLiveControls()
|
||||
{
|
||||
@@ -186,6 +192,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("select[data-test='flow-method-select']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a viewer can load a replay but gets no retry or discard actions on a parked node.</summary>
|
||||
[Fact]
|
||||
public void Viewer_LoadsAReplay_ButGetsNoRetryOrDiscardOnAParkedNode()
|
||||
{
|
||||
@@ -209,6 +216,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_flow.Received(1).ReplayAsync(root, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a viewer drilling in with a method id does not open the live run surface.</summary>
|
||||
[Fact]
|
||||
public void Viewer_DrillInWithAMethodId_DoesNotOpenTheLiveRunSurface()
|
||||
{
|
||||
@@ -225,6 +233,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.NotNull(cut.Find("input[data-test='flow-execution-id']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that choosing a method shows the predicted graph and the inputs form.</summary>
|
||||
[Fact]
|
||||
public void ChoosingAMethod_ShowsThePredictedGraph_AndTheInputsForm()
|
||||
{
|
||||
@@ -236,7 +245,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForAssertion(() => Assert.Equal(3, cut.FindAll("g[data-test='flow-node']").Count));
|
||||
Assert.Equal("Predicted", cut.Find("[data-test='flow-state']").TextContent.Trim());
|
||||
Assert.NotNull(cut.Find("[data-test='flow-predicted-note']"));
|
||||
// #37: the method pane is the shared ParameterValueForm (typed inputs by field id), and
|
||||
// The method pane is the shared ParameterValueForm (typed inputs by field id), and
|
||||
// Run is gated on its validity — MachineCode is required, so Run stays off until it is set.
|
||||
Assert.NotEmpty(cut.FindAll("[data-test='flow-method-params']"));
|
||||
Assert.NotNull(cut.Find("#param-MachineCode"));
|
||||
@@ -246,6 +255,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a run sends the typed inputs and renders the returned graph.</summary>
|
||||
[Fact]
|
||||
public void Run_SendsTypedInputs_AndRendersTheReturnedGraph()
|
||||
{
|
||||
@@ -274,6 +284,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Contains("POST /api/DelmiaRecipeDownload", cut.Find("[data-test='flow-step-title']").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a standby node refusal is shown verbatim in an inline alert.</summary>
|
||||
[Fact]
|
||||
public void Run_StandbyNodeRefusal_ShowsInlineAlertVerbatim()
|
||||
{
|
||||
@@ -293,6 +304,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("Failed", cut.Find("[data-test='flow-state']").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an execution id query parameter triggers a replay on the first render.</summary>
|
||||
[Fact]
|
||||
public void ExecutionIdQueryParameter_TriggersReplayOnFirstRender()
|
||||
{
|
||||
@@ -310,6 +322,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Contains("btn-primary", cut.Find("button[data-test='flow-mode-replay']").ClassName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a run with no site steps and no child execution id shows the site-not-traced label.</summary>
|
||||
[Fact]
|
||||
public void Run_WithNoSiteStepsAndNoChildId_ShowsSiteNotTracedLabel()
|
||||
{
|
||||
@@ -325,7 +338,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForElement("[data-test='flow-site-not-traced']");
|
||||
}
|
||||
|
||||
// ---- Node actions (follow-ups plan 2026-09-06, Task 8): Retry / Discard on a Parked node -------
|
||||
// ---- Node actions (follow-ups plan 2026-09-06): Retry / Discard on a Parked node -------
|
||||
|
||||
/// <summary>Same events as <see cref="OutboundOutcome"/> but the cached call ends Parked; the second
|
||||
/// outcome shares the event objects (stable ids, as real audit rows are) and adds a Delivered resolve.</summary>
|
||||
@@ -379,6 +392,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
return cut;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retrying a parked node confirms, relays the retry and re-polls until the node is no longer parked.</summary>
|
||||
[Fact]
|
||||
public void ParkedNode_Retry_ConfirmsRelaysAndRepollsUntilNotParked()
|
||||
{
|
||||
@@ -401,6 +415,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("button[data-test='flow-step-retry']")); // the selected node is no longer parked
|
||||
}
|
||||
|
||||
/// <summary>Verifies that discarding a parked node uses a danger confirmation and does not poll when the confirmation is refused.</summary>
|
||||
[Fact]
|
||||
public void ParkedNode_Discard_UsesDangerConfirm_AndDoesNotPollWhenRefused()
|
||||
{
|
||||
@@ -420,6 +435,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.NotNull(cut.Find("button[data-test='flow-step-retry']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that declining the confirmation for a parked node never relays the action.</summary>
|
||||
[Fact]
|
||||
public void ParkedNode_ConfirmDeclined_NeverRelays()
|
||||
{
|
||||
@@ -436,6 +452,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_flow.DidNotReceive().ReplayAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an action on a parked node at an out-of-scope site is refused before any confirmation.</summary>
|
||||
[Fact]
|
||||
public void ParkedNode_OutOfScopeSite_RefusedBeforeConfirm()
|
||||
{
|
||||
@@ -452,7 +469,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_flow.DidNotReceive().RetryOutboundAsync(Arg.Any<FlowStep>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ---- Outbound calls (2026-09-06 plan, Task 9): pending refresh after a Live run ----------
|
||||
// ---- Outbound calls (2026-09-06 plan): pending refresh after a Live run ----------
|
||||
|
||||
/// <summary>
|
||||
/// Inbound → route → site run → cached API call; the call is Pending unless <paramref name="resolved"/>.
|
||||
@@ -498,6 +515,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a live run with a pending outbound call polls the replay until the call reaches a terminal state.</summary>
|
||||
[Fact]
|
||||
public void LiveRun_WithPendingOutbound_PollsReplayUntilTerminal()
|
||||
{
|
||||
@@ -560,6 +578,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
return new FlowDebugOutcome(FlowGraphBuilder.Build(events), raw, null, null);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a pending refresh keeps the steps the live run already had when the replay lags behind telemetry.</summary>
|
||||
[Fact]
|
||||
public void PendingRefresh_KeepsStepsTheLiveRunAlreadyHad_WhenReplayLagsBehindTelemetry()
|
||||
{
|
||||
@@ -593,6 +612,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Contains(cut.FindAll("[data-test='flow-node-pill']"), p => p.TextContent.Trim() == "Ok");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a live run with no pending step never polls.</summary>
|
||||
[Fact]
|
||||
public void LiveRun_WithNoPendingStep_NeverPolls()
|
||||
{
|
||||
@@ -611,6 +631,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test='flow-pending-count']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that replay mode never polls but the refresh button replays once.</summary>
|
||||
[Fact]
|
||||
public void ReplayMode_NeverPolls_ButRefreshButtonReplaysOnce()
|
||||
{
|
||||
@@ -665,6 +686,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_flow.PredictScriptAsync(7, Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(ThreeNodePrediction());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the script entry lists scripts with their trigger labels once a site and instance are chosen.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_ListsScriptsWithTriggerLabels_AfterSiteAndInstance()
|
||||
{
|
||||
@@ -679,6 +701,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("select[data-test='flow-attribute-select']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a write-kind script shows a value field for the trigger attribute and gates Run on it.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_WriteKind_ShowsTheValueField_ForTheTriggerAttribute_AndGatesRunOnIt()
|
||||
{
|
||||
@@ -697,6 +720,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a call-kind script shows the parameter form and runs with the entered parameters.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_CallKind_ShowsTheParameterForm_AndRunsWithParameters()
|
||||
{
|
||||
@@ -717,6 +741,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_ = _flow.Received(1).RunScriptAsync(7, "Manual", Arg.Is<JsonElement?>(p => p != null && p.Value.GetProperty("n").GetInt64() == 3), null, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a call-kind script without parameters says so and can be run.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_CallKindWithoutParameters_SaysSo_AndCanRun()
|
||||
{
|
||||
@@ -731,6 +756,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deep link with an instance and a script preselects both and shows the prediction.</summary>
|
||||
[Fact]
|
||||
public void DeepLink_InstanceAndScript_PreselectsAndPredicts()
|
||||
{
|
||||
@@ -774,6 +800,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
return cut;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that switching between call-kind scripts resets the parameter form and its validity.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_SwitchingBetweenCallKindScripts_ResetsTheFormAndValidity()
|
||||
{
|
||||
@@ -792,6 +819,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that switching from an invalid form to a script without parameters enables Run.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_SwitchingFromAnInvalidFormToANoParameterScript_EnablesRun()
|
||||
{
|
||||
@@ -805,6 +833,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the instance picker shows areas collapsed until they are expanded.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_InstancePicker_ShowsAreasCollapsed_UntilExpanded()
|
||||
{
|
||||
@@ -838,6 +867,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal(["— choose —", "Manual (Call)", "OnFlag (ValueChange: Flag)", "Tick (Interval)"], options);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deep link with an instance id shows the instance name in the picker.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_DeepLink_InstanceId_ShowsItsNameInThePicker()
|
||||
{
|
||||
@@ -913,6 +943,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.False(cut.Find("[data-test='flow-instance-input']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the site clears the picked instance.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntry_SiteChange_ClearsThePickedInstance()
|
||||
{
|
||||
@@ -940,7 +971,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal(["— choose —"], cut.FindAll("select[data-test='flow-script-select'] option").Select(o => o.TextContent.Trim()).ToList());
|
||||
}
|
||||
|
||||
// ---- Step logs (plan 2026-09-09 D5/D7, Task 13): status-strip counts, truncation notes, cross-step hint ----
|
||||
// ---- Step logs (plan 2026-09-09 D5/D7): status-strip counts, truncation notes, cross-step hint ----
|
||||
|
||||
/// <summary>One <c>ScriptTrace.ScriptLog</c> row — a line the builder folds onto a step, never a node of its own.</summary>
|
||||
private static AuditEvent LogLine(Guid execution, string level, int ms, Guid? stepEventId = null, string message = "hello") =>
|
||||
@@ -995,6 +1026,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
return cut;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the status strip shows log counts by level.</summary>
|
||||
[Fact]
|
||||
public void StatusStrip_ShowsLogCountsByLevel()
|
||||
{
|
||||
@@ -1005,6 +1037,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("1 errors · 2 warnings · 3 info", cut.Find("[data-test='flow-log-counts']").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the status strip omits log counts when the run logged nothing.</summary>
|
||||
[Fact]
|
||||
public void StatusStrip_OmitsLogCounts_WhenTheRunLoggedNothing()
|
||||
{
|
||||
@@ -1017,6 +1050,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test='flow-unattached-logs']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the status strip shows the inline log truncation badge.</summary>
|
||||
[Fact]
|
||||
public void StatusStrip_ShowsTheInlineLogTruncationBadge()
|
||||
{
|
||||
@@ -1028,6 +1062,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Contains("inline log lines capped", cut.Find("[data-test='flow-logs-truncated']").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the status strip notes log lines that could not be placed on a step.</summary>
|
||||
[Fact]
|
||||
public void StatusStrip_NotesLinesThatCouldNotBePlacedOnAStep()
|
||||
{
|
||||
@@ -1040,6 +1075,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("0 errors · 0 warnings · 1 info", cut.Find("[data-test='flow-log-counts']").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the state chip stays completed when the only errors are log lines.</summary>
|
||||
[Fact]
|
||||
public void StateChip_StaysCompleted_WhenTheOnlyErrorsAreLogLines()
|
||||
{
|
||||
@@ -1052,6 +1088,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("2 errors · 0 warnings · 0 info", cut.Find("[data-test='flow-log-counts']").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the step detail log hint selects the sibling step that carries the other log lines.</summary>
|
||||
[Fact]
|
||||
public void StepDetail_LogHint_SelectsTheSiblingStepCarryingTheOtherLines()
|
||||
{
|
||||
@@ -1134,6 +1171,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
return (run, refreshed, root);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that refreshing outcomes clears the inline cap badge once it merges in more log rows.</summary>
|
||||
[Fact]
|
||||
public void RefreshOutcomes_ClearsTheInlineCapBadge_OnceItMergesInMoreLogRows()
|
||||
{
|
||||
@@ -1150,6 +1188,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("0 errors · 1 warnings · 1 info", cut.Find("[data-test='flow-log-counts']").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that refreshing outcomes keeps the inline cap badge when the re-read brings back no new lines.</summary>
|
||||
[Fact]
|
||||
public void RefreshOutcomes_KeepsTheInlineCapBadge_WhenTheReReadBringsBackNoNewLines()
|
||||
{
|
||||
@@ -1170,6 +1209,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
// ---- CountLogs: the level buckets, straight ---------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that log counting buckets lines by level case-insensitively and counts everything else as info.</summary>
|
||||
[Fact]
|
||||
public void CountLogs_BucketsByLevel_CaseInsensitively_WithEverythingElseAsInfo()
|
||||
{
|
||||
@@ -1189,6 +1229,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal((1, 1, 3, 5), FlowDebuggerPage.CountLogs(graph));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that counting logs for a null graph yields all zeroes.</summary>
|
||||
[Fact]
|
||||
public void CountLogs_NullGraph_IsAllZeroes()
|
||||
{
|
||||
@@ -1197,7 +1238,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the method pane's <c>ParameterValueForm</c> and fills the fixture method's one
|
||||
/// required field, so Run is enabled (#37 gates Run on the form's validity).
|
||||
/// required field, so Run is enabled (Run is gated on the form's validity).
|
||||
/// </summary>
|
||||
private static void FillRequiredMethodInput(IRenderedComponent<FlowDebuggerPage> cut)
|
||||
{
|
||||
@@ -1205,8 +1246,9 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
// ── #37: structured method parameters ───────────────────────────────────────────────────
|
||||
// ── Structured method parameters ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that an array of objects entered in the item editors round-trips into the run payload.</summary>
|
||||
[Fact]
|
||||
public void MethodEntry_ArrayOfObjects_RoundTripsFromItemEditorsToTheRunPayload()
|
||||
{
|
||||
@@ -1241,6 +1283,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("WO-2", orders[1].GetProperty("WorkOrderNumber").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that leaving a required list item field empty keeps Run disabled.</summary>
|
||||
[Fact]
|
||||
public void MethodEntry_RequiredListItemField_LeftEmpty_KeepsRunDisabled()
|
||||
{
|
||||
@@ -1258,6 +1301,8 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an invalid raw JSON document blocks Run and shows the error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MethodEntry_RawJson_InvalidDocument_BlocksRun_AndShowsTheError()
|
||||
{
|
||||
@@ -1289,6 +1334,8 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
_ = _flow.DidNotReceive().RunMethodAsync(Arg.Any<int>(), Arg.Any<JsonElement?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a valid raw JSON document is sent verbatim and survives switching back to the form.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MethodEntry_RawJson_ValidDocument_IsSentVerbatim_AndSurvivesTheSwitchBackToForm()
|
||||
{
|
||||
@@ -1322,6 +1369,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.Equal("P-1", sent.Value.GetProperty("WorkOrders")[0].GetProperty("PartNumber").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that switching methods resets the form and its validity.</summary>
|
||||
[Fact]
|
||||
public void MethodEntry_SwitchingMethods_ResetsTheFormAndItsValidity()
|
||||
{
|
||||
@@ -1339,6 +1387,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a persisted value tree is restored into the form.</summary>
|
||||
[Fact]
|
||||
public void MethodEntry_PersistedValueTree_IsRestoredIntoTheForm()
|
||||
{
|
||||
@@ -1353,11 +1402,12 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy flat string entry that no longer validates is dropped.</summary>
|
||||
[Fact]
|
||||
public void MethodEntry_LegacyFlatStringEntry_ThatNoLongerValidates_IsDropped()
|
||||
{
|
||||
RegisterServices();
|
||||
// Pre-#37 entries stored every field as a string; "7" is not an Integer, so the whole
|
||||
// Earlier entries stored every field as a string; "7" is not an Integer, so the whole
|
||||
// entry is discarded rather than painted into the form as an error.
|
||||
JSInterop.Setup<string?>("localStorage.getItem", "flowDebugger.inputs.4")
|
||||
.SetResult("{\"MachineCode\":\"Z9\",\"Qty\":\"7\"}");
|
||||
|
||||
@@ -5,16 +5,16 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger outbound-calls plan (2026-09-06) Task 8: the step detail panel's Lifecycle tab,
|
||||
/// outbound "Where this ran" fields and kind-specific Input tab labels. Follow-ups plan (2026-09-06)
|
||||
/// Task 6: report deep links from a correlated outbound step and Retry/Discard on a parked one (D2/D5).
|
||||
/// Step-logs plan (2026-09-09) Task 12: the Log tab — exception block, level-filtered lines and the
|
||||
/// cross-step hint (design D8).
|
||||
/// Tests for the Flow Debugger step detail panel: the Lifecycle tab, outbound "Where this ran" fields
|
||||
/// and kind-specific Input tab labels; report deep links from a correlated outbound step and
|
||||
/// Retry/Discard on a parked one (D2/D5); and the Log tab — exception block, level-filtered lines and
|
||||
/// the cross-step hint (design D8).
|
||||
/// </summary>
|
||||
public class FlowStepDetailTests : BunitContext
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
/// <summary>Initializes the test context with loose JS interop so the Log tab's clipboard calls are accepted.</summary>
|
||||
public FlowStepDetailTests()
|
||||
{
|
||||
// The Log tab's Copy buttons call navigator.clipboard through IJSRuntime.
|
||||
@@ -59,6 +59,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
new(Guid.NewGuid(), "CachedResolve", terminalStatus, T0.AddMilliseconds(4100), "node-b", null, null),
|
||||
];
|
||||
|
||||
/// <summary>Verifies that the Lifecycle tab renders its rows in time order and shows who delivered the operation.</summary>
|
||||
[Fact]
|
||||
public void LifecycleTab_RendersRowsInTimeOrder_AndDeliveredBy()
|
||||
{
|
||||
@@ -84,6 +85,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-parked-hint]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a step without lifecycle rows hides the Lifecycle tab.</summary>
|
||||
[Fact]
|
||||
public void NoLifecycle_HidesTheTab()
|
||||
{
|
||||
@@ -93,6 +95,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Equal("Input", cut.Find("[data-test=flow-step-tab-input]").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parked notification step shows the hint, the list label and a parked badge.</summary>
|
||||
[Fact]
|
||||
public void ParkedNotify_ShowsHint_AndListLabel_AndParkedBadge()
|
||||
{
|
||||
@@ -115,6 +118,10 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Contains("Site Calls / Notification", cut.Find("[data-test=flow-step-parked-hint]").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each outbound step kind labels its Input tab and target field appropriately.</summary>
|
||||
/// <param name="kind">The outbound step kind under test.</param>
|
||||
/// <param name="inputLabel">The expected label of the Input tab.</param>
|
||||
/// <param name="targetLabel">The expected label of the target field.</param>
|
||||
[Theory]
|
||||
[InlineData(FlowStepKind.ApiCall, "Request", "System")]
|
||||
[InlineData(FlowStepKind.DbCall, "SQL + parameters", "Connection")]
|
||||
@@ -127,6 +134,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Contains(targetLabel, cut.Find("[data-test=flow-step-where]").TextContent);
|
||||
Assert.Equal("Pending", cut.Find("[data-test=flow-step-outcome]").TextContent.Trim());
|
||||
}
|
||||
/// <summary>Verifies that an outbound step with a correlation id links to its report page.</summary>
|
||||
[Fact]
|
||||
public void OutboundStepWithCorrelation_LinksToTheReportPage()
|
||||
{
|
||||
@@ -149,6 +157,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=open-report]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parked step with callbacks shows Retry and Discard actions.</summary>
|
||||
[Fact]
|
||||
public void ParkedStep_WithCallbacks_ShowsRetryAndDiscard()
|
||||
{
|
||||
@@ -187,6 +196,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.True(cut.Find("button[data-test=flow-step-discard]").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parked step without callbacks shows only the hint link.</summary>
|
||||
[Fact]
|
||||
public void ParkedStep_WithoutCallbacks_ShowsOnlyTheHintLink()
|
||||
{
|
||||
@@ -205,6 +215,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Equal($"/notifications/report?correlationId={c:N}", hintLink!.GetAttribute("href"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful step never shows Retry or Discard actions.</summary>
|
||||
[Fact]
|
||||
public void OkStep_NeverShowsActions()
|
||||
{
|
||||
@@ -218,6 +229,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-discard]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a step with no log lines and no error detail hides the Log tab.</summary>
|
||||
[Fact]
|
||||
public void NoLinesAndNoDetail_HidesTheLogTab()
|
||||
{
|
||||
@@ -225,6 +237,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-tab-log]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Log tab shows a count of its lines.</summary>
|
||||
[Fact]
|
||||
public void LogTab_CountsItsLines()
|
||||
{
|
||||
@@ -239,6 +252,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Equal("Log (3)", cut.Find("[data-test=flow-step-tab-log]").TextContent.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failed step with error detail opens on the Log tab and renders the exception block.</summary>
|
||||
[Fact]
|
||||
public void FailedStepWithDetail_OpensOnLog_AndRendersTheExceptionBlock()
|
||||
{
|
||||
@@ -254,10 +268,11 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.NotNull(cut.Find("[data-test=flow-step-exception-copy]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the exception block header names the innermost exception type in full for a wrapped exception.</summary>
|
||||
[Fact]
|
||||
public void WrappedException_TheBlockHeaderNamesTheINNERMOSTType_InFull()
|
||||
{
|
||||
// Final review M1: the panel used to parse the OUTERMOST exception of a " ---> " chain
|
||||
// The panel used to parse the OUTERMOST exception of a " ---> " chain
|
||||
// while the graph node's subtitle showed the innermost — the two now share
|
||||
// ExceptionHeader, which always follows the chain to its end. The panel still shows the
|
||||
// type with its namespace (only the node badge shortens it), and the raw detail is
|
||||
@@ -275,6 +290,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Contains("System.AggregateException", exc.QuerySelector("pre")!.TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a successful step with log lines opens on the Input tab.</summary>
|
||||
[Fact]
|
||||
public void OkStepWithLines_OpensOnInput()
|
||||
{
|
||||
@@ -286,6 +302,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-exception]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the warning filter leaves only warning and error rows.</summary>
|
||||
[Fact]
|
||||
public void WarnFilter_LeavesOnlyWarnAndErrorRows()
|
||||
{
|
||||
@@ -311,6 +328,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Equal(4, cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]").Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the hint names another step of the same execution and selects it when clicked.</summary>
|
||||
[Fact]
|
||||
public void Hint_NamesAnotherStepOfTheSameExecution_AndSelectsIt()
|
||||
{
|
||||
@@ -336,6 +354,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Same(other, picked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hint without a select callback is rendered as plain text.</summary>
|
||||
[Fact]
|
||||
public void Hint_WithoutASelectCallback_IsPlainText()
|
||||
{
|
||||
@@ -352,6 +371,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Null(hint.QuerySelector("button"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the see-exception link in the output alert switches to the Log tab.</summary>
|
||||
[Fact]
|
||||
public void OutputAlert_SeeExceptionLink_SwitchesToTheLogTab()
|
||||
{
|
||||
@@ -367,6 +387,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.NotNull(cut.Find("[data-test=flow-step-exception]"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a runtime-sourced log line shows its source and any attached exception.</summary>
|
||||
[Fact]
|
||||
public void RuntimeSourcedLine_ShowsItsSource_AndAnAttachedException()
|
||||
{
|
||||
@@ -384,6 +405,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Contains("System.TimeoutException", row.QuerySelector("details pre")!.TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that switching steps keeps the selected tab when it is still valid for the new step.</summary>
|
||||
[Fact]
|
||||
public void SwitchingSteps_KeepsAStillValidTab()
|
||||
{
|
||||
@@ -400,6 +422,7 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.DoesNotContain("active", cut.Find("[data-test=flow-step-tab-input]").ClassName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that refreshing the same step keeps the selected tab and the level filter.</summary>
|
||||
[Fact]
|
||||
public void RefreshingTheSameStep_KeepsTheTabAndTheLevelFilter()
|
||||
{
|
||||
|
||||
+4
-1
@@ -19,7 +19,7 @@ using InstanceConfigurePage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.D
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle D drill-in test (#23 M7-T12) for the Instance Configure page. The
|
||||
/// Drill-in test for the Instance Configure page. The
|
||||
/// chip routes operators into the central Audit Log pre-filtered by
|
||||
/// <c>?instance={Instance.UniqueName}</c>. Instance is UI-only on the filter
|
||||
/// bar (the repository filter contract has no instance column), so the page
|
||||
@@ -32,6 +32,7 @@ public class InstanceConfigureAuditDrillinTests : BunitContext
|
||||
Substitute.For<ITemplateEngineRepository>();
|
||||
private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
|
||||
|
||||
/// <summary>Initializes the test context and registers the services the Instance Configure page depends on.</summary>
|
||||
public InstanceConfigureAuditDrillinTests()
|
||||
{
|
||||
// Loose JS interop because shared components on the page render
|
||||
@@ -65,6 +66,7 @@ public class InstanceConfigureAuditDrillinTests : BunitContext
|
||||
AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the page renders a recent audit activity link carrying the instance unique name.</summary>
|
||||
[Fact]
|
||||
public void Page_HasRecentAuditActivityLink_WithInstanceUniqueName()
|
||||
{
|
||||
@@ -114,6 +116,7 @@ public class InstanceConfigureAuditDrillinTests : BunitContext
|
||||
/// protocol == "OpcUa". Previously BuildTestableRows filtered to OPC UA
|
||||
/// only, leaving the button greyed for MxGateway bindings.
|
||||
/// </summary>
|
||||
/// <param name="protocol">The data connection protocol of the bound attribute.</param>
|
||||
[Theory]
|
||||
[InlineData("MxGateway")]
|
||||
[InlineData("OpcUa")]
|
||||
|
||||
+5
@@ -33,6 +33,7 @@ public class InstanceConfigureBindingBehaviourTests : BunitContext
|
||||
private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
|
||||
private readonly IFlatteningPipeline _pipeline = Substitute.For<IFlatteningPipeline>();
|
||||
|
||||
/// <summary>Creates the test class and registers the stubbed services the page needs.</summary>
|
||||
public InstanceConfigureBindingBehaviourTests()
|
||||
{
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
@@ -85,6 +86,7 @@ public class InstanceConfigureBindingBehaviourTests : BunitContext
|
||||
new() { CanonicalName = canonical, DataType = "Boolean", DataSourceReference = effective ?? dflt,
|
||||
DefaultDataSourceReference = dflt, BoundDataConnectionId = conn };
|
||||
|
||||
/// <summary>Verifies that binding rows come from the flattened configuration with composed members grouped.</summary>
|
||||
[Fact]
|
||||
public void Rows_ComeFromFlattenedConfiguration_ComposedMembersGrouped()
|
||||
{
|
||||
@@ -110,6 +112,7 @@ public class InstanceConfigureBindingBehaviourTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flatten failure falls back to the direct attributes and shows a banner.</summary>
|
||||
[Fact]
|
||||
public void FlattenFailure_FallsBackToDirectAttributes_WithBanner()
|
||||
{
|
||||
@@ -128,6 +131,8 @@ public class InstanceConfigureBindingBehaviourTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying a connection to a mixed group keeps each member connection and that save posts the composed rows.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApplyOnMixedConnectionGroup_KeepsEachMembersConnection_AndSavePostsComposedRows()
|
||||
{
|
||||
|
||||
+6
@@ -9,6 +9,7 @@ public class InstanceConfigureBindingGroupsTests
|
||||
{
|
||||
private static string Markup => InstanceConfigureBindingRowsTests.ReadMarkup();
|
||||
|
||||
/// <summary>Verifies that a group header has a connection selector, an object selector and an Apply button.</summary>
|
||||
[Fact]
|
||||
public void GroupHeader_HasConnectionObjectAndApply()
|
||||
{
|
||||
@@ -21,6 +22,7 @@ public class InstanceConfigureBindingGroupsTests
|
||||
Assert.Contains("BindingGroupBuilder.ApplyObject(", m);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a group header shows unbound and mixed status pills.</summary>
|
||||
[Fact]
|
||||
public void GroupHeader_ShowsUnboundAndMixedPills()
|
||||
{
|
||||
@@ -30,6 +32,7 @@ public class InstanceConfigureBindingGroupsTests
|
||||
Assert.Contains("data-test=\"binding-total-unbound\"", m);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the toolbar offers an unbound-only filter and an expand-all control.</summary>
|
||||
[Fact]
|
||||
public void Toolbar_HasUnboundFilterAndExpandAll()
|
||||
{
|
||||
@@ -39,18 +42,21 @@ public class InstanceConfigureBindingGroupsTests
|
||||
Assert.Contains("data-test=\"binding-bulk-select\"", m); // existing all-groups connection select retained
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a composed member with a bare default binding is flagged.</summary>
|
||||
[Fact]
|
||||
public void ComposedBareDefault_IsFlagged()
|
||||
{
|
||||
Assert.Contains("data-test=\"binding-bare-default\"", Markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old flat bindings table is no longer rendered.</summary>
|
||||
[Fact]
|
||||
public void OldFlatTable_IsGone()
|
||||
{
|
||||
Assert.DoesNotContain("_bindingDataSourceAttrs", Markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the per-row Browse button is retained.</summary>
|
||||
[Fact]
|
||||
public void PerRowBrowseButton_IsRetained()
|
||||
{
|
||||
|
||||
+5
@@ -8,6 +8,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
/// </summary>
|
||||
public class InstanceConfigureBindingRowsTests
|
||||
{
|
||||
/// <summary>Reads the InstanceConfigure page markup from the source tree.</summary>
|
||||
/// <returns>The full text of <c>InstanceConfigure.razor</c>.</returns>
|
||||
internal static string ReadMarkup()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
@@ -18,6 +20,7 @@ public class InstanceConfigureBindingRowsTests
|
||||
|
||||
private static string Markup => ReadMarkup();
|
||||
|
||||
/// <summary>Verifies that binding rows come from the flattened configuration rather than the template attributes.</summary>
|
||||
[Fact]
|
||||
public void BindingRows_ComeFromFlattenedConfiguration_NotTemplateAttributes()
|
||||
{
|
||||
@@ -29,6 +32,7 @@ public class InstanceConfigureBindingRowsTests
|
||||
Assert.Contains("DefaultDataSourceReference", m);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that binding rows fall back to direct attributes with a warning banner when flattening fails.</summary>
|
||||
[Fact]
|
||||
public void BindingRows_FallBackToDirectAttributes_WithBanner_WhenFlattenFails()
|
||||
{
|
||||
@@ -37,6 +41,7 @@ public class InstanceConfigureBindingRowsTests
|
||||
Assert.Contains("data-test=\"binding-source-warning\"", m);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that save and test cover every binding row.</summary>
|
||||
[Fact]
|
||||
public void SaveAndTest_CoverEveryRow()
|
||||
{
|
||||
|
||||
+7
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// MV-14: the Instance Configure attribute-override panel uses the shared
|
||||
/// The Instance Configure attribute-override panel uses the shared
|
||||
/// <c>AttributeListEditor</c> for a List attribute (whole-list replacement; the
|
||||
/// element type is fixed by the base attribute, so the type select is hidden via
|
||||
/// <c>ShowElementType="false"</c>). Loading an existing override decodes its JSON
|
||||
@@ -30,6 +30,7 @@ public class InstanceConfigureListOverrideTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list override, reveals shared editor, with element type hidden.</summary>
|
||||
[Fact]
|
||||
public void ListOverride_RevealsSharedEditor_WithElementTypeHidden()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public class InstanceConfigureListOverrideTests
|
||||
Assert.Contains("RowsChanged=\"@(r => OnListRowsChanged(attr.Name, r))\"", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list override, decodes on load, and encodes on save with guard.</summary>
|
||||
[Fact]
|
||||
public void ListOverride_DecodesOnLoad_AndEncodesOnSaveWithGuard()
|
||||
{
|
||||
@@ -59,6 +61,7 @@ public class InstanceConfigureListOverrideTests
|
||||
Assert.Contains("_overrideErrors", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list override, clear removes the override row.</summary>
|
||||
[Fact]
|
||||
public void ListOverride_ClearRemovesTheOverrideRow()
|
||||
{
|
||||
@@ -70,6 +73,7 @@ public class InstanceConfigureListOverrideTests
|
||||
Assert.Contains("HasOverrideRow", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non list override, keeps single input ux.</summary>
|
||||
[Fact]
|
||||
public void NonListOverride_KeepsSingleInputUx()
|
||||
{
|
||||
@@ -79,6 +83,7 @@ public class InstanceConfigureListOverrideTests
|
||||
Assert.Contains("OnOverrideChanged(attr.Name, e)", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encoded rows, round trip through codec, as the page does.</summary>
|
||||
[Fact]
|
||||
public void EncodedRows_RoundTripThroughCodec_AsThePageDoes()
|
||||
{
|
||||
@@ -103,6 +108,7 @@ public class InstanceConfigureListOverrideTests
|
||||
Assert.Equal(json, reEncoded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that malformed list element, surfaces format exception, for inline error.</summary>
|
||||
[Fact]
|
||||
public void MalformedListElement_SurfacesFormatException_ForInlineError()
|
||||
{
|
||||
|
||||
+13
@@ -34,6 +34,7 @@ public class InstanceConfigureMesReportableTests
|
||||
private static string InstanceConfigureMarkup
|
||||
=> File.ReadAllText(Path.Combine(DeploymentComponentsRoot, "InstanceConfigure.razor"));
|
||||
|
||||
/// <summary>Verifies that the override modal has a tri-state MES-reportable select.</summary>
|
||||
[Fact]
|
||||
public void OverrideModal_HasTriStateMesReportableSelect()
|
||||
{
|
||||
@@ -57,6 +58,10 @@ public class InstanceConfigureMesReportableTests
|
||||
Assert.Contains("value=\"false\"", selectBody);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting only the MES-reportable flag counts as an override delta.</summary>
|
||||
/// <param name="overrideJson">The override json.</param>
|
||||
/// <param name="priority">The priority.</param>
|
||||
/// <param name="mesReportable">The mes reportable.</param>
|
||||
[Theory]
|
||||
// The new term: a reportable-only override is a real delta in BOTH directions
|
||||
// (false is not "no override" — it pins a reportable template alarm off).
|
||||
@@ -66,6 +71,10 @@ public class InstanceConfigureMesReportableTests
|
||||
string? overrideJson, int? priority, bool? mesReportable)
|
||||
=> Assert.True(InstanceConfigure.HasOverrideDelta(overrideJson, priority, mesReportable));
|
||||
|
||||
/// <summary>Verifies that an override with nothing set is not a delta.</summary>
|
||||
/// <param name="overrideJson">The override json.</param>
|
||||
/// <param name="priority">The priority.</param>
|
||||
/// <param name="mesReportable">The mes reportable.</param>
|
||||
[Theory]
|
||||
[InlineData(null, null, null)]
|
||||
[InlineData("", null, null)]
|
||||
@@ -74,6 +83,10 @@ public class InstanceConfigureMesReportableTests
|
||||
string? overrideJson, int? priority, bool? mesReportable)
|
||||
=> Assert.False(InstanceConfigure.HasOverrideDelta(overrideJson, priority, mesReportable));
|
||||
|
||||
/// <summary>Verifies that the existing override terms still count as a delta.</summary>
|
||||
/// <param name="overrideJson">The override json.</param>
|
||||
/// <param name="priority">The priority.</param>
|
||||
/// <param name="mesReportable">The mes reportable.</param>
|
||||
[Theory]
|
||||
[InlineData("{\"hi\":70}", null, null)]
|
||||
[InlineData(null, 750, null)]
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Task 25: Instance Configure exposes a "Native Alarm Source Overrides" card —
|
||||
/// Instance Configure exposes a "Native Alarm Source Overrides" card —
|
||||
/// per-instance retarget of an inherited native alarm source binding (connection /
|
||||
/// source reference / filter; blank = inherited). <c>InstanceConfigure</c> is a
|
||||
/// heavyweight page (7 injected services incl. <c>InstanceService</c> and the
|
||||
@@ -24,6 +24,7 @@ public class InstanceConfigureNativeAlarmTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Instance Configure page has a native alarm source overrides card.</summary>
|
||||
[Fact]
|
||||
public void InstanceConfigure_HasNativeAlarmSourceOverridesCard()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class InstanceConfigureNativeAlarmTests
|
||||
Assert.Contains("HasNativeOverride", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native override row has connection, source reference and filter editors.</summary>
|
||||
[Fact]
|
||||
public void NativeOverrideRow_HasConnectionSourceRefAndFilterEditors()
|
||||
{
|
||||
@@ -44,6 +46,7 @@ public class InstanceConfigureNativeAlarmTests
|
||||
Assert.Contains("(inherited)", markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native override wires the repository upsert and clear operations.</summary>
|
||||
[Fact]
|
||||
public void NativeOverride_WiresRepositoryUpsertAndClear()
|
||||
{
|
||||
|
||||
+8
@@ -40,6 +40,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
private readonly IAuditService _audit = Substitute.For<IAuditService>();
|
||||
private readonly DialogService _dialog = new();
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>InstanceCreateTemplateFieldTests</c> class.</summary>
|
||||
public InstanceCreateTemplateFieldTests()
|
||||
{
|
||||
Services.AddSingleton(_repo);
|
||||
@@ -144,6 +145,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
/// <summary>Wrapper that renders InstanceCreate next to a DialogHost, as MainLayout does.</summary>
|
||||
public sealed class TestHostWrapper : ComponentBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<InstanceCreatePage>(0);
|
||||
@@ -158,6 +160,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
.First(li => li.QuerySelector(".tv-label")!.TextContent.Trim() == label)
|
||||
.QuerySelector(".tv-toggle")!.Click();
|
||||
|
||||
/// <summary>Verifies that template field is read only with choose button and placeholder.</summary>
|
||||
[Fact]
|
||||
public void TemplateField_IsReadOnlyWithChooseButton_AndPlaceholder()
|
||||
{
|
||||
@@ -169,6 +172,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
Assert.NotNull(cut.Find("button[data-test='create-template-choose']"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that template id query prefills field with folder path.</summary>
|
||||
[Fact]
|
||||
public void TemplateIdQuery_PrefillsFieldWithFolderPath()
|
||||
{
|
||||
@@ -177,6 +181,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
Assert.Equal("A / B / Deep", cut.Find("input[data-test='create-template']").GetAttribute("value"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown template id query leaves field empty and shows hint.</summary>
|
||||
[Fact]
|
||||
public void UnknownTemplateIdQuery_LeavesFieldEmpty_AndShowsHint()
|
||||
{
|
||||
@@ -186,6 +191,7 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
Assert.Contains("Template 999 no longer exists", cut.Markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no templates disables choose and links to templates page.</summary>
|
||||
[Fact]
|
||||
public void NoTemplates_DisablesChoose_AndLinksToTemplatesPage()
|
||||
{
|
||||
@@ -195,6 +201,8 @@ public class InstanceCreateTemplateFieldTests : BunitContext
|
||||
Assert.Contains("/design/templates", cut.Markup);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that choosing in picker sets template and re previews bindings when site set.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ChoosingInPicker_SetsTemplate_AndRePreviewsBindingsWhenSiteSet()
|
||||
{
|
||||
|
||||
+5
@@ -41,6 +41,7 @@ public class TopologyDisableConfirmLabelTests : BunitContext
|
||||
private readonly IAuditService _audit = Substitute.For<IAuditService>();
|
||||
private readonly DialogService _dialog = new();
|
||||
|
||||
/// <summary>Initializes the test host with stubbed services and the dialog service.</summary>
|
||||
public TopologyDisableConfirmLabelTests()
|
||||
{
|
||||
Services.AddSingleton(_repo);
|
||||
@@ -111,6 +112,7 @@ public class TopologyDisableConfirmLabelTests : BunitContext
|
||||
/// <summary>Wrapper so the page and the dialog host share one render tree.</summary>
|
||||
public sealed class TestHostWrapper : ComponentBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<TopologyPage>(0);
|
||||
@@ -151,6 +153,7 @@ public class TopologyDisableConfirmLabelTests : BunitContext
|
||||
return (cut, cut.FindAll("div.modal-footer button").ToList());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the disable confirmation button is labelled Disable rather than Delete.</summary>
|
||||
[Fact]
|
||||
public void Disable_confirm_button_says_disable_not_delete()
|
||||
{
|
||||
@@ -162,6 +165,7 @@ public class TopologyDisableConfirmLabelTests : BunitContext
|
||||
Assert.Contains("Cancel", footer.Select(b => b.TextContent.Trim()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the disable confirmation body names the instance being disabled.</summary>
|
||||
[Fact]
|
||||
public void Disable_confirm_body_names_the_instance()
|
||||
{
|
||||
@@ -169,6 +173,7 @@ public class TopologyDisableConfirmLabelTests : BunitContext
|
||||
Assert.Contains("Disable instance 'Pump-001'?", cut.Find(".modal-body").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the delete confirmation button still says Delete.</summary>
|
||||
[Fact]
|
||||
public void Delete_confirm_button_still_says_delete()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user