using Microsoft.Playwright; using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design; /// /// End-to-end round-trip for the Template design pages: /// create → add attribute → delete, all via the Central UI against the /// running dev cluster. /// [Collection("Playwright")] public class TemplateCrudTests { private readonly PlaywrightFixture _fixture; public TemplateCrudTests(PlaywrightFixture fixture) { _fixture = fixture; } [SkippableFact] public async Task CreateAddAttributeDelete_Template_RoundTrips() { Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); var name = $"zztest-tmpl-{Guid.NewGuid().ToString("N")[..8]}"; var page = await _fixture.NewAuthenticatedPageAsync(); try { // ── CREATE ──────────────────────────────────────────────────────────────── await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); // Name input is label-anchored: // followed by . // Use the label's sibling input scoped to the containing div to avoid any // strict-mode violation from the Description input (also form-control). await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name); // Leave Parent Template at the default "(None - root template)". // Click the green Create button. await page.ClickAsync("button.btn.btn-success:has-text('Create')"); // After a successful create, Blazor navigates to /design/templates/{id}. // Poll window.location until the path matches /design/templates/ + digits // and does not still say /create. await PlaywrightFixture.WaitForPathAsync(page, "/design/templates/", excludePath: "/create"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); // Sanity: we must be on a numeric template detail URL. Assert.Matches(@"/design/templates/\d+$", page.Url); // ── ADD ATTRIBUTE ───────────────────────────────────────────────────────── // The Attributes tab is the default-active tab (_activeTab = "attributes"), // so we don't need to click it, but we do wait for the tab panel to render. await Assertions.Expect( page.Locator("button.nav-link:has-text('Attributes')")) .ToBeVisibleAsync(); // Click Add Attribute. await page.ClickAsync("button.btn.btn-primary.btn-sm:has-text('Add Attribute')"); // The modal is a page-local .modal.show.d-block — NOT the global DialogHost. // DialogHost adds a `fade` class; the page-local modal does not, so :not(.fade) // ensures we match only the page-local Add-Attribute modal. var modal = page.Locator(".modal.show.d-block:not(.fade)"); await Assertions.Expect(modal).ToBeVisibleAsync(); await Assertions.Expect(modal.Locator(".modal-title")).ToHaveTextAsync("Add Attribute"); // Fill Name field inside the modal. await modal.Locator("div.col-12:has(label:has-text('Name')) input.form-control").FillAsync("Val"); // Select Data Type = Double. // The select is label-anchored: +