diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs new file mode 100644 index 00000000..09c18bb4 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs @@ -0,0 +1,86 @@ +using Microsoft.Playwright; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Admin; + +/// +/// End-to-end CRUD round-trip for the LDAP Group Mappings admin page. +/// Covers create → edit → delete via the UI against the running dev cluster. +/// +[Collection("Playwright")] +public class LdapMappingCrudTests +{ + private readonly PlaywrightFixture _fixture; + + public LdapMappingCrudTests(PlaywrightFixture fixture) + { + _fixture = fixture; + } + + [SkippableFact] + public async Task CreateEditDelete_LdapMapping_RoundTrips() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + // Keep the group name short and unique to avoid collisions with other test runs. + var group = $"zztest-grp-{Guid.NewGuid():N}"[..18]; + + var page = await _fixture.NewAuthenticatedPageAsync(); + + // ── CREATE ──────────────────────────────────────────────────────────────── + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/ldap-mappings/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // The LDAP Group Name label has no `for=` attribute so GetByLabel does not + // work. Locate the input that immediately follows the label text instead. + await page.Locator("label:has-text('LDAP Group Name') + input.form-control.form-control-sm").FillAsync(group); + await page.SelectOptionAsync(".form-select.form-select-sm", "Designer"); + await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')"); + + // Wait for Blazor enhanced navigation back to the list page. + await PlaywrightFixture.WaitForPathAsync(page, "/admin/ldap-mappings", excludePath: "/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // The new row must be visible on the list. + var newRow = page.Locator("tr", new() { HasText = group }); + await Assertions.Expect(newRow).ToBeVisibleAsync(); + + // ── EDIT ────────────────────────────────────────────────────────────────── + // Click the Edit button within that row. + await newRow.Locator("button.btn.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync(); + + await PlaywrightFixture.WaitForPathAsync(page, "/edit"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Change the role to Viewer and save. + await page.SelectOptionAsync(".form-select.form-select-sm", "Viewer"); + await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')"); + + await PlaywrightFixture.WaitForPathAsync(page, "/admin/ldap-mappings", excludePath: "/edit"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Row must still be present after the edit. + var editedRow = page.Locator("tr", new() { HasText = group }); + await Assertions.Expect(editedRow).ToBeVisibleAsync(); + + // ── DELETE (no confirmation dialog) ─────────────────────────────────────── + // Scope all dropdown interactions to the row's .dropdown container so we + // never accidentally match a Delete button from another row's menu. + var rowDropdown = editedRow.Locator(".dropdown"); + var kebab = rowDropdown.Locator("button[aria-label^='More actions']"); + await kebab.ClickAsync(); + + // Click Delete in the now-open dropdown within this row — no confirm dialog. + var deleteBtn = rowDropdown.Locator(".dropdown-menu button.dropdown-item.text-danger"); + await deleteBtn.ClickAsync(); + + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // The row must be gone. + await Assertions.Expect(page.Locator("tr", new() { HasText = group })) + .ToHaveCountAsync(0, new() { Timeout = 10_000 }); + + // TODO: best-effort cleanup if a mid-test failure left a zztest-grp-* row. + // The happy path deletes via the UI above; no CLI cleanup needed for the passing case. + } +}