diff --git a/tests/ScadaLink.CentralUI.Tests/TemplatesPageTests.cs b/tests/ScadaLink.CentralUI.Tests/TemplatesPageTests.cs
new file mode 100644
index 0000000..736a0bc
--- /dev/null
+++ b/tests/ScadaLink.CentralUI.Tests/TemplatesPageTests.cs
@@ -0,0 +1,129 @@
+using System.Security.Claims;
+using Bunit;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using ScadaLink.Commons.Entities.Templates;
+using ScadaLink.Commons.Interfaces.Repositories;
+using ScadaLink.Commons.Interfaces.Services;
+using ScadaLink.TemplateEngine;
+using ScadaLink.TemplateEngine.Services;
+using TemplatesPage = ScadaLink.CentralUI.Components.Pages.Design.Templates;
+
+namespace ScadaLink.CentralUI.Tests;
+
+///
+/// bUnit rendering tests for the Templates page that verify the folder/template
+/// tree builds the expected DOM for the main shape categories: empty state,
+/// folder-containing-template nesting, and composition leaves under their owner.
+///
+public class TemplatesPageTests : BunitContext
+{
+ private readonly ITemplateEngineRepository _repo = Substitute.For();
+ private readonly IAuditService _audit = Substitute.For();
+
+ public TemplatesPageTests()
+ {
+ // The page's TemplateService / TemplateFolderService are constructed via DI
+ // from the repository and audit service, mirroring real Host wiring.
+ Services.AddSingleton(_repo);
+ Services.AddSingleton(_audit);
+ Services.AddScoped();
+ Services.AddScoped();
+ AddTestAuth();
+
+ // The TreeView inside the page persists expansion state via JS interop
+ // against sessionStorage (`templates-tree` key). bUnit requires explicit
+ // stubs for all JS interop calls, otherwise rendering throws.
+ JSInterop.Setup("treeviewStorage.load", _ => true).SetResult(null);
+ JSInterop.SetupVoid("treeviewStorage.save", _ => true);
+ }
+
+ private void AddTestAuth()
+ {
+ // The page resolves the current user via the "Username" claim in
+ // GetCurrentUserAsync(); supply a stub so OnInitializedAsync doesn't crash.
+ var claims = new[]
+ {
+ new Claim("Username", "tester"),
+ new Claim(ClaimTypes.Role, "Design")
+ };
+ var identity = new ClaimsIdentity(claims, "TestAuth");
+ var user = new ClaimsPrincipal(identity);
+ Services.AddSingleton(new TestAuthStateProvider(user));
+ Services.AddAuthorizationCore();
+ }
+
+ [Fact]
+ public void Renders_EmptyState_WhenNoTemplatesOrFolders()
+ {
+ _repo.GetAllTemplatesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+ _repo.GetAllFoldersAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+
+ var cut = Render();
+
+ Assert.Contains("No templates yet", cut.Markup);
+ }
+
+ [Fact]
+ public void Renders_FolderAndTemplate_AtCorrectNesting()
+ {
+ var folder = new TemplateFolder("Dev") { Id = 1 };
+ var template = new Template("TestMachine") { Id = 5, FolderId = 1 };
+ _repo.GetAllTemplatesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List { template }));
+ _repo.GetAllFoldersAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List { folder }));
+
+ var cut = Render();
+
+ // The folder is rendered collapsed; assert the folder label is present,
+ // then expand it and assert the nested template label appears.
+ Assert.Contains("Dev", cut.Markup);
+
+ var folderToggle = cut.FindAll("li[role='treeitem']")
+ .FirstOrDefault(li => li.TextContent.Contains("Dev"))
+ ?.QuerySelector(".tv-toggle");
+ Assert.NotNull(folderToggle);
+ folderToggle!.Click();
+
+ Assert.Contains("TestMachine", cut.Markup);
+ }
+
+ [Fact]
+ public void Renders_CompositionChildren_UnderOwningTemplate()
+ {
+ var template = new Template("TestMachine") { Id = 5 };
+ template.Compositions.Add(
+ new TemplateComposition("DelmiaReceiver") { Id = 10, ComposedTemplateId = 99 });
+ var composed = new Template("Other") { Id = 99 };
+
+ _repo.GetAllTemplatesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List { template, composed }));
+ _repo.GetAllFoldersAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+
+ var cut = Render();
+
+ // The owning template must be expanded for its composition leaves to be
+ // in the DOM — composition children only render under an expanded parent.
+ var ownerToggle = cut.FindAll("li[role='treeitem']")
+ .FirstOrDefault(li => li.TextContent.Contains("TestMachine"))
+ ?.QuerySelector(".tv-toggle");
+ Assert.NotNull(ownerToggle);
+ ownerToggle!.Click();
+
+ Assert.Contains("DelmiaReceiver", cut.Markup);
+ Assert.Contains("→", cut.Markup);
+ }
+}
+
+internal sealed class TestAuthStateProvider : AuthenticationStateProvider
+{
+ private readonly ClaimsPrincipal _user;
+ public TestAuthStateProvider(ClaimsPrincipal user) => _user = user;
+ public override Task GetAuthenticationStateAsync()
+ => Task.FromResult(new AuthenticationState(_user));
+}