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:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// Covers the <c>AlarmStateBadges</c> shared component lifted out of
/// <c>DebugView</c> (M7-T13 prep). The badge cluster must render identically
/// <c>DebugView</c>. The badge cluster must render identically
/// wherever it is hosted, so these tests pin the rendered markup: state badge
/// class, kind label, native sub-state badges, severity, and HiLo level.
/// </summary>
@@ -32,6 +32,7 @@ public class AlarmStateBadgesTests : BunitContext
Severity: 700),
};
/// <summary>Verifies that the state, kind, sub-state, severity and level badges are rendered.</summary>
[Fact]
public void Renders_StateKindSubStateSeverityAndLevel_Badges()
{
@@ -9,7 +9,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="AuditDrilldownDrawer"/> (#23 M7 Bundle C / M7-T4..T8).
/// bUnit tests for <see cref="AuditDrilldownDrawer"/>.
///
/// The drawer is a child component opened from the Audit Log page when a grid row
/// is clicked. It renders the offcanvas chrome (header, open/close) and delegates
@@ -24,6 +24,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// </summary>
public class AuditDrilldownDrawerTests : BunitContext
{
/// <summary>Initializes the test context with loose JS interop and the services the drawer needs.</summary>
public AuditDrilldownDrawerTests()
{
// Default to Loose so the cURL clipboard call does not blow up tests
@@ -70,6 +71,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Extra = extra,
};
/// <summary>Verifies that the drawer renders the occurred-at UTC field.</summary>
[Fact]
public void Drawer_RendersField_OccurredAtUtc()
{
@@ -86,6 +88,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("2026-05-20T12:30:45", cut.Markup);
}
/// <summary>Verifies that a JSON request summary is pretty-printed with indentation.</summary>
[Fact]
public void Drawer_JsonRequestSummary_PrettyPrinted_Indented()
{
@@ -104,6 +107,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Matches(@"\n\s+""b"":\s*""two""", cut.Markup);
}
/// <summary>Verifies that a non-JSON request summary is rendered verbatim.</summary>
[Fact]
public void Drawer_NonJsonRequestSummary_RenderedVerbatim()
{
@@ -118,6 +122,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("not really json {{}", cut.Markup);
}
/// <summary>Verifies that a DbOutbound event renders its SQL block.</summary>
[Fact]
public void Drawer_DbOutboundChannel_RendersSqlBlock()
{
@@ -140,6 +145,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("abc", cut.Markup);
}
/// <summary>Verifies that an API outbound event shows the copy-as-cURL button.</summary>
[Fact]
public void Drawer_ApiOutbound_ShowsCopyAsCurlButton()
{
@@ -152,6 +158,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("data-test=\"copy-as-curl\"", cut.Markup);
}
/// <summary>Verifies that a non-API channel event hides the copy-as-cURL button.</summary>
[Fact]
public void Drawer_NotApiChannel_HidesCopyAsCurlButton()
{
@@ -165,6 +172,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"copy-as-curl\"", cut.Markup);
}
/// <summary>Verifies that a null correlation id hides the show-all button.</summary>
[Fact]
public void Drawer_NullCorrelationId_HidesShowAllButton()
{
@@ -177,6 +185,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"show-all-events\"", cut.Markup);
}
/// <summary>Verifies that a redacted body shows the redaction badge.</summary>
[Fact]
public void Drawer_RedactedBody_ShowsRedactionBadge()
{
@@ -191,6 +200,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("data-test=\"redaction-badge-request\"", cut.Markup);
}
/// <summary>Verifies that a non-redacted body hides the redaction badge.</summary>
[Fact]
public void Drawer_NonRedactedBody_HidesBadge()
{
@@ -203,6 +213,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"redaction-badge-request\"", cut.Markup);
}
/// <summary>Verifies that show-all-for-operation navigates with the correlation id in the query string.</summary>
[Fact]
public void ShowAllForOperation_Navigates_WithCorrelationIdQueryString()
{
@@ -220,6 +231,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains(corr.ToString(), nav.Uri);
}
/// <summary>Verifies that a null execution id hides the view-this-execution button.</summary>
[Fact]
public void Drawer_NullExecutionId_HidesViewThisExecutionButton()
{
@@ -232,6 +244,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"view-this-execution\"", cut.Markup);
}
/// <summary>Verifies that a non-null execution id shows the view-this-execution button.</summary>
[Fact]
public void Drawer_NonNullExecutionId_ShowsViewThisExecutionButton()
{
@@ -244,6 +257,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("data-test=\"view-this-execution\"", cut.Markup);
}
/// <summary>Verifies that view-this-execution navigates with the execution id in the query string.</summary>
[Fact]
public void ViewThisExecution_Navigates_WithExecutionIdQueryString()
{
@@ -260,6 +274,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains($"/audit/log?executionId={exec}", nav.Uri);
}
/// <summary>Verifies that a null parent execution id hides the view-parent-execution button.</summary>
[Fact]
public void Drawer_NullParentExecutionId_HidesViewParentExecutionButton()
{
@@ -272,6 +287,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"view-parent-execution\"", cut.Markup);
}
/// <summary>Verifies that a non-null parent execution id shows the view-parent-execution button.</summary>
[Fact]
public void Drawer_NonNullParentExecutionId_ShowsViewParentExecutionButton()
{
@@ -284,6 +300,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("data-test=\"view-parent-execution\"", cut.Markup);
}
/// <summary>Verifies that view-parent-execution navigates with the parent execution id in the query string.</summary>
[Fact]
public void ViewParentExecution_Navigates_WithExecutionIdQueryString()
{
@@ -303,6 +320,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains($"/audit/log?executionId={parent}", nav.Uri);
}
/// <summary>Verifies that a null execution id hides the view-execution-chain button.</summary>
[Fact]
public void Drawer_NullExecutionId_HidesViewExecutionChainButton()
{
@@ -315,6 +333,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.DoesNotContain("data-test=\"view-execution-chain\"", cut.Markup);
}
/// <summary>Verifies that a non-null execution id shows the view-execution-chain button.</summary>
[Fact]
public void Drawer_NonNullExecutionId_ShowsViewExecutionChainButton()
{
@@ -327,6 +346,7 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains("data-test=\"view-execution-chain\"", cut.Markup);
}
/// <summary>Verifies that view-execution-chain navigates to the execution tree page.</summary>
[Fact]
public void ViewExecutionChain_Navigates_ToExecutionTreePage()
{
@@ -345,6 +365,8 @@ public class AuditDrilldownDrawerTests : BunitContext
Assert.Contains($"/audit/execution-tree?executionId={exec}", nav.Uri);
}
/// <summary>Verifies that copy-as-cURL invokes the clipboard with the cURL command string.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CopyAsCurl_InvokesClipboard_WithCurlString()
{
@@ -10,8 +10,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="AuditEventDetail"/> — the reusable single-row
/// detail body extracted from <see cref="AuditDrilldownDrawer"/> (Task 1 of the
/// Execution-Tree Node Detail Modal feature).
/// detail body extracted from <see cref="AuditDrilldownDrawer"/> for reuse by the
/// Execution-Tree Node Detail Modal.
///
/// These tests render the detail component directly (not via the drawer) and
/// pin the contract the drawer — and any future modal host — relies on:
@@ -22,6 +22,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// </summary>
public class AuditEventDetailTests : BunitContext
{
/// <summary>Initializes the test context with loose JS interop so unrelated tests do not fail on the clipboard call.</summary>
public AuditEventDetailTests()
{
// Loose so the cURL clipboard call does not blow up tests that do not
@@ -67,6 +68,7 @@ public class AuditEventDetailTests : BunitContext
Extra = extra,
};
/// <summary>Verifies that the read-only field block renders the audit event fields.</summary>
[Fact]
public void RendersFieldBlock()
{
@@ -81,6 +83,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("2026-05-20T12:30:45", cut.Markup);
}
/// <summary>Verifies that the source node field renders between the site and instance fields.</summary>
[Fact]
public void RendersSourceNodeField_BetweenSiteAndInstance()
{
@@ -103,6 +106,7 @@ public class AuditEventDetailTests : BunitContext
$"Expected SourceSiteId < SourceNode < SourceInstanceId; got {siteIdx}, {nodeIdx}, {instanceIdx}");
}
/// <summary>Verifies that a null source node renders as a dash.</summary>
[Fact]
public void RendersSourceNodeField_AsDashWhenNull()
{
@@ -119,6 +123,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains(">—<", cut.Markup);
}
/// <summary>Verifies that the error section renders when the event carries an error.</summary>
[Fact]
public void ErrorSection_RendersWhenErrorPresent()
{
@@ -134,6 +139,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("stack trace here", cut.Markup);
}
/// <summary>Verifies that the error section is hidden when the event has no error.</summary>
[Fact]
public void ErrorSection_HiddenWhenNoError()
{
@@ -144,6 +150,7 @@ public class AuditEventDetailTests : BunitContext
Assert.DoesNotContain("data-test=\"section-error\"", cut.Markup);
}
/// <summary>Verifies that the request section pretty-prints a JSON payload.</summary>
[Fact]
public void RequestSection_PrettyPrintsJson()
{
@@ -156,6 +163,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Matches(@"\n\s+""a"":\s*1", cut.Markup);
}
/// <summary>Verifies that the response section renders when a response payload is present.</summary>
[Fact]
public void ResponseSection_RendersWhenPresent()
{
@@ -167,6 +175,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"response-body\"", cut.Markup);
}
/// <summary>Verifies that the extra section renders when extra data is present.</summary>
[Fact]
public void ExtraSection_RendersWhenPresent()
{
@@ -177,6 +186,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"section-extra\"", cut.Markup);
}
/// <summary>Verifies that a redacted body shows the redaction badge.</summary>
[Fact]
public void RedactedBody_ShowsRedactionBadge()
{
@@ -187,6 +197,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"redaction-badge-request\"", cut.Markup);
}
/// <summary>Verifies that a body that was not redacted hides the redaction badge.</summary>
[Fact]
public void NonRedactedBody_HidesRedactionBadge()
{
@@ -197,6 +208,7 @@ public class AuditEventDetailTests : BunitContext
Assert.DoesNotContain("data-test=\"redaction-badge-request\"", cut.Markup);
}
/// <summary>Verifies that a database outbound event renders its SQL block.</summary>
[Fact]
public void DbOutboundChannel_RendersSqlBlock()
{
@@ -210,6 +222,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"sql-parameters\"", cut.Markup);
}
/// <summary>Verifies that an API channel event shows the copy-as-cURL button.</summary>
[Fact]
public void ApiChannel_ShowsCopyAsCurlButton()
{
@@ -220,6 +233,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"copy-as-curl\"", cut.Markup);
}
/// <summary>Verifies that a non-API channel event hides the copy-as-cURL button.</summary>
[Fact]
public void NonApiChannel_HidesCopyAsCurlButton()
{
@@ -230,6 +244,7 @@ public class AuditEventDetailTests : BunitContext
Assert.DoesNotContain("data-test=\"copy-as-curl\"", cut.Markup);
}
/// <summary>Verifies that a null correlation id hides the show-all-for-operation button.</summary>
[Fact]
public void NullCorrelationId_HidesShowAllButton()
{
@@ -240,6 +255,7 @@ public class AuditEventDetailTests : BunitContext
Assert.DoesNotContain("data-test=\"show-all-events\"", cut.Markup);
}
/// <summary>Verifies that a non-null correlation id shows the show-all-for-operation button.</summary>
[Fact]
public void NonNullCorrelationId_ShowsShowAllButton()
{
@@ -250,6 +266,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"show-all-events\"", cut.Markup);
}
/// <summary>Verifies that the execution buttons render only when the matching execution ids are present.</summary>
[Fact]
public void ExecutionButtons_ConditionalOnExecutionIds()
{
@@ -264,6 +281,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains("data-test=\"view-execution-chain\"", cut.Markup);
}
/// <summary>Verifies that show-all-for-operation navigates with the correlation id in the query string.</summary>
[Fact]
public void ShowAllForOperation_Navigates_WithCorrelationIdQueryString()
{
@@ -278,6 +296,7 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains($"/audit/log?correlationId={corr}", nav.Uri);
}
/// <summary>Verifies that view-execution-chain navigates to the execution tree page.</summary>
[Fact]
public void ViewExecutionChain_Navigates_ToExecutionTreePage()
{
@@ -292,6 +311,8 @@ public class AuditEventDetailTests : BunitContext
Assert.Contains($"/audit/execution-tree?executionId={exec}", nav.Uri);
}
/// <summary>Verifies that copy-as-cURL invokes the clipboard with the generated cURL command.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CopyAsCurl_InvokesClipboard_WithCurlString()
{
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="AuditFilterBar"/> (#23 M7-T2 / Bundle B).
/// bUnit tests for <see cref="AuditFilterBar"/>.
///
/// The bar carries the 10 spec filter elements plus the Errors-only toggle.
/// Channel is a single-select <c>&lt;select data-test="filter-channel-select"&gt;</c>;
@@ -33,6 +33,7 @@ public class AuditFilterBarTests : BunitContext
private readonly ISiteRepository _siteRepo;
private readonly IAuditLogQueryService _auditLogQueryService;
/// <summary>Initializes the bUnit context with substitute site and audit query services.</summary>
public AuditFilterBarTests()
{
_siteRepo = Substitute.For<ISiteRepository>();
@@ -44,7 +45,7 @@ public class AuditFilterBarTests : BunitContext
}));
Services.AddSingleton(_siteRepo);
// Task 15: the Node multi-select pulls its options from
// The Node multi-select pulls its options from
// IAuditLogQueryService.GetDistinctSourceNodesAsync. The default stub
// returns the two central nodes the cluster uses; individual tests can
// override via _auditLogQueryService.GetDistinctSourceNodesAsync(...).Returns(...).
@@ -56,6 +57,7 @@ public class AuditFilterBarTests : BunitContext
Services.AddSingleton(_auditLogQueryService);
}
/// <summary>Verifies that the filter bar renders all ten filter elements plus the errors-only toggle.</summary>
[Fact]
public void Render_AllTenElements_Plus_ErrorsOnlyToggle_Present()
{
@@ -86,6 +88,7 @@ public class AuditFilterBarTests : BunitContext
}
}
/// <summary>Verifies that applying the filters raises the filter-changed callback with the selected filters.</summary>
[Fact]
public void Apply_RaisesOnFilterChanged_WithSelectedFilters()
{
@@ -103,6 +106,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Equal("Plant-A-OPC", captured.Target);
}
/// <summary>Verifies that choosing a different channel replaces the previous selection because the channel is single-select.</summary>
[Fact]
public void ChangingChannel_ReplacesTheSelection_SingleSelect()
{
@@ -125,6 +129,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Null(captured!.Channels);
}
/// <summary>Verifies that selecting a channel narrows the available kind options.</summary>
[Fact]
public void Channel_Narrows_Kind_Options_When_Selected()
{
@@ -149,6 +154,7 @@ public class AuditFilterBarTests : BunitContext
Assert.DoesNotContain($"data-test=\"filter-kind-ms-opt-{AuditKind.InboundRequest}\"", cut.Markup);
}
/// <summary>Verifies that the errors-only toggle adds the failed, parked and discarded statuses when no status is selected.</summary>
[Fact]
public void ErrorsOnly_Toggle_Adds_FailedParkedDiscarded_ToStatus_WhenStatusIsEmpty()
{
@@ -161,7 +167,7 @@ public class AuditFilterBarTests : BunitContext
cut.Find("[data-test=\"filter-apply\"]").Click();
Assert.NotNull(captured);
// Task 9: Errors-only targets the full non-success set {Failed, Parked, Discarded}.
// Errors-only targets the full non-success set {Failed, Parked, Discarded}.
Assert.NotNull(captured!.Statuses);
Assert.Equal(3, captured.Statuses!.Count);
Assert.Contains(AuditStatus.Failed, captured.Statuses);
@@ -175,10 +181,11 @@ public class AuditFilterBarTests : BunitContext
Assert.Equal(new[] { AuditStatus.Delivered }, captured!.Statuses);
}
/// <summary>Verifies that the node multi-select renders its options from the query service and maps the selection through to the filter.</summary>
[Fact]
public void NodeMultiSelect_RendersOptions_FromQueryService_AndMapsThroughToFilter()
{
// Task 15: the Node filter pulls its option set from
// The Node filter pulls its option set from
// IAuditLogQueryService.GetDistinctSourceNodesAsync and threads the
// chip selection into AuditLogQueryFilter.SourceNodes.
AuditLogQueryFilter? captured = null;
@@ -199,10 +206,11 @@ public class AuditFilterBarTests : BunitContext
Assert.Equal(new[] { "central-a" }, captured.SourceNodes);
}
/// <summary>Verifies that applying with several status chips selected passes every selected status to the filter.</summary>
[Fact]
public void Apply_WithMultipleStatusChips_PassesAllSelectedStatuses()
{
// Task 9: multiple explicit Status chips all reach the filter — and they
// Multiple explicit Status chips all reach the filter — and they
// win over the Errors-only default.
AuditLogQueryFilter? captured = null;
var cut = Render<AuditFilterBar>(p => p
@@ -219,6 +227,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Contains(AuditStatus.Failed, captured.Statuses);
}
/// <summary>Verifies that a pasted execution id is mapped through to the filter on apply.</summary>
[Fact]
public void Apply_WithPastedExecutionId_MapsThroughToFilter()
{
@@ -236,6 +245,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Equal(executionId, captured!.ExecutionId);
}
/// <summary>Verifies that a blank or unparseable execution id leaves the filter execution id null.</summary>
[Fact]
public void Apply_WithBlankOrUnparseableExecutionId_LeavesFilterExecutionIdNull()
{
@@ -255,6 +265,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Null(captured!.ExecutionId);
}
/// <summary>Verifies that a pasted parent execution id is mapped through to the filter on apply.</summary>
[Fact]
public void Apply_WithPastedParentExecutionId_MapsThroughToFilter()
{
@@ -272,6 +283,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Equal(parentExecutionId, captured!.ParentExecutionId);
}
/// <summary>Verifies that a blank or unparseable parent execution id leaves the filter parent execution id null.</summary>
[Fact]
public void Apply_WithBlankOrUnparseableParentExecutionId_LeavesFilterParentExecutionIdNull()
{
@@ -291,6 +303,7 @@ public class AuditFilterBarTests : BunitContext
Assert.Null(captured!.ParentExecutionId);
}
/// <summary>Verifies that choosing the last-hour time range sets the from-UTC value to approximately one hour ago.</summary>
[Fact]
public void TimeRange_LastHour_PopulatesFromUtc_ApproxOneHourAgo()
{
@@ -10,7 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="AuditResultsGrid"/> (#23 M7-T3 / Bundle B). The grid
/// bUnit tests for <see cref="AuditResultsGrid"/>. The grid
/// renders 10 columns, paginates via keyset (passing the last row's
/// (OccurredAtUtc, EventId) back to the service), raises a row-click callback
/// that Bundle C wires to the drilldown drawer, and styles non-success status
@@ -39,6 +39,7 @@ public class AuditResultsGridTests : BunitContext
ErrorMessage = status == AuditStatus.Failed ? "boom — unreachable" : null,
};
/// <summary>Initializes a new instance of the <see cref="AuditResultsGridTests"/> class.</summary>
public AuditResultsGridTests()
{
_service = Substitute.For<IAuditLogQueryService>();
@@ -62,6 +63,7 @@ public class AuditResultsGridTests : BunitContext
});
}
/// <summary>Verifies the case: render, ten Columns, from Stub Service.</summary>
[Fact]
public void Render_TenColumns_FromStubService()
{
@@ -84,6 +86,7 @@ public class AuditResultsGridTests : BunitContext
}
}
/// <summary>Verifies the case: click, next Page, calls Service, with Cursor, of Last Row.</summary>
[Fact]
public void Click_NextPage_CallsService_WithCursor_OfLastRow()
{
@@ -105,6 +108,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Equal(second.EventId, nextCall.Paging.AfterEventId);
}
/// <summary>Verifies the case: click, row, raises On Row Selected.</summary>
[Fact]
public void Click_Row_RaisesOnRowSelected()
{
@@ -122,10 +126,11 @@ public class AuditResultsGridTests : BunitContext
Assert.Equal(target.EventId, captured!.EventId);
}
/// <summary>Verifies the case: render, includes Node Column, between Site And Channel.</summary>
[Fact]
public void Render_IncludesNodeColumn_BetweenSiteAndChannel()
{
// Task 15: the grid surfaces SourceNode in a dedicated "Node" column
// The grid surfaces SourceNode in a dedicated "Node" column
// positioned between Site and Channel.
StubPage(new List<AuditEventView>
{
@@ -144,6 +149,7 @@ public class AuditResultsGridTests : BunitContext
Assert.True(nodeIdx < channelIdx, "Node column must precede Channel.");
}
/// <summary>Verifies the case: render, includes Execution Id Column.</summary>
[Fact]
public void Render_IncludesExecutionIdColumn()
{
@@ -158,6 +164,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Contains("data-test=\"col-header-ExecutionId\"", cut.Markup);
}
/// <summary>Verifies the case: execution Id, non Null Row, renders Short Monospace Value.</summary>
[Fact]
public void ExecutionId_NonNullRow_RendersShortMonospaceValue()
{
@@ -175,6 +182,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Equal(executionId.ToString(), cell.GetAttribute("title"));
}
/// <summary>Verifies the case: execution Id, null Row, renders Blank Placeholder, no Execution Id Cell.</summary>
[Fact]
public void ExecutionId_NullRow_RendersBlankPlaceholder_NoExecutionIdCell()
{
@@ -187,6 +195,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Empty(cut.FindAll($"[data-test=\"execution-id-{row.EventId}\"]"));
}
/// <summary>Verifies the case: render, includes Parent Execution Id Column.</summary>
[Fact]
public void Render_IncludesParentExecutionIdColumn()
{
@@ -201,6 +210,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Contains("data-test=\"col-header-ParentExecutionId\"", cut.Markup);
}
/// <summary>Verifies the case: parent Execution Id, non Null Row, renders Short Monospace Value.</summary>
[Fact]
public void ParentExecutionId_NonNullRow_RendersShortMonospaceValue()
{
@@ -218,6 +228,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Equal(parentExecutionId.ToString(), cell.GetAttribute("title"));
}
/// <summary>Verifies the case: parent Execution Id, null Row, renders Blank Placeholder, no Parent Execution Id Cell.</summary>
[Fact]
public void ParentExecutionId_NullRow_RendersBlankPlaceholder_NoParentExecutionIdCell()
{
@@ -230,6 +241,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Empty(cut.FindAll($"[data-test=\"parent-execution-id-{row.EventId}\"]"));
}
/// <summary>Verifies the case: status, failed Row, has Error Badge Class.</summary>
[Fact]
public void Status_FailedRow_HasErrorBadgeClass()
{
@@ -247,7 +259,7 @@ public class AuditResultsGridTests : BunitContext
Assert.Contains("bg-success", deliveredBadge.GetAttribute("class") ?? string.Empty);
}
// --- column resize + reorder UX (#23 follow-ups Task 10) ---------------
// --- column resize + reorder UX ---------------
//
// The drag interaction itself is browser-side (audit-grid.js) and covered
// by the Playwright suite. The bUnit tests below exercise the .NET-side
@@ -265,6 +277,7 @@ public class AuditResultsGridTests : BunitContext
private static int HeaderIndex(string markup, string key)
=> markup.IndexOf($"data-col-key=\"{key}\"", StringComparison.Ordinal);
/// <summary>Verifies the case: headers, render Resize Handle And Drag Key, for Every Column.</summary>
[Fact]
public void Headers_RenderResizeHandleAndDragKey_ForEveryColumn()
{
@@ -280,6 +293,7 @@ public class AuditResultsGridTests : BunitContext
}
}
/// <summary>Verifies the case: column Order Parameter, drives Header Order.</summary>
[Fact]
public void ColumnOrderParameter_DrivesHeaderOrder()
{
@@ -300,6 +314,8 @@ public class AuditResultsGridTests : BunitContext
}
}
/// <summary>Verifies the case: on Column Reordered, moves Column Into Target Slot, and Persists.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OnColumnReordered_MovesColumnIntoTargetSlot_AndPersists()
{
@@ -318,6 +334,8 @@ public class AuditResultsGridTests : BunitContext
Assert.Contains("Status", (string)save.Arguments[1]!);
}
/// <summary>Verifies the case: on Column Resized, below Minimum, clamps To64px, and Persists.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OnColumnResized_BelowMinimum_ClampsTo64px_AndPersists()
{
@@ -335,6 +353,7 @@ public class AuditResultsGridTests : BunitContext
i => i.Identifier == "auditGrid.save" && (string)i.Arguments[0]! == "columnWidths");
}
/// <summary>Verifies the case: stored Order, with Unknown Key, degrades Gracefully.</summary>
[Fact]
public void StoredOrder_WithUnknownKey_DegradesGracefully()
{
@@ -360,6 +379,7 @@ public class AuditResultsGridTests : BunitContext
}
}
/// <summary>Verifies the case: stored Widths, for Unknown Column, are Ignored.</summary>
[Fact]
public void StoredWidths_ForUnknownColumn_AreIgnored()
{
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="ExecutionDetailModal"/> (Execution-Tree Node Detail
/// Modal, Task 3). The modal opens on an execution-tree node double-click: given
/// Modal). The modal opens on an execution-tree node double-click: given
/// an <c>ExecutionId</c> it loads that execution's audit rows via
/// <see cref="IAuditLogQueryService"/> and shows a list → per-row detail.
///
@@ -24,6 +24,7 @@ public class ExecutionDetailModalTests : BunitContext
private readonly IAuditLogQueryService _service;
private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new();
/// <summary>Initializes a new instance of the <c>ExecutionDetailModalTests</c> class.</summary>
public ExecutionDetailModalTests()
{
_service = Substitute.For<IAuditLogQueryService>();
@@ -66,6 +67,7 @@ public class ExecutionDetailModalTests : BunitContext
});
}
/// <summary>Verifies that closed modal renders nothing and does not query.</summary>
[Fact]
public void ClosedModal_RendersNothing_AndDoesNotQuery()
{
@@ -79,6 +81,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Empty(_calls);
}
/// <summary>Verifies that open transition queries by execution id with page size 100.</summary>
[Fact]
public void OpenTransition_QueriesByExecutionId_WithPageSize100()
{
@@ -101,6 +104,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Equal(100, _calls[0].Paging!.PageSize);
}
/// <summary>Verifies that still open non open parameter change does not requery.</summary>
[Fact]
public void StillOpen_NonOpenParameterChange_DoesNotRequery()
{
@@ -119,6 +123,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Single(_calls);
}
/// <summary>Verifies that multi row renders list view with one button per row.</summary>
[Fact]
public void MultiRow_RendersListView_WithOneButtonPerRow()
{
@@ -138,6 +143,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=\"drawer-fields\"]"));
}
/// <summary>Verifies that multi row click row shows audit event detail.</summary>
[Fact]
public void MultiRow_ClickRow_ShowsAuditEventDetail()
{
@@ -158,6 +164,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.NotNull(cut.Find("[data-test=\"execution-detail-back\"]"));
}
/// <summary>Verifies that multi row back control returns to list.</summary>
[Fact]
public void MultiRow_BackControl_ReturnsToList()
{
@@ -180,6 +187,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=\"drawer-fields\"]"));
}
/// <summary>Verifies that single row opens straight to detail no back control.</summary>
[Fact]
public void SingleRow_OpensStraightToDetail_NoBackControl()
{
@@ -197,6 +205,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=\"execution-detail-back\"]"));
}
/// <summary>Verifies that zero row shows friendly empty state.</summary>
[Fact]
public void ZeroRow_ShowsFriendlyEmptyState()
{
@@ -211,6 +220,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Contains("This execution emitted no audit rows.", empty.TextContent);
}
/// <summary>Verifies that query throws shows inline error state does not rethrow.</summary>
[Fact]
public void QueryThrows_ShowsInlineErrorState_DoesNotRethrow()
{
@@ -228,6 +238,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.Contains("db is down", error.TextContent);
}
/// <summary>Verifies that close button raises on close.</summary>
[Fact]
public void CloseButton_RaisesOnClose()
{
@@ -245,6 +256,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.True(closed);
}
/// <summary>Verifies that backdrop click raises on close.</summary>
[Fact]
public void BackdropClick_RaisesOnClose()
{
@@ -262,6 +274,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.True(closed);
}
/// <summary>Verifies that escape key raises on close.</summary>
[Fact]
public void EscapeKey_RaisesOnClose()
{
@@ -279,6 +292,7 @@ public class ExecutionDetailModalTests : BunitContext
Assert.True(closed);
}
/// <summary>Verifies that header shows short execution id and row count.</summary>
[Fact]
public void Header_ShowsShortExecutionId_AndRowCount()
{
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
/// <summary>
/// bUnit tests for <see cref="ExecutionTree"/> (Audit Log ParentExecutionId
/// feature, Task 10). The component takes the FLAT
/// feature). The component takes the FLAT
/// <see cref="ExecutionTreeNode"/> list the repository returns, assembles it
/// into a tree by joining <see cref="ExecutionTreeNode.ParentExecutionId"/> to a
/// parent node's <see cref="ExecutionTreeNode.ExecutionId"/>, and renders it
@@ -35,6 +35,7 @@ public class ExecutionTreeTests : BunitContext
rowCount == 0 ? null : new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
rowCount == 0 ? null : new DateTime(2026, 5, 20, 12, 0, 5, DateTimeKind.Utc));
/// <summary>Verifies that a single node renders as one tree node.</summary>
[Fact]
public void SingleNode_RendersOneTreeNode()
{
@@ -48,6 +49,7 @@ public class ExecutionTreeTests : BunitContext
Assert.Contains($"data-test=\"tree-node-{id}\"", cut.Markup);
}
/// <summary>Verifies that a multi-level tree is assembled from the flat node list.</summary>
[Fact]
public void MultiLevel_AssemblesTree_FromFlatList()
{
@@ -82,6 +84,7 @@ public class ExecutionTreeTests : BunitContext
Assert.True(childIdx < grandIdx, "child must render before grandchild");
}
/// <summary>Verifies that a stub node renders with a stub marker.</summary>
[Fact]
public void StubNode_RendersStubMarker()
{
@@ -104,6 +107,7 @@ public class ExecutionTreeTests : BunitContext
Assert.Contains("no audited actions", cut.Markup);
}
/// <summary>Verifies that the node the user arrived from is visually highlighted.</summary>
[Fact]
public void ArrivedFromNode_IsVisuallyHighlighted()
{
@@ -128,6 +132,7 @@ public class ExecutionTreeTests : BunitContext
Assert.DoesNotContain("execution-tree-node--current", other.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that each node link points to the audit log filtered by that node's execution.</summary>
[Fact]
public void NodeLink_PointsTo_AuditLogFilteredByThatExecution()
{
@@ -154,6 +159,7 @@ public class ExecutionTreeTests : BunitContext
Assert.Equal($"/audit/log?executionId={root}", rootLink.GetAttribute("href"));
}
/// <summary>Verifies that an empty node list renders nothing and does not throw.</summary>
[Fact]
public void EmptyNodeList_RendersNothingWithoutThrowing()
{
@@ -164,6 +170,7 @@ public class ExecutionTreeTests : BunitContext
Assert.DoesNotContain("data-test=\"tree-node-", cut.Markup);
}
/// <summary>Verifies that a cyclic flat list is handled without looping forever.</summary>
[Fact]
public void CyclicFlatList_TerminatesWithoutInfiniteLoop()
{
@@ -186,6 +193,7 @@ public class ExecutionTreeTests : BunitContext
Assert.Equal(1, CountOccurrences(cut.Markup, $"data-test=\"tree-node-{b}\""));
}
/// <summary>Verifies that toggling a node collapses its child subtree and toggling again re-expands it.</summary>
[Fact]
public void ToggleExpand_CollapsesAndReExpandsChildSubtree()
{
@@ -229,6 +237,7 @@ public class ExecutionTreeTests : BunitContext
cut.Find($"[data-test=\"tree-toggle-{root}\"]").GetAttribute("aria-expanded"));
}
/// <summary>Verifies that double-clicking a node raises <c>OnNodeActivated</c> with that node's execution id.</summary>
[Fact]
public void DoubleClickingNode_RaisesOnNodeActivated_WithExecutionId()
{
@@ -255,6 +264,7 @@ public class ExecutionTreeTests : BunitContext
Assert.Equal(root, activated);
}
/// <summary>Verifies that double-clicking a nested node bubbles the activation up to the root callback.</summary>
[Fact]
public void DoubleClickingNestedNode_BubblesOnNodeActivated_ToRoot()
{
@@ -16,7 +16,7 @@ using ConnectionCertificates =
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// Covers the M7-B10 (T17) connection-certificates management page: an
/// Covers the connection-certificates management page: an
/// Administrator-gated page that resolves a data connection's owning site, lists
/// the site's trusted-peer / rejected certificates via
/// <see cref="ICertManagementService.ListAsync"/>, and removes a certificate via
@@ -31,6 +31,7 @@ public class ConnectionCertificatesTests : BunitContext
private const int ConnectionId = 7;
private const string SiteIdentifier = "plant-a";
/// <summary>Initializes the test context and registers the services the page requires.</summary>
public ConnectionCertificatesTests()
{
Services.AddSingleton(_certs);
@@ -84,6 +85,7 @@ public class ConnectionCertificatesTests : BunitContext
return host.FindComponent<ConnectionCertificates>();
}
/// <summary>Verifies that the page lists both certificates from the owning site.</summary>
[Fact]
public void Lists_Two_Certs_From_Owning_Site()
{
@@ -105,6 +107,7 @@ public class ConnectionCertificatesTests : BunitContext
_certs.Received(1).ListAsync(SiteIdentifier, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that removing a certificate calls RemoveAsync with the site identifier and thumbprint.</summary>
[Fact]
public void Remove_Calls_RemoveAsync_With_SiteIdentifier_And_Thumbprint()
{
@@ -119,6 +122,7 @@ public class ConnectionCertificatesTests : BunitContext
_certs.Received(1).RemoveAsync(SiteIdentifier, "AAAA1111", Arg.Any<CancellationToken>());
}
/// <summary>Verifies that an empty certificate store shows the empty-state note.</summary>
[Fact]
public void Empty_Store_Shows_Empty_Note()
{
@@ -131,6 +135,7 @@ public class ConnectionCertificatesTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=cert-row]"));
}
/// <summary>Verifies that a list failure shows a load error.</summary>
[Fact]
public void List_Failure_Shows_Load_Error()
{
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Health;
/// <summary>
/// bUnit tests for <see cref="AuditKpiTiles"/> (#23 M7 Bundle E / M7-T13). The
/// bUnit tests for <see cref="AuditKpiTiles"/>. The
/// component renders three Bootstrap-card tiles — Volume, Error Rate, Backlog —
/// from a single <see cref="AuditLogKpiSnapshot"/>. The tests pin:
///
@@ -25,6 +25,7 @@ public class AuditKpiTilesTests : BunitContext
private static AuditLogKpiSnapshot MakeSnapshot(long total, long errors, long backlog) =>
new(total, errors, backlog, new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc));
/// <summary>Verifies that three tiles are rendered from the snapshot.</summary>
[Fact]
public void Renders_ThreeTiles_FromSnapshot()
{
@@ -43,6 +44,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.Contains("7", cut.Markup); // backlog
}
/// <summary>Verifies that the error rate is computed from the total and error counts.</summary>
[Fact]
public void ErrorRate_Computed_From_Total_AndErrors()
{
@@ -54,6 +56,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.Contains("5.0%", cut.Markup);
}
/// <summary>Verifies that zero events do not cause a division by zero and render a zero percent error rate.</summary>
[Fact]
public void ZeroEvents_DoesNotDivideByZero_RendersZeroPercent()
{
@@ -70,6 +73,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.Contains("data-test=\"audit-kpi-volume\"", cut.Markup);
}
/// <summary>Verifies that an unavailable snapshot renders em dashes and an error message.</summary>
[Fact]
public void UnavailableSnapshot_RendersEmDashes_AndErrorMessage()
{
@@ -85,6 +89,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.Contains("DB connection refused", cut.Markup);
}
/// <summary>Verifies that clicking the error rate tile navigates to the audit log filtered by failed status.</summary>
[Fact]
public void ErrorRateTile_Click_NavigatesToAuditLog_WithFailedStatusFilter()
{
@@ -102,6 +107,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.Contains("/audit/log?status=Failed", nav.Uri);
}
/// <summary>Verifies that clicking the volume tile navigates to the unfiltered audit log.</summary>
[Fact]
public void VolumeTile_Click_NavigatesToUnfilteredAuditLog()
{
@@ -117,6 +123,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.EndsWith("/audit/log", nav.Uri);
}
/// <summary>Verifies that clicking the backlog tile navigates to the audit log.</summary>
[Fact]
public void BacklogTile_Click_NavigatesToAuditLog()
{
@@ -131,6 +138,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.EndsWith("/audit/log", nav.Uri);
}
/// <summary>Verifies that a nonzero error rate below ten percent gets a warning border rather than a danger border.</summary>
[Fact]
public void NonzeroErrorRate_GetsWarningBorder_NotDangerBelowTenPercent()
{
@@ -144,6 +152,7 @@ public class AuditKpiTilesTests : BunitContext
Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that a high error rate gets a danger border.</summary>
[Fact]
public void HighErrorRate_GetsDangerBorder()
{
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Health;
/// <summary>
/// bUnit tests for <see cref="SiteCallKpiTiles"/> (Site Call Audit #22, Task 7).
/// bUnit tests for <see cref="SiteCallKpiTiles"/> .
/// The component renders three Bootstrap-card tiles — Buffered, Stuck, Parked —
/// from a single <see cref="SiteCallKpiResponse"/> snapshot. The tests pin:
///
@@ -35,6 +35,7 @@ public class SiteCallKpiTilesTests : BunitContext
OldestPendingAge: null,
StuckCount: stuck);
/// <summary>Verifies that the component renders three tiles from a snapshot.</summary>
[Fact]
public void Renders_ThreeTiles_FromSnapshot()
{
@@ -54,6 +55,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.Contains(">3<", cut.Markup); // parked
}
/// <summary>Verifies that an unavailable snapshot renders em dashes and the error message.</summary>
[Fact]
public void UnavailableSnapshot_RendersEmDashes_AndErrorMessage()
{
@@ -69,6 +71,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.Contains("site call repository unavailable", cut.Markup);
}
/// <summary>Verifies that the parked tile gets a danger border when the parked count is above zero.</summary>
[Fact]
public void ParkedTile_GetsDangerBorder_WhenParkedAboveZero()
{
@@ -80,6 +83,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.Contains("border-danger", tile.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that the parked tile has no danger border when the parked count is zero.</summary>
[Fact]
public void ParkedTile_NoDangerBorder_WhenParkedZero()
{
@@ -91,6 +95,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that the stuck tile gets a warning border when the stuck count is above zero.</summary>
[Fact]
public void StuckTile_GetsWarningBorder_WhenStuckAboveZero()
{
@@ -104,6 +109,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that the stuck tile has no warning border when the stuck count is zero.</summary>
[Fact]
public void StuckTile_NoWarningBorder_WhenStuckZero()
{
@@ -115,6 +121,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.DoesNotContain("border-warning", tile.GetAttribute("class") ?? string.Empty);
}
/// <summary>Verifies that the buffered tile never gets a threshold border, even with a high count.</summary>
[Fact]
public void BufferedTile_HasNoThresholdBorder_EvenWithHighCount()
{
@@ -130,6 +137,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.DoesNotContain("border-warning", cls);
}
/// <summary>Verifies that clicking the buffered tile navigates to the unfiltered site calls report.</summary>
[Fact]
public void BufferedTile_Click_NavigatesToUnfilteredSiteCallsReport()
{
@@ -145,6 +153,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.EndsWith("/site-calls/report", nav.Uri);
}
/// <summary>Verifies that clicking the stuck tile navigates to the site calls report with the stuck filter.</summary>
[Fact]
public void StuckTile_Click_NavigatesToSiteCallsReport_WithStuckFilter()
{
@@ -160,6 +169,7 @@ public class SiteCallKpiTilesTests : BunitContext
Assert.Contains("/site-calls/report?stuck=true", nav.Uri);
}
/// <summary>Verifies that clicking the parked tile navigates to the site calls report with the parked status filter.</summary>
[Fact]
public void ParkedTile_Click_NavigatesToSiteCallsReport_WithParkedStatusFilter()
{
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// M7-T16: the Instance Configure page accepts a CSV of attribute overrides via
/// The Instance Configure page accepts a CSV of attribute overrides via
/// an <c>&lt;InputFile&gt;</c>, parses it with the shared
/// <see cref="OverrideCsvParser"/>, validates each row against the instance's
/// overridable attributes (name must exist + not be template-locked, value must be
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <para>
/// <c>InstanceConfigure</c> is a heavyweight page (≈7 injected services incl.
/// <c>InstanceService</c> + the flattening pipeline), so — consistent with the
/// MV-14 List-override and native-alarm coverage — the parse→validate→build-dict
/// list-override and native-alarm coverage — the parse→validate→build-dict
/// core is extracted to an <c>internal static</c> helper exercised directly here,
/// plus structural assertions over the component source that pin the InputFile +
/// reuse-the-existing-submit-path wiring.
@@ -49,6 +49,7 @@ public class InstanceConfigureCsvImportTests
// ── Core: valid CSV → override dict, no errors ──────────────────────────
/// <summary>Verifies that a valid CSV builds the override dictionary with no errors.</summary>
[Fact]
public void ValidCsv_BuildsOverrideDict_WithNoErrors()
{
@@ -64,6 +65,7 @@ public class InstanceConfigureCsvImportTests
Assert.Equal("Pump-A", outcome.Overrides["Name"]);
}
/// <summary>Verifies that a valid list-valued CSV row builds canonical JSON that round-trips through the value codec.</summary>
[Fact]
public void ValidListCsv_BuildsCanonicalJson_PerCodecRoundTrip()
{
@@ -81,6 +83,7 @@ public class InstanceConfigureCsvImportTests
// ── Core: bad rows → errors, NO dict (all-or-nothing) ───────────────────
/// <summary>Verifies that a row naming an unknown attribute produces an error and applies nothing.</summary>
[Fact]
public void UnknownAttribute_ProducesError_AndAppliesNothing()
{
@@ -94,6 +97,7 @@ public class InstanceConfigureCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("DoesNotExist") && e.Contains("Line 2"));
}
/// <summary>Verifies that a template-locked attribute is rejected, matching the manual editor.</summary>
[Fact]
public void TemplateLockedAttribute_IsRejected_LikeTheEditor()
{
@@ -109,6 +113,7 @@ public class InstanceConfigureCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("SerialNumber"));
}
/// <summary>Verifies that a type-incompatible value produces an error and applies nothing.</summary>
[Fact]
public void TypeIncompatibleValue_ProducesError_AndAppliesNothing()
{
@@ -123,6 +128,7 @@ public class InstanceConfigureCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("Setpoint") && e.Contains("Line 2"));
}
/// <summary>Verifies that malformed list JSON produces an error and applies nothing.</summary>
[Fact]
public void MalformedListJson_ProducesError_AndAppliesNothing()
{
@@ -136,6 +142,7 @@ public class InstanceConfigureCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("Counts"));
}
/// <summary>Verifies that parser errors propagate through the helper and nothing is applied.</summary>
[Fact]
public void ParserErrors_PropagateThrough_AndApplyNothing()
{
@@ -153,6 +160,7 @@ public class InstanceConfigureCsvImportTests
// ── Structural: InputFile + reuse-the-existing-submit-path wiring ───────
/// <summary>Verifies that the page source wires up the CSV input file control with its test hooks.</summary>
[Fact]
public void Page_WiresCsvInputFile_WithTestHooks()
{
@@ -165,6 +173,7 @@ public class InstanceConfigureCsvImportTests
Assert.Contains("OverrideCsvParser.Parse", markup);
}
/// <summary>Verifies that the page applies the imported overrides through the existing set-attribute-override path.</summary>
[Fact]
public void Page_AppliesViaExistingSetAttributeOverridePath()
{
@@ -55,6 +55,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
// ── Core: valid CSV → rows, no errors ───────────────────────────────────
/// <summary>Verifies that a valid CSV builds retarget rows with no errors.</summary>
[Fact]
public void ValidCsv_BuildsRetargetRows_WithNoErrors()
{
@@ -83,6 +84,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.Null(pump.Filter);
}
/// <summary>Verifies that a row with all fields blank is marked as a clear.</summary>
[Fact]
public void AllBlankRow_IsMarkedAsClear()
{
@@ -98,6 +100,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.True(row.IsClear);
}
/// <summary>Verifies that sources absent from the file are left untouched because the import merges rather than replaces.</summary>
[Fact]
public void SourcesAbsentFromTheFile_AreUntouched_MergeNotReplace()
{
@@ -114,6 +117,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
// ── Core: bad rows → errors, NO rows (all-or-nothing) ───────────────────
/// <summary>Verifies that an unknown source produces an error and nothing is applied.</summary>
[Fact]
public void UnknownSource_ProducesError_AndAppliesNothing()
{
@@ -129,6 +133,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("DoesNotExist") && e.Contains("Line 2"));
}
/// <summary>Verifies that a template-locked source is rejected, matching the server's behavior.</summary>
[Fact]
public void TemplateLockedSource_IsRejected_LikeTheServer()
{
@@ -142,6 +147,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("LockedAlarms") && e.Contains("locked"));
}
/// <summary>Verifies that a duplicate source is rejected and nothing is applied.</summary>
[Fact]
public void DuplicateSource_IsRejected_AndAppliesNothing()
{
@@ -157,6 +163,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.Contains(outcome.Errors, e => e.Contains("BoilerAlarms") && e.Contains("more than once"));
}
/// <summary>Verifies that parser errors propagate through and nothing is applied.</summary>
[Fact]
public void ParserErrors_PropagateThrough_AndApplyNothing()
{
@@ -174,6 +181,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
// ── Structural: InputFile + reuse-the-existing-save-path wiring ─────────
/// <summary>Verifies that the page wires the native-alarm-source CSV input file with its test hooks.</summary>
[Fact]
public void Page_WiresNativeAlarmSourceCsvInputFile_WithTestHooks()
{
@@ -186,6 +194,7 @@ public class InstanceConfigureNativeAlarmCsvImportTests
Assert.Contains("BuildNativeAlarmSourceCsvImport", markup);
}
/// <summary>Verifies that the import applies changes through the existing repository upsert path.</summary>
[Fact]
public void Import_AppliesViaTheExistingRepositoryUpsertPath()
{
@@ -10,7 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// Covers the M7-B6 (T15/T16) additions to <c>NodeBrowserDialog</c>: the
/// Covers the search additions to <c>NodeBrowserDialog</c>: the
/// address-space search box that renders <see cref="IBrowseService.SearchAsync"/>
/// matches as a flat selectable list, and that selecting a search result feeds
/// the SAME selection mechanism the tree uses (so the dialog's existing
@@ -20,6 +20,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
{
private readonly IBrowseService _browse = Substitute.For<IBrowseService>();
/// <summary>Initializes the test context with a substituted browse service that returns an empty root.</summary>
public NodeBrowserDialogSearchTests()
{
Services.AddSingleton(_browse);
@@ -63,6 +64,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
private Func<string?> _capturedSelection = () => null;
/// <summary>Verifies that search matches render as rows carrying the expected data-test hooks.</summary>
[Fact]
public void Search_RendersMatchRows_WithDataTestHooks()
{
@@ -84,6 +86,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
Assert.Contains("Double", cut.Markup);
}
/// <summary>Verifies that clicking a search result raises the selection callback with the node id.</summary>
[Fact]
public void ClickingSearchResult_RaisesSelectionCallback_WithNodeId()
{
@@ -106,6 +109,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
Assert.Equal("ns=2;s=Pump1.Speed", _capturedSelection());
}
/// <summary>Verifies that submitting a blank query clears the displayed search results.</summary>
[Fact]
public void BlankQuery_ClearsResults()
{
@@ -125,6 +129,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=node-search-result]"));
}
/// <summary>Verifies that a blank query after a failed search clears the stale failure alert.</summary>
[Fact]
public void BlankQueryAfterFailure_ClearsStaleFailureAlert()
{
@@ -13,6 +13,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
{
private readonly IBrowseService _browse = Substitute.For<IBrowseService>();
/// <summary>Initializes the test context with a substituted browse service.</summary>
public NodeBrowserDialogSelectionTests()
{
Services.AddSingleton(_browse);
@@ -36,6 +37,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
CapReached: false, Failure: null));
}
/// <summary>Verifies that confirming a selection emits the full node including its data type through the node-selected callback.</summary>
[Fact]
public void Confirm_emits_full_node_with_datatype_on_OnNodeSelected()
{
@@ -57,6 +59,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
Assert.Equal("Double", captured.DataType);
}
/// <summary>Verifies that setting ShowSearch to false hides the search box.</summary>
[Fact]
public void ShowSearch_false_hides_search_box()
{
@@ -74,6 +77,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
// ONLY user-reachable selection path in production is the tree (Select(TreeNode)
// → _selectedNode = node.Source). This guards that path end-to-end: a tree-node
// click must carry the node's DataType through Confirm → OnNodeSelected.
/// <summary>Verifies that selecting a node in the tree emits the full node including its data type through the node-selected callback.</summary>
[Fact]
public void TreeSelect_emits_full_node_with_datatype_on_OnNodeSelected()
{
@@ -107,6 +111,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
// SelectableNodeClasses (design 2026-08-21): the create page needs to pick a
// module's CONTAINER object, not just a leaf tag. Default stays Variable-only
// so every existing caller is unaffected.
/// <summary>Verifies that an Object node is not selectable by default.</summary>
[Fact]
public void Object_node_is_not_selectable_by_default()
{
@@ -122,6 +127,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
Assert.Single(cut.FindAll("[data-test=node-search-result] button.btn-link"));
}
/// <summary>Verifies that an Object node is selectable when its class is listed as selectable.</summary>
[Fact]
public void Object_node_is_selectable_when_listed()
{
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// Covers the M7-B8 (T17) "Verify endpoint" affordance added to
/// Covers the "Verify endpoint" affordance added to
/// <see cref="OpcUaEndpointEditor"/>: a button that calls the injected
/// <see cref="IEndpointVerificationService"/> with the current endpoint config and
/// renders the typed <see cref="VerifyEndpointResult"/> — green on success, a red
@@ -21,7 +21,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <see cref="VerifyFailureKind.UntrustedCertificate"/> with a captured cert.
///
/// <para>
/// Also covers the M7-B10 (T17) "Trust certificate" affordance on the untrusted-cert
/// Also covers the "Trust certificate" affordance on the untrusted-cert
/// panel: an Administrator-gated button that calls
/// <see cref="ICertManagementService.TrustAsync"/> and re-runs Verify on success.
/// </para>
@@ -34,6 +34,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
private readonly ICertManagementService _certs =
Substitute.For<ICertManagementService>();
/// <summary>Registers the substitute verification and certificate-management services with the test container.</summary>
public OpcUaEndpointVerifyTests()
{
Services.AddSingleton(_verify);
@@ -86,6 +87,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
return host.FindComponent<OpcUaEndpointEditor>();
}
/// <summary>Verifies that a successful endpoint verification shows a success message.</summary>
[Fact]
public void Verify_Success_ShowsSuccessMessage()
{
@@ -102,6 +104,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
Assert.Empty(cut.FindAll("[data-test=verify-cert-panel]"));
}
/// <summary>Verifies that an untrusted server certificate shows the certificate panel with its thumbprint.</summary>
[Fact]
public void Verify_UntrustedCertificate_ShowsCertPanelWithThumbprint()
{
@@ -128,6 +131,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
Assert.NotEmpty(cut.FindAll("[data-test=verify-failure]"));
}
/// <summary>Verifies that a generic verification failure shows a failure message.</summary>
[Fact]
public void Verify_GenericFailure_ShowsFailureMessage()
{
@@ -161,6 +165,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
.Returns(new VerifyEndpointResult(
false, VerifyFailureKind.UntrustedCertificate, "Untrusted certificate.", cert));
/// <summary>Verifies that an administrator sees the trust button and that clicking it calls the trust operation.</summary>
[Fact]
public void TrustButton_Admin_SeesButton_AndClick_CallsTrustAsync()
{
@@ -183,6 +188,7 @@ public class OpcUaEndpointVerifyTests : BunitContext
"plant-a", "PLC-OPC", cert.DerBase64, cert.Thumbprint, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a user with only the Designer role does not see the trust button.</summary>
[Fact]
public void TrustButton_NonAdmin_DesignerOnly_DoesNotSeeButton()
{
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
public class SecuredWriteDataTypeMapperTests
{
/// <summary>Verifies that known Galaxy data type names map to the expected data type.</summary>
/// <param name="galaxy">The Galaxy data type name.</param>
/// <param name="expected">The expected mapped data type.</param>
[Theory]
[InlineData("Boolean", DataType.Boolean)]
[InlineData("bool", DataType.Boolean)]
@@ -22,6 +25,8 @@ public class SecuredWriteDataTypeMapperTests
Assert.Equal(expected, dt);
}
/// <summary>Verifies that unknown, null or blank Galaxy type names fail to map.</summary>
/// <param name="galaxy">The Galaxy data type name, possibly null or blank.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -17,7 +17,7 @@ using SecuredWritesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Opera
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;
/// <summary>
/// bUnit rendering + interaction tests for the Secured Writes page (M7 T14b / C5).
/// bUnit rendering + interaction tests for the Secured Writes page.
/// The page is the UI for the two-person MxGateway write workflow: an Operator submits,
/// a different Verifier approves/rejects, and everyone sees the history. The page only
/// SUBMITS commands through <see cref="ISecuredWriteService"/> — the server enforces
@@ -37,6 +37,7 @@ public class SecuredWritesTests : BunitContext
// even though these tests never drive the browser.
private readonly IBrowseService _browse = Substitute.For<IBrowseService>();
/// <summary>Registers the substitute services and default stubs the Secured Writes page needs when it renders.</summary>
public SecuredWritesTests()
{
Services.AddSingleton(_service);
@@ -48,7 +49,7 @@ public class SecuredWritesTests : BunitContext
// BOTH overloads must be stubbed: Reject takes the bool form, Approve takes the
// DialogSeverity form (register row 75). An unstubbed NSubstitute call returns a
// null Task, so missing one here surfaces as an NRE inside the click handler
// rather than as anything that names the dialog — the row-28 gotcha.
// rather than as anything that names the dialog — a known gotcha.
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>())
.Returns(Task.FromResult(true));
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DialogSeverity>(), Arg.Any<string?>())
@@ -130,6 +131,7 @@ public class SecuredWritesTests : BunitContext
// ── (a) Operator: submit form present + Submit calls SubmitAsync with the values ──
/// <summary>Verifies that a user with the Operator role sees the submit form.</summary>
[Fact]
public void Operator_SeesSubmitForm()
{
@@ -145,6 +147,7 @@ public class SecuredWritesTests : BunitContext
});
}
/// <summary>Verifies that submitting as an Operator calls the service with the entered values.</summary>
[Fact]
public void Operator_Submit_CallsServiceWithEnteredValues()
{
@@ -175,6 +178,7 @@ public class SecuredWritesTests : BunitContext
// ── (b) Verifier: pending row renders with Approve/Reject; own-row actions disabled ──
/// <summary>Verifies that a Verifier sees enabled approve and reject actions on a pending row submitted by another operator.</summary>
[Fact]
public void Verifier_PendingRow_HasEnabledActions_ForOtherOperatorsRow()
{
@@ -194,6 +198,7 @@ public class SecuredWritesTests : BunitContext
});
}
/// <summary>Verifies that a Verifier's own submission has its approve and reject actions disabled.</summary>
[Fact]
public void Verifier_OwnSubmission_HasApproveAndRejectDisabled()
{
@@ -216,6 +221,7 @@ public class SecuredWritesTests : BunitContext
});
}
/// <summary>Verifies that approving a pending row as a Verifier calls <c>ApproveAsync</c> on the service.</summary>
[Fact]
public void Verifier_Approve_CallsApproveAsync()
{
@@ -233,6 +239,7 @@ public class SecuredWritesTests : BunitContext
// ── (c) History: terminal rows render ──
/// <summary>Verifies that the history section renders rows in terminal states.</summary>
[Fact]
public void History_RendersTerminalRows()
{
@@ -263,6 +270,7 @@ public class SecuredWritesTests : BunitContext
// ── (d) History pager: page-size 50, driven by TotalCount ──
/// <summary>Verifies that the history pager requests the second page using the total count when Next is clicked.</summary>
[Fact]
public void History_Pager_NextRequestsSecondPage_UsingTotalCount()
{
@@ -285,13 +293,14 @@ public class SecuredWritesTests : BunitContext
cut.Find("[data-test='secured-write-history-next']").Click();
// The history page-2 fetch offsets by one page (skip: 50).
// The history second-page fetch offsets by one page (skip: 50).
_service.Received().ListAsync(
Arg.Any<string?>(), Arg.Any<string?>(), 50, Arg.Any<int>(), Arg.Any<CancellationToken>());
}
// ── (e) Approve dialog surfaces submission age (stale-setpoint scenario) ──
/// <summary>Verifies that the approve confirmation dialog shows the age of the submission.</summary>
[Fact]
public void Approve_ConfirmDialog_ShowsSubmissionAge()
{
@@ -316,6 +325,7 @@ public class SecuredWritesTests : BunitContext
// ── (f) Approve/Reject confirm-button severity (register row 75) ──
/// <summary>Verifies that the approve confirmation dialog uses caution styling rather than destructive styling.</summary>
[Fact]
public void Approve_ConfirmDialog_IsCautionNotDestructive()
{
@@ -343,6 +353,7 @@ public class SecuredWritesTests : BunitContext
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>());
}
/// <summary>Verifies that the reject confirmation dialog keeps its destructive styling.</summary>
[Fact]
public void Reject_ConfirmDialog_KeepsDestructiveStyling()
{
@@ -364,6 +375,7 @@ public class SecuredWritesTests : BunitContext
Arg.Any<string>(), Arg.Any<string>(), danger: true, confirmLabel: "Reject");
}
/// <summary>Verifies that a Viewer does not see the Operator or Verifier regions.</summary>
[Fact]
public void Viewer_DoesNotSeeOperatorOrVerifierRegions()
{
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Shared;
/// <summary>
/// bUnit tests for <see cref="KpiTrendChart"/> (M6 K12) — the reusable
/// bUnit tests for <see cref="KpiTrendChart"/> — the reusable
/// dependency-free SVG trend chart. Coverage pins the three render states
/// (multi-point chart, unavailable placeholder, single-sample note), the stable
/// <c>data-test</c> slug, and the coordinate-math edge guards (all-equal
@@ -22,6 +22,7 @@ public class KpiTrendChartTests : BunitContext
new KpiSeriesPoint(Base.AddMinutes(10), 2.0),
};
/// <summary>Verifies that an available series with three points renders a polyline, a title and a data-test attribute.</summary>
[Fact]
public void Available_WithThreePoints_RendersPolylineTitleAndDataTest()
{
@@ -38,6 +39,7 @@ public class KpiTrendChartTests : BunitContext
Assert.Contains("<title>", markup);
}
/// <summary>Verifies that a unit is appended to the rendered value labels.</summary>
[Fact]
public void Available_WithUnit_AppendsUnitToValueLabels()
{
@@ -51,6 +53,7 @@ public class KpiTrendChartTests : BunitContext
Assert.Contains("max 4 s", cut.Markup);
}
/// <summary>Verifies that an unavailable series renders an em dash placeholder and the error text without a polyline.</summary>
[Fact]
public void Unavailable_RendersEmDashPlaceholderAndError_NoPolyline()
{
@@ -66,6 +69,7 @@ public class KpiTrendChartTests : BunitContext
Assert.DoesNotContain("<polyline", markup);
}
/// <summary>Verifies that null points render the placeholder without a polyline.</summary>
[Fact]
public void NullPoints_RendersPlaceholder_NoPolyline()
{
@@ -79,6 +83,7 @@ public class KpiTrendChartTests : BunitContext
Assert.DoesNotContain("<polyline", markup);
}
/// <summary>Verifies that a single point renders the single-sample state without a polyline and does not throw.</summary>
[Fact]
public void SinglePoint_RendersSingleSampleState_NoPolyline_DoesNotThrow()
{
@@ -92,6 +97,7 @@ public class KpiTrendChartTests : BunitContext
Assert.DoesNotContain("<polyline", markup);
}
/// <summary>Verifies that points with identical timestamps render a polyline without throwing.</summary>
[Fact]
public void AllEqualTimestamps_DoesNotThrow_AndRendersPolyline()
{
@@ -114,6 +120,7 @@ public class KpiTrendChartTests : BunitContext
Assert.DoesNotContain("Infinity", markup);
}
/// <summary>Verifies that all-zero values render a flat polyline without throwing.</summary>
[Fact]
public void AllZeroValues_DoesNotThrow_AndRendersFlatPolyline()
{
@@ -136,6 +143,9 @@ public class KpiTrendChartTests : BunitContext
Assert.DoesNotContain("Infinity", markup);
}
/// <summary>Verifies that a chart title is converted to a stable data-test slug.</summary>
/// <param name="title">The chart title to slugify.</param>
/// <param name="expectedDataTest">The expected data-test slug.</param>
[Theory]
[InlineData("Queue Depth", "kpi-trend-queue-depth")]
[InlineData("Oldest Pending (s)", "kpi-trend-oldest-pending-s")]