test(playwright): repair 14 selector-drift failures from two Central UI refactors
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.
This commit is contained in:
+8
-5
@@ -82,9 +82,12 @@ public class AuditConfigurationTests
|
|||||||
await Assertions.Expect(page.Locator("#audit-filter-entity-type")).ToBeVisibleAsync();
|
await Assertions.Expect(page.Locator("#audit-filter-entity-type")).ToBeVisibleAsync();
|
||||||
|
|
||||||
// Filter by the unique marker so only this run's 3 rows remain, then
|
// Filter by the unique marker so only this run's 3 rows remain, then
|
||||||
// Search to load the grid.
|
// Search to load the grid. The Search/Clear pair sits in a
|
||||||
|
// `btn-group btn-group-sm` since the 2026-08-11 UI density sweep, so the
|
||||||
|
// size class lives on the group and NOT on the button — do not re-add
|
||||||
|
// `.btn-sm` to this selector.
|
||||||
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
||||||
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
|
await page.Locator("button.btn.btn-primary:has-text('Search')").ClickAsync();
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
// Seeded rows are isolated: a known row is visible AND the footer total
|
// Seeded rows are isolated: a known row is visible AND the footer total
|
||||||
@@ -116,7 +119,7 @@ public class AuditConfigurationTests
|
|||||||
|
|
||||||
// Filter by the marker so the totals/page-count reflect only this run.
|
// Filter by the marker so the totals/page-count reflect only this run.
|
||||||
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
||||||
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
|
await page.Locator("button.btn.btn-primary:has-text('Search')").ClickAsync();
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
// Page 1 of 2: Previous disabled, Next enabled. (M10 OffsetPager summary:
|
// Page 1 of 2: Previous disabled, Next enabled. (M10 OffsetPager summary:
|
||||||
@@ -161,7 +164,7 @@ public class AuditConfigurationTests
|
|||||||
// Filter by the unique marker and Search so only this run's single
|
// Filter by the unique marker and Search so only this run's single
|
||||||
// large-state row populates the grid.
|
// large-state row populates the grid.
|
||||||
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
||||||
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
|
await page.Locator("button.btn.btn-primary:has-text('Search')").ClickAsync();
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
// Open the modal from the seeded large-state row. The marker is unique,
|
// Open the modal from the seeded large-state row. The marker is unique,
|
||||||
@@ -198,7 +201,7 @@ public class AuditConfigurationTests
|
|||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
await page.Locator("#audit-filter-entity-type").FillAsync(marker);
|
||||||
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
|
await page.Locator("button.btn.btn-primary:has-text('Search')").ClickAsync();
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
// Click the copy-entity-ID button on the (only) seeded row.
|
// Click the copy-entity-ID button on the (only) seeded row.
|
||||||
|
|||||||
+6
-3
@@ -92,10 +92,13 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
|||||||
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
||||||
|
|
||||||
// First select = Site (option value = site.Id); then the name; then Create.
|
// First select = Site (option value = site.Id); then the name; then Create.
|
||||||
|
// The dialog footer pair sits in a `btn-group btn-group-sm` since the
|
||||||
|
// 2026-08-11 UI density sweep, so the size class lives on the group and NOT
|
||||||
|
// on the button — do not re-add `.btn-sm` to the footer selectors here.
|
||||||
await dialog.Locator("select").First.SelectOptionAsync(
|
await dialog.Locator("select").First.SelectOptionAsync(
|
||||||
new SelectOptionValue { Value = _cluster.SiteAId.ToString() });
|
new SelectOptionValue { Value = _cluster.SiteAId.ToString() });
|
||||||
await dialog.Locator("input[placeholder='Area name']").FillAsync(areaName);
|
await dialog.Locator("input[placeholder='Area name']").FillAsync(areaName);
|
||||||
await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Create')").ClickAsync();
|
await dialog.Locator("button.btn.btn-primary:has-text('Create')").ClickAsync();
|
||||||
|
|
||||||
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
await Assertions.Expect(page.Locator($"span.tv-label:has-text('{areaName}')"))
|
await Assertions.Expect(page.Locator($"span.tv-label:has-text('{areaName}')"))
|
||||||
@@ -207,7 +210,7 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
|||||||
// the option's exact visible text. A <select> commits @bind on change, so the
|
// the option's exact visible text. A <select> commits @bind on change, so the
|
||||||
// selection alone flushes _targetParentId (no DispatchEventAsync needed).
|
// selection alone flushes _targetParentId (no DispatchEventAsync needed).
|
||||||
await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = parentName });
|
await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = parentName });
|
||||||
await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Move')").ClickAsync();
|
await dialog.Locator("button.btn.btn-primary:has-text('Move')").ClickAsync();
|
||||||
|
|
||||||
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
}
|
}
|
||||||
@@ -241,7 +244,7 @@ public class TopologyAreaTests : IClassFixture<DeploymentFixture>
|
|||||||
var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text(\"Move '\"))");
|
var dialog = page.Locator(".modal.show:has(h6.modal-title:has-text(\"Move '\"))");
|
||||||
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
||||||
await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = areaName });
|
await dialog.Locator("select").SelectOptionAsync(new SelectOptionValue { Label = areaName });
|
||||||
await dialog.Locator("button.btn.btn-primary.btn-sm:has-text('Move')").ClickAsync();
|
await dialog.Locator("button.btn.btn-primary:has-text('Move')").ClickAsync();
|
||||||
|
|
||||||
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,10 +65,13 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
|||||||
// Click Add Attribute.
|
// Click Add Attribute.
|
||||||
await page.ClickAsync("button.btn.btn-primary.btn-sm:has-text('Add Attribute')");
|
await page.ClickAsync("button.btn.btn-primary.btn-sm:has-text('Add Attribute')");
|
||||||
|
|
||||||
// The modal is a page-local .modal.show.d-block — NOT the global DialogHost.
|
// TemplateEdit's four page-embedded modals were migrated onto the global
|
||||||
// DialogHost adds a `fade` class; the page-local modal does not, so :not(.fade)
|
// DialogService host, so the Add-Attribute modal IS the DialogHost modal now
|
||||||
// ensures we match only the page-local Add-Attribute modal.
|
// (`modal fade show d-block`, title in an h5). The old `:not(.fade)` guard
|
||||||
var modal = page.Locator(".modal.show.d-block:not(.fade)");
|
// against the page-local variant would exclude it — TemplateEdit renders no
|
||||||
|
// page-local modal of its own any more, so `.modal.show.d-block` is
|
||||||
|
// unambiguous on this page.
|
||||||
|
var modal = page.Locator(".modal.show.d-block");
|
||||||
await Assertions.Expect(modal).ToBeVisibleAsync();
|
await Assertions.Expect(modal).ToBeVisibleAsync();
|
||||||
await Assertions.Expect(modal.Locator(".modal-title")).ToHaveTextAsync("Add Attribute");
|
await Assertions.Expect(modal.Locator(".modal-title")).ToHaveTextAsync("Add Attribute");
|
||||||
|
|
||||||
@@ -242,12 +245,13 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
|||||||
await page.ClickAsync("button[aria-label=\"More actions for Val\"]");
|
await page.ClickAsync("button[aria-label=\"More actions for Val\"]");
|
||||||
await page.ClickAsync("button.dropdown-item:has-text('Edit…')");
|
await page.ClickAsync("button.dropdown-item:has-text('Edit…')");
|
||||||
|
|
||||||
// The page-local attribute modal is .modal.show.d-block WITHOUT `fade`
|
// The attribute modal is rendered by the global DialogService host since
|
||||||
// (the DialogHost confirm modal HAS `fade`). :not(.fade) pins the
|
// TemplateEdit's page-embedded modals were migrated to it — `modal fade show
|
||||||
// page-local one.
|
// d-block` with the title in an h5. TemplateEdit renders no page-local modal
|
||||||
var modal = page.Locator(".modal.show.d-block:not(.fade)");
|
// of its own, so `.modal.show.d-block` is unambiguous on this page.
|
||||||
|
var modal = page.Locator(".modal.show.d-block");
|
||||||
await Assertions.Expect(modal).ToBeVisibleAsync();
|
await Assertions.Expect(modal).ToBeVisibleAsync();
|
||||||
await Assertions.Expect(modal.Locator("h6.modal-title")).ToHaveTextAsync("Edit Attribute");
|
await Assertions.Expect(modal.Locator("h5.modal-title")).ToHaveTextAsync("Edit Attribute");
|
||||||
|
|
||||||
// When editing, the Name input is rendered readonly (readonly="@editing").
|
// When editing, the Name input is rendered readonly (readonly="@editing").
|
||||||
await Assertions.Expect(
|
await Assertions.Expect(
|
||||||
|
|||||||
+8
-4
@@ -109,8 +109,10 @@ public class NotificationActionTests
|
|||||||
var (page, row) = await SeedAndLocateParkedRowAsync(notificationId, marker, subject);
|
var (page, row) = await SeedAndLocateParkedRowAsync(notificationId, marker, subject);
|
||||||
|
|
||||||
// The Retry button is only rendered for Parked rows (btn-outline-success).
|
// The Retry button is only rendered for Parked rows (btn-outline-success).
|
||||||
await Assertions.Expect(row.Locator("button.btn.btn-outline-success.btn-sm")).ToBeVisibleAsync();
|
// Its size class moved onto the enclosing `btn-group btn-group-sm` in the
|
||||||
await row.Locator("button.btn.btn-outline-success.btn-sm").ClickAsync();
|
// 2026-08-11 UI density sweep — do not re-add `.btn-sm` here.
|
||||||
|
await Assertions.Expect(row.Locator("button.btn.btn-outline-success")).ToBeVisibleAsync();
|
||||||
|
await row.Locator("button.btn.btn-outline-success").ClickAsync();
|
||||||
|
|
||||||
// Confirm the action — non-danger footer button labelled "Confirm".
|
// Confirm the action — non-danger footer button labelled "Confirm".
|
||||||
var confirmButton = page.Locator(".modal-footer .btn-primary");
|
var confirmButton = page.Locator(".modal-footer .btn-primary");
|
||||||
@@ -146,8 +148,10 @@ public class NotificationActionTests
|
|||||||
var (page, row) = await SeedAndLocateParkedRowAsync(notificationId, marker, subject);
|
var (page, row) = await SeedAndLocateParkedRowAsync(notificationId, marker, subject);
|
||||||
|
|
||||||
// The Discard button is only rendered for Parked rows (btn-outline-danger).
|
// The Discard button is only rendered for Parked rows (btn-outline-danger).
|
||||||
await Assertions.Expect(row.Locator("button.btn.btn-outline-danger.btn-sm")).ToBeVisibleAsync();
|
// Its size class moved onto the enclosing `btn-group btn-group-sm` in the
|
||||||
await row.Locator("button.btn.btn-outline-danger.btn-sm").ClickAsync();
|
// 2026-08-11 UI density sweep — do not re-add `.btn-sm` here.
|
||||||
|
await Assertions.Expect(row.Locator("button.btn.btn-outline-danger")).ToBeVisibleAsync();
|
||||||
|
await row.Locator("button.btn.btn-outline-danger").ClickAsync();
|
||||||
|
|
||||||
// Confirm the action — danger footer button labelled "Delete" (the discard
|
// Confirm the action — danger footer button labelled "Delete" (the discard
|
||||||
// dialog opens with danger: true).
|
// dialog opens with danger: true).
|
||||||
|
|||||||
+6
-2
@@ -65,7 +65,11 @@ public class NotificationListCrudTests
|
|||||||
await Assertions.Expect(listRow).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
await Assertions.Expect(listRow).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
||||||
|
|
||||||
// ── ADD RECIPIENT ─────────────────────────────────────────────────────────
|
// ── ADD RECIPIENT ─────────────────────────────────────────────────────────
|
||||||
await listRow.Locator("button.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync();
|
// 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
|
// 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.
|
// loads the edit form's data); wait for it to settle before asserting the heading.
|
||||||
@@ -96,7 +100,7 @@ public class NotificationListCrudTests
|
|||||||
var listRowAgain = page.Locator("tr").Filter(new() { HasText = name });
|
var listRowAgain = page.Locator("tr").Filter(new() { HasText = name });
|
||||||
// Make the row locator strict-mode-safe: assert exactly one match before acting.
|
// Make the row locator strict-mode-safe: assert exactly one match before acting.
|
||||||
await Assertions.Expect(listRowAgain).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
await Assertions.Expect(listRowAgain).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
||||||
await listRowAgain.Locator("button.btn-outline-danger.btn-sm:has-text('Delete')").ClickAsync();
|
await listRowAgain.Locator("button.btn-outline-danger:has-text('Delete')").ClickAsync();
|
||||||
|
|
||||||
// Confirm the global danger dialog.
|
// Confirm the global danger dialog.
|
||||||
await Assertions.Expect(page.Locator(".modal-footer .btn-danger")).ToBeVisibleAsync();
|
await Assertions.Expect(page.Locator(".modal-footer .btn-danger")).ToBeVisibleAsync();
|
||||||
|
|||||||
+3
-1
@@ -172,7 +172,9 @@ public class SmsNotificationE2ETests
|
|||||||
// ── OPEN EDIT — recipient inputs only render once the list exists ────────────
|
// ── OPEN EDIT — recipient inputs only render once the list exists ────────────
|
||||||
var listRow = page.Locator("tr").Filter(new() { HasText = name });
|
var listRow = page.Locator("tr").Filter(new() { HasText = name });
|
||||||
await Assertions.Expect(listRow).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
await Assertions.Expect(listRow).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
||||||
await listRow.Locator("button.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync();
|
// Row actions live in a `btn-group btn-group-sm` since the 2026-08-11 UI
|
||||||
|
// density sweep — the size class is on the group, not the button.
|
||||||
|
await listRow.Locator("button.btn-outline-primary:has-text('Edit')").ClickAsync();
|
||||||
|
|
||||||
// Blazor enhanced navigation (SignalR round-trip) loads the edit form's data.
|
// Blazor enhanced navigation (SignalR round-trip) loads the edit form's data.
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|||||||
@@ -74,8 +74,11 @@ public class SiteCrudTests
|
|||||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/sites/create");
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/sites/create");
|
||||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
await Expect(page.Locator("h6:has-text('Node A')")).ToBeVisibleAsync();
|
// The 2026-08-11 UI density sweep re-cast the per-node subsections from bare
|
||||||
await Expect(page.Locator("h6:has-text('Node B')")).ToBeVisibleAsync();
|
// <h6> headings to Bootstrap cards, so the section label is now the card header
|
||||||
|
// (`.card-header > span.fw-semibold`) rather than a heading element.
|
||||||
|
await Expect(page.Locator(".card-header:has-text('Node A')")).ToBeVisibleAsync();
|
||||||
|
await Expect(page.Locator(".card-header:has-text('Node B')")).ToBeVisibleAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user