Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ExternalSystemCrudTests.cs
T
Joseph Doherty 513fa1aa16 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.
2026-10-07 15:05:49 -04:00

111 lines
5.5 KiB
C#

using Microsoft.Playwright;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
using Xunit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
[Collection("Playwright")]
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;
}
/// <summary>
/// Full create → card → delete round-trip for an external system via the Central UI.
/// 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()
{
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
var name = CliRunner.UniqueName("extsys");
var page = await _pw.NewAuthenticatedPageAsync();
try
{
// ── CREATE ────────────────────────────────────────────────────────────────
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems/create");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Assertions.Expect(page.Locator("h4:has-text('Add External System')")).ToBeVisibleAsync();
// 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 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);
await page.Locator("div.mb-3:has(label:has-text('Endpoint URL')) input[type=text].form-control")
.FillAsync("https://example.invalid/api");
await page.Locator("button.btn-success:has-text('Save')").ClickAsync();
// Save redirects back to the list (no toast).
await PlaywrightFixture.WaitForPathAsync(page, "/design/external-systems", excludePath: "/create");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// The new card must be present (and unique) before we act on it.
var card = page.Locator("div.card").Filter(new() { HasText = name });
await Assertions.Expect(card).ToHaveCountAsync(1, new() { Timeout = 10_000 });
// ── DELETE ────────────────────────────────────────────────────────────────
// Scope the kebab + delete item to the card's .dropdown for strict-mode safety.
var cardDropdown = card.Locator(".dropdown");
await cardDropdown.Locator("button[aria-label^='More actions']").ClickAsync();
await cardDropdown.Locator(".dropdown-menu button.dropdown-item.text-danger").ClickAsync();
// Confirm the delete dialog.
await Assertions.Expect(page.Locator(".modal-title:has-text('Delete External System')")).ToBeVisibleAsync();
await page.Locator(".modal-footer .btn-danger").ClickAsync();
// Single web-first assertion on the success toast — toasts auto-dismiss at 5s,
// so we do NOT chase .toast-body in a second sequential check.
await Assertions.Expect(page.Locator(".toast", new() { HasText = "Deleted." }))
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
// The card must be gone.
await Assertions.Expect(card).ToHaveCountAsync(0, new() { Timeout = 10_000 });
}
finally
{
// Best-effort teardown: the happy path deletes via the UI, so this finds
// nothing; it only fires if the UI path failed mid-way.
foreach (var id in await CliRunner.ListExternalSystemIdsByNamePrefixAsync(name))
await CliRunner.DeleteExternalSystemAsync(id);
}
}
/// <summary>
/// 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()
{
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
var page = await _pw.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems/create");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await page.Locator("button.btn-success:has-text('Save')").ClickAsync();
await Assertions.Expect(page.Locator("div.text-danger.small:has-text('Name and URL required.')"))
.ToBeVisibleAsync();
await Assertions.Expect(page)
.ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/create"));
}
}