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