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:
@@ -32,6 +32,9 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly ApiSurfaceFixture _api;
|
||||
|
||||
/// <summary>Creates the test class with the shared Playwright and API surface fixtures.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
/// <param name="api">The API surface fixture.</param>
|
||||
public ApiKeyCrudTests(PlaywrightFixture fixture, ApiSurfaceFixture api)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -44,6 +47,7 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
/// adjacent "Copy" button. MUTATES — the created key is deleted via the CLI in a
|
||||
/// <c>finally</c> (resolve by name, best-effort delete) so nothing leaks.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_RevealsOneTimeToken()
|
||||
{
|
||||
@@ -104,6 +108,7 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
/// literal <c>"Name is required."</c> message in <c>div.text-danger</c>; no token panel
|
||||
/// appears, so nothing is created and there is no teardown.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_EmptyName_ShowsValidationError()
|
||||
{
|
||||
@@ -136,6 +141,7 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
/// <c>div.text-danger</c>; no token panel appears, so nothing is created and there is no
|
||||
/// teardown.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_NoMethods_ShowsValidationError()
|
||||
{
|
||||
@@ -193,6 +199,7 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
/// cell: it must appear after Disable and be gone after Enable. The CLI-created key is deleted
|
||||
/// in a <c>finally</c> (best-effort) so nothing leaks.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ToggleEnabled_TransitionsDisabledBadge()
|
||||
{
|
||||
@@ -251,6 +258,7 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
/// gone. The CLI delete in the <c>finally</c> is an idempotent safety net (no-op if the UI already
|
||||
/// removed it).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Delete_WithConfirm_RemovesRow()
|
||||
{
|
||||
|
||||
@@ -9,10 +9,15 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Admin;
|
||||
/// </summary>
|
||||
public sealed class ApiSurfaceFixture : IAsyncLifetime
|
||||
{
|
||||
/// <summary>The id of the provisioned inbound API method.</summary>
|
||||
public int MethodId { get; private set; }
|
||||
/// <summary>The unique name of the provisioned inbound API method.</summary>
|
||||
public string MethodName { get; private set; } = string.Empty;
|
||||
/// <summary>Whether the cluster was reachable and the method was provisioned.</summary>
|
||||
public bool Available { get; private set; }
|
||||
|
||||
/// <summary>Prepares the test class before the tests run.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Available = await ClusterAvailability.IsAvailableAsync();
|
||||
@@ -39,6 +44,8 @@ public sealed class ApiSurfaceFixture : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class asynchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (Available)
|
||||
|
||||
+10
-4
@@ -13,11 +13,15 @@ public class LdapMappingCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture providing browser and cluster access.</param>
|
||||
public LdapMappingCrudTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an LDAP group mapping can be created, edited and deleted through the UI.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateEditDelete_LdapMapping_RoundTrips()
|
||||
{
|
||||
@@ -38,10 +42,10 @@ public class LdapMappingCrudTests
|
||||
// The LDAP Group Name label has no `for=` attribute so GetByLabel does not
|
||||
// work. Locate the input that immediately follows the label text instead.
|
||||
await page.Locator("label:has-text('LDAP Group Name') + input.form-control.form-control-sm").FillAsync(group);
|
||||
// Scope the role select to the div.mb-2 that owns the "Role" label so a
|
||||
// Scope the role select to the container div that owns the "Role" label so a
|
||||
// second select (Site Scope) on the edit page cannot cause a strict-mode
|
||||
// violation. The LdapMappingForm.razor uses Blazor @bind, not <form>,
|
||||
// so the "Role" label's parent div.mb-2 is the tightest stable scope.
|
||||
// so the "Role" label's parent container div is the tightest stable scope.
|
||||
await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm").SelectOptionAsync("Designer");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
@@ -127,12 +131,13 @@ public class LdapMappingCrudTests
|
||||
/// <summary>
|
||||
/// The create form's <c>SaveMapping()</c> validates manually (no EditForm /
|
||||
/// DataAnnotations): a blank LDAP Group Name renders "LDAP Group Name is
|
||||
/// required." in <c>div.text-danger.small.mt-2</c> and returns early; a present
|
||||
/// required." in <c>div.text-danger.small</c> and returns early; a present
|
||||
/// group but blank role renders "Role is required." Both branches return before
|
||||
/// any persistence, so nothing is created and no teardown is needed. The first
|
||||
/// Save is a full circuit roundtrip — proving the Blazor circuit is live — which
|
||||
/// makes the second-leg fill (group → blank role) safe from the prerender race.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Save_MissingGroupName_ShowsRequiredError()
|
||||
{
|
||||
@@ -176,10 +181,11 @@ public class LdapMappingCrudTests
|
||||
/// Attempting to create a mapping whose LDAP group already exists violates the DB
|
||||
/// unique index on <c>LdapGroupName</c>. The form has no friendly pre-check, so
|
||||
/// the persistence exception is caught and surfaced as "Save failed: {message}"
|
||||
/// in <c>div.text-danger.small.mt-2</c>, keeping the user on the create page.
|
||||
/// in <c>div.text-danger.small</c>, keeping the user on the create page.
|
||||
/// The duplicate is CLI-seeded (and torn down) so this asserts the save-failure
|
||||
/// branch without depending on the raw DB error text.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_DuplicateGroup_ShowsSaveFailedError()
|
||||
{
|
||||
|
||||
+18
-6
@@ -16,8 +16,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
||||
/// rows in <c>finally</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// This part (Wave 3, Task 10) covers render / search-narrows / pagination. The
|
||||
/// class is intentionally structured so Task 11 can APPEND modal/copy/chip facts
|
||||
/// This part covers render / search-narrows / pagination. The
|
||||
/// class is intentionally structured so modal/copy/chip facts can be APPENDED
|
||||
/// to it without restructuring: shared constants (the DB-unavailable skip reason,
|
||||
/// the page route) and the seeded-test gate idiom live here once.
|
||||
/// </para>
|
||||
@@ -45,6 +45,8 @@ public class AuditConfigurationTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The Playwright fixture that supplies authenticated browser pages.</param>
|
||||
public AuditConfigurationTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -58,6 +60,8 @@ public class AuditConfigurationTests
|
||||
/// </summary>
|
||||
private static string NewMarker() => "zzCfgAudit-" + Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
/// <summary>Verifies that the grid renders and that searching narrows the displayed rows.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Grid_RendersAndSearchNarrows()
|
||||
{
|
||||
@@ -102,6 +106,8 @@ public class AuditConfigurationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Previous is disabled on page 1 and that Next advances to the following page.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Pagination_PrevDisabledOnPage1_NextPaginates()
|
||||
{
|
||||
@@ -122,7 +128,7 @@ public class AuditConfigurationTests
|
||||
await page.Locator("button.btn.btn-primary:has-text('Search')").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Page 1 of 2: Previous disabled, Next enabled. (M10 OffsetPager summary:
|
||||
// Page 1 of 2: Previous disabled, Next enabled. (OffsetPager summary:
|
||||
// "Page {N} of {Y} · {Total} total".)
|
||||
await Assertions.Expect(page.Locator("[data-test='pager-summary']")).ToContainTextAsync("Page 1 of 2");
|
||||
await Assertions.Expect(page.Locator("[data-test='pager-summary']")).ToContainTextAsync("55 total");
|
||||
@@ -143,6 +149,8 @@ public class AuditConfigurationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a large state value opens a modal that can then be closed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task LargeState_OpensAndClosesModal()
|
||||
{
|
||||
@@ -185,6 +193,8 @@ public class AuditConfigurationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that copying an entity ID shows a toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CopyEntityId_ShowsToast()
|
||||
{
|
||||
@@ -221,6 +231,8 @@ public class AuditConfigurationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the bundle import ID drill-in applies a filter chip that narrows the grid and can be cleared.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task BundleImportId_DrillIn_ChipFiltersAndClears()
|
||||
{
|
||||
@@ -231,7 +243,7 @@ public class AuditConfigurationTests
|
||||
try
|
||||
{
|
||||
// 1 bulk row (EntityName "{marker}-0", BundleImportId NULL) + 2 bundle
|
||||
// rows (EntityName "{marker}-bundle-0/1", BundleImportId = bundleId), all
|
||||
// rows (EntityName "{marker}-bundle-" plus an index, BundleImportId = bundleId), all
|
||||
// EntityType = marker so the finally cleanup removes every row.
|
||||
await ConfigAuditDataSeeder.SeedAsync(marker, bundleId, bulkCount: 1, bundleRows: 2);
|
||||
|
||||
@@ -245,9 +257,9 @@ public class AuditConfigurationTests
|
||||
// The filter chip is shown for the active bundle import.
|
||||
await Assertions.Expect(page.Locator("span.badge.bg-primary:has-text('Filtered by Bundle Import:')")).ToBeVisibleAsync();
|
||||
|
||||
// Exactly the 2 bundle rows show (EntityName "{marker}-bundle-0/1"). The
|
||||
// Exactly the 2 bundle rows show (EntityName "{marker}-bundle-" plus an index). The
|
||||
// bulk row (EntityName "{marker}-0", BundleImportId NULL) is filtered out.
|
||||
// The "{marker}-0" token is NOT a substring of "{marker}-bundle-0", so the
|
||||
// The "{marker}-0" token is NOT a substring of the "{marker}-bundle-" names, so the
|
||||
// two locators distinguish the bundle rows from the bulk row unambiguously.
|
||||
await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = $"{marker}-bundle-" }))
|
||||
.ToHaveCountAsync(2, new() { Timeout = 10_000 });
|
||||
|
||||
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Direct-SQL seeding helper for the Audit Log Playwright E2E tests (#23 M7-T16).
|
||||
/// Direct-SQL seeding helper for the Audit Log Playwright E2E tests.
|
||||
///
|
||||
/// <para>
|
||||
/// The Playwright suite runs against the live Docker cluster (the same one that
|
||||
@@ -50,6 +50,25 @@ internal static class AuditDataSeeder
|
||||
/// derive automatically. All optional fields are nullable so individual tests can shape
|
||||
/// the row to whatever payload they need for their drawer/grid assertions.
|
||||
/// </summary>
|
||||
/// <param name="eventId">The unique event identifier.</param>
|
||||
/// <param name="occurredAtUtc">When the event occurred, in UTC.</param>
|
||||
/// <param name="channel">The audit channel.</param>
|
||||
/// <param name="kind">The audit kind.</param>
|
||||
/// <param name="status">The audit status.</param>
|
||||
/// <param name="sourceSiteId">The originating site identifier, if any.</param>
|
||||
/// <param name="target">The call target, if any.</param>
|
||||
/// <param name="actor">The acting principal, if any.</param>
|
||||
/// <param name="correlationId">The per-operation correlation identifier, if any.</param>
|
||||
/// <param name="executionId">The per-run execution identifier, if any.</param>
|
||||
/// <param name="parentExecutionId">The spawning execution identifier, if any.</param>
|
||||
/// <param name="httpStatus">The HTTP status code, if any.</param>
|
||||
/// <param name="durationMs">The duration in milliseconds, if any.</param>
|
||||
/// <param name="errorMessage">The error message, if any.</param>
|
||||
/// <param name="requestSummary">The request summary, if any.</param>
|
||||
/// <param name="responseSummary">The response summary, if any.</param>
|
||||
/// <param name="extra">Additional JSON details, if any.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task InsertAuditEventAsync(
|
||||
Guid eventId,
|
||||
DateTime occurredAtUtc,
|
||||
@@ -135,6 +154,9 @@ VALUES
|
||||
/// stuck row carrying a random GUID suffix does not collide with future
|
||||
/// runs and tests should not fail teardown.
|
||||
/// </summary>
|
||||
/// <param name="targetPrefix">The target prefix identifying the rows to delete.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteByTargetPrefixAsync(string targetPrefix, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
@@ -158,6 +180,8 @@ VALUES
|
||||
/// per-test setup on this; when the cluster is down the test fails with a
|
||||
/// clear "MSSQL unavailable" message instead of an opaque SqlException.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns><c>true</c> if the database accepted a connection; otherwise <c>false</c>.</returns>
|
||||
public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
|
||||
+10
-2
@@ -4,8 +4,8 @@ using Xunit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end coverage for the Audit Log results-grid column UX (#23
|
||||
/// follow-ups Task 10): drag-to-resize and drag-to-reorder columns, with the
|
||||
/// End-to-end coverage for the Audit Log results-grid column UX (Audit Log
|
||||
/// follow-ups): drag-to-resize and drag-to-reorder columns, with the
|
||||
/// chosen widths + order persisted in the browser's <c>sessionStorage</c>.
|
||||
///
|
||||
/// <para>
|
||||
@@ -34,6 +34,8 @@ public class AuditGridColumnTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public AuditGridColumnTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -113,6 +115,8 @@ public class AuditGridColumnTests
|
||||
expectedFirstKey);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dragging a resize handle widens the column and the width survives a reload.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ResizeHandle_DraggingWidensColumn_AndSurvivesReload()
|
||||
{
|
||||
@@ -171,6 +175,8 @@ public class AuditGridColumnTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dragging a column header reorders the columns and the order survives a reload.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ReorderDrag_MovesColumn_AndSurvivesReload()
|
||||
{
|
||||
@@ -220,6 +226,8 @@ public class AuditGridColumnTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that column order and widths persist across a reload via session storage.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ColumnOrderAndWidths_PersistAcrossReload_ViaSessionStorage()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end coverage for the central Audit Log surface (#23 M7-T16 / Bundle H).
|
||||
/// End-to-end coverage for the central Audit Log surface (Bundle H).
|
||||
///
|
||||
/// <para>
|
||||
/// Each test seeds its own <c>AuditLog</c> rows directly into the running cluster's
|
||||
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Scenarios covered (per the M7-T16 brief):
|
||||
/// Scenarios covered (per the brief):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>FilterNarrowing</c> — the channel filter narrows the results grid.</item>
|
||||
/// <item><c>DrilldownDrawer_JsonPrettyPrint</c> — JSON request bodies pretty-print.</item>
|
||||
@@ -50,11 +50,15 @@ public class AuditLogPageTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public AuditLogPageTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying a channel filter shrinks the results grid.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task FilterNarrowing_ChannelFilterShrinksGrid()
|
||||
{
|
||||
@@ -112,6 +116,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that combining a channel filter with a target filter narrows the grid to the matching row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task FilterCombination_ChannelPlusTarget_NarrowsToMatch()
|
||||
{
|
||||
@@ -172,6 +178,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying a filter with no matches shows the empty state.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EmptyResults_AfterApply_ShowsEmptyState()
|
||||
{
|
||||
@@ -195,6 +203,8 @@ public class AuditLogPageTests
|
||||
.ToContainTextAsync("No audit events match the current filter.");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the drilldown drawer pretty-prints a JSON request body.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrilldownDrawer_JsonPrettyPrintsRequestBody()
|
||||
{
|
||||
@@ -257,6 +267,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the copy-as-curl button is visible and clickable for an inbound API row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CopyAsCurlButton_IsVisibleAndClickableForApiInbound()
|
||||
{
|
||||
@@ -309,6 +321,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that drilling in from a correlation id lands on the Audit Log with the filter applied.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrillInFromCorrelationId_LandsOnAuditLogWithFilterContext()
|
||||
{
|
||||
@@ -366,6 +380,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that drilling in from an execution id lands on the Audit Log with the filter applied.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrillInFromExecutionId_LandsOnAuditLogWithFilterContext()
|
||||
{
|
||||
@@ -421,6 +437,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that drilling in from a parent execution filters the grid to the spawner execution.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrillInFromParentExecution_FiltersGridToSpawnerExecution()
|
||||
{
|
||||
@@ -498,12 +516,14 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that drilling into the execution chain renders the tree and that clicking a node filters the grid.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrillInToExecutionChain_RendersTree_AndNodeClickFiltersGrid()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
// Audit Log ParentExecutionId feature, Task 10: the drawer's "View
|
||||
// Audit Log ParentExecutionId feature: the drawer's "View
|
||||
// execution chain" action opens /audit/execution-tree?executionId={id}.
|
||||
// We seed a spawner row + a child row, open the child's drawer, click
|
||||
// "View execution chain", assert the tree renders BOTH executions, then
|
||||
@@ -577,12 +597,14 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that double-clicking a tree node opens the execution row modal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DoubleClickTreeNode_OpensExecutionRowModal()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
// Execution-Tree Node Detail Modal feature, Task 5: double-clicking a
|
||||
// Execution-Tree Node Detail Modal feature: double-clicking a
|
||||
// node on the /audit/execution-tree page opens ExecutionDetailModal —
|
||||
// a modal listing that execution's audit rows, with click-through to
|
||||
// each row's full <AuditEventDetail> view. We seed ONE execution with
|
||||
@@ -678,6 +700,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Notifications page renders the audit drill-in link pattern.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NotificationsPage_RendersAuditDrillInLinkPattern()
|
||||
{
|
||||
@@ -712,6 +736,8 @@ public class AuditLogPageTests
|
||||
// all we can assert without exercising the Akka query path to seed one.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the CSV export link is visible to a user with audit export permission and triggers a download.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExportCsv_LinkVisibleForAuditExportUserAndTriggersDownload()
|
||||
{
|
||||
@@ -742,6 +768,8 @@ public class AuditLogPageTests
|
||||
Assert.EndsWith(".csv", download.SuggestedFilename);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Designer without the operational audit permission is denied access.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PermissionGating_DesignerWithoutOperationalAudit_IsDeniedAccess()
|
||||
{
|
||||
@@ -766,6 +794,8 @@ public class AuditLogPageTests
|
||||
Assert.Equal(0, await page.Locator("[data-test='audit-results-grid']").CountAsync());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the drawer for a non-API row omits the copy-as-curl button.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task NonApiRow_Drawer_OmitsCopyAsCurl()
|
||||
{
|
||||
@@ -821,6 +851,8 @@ public class AuditLogPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the drawer's close controls dismiss the drawer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Drawer_CloseControls_DismissTheDrawer()
|
||||
{
|
||||
|
||||
@@ -50,6 +50,7 @@ VALUES (@user, @action, @entityType, @entityId, @entityName, @afterState, @ts, @
|
||||
/// "MSSQL unavailable" message instead of an opaque SqlException. Mirrors
|
||||
/// <see cref="AuditDataSeeder.IsAvailableAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A task whose result is <c>true</c> when the configuration database accepts a connection; otherwise <c>false</c>.</returns>
|
||||
public static async Task<bool> IsAvailableAsync()
|
||||
{
|
||||
try
|
||||
@@ -77,6 +78,7 @@ VALUES (@user, @action, @entityType, @entityId, @entityName, @afterState, @ts, @
|
||||
/// <param name="bundleId">Bundle import id stamped on the extra bundle rows.</param>
|
||||
/// <param name="bulkCount">Number of bulk rows (default 55 — spans two pages at 50/page).</param>
|
||||
/// <param name="bundleRows">Number of extra rows carrying the bundle import id (default 2).</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task SeedAsync(string marker, Guid bundleId, int bulkCount = 55, int bundleRows = 2)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
@@ -126,6 +128,8 @@ VALUES (@user, @action, @entityType, @entityId, @entityName, @afterState, @ts, @
|
||||
/// should not fail teardown. Mirrors
|
||||
/// <see cref="AuditDataSeeder.DeleteByTargetPrefixAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="marker">The unique per-test marker whose rows are deleted.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteByMarkerAsync(string marker)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -35,6 +35,7 @@ public static partial class CliRunner
|
||||
/// teardown targets.
|
||||
/// </summary>
|
||||
/// <param name="kind">Short entity discriminator, e.g. <c>"tmpl"</c> or <c>"inst"</c>.</param>
|
||||
/// <returns>The generated unique fixture name.</returns>
|
||||
public static string UniqueName(string kind) =>
|
||||
$"zztest-{kind}-{Guid.NewGuid().ToString("N")[..8]}";
|
||||
|
||||
@@ -78,6 +79,7 @@ public static partial class CliRunner
|
||||
/// where <c>DataSourceReference</c> is non-empty, so an attribute that needs
|
||||
/// to appear in the Connection Bindings panel MUST be created with this set.
|
||||
/// </param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static async Task AddAttributeAsync(
|
||||
int templateId, string name, string dataType = "Double",
|
||||
@@ -112,6 +114,17 @@ public static partial class CliRunner
|
||||
/// lets one helper provision both the fixture's HiLo alarm and a ValueMatch alarm
|
||||
/// (<c>--match-value</c>) without a parallel helper.
|
||||
/// </remarks>
|
||||
/// <param name="templateId">Owning template id.</param>
|
||||
/// <param name="name">Alarm name.</param>
|
||||
/// <param name="triggerType">CLI alarm trigger-type token, e.g. <c>HiLo</c> or <c>ValueMatch</c>.</param>
|
||||
/// <param name="priority">Alarm priority.</param>
|
||||
/// <param name="attribute">Name of the monitored attribute, or null to omit it.</param>
|
||||
/// <param name="hi">High setpoint, or null to omit it.</param>
|
||||
/// <param name="hiHi">High-high setpoint, or null to omit it.</param>
|
||||
/// <param name="lo">Low setpoint, or null to omit it.</param>
|
||||
/// <param name="loLo">Low-low setpoint, or null to omit it.</param>
|
||||
/// <param name="matchValue">Value to match for a ValueMatch trigger, or null to omit it.</param>
|
||||
/// <returns>The id of the newly created alarm.</returns>
|
||||
public static async Task<int> AddAlarmAsync(
|
||||
int templateId, string name, string triggerType = "HiLo", int priority = 500,
|
||||
string? attribute = null, double? hi = null, double? hiHi = null,
|
||||
@@ -183,6 +196,7 @@ public static partial class CliRunner
|
||||
/// the id, but teardown re-resolves defensively in case the test failed mid-flow).
|
||||
/// </summary>
|
||||
/// <param name="identifierPrefix">The <c>siteIdentifier</c> prefix to match.</param>
|
||||
/// <returns>The ids of the matching sites.</returns>
|
||||
public static async Task<IReadOnlyList<int>> ListSiteIdsByIdentifierPrefixAsync(string identifierPrefix)
|
||||
{
|
||||
using var doc = await RunJsonAsync("site", "list");
|
||||
@@ -268,16 +282,19 @@ public static partial class CliRunner
|
||||
|
||||
/// <summary>Deploys an instance via <c>instance deploy</c>. Surfaces CLI errors.</summary>
|
||||
/// <param name="id">Instance id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static Task DeployInstanceAsync(int id) => RunInstanceVerbAsync("deploy", id);
|
||||
|
||||
/// <summary>Enables an instance via <c>instance enable</c>. Surfaces CLI errors.</summary>
|
||||
/// <param name="id">Instance id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static Task EnableInstanceAsync(int id) => RunInstanceVerbAsync("enable", id);
|
||||
|
||||
/// <summary>Disables an instance via <c>instance disable</c>. Surfaces CLI errors.</summary>
|
||||
/// <param name="id">Instance id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static Task DisableInstanceAsync(int id) => RunInstanceVerbAsync("disable", id);
|
||||
|
||||
@@ -286,6 +303,7 @@ public static partial class CliRunner
|
||||
/// swallows any failure (the entity may already be gone).
|
||||
/// </summary>
|
||||
/// <param name="id">Instance id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteInstanceAsync(int id) => BestEffortAsync("instance", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
@@ -293,6 +311,7 @@ public static partial class CliRunner
|
||||
/// swallows any failure.
|
||||
/// </summary>
|
||||
/// <param name="id">Template id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteTemplateAsync(int id) => BestEffortAsync("template", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
@@ -309,6 +328,7 @@ public static partial class CliRunner
|
||||
/// The inline try/catch is kept here deliberately — if you need to fix teardown
|
||||
/// logic, update both this method and any other three-word deletes together.
|
||||
/// </remarks>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteAreaAsync(int id)
|
||||
{
|
||||
try
|
||||
@@ -374,11 +394,17 @@ public static partial class CliRunner
|
||||
/// any failure.
|
||||
/// </summary>
|
||||
/// <param name="id">Site id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteSiteAsync(int id) => BestEffortAsync("site", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a data connection on a site via <c>data-connection create</c> and returns its new <c>id</c>.
|
||||
/// </summary>
|
||||
/// <param name="siteId">Owning site id.</param>
|
||||
/// <param name="name">Data connection name.</param>
|
||||
/// <param name="protocol">Protocol token, e.g. <c>OpcUa</c>.</param>
|
||||
/// <param name="primaryConfig">Optional primary configuration JSON.</param>
|
||||
/// <returns>The id of the newly created data connection.</returns>
|
||||
public static async Task<int> CreateDataConnectionAsync(int siteId, string name, string protocol = "OpcUa", string? primaryConfig = null)
|
||||
{
|
||||
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||
@@ -400,12 +426,18 @@ public static partial class CliRunner
|
||||
}
|
||||
|
||||
/// <summary>Best-effort delete of a data connection via <c>data-connection delete</c> for teardown.</summary>
|
||||
/// <param name="id">Data connection id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteDataConnectionAsync(int id) => BestEffortAsync("data-connection", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an inbound API method via <c>api-method create</c> (so it appears as a checkbox in the
|
||||
/// API-key form) and returns its new <c>id</c>.
|
||||
/// </summary>
|
||||
/// <param name="name">API method name.</param>
|
||||
/// <param name="script">Method script body.</param>
|
||||
/// <param name="parameters">Optional parameter definitions JSON.</param>
|
||||
/// <returns>The id of the newly created API method.</returns>
|
||||
public static async Task<int> CreateApiMethodAsync(string name, string script = "return null;", string? parameters = null)
|
||||
{
|
||||
var args = new List<string> { "api-method", "create", "--name", name, "--script", script };
|
||||
@@ -420,6 +452,8 @@ public static partial class CliRunner
|
||||
}
|
||||
|
||||
/// <summary>Best-effort delete of an API method via <c>api-method delete</c> for teardown.</summary>
|
||||
/// <param name="id">API method id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteApiMethodAsync(int id) => BestEffortAsync("api-method", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
@@ -452,6 +486,8 @@ public static partial class CliRunner
|
||||
/// Resolves an API key's opaque string <c>keyId</c> from its display name via
|
||||
/// <c>security api-key list</c>; returns <see langword="null"/> if no key matches.
|
||||
/// </summary>
|
||||
/// <param name="name">The key display name to look up.</param>
|
||||
/// <returns>The key's opaque <c>keyId</c>, or <see langword="null"/> if no key matches.</returns>
|
||||
public static async Task<string?> ResolveApiKeyIdByNameAsync(string name)
|
||||
{
|
||||
using var doc = await RunJsonAsync("security", "api-key", "list");
|
||||
@@ -477,6 +513,8 @@ public static partial class CliRunner
|
||||
/// Best-effort delete of an API key via <c>security api-key delete --key-id</c> for teardown.
|
||||
/// The key id is an opaque string, so this cannot use the int-based <see cref="BestEffortAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="keyId">The opaque API key id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteApiKeyAsync(string keyId)
|
||||
{
|
||||
try
|
||||
@@ -499,6 +537,8 @@ public static partial class CliRunner
|
||||
/// scalars). The caller OWNS it and MUST wrap the call in <c>using var doc = …</c>; the
|
||||
/// <c>Document</c> suffix is the signal that this returns a disposable resource, not plain data.
|
||||
/// </remarks>
|
||||
/// <param name="id">Instance id.</param>
|
||||
/// <returns>A task whose result is the instance document; the caller owns it and must dispose it.</returns>
|
||||
public static Task<JsonDocument> GetInstanceDocumentAsync(int id) =>
|
||||
RunJsonAsync("instance", "get", "--id", id.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
@@ -507,6 +547,7 @@ public static partial class CliRunner
|
||||
/// <paramref name="prefix"/>, via <c>template list</c>.
|
||||
/// </summary>
|
||||
/// <param name="prefix">Name prefix to filter by (ordinal comparison).</param>
|
||||
/// <returns>The ids of the matching templates.</returns>
|
||||
public static async Task<IReadOnlyList<int>> ListTemplateIdsByNamePrefixAsync(string prefix)
|
||||
{
|
||||
using var doc = await RunJsonAsync("template", "list");
|
||||
@@ -535,6 +576,7 @@ public static partial class CliRunner
|
||||
/// <param name="name">External system name (typically from <see cref="UniqueName"/>).</param>
|
||||
/// <param name="endpointUrl">Endpoint base URL (defaults to an unreachable placeholder).</param>
|
||||
/// <param name="authType">Auth type token; one of <c>ApiKey</c> or <c>BasicAuth</c>.</param>
|
||||
/// <returns>The id of the newly created external system.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The CLI failed, or the response did not carry an integer <c>id</c>.
|
||||
/// </exception>
|
||||
@@ -552,6 +594,7 @@ public static partial class CliRunner
|
||||
/// </summary>
|
||||
/// <param name="name">Notification list name (typically from <see cref="UniqueName"/>).</param>
|
||||
/// <param name="emails">Comma-separated recipient emails.</param>
|
||||
/// <returns>The id of the newly created notification list.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The CLI failed, or the response did not carry an integer <c>id</c>.
|
||||
/// </exception>
|
||||
@@ -566,6 +609,7 @@ public static partial class CliRunner
|
||||
/// </summary>
|
||||
/// <param name="name">Shared script name (typically from <see cref="UniqueName"/>).</param>
|
||||
/// <param name="code">Script body.</param>
|
||||
/// <returns>The id of the newly created shared script.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The CLI failed, or the response did not carry an integer <c>id</c>.
|
||||
/// </exception>
|
||||
@@ -586,6 +630,7 @@ public static partial class CliRunner
|
||||
/// <param name="id">Shared script id.</param>
|
||||
/// <param name="name">Shared script name (unchanged; the CLI requires it).</param>
|
||||
/// <param name="code">The new script body.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static async Task UpdateSharedScriptCodeAsync(int id, string name, string code)
|
||||
{
|
||||
@@ -601,6 +646,7 @@ public static partial class CliRunner
|
||||
/// </summary>
|
||||
/// <param name="ldapGroup">LDAP group name to map (typically from <see cref="UniqueName"/>).</param>
|
||||
/// <param name="role">Role to grant members of the group; defaults to <c>Designer</c>.</param>
|
||||
/// <returns>The id of the newly created role mapping.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The CLI failed, or the response did not carry an integer <c>id</c>.
|
||||
/// </exception>
|
||||
@@ -618,6 +664,7 @@ public static partial class CliRunner
|
||||
/// external system a test created through the UI (where the new id is never surfaced).
|
||||
/// </summary>
|
||||
/// <param name="prefix">Name prefix to filter by (ordinal comparison).</param>
|
||||
/// <returns>The ids of the matching external systems.</returns>
|
||||
public static async Task<IReadOnlyList<int>> ListExternalSystemIdsByNamePrefixAsync(string prefix)
|
||||
{
|
||||
using var doc = await RunJsonAsync("external-system", "list");
|
||||
@@ -630,6 +677,7 @@ public static partial class CliRunner
|
||||
/// notification list a test created through the UI (where the new id is never surfaced).
|
||||
/// </summary>
|
||||
/// <param name="prefix">Name prefix to filter by (ordinal comparison).</param>
|
||||
/// <returns>The ids of the matching notification lists.</returns>
|
||||
public static async Task<IReadOnlyList<int>> ListNotificationListIdsByNamePrefixAsync(string prefix)
|
||||
{
|
||||
using var doc = await RunJsonAsync("notification", "list");
|
||||
@@ -637,12 +685,18 @@ public static partial class CliRunner
|
||||
}
|
||||
|
||||
/// <summary>Best-effort delete of an external system via <c>external-system delete</c> for teardown.</summary>
|
||||
/// <param name="id">External system id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteExternalSystemAsync(int id) => BestEffortAsync("external-system", "delete", id);
|
||||
|
||||
/// <summary>Best-effort delete of a notification list via <c>notification delete</c> for teardown.</summary>
|
||||
/// <param name="id">Notification list id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteNotificationListAsync(int id) => BestEffortAsync("notification", "delete", id);
|
||||
|
||||
/// <summary>Best-effort delete of a shared script via <c>shared-script delete</c> for teardown.</summary>
|
||||
/// <param name="id">Shared script id.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task DeleteSharedScriptAsync(int id) => BestEffortAsync("shared-script", "delete", id);
|
||||
|
||||
/// <summary>
|
||||
@@ -667,6 +721,7 @@ public static partial class CliRunner
|
||||
/// <see cref="DeleteAreaAsync"/>, <see cref="DeleteInstanceAlarmOverrideAsync"/>, and this method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteRoleMappingAsync(int id)
|
||||
{
|
||||
try
|
||||
@@ -682,6 +737,9 @@ public static partial class CliRunner
|
||||
}
|
||||
|
||||
/// <summary>Best-effort delete of an instance alarm override (teardown). Never throws.</summary>
|
||||
/// <param name="instanceId">Instance id.</param>
|
||||
/// <param name="alarmCanonicalName">Canonical name of the alarm whose override is removed.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteInstanceAlarmOverrideAsync(int instanceId, string alarmCanonicalName)
|
||||
{
|
||||
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||
@@ -711,6 +769,7 @@ public static partial class CliRunner
|
||||
/// <param name="sourceEnvironment">
|
||||
/// <c>SourceEnvironment</c> value stamped into the bundle manifest.
|
||||
/// </param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The template id could not be resolved to a name, or the CLI failed.
|
||||
/// </exception>
|
||||
@@ -748,6 +807,7 @@ public static partial class CliRunner
|
||||
/// <param name="siteIdentifier">The <c>SiteIdentifier</c> of the single site to export.</param>
|
||||
/// <param name="passphrase">Encryption passphrase for the bundle.</param>
|
||||
/// <param name="sourceEnvironment"><c>SourceEnvironment</c> value stamped into the bundle manifest.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
|
||||
public static async Task BundleExportSiteAsync(
|
||||
string outputPath, string siteIdentifier, string passphrase, string sourceEnvironment)
|
||||
@@ -771,6 +831,7 @@ public static partial class CliRunner
|
||||
/// <param name="sharedScriptName">Name of the single shared script to export.</param>
|
||||
/// <param name="passphrase">Encryption passphrase for the bundle.</param>
|
||||
/// <param name="sourceEnvironment"><c>SourceEnvironment</c> value stamped into the bundle manifest.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The shared-script name contains a comma (it would split the comma-separated
|
||||
/// <c>--shared-scripts</c> selector), or the CLI failed.
|
||||
|
||||
@@ -62,6 +62,8 @@ public static partial class CliRunner
|
||||
/// stdout. The connection flags (<c>--url</c>, <c>--username</c>,
|
||||
/// <c>--password</c>, <c>--format json</c>) are prepended automatically.
|
||||
/// </summary>
|
||||
/// <param name="args">The CLI arguments to pass after the connection flags.</param>
|
||||
/// <returns>The raw standard output of the CLI invocation.</returns>
|
||||
/// <exception cref="TimeoutException">
|
||||
/// The invocation exceeded the 60-second timeout; the process tree is killed.
|
||||
/// </exception>
|
||||
@@ -135,6 +137,8 @@ public static partial class CliRunner
|
||||
/// as a <see cref="JsonDocument"/>. The caller owns the returned document and
|
||||
/// must dispose it.
|
||||
/// </summary>
|
||||
/// <param name="args">The CLI arguments to pass after the connection flags.</param>
|
||||
/// <returns>The parsed JSON output of the CLI invocation.</returns>
|
||||
public static async Task<JsonDocument> RunJsonAsync(params string[] args) =>
|
||||
JsonDocument.Parse(await RunAsync(args));
|
||||
|
||||
|
||||
+10
@@ -18,6 +18,7 @@ public class CliRunnerHelpersTests
|
||||
/// <see cref="CliRunner.ListTemplateIdsByNamePrefixAsync"/>, and
|
||||
/// <see cref="CliRunner.DeleteTemplateAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteTemplate_RoundTrips()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public class CliRunnerHelpersTests
|
||||
/// <c>site-a</c> seed site by its <c>siteIdentifier</c> and returns a
|
||||
/// positive id.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ResolveSiteA_ReturnsId()
|
||||
{
|
||||
@@ -49,6 +51,7 @@ public class CliRunnerHelpersTests
|
||||
/// deleted in teardown, exercising <see cref="CliRunner.CreateDataConnectionAsync"/>
|
||||
/// and <see cref="CliRunner.DeleteDataConnectionAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteDataConnection_RoundTrips()
|
||||
{
|
||||
@@ -70,6 +73,7 @@ public class CliRunnerHelpersTests
|
||||
/// teardown, exercising <see cref="CliRunner.CreateApiMethodAsync"/> and
|
||||
/// <see cref="CliRunner.DeleteApiMethodAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteApiMethod_RoundTrips()
|
||||
{
|
||||
@@ -93,6 +97,7 @@ public class CliRunnerHelpersTests
|
||||
/// <see cref="CliRunner.ListAreaIdsByNamePrefixAsync"/>, and
|
||||
/// <see cref="CliRunner.DeleteAreaAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ListAreaIdsByNamePrefix_FindsCreatedArea()
|
||||
{
|
||||
@@ -118,6 +123,7 @@ public class CliRunnerHelpersTests
|
||||
/// <see cref="CliRunner.ListExternalSystemIdsByNamePrefixAsync"/>, and
|
||||
/// <see cref="CliRunner.DeleteExternalSystemAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteExternalSystem_RoundTrips()
|
||||
{
|
||||
@@ -138,6 +144,7 @@ public class CliRunnerHelpersTests
|
||||
/// <see cref="CliRunner.ListNotificationListIdsByNamePrefixAsync"/>, and
|
||||
/// <see cref="CliRunner.DeleteNotificationListAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteNotificationList_RoundTrips()
|
||||
{
|
||||
@@ -157,6 +164,7 @@ public class CliRunnerHelpersTests
|
||||
/// teardown, exercising <see cref="CliRunner.CreateSharedScriptAsync"/> and
|
||||
/// <see cref="CliRunner.DeleteSharedScriptAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteSharedScript_RoundTrips()
|
||||
{
|
||||
@@ -172,6 +180,7 @@ public class CliRunnerHelpersTests
|
||||
/// teardown, exercising <see cref="CliRunner.CreateRoleMappingAsync"/> and
|
||||
/// <see cref="CliRunner.DeleteRoleMappingAsync"/> as a round-trip.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateThenDeleteRoleMapping_RoundTrips()
|
||||
{
|
||||
@@ -194,6 +203,7 @@ public class CliRunnerHelpersTests
|
||||
/// <see cref="CliRunner.AddAlarmAsync"/> with <c>--hi</c>/<c>--hihi</c> returns a
|
||||
/// positive id, confirming the server accepted the serialized trigger-config JSON.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AddAlarmWithTypedFlags_RoundTrips()
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ public class CliRunnerSmokeTests
|
||||
/// <c>scadabridge site list</c> round-trips through the CLI and returns a
|
||||
/// JSON document (the CLI emits a JSON array of sites in <c>--format json</c>).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SiteList_ReturnsJson()
|
||||
{
|
||||
|
||||
+3
-2
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
/// reachable through the CLI. State-changing E2E tests gate their setup on this so
|
||||
/// a downed cluster surfaces as a Skipped fact (via <c>Skip.IfNot</c>) rather than
|
||||
/// an opaque failure. The result is cached for the process; <see cref="SkippedCount"/>
|
||||
/// is bumped each time a test would skip and is logged at suite teardown (Task 3).
|
||||
/// is bumped each time a test would skip and is logged at suite teardown.
|
||||
/// </summary>
|
||||
public static class ClusterAvailability
|
||||
{
|
||||
@@ -18,7 +18,7 @@ public static class ClusterAvailability
|
||||
/// <summary>
|
||||
/// Number of times a test would have skipped because the cluster is
|
||||
/// unavailable. Incremented on every <see cref="IsAvailableAsync"/> call that
|
||||
/// returns <see langword="false"/>; logged at suite teardown (Task 3).
|
||||
/// returns <see langword="false"/>; logged at suite teardown.
|
||||
/// </summary>
|
||||
public static int SkippedCount;
|
||||
|
||||
@@ -26,6 +26,7 @@ public static class ClusterAvailability
|
||||
/// Returns whether the cluster is reachable, probing once (a <c>site list</c>
|
||||
/// round-trip through the CLI) and caching the result for the process.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when the cluster is reachable; otherwise <see langword="false"/>.</returns>
|
||||
public static async Task<bool> IsAvailableAsync()
|
||||
{
|
||||
if (_cached is { } cached)
|
||||
|
||||
+2
-1
@@ -7,10 +7,11 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
/// line reporting how many cluster/DB-dependent test gates skipped because the
|
||||
/// cluster was unavailable (see <see cref="ClusterAvailability.SkippedCount"/>).
|
||||
/// Registered once via a <see cref="ModuleInitializerAttribute"/> so it runs
|
||||
/// regardless of how the test host is launched (Task 3).
|
||||
/// regardless of how the test host is launched.
|
||||
/// </summary>
|
||||
internal static class SkipSummaryReporter
|
||||
{
|
||||
/// <summary>Registers the skip-summary hook that runs when the process exits.</summary>
|
||||
[ModuleInitializer]
|
||||
internal static void Init() =>
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
|
||||
@@ -50,12 +50,17 @@ public class DebugViewTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright and cluster fixtures.</summary>
|
||||
/// <param name="pw">The shared Playwright browser fixture.</param>
|
||||
/// <param name="cluster">The shared deployment cluster fixture.</param>
|
||||
public DebugViewTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Debug View controls render and the Connect button is gated on an instance selection.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DebugView_ControlsRender_ConnectGatedOnSelection()
|
||||
{
|
||||
@@ -66,7 +71,7 @@ public class DebugViewTests : IClassFixture<DeploymentFixture>
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await Assertions.Expect(page.Locator("h4:has-text('Debug View')")).ToBeVisibleAsync();
|
||||
// The data-test hooks from Task 4 must be served (proves the rebuild took).
|
||||
// The data-test hooks must be served (proves the rebuild took).
|
||||
await Assertions.Expect(page.Locator("[data-test='debug-site-select']")).ToHaveCountAsync(1);
|
||||
await Assertions.Expect(page.Locator("[data-test='debug-instance-picker']")).ToHaveCountAsync(1);
|
||||
|
||||
@@ -77,6 +82,8 @@ public class DebugViewTests : IClassFixture<DeploymentFixture>
|
||||
.ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connect-enabled instance resolves and can be disconnected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DebugView_ConnectEnabledInstance_ResolvesAndDisconnect()
|
||||
{
|
||||
|
||||
@@ -53,12 +53,17 @@ public class DebugViewTreeTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright and deployment fixtures.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
/// <param name="cluster">The shared deployment fixture.</param>
|
||||
public DebugViewTreeTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a connected debug view renders tabbed trees and that the alarms tab can be switched to.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DebugView_Connected_RendersTabbedTrees_AndAlarmsTabSwitches()
|
||||
{
|
||||
|
||||
+11
@@ -41,12 +41,17 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>DeploymentActionTests</c> class.</summary>
|
||||
/// <param name="pw">The pw.</param>
|
||||
/// <param name="cluster">The cluster.</param>
|
||||
public DeploymentActionTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Helper: deploy instance shows outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Deploy_Instance_ShowsOutcomeToast()
|
||||
{
|
||||
@@ -80,6 +85,8 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper: disable instance shows outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Disable_Instance_ShowsOutcomeToast()
|
||||
{
|
||||
@@ -113,6 +120,8 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper: enable instance shows outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Enable_Instance_ShowsOutcomeToast()
|
||||
{
|
||||
@@ -141,6 +150,8 @@ public class DeploymentActionTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper: delete instance removes from tree.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Delete_Instance_RemovesFromTree()
|
||||
{
|
||||
|
||||
@@ -69,6 +69,7 @@ public sealed class DeploymentFixture : IAsyncLifetime
|
||||
/// is unavailable, sets <see cref="Available"/> to <see langword="false"/> and
|
||||
/// returns without touching the cluster.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Available = await ClusterAvailability.IsAvailableAsync();
|
||||
@@ -148,6 +149,7 @@ public sealed class DeploymentFixture : IAsyncLifetime
|
||||
/// <see cref="InstanceNamePrefix"/>, then deletes the fixture's area and
|
||||
/// template. Swallows every error so a teardown hiccup never fails the suite.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (!Available)
|
||||
|
||||
+7
@@ -39,12 +39,17 @@ public class DeploymentsRealtimeTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright and deployment fixtures.</summary>
|
||||
/// <param name="pw">The shared Playwright browser fixture.</param>
|
||||
/// <param name="cluster">The fixture that provides access to the running cluster.</param>
|
||||
public DeploymentsRealtimeTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deploying an instance pushes its row to the deployments page without a manual refresh.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DeployingInstance_PushesRowWithoutManualRefresh()
|
||||
{
|
||||
@@ -73,6 +78,8 @@ public class DeploymentsRealtimeTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pausing updates suppresses the pushed row and that refreshing restores it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task PausedUpdates_SuppressPush_RefreshRestoresRow()
|
||||
{
|
||||
|
||||
+4
-2
@@ -39,7 +39,8 @@ public sealed class FlowDebuggerFixture : IAsyncLifetime
|
||||
/// <summary>Name of the ephemeral inbound method (its <c>POST /api/{name}</c> segment).</summary>
|
||||
public string MethodName { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>Probes cluster availability and, when available, creates the ephemeral inbound method used by the tests.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Available = await ClusterAvailability.IsAvailableAsync();
|
||||
@@ -69,7 +70,8 @@ public sealed class FlowDebuggerFixture : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>Deletes the ephemeral inbound method if one was created.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task DisposeAsync() =>
|
||||
MethodId > 0 ? CliRunner.DeleteApiMethodAsync(MethodId) : Task.CompletedTask;
|
||||
}
|
||||
|
||||
+14
-3
@@ -7,7 +7,7 @@ using Xunit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Real-browser coverage for the Flow Debugger page (follow-ups plan 2026-09-06, Task 9):
|
||||
/// Real-browser coverage for the Flow Debugger page (follow-ups plan 2026-09-06):
|
||||
/// a Live run of an inbound method completes and draws a graph, and the method choice survives
|
||||
/// a reload through the page's <c>localStorage</c> restore path. Both go through the same
|
||||
/// prediction path that used to deadlock the circuit (see <see cref="FlowDebuggerFixture"/>) —
|
||||
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
||||
/// so these tests are the browser-level guard for that fix as well.
|
||||
///
|
||||
/// <para>
|
||||
/// The script entry (design 2026-09-08, Task 11) adds two more: a Live direct call of the rig's
|
||||
/// The script entry (design 2026-09-08) adds two more: a Live direct call of the rig's
|
||||
/// <c>OutboundDemo</c> Call script on <c>DelmiaSim01</c>, and the write-kind Inputs card naming the
|
||||
/// trigger's own attribute. Unlike the method tests those drive RIG-SEEDED objects the fixture does
|
||||
/// not provision, so each picker step skips with a "run docker/seed-…" message when it is absent.
|
||||
@@ -29,12 +29,17 @@ public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly FlowDebuggerFixture _flow;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>FlowDebuggerTests</c> class.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
/// <param name="flow">The Flow Debugger fixture.</param>
|
||||
public FlowDebuggerTests(PlaywrightFixture pw, FlowDebuggerFixture flow)
|
||||
{
|
||||
_pw = pw;
|
||||
_flow = flow;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a live run in the browser completes and draws the flow graph.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task LiveRun_InBrowser_CompletesAndDrawsTheGraph()
|
||||
{
|
||||
@@ -47,7 +52,7 @@ public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
||||
await SelectMethodAsync(page);
|
||||
|
||||
// The prediction (a Roslyn walk + the route-target resolve) finished: the state chip flips
|
||||
// to Predicted and the method's ParameterValueForm (#37: the same typed form the script
|
||||
// to Predicted and the method's ParameterValueForm (the same typed form the script
|
||||
// pane uses, inputs addressed by field id) appears.
|
||||
await Assertions.Expect(page.Locator("[data-test='flow-state']")).ToHaveTextAsync("Predicted", new() { Timeout = 15_000 });
|
||||
var input = page.Locator("#param-Text");
|
||||
@@ -72,6 +77,8 @@ public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
||||
Assert.DoesNotContain(consoleErrors, e => e.Contains("JSInterop", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the chosen inbound API method survives a page reload through local storage.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task MethodChoice_SurvivesReload_ViaLocalStorage()
|
||||
{
|
||||
@@ -100,6 +107,8 @@ public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
||||
Assert.DoesNotContain(consoleErrors, e => e.Contains("JSInterop", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a live run of a call script from the script entry completes in the browser.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ScriptEntry_LiveRunOfACallScript_InBrowser_Completes()
|
||||
{
|
||||
@@ -151,6 +160,8 @@ public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
||||
Assert.DoesNotContain(consoleErrors, e => e.Contains("JSInterop", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that selecting a write-kind script shows the value field for its trigger attribute.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ScriptEntry_WriteKindScript_ShowsTheValueFieldForTheTriggerAttribute()
|
||||
{
|
||||
|
||||
+10
@@ -27,10 +27,15 @@ public sealed class InstanceConfigureFixture : IAsyncLifetime
|
||||
{
|
||||
private const string SiteAIdentifier = "site-a";
|
||||
|
||||
/// <summary>Gets the database id of the site-a site resolved for the fixture.</summary>
|
||||
public int SiteAId { get; private set; }
|
||||
/// <summary>Gets the id of the fixture template.</summary>
|
||||
public int TemplateId { get; private set; }
|
||||
/// <summary>Gets the id of the fixture data connection.</summary>
|
||||
public int ConnectionId { get; private set; }
|
||||
/// <summary>Gets the id of the fixture area.</summary>
|
||||
public int AreaId { get; private set; }
|
||||
/// <summary>Gets the id of the fixture instance.</summary>
|
||||
public int InstanceId { get; private set; }
|
||||
|
||||
/// <summary>The single bindable/overridable attribute name on the fixture template.</summary>
|
||||
@@ -57,8 +62,11 @@ public sealed class InstanceConfigureFixture : IAsyncLifetime
|
||||
/// <summary>The fixture data-connection name (for locating it in the bindings UI dropdown).</summary>
|
||||
public string ConnectionName { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets a value indicating whether the live cluster was reachable and the fixture data was created.</summary>
|
||||
public bool Available { get; private set; }
|
||||
|
||||
/// <summary>Checks cluster availability and creates the fixture site, template, connection, area and instance.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Available = await ClusterAvailability.IsAvailableAsync();
|
||||
@@ -95,6 +103,8 @@ public sealed class InstanceConfigureFixture : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this fixture asynchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (!Available)
|
||||
|
||||
+15
@@ -29,12 +29,17 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly InstanceConfigureFixture _cfg;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright and instance configuration fixtures.</summary>
|
||||
/// <param name="fixture">The shared Playwright browser fixture.</param>
|
||||
/// <param name="cfg">The fixture that provides the instance under test.</param>
|
||||
public InstanceConfigureTests(PlaywrightFixture fixture, InstanceConfigureFixture cfg)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_cfg = cfg;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that binding all attributes through the bulk select saves and persists the bindings.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task BindAllAttributes_SavesAndPersists()
|
||||
{
|
||||
@@ -81,6 +86,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// exactly one toast, then verifies the override persisted via a CLI <c>instance get</c>
|
||||
/// read-back (not just the toast).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SaveOverride_RoundTrips()
|
||||
{
|
||||
@@ -124,6 +130,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// mutates the shared fixture instance's area, but is independent of the other tests (each
|
||||
/// gets a fresh page and asserts only on its own effect).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SetArea_RoundTrips()
|
||||
{
|
||||
@@ -156,6 +163,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// page's error alert (<c>data-test='instance-error-alert'</c>) carrying the
|
||||
/// <c>$"Instance #{Id} not found."</c> message built in <c>InstanceConfigure.OnInitializedAsync</c>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task NotFoundInstance_ShowsErrorAlert()
|
||||
{
|
||||
@@ -191,6 +199,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// <c>finally</c>, leaving the instance override-free as found.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_SetPriority_ThenClear_RoundTrips()
|
||||
{
|
||||
@@ -265,6 +274,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// is intentionally left blank so the only delta is the setpoint.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_HiLoSetpoint_Merges()
|
||||
{
|
||||
@@ -323,6 +333,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// per-key diff. ValueMatch / RangeViolation / RateOfChange overrides whole-replace the inherited
|
||||
/// trigger config (unlike HiLo, which merges).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_NonHiLo_WholeReplaces()
|
||||
{
|
||||
@@ -384,6 +395,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// branch in <c>SaveOverrideFromModal</c> that fires before any server round-trip.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_InvalidValue_ShowsError_StaysOpen()
|
||||
{
|
||||
@@ -431,6 +443,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// gone), no toast/badge appears, and CLI read-back confirms NO override was written for the alarm
|
||||
/// — the in-flight edit was discarded.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_Cancel_DiscardsChanges()
|
||||
{
|
||||
@@ -474,6 +487,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// own clear path (distinct from the row-level Clear button covered by
|
||||
/// <see cref="AlarmOverride_SetPriority_ThenClear_RoundTrips"/>).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_ClearFromModal_RemovesOverride()
|
||||
{
|
||||
@@ -537,6 +551,7 @@ public sealed class InstanceConfigureTests : IClassFixture<InstanceConfigureFixt
|
||||
/// <c>finally</c>, exactly as <see cref="AlarmOverride_SetPriority_ThenClear_RoundTrips"/> does.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task AlarmOverride_MesReportable_RoundTrips()
|
||||
{
|
||||
|
||||
@@ -71,12 +71,17 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _pw;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes the test class with the Playwright and deployment cluster fixtures.</summary>
|
||||
/// <param name="pw">The Playwright browser fixture.</param>
|
||||
/// <param name="cluster">The deployment cluster fixture.</param>
|
||||
public TopologyAreaTests(PlaywrightFixture pw, DeploymentFixture cluster)
|
||||
{
|
||||
_pw = pw;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an area created from the toolbar appears in the topology tree and persists.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateArea_ViaToolbar_AppearsInTreeAndPersists()
|
||||
{
|
||||
@@ -114,6 +119,8 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pressing Enter while renaming an area commits and persists the new name.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task RenameArea_EnterCommits_PersistsNewName()
|
||||
{
|
||||
@@ -153,6 +160,8 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pressing Escape while renaming an area reverts to the original label.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task RenameArea_Escape_RevertsToOriginalLabel()
|
||||
{
|
||||
@@ -183,6 +192,8 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area under another area shows a moved toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task MoveArea_UnderAnotherArea_ShowsMovedToast()
|
||||
{
|
||||
@@ -221,6 +232,8 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an instance to an area shows a moved toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task MoveInstance_ToArea_ShowsMovedToast()
|
||||
{
|
||||
@@ -255,6 +268,8 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the diff action on a deployed instance opens the diff dialog.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Diff_OnDeployedInstance_OpensDialog()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@ public class ApiMethodFormTests
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public ApiMethodFormTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -21,6 +23,7 @@ public class ApiMethodFormTests
|
||||
/// inline "Name and script required." error. The form does not navigate, so nothing is
|
||||
/// saved — this fixture name never reaches the database.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateForm_NameWithoutScript_ShowsInlineError()
|
||||
{
|
||||
@@ -33,7 +36,7 @@ public class ApiMethodFormTests
|
||||
await Assertions.Expect(page.Locator("h4:has-text('Add API Method')")).ToBeVisibleAsync();
|
||||
|
||||
// The form has several text inputs (Name + SchemaBuilder/parameter rows that also
|
||||
// carry .form-control), so anchor the Name input to its labelled mb-3 wrapper and
|
||||
// carry .form-control), so anchor the Name input to its labelled wrapper and
|
||||
// require .form-control (the peer ExternalSystemCrudTests pattern). The count guard
|
||||
// makes any future selector ambiguity fail loudly instead of filling the wrong field.
|
||||
var nameInput = page.Locator("div.mb-3:has(label:has-text('Name')) input[type=text].form-control");
|
||||
@@ -57,6 +60,7 @@ public class ApiMethodFormTests
|
||||
/// delete round-trip is the behavior under test. A best-effort CLI delete in finally
|
||||
/// guarantees no zztest-method-* row leaks if the UI path fails mid-way.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CliCreated_Method_VisibleAndDeletes()
|
||||
{
|
||||
|
||||
+4
@@ -9,6 +9,8 @@ public class DataConnectionCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="DataConnectionCrudTests"/> class.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public DataConnectionCrudTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -22,6 +24,7 @@ public class DataConnectionCrudTests
|
||||
/// CLI (a minimal create needs no primary config) and only drive the delete via the UI.
|
||||
/// The best-effort <c>finally</c> is a safety net: the happy path deletes via the UI.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CliCreated_Connection_DeletesViaTree()
|
||||
{
|
||||
@@ -89,6 +92,7 @@ public class DataConnectionCrudTests
|
||||
/// The "+ Connection" button is gated on a tree-node selection: it starts disabled
|
||||
/// and only enables once a site (or connection) node is selected. Mutates nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateButton_GatedOnNodeSelection()
|
||||
{
|
||||
|
||||
+5
-1
@@ -9,6 +9,8 @@ public class ExternalSystemCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright browser fixture.</param>
|
||||
public ExternalSystemCrudTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -19,6 +21,7 @@ public class ExternalSystemCrudTests
|
||||
/// Uses CliRunner for best-effort teardown so no zztest-extsys-* definition leaks on
|
||||
/// failure (the happy path already deletes via the UI).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_Delete_RoundTrips()
|
||||
{
|
||||
@@ -37,7 +40,7 @@ public class ExternalSystemCrudTests
|
||||
|
||||
// ExternalSystemForm.razor has THREE input[type=text].form-control fields
|
||||
// (Name, Endpoint URL, and Auth Config JSON), so index-based selection is
|
||||
// fragile. Anchor each fill to its own div.mb-3 wrapper via the field label
|
||||
// fragile. Anchor each fill to its own form-group wrapper div via the field label
|
||||
// so the selectors survive field reordering and uniquely match one element.
|
||||
await page.Locator("div.mb-3:has(label:has-text('Name')) input[type=text].form-control")
|
||||
.FillAsync(name);
|
||||
@@ -86,6 +89,7 @@ public class ExternalSystemCrudTests
|
||||
/// Saving the create form with both fields blank surfaces the inline validation
|
||||
/// error and keeps the user on the create page. Mutates nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateForm_EmptyFields_ShowsInlineError()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Xunit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
||||
|
||||
/// <summary>
|
||||
/// Browser-level coverage for the M9 design surfaces, run against the live dev cluster:
|
||||
/// Browser-level coverage for the design surfaces, run against the live dev cluster:
|
||||
/// <list type="bullet">
|
||||
/// <item>the <c>/design/templates</c> tree search filter (TemplateFolderTree pruning),</item>
|
||||
/// <item>the <c>/design/schema-library</c> page (render-smoke + UI create→delete round-trip),</item>
|
||||
@@ -22,6 +22,8 @@ public class M9SurfaceTests
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes a new instance of the test class.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public M9SurfaceTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -34,6 +36,7 @@ public class M9SurfaceTests
|
||||
/// DOM. The filter rebuilds <c>_visibleRoots</c> from matches only — non-matches are gone,
|
||||
/// not CSS-hidden — so a <c>ToHaveCountAsync(0)</c> on the non-match is the correct assertion.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task TemplateSearch_FiltersTree()
|
||||
{
|
||||
@@ -90,6 +93,7 @@ public class M9SurfaceTests
|
||||
/// the SchemaBuilder editor, and the success/delete toasts all render with no circuit crash.
|
||||
/// The DB safety-net <c>finally</c> only fires if the UI delete failed mid-flow.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SchemaLibrary_CreateDelete_RoundTrips()
|
||||
{
|
||||
@@ -157,6 +161,7 @@ public class M9SurfaceTests
|
||||
/// sites exist on the live cluster, so the picker (rather than the "no other site" fallback)
|
||||
/// renders.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task MoveConnectionDialog_OpensWithSitePicker()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@ public class SharedScriptCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public SharedScriptCrudTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -21,6 +23,7 @@ public class SharedScriptCrudTests
|
||||
/// best-effort CLI delete in finally guarantees no zztest-script-* definition leaks
|
||||
/// if the UI path fails mid-way (the happy path already deletes via the UI).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CliCreated_Script_DeletesViaCard()
|
||||
{
|
||||
@@ -72,6 +75,7 @@ public class SharedScriptCrudTests
|
||||
/// the Monaco editor surface. Asserts render only — does NOT click Save, Check Syntax,
|
||||
/// or Test Run (Test Run fires real I/O). Mutates nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateForm_Renders()
|
||||
{
|
||||
|
||||
@@ -16,12 +16,21 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright and cluster fixtures.</summary>
|
||||
/// <param name="fixture">The fixture.</param>
|
||||
/// <param name="cluster">The cluster.</param>
|
||||
/// <summary>Initializes the test class with the shared Playwright and cluster fixtures.</summary>
|
||||
/// <param name="fixture">The fixture.</param>
|
||||
/// <param name="cluster">The cluster.</param>
|
||||
public TemplateCrudTests(PlaywrightFixture fixture, DeploymentFixture cluster)
|
||||
{
|
||||
_fixture = fixture;
|
||||
/// <summary>Verifies that a template can be created, given an attribute and deleted again.</summary>
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template can be created, given an attribute and deleted again.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateAddAttributeDelete_Template_RoundTrips()
|
||||
{
|
||||
@@ -126,10 +135,13 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
catch
|
||||
{
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
/// <summary>Verifies that creating a template with a duplicate name shows an inline error.</summary>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a template with a duplicate name shows an inline error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateTemplate_DuplicateName_ShowsInlineError()
|
||||
{
|
||||
@@ -175,11 +187,14 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
catch
|
||||
{
|
||||
/// <summary>Verifies that cancelling template creation returns to the list without creating a template.</summary>
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancelling template creation returns to the list without creating a template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateTemplate_Cancel_ReturnsToListWithoutCreating()
|
||||
{
|
||||
@@ -216,12 +231,15 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
catch
|
||||
/// <summary>Verifies that editing an attribute persists the change.</summary>
|
||||
{
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that editing an attribute persists the change.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EditAttribute_PersistsChange()
|
||||
{
|
||||
@@ -289,6 +307,7 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
await CliRunner.DeleteTemplateAsync(leftover);
|
||||
}
|
||||
}
|
||||
/// <summary>Verifies that deleting a template that has an instance is blocked.</summary>
|
||||
catch
|
||||
{
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
@@ -296,6 +315,8 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a template that has an instance is blocked.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DeleteTemplate_WithInstance_IsBlocked()
|
||||
{
|
||||
|
||||
@@ -8,11 +8,15 @@ public class LoginTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public LoginTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unauthenticated user is redirected to the login page.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnauthenticatedUser_RedirectsToLogin()
|
||||
{
|
||||
@@ -30,6 +34,8 @@ public class LoginTests
|
||||
await Expect(page.Locator("button[type='submit']")).ToHaveTextAsync("Sign in");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that valid credentials authenticate the user successfully.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ValidCredentials_AuthenticatesSuccessfully()
|
||||
{
|
||||
@@ -39,6 +45,8 @@ public class LoginTests
|
||||
Assert.DoesNotContain("/login", page.Url);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalid credentials show an error on the login page.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InvalidCredentials_ShowsError()
|
||||
{
|
||||
@@ -66,6 +74,8 @@ public class LoginTests
|
||||
Assert.Equal(200, status); // redirect followed to login page
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the token endpoint returns a JWT for valid credentials.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TokenEndpoint_ReturnsJwt()
|
||||
{
|
||||
@@ -91,6 +101,8 @@ public class LoginTests
|
||||
Assert.Equal("multi-role", result.GetProperty("username").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the token endpoint returns HTTP 401 for invalid credentials.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TokenEndpoint_InvalidCredentials_Returns401()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests;
|
||||
[Collection("Playwright")]
|
||||
public class LowReviewSmokeTests(PlaywrightFixture fixture)
|
||||
{
|
||||
/// <summary>Verifies that CSRF login, lazy topology loading and Flow Debugger navigation work in the browser.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LowReview_CsrfLogin_LazyTopology_AndFlowDebuggerNavigation()
|
||||
{
|
||||
|
||||
@@ -36,6 +36,8 @@ public class EventLogsTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the Playwright fixture.</summary>
|
||||
/// <param name="fixture">The Playwright browser fixture.</param>
|
||||
public EventLogsTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -46,6 +48,7 @@ public class EventLogsTests
|
||||
/// enabled once a site is selected from the site dropdown. Deterministic regardless
|
||||
/// of whether the site has any logged events.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EventLogs_SearchGatedOnSiteSelection()
|
||||
{
|
||||
@@ -76,6 +79,7 @@ public class EventLogsTests
|
||||
/// rows exist — a site may have zero logged events, and event logs are not seedable
|
||||
/// from central, so the empty-state render is itself the assertion in that case.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EventLogs_Search_RendersTableOrEmptyState()
|
||||
{
|
||||
|
||||
+5
-2
@@ -50,6 +50,8 @@ public class HealthDashboardTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public HealthDashboardTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -66,6 +68,7 @@ public class HealthDashboardTests
|
||||
/// rather than using a fixed sleep, so a fast cluster will pass quickly.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task KpiTiles_ResolveToValues_NotDegradePlaceholder()
|
||||
{
|
||||
@@ -83,10 +86,10 @@ public class HealthDashboardTests
|
||||
//
|
||||
// Health.razor structure:
|
||||
// <div class="d-flex ...">
|
||||
// <h6 class="text-muted mb-0">Notification Outbox</h6> ← anchor
|
||||
// <h6 class="text-muted">Notification Outbox</h6> ← anchor
|
||||
// <a ...>View details →</a>
|
||||
// </div>
|
||||
// <div class="row g-3 mb-3"> ← outboxSection (+sibling)
|
||||
// <div class="row g-3"> ← outboxSection (+sibling)
|
||||
// <div class="col-..."><div class="card">Queue Depth</div></div>
|
||||
// <div class="col-..."><div class="card">Stuck</div></div>
|
||||
// <div class="col-..."><div class="card">Parked</div></div>
|
||||
|
||||
+4
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// E2E coverage for the M6 "KPI History & Trends" feature (K17's spec slice):
|
||||
/// E2E coverage for the "KPI History & Trends" feature:
|
||||
/// proves a <c>KpiTrendChart</c> trend section renders in the live Central UI.
|
||||
///
|
||||
/// <para>
|
||||
@@ -51,6 +51,8 @@ public class KpiTrendChartTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture that provides the browser and login helpers.</param>
|
||||
public KpiTrendChartTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -62,6 +64,7 @@ public class KpiTrendChartTests
|
||||
/// <c>kpi-trend-*</c> card is present, and (tolerantly) reports whether any
|
||||
/// polyline has been plotted yet.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task NotificationKpis_RendersTrendSection_WithKpiTrendCards()
|
||||
{
|
||||
|
||||
+5
@@ -36,6 +36,8 @@ public class ParkedMessagesActionTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The Playwright fixture that supplies authenticated browser pages.</param>
|
||||
public ParkedMessagesActionTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -45,6 +47,7 @@ public class ParkedMessagesActionTests
|
||||
/// The Query button must be disabled on page load (no site selected) and become
|
||||
/// enabled once a site is selected from the site dropdown.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task QueryButton_DisabledUntilSiteSelected()
|
||||
{
|
||||
@@ -67,6 +70,7 @@ public class ParkedMessagesActionTests
|
||||
/// The Clear button must be disabled on page load (no active filters) and become
|
||||
/// enabled once any filter is changed. Clicking Clear must re-disable it.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ClearButton_DisabledUntilFilterSet_ThenResets()
|
||||
{
|
||||
@@ -93,6 +97,7 @@ public class ParkedMessagesActionTests
|
||||
/// are present (the common case in a clean test environment — parked rows are not
|
||||
/// seedable from central), the test exits early as a tolerated no-op.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SelectingParkedRow_RevealsBulkRetryDiscardBar_WhenRowsPresent()
|
||||
{
|
||||
|
||||
+4
@@ -34,11 +34,15 @@ public class ParkedMessagesTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public ParkedMessagesTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that querying the parked messages page for a site renders without hanging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ParkedMessages_QueryForSite_RendersWithoutHang()
|
||||
{
|
||||
|
||||
@@ -32,11 +32,15 @@ public class NavCollapseTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>NavCollapseTests</c> class.</summary>
|
||||
/// <param name="fixture">The fixture.</param>
|
||||
public NavCollapseTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all nav sections are expanded by default after login.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Sections_AreExpandedByDefault_AfterLogin()
|
||||
{
|
||||
@@ -52,6 +56,8 @@ public class NavCollapseTests
|
||||
await Expect(page.Locator("a.rail-link:has-text('Topology')")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that clicking a section header toggles its items.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ClickingSectionHeader_TogglesItsItems()
|
||||
{
|
||||
@@ -76,6 +82,8 @@ public class NavCollapseTests
|
||||
await Expect(topology).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the collapse state survives a page reload.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CollapseState_SurvivesPageReload()
|
||||
{
|
||||
@@ -97,6 +105,8 @@ public class NavCollapseTests
|
||||
await Expect(page.Locator("a.rail-link:has-text('Topology')")).Not.ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that navigating into a collapsed section expands it automatically.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task NavigatingIntoCollapsedSection_AutoExpandsIt()
|
||||
{
|
||||
|
||||
@@ -7,11 +7,15 @@ public class NavigationTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture that provides the browser and login helpers.</param>
|
||||
public NavigationTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the dashboard is visible after logging in.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dashboard_IsVisibleAfterLogin()
|
||||
{
|
||||
@@ -24,6 +28,10 @@ public class NavigationTests
|
||||
await Expect(page.GetByRole(AriaRole.Link, new() { Name = "Dashboard", Exact = true })).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each Admin navigation link leads to its expected page.</summary>
|
||||
/// <param name="linkText">The visible text of the navigation link to click.</param>
|
||||
/// <param name="expectedPath">The URL path expected after navigation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Sites", "/admin/sites")]
|
||||
[InlineData("API Keys", "/admin/api-keys")]
|
||||
@@ -34,6 +42,10 @@ public class NavigationTests
|
||||
await ClickNavAndWait(page, linkText, expectedPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each Notifications navigation link leads to its expected page.</summary>
|
||||
/// <param name="linkText">The visible text of the navigation link to click.</param>
|
||||
/// <param name="expectedPath">The URL path expected after navigation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("SMTP Configuration", "/notifications/smtp")]
|
||||
[InlineData("Notification Lists", "/notifications/lists")]
|
||||
@@ -45,6 +57,10 @@ public class NavigationTests
|
||||
await ClickNavAndWait(page, linkText, expectedPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each Design navigation link leads to its expected page.</summary>
|
||||
/// <param name="linkText">The visible text of the navigation link to click.</param>
|
||||
/// <param name="expectedPath">The URL path expected after navigation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Templates", "/design/templates")]
|
||||
[InlineData("Shared Scripts", "/design/shared-scripts")]
|
||||
@@ -56,6 +72,10 @@ public class NavigationTests
|
||||
await ClickNavAndWait(page, linkText, expectedPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each Deployment navigation link leads to its expected page.</summary>
|
||||
/// <param name="linkText">The visible text of the navigation link to click.</param>
|
||||
/// <param name="expectedPath">The URL path expected after navigation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Topology", "/deployment/topology")]
|
||||
[InlineData("Deployments", "/deployment/deployments")]
|
||||
@@ -65,6 +85,10 @@ public class NavigationTests
|
||||
await ClickNavAndWait(page, linkText, expectedPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each Monitoring navigation link leads to its expected page.</summary>
|
||||
/// <param name="linkText">The visible text of the navigation link to click.</param>
|
||||
/// <param name="expectedPath">The URL path expected after navigation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Health Dashboard", "/monitoring/health")]
|
||||
[InlineData("Event Logs", "/monitoring/event-logs")]
|
||||
|
||||
+12
-2
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end coverage for the central Notification Report page's Retry / Discard
|
||||
/// actions on parked notifications (Notification Outbox #21).
|
||||
/// actions on parked notifications (Notification Outbox).
|
||||
///
|
||||
/// <para>
|
||||
/// Each test seeds its own <c>Parked</c> row directly into the running cluster's
|
||||
@@ -34,6 +34,8 @@ public class NotificationActionTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public NotificationActionTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -94,6 +96,8 @@ public class NotificationActionTests
|
||||
return (page, row);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retrying a parked notification shows an outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Retry_ParkedNotification_ShowsOutcomeToast()
|
||||
{
|
||||
@@ -133,6 +137,8 @@ public class NotificationActionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that discarding a parked notification shows an outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Discard_ParkedNotification_ShowsOutcomeToast()
|
||||
{
|
||||
@@ -179,6 +185,7 @@ public class NotificationActionTests
|
||||
/// than widening — the marker isolates the pair from ambient cluster rows, and the
|
||||
/// subject keyword discriminates between them.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task FilterCombination_StatusPlusList_NarrowsToMatch()
|
||||
{
|
||||
@@ -237,6 +244,7 @@ public class NotificationActionTests
|
||||
/// close affordances are wired to the same <c>CloseDetail</c> handler; this exercises
|
||||
/// each path independently (open → X-close → reopen → footer-close).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DetailModal_OpenOnDblClick_CloseViaXAndFooter()
|
||||
{
|
||||
@@ -308,6 +316,7 @@ public class NotificationActionTests
|
||||
/// docker cluster carries no override — so a Retrying row back-dated 15 min passes both
|
||||
/// the repository's StuckOnly SQL predicate and the page's <c>IsStuck</c> derivation.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task StuckOnlyFilter_NarrowsToStuckRows()
|
||||
{
|
||||
@@ -384,6 +393,7 @@ public class NotificationActionTests
|
||||
/// disables both pager buttons — so the button-state assertions never race the in-flight
|
||||
/// query.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Pagination_PageNumberNextAndPrev_TraversesPages()
|
||||
{
|
||||
@@ -413,7 +423,7 @@ public class NotificationActionTests
|
||||
await page.Locator("button.btn-primary:has-text('Query')").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Pager locators (M10 OffsetPager). Previous/Next are the pager's data-test
|
||||
// Pager locators (OffsetPager). Previous/Next are the pager's data-test
|
||||
// hooks (the page header's Refresh button is also a .btn-outline-secondary.btn-sm,
|
||||
// so we no longer scope by text). The summary span carries the page/total readout
|
||||
// ("Page {N} of {Y} · {Total} total").
|
||||
|
||||
+8
-2
@@ -4,8 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Direct-SQL seeding helper for the Notification Report page Playwright E2E tests
|
||||
/// (Notification Outbox #21).
|
||||
/// Direct-SQL seeding helper for the Notification Report page Playwright E2E tests.
|
||||
///
|
||||
/// <para>
|
||||
/// The Notification Report page reads the central <c>Notifications</c> table through the
|
||||
@@ -74,6 +73,7 @@ internal static class NotificationDataSeeder
|
||||
/// <param name="retryCount">Retry count to display on the row.</param>
|
||||
/// <param name="lastError">Optional last-error text shown beneath the subject.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task InsertNotificationAsync(
|
||||
Guid notificationId,
|
||||
string listNameMarker,
|
||||
@@ -132,6 +132,7 @@ VALUES
|
||||
/// <param name="retryCount">Retry count to display on the row.</param>
|
||||
/// <param name="lastError">Optional last-error text shown beneath the subject.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static Task InsertParkedNotificationAsync(
|
||||
Guid notificationId,
|
||||
string listNameMarker,
|
||||
@@ -152,6 +153,9 @@ VALUES
|
||||
/// to <c>Pending</c> (and a subsequent dispatch sweep) does not change the <c>ListName</c>,
|
||||
/// so the marker still matches whatever terminal state the row ends in.
|
||||
/// </summary>
|
||||
/// <param name="listNameMarker">Unique per-run marker stored in <c>ListName</c>.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteByMarkerAsync(string listNameMarker, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
@@ -175,6 +179,8 @@ VALUES
|
||||
/// this so a downed cluster surfaces a clear message rather than an opaque
|
||||
/// <see cref="SqlException"/>.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns><c>true</c> if a connection could be opened; otherwise <c>false</c>.</returns>
|
||||
public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
|
||||
+4
@@ -22,6 +22,8 @@ public class NotificationKpisTests
|
||||
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public NotificationKpisTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -32,6 +34,7 @@ public class NotificationKpisTests
|
||||
/// real state — EITHER all 5 KPI tile labels rendered OR the 'KPIs unavailable'
|
||||
/// alert is shown.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task KpisPage_RendersTilesOrError()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public class NotificationKpisTests
|
||||
/// that the button re-enables within 10 s — proving the refresh round-trip completed
|
||||
/// without hanging.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task KpisPage_RefreshReenables()
|
||||
{
|
||||
|
||||
+4
@@ -23,6 +23,8 @@ public class NotificationListCrudTests
|
||||
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Creates the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public NotificationListCrudTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -33,6 +35,7 @@ public class NotificationListCrudTests
|
||||
/// open it for edit, add a recipient, remove that recipient, then delete the list —
|
||||
/// asserting the delete confirm dialog, the success toast, and the row disappearing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_AddRecipient_RemoveRecipient_Delete_RoundTrips()
|
||||
{
|
||||
@@ -124,6 +127,7 @@ public class NotificationListCrudTests
|
||||
/// Submits the create form with a blank name and asserts the inline 'Name required.'
|
||||
/// validation error appears and the page does not navigate away. Mutates nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateForm_EmptyName_ShowsInlineError()
|
||||
{
|
||||
|
||||
+5
@@ -50,6 +50,8 @@ public class SmsNotificationE2ETests
|
||||
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes the tests with the shared Playwright fixture.</summary>
|
||||
/// <param name="pw">The shared Playwright browser fixture.</param>
|
||||
public SmsNotificationE2ETests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -74,6 +76,7 @@ public class SmsNotificationE2ETests
|
||||
/// fills, and never raise the saved toast — all of which are asserted with web-first waits.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SmsConfigPage_CreateOrRender_NeverLeaksAuthToken()
|
||||
{
|
||||
@@ -141,6 +144,7 @@ public class SmsNotificationE2ETests
|
||||
/// phone recipient and assert the row appears. Best-effort CLI teardown in <c>finally</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ListCreate_SmsType_ShowsPhoneInput_NotEmail()
|
||||
{
|
||||
@@ -208,6 +212,7 @@ public class SmsNotificationE2ETests
|
||||
/// the list page, and scopes the assertion to that list's row so it cannot match another
|
||||
/// list's Type cell. Best-effort CLI teardown in <c>finally</c>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ListCreate_SmsType_ShowsSmsInTypeColumn()
|
||||
{
|
||||
|
||||
@@ -40,6 +40,8 @@ public class SmtpConfigTests
|
||||
|
||||
private readonly PlaywrightFixture _pw;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SmtpConfigTests"/> class.</summary>
|
||||
/// <param name="pw">The shared Playwright fixture.</param>
|
||||
public SmtpConfigTests(PlaywrightFixture pw)
|
||||
{
|
||||
_pw = pw;
|
||||
@@ -51,6 +53,7 @@ public class SmtpConfigTests
|
||||
/// The page's <c>Save</c> handler returns at this guard BEFORE persisting, so this fact
|
||||
/// mutates nothing. Then it cancels and asserts the form closed.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SmtpForm_MissingRequired_ShowsInlineError()
|
||||
{
|
||||
@@ -95,6 +98,7 @@ public class SmtpConfigTests
|
||||
/// card (Credentials value '(stored)' or '(not set)') is present, OR the empty-state
|
||||
/// 'No SMTP configuration set.' text is shown. Pure-read; mutates nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SmtpPage_RendersConfigOrEmptyState()
|
||||
{
|
||||
@@ -130,6 +134,7 @@ public class SmtpConfigTests
|
||||
/// skips (SMTP config has no delete verb, so we never create one we could not delete).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SmtpEdit_NoopSave_ShowsSavedToast()
|
||||
{
|
||||
|
||||
@@ -34,12 +34,17 @@ public class PendingUpdatesTests
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
/// <summary>Initializes the test class with the Playwright fixture and the test output helper.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
/// <param name="output">The xUnit test output helper.</param>
|
||||
public PendingUpdatesTests(PlaywrightFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_output = output;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the rail footer shows the pending-updates indicator and that it settles out of the checking state.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task RailFooter_ShowsPendingUpdatesIndicator_AndSettles()
|
||||
{
|
||||
@@ -69,6 +74,8 @@ public class PendingUpdatesTests
|
||||
$"the fleet snapshot could not be read — indicator says: {text}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Sites page shows an artifacts badge for each site and a re-check control.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task SitesPage_ShowsAnArtifactsBadgePerSite_AndARecheckControl()
|
||||
{
|
||||
@@ -115,6 +122,8 @@ public class PendingUpdatesTests
|
||||
Assert.Equal(0, await page.Locator("[aria-label='Artifacts: unavailable']").CountAsync());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the navigation rail has its own scroll pane at desktop width.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task NavRail_HasAScrollPane_AtDesktopWidth()
|
||||
{
|
||||
|
||||
@@ -24,7 +24,9 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
public const string TestUsername = "multi-role";
|
||||
public const string TestPassword = "password";
|
||||
|
||||
/// <summary>The Playwright driver instance created during initialization.</summary>
|
||||
public IPlaywright Playwright { get; private set; } = null!;
|
||||
/// <summary>The browser connected to the Playwright server.</summary>
|
||||
public IBrowser Browser { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -44,12 +46,16 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// </summary>
|
||||
private const int MaxRetainedContexts = 4;
|
||||
|
||||
/// <summary>Creates the Playwright driver and connects to the remote Chromium endpoint.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
|
||||
Browser = await Playwright.Chromium.ConnectAsync(PlaywrightWsEndpoint);
|
||||
}
|
||||
|
||||
/// <summary>Closes the browser connection and disposes the Playwright driver.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await Browser.CloseAsync();
|
||||
@@ -62,6 +68,7 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// <see cref="MaxRetainedContexts"/> are open, to bound the number of concurrent Blazor
|
||||
/// Server circuits the run holds on the Central UI.
|
||||
/// </summary>
|
||||
/// <returns>The new page, which belongs to its own isolated browser context.</returns>
|
||||
public async Task<IPage> NewPageAsync()
|
||||
{
|
||||
var context = await Browser.NewContextAsync();
|
||||
@@ -92,6 +99,7 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// <summary>
|
||||
/// Create a new page and log in with the default multi-role test user.
|
||||
/// </summary>
|
||||
/// <returns>A page that is authenticated as the default test user.</returns>
|
||||
public Task<IPage> NewAuthenticatedPageAsync() =>
|
||||
NewAuthenticatedPageAsync(TestUsername, TestPassword);
|
||||
|
||||
@@ -100,6 +108,9 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// Uses JavaScript fetch() to POST to /auth/login from within the browser,
|
||||
/// which sets the auth cookie in the browser context. Then navigates to the dashboard.
|
||||
/// </summary>
|
||||
/// <param name="username">The LDAP user name to log in with.</param>
|
||||
/// <param name="password">The password for <paramref name="username"/>.</param>
|
||||
/// <returns>A page that is authenticated as the given user.</returns>
|
||||
public async Task<IPage> NewAuthenticatedPageAsync(string username, string password)
|
||||
{
|
||||
var page = await NewPageAsync();
|
||||
@@ -144,6 +155,11 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// Blazor Server uses SignalR for client-side navigation (no full page reload),
|
||||
/// so standard WaitForURLAsync times out. This polls window.location instead.
|
||||
/// </summary>
|
||||
/// <param name="page">The page whose location is polled.</param>
|
||||
/// <param name="path">A path fragment the location must contain.</param>
|
||||
/// <param name="excludePath">An optional path fragment the location must not contain.</param>
|
||||
/// <param name="timeoutMs">The maximum time to wait, in milliseconds.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task WaitForPathAsync(IPage page, string path, string? excludePath = null, int timeoutMs = 10000)
|
||||
{
|
||||
var js = excludePath != null
|
||||
@@ -163,6 +179,8 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
/// in the DOM but are hidden until the section is expanded. Call this after
|
||||
/// authenticating, before interacting with sectioned nav links.
|
||||
/// </summary>
|
||||
/// <param name="page">The authenticated page whose navigation sections are expanded.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task ExpandAllNavSectionsAsync(IPage page)
|
||||
{
|
||||
var toggles = page.Locator("summary.rail-eyebrow-toggle");
|
||||
|
||||
@@ -22,6 +22,8 @@ public class RoleNavigationTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public RoleNavigationTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -29,6 +31,8 @@ public class RoleNavigationTests
|
||||
|
||||
// ── Admin-only user ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the admin user sees the Admin navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminUser_SeesAdminSection()
|
||||
{
|
||||
@@ -41,6 +45,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkVisible(page, "Audit Log");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the admin user does not see the Design navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminUser_DoesNotSeeDesignSection()
|
||||
{
|
||||
@@ -52,6 +58,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "External Systems");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the admin user does not see the Deployment navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminUser_DoesNotSeeDeploymentSection()
|
||||
{
|
||||
@@ -62,6 +70,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "Debug View");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the admin user sees the Health Dashboard but not Deployment monitoring.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminUser_SeesHealthDashboard_NotDeploymentMonitoring()
|
||||
{
|
||||
@@ -76,6 +86,8 @@ public class RoleNavigationTests
|
||||
|
||||
// ── Design-only user ────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the design user sees the Design navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DesignUser_SeesDesignSection()
|
||||
{
|
||||
@@ -87,6 +99,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkVisible(page, "External Systems");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the design user does not see the Admin navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DesignUser_DoesNotSeeAdminSection()
|
||||
{
|
||||
@@ -99,6 +113,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "Audit Log");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the design user does not see the Deployment navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DesignUser_DoesNotSeeDeploymentSection()
|
||||
{
|
||||
@@ -109,6 +125,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "Debug View");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the design user sees the Health Dashboard but not Deployment monitoring or audit links.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DesignUser_SeesHealthDashboard_NotDeploymentMonitoringOrAudit()
|
||||
{
|
||||
@@ -124,6 +142,8 @@ public class RoleNavigationTests
|
||||
|
||||
// ── Deployment-only user ────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the deployment user sees the Deployment navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeploymentUser_SeesDeploymentSection()
|
||||
{
|
||||
@@ -134,6 +154,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkVisible(page, "Debug View");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deployment user does not see the Admin navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeploymentUser_DoesNotSeeAdminSection()
|
||||
{
|
||||
@@ -146,6 +168,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "Audit Log");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deployment user does not see the Design navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeploymentUser_DoesNotSeeDesignSection()
|
||||
{
|
||||
@@ -157,6 +181,8 @@ public class RoleNavigationTests
|
||||
await AssertNavLinkHidden(page, "External Systems");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deployment user sees monitoring links but not the configuration audit log.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeploymentUser_SeesMonitoringButNotConfigurationAuditLog()
|
||||
{
|
||||
@@ -172,6 +198,8 @@ public class RoleNavigationTests
|
||||
|
||||
// ── Multi-role user (Admin + Design + Deployment) ───────────────
|
||||
|
||||
/// <summary>Verifies that the multi-role user sees every navigation section.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MultiRoleUser_SeesAllSections()
|
||||
{
|
||||
@@ -203,6 +231,9 @@ public class RoleNavigationTests
|
||||
|
||||
// ── All users see Dashboard ─────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that every user sees the Dashboard link.</summary>
|
||||
/// <param name="username">The username to sign in with.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("admin")]
|
||||
[InlineData("designer")]
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests;
|
||||
[Collection("Playwright")]
|
||||
public class SandboxWorkerDeploymentTests(PlaywrightFixture fixture)
|
||||
{
|
||||
/// <summary>Verifies that the worker runs scripts, bounds their output and recovers after a noncooperative script.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Medium_WorkerRunsBoundsOutputAndRecoversAfterNoncooperativeScript()
|
||||
{
|
||||
|
||||
+2
@@ -7,6 +7,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests;
|
||||
[Collection("Playwright")]
|
||||
public class SessionActivityTrustedInputTests(PlaywrightFixture fixture)
|
||||
{
|
||||
/// <summary>Verifies that synthetic input and pings stay passive while a trusted key press renews the session with antiforgery.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UI2_SyntheticInputAndPingStayPassive_TrustedKeyRenewsWithAntiforgery()
|
||||
{
|
||||
|
||||
+19
-1
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.SiteCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Direct-SQL seeding helper for the Site Calls page Playwright E2E tests
|
||||
/// (Site Call Audit #22, follow-ups Task 6).
|
||||
/// (Site Call Audit).
|
||||
///
|
||||
/// <para>
|
||||
/// The Site Calls page reads the central <c>SiteCalls</c> table through the
|
||||
@@ -38,6 +38,19 @@ internal static class SiteCallDataSeeder
|
||||
/// needs for its grid assertions. <c>TrackedOperationId</c> is stored as the
|
||||
/// 36-character GUID string the entity mapping expects.
|
||||
/// </summary>
|
||||
/// <param name="trackedOperationId">The tracked operation id of the row.</param>
|
||||
/// <param name="channel">The channel of the site call.</param>
|
||||
/// <param name="target">The target of the site call.</param>
|
||||
/// <param name="sourceSite">The identifier of the originating site.</param>
|
||||
/// <param name="status">The status of the site call.</param>
|
||||
/// <param name="retryCount">The retry count.</param>
|
||||
/// <param name="createdAtUtc">The UTC creation time.</param>
|
||||
/// <param name="updatedAtUtc">The UTC time of the last update.</param>
|
||||
/// <param name="lastError">The last error message, if any.</param>
|
||||
/// <param name="httpStatus">The HTTP status code of the last attempt, if any.</param>
|
||||
/// <param name="terminalAtUtc">The UTC time the call reached a terminal status, if any.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task InsertSiteCallAsync(
|
||||
Guid trackedOperationId,
|
||||
string channel,
|
||||
@@ -84,6 +97,9 @@ VALUES
|
||||
/// starts with <paramref name="targetPrefix"/>. Swallows all errors — the
|
||||
/// prefix carries a per-run GUID so the rows are unique to this test run.
|
||||
/// </summary>
|
||||
/// <param name="targetPrefix">The target prefix identifying the rows to delete.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public static async Task DeleteByTargetPrefixAsync(string targetPrefix, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
@@ -107,6 +123,8 @@ VALUES
|
||||
/// setup on this so a downed cluster surfaces a clear message rather than an
|
||||
/// opaque <see cref="SqlException"/>.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that resolves to <c>true</c> when the configuration database is reachable; otherwise <c>false</c>.</returns>
|
||||
public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
|
||||
+22
-7
@@ -4,8 +4,7 @@ using Xunit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.SiteCalls;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end coverage for the central Site Calls page (Site Call Audit #22,
|
||||
/// follow-ups Task 6).
|
||||
/// End-to-end coverage for the central Site Calls page (Site Call Audit).
|
||||
///
|
||||
/// <para>
|
||||
/// Each test seeds its own <c>SiteCalls</c> rows directly into the running
|
||||
@@ -14,13 +13,13 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.SiteCalls;
|
||||
/// their <c>Target</c> prefix. The Site Calls page reads the <c>SiteCalls</c>
|
||||
/// table through the <c>SiteCallAuditActor</c> (a pure read-from-table mirror),
|
||||
/// so a directly-INSERTed row surfaces exactly as a telemetry-ingested row
|
||||
/// would — the same seeding model the Audit Log E2E tests use. The pattern
|
||||
/// would — the same seeding model the Audit Log end-to-end tests use. The pattern
|
||||
/// keeps each test self-contained without touching
|
||||
/// <c>infra/mssql/seed-config.sql</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Scenarios covered (per the Task 6 brief):
|
||||
/// Scenarios covered:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>PageLoads</c> — the page renders for a Deployment-role user.</item>
|
||||
/// <item><c>FilterNarrowing</c> — a channel filter narrows the results grid.</item>
|
||||
@@ -31,7 +30,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.SiteCalls;
|
||||
/// <item><c>RetryClickThrough</c> — clicking Retry on a Parked row confirms
|
||||
/// the dialog, relays to the owning site, and surfaces an outcome toast.</item>
|
||||
/// <item><c>DiscardClickThrough</c> — clicking Discard on a Parked row confirms
|
||||
/// the danger dialog ("Discard" since the row-28 explicit labels), relays to the owning site, and surfaces an
|
||||
/// the danger dialog ("Discard", an explicit label), relays to the owning site, and surfaces an
|
||||
/// outcome toast.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
@@ -49,6 +48,8 @@ public class SiteCallsPageTests
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public SiteCallsPageTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
@@ -88,6 +89,8 @@ public class SiteCallsPageTests
|
||||
await search.DispatchEventAsync("change");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Site Calls page loads for a user with the Deployment role.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PageLoads_ForDeploymentUser()
|
||||
{
|
||||
@@ -106,6 +109,8 @@ public class SiteCallsPageTests
|
||||
"SiteCallDataSeeder cannot reach MSSQL at localhost:1433 — bring up infra/docker-compose and docker/deploy.sh, " +
|
||||
"or set SCADABRIDGE_PLAYWRIGHT_DB to a reachable connection string.";
|
||||
|
||||
/// <summary>Verifies that applying a channel filter shrinks the results grid.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task FilterNarrowing_ChannelFilterShrinksGrid()
|
||||
{
|
||||
@@ -162,6 +167,8 @@ public class SiteCallsPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the view audit history link navigates to the Audit Log pre-filtered to the call.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DrillIn_ViewAuditHistory_NavigatesToPreFilteredAuditLog()
|
||||
{
|
||||
@@ -209,6 +216,8 @@ public class SiteCallsPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Retry and Discard actions are visible only on parked rows.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task RetryDiscard_VisibleOnlyOnParkedRows()
|
||||
{
|
||||
@@ -266,6 +275,8 @@ public class SiteCallsPageTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retrying a parked row confirms the dialog, relays to the site and shows an outcome toast.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task RetryClickThrough_OnParkedRow_ConfirmsRelayAndShowsOutcomeToast()
|
||||
{
|
||||
@@ -336,9 +347,10 @@ public class SiteCallsPageTests
|
||||
/// Mirrors <see cref="RetryClickThrough_OnParkedRow_ConfirmsRelayAndShowsOutcomeToast"/>
|
||||
/// but exercises the Discard path: clicking Discard opens a <em>danger</em>
|
||||
/// confirm modal (Dialog.ConfirmAsync with <c>danger: true</c>), whose footer
|
||||
/// button is labelled "Discard" (explicit label since row 28; not "Confirm"). Confirming relays a discard to
|
||||
/// button is labelled "Discard" (an explicit label, not "Confirm"). Confirming relays a discard to
|
||||
/// the owning site and <c>ShowRelayOutcome</c> surfaces exactly one outcome toast.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task DiscardClickThrough_OnParkedRow_ConfirmsRelayAndShowsOutcomeToast()
|
||||
{
|
||||
@@ -401,6 +413,7 @@ public class SiteCallsPageTests
|
||||
/// <c>FilterPermittedAsync</c> drops rows whose source site is not permitted.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task StatusFilter_NarrowsToSelectedStatus()
|
||||
{
|
||||
@@ -470,6 +483,7 @@ public class SiteCallsPageTests
|
||||
/// <see cref="IPage.GetByText(string,PageGetByTextOptions)"/> instead.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EmptyState_NoMatch_ShowsEmptyCard()
|
||||
{
|
||||
@@ -513,6 +527,7 @@ public class SiteCallsPageTests
|
||||
/// mid-fetch — reading button state before the fetch settles would race.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Pagination_KeysetNextAndPrev_TraversesPages()
|
||||
{
|
||||
@@ -546,7 +561,7 @@ public class SiteCallsPageTests
|
||||
await SetSearchKeywordAsync(page, sharedTarget);
|
||||
await page.ClickAsync("[data-test='site-calls-query']");
|
||||
|
||||
// The M10 KeysetPager summary span (`Page {N} · {rows} rows`), exposed
|
||||
// The KeysetPager summary span (`Page {N} · {rows} rows`), exposed
|
||||
// via its stable data-test hook so a future markup tweak can't make this
|
||||
// match ambiguous under strict mode.
|
||||
var pageIndicator = page.Locator("[data-test='keyset-summary']");
|
||||
|
||||
@@ -8,11 +8,15 @@ public class SiteCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Creates the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public SiteCrudTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the sites page shows the site management view.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SitesPage_ShowsSiteManagement()
|
||||
{
|
||||
@@ -27,6 +31,8 @@ public class SiteCrudTests
|
||||
await Expect(page.Locator("button:has-text('Add Site')")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the add site button navigates to the create page.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSiteButton_NavigatesToCreatePage()
|
||||
{
|
||||
@@ -41,6 +47,8 @@ public class SiteCrudTests
|
||||
Assert.True(inputCount >= 2, $"Expected at least 2 inputs, found {inputCount}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the back button on the create page returns to the site list.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreatePage_BackButton_ReturnsToList()
|
||||
{
|
||||
@@ -54,6 +62,8 @@ public class SiteCrudTests
|
||||
await Expect(page.Locator("h4:has-text('Site Management')")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the cancel button on the create page returns to the site list.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreatePage_CancelButton_ReturnsToList()
|
||||
{
|
||||
@@ -66,6 +76,8 @@ public class SiteCrudTests
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/create");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the create page has node address subsections.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreatePage_HasNodeSubsections()
|
||||
{
|
||||
@@ -81,6 +93,8 @@ public class SiteCrudTests
|
||||
await Expect(page.Locator(".card-header:has-text('Node B')")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that saving without a name shows an error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreatePage_SaveWithoutName_ShowsError()
|
||||
{
|
||||
@@ -99,6 +113,7 @@ public class SiteCrudTests
|
||||
/// Full create → edit → delete round-trip for a site via the Central UI.
|
||||
/// Uses CliRunner for best-effort teardown so no zztest-* site leaks on failure.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task CreateEditDelete_Site_RoundTrips()
|
||||
{
|
||||
@@ -205,6 +220,7 @@ public class SiteCrudTests
|
||||
/// (a raw EF message) and stays on /create. The Name is deliberately distinct so
|
||||
/// only the SiteIdentifier unique index can trip (not the Name unique index).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task Create_DuplicateIdentifier_ShowsSaveFailedError()
|
||||
{
|
||||
@@ -243,6 +259,7 @@ public class SiteCrudTests
|
||||
/// (like Back) calls GoBack() → /admin/sites with NO dirty-check and NO persistence,
|
||||
/// so re-opening the edit must show the ORIGINAL (empty) Description.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task EditCancel_DiscardsChanges()
|
||||
{
|
||||
|
||||
+8
-4
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Happy-path end-to-end for the Transport Export wizard (Component #24, Task T21).
|
||||
/// Happy-path end-to-end for the Transport Export wizard.
|
||||
///
|
||||
/// <para>
|
||||
/// Drives the full four-step wizard at <c>/design/transport/export</c> against the
|
||||
@@ -40,11 +40,15 @@ public sealed class TransportExportTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture.</param>
|
||||
public TransportExportTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that exporting a throwaway template through the wizard reaches the download summary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ExportTemplate_ReachesDownloadSummary()
|
||||
{
|
||||
@@ -57,7 +61,7 @@ public sealed class TransportExportTests
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/export");
|
||||
// Wait for Step-1 to render (OnInitializedAsync makes 8 repo calls over SignalR
|
||||
// Wait for step 1 to render (OnInitializedAsync makes 8 repo calls over SignalR
|
||||
// before the template tree appears — NetworkIdle is an unreliable proxy for this).
|
||||
await Assertions.Expect(page.Locator("[data-testid='group-templates']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
@@ -85,8 +89,8 @@ public sealed class TransportExportTests
|
||||
|
||||
// ── STEP 2: Review ────────────────────────────────────────────────────────
|
||||
// The seed group lists the picked template; Next is always enabled here.
|
||||
// Scope the Next click to the Step-2 panel — identified by its unique
|
||||
// #include-deps toggle — so it cannot accidentally match a Step-1 button.
|
||||
// Scope the Next click to the step 2 panel — identified by its unique
|
||||
// #include-deps toggle — so it cannot accidentally match a step 1 button.
|
||||
await Assertions.Expect(page.Locator("[data-testid='seed-group']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await page.Locator("div:has(#include-deps) button.btn.btn-primary:has-text('Next')")
|
||||
|
||||
+13
-6
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end round-trip for the Transport Import wizard (Component #24, Task T22).
|
||||
/// End-to-end round-trip for the Transport Import wizard (Transport component).
|
||||
///
|
||||
/// <para>
|
||||
/// The test exercises the full five-step Apply path at
|
||||
@@ -39,17 +39,21 @@ public sealed class TransportImportTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
/// <summary>Initializes the test class with the shared Playwright fixture.</summary>
|
||||
/// <param name="fixture">The shared Playwright fixture providing browser and cluster access.</param>
|
||||
public TransportImportTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that importing a synthetic bundle applies it and shows the audit drill-in.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ImportSyntheticBundle_AppliesAndShowsAuditDrillIn()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
// The source-environment name doubles as the Step-4 confirm phrase, so it
|
||||
// The source-environment name doubles as the final-step confirm phrase, so it
|
||||
// must round-trip verbatim through the bundle manifest and back into the UI.
|
||||
var env = CliRunner.UniqueName("env");
|
||||
var tmplName = CliRunner.UniqueName("imp");
|
||||
@@ -84,7 +88,7 @@ public sealed class TransportImportTests
|
||||
// The bundle is encrypted (we exported with a passphrase), so the page
|
||||
// peeks the manifest with no passphrase, fails to decrypt, and shows the
|
||||
// encrypted-bundle notice instead of the (unencrypted-only) manifest
|
||||
// summary. Either way, the Step-1 Next button only renders once the file
|
||||
// summary. Either way, the first-step Next button only renders once the file
|
||||
// was read successfully — its appearance proves SetInputFiles worked.
|
||||
await Assertions.Expect(page.Locator("[data-testid='encrypted-bundle-notice']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
@@ -175,6 +179,7 @@ public sealed class TransportImportTests
|
||||
/// deleted before import; teardown drops it by name prefix.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ImportWithWrongPassphrase_ShowsErrorAndStaysOnPassphraseStep()
|
||||
{
|
||||
@@ -242,8 +247,8 @@ public sealed class TransportImportTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M8 Map step (#230): drive an import whose bundle carries a <c>Site</c>, so the
|
||||
/// preview surfaces a <see cref="RequiredSiteMapping"/> and the wizard's Step-3
|
||||
/// Map step: drive an import whose bundle carries a <c>Site</c>, so the
|
||||
/// preview surfaces a <see cref="RequiredSiteMapping"/> and the wizard's third-step
|
||||
/// <strong>Map</strong> sub-section (<c>[data-testid='map-section']</c>) renders.
|
||||
///
|
||||
/// <para>
|
||||
@@ -265,6 +270,7 @@ public sealed class TransportImportTests
|
||||
/// wizard.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ImportSiteBundle_RendersMapStep_AndMappingEnablesNext()
|
||||
{
|
||||
@@ -365,7 +371,7 @@ public sealed class TransportImportTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M8 per-line diff (#230): drive an import where a bundled shared script's
|
||||
/// Per-line diff: drive an import where a bundled shared script's
|
||||
/// <c>Code</c> diverges from the destination's, so the preview classifies it
|
||||
/// <see cref="ConflictKind.Modified"/> and the row's "Field diff" carries a
|
||||
/// structured <c>lineDiff</c> — rendered by the <c>LineDiffView</c> component as a
|
||||
@@ -386,6 +392,7 @@ public sealed class TransportImportTests
|
||||
/// required because the line-diff block is not laid out until the disclosure opens.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[SkippableFact]
|
||||
public async Task ImportModifiedSharedScript_RendersPerLineDiff()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user