diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs
new file mode 100644
index 00000000..41c2a532
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Design/TemplateCrudTests.cs
@@ -0,0 +1,120 @@
+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.
+ var modal = page.Locator(".modal.show.d-block");
+ 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: +