ui: Central UI density/consistency sweep + Theme 0.4.1
Applies the family-wide admin-UI cleanup playbook to the Central UI so the Blazor surfaces stop diverging from the shared kit: buttons are grouped rather than individually sized, long cell values are contained instead of widening tables, and hard-coded colours give way to theme tokens. The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell, which the kit emits as an inline style on the shell root. Being a descendant of <html>, it beat the [data-bs-theme="dark"] override for the entire app, so the dark accent had never rendered. Declaring --accent in site.css :root instead lets both schemes resolve; light is unchanged because the value already matched the kit's light default. Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so that block is deleted here rather than duplicated. Verified byte-identical before removal; the repo now declares no --bs-btn-* anywhere. NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages, SiteCallsReport) were additionally refactored from holding the selected record to holding its id and re-resolving from the current page each render, with the resolve doubling as the visibility gate. A background refresh that drops the row now closes the modal instead of showing a stale snapshot. This is a behaviour change and is called out rather than buried: a full-suite run turned up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack (GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler was disposed between render and click — a window the previous field-held record made structurally impossible. Treat the modal lifecycle here as unreviewed. Build 0/0; suite green apart from that one intermittent failure.
This commit is contained in:
@@ -43,7 +43,7 @@ public class LdapMappingCrudTests
|
||||
// violation. The LdapMappingForm.razor uses Blazor @bind, not <form>,
|
||||
// so the "Role" label's parent div.mb-2 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.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
// Wait for Blazor enhanced navigation back to the list page.
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/ldap-mappings", excludePath: "/create");
|
||||
@@ -62,7 +62,7 @@ public class LdapMappingCrudTests
|
||||
|
||||
// Change the role to Viewer and save.
|
||||
await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm").SelectOptionAsync("Viewer");
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/ldap-mappings", excludePath: "/edit");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
@@ -148,7 +148,7 @@ public class LdapMappingCrudTests
|
||||
// FIRST in SaveMapping) is unambiguously the one that fires.
|
||||
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.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
await Assertions.Expect(page.Locator("div.text-danger.small.mt-2"))
|
||||
.ToHaveTextAsync("LDAP Group Name is required.");
|
||||
@@ -162,7 +162,7 @@ public class LdapMappingCrudTests
|
||||
.FillAsync(group);
|
||||
await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm")
|
||||
.SelectOptionAsync(new[] { "" });
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
await Assertions.Expect(page.Locator("div.text-danger.small.mt-2"))
|
||||
.ToHaveTextAsync("Role is required.");
|
||||
@@ -200,7 +200,7 @@ public class LdapMappingCrudTests
|
||||
.FillAsync(group);
|
||||
await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm")
|
||||
.SelectOptionAsync("Viewer");
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
// The catch block prefixes the raw DB message with "Save failed:" — assert
|
||||
// only on that stable prefix, never the provider-specific tail.
|
||||
|
||||
@@ -117,7 +117,7 @@ public class M9SurfaceTests
|
||||
|
||||
// ── CREATE ────────────────────────────────────────────────────────────────────
|
||||
await nameInput.FillAsync(name);
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
// Success toast fires and the row appears in the table with its lib: reference.
|
||||
await Assertions.Expect(page.Locator(".toast", new() { HasText = "created" }))
|
||||
|
||||
@@ -79,8 +79,10 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
// The select is label-anchored: <label>Data Type</label> + <select class="form-select">.
|
||||
await modal.Locator("div.col-12:has(label:has-text('Data Type')) select.form-select").SelectOptionAsync("Double");
|
||||
|
||||
// Click the footer Add button (btn-success btn-sm, text "Add").
|
||||
await modal.Locator(".modal-footer button.btn-success.btn-sm:has-text('Add')").ClickAsync();
|
||||
// Click the footer Add button (btn-success, text "Add"). The dialog feet were
|
||||
// wrapped in a btn-group btn-group-sm by the 2026-08-11 UI sweep, so the size
|
||||
// class now lives on the group rather than on the button itself.
|
||||
await modal.Locator(".modal-footer button.btn-success:has-text('Add')").ClickAsync();
|
||||
|
||||
// The modal should dismiss and the attribute table should show "Val".
|
||||
await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
@@ -89,7 +91,7 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
|
||||
// ── DELETE ────────────────────────────────────────────────────────────────
|
||||
// Click the header Delete button (btn-outline-danger btn-sm, text "Delete").
|
||||
await page.ClickAsync("button.btn.btn-outline-danger.btn-sm:has-text('Delete')");
|
||||
await page.ClickAsync("button.btn.btn-outline-danger:has-text('Delete')");
|
||||
|
||||
// The global DialogHost renders a confirm dialog.
|
||||
// The danger confirm button carries class "btn-danger" and text "Delete".
|
||||
@@ -258,8 +260,9 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
var valueInput = modal.Locator("div.col-12:has(label:has-text('Value')) input.form-control");
|
||||
await valueInput.FillAsync("42.5");
|
||||
|
||||
// Footer button text is "Save" when editing (vs "Add" when adding).
|
||||
await modal.Locator(".modal-footer button.btn-success.btn-sm:has-text('Save')").ClickAsync();
|
||||
// Footer button text is "Save" when editing (vs "Add" when adding). Size class
|
||||
// sits on the enclosing btn-group-sm since the 2026-08-11 UI sweep.
|
||||
await modal.Locator(".modal-footer button.btn-success:has-text('Save')").ClickAsync();
|
||||
|
||||
// SaveAttribute persists then reloads, dismissing the modal.
|
||||
await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
@@ -306,7 +309,7 @@ public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Header Delete button (btn-outline-danger btn-sm).
|
||||
await page.ClickAsync("button.btn.btn-outline-danger.btn-sm:has-text('Delete')");
|
||||
await page.ClickAsync("button.btn.btn-outline-danger:has-text('Delete')");
|
||||
|
||||
// Confirm via the global DialogHost modal's danger button (labelled
|
||||
// "Delete"). This modal HAS the `fade` class — distinct from the
|
||||
|
||||
@@ -130,7 +130,7 @@ public class SiteCrudTests
|
||||
await grpcInputs.Nth(0).FillAsync("http://zz:8083");
|
||||
await grpcInputs.Nth(1).FillAsync("http://zz:8083");
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
// Wait for Blazor enhanced navigation back to the list page.
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/create");
|
||||
@@ -149,7 +149,7 @@ public class SiteCrudTests
|
||||
|
||||
// Update the description and save.
|
||||
await page.Locator("label:has-text('Description') + input.form-control.form-control-sm").FillAsync("e2e-edited");
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/edit");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
@@ -222,7 +222,7 @@ public class SiteCrudTests
|
||||
await page.Locator("label:has-text('Identifier') + input.form-control.form-control-sm").FillAsync("site-a");
|
||||
await page.Locator("label:has-text('Name') + input.form-control.form-control-sm").FillAsync(distinctName);
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
|
||||
// The inline error surface must report the failed save and we must stay on /create.
|
||||
// The full message is a raw DbUpdateException — assert only the "Save failed" prefix.
|
||||
@@ -260,7 +260,7 @@ public class SiteCrudTests
|
||||
await page.Locator("label:has-text('Identifier') + input.form-control.form-control-sm").FillAsync(ident);
|
||||
await page.Locator("label:has-text('Name') + input.form-control.form-control-sm").FillAsync(name);
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Save')");
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ public class NodeBrowserDialogSearchTests : BunitContext
|
||||
// Click the first result's link — this drives the SAME selection
|
||||
// mechanism the tree uses, so the footer Select button confirms and
|
||||
// OnSelected fires.
|
||||
cut.FindAll("[data-test=node-search-result] a")[0].Click();
|
||||
cut.FindAll("[data-test=node-search-result] button.btn-link")[0].Click();
|
||||
cut.Find(".modal-footer .btn-primary").Click();
|
||||
|
||||
Assert.Equal("ns=2;s=Pump1.Speed", _capturedSelection());
|
||||
|
||||
+5
-3
@@ -46,7 +46,7 @@ public class NodeBrowserDialogSelectionTests : BunitContext
|
||||
|
||||
cut.Find("[data-test=node-search-input]").Input("Pump1");
|
||||
cut.Find("[data-test=node-search-button]").Click();
|
||||
cut.FindAll("[data-test=node-search-result] a")[0].Click();
|
||||
cut.FindAll("[data-test=node-search-result] button.btn-link")[0].Click();
|
||||
cut.Find(".modal-footer .btn-primary").Click();
|
||||
|
||||
Assert.NotNull(captured);
|
||||
@@ -90,8 +90,10 @@ public class NodeBrowserDialogSelectionTests : BunitContext
|
||||
cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-MX", null));
|
||||
cut.Render();
|
||||
|
||||
// Click the tree node's select link (tree path, not search).
|
||||
cut.Find(".node-browser-tree a").Click();
|
||||
// Click the tree node's select control (tree path, not search). It is a
|
||||
// <button class="btn btn-link"> since the 2026-08-11 UI sweep replaced the
|
||||
// href="javascript:void(0)" anchor; both click handlers were preserved.
|
||||
cut.Find(".node-browser-tree button.btn-link").Click();
|
||||
cut.Find(".modal-footer .btn-primary").Click();
|
||||
|
||||
Assert.NotNull(captured);
|
||||
|
||||
+3
-4
@@ -175,10 +175,10 @@ public class TransportExportPageTests : BunitContext
|
||||
Assert.NotNull(cut.Find($"[data-testid='{groupId}']"));
|
||||
}
|
||||
|
||||
// The API-keys selection group is gone, replaced by an info note explaining
|
||||
// that keys must be re-created per environment.
|
||||
// The API-keys selection group is gone. The explanatory note that used to sit
|
||||
// beside it moved into docs/requirements/Component-Transport.md (UI cleanup
|
||||
// sweep 2026-08-11) — the absence of the group is the assertion that matters.
|
||||
Assert.Empty(cut.FindAll("[data-testid='group-api-keys']"));
|
||||
Assert.NotNull(cut.Find("[data-testid='api-keys-not-transported']"));
|
||||
|
||||
// Sanity: each artifact shows its label.
|
||||
Assert.Contains("Helpers", cut.Markup);
|
||||
@@ -354,7 +354,6 @@ public class TransportExportPageTests : BunitContext
|
||||
|
||||
// The Sites & Instances group is present alongside the existing artifact groups.
|
||||
Assert.NotNull(cut.Find("[data-testid='group-sites']"));
|
||||
Assert.NotNull(cut.Find("[data-testid='sites-hint']"));
|
||||
Assert.Contains("North Plant", cut.Markup);
|
||||
Assert.Contains("north", cut.Markup);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user