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 }); + } +}