diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs index 3b937abb..5b16db54 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs @@ -120,4 +120,96 @@ public class TemplateCrudTests } } } + + [SkippableFact] + public async Task CreateTemplate_DuplicateName_ShowsInlineError() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + // CLI-seed an existing base template, then UI-attempt to create a duplicate. + // Base (non-derived) Template.Name has a unique index + // (HasIndex(t => t.Name).IsUnique().HasFilter("[IsDerived]=0")) and + // TemplateService.CreateTemplateAsync has no friendly duplicate pre-check, + // so the DB-constraint exception is caught into _formError and rendered inline + // in div.text-danger.small with no navigation (stays on /create). + // (Empirically confirmed: duplicate create surfaces inline; duplicate-name path used.) + var name = CliRunner.UniqueName("tmpl"); + var seededId = await CliRunner.CreateTemplateAsync(name); + + try + { + var page = await _fixture.NewAuthenticatedPageAsync(); + + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Fill the Name input with the duplicate name and click Create. + await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name); + await page.ClickAsync("button.btn.btn-success:has-text('Create')"); + + // Web-first assertions: the inline error becomes visible and we stay on /create. + // Do NOT assert a literal message — it is the DB-constraint exception text. + await Assertions.Expect(page.Locator("div.text-danger.small")).ToBeVisibleAsync(); + await Assertions.Expect(page) + .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/design/templates/create")); + } + finally + { + // Delete the seeded source template, then sweep any leftover by name. + await CliRunner.DeleteTemplateAsync(seededId); + try + { + foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name)) + { + await CliRunner.DeleteTemplateAsync(id); + } + } + catch + { + // Best-effort — swallow to avoid masking the original test failure. + } + } + } + + [SkippableFact] + public async Task CreateTemplate_Cancel_ReturnsToListWithoutCreating() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var name = CliRunner.UniqueName("tmpl"); + + try + { + var page = await _fixture.NewAuthenticatedPageAsync(); + + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Fill the Name input, then click Cancel — Blazor navigates back to the list. + await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name); + await page.ClickAsync("button.btn.btn-outline-secondary:has-text('Cancel')"); + + // excludePath: "/create" rejects the /design/templates/create URL we came from. + await PlaywrightFixture.WaitForPathAsync(page, "/design/templates", excludePath: "/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Nothing was created: no template exists with our unique name. + Assert.Empty(await CliRunner.ListTemplateIdsByNamePrefixAsync(name)); + } + finally + { + // Defensive sweep by name in case of an unexpected create. + try + { + foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name)) + { + await CliRunner.DeleteTemplateAsync(id); + } + } + catch + { + // Best-effort — swallow to avoid masking the original test failure. + } + } + } }