diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ExternalSystemCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ExternalSystemCrudTests.cs
new file mode 100644
index 00000000..d870f87b
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/ExternalSystemCrudTests.cs
@@ -0,0 +1,104 @@
+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;
+
+ public ExternalSystemCrudTests(PlaywrightFixture pw)
+ {
+ _pw = pw;
+ }
+
+ ///
+ /// 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).
+ ///
+ [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();
+
+ // Name and Endpoint URL are both input[type=text].form-control. Per
+ // ExternalSystemForm.razor, Name is the first text input and Endpoint URL
+ // is the second — select by order.
+ var textInputs = page.Locator("input[type=text].form-control");
+ await textInputs.Nth(0).FillAsync(name);
+ await textInputs.Nth(1).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);
+ }
+ }
+
+ ///
+ /// Saving the create form with both fields blank surfaces the inline validation
+ /// error and keeps the user on the create page. Mutates nothing.
+ ///
+ [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"));
+ }
+}