014038fbbd
All 14 pre-existing Playwright failures triage to selector drift, not rig data,
timing, or application defects — every one is a test-side assumption that two
merged UI refactors invalidated without updating this suite.
`9e243493` (2026-08-11 density sweep + Theme 0.4.1) regrouped page/dialog action
buttons into `btn-group btn-group-sm`, moving the size class off the button and
onto the group. Twelve selectors still pinned `.btn-sm` on the button itself:
ConfigurationAuditLog's Search (4 tests), the Topology Create/Move dialog footers
(3), the Notification Lists row Edit/Delete (2), and the Notification Report row
Retry/Discard (2). The same sweep re-cast SiteForm's per-node subsections from
bare `<h6>` headings to Bootstrap cards, so `h6:has-text('Node A')` matches
nothing (1). Note the swept surfaces are a subset — the recipient table on the
notification-list edit form, OffsetPager, DiffDialog and friends still carry
`.btn-sm` on the button, so the selectors are deliberately asymmetric; the
remaining `.btn-sm` selectors in this suite were each re-verified against current
markup rather than swept along.
`a506b19d` moved TemplateEdit's page-embedded modals onto the DialogService host.
The attribute modal is now the DialogHost modal (`modal fade show d-block`, title
in an h5), so the `:not(.fade)` guard written to exclude DialogHost — plus the
`h6.modal-title` assertion — excluded the very modal under test (2). TemplateEdit
renders no page-local modal any more, so the guard has nothing left to guard.
Comments at each site record why the size class is absent, so a future reader
does not "fix" the selectors back.
Verified: 14 failed / 158 passed baseline -> the 36 tests in the seven affected
classes all green; solution build 0 warnings / 0 errors.
142 lines
7.8 KiB
C#
142 lines
7.8 KiB
C#
using Microsoft.Playwright;
|
|
using Xunit;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;
|
|
|
|
/// <summary>
|
|
/// Full-UI round-trip coverage for Notification Lists (<c>/notifications/lists</c>).
|
|
///
|
|
/// <para>
|
|
/// This page requires the Design role; the test user <c>multi-role</c> has it. The
|
|
/// happy-path fact drives the entire lifecycle through the UI only — create the list,
|
|
/// open it for edit, add a recipient, remove that recipient, then delete the list and
|
|
/// assert the confirm-dialog + success-toast + row-gone sequence. A <c>finally</c> block
|
|
/// performs best-effort CLI teardown so no <c>zztest-notiflist-*</c> list leaks on failure.
|
|
/// The validation fact mutates nothing.
|
|
/// </para>
|
|
/// </summary>
|
|
[Collection("Playwright")]
|
|
public class NotificationListCrudTests
|
|
{
|
|
private const string ListsUrl = "/notifications/lists";
|
|
|
|
private readonly PlaywrightFixture _pw;
|
|
|
|
public NotificationListCrudTests(PlaywrightFixture pw)
|
|
{
|
|
_pw = pw;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drives the complete Notification List lifecycle through the UI: create the list,
|
|
/// 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>
|
|
[SkippableFact]
|
|
public async Task Create_AddRecipient_RemoveRecipient_Delete_RoundTrips()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var name = CliRunner.UniqueName("notiflist");
|
|
const string recipEmail = "zzrec@example.invalid";
|
|
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
|
|
try
|
|
{
|
|
// ── CREATE ────────────────────────────────────────────────────────────────
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ListsUrl}/create");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
await Assertions.Expect(page.Locator("h4:has-text('Add Notification List')")).ToBeVisibleAsync();
|
|
|
|
// The create form has exactly one input (the list name); recipients are edit-only.
|
|
await page.Locator("input.form-control").First.FillAsync(name);
|
|
await page.Locator("button.btn-success:has-text('Save')").ClickAsync();
|
|
|
|
// On success the form redirects back to the list page (no toast).
|
|
await PlaywrightFixture.WaitForPathAsync(page, ListsUrl, excludePath: "/create");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
var listRow = page.Locator("tr").Filter(new() { HasText = name });
|
|
await Assertions.Expect(listRow).ToBeVisibleAsync();
|
|
// Make the row locator strict-mode-safe: assert exactly one match before acting.
|
|
await Assertions.Expect(listRow).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
|
|
|
// ── ADD RECIPIENT ─────────────────────────────────────────────────────────
|
|
// The list page's row actions moved into a `btn-group btn-group-sm` in the
|
|
// 2026-08-11 UI density sweep, so the size class sits on the group, not the
|
|
// button. The recipient table on the *edit form* was not swept and still
|
|
// carries `.btn-sm` on the button itself — hence the asymmetry below.
|
|
await listRow.Locator("button.btn-outline-primary:has-text('Edit')").ClickAsync();
|
|
|
|
// The Edit click triggers Blazor enhanced navigation (a SignalR round-trip that
|
|
// loads the edit form's data); wait for it to settle before asserting the heading.
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
await Assertions.Expect(page.Locator("h4:has-text('Edit Notification List')")).ToBeVisibleAsync();
|
|
|
|
// The edit page has TWO "Name" text inputs (list name + recipient name). Scope the
|
|
// recipient form to the card that contains the email input to disambiguate.
|
|
var recipientCard = page.Locator(".card").Filter(new() { Has = page.Locator("input[type=email]") });
|
|
await recipientCard.Locator("input[type=text].form-control").FillAsync("zzrec");
|
|
await recipientCard.Locator("input[type=email]").FillAsync(recipEmail);
|
|
await recipientCard.Locator("button.btn-success:has-text('Add')").ClickAsync();
|
|
|
|
var recipientRow = page.Locator("tr").Filter(new() { HasText = recipEmail });
|
|
await Assertions.Expect(recipientRow).ToBeVisibleAsync();
|
|
|
|
// ── REMOVE RECIPIENT ──────────────────────────────────────────────────────
|
|
// Direct delete — no confirm dialog.
|
|
await recipientRow.Locator("button.btn-outline-danger.btn-sm:has-text('Delete')").ClickAsync();
|
|
await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = recipEmail }))
|
|
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
|
|
|
// ── DELETE LIST ───────────────────────────────────────────────────────────
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ListsUrl}");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
var listRowAgain = page.Locator("tr").Filter(new() { HasText = name });
|
|
// Make the row locator strict-mode-safe: assert exactly one match before acting.
|
|
await Assertions.Expect(listRowAgain).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
|
await listRowAgain.Locator("button.btn-outline-danger:has-text('Delete')").ClickAsync();
|
|
|
|
// Confirm the global danger dialog.
|
|
await Assertions.Expect(page.Locator(".modal-footer .btn-danger")).ToBeVisibleAsync();
|
|
await page.Locator(".modal-footer .btn-danger").ClickAsync();
|
|
|
|
// Assert the success toast in one web-first check — count + body text together — so the
|
|
// second assertion can't race the toast's 5s auto-dismiss.
|
|
await Assertions.Expect(page.Locator(".toast", new() { HasText = "Deleted." }))
|
|
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
|
await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = name }))
|
|
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
|
}
|
|
finally
|
|
{
|
|
foreach (var id in await CliRunner.ListNotificationListIdsByNamePrefixAsync(name))
|
|
await CliRunner.DeleteNotificationListAsync(id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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>
|
|
[SkippableFact]
|
|
public async Task CreateForm_EmptyName_ShowsInlineError()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ListsUrl}/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 required.')")).ToBeVisibleAsync();
|
|
await Assertions.Expect(page).ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/create"));
|
|
}
|
|
}
|