diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/SharedScriptCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/SharedScriptCrudTests.cs
new file mode 100644
index 00000000..fd6ca564
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/SharedScriptCrudTests.cs
@@ -0,0 +1,97 @@
+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 SharedScriptCrudTests
+{
+ private readonly PlaywrightFixture _pw;
+
+ public SharedScriptCrudTests(PlaywrightFixture pw)
+ {
+ _pw = pw;
+ }
+
+ ///
+ /// CLI-create a shared script, then exercise the Central UI list render + UI delete
+ /// round-trip. The create/edit form is driven by a Monaco editor (brittle to type),
+ /// so authoring happens via the CLI; the UI delete is the behavior under test. A
+ /// best-effort CLI delete in finally guarantees no zztest-script-* definition leaks
+ /// if the UI path fails mid-way (the happy path already deletes via the UI).
+ ///
+ [SkippableFact]
+ public async Task CliCreated_Script_DeletesViaCard()
+ {
+ Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
+
+ var name = CliRunner.UniqueName("script");
+ int id = await CliRunner.CreateSharedScriptAsync(name);
+
+ try
+ {
+ var page = await _pw.NewAuthenticatedPageAsync();
+ await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/shared-scripts");
+ await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ await Assertions.Expect(page.Locator("h4:has-text('Shared Scripts')")).ToBeVisibleAsync();
+
+ // Narrow the list so the card locator stays unambiguous even with many scripts.
+ await page.Locator("input[placeholder='Filter by name or code…']").FillAsync(name);
+
+ var card = page.Locator("div.card").Filter(new() { HasText = name });
+ await Assertions.Expect(card).ToHaveCountAsync(1, new() { Timeout = 10_000 });
+
+ // Scope the kebab + delete item to the card's .dropdown for strict-mode safety.
+ var dropdown = card.Locator(".dropdown");
+ await dropdown.Locator("button[aria-label^='More actions']").ClickAsync();
+ await dropdown.Locator(".dropdown-menu button.dropdown-item.text-danger").ClickAsync();
+
+ await Assertions.Expect(page.Locator(".modal-title:has-text('Delete Shared Script')")).ToBeVisibleAsync();
+ await page.Locator(".modal-footer .btn-danger").ClickAsync();
+
+ // Single web-first assertion on the success toast — toasts auto-dismiss, so we
+ // do NOT chase the 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(page.Locator("div.card").Filter(new() { HasText = name }))
+ .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.
+ await CliRunner.DeleteSharedScriptAsync(id);
+ }
+ }
+
+ ///
+ /// The create form renders: heading, the (enabled) Name input, the editor tabs, and
+ /// the Monaco editor surface. Asserts render only — does NOT click Save, Check Syntax,
+ /// or Test Run (Test Run fires real I/O). Mutates nothing.
+ ///
+ [SkippableFact]
+ public async Task CreateForm_Renders()
+ {
+ Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
+
+ var page = await _pw.NewAuthenticatedPageAsync();
+
+ await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/shared-scripts/create");
+ await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ await Assertions.Expect(page.Locator("h4:has-text('New Shared Script')")).ToBeVisibleAsync();
+
+ var nameInput = page.Locator("input[type=text].form-control.form-control-sm");
+ await Assertions.Expect(nameInput).ToBeVisibleAsync();
+ await Assertions.Expect(nameInput).ToBeEnabledAsync();
+
+ await Assertions.Expect(page.Locator("button.nav-link:has-text('Code')")).ToBeVisibleAsync();
+ await Assertions.Expect(page.Locator("button.nav-link:has-text('Parameters')")).ToBeVisibleAsync();
+ await Assertions.Expect(page.Locator("button.nav-link:has-text('Return type')")).ToBeVisibleAsync();
+
+ // Monaco mounts asynchronously via JS interop — give it a generous timeout.
+ await Assertions.Expect(page.Locator(".monaco-editor")).ToBeVisibleAsync(new() { Timeout = 15_000 });
+ }
+}