Procházet zdrojové kódy

Merge pull request #148 from Timmoth/v1.0.0-prep

Added data-testid
Tim Jones před 1 měsícem
rodič
revize
d816ed4b49

+ 43 - 27
Shared.Rcl/AccessPoints/AccessPointCardComponent.razor

@@ -5,11 +5,15 @@
 @inject ICloneResourceUseCase<AccessPoint> CloneUseCase
 @inject NavigationManager Nav
 
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"accesspoint-item-{AccessPoint.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
 
         <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{AccessPoint.Name}")" class="block">
+            <NavLink href="@($"resources/hardware/{AccessPoint.Name}")"
+                     class="block"
+                     data-testid="open-accesspoint-link">
                 @AccessPoint.Name
             </NavLink>
         </div>
@@ -17,34 +21,40 @@
         <div class="flex gap-3 text-xs">
             @if (!_isEditing)
             {
-                <button class="text-zinc-400 hover:text-zinc-200"
+                <button data-testid="edit-accesspoint-button"
+                        class="text-zinc-400 hover:text-zinc-200"
                         @onclick="BeginEdit">
                     Edit
                 </button>
 
-                <button class="text-blue-400 hover:text-blue-300"
+                <button data-testid="rename-accesspoint-button"
+                        class="text-blue-400 hover:text-blue-300"
                         @onclick="OpenRename">
                     Rename
                 </button>
 
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="clone-accesspoint-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="OpenClone">
                     Clone
                 </button>
 
-                <button class="text-red-400 hover:text-red-300"
+                <button data-testid="delete-accesspoint-button"
+                        class="text-red-400 hover:text-red-300"
                         @onclick="ConfirmDelete">
                     Delete
                 </button>
             }
             else
             {
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="save-accesspoint-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="Save">
                     Save
                 </button>
 
-                <button class="text-zinc-500 hover:text-zinc-300"
+                <button data-testid="cancel-accesspoint-button"
+                        class="text-zinc-500 hover:text-zinc-300"
                         @onclick="Cancel">
                     Cancel
                 </button>
@@ -55,38 +65,40 @@
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- Model -->
-        <div>
+        <div data-testid="accesspoint-model-section">
             <div class="text-zinc-400 mb-1">Model</div>
 
             @if (_isEditing)
             {
-                <input class="w-full px-3 py-2 rounded-md
-                          bg-zinc-800 text-zinc-100
-                          border border-zinc-600"
-                       @bind="_edit.Model"/>
+                <input data-testid="accesspoint-model-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Model" />
             }
             else if (!string.IsNullOrWhiteSpace(AccessPoint.Model))
             {
-                <div class="text-zinc-300">@AccessPoint.Model</div>
+                <div class="text-zinc-300"
+                     data-testid="accesspoint-model-value">
+                    @AccessPoint.Model
+                </div>
             }
         </div>
 
         <!-- Speed -->
-        <div>
+        <div data-testid="accesspoint-speed-section">
             <div class="text-zinc-400 mb-1">Speed (Gbps)</div>
 
             @if (_isEditing)
             {
                 <input type="number"
                        step="0.1"
-                       class="w-full px-3 py-2 rounded-md
-                          bg-zinc-800 text-zinc-100
-                          border border-zinc-600"
-                       @bind="_edit.Speed"/>
+                       data-testid="accesspoint-speed-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Speed" />
             }
             else if (AccessPoint.Speed is not null)
             {
-                <div class="text-zinc-300">
+                <div class="text-zinc-300"
+                     data-testid="accesspoint-speed-value">
                     @AccessPoint.Speed Gbps
                 </div>
             }
@@ -94,25 +106,27 @@
 
         <ResourceTagEditor Resource="AccessPoint"/>
 
-        <div class="md:col-span-2">
+        <div class="md:col-span-2"
+             data-testid="accesspoint-notes-section">
+
             <div class="text-zinc-400 mb-1">Notes</div>
 
             @if (_isEditing)
             {
                 <MarkdownEditor
                     @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                    ShowActionButtons="false"
+                    TestIdPrefix="accesspoint-notes-editor" />
             }
             else
             {
                 <MarkdownViewer
                     Value="@AccessPoint.Notes"
-                    ShowEditButton="false"/>
+                    ShowEditButton="false"
+                    TestIdPrefix="accesspoint-notes-viewer" />
             }
         </div>
-
     </div>
-
 </div>
 
 <ConfirmModal
@@ -133,7 +147,8 @@
     Description="Enter a new name for this accesspoint"
     Label="New accesspoint name"
     Value="@AccessPoint.Name"
-    OnSubmit="HandleRenameSubmit"/>
+    OnSubmit="HandleRenameSubmit"
+    TestIdPrefix="accesspoint-rename"/>
 
 <StringValueModal
     IsOpen="_cloneOpen"
@@ -142,7 +157,8 @@
     Description="Enter a name for the cloned resource"
     Label="New resource name"
     Value="@($"{AccessPoint.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+    OnSubmit="HandleCloneSubmit"
+    TestIdPrefix="accesspoint-clone"/>
 
 @code {
     [Parameter] [EditorRequired] public AccessPoint AccessPoint { get; set; } = default!;

+ 15 - 3
Shared.Rcl/Components/MarkdownEditor.razor

@@ -1,14 +1,19 @@
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid="@BaseTestId">
 
     @if (ShowActionButtons)
     {
-        <div class="flex justify-end gap-3 mb-2 text-xs">
+        <div class="flex justify-end gap-3 mb-2 text-xs"
+             data-testid="@($"{BaseTestId}-actions")">
+
             <button class="text-emerald-400 hover:text-emerald-300 transition"
+                    data-testid="@($"{BaseTestId}-save")"
                     @onclick="HandleSave">
                 Save
             </button>
 
             <button class="text-zinc-500 hover:text-zinc-300 transition"
+                    data-testid="@($"{BaseTestId}-cancel")"
                     @onclick="HandleCancel">
                 Cancel
             </button>
@@ -17,6 +22,7 @@
 
     <textarea
         class="w-full h-64 bg-zinc-950 text-zinc-200 border border-zinc-700 rounded p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500"
+        data-testid="@($"{BaseTestId}-textarea")"
         value="@Value"
         @oninput="HandleInput">
     </textarea>
@@ -32,6 +38,13 @@
     [Parameter] public EventCallback OnSave { get; set; }
     [Parameter] public EventCallback OnCancel { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "markdown-editor"
+            : $"{TestIdPrefix}-markdown-editor";
+
     async Task HandleInput(ChangeEventArgs e)
     {
         Value = e.Value?.ToString();
@@ -49,5 +62,4 @@
         if (OnCancel.HasDelegate)
             await OnCancel.InvokeAsync();
     }
-
 }

+ 21 - 7
Shared.Rcl/Components/MarkdownViewer.razor

@@ -1,10 +1,15 @@
 @using Markdig
-<div class="relative border border-zinc-800 rounded p-4 bg-zinc-900">
+
+<div class="relative border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid="@BaseTestId">
 
     @if (ShowEditButton)
     {
-        <div class="absolute top-2 right-2 z-10">
+        <div class="absolute top-2 right-2 z-10"
+             data-testid="@($"{BaseTestId}-edit-container")">
+
             <button class="text-xs text-blue-400 hover:text-blue-300 transition"
+                    data-testid="@($"{BaseTestId}-edit-button")"
                     @onclick="HandleEdit">
                 @(string.IsNullOrWhiteSpace(Value) ? "Add" : "Edit")
             </button>
@@ -13,13 +18,15 @@
 
     @if (string.IsNullOrWhiteSpace(Value))
     {
-        <div class="text-sm text-zinc-500 italic">
+        <div class="text-sm text-zinc-500 italic"
+             data-testid="@($"{BaseTestId}-empty")">
             No notes
         </div>
     }
     else
     {
-        <div class="markdown text-sm">
+        <div class="markdown text-sm"
+             data-testid="@($"{BaseTestId}-content")">
             @((MarkupString)_html)
         </div>
     }
@@ -31,6 +38,13 @@
     [Parameter] public bool ShowEditButton { get; set; }
     [Parameter] public EventCallback OnEdit { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "markdown-viewer"
+            : $"{TestIdPrefix}-markdown-viewer";
+
     private string _html = string.Empty;
 
     private static readonly MarkdownPipeline Pipeline =
@@ -45,8 +59,8 @@
 
     private Task HandleEdit()
     {
-        return OnEdit.HasDelegate ? OnEdit.InvokeAsync() : Task.CompletedTask;
+        return OnEdit.HasDelegate
+            ? OnEdit.InvokeAsync()
+            : Task.CompletedTask;
     }
-
-
 }

+ 107 - 155
Shared.Rcl/Desktops/DesktopCardComponent.razor

@@ -4,82 +4,79 @@
 @using RackPeek.Domain.UseCases.Drives
 @using RackPeek.Domain.UseCases.Gpus
 @using RackPeek.Domain.UseCases.Nics
-@inject IGetAllResourcesByKindUseCase<Desktop> GetAllUseCase
 @inject IGetResourceByNameUseCase<Desktop> GetByNameUseCase
-
-@inject UpdateDesktopUseCase UpdateDesktopUseCase
-@inject IDeleteResourceUseCase<Desktop> DeleteDesktopUseCase
-
+@inject UpdateDesktopUseCase UpdateUseCase
+@inject IDeleteResourceUseCase<Desktop> DeleteUseCase
 @inject IAddCpuUseCase<Desktop> AddCpuUseCase
 @inject IUpdateCpuUseCase<Desktop> UpdateCpuUseCase
 @inject IRemoveCpuUseCase<Desktop> RemoveCpuUseCase
-
 @inject IAddDriveUseCase<Desktop> AddDriveUseCase
 @inject IUpdateDriveUseCase<Desktop> UpdateDriveUseCase
 @inject IRemoveDriveUseCase<Desktop> RemoveDriveUseCase
-
 @inject IAddNicUseCase<Desktop> AddNicUseCase
 @inject IUpdateNicUseCase<Desktop> UpdateNicUseCase
 @inject IRemoveNicUseCase<Desktop> RemoveNicUseCase
-
 @inject IAddGpuUseCase<Desktop> AddGpuUseCase
 @inject IUpdateGpuUseCase<Desktop> UpdateGpuUseCase
 @inject IRemoveGpuUseCase<Desktop> RemoveGpuUseCase
 @inject ICloneResourceUseCase<Desktop> CloneUseCase
-
 @inject IRenameResourceUseCase<Desktop> RenameUseCase
 @inject NavigationManager Nav
 
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"desktop-item-{Desktop.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
+
         <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Desktop.Name}")" class="block">
+            <NavLink href="@($"resources/hardware/{Desktop.Name}")"
+                     class="block"
+                     data-testid="open-desktop-link">
                 @Desktop.Name
             </NavLink>
         </div>
 
-        <div class="flex justify-between items-center mb-3">
+        <div class="flex items-center gap-2">
+
             @if (!string.IsNullOrWhiteSpace(Desktop.Model))
             {
-                <span class="text-xs text-zinc-400">
+                <span class="text-xs text-zinc-400"
+                      data-testid="desktop-model-badge">
                     @Desktop.Model
                 </span>
             }
-            <div class="flex items-center gap-2">
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
-                        @onclick="OpenRename">
-                    Rename
-                </button>
-                <button
+
+            <button data-testid="rename-desktop-button"
+                    class="text-xs text-blue-400 hover:text-blue-300 transition"
+                    @onclick="OpenRename">
+                Rename
+            </button>
+
+            <button data-testid="clone-desktop-button"
                     class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
                     @onclick="OpenClone">
-                    Clone
-                </button>
-                <button
+                Clone
+            </button>
+
+            <button data-testid="delete-desktop-button"
                     class="text-xs text-red-400 hover:text-red-300 transition"
-                    title="Delete server"
                     @onclick="ConfirmDelete">
-                    Delete
-                </button>
-            </div>
-        </div>
-
+                Delete
+            </button>
 
+        </div>
     </div>
 
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- CPU -->
-        <div>
+        <div data-testid="desktop-cpu-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     CPU
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add CPU"
-                        @onclick="OpenAddCpu">
+                    <button data-testid="add-cpu-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddCpu">
                         +
                     </button>
                 </div>
@@ -89,12 +86,10 @@
             {
                 @foreach (var cpu in Desktop.Cpus)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            title="Edit CPU"
-                            @onclick="() => OpenEditCpu(cpu)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-cpu-{cpu.ToString().Replace(" ", "-")}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditCpu(cpu)">
                             @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
                         </button>
                     </div>
@@ -103,24 +98,22 @@
         </div>
 
         <!-- RAM -->
-        <div>
+        <div data-testid="desktop-ram-section">
             <div class="text-zinc-400 mb-1">
                 RAM
-                <button
-                    class="hover:text-emerald-400 transition"
-                    title="Edit RAM"
-                    @onclick="EditRam">
+                <button data-testid="edit-ram-button"
+                        class="hover:text-emerald-400 transition"
+                        @onclick="EditRam">
                     +
                 </button>
             </div>
 
             @if (Desktop.Ram is not null)
             {
-                <div
-                    class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                    <button
-                        class="hover:text-emerald-400"
-                        @onclick="EditRam">
+                <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                    <button data-testid="ram-value-button"
+                            class="hover:text-emerald-400"
+                            @onclick="EditRam">
                         @($"{Desktop.Ram.Size} GB {Desktop.Ram.Mts} MT/s")
                     </button>
                 </div>
@@ -128,14 +121,13 @@
         </div>
 
         <!-- Drives -->
-        <div>
+        <div data-testid="desktop-drive-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     Drives
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add Drive"
-                        @onclick="OpenAddDrive">
+                    <button data-testid="add-drive-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddDrive">
                         +
                     </button>
                 </div>
@@ -145,11 +137,10 @@
             {
                 @foreach (var drive in Desktop.Drives)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            @onclick="() => OpenEditDrive(drive)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-drive-{drive.Type}-{drive.Size}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditDrive(drive)">
                             @drive.Type — @drive.Size GB
                         </button>
                     </div>
@@ -158,14 +149,13 @@
         </div>
 
         <!-- NICs -->
-        <div>
+        <div data-testid="desktop-nic-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     NICs
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add NIC"
-                        @onclick="OpenAddNic">
+                    <button data-testid="add-nic-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddNic">
                         +
                     </button>
                 </div>
@@ -175,11 +165,10 @@
             {
                 @foreach (var nic in Desktop.Nics)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            @onclick="() => OpenEditNic(nic)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-nic-{nic.Type}-{nic.Speed}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditNic(nic)">
                             @nic.Type — @nic.Speed Gbps (@nic.Ports ports)
                         </button>
                     </div>
@@ -188,14 +177,13 @@
         </div>
 
         <!-- GPUs -->
-        <div>
+        <div data-testid="desktop-gpu-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     GPUs
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add GPU"
-                        @onclick="OpenAddGpu">
+                    <button data-testid="add-gpu-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddGpu">
                         +
                     </button>
                 </div>
@@ -205,11 +193,10 @@
             {
                 @foreach (var gpu in Desktop.Gpus)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            @onclick="() => OpenEditGpu(gpu)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-gpu-{gpu.Model}-{gpu.Vram}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditGpu(gpu)">
                             @gpu.Model — @gpu.Vram GB VRAM
                         </button>
                     </div>
@@ -221,89 +208,54 @@
 
     </div>
 
-    <div class="md:col-span-2">
+    <div class="md:col-span-2" data-testid="desktop-notes-section">
         <div class="text-zinc-400 mb-1">Notes</div>
 
         @if (!_editingNotes)
         {
-            <MarkdownViewer
-                Value="@Desktop.Notes"
-                ShowEditButton="true"
-                OnEdit="BeginNotesEdit"/>
+            <MarkdownViewer Value="@Desktop.Notes"
+                            ShowEditButton="true"
+                            OnEdit="BeginNotesEdit"
+                            TestIdPrefix="desktop-notes-viewer"/>
         }
         else
         {
-            <MarkdownEditor
-                @bind-Value="_notesDraft"
-                ShowActionButtons="true"
-                OnSave="SaveNotes"
-                OnCancel="CancelNotesEdit"/>
+            <MarkdownEditor @bind-Value="_notesDraft"
+                            ShowActionButtons="true"
+                            OnSave="SaveNotes"
+                            OnCancel="CancelNotesEdit"
+                            TestIdPrefix="desktop-notes-editor"/>
         }
     </div>
-
 </div>
-<CpuModal
-    IsOpen="@_cpuModalOpen"
-    IsOpenChanged="v => _cpuModalOpen = v"
-    Value="@_editingCpu"
-    OnSubmit="HandleCpuSubmit"
-    OnDelete="HandleCpuDelete"/>
-
-<RamModal
-    IsOpen="@_isRamModalOpen"
-    IsOpenChanged="v => _isRamModalOpen = v"
-    Value="@Desktop.Ram"
-    OnSubmit="HandleRamSubmit"/>
-
-<DriveModal
-    IsOpen="@_driveModalOpen"
-    IsOpenChanged="v => _driveModalOpen = v"
-    Value="@_editingDrive"
-    OnSubmit="HandleDriveSubmit"
-    OnDelete="HandleDriveDelete"/>
-
-<NicModal
-    IsOpen="@_nicModalOpen"
-    IsOpenChanged="v => _nicModalOpen = v"
-    Value="@_editingNic"
-    OnSubmit="HandleNicSubmit"
-    OnDelete="HandleNicDelete"/>
-
-<GpuModal
-    IsOpen="@_gpuModalOpen"
-    IsOpenChanged="v => _gpuModalOpen = v"
-    Value="@_editingGpu"
-    OnSubmit="HandleGpuSubmit"
-    OnDelete="HandleGpuDelete"/>
-
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete server"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteServer"
-    TestIdPrefix="Desktop">
+
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete desktop"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteServer"
+              TestIdPrefix="Desktop">
     Are you sure you want to delete <strong>@Desktop.Name</strong>?
-    <br/>
-    This will detach all dependent systems.
 </ConfirmModal>
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename desktop"
-    Description="Enter a new name for this desktop"
-    Label="New desktop name"
-    Value="@Desktop.Name"
-    OnSubmit="HandleRenameSubmit"/>
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Desktop.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename desktop"
+                  Description="Enter a new name for this desktop"
+                  Label="New desktop name"
+                  Value="@Desktop.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="desktop-rename"/>
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Desktop.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="desktop-clone"/>
 
 @code {
     [Parameter] [EditorRequired] public Desktop Desktop { get; set; } = default!;
@@ -320,7 +272,7 @@
     private async Task HandleRamSubmit(Ram value)
     {
         _isRamModalOpen = false;
-        await UpdateDesktopUseCase.ExecuteAsync(Desktop.Name, Desktop.Model, value.Size, value.Mts);
+        await UpdateUseCase.ExecuteAsync(Desktop.Name, Desktop.Model, value.Size, value.Mts);
         Desktop = await GetByNameUseCase.ExecuteAsync(Desktop.Name);
     }
 
@@ -541,7 +493,7 @@
     {
         _confirmDeleteOpen = false;
 
-        await DeleteDesktopUseCase.ExecuteAsync(Desktop.Name);
+        await DeleteUseCase.ExecuteAsync(Desktop.Name);
 
         if (OnDeleted.HasDelegate)
             await OnDeleted.InvokeAsync(Desktop.Name);
@@ -605,7 +557,7 @@
     {
         _editingNotes = false;
 
-        await UpdateDesktopUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Desktop.Name,
             Desktop.Model,
             Desktop.Ram?.Size,

+ 102 - 90
Shared.Rcl/Firewalls/FirewallCardComponent.razor

@@ -1,61 +1,66 @@
 @using RackPeek.Domain.Resources.Firewalls
 @using RackPeek.Domain.Resources.SubResources
 @using RackPeek.Domain.UseCases.Ports
-@inject UpdateFirewallUseCase UpdateFirewallUseCase
-@inject IGetAllResourcesByKindUseCase<Firewall> GetAllUseCase
+@inject UpdateFirewallUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<Firewall> GetByNameUseCase
-@inject IAddPortUseCase<Firewall> AddFirewallPortUseCase
-@inject IUpdatePortUseCase<Firewall> UpdateFirewallPortUseCase
-@inject IRemovePortUseCase<Firewall> RemoveFirewallPortUseCase
+@inject IAddPortUseCase<Firewall> AddPortUseCase
+@inject IUpdatePortUseCase<Firewall> UpdatePortUseCase
+@inject IRemovePortUseCase<Firewall> RemovePortUseCase
 @inject IDeleteResourceUseCase<Firewall> DeleteUseCase
 @inject ICloneResourceUseCase<Firewall> CloneUseCase
-
 @inject IRenameResourceUseCase<Firewall> RenameUseCase
 @inject NavigationManager Nav
 
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"firewall-item-{Firewall.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
 
         <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Firewall.Name}")" class="block">
-
+            <NavLink href="@($"resources/hardware/{Firewall.Name}")"
+                     class="block"
+                     data-testid="open-firewall-link">
                 @Firewall.Name
             </NavLink>
         </div>
 
-
         <div class="flex gap-3 text-xs">
             @if (!_isEditing)
             {
-                <button class="text-zinc-400 hover:text-zinc-200"
+                <button data-testid="edit-firewall-button"
+                        class="text-zinc-400 hover:text-zinc-200"
                         @onclick="BeginEdit">
                     Edit
                 </button>
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
+
+                <button data-testid="rename-firewall-button"
+                        class="text-blue-400 hover:text-blue-300 transition"
                         @onclick="OpenRename">
                     Rename
                 </button>
-                <button
-                    class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
-                    @onclick="OpenClone">
+
+                <button data-testid="clone-firewall-button"
+                        class="text-emerald-400 hover:text-emerald-300 transition"
+                        @onclick="OpenClone">
                     Clone
                 </button>
-                <button
-                    class="text-xs text-red-400 hover:text-red-300 transition"
-                    title="Delete server"
-                    @onclick="ConfirmDelete">
+
+                <button data-testid="delete-firewall-button"
+                        class="text-red-400 hover:text-red-300 transition"
+                        @onclick="ConfirmDelete">
                     Delete
                 </button>
             }
             else
             {
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="save-firewall-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="Save">
                     Save
                 </button>
-                <button class="text-zinc-500 hover:text-zinc-300"
+
+                <button data-testid="cancel-firewall-button"
+                        class="text-zinc-500 hover:text-zinc-300"
                         @onclick="Cancel">
                     Cancel
                 </button>
@@ -66,41 +71,42 @@
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- Model -->
-        <div>
+        <div data-testid="firewall-model-section">
             <div class="text-zinc-400 mb-1">Model</div>
+
             @if (_isEditing)
             {
-                <input
-                    class="w-full px-3 py-2 rounded-md
-                           bg-zinc-800 text-zinc-100
-                           border border-zinc-600
-                           placeholder-zinc-500
-                           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
-                           hover:border-zinc-400
-                           transition-colors duration-150
-                           cursor-text"
-                    @bind="_edit.Model"/>
+                <input data-testid="firewall-model-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Model" />
             }
             else if (!string.IsNullOrWhiteSpace(Firewall.Model))
             {
-                <div class="text-zinc-300">@Firewall.Model</div>
+                <div class="text-zinc-300"
+                     data-testid="firewall-model-value">
+                    @Firewall.Model
+                </div>
             }
         </div>
 
         <!-- Features -->
-        <div>
+        <div data-testid="firewall-features-section">
             <div class="text-zinc-400 mb-1">Features</div>
 
             @if (_isEditing)
             {
                 <div class="flex gap-4">
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Managed"/>
+                        <input type="checkbox"
+                               data-testid="firewall-managed-checkbox"
+                               @bind="_edit.Managed" />
                         Managed
                     </label>
 
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Poe"/>
+                        <input type="checkbox"
+                               data-testid="firewall-poe-checkbox"
+                               @bind="_edit.Poe" />
                         PoE
                     </label>
                 </div>
@@ -110,13 +116,15 @@
                 <div class="flex gap-2 flex-wrap">
                     @if (Firewall.Managed == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="firewall-managed-badge">
                             Managed
                         </span>
                     }
                     @if (Firewall.Poe == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="firewall-poe-badge">
                             PoE
                         </span>
                     }
@@ -125,12 +133,14 @@
         </div>
 
         <!-- Ports -->
-        <div class="md:col-span-2">
+        <div class="md:col-span-2"
+             data-testid="firewall-ports-section">
+
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     Ports
-                    <button class="hover:text-emerald-400 ml-1"
-                            title="Add Port"
+                    <button data-testid="add-port-button"
+                            class="hover:text-emerald-400 ml-1"
                             @onclick="OpenAddPort">
                         +
                     </button>
@@ -141,10 +151,9 @@
             {
                 @foreach (var port in Firewall.Ports)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button class="hover:text-emerald-400"
-                                title="Edit Port"
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-port-{port.Type}-{port.Speed}")
+                                class="hover:text-emerald-400"
                                 @onclick="() => OpenEditPort(port)">
                             @port.Count× @port.Type — @port.Speed Gbps
                         </button>
@@ -152,61 +161,64 @@
                 }
             }
         </div>
-        <ResourceTagEditor Resource="Firewall"/>
 
-        <div class="md:col-span-2">
+        <ResourceTagEditor Resource="Firewall" />
+
+        <div class="md:col-span-2"
+             data-testid="firewall-notes-section">
+
             <div class="text-zinc-400 mb-1">Notes</div>
 
             @if (_isEditing)
             {
-                <MarkdownEditor
-                    @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                <MarkdownEditor @bind-Value="_edit.Notes"
+                                ShowActionButtons="false"
+                                TestIdPrefix="firewall-notes-editor" />
             }
             else
             {
-                <MarkdownViewer
-                    Value="@Firewall.Notes"
-                    ShowEditButton="false"/>
+                <MarkdownViewer Value="@Firewall.Notes"
+                                ShowEditButton="false"
+                                TestIdPrefix="firewall-notes-viewer" />
             }
         </div>
 
     </div>
 </div>
 
-<PortModal
-    IsOpen="@_portModalOpen"
-    IsOpenChanged="v => _portModalOpen = v"
-    Value="@_editingPort"
-    OnSubmit="HandlePortSubmit"
-    OnDelete="HandlePortDelete"/>
-
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete firewall"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteServer"
-    TestIdPrefix="Firewall">
+<PortModal IsOpen="@_portModalOpen"
+           IsOpenChanged="v => _portModalOpen = v"
+           Value="@_editingPort"
+           OnSubmit="HandlePortSubmit"
+           OnDelete="HandlePortDelete" />
+
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete firewall"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteServer"
+              TestIdPrefix="Firewall">
     Are you sure you want to delete <strong>@Firewall.Name</strong>?
 </ConfirmModal>
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename firewall"
-    Description="Enter a new name for this firewall"
-    Label="New firewall name"
-    Value="@Firewall.Name"
-    OnSubmit="HandleRenameSubmit"/>
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Firewall.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename firewall"
+                  Description="Enter a new name for this firewall"
+                  Label="New firewall name"
+                  Value="@Firewall.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="firewall-rename" />
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Firewall.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="firewall-clone" />
 
 @code {
     [Parameter] [EditorRequired] public Firewall Firewall { get; set; } = default!;
@@ -224,7 +236,7 @@
     {
         _isEditing = false;
 
-        await UpdateFirewallUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Firewall.Name,
             _edit.Model,
             _edit.Managed,
@@ -264,7 +276,7 @@
     {
         if (_editingPortIndex < 0)
         {
-            await AddFirewallPortUseCase.ExecuteAsync(
+            await AddPortUseCase.ExecuteAsync(
                 Firewall.Name,
                 port.Type,
                 port.Speed,
@@ -272,7 +284,7 @@
         }
         else
         {
-            await UpdateFirewallPortUseCase.ExecuteAsync(
+            await UpdatePortUseCase.ExecuteAsync(
                 Firewall.Name,
                 _editingPortIndex,
                 port.Type,
@@ -286,7 +298,7 @@
 
     async Task HandlePortDelete(Port _)
     {
-        await RemoveFirewallPortUseCase.ExecuteAsync(
+        await RemovePortUseCase.ExecuteAsync(
             Firewall.Name,
             _editingPortIndex);
 

+ 126 - 137
Shared.Rcl/Laptops/LaptopCardComponent.razor

@@ -1,79 +1,79 @@
-@using RackPeek.Domain.Resources.Laptops
+
+@using RackPeek.Domain.Resources.Laptops
 @using RackPeek.Domain.Resources.SubResources
 @using RackPeek.Domain.UseCases.Cpus
 @using RackPeek.Domain.UseCases.Drives
 @using RackPeek.Domain.UseCases.Gpus
 @inject IGetResourceByNameUseCase<Laptop> GetByNameUseCase
-@inject UpdateLaptopUseCase UpdateLaptopUseCase
-@inject IDeleteResourceUseCase<Laptop> DeleteLaptopUseCase
-
+@inject UpdateLaptopUseCase UpdateUseCase
+@inject IDeleteResourceUseCase<Laptop> DeleteUseCase
 @inject IAddCpuUseCase<Laptop> AddCpuUseCase
 @inject IUpdateCpuUseCase<Laptop> UpdateCpuUseCase
 @inject IRemoveCpuUseCase<Laptop> RemoveCpuUseCase
-
 @inject IAddDriveUseCase<Laptop> AddDriveUseCase
 @inject IUpdateDriveUseCase<Laptop> UpdateDriveUseCase
 @inject IRemoveDriveUseCase<Laptop> RemoveDriveUseCase
-
 @inject IAddGpuUseCase<Laptop> AddGpuUseCase
 @inject IUpdateGpuUseCase<Laptop> UpdateGpuUseCase
 @inject IRemoveGpuUseCase<Laptop> RemoveGpuUseCase
 @inject ICloneResourceUseCase<Laptop> CloneUseCase
-
 @inject IRenameResourceUseCase<Laptop> RenameUseCase
 @inject NavigationManager Nav
 
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"laptop-item-{Laptop.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
-        <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Laptop.Name}")" class="block">
 
+        <div class="text-zinc-100 hover:text-emerald-300">
+            <NavLink href="@($"resources/hardware/{Laptop.Name}")"
+                     class="block"
+                     data-testid="open-laptop-link">
                 @Laptop.Name
             </NavLink>
         </div>
 
+        <div class="flex items-center gap-2">
 
-        <div class="flex justify-between items-center mb-3">
             @if (!string.IsNullOrWhiteSpace(Laptop.Model))
             {
-                <span class="text-xs text-zinc-400">
+                <span class="text-xs text-zinc-400"
+                      data-testid="laptop-model-badge">
                     @Laptop.Model
                 </span>
             }
-            <div class="flex items-center gap-2">
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
-                        @onclick="OpenRename">
-                    Rename
-                </button>
-                <button
+
+            <button data-testid="rename-laptop-button"
+                    class="text-xs text-blue-400 hover:text-blue-300 transition"
+                    @onclick="OpenRename">
+                Rename
+            </button>
+
+            <button data-testid="clone-laptop-button"
                     class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
                     @onclick="OpenClone">
-                    Clone
-                </button>
-                <button
+                Clone
+            </button>
+
+            <button data-testid="delete-laptop-button"
                     class="text-xs text-red-400 hover:text-red-300 transition"
-                    title="Delete server"
                     @onclick="ConfirmDelete">
-                    Delete
-                </button>
-            </div>
-        </div>
+                Delete
+            </button>
 
+        </div>
     </div>
 
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- CPU -->
-        <div>
+        <div data-testid="laptop-cpu-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     CPU
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add CPU"
-                        @onclick="OpenAddCpu">
+                    <button data-testid="add-cpu-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddCpu">
                         +
                     </button>
                 </div>
@@ -83,12 +83,10 @@
             {
                 @foreach (var cpu in Laptop.Cpus)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            title="Edit CPU"
-                            @onclick="() => OpenEditCpu(cpu)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-cpu-{cpu.ToString().Replace(" ", "-")}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditCpu(cpu)">
                             @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
                         </button>
                     </div>
@@ -97,24 +95,22 @@
         </div>
 
         <!-- RAM -->
-        <div>
+        <div data-testid="laptop-ram-section">
             <div class="text-zinc-400 mb-1">
                 RAM
-                <button
-                    class="hover:text-emerald-400 transition"
-                    title="Edit RAM"
-                    @onclick="EditRam">
+                <button data-testid="edit-ram-button"
+                        class="hover:text-emerald-400 transition"
+                        @onclick="EditRam">
                     +
                 </button>
             </div>
 
             @if (Laptop.Ram is not null)
             {
-                <div
-                    class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                    <button
-                        class="hover:text-emerald-400"
-                        @onclick="EditRam">
+                <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                    <button data-testid="ram-value-button"
+                            class="hover:text-emerald-400"
+                            @onclick="EditRam">
                         @($"{Laptop.Ram.Size} GB {Laptop.Ram.Mts} MT/s")
                     </button>
                 </div>
@@ -122,14 +118,13 @@
         </div>
 
         <!-- Drives -->
-        <div>
+        <div data-testid="laptop-drive-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     Drives
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add Drive"
-                        @onclick="OpenAddDrive">
+                    <button data-testid="add-drive-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddDrive">
                         +
                     </button>
                 </div>
@@ -139,11 +134,10 @@
             {
                 @foreach (var drive in Laptop.Drives)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            @onclick="() => OpenEditDrive(drive)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-drive-{drive.Type}-{drive.Size}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditDrive(drive)">
                             @drive.Type — @drive.Size GB
                         </button>
                     </div>
@@ -152,14 +146,13 @@
         </div>
 
         <!-- GPUs -->
-        <div>
+        <div data-testid="laptop-gpu-section">
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     GPUs
-                    <button
-                        class="hover:text-emerald-400 transition"
-                        title="Add GPU"
-                        @onclick="OpenAddGpu">
+                    <button data-testid="add-gpu-button"
+                            class="hover:text-emerald-400 transition"
+                            @onclick="OpenAddGpu">
                         +
                     </button>
                 </div>
@@ -169,11 +162,10 @@
             {
                 @foreach (var gpu in Laptop.Gpus)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button
-                            class="hover:text-emerald-400"
-                            @onclick="() => OpenEditGpu(gpu)">
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-gpu-{gpu.Model}-{gpu.Vram}")
+                                class="hover:text-emerald-400"
+                                @onclick="() => OpenEditGpu(gpu)">
                             @gpu.Model — @gpu.Vram GB VRAM
                         </button>
                     </div>
@@ -181,87 +173,84 @@
             }
         </div>
 
-
     </div>
-    <ResourceTagEditor Resource="Laptop"/>
 
-    <div class="md:col-span-2">
+    <ResourceTagEditor Resource="Laptop" />
+
+    <div class="md:col-span-2"
+         data-testid="laptop-notes-section">
+
         <div class="text-zinc-400 mb-1">Notes</div>
 
         @if (!_editingNotes)
         {
-            <MarkdownViewer
-                Value="@Laptop.Notes"
-                ShowEditButton="true"
-                OnEdit="BeginNotesEdit"/>
+            <MarkdownViewer Value="@Laptop.Notes"
+                            ShowEditButton="true"
+                            OnEdit="BeginNotesEdit"
+                            TestIdPrefix="laptop-notes-viewer" />
         }
         else
         {
-            <MarkdownEditor
-                @bind-Value="_notesDraft"
-                ShowActionButtons="true"
-                OnSave="SaveNotes"
-                OnCancel="CancelNotesEdit"/>
+            <MarkdownEditor @bind-Value="_notesDraft"
+                            ShowActionButtons="true"
+                            OnSave="SaveNotes"
+                            OnCancel="CancelNotesEdit"
+                            TestIdPrefix="laptop-notes-editor" />
         }
     </div>
 
 </div>
-<CpuModal
-    IsOpen="@_cpuModalOpen"
-    IsOpenChanged="v => _cpuModalOpen = v"
-    Value="@_editingCpu"
-    OnSubmit="HandleCpuSubmit"
-    OnDelete="HandleCpuDelete"/>
-
-<RamModal
-    IsOpen="@_isRamModalOpen"
-    IsOpenChanged="v => _isRamModalOpen = v"
-    Value="@Laptop.Ram"
-    OnSubmit="HandleRamSubmit"/>
-
-<DriveModal
-    IsOpen="@_driveModalOpen"
-    IsOpenChanged="v => _driveModalOpen = v"
-    Value="@_editingDrive"
-    OnSubmit="HandleDriveSubmit"
-    OnDelete="HandleDriveDelete"/>
-
-<GpuModal
-    IsOpen="@_gpuModalOpen"
-    IsOpenChanged="v => _gpuModalOpen = v"
-    Value="@_editingGpu"
-    OnSubmit="HandleGpuSubmit"
-    OnDelete="HandleGpuDelete"/>
-
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete laptop"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteServer"
-    TestIdPrefix="Laptop">
+
+<CpuModal IsOpen="@_cpuModalOpen"
+          IsOpenChanged="v => _cpuModalOpen = v"
+          Value="@_editingCpu"
+          OnSubmit="HandleCpuSubmit"
+          OnDelete="HandleCpuDelete" />
+
+<RamModal IsOpen="@_isRamModalOpen"
+          IsOpenChanged="v => _isRamModalOpen = v"
+          Value="@Laptop.Ram"
+          OnSubmit="HandleRamSubmit" />
+
+<DriveModal IsOpen="@_driveModalOpen"
+            IsOpenChanged="v => _driveModalOpen = v"
+            Value="@_editingDrive"
+            OnSubmit="HandleDriveSubmit"
+            OnDelete="HandleDriveDelete" />
+
+<GpuModal IsOpen="@_gpuModalOpen"
+          IsOpenChanged="v => _gpuModalOpen = v"
+          Value="@_editingGpu"
+          OnSubmit="HandleGpuSubmit"
+          OnDelete="HandleGpuDelete" />
+
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete laptop"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteServer"
+              TestIdPrefix="Laptop">
     Are you sure you want to delete <strong>@Laptop.Name</strong>?
-    <br/>
-    This will detach all dependent systems.
 </ConfirmModal>
 
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename laptop"
-    Description="Enter a new name for this laptop"
-    Label="New laptop name"
-    Value="@Laptop.Name"
-    OnSubmit="HandleRenameSubmit"/>
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Laptop.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename laptop"
+                  Description="Enter a new name for this laptop"
+                  Label="New laptop name"
+                  Value="@Laptop.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="laptop-rename" />
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Laptop.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="laptop-clone" />
 
 @code {
     [Parameter] [EditorRequired] public Laptop Laptop { get; set; } = default!;
@@ -278,7 +267,7 @@
     private async Task HandleRamSubmit(Ram value)
     {
         _isRamModalOpen = false;
-        await UpdateLaptopUseCase.ExecuteAsync(Laptop.Name, Laptop.Model, value.Size, value.Mts, Laptop.Notes);
+        await UpdateUseCase.ExecuteAsync(Laptop.Name, Laptop.Model, value.Size, value.Mts, Laptop.Notes);
         Laptop = await GetByNameUseCase.ExecuteAsync(Laptop.Name);
     }
 
@@ -445,7 +434,7 @@
     {
         _confirmDeleteOpen = false;
 
-        await DeleteLaptopUseCase.ExecuteAsync(Laptop.Name);
+        await DeleteUseCase.ExecuteAsync(Laptop.Name);
 
         if (OnDeleted.HasDelegate)
             await OnDeleted.InvokeAsync(Laptop.Name);
@@ -510,7 +499,7 @@
     {
         _editingNotes = false;
 
-        await UpdateLaptopUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Laptop.Name,
             Laptop.Model,
             Laptop.Ram?.Size,

+ 65 - 36
Shared.Rcl/Modals/CpuModal.razor

@@ -1,64 +1,84 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify CPU" : "Add CPU")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
-                    <div>
+
+                    <div data-testid="@($"{BaseTestId}-model-field")">
                         <label class="block text-zinc-400 mb-1">Model</label>
-                        <InputText
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Model"/>
+                        <InputText class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                   data-testid="@($"{BaseTestId}-model-input")"
+                                   @bind-Value="_model.Model" />
                     </div>
 
                     <div class="grid grid-cols-2 gap-3">
-                        <div>
+
+                        <div data-testid="@($"{BaseTestId}-cores-field")">
                             <label class="block text-zinc-400 mb-1">Cores</label>
-                            <InputNumber
-                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                                @bind-Value="_model.Cores"/>
+                            <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                         data-testid="@($"{BaseTestId}-cores-input")"
+                                         @bind-Value="_model.Cores" />
                         </div>
 
-                        <div>
+                        <div data-testid="@($"{BaseTestId}-threads-field")">
                             <label class="block text-zinc-400 mb-1">Threads</label>
-                            <InputNumber
-                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                                @bind-Value="_model.Threads"/>
+                            <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                         data-testid="@($"{BaseTestId}-threads-input")"
+                                         @bind-Value="_model.Threads" />
                         </div>
+
                     </div>
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
                     @if (IsEdit)
                     {
-                        <button
-                            type="button"
-                            class="text-red-400 hover:text-red-300 text-sm"
-                            @onclick="HandleDelete">
+                        <button type="button"
+                                class="text-red-400 hover:text-red-300 text-sm"
+                                data-testid="@($"{BaseTestId}-delete")"
+                                @onclick="HandleDelete">
                             Delete CPU
                         </button>
                     }
@@ -68,20 +88,23 @@
                     }
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -97,6 +120,13 @@
 
     [Parameter] public EventCallback<Cpu> OnDelete { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "cpu-modal"
+            : $"{TestIdPrefix}-cpu-modal";
+
     private CpuFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -157,5 +187,4 @@
 
         [Range(1, 2048)] public int? Threads { get; set; }
     }
-
 }

+ 60 - 32
Shared.Rcl/Modals/DriveModal.razor

@@ -1,40 +1,56 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify Drive" : "Add Drive")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
-                    <div>
+
+                    <div data-testid="@($"{BaseTestId}-type-field")">
                         <label class="block text-zinc-400 mb-1">
                             Type
                         </label>
 
-                        <InputSelect
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Type">
+                        <InputSelect class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-type-input")"
+                                     @bind-Value="_model.Type">
                             <option value="">Select type</option>
 
                             @foreach (var type in Drive.ValidDriveTypes)
@@ -44,25 +60,28 @@
                         </InputSelect>
                     </div>
 
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-size-field")">
                         <label class="block text-zinc-400 mb-1">
                             Size (GB)
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Size"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-size-input")"
+                                     @bind-Value="_model.Size" />
                     </div>
+
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
                     @if (IsEdit)
                     {
-                        <button
-                            type="button"
-                            class="text-red-400 hover:text-red-300 text-sm"
-                            @onclick="HandleDelete">
+                        <button type="button"
+                                class="text-red-400 hover:text-red-300 text-sm"
+                                data-testid="@($"{BaseTestId}-delete")"
+                                @onclick="HandleDelete">
                             Delete Drive
                         </button>
                     }
@@ -72,20 +91,23 @@
                     }
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -101,6 +123,13 @@
 
     [Parameter] public EventCallback<Drive> OnDelete { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "drive-modal"
+            : $"{TestIdPrefix}-drive-modal";
+
     private DriveFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -157,5 +186,4 @@
 
         [Range(1, 1024 * 1024)] public int? Size { get; set; }
     }
-
 }

+ 61 - 32
Shared.Rcl/Modals/GpuModal.razor

@@ -1,63 +1,82 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify GPU" : "Add GPU")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
+
                     <!-- Model -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-model-field")">
                         <label class="block text-zinc-400 mb-1">
                             Model
                         </label>
 
-                        <InputText
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Model"/>
+                        <InputText class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                   data-testid="@($"{BaseTestId}-model-input")"
+                                   @bind-Value="_model.Model" />
                     </div>
 
                     <!-- VRAM -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-vram-field")">
                         <label class="block text-zinc-400 mb-1">
                             VRAM (GB)
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Vram"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-vram-input")"
+                                     @bind-Value="_model.Vram" />
                     </div>
+
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
                     @if (IsEdit)
                     {
-                        <button
-                            type="button"
-                            class="text-red-400 hover:text-red-300 text-sm"
-                            @onclick="HandleDelete">
+                        <button type="button"
+                                class="text-red-400 hover:text-red-300 text-sm"
+                                data-testid="@($"{BaseTestId}-delete")"
+                                @onclick="HandleDelete">
                             Delete GPU
                         </button>
                     }
@@ -67,20 +86,23 @@
                     }
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -95,6 +117,14 @@
     [Parameter] public EventCallback<Gpu> OnSubmit { get; set; }
     [Parameter] public EventCallback<Gpu> OnDelete { get; set; }
 
+    // Same prefix pattern as ConfirmModal
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "gpu-modal"
+            : $"{TestIdPrefix}-gpu-modal";
+
     private GpuFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -151,5 +181,4 @@
 
         [Range(1, 256)] public int? Vram { get; set; }
     }
-
 }

+ 64 - 36
Shared.Rcl/Modals/NicModal.razor

@@ -1,41 +1,57 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify NIC" : "Add NIC")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
+
                     <!-- Type -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-type-field")">
                         <label class="block text-zinc-400 mb-1">
                             Type
                         </label>
 
-                        <InputSelect
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Type">
+                        <InputSelect class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-type-input")"
+                                     @bind-Value="_model.Type">
                             <option value="">Select type</option>
 
                             @foreach (var type in Nic.ValidNicTypes)
@@ -46,36 +62,39 @@
                     </div>
 
                     <!-- Speed -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-speed-field")">
                         <label class="block text-zinc-400 mb-1">
                             Speed (Gbps)
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Speed"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-speed-input")"
+                                     @bind-Value="_model.Speed" />
                     </div>
 
                     <!-- Ports -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-ports-field")">
                         <label class="block text-zinc-400 mb-1">
                             Ports
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Ports"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-ports-input")"
+                                     @bind-Value="_model.Ports" />
                     </div>
+
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
                     @if (IsEdit)
                     {
-                        <button
-                            type="button"
-                            class="text-red-400 hover:text-red-300 text-sm"
-                            @onclick="HandleDelete">
+                        <button type="button"
+                                class="text-red-400 hover:text-red-300 text-sm"
+                                data-testid="@($"{BaseTestId}-delete")"
+                                @onclick="HandleDelete">
                             Delete NIC
                         </button>
                     }
@@ -85,20 +104,23 @@
                     }
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -113,6 +135,13 @@
     [Parameter] public EventCallback<Nic> OnSubmit { get; set; }
     [Parameter] public EventCallback<Nic> OnDelete { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "nic-modal"
+            : $"{TestIdPrefix}-nic-modal";
+
     private NicFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -173,5 +202,4 @@
 
         [Range(1, 128)] public int? Ports { get; set; }
     }
-
 }

+ 64 - 37
Shared.Rcl/Modals/PortModal.razor

@@ -1,41 +1,57 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify Port" : "Add Port")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
+
                     <!-- Type -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-type-field")">
                         <label class="block text-zinc-400 mb-1">
                             Type
                         </label>
 
-                        <InputSelect
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Type">
+                        <InputSelect class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-type-input")"
+                                     @bind-Value="_model.Type">
                             <option value="">Select type</option>
 
                             @foreach (var type in Nic.ValidNicTypes)
@@ -45,38 +61,40 @@
                         </InputSelect>
                     </div>
 
-
                     <!-- Speed -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-speed-field")">
                         <label class="block text-zinc-400 mb-1">
                             Speed
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Speed"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-speed-input")"
+                                     @bind-Value="_model.Speed" />
                     </div>
 
                     <!-- Count -->
-                    <div>
+                    <div data-testid="@($"{BaseTestId}-count-field")">
                         <label class="block text-zinc-400 mb-1">
                             Count
                         </label>
 
-                        <InputNumber
-                            class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                            @bind-Value="_model.Count"/>
+                        <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                     data-testid="@($"{BaseTestId}-count-input")"
+                                     @bind-Value="_model.Count" />
                     </div>
+
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
                     @if (IsEdit)
                     {
-                        <button
-                            type="button"
-                            class="text-red-400 hover:text-red-300 text-sm"
-                            @onclick="HandleDelete">
+                        <button type="button"
+                                class="text-red-400 hover:text-red-300 text-sm"
+                                data-testid="@($"{BaseTestId}-delete")"
+                                @onclick="HandleDelete">
                             Delete Port
                         </button>
                     }
@@ -86,20 +104,23 @@
                     }
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -114,6 +135,13 @@
     [Parameter] public EventCallback<Port> OnSubmit { get; set; }
     [Parameter] public EventCallback<Port> OnDelete { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "port-modal"
+            : $"{TestIdPrefix}-port-modal";
+
     private PortFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -174,5 +202,4 @@
 
         [Range(1, 256)] public int? Count { get; set; }
     }
-
 }

+ 57 - 31
Shared.Rcl/Modals/RamModal.razor

@@ -1,73 +1,95 @@
 @using System.ComponentModel.DataAnnotations
 @using RackPeek.Domain.Resources.SubResources
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-4">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-4"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @(IsEdit ? "Modify RAM" : "Add RAM")
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
-                <ValidationSummary class="text-xs text-red-400 mb-3"/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
+                <ValidationSummary class="text-xs text-red-400 mb-3"
+                                   data-testid="@($"{BaseTestId}-validation-summary")" />
 
                 <div class="space-y-3 text-sm">
+
                     <div class="grid grid-cols-2 gap-3">
-                        <div>
+
+                        <div data-testid="@($"{BaseTestId}-size-field")">
                             <label class="block text-zinc-400 mb-1">
                                 Size (GB)
                             </label>
-                            <InputNumber
-                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                                @bind-Value="_model.Size"/>
+                            <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                         data-testid="@($"{BaseTestId}-size-input")"
+                                         @bind-Value="_model.Size" />
                         </div>
 
-                        <div>
+                        <div data-testid="@($"{BaseTestId}-mts-field")">
                             <label class="block text-zinc-400 mb-1">
                                 Speed (MT/s)
                             </label>
-                            <InputNumber
-                                class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                                @bind-Value="_model.Mts"/>
+                            <InputNumber class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                                         data-testid="@($"{BaseTestId}-mts-input")"
+                                         @bind-Value="_model.Mts" />
                         </div>
+
                     </div>
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-between items-center mt-5">
-                    <span></span>
+                <div class="flex justify-between items-center mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
 
+                    <span></span>
 
                     <div class="flex gap-2">
-                        <button
-                            type="button"
-                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                            @onclick="Cancel">
+
+                        <button type="button"
+                                class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                                data-testid="@($"{BaseTestId}-cancel")"
+                                @onclick="Cancel">
                             Cancel
                         </button>
 
-                        <button
-                            type="submit"
-                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                        <button type="submit"
+                                class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                                data-testid="@($"{BaseTestId}-submit")">
                             @(IsEdit ? "Update" : "Add")
                         </button>
+
                     </div>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -81,6 +103,13 @@
 
     [Parameter] public EventCallback<Ram> OnSubmit { get; set; }
 
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "ram-modal"
+            : $"{TestIdPrefix}-ram-modal";
+
     private RamFormModel _model = new();
 
     private bool IsEdit => Value is not null;
@@ -124,11 +153,8 @@
 
     private class RamFormModel
     {
-        // Both are intentionally nullable and optional
         [Range(1, 1024)] public int? Size { get; set; }
 
         [Range(1, 10000)] public int? Mts { get; set; }
     }
-
 }
-

+ 54 - 32
Shared.Rcl/Modals/StringValueModal.razor

@@ -1,68 +1,89 @@
 @using System.ComponentModel.DataAnnotations
+
 @if (IsOpen)
 {
-    <div class="fixed inset-0 z-50 flex items-center justify-center">
+    <div class="fixed inset-0 z-50 flex items-center justify-center"
+         data-testid="@BaseTestId">
+
         <!-- Backdrop -->
-        <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
+        <div class="absolute inset-0 bg-black/70"
+             data-testid="@($"{BaseTestId}-backdrop")"
+             @onclick="Cancel">
+        </div>
 
         <!-- Modal -->
-        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4">
+        <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-md p-4"
+             data-testid="@($"{BaseTestId}-container")">
+
             <!-- Header -->
-            <div class="flex justify-between items-center mb-3">
-                <div class="text-zinc-100 text-sm font-medium">
+            <div class="flex justify-between items-center mb-3"
+                 data-testid="@($"{BaseTestId}-header")">
+
+                <div class="text-zinc-100 text-sm font-medium"
+                     data-testid="@($"{BaseTestId}-title")">
                     @Title
                 </div>
 
-                <button
-                    class="text-zinc-400 hover:text-zinc-200"
-                    @onclick="Cancel">
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        data-testid="@($"{BaseTestId}-close")"
+                        @onclick="Cancel">
                 </button>
             </div>
 
             @if (!string.IsNullOrWhiteSpace(Description))
             {
-                <div class="text-xs text-zinc-400 mb-4">
+                <div class="text-xs text-zinc-400 mb-4"
+                     data-testid="@($"{BaseTestId}-description")">
                     @Description
                 </div>
             }
 
             <!-- Form -->
-            <EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
-                <DataAnnotationsValidator/>
+            <EditForm Model="_model"
+                      OnValidSubmit="HandleValidSubmit"
+                      data-testid="@($"{BaseTestId}-form")">
+
+                <DataAnnotationsValidator />
 
                 @if (!string.IsNullOrEmpty(_error))
                 {
-                    <div class="text-xs text-red-400 mb-3">
+                    <div class="text-xs text-red-400 mb-3"
+                         data-testid="@($"{BaseTestId}-error")">
                         @_error
                     </div>
                 }
 
-                <div class="text-sm">
+                <div class="text-sm"
+                     data-testid="@($"{BaseTestId}-field")">
+
                     <label class="block text-zinc-400 mb-1">
                         @Label
                     </label>
 
-                    <InputText
-                        class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
-                        @bind-Value="_model.Value"/>
+                    <InputText class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
+                               data-testid="@($"{BaseTestId}-input")"
+                               @bind-Value="_model.Value" />
                 </div>
 
                 <!-- Actions -->
-                <div class="flex justify-end gap-2 mt-5">
-                    <button
-                        type="button"
-                        class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
-                        @onclick="Cancel">
+                <div class="flex justify-end gap-2 mt-5"
+                     data-testid="@($"{BaseTestId}-actions")">
+
+                    <button type="button"
+                            class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
+                            data-testid="@($"{BaseTestId}-cancel")"
+                            @onclick="Cancel">
                         Cancel
                     </button>
 
-                    <button
-                        type="submit"
-                        class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500">
+                    <button type="submit"
+                            class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
+                            data-testid="@($"{BaseTestId}-submit")">
                         Accept
                     </button>
                 </div>
+
             </EditForm>
         </div>
     </div>
@@ -78,12 +99,15 @@
 
     [Parameter] public string? Value { get; set; }
 
-    /// <summary>
-    ///     Called when Accept is clicked.
-    ///     May throw an exception (e.g. validation error).
-    /// </summary>
-    [Parameter]
-    public EventCallback<string> OnSubmit { get; set; }
+    [Parameter] public EventCallback<string> OnSubmit { get; set; }
+
+    // Same prefix pattern as ConfirmModal
+    [Parameter] public string? TestIdPrefix { get; set; }
+
+    private string BaseTestId =>
+        string.IsNullOrWhiteSpace(TestIdPrefix)
+            ? "string-value-modal"
+            : $"{TestIdPrefix}-string-value-modal";
 
     private FormModel _model = new();
     private string? _error;
@@ -111,7 +135,6 @@
         }
         catch (Exception ex)
         {
-            // Show exception message instead of closing
             _error = ex.Message;
         }
     }
@@ -132,5 +155,4 @@
     {
         [Required] public string? Value { get; set; }
     }
-
 }

+ 0 - 68
Shared.Rcl/Routers/AddRouterComponent.razor

@@ -1,68 +0,0 @@
-@using Router = RackPeek.Domain.Resources.Routers.Router
-@inject IAddResourceUseCase<Router> AddRouter
-
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
-    <div class="text-zinc-100 mb-3">
-        Add Router
-    </div>
-
-    <div class="flex gap-2">
-        <input
-            class="flex-1 bg-zinc-950 border border-zinc-800 rounded px-3 py-2 text-sm text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-zinc-600"
-            placeholder="Router name"
-            @bind="_name"
-            @bind:event="oninput"/>
-
-        <button
-            class="px-4 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-100 disabled:opacity-50"
-            disabled="@_isSubmitting"
-            @onclick="CreateAsync">
-            add
-        </button>
-    </div>
-
-    @if (_error is not null)
-    {
-        <div class="mt-2 text-sm text-red-400">
-            @_error
-        </div>
-    }
-</div>
-
-@code {
-    [Parameter] public EventCallback<string> OnCreated { get; set; }
-
-    private string _name = string.Empty;
-    private string? _error;
-    private bool _isSubmitting;
-
-    private async Task CreateAsync()
-    {
-        _error = null;
-
-        if (string.IsNullOrWhiteSpace(_name))
-        {
-            _error = "name is required";
-            return;
-        }
-
-        try
-        {
-            _isSubmitting = true;
-            var name = _name.Trim();
-            await AddRouter.ExecuteAsync(name);
-            _name = string.Empty;
-
-            await OnCreated.InvokeAsync(name);
-        }
-        catch (Exception ex)
-        {
-            _error = ex.Message;
-        }
-        finally
-        {
-            _isSubmitting = false;
-        }
-    }
-
-}

+ 104 - 88
Shared.Rcl/Routers/RouterCardComponent.razor

@@ -3,20 +3,25 @@
 @using RackPeek.Domain.UseCases.Ports
 @using Router = RackPeek.Domain.Resources.Routers.Router
 
-@inject UpdateRouterUseCase UpdateRouterUseCase
+@inject UpdateRouterUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<Router> GetByNameUseCase
-@inject IAddPortUseCase<Router> AddRouterPortUseCase
-@inject IUpdatePortUseCase<Router> UpdateRouterPortUseCase
-@inject IRemovePortUseCase<Router> RemoveRouterPortUseCase
+@inject IAddPortUseCase<Router> AddPortUseCase
+@inject IUpdatePortUseCase<Router> UpdatePortUseCase
+@inject IRemovePortUseCase<Router> RemovePortUseCase
 @inject IDeleteResourceUseCase<Router> DeleteUseCase
 @inject IRenameResourceUseCase<Router> RenameUseCase
 @inject ICloneResourceUseCase<Router> CloneUseCase
-
 @inject NavigationManager Nav
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"router-item-{Router.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
+
         <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Router.Name}")" class="block">
+            <NavLink href="@($"resources/hardware/{Router.Name}")"
+                     class="block"
+                     data-testid="open-router-link">
                 @Router.Name
             </NavLink>
         </div>
@@ -24,35 +29,40 @@
         <div class="flex gap-3 text-xs">
             @if (!_isEditing)
             {
-                <button class="text-zinc-400 hover:text-zinc-200"
+                <button data-testid="edit-router-button"
+                        class="text-zinc-400 hover:text-zinc-200"
                         @onclick="BeginEdit">
                     Edit
                 </button>
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
+
+                <button data-testid="rename-router-button"
+                        class="text-blue-400 hover:text-blue-300 transition"
                         @onclick="OpenRename">
                     Rename
                 </button>
-                <button
-                    class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
-                    @onclick="OpenClone">
+
+                <button data-testid="clone-router-button"
+                        class="text-emerald-400 hover:text-emerald-300 transition"
+                        @onclick="OpenClone">
                     Clone
                 </button>
-                <button
-                    class="text-xs text-red-400 hover:text-red-300 transition"
-                    title="Delete server"
-                    @onclick="ConfirmDelete">
+
+                <button data-testid="delete-router-button"
+                        class="text-red-400 hover:text-red-300 transition"
+                        @onclick="ConfirmDelete">
                     Delete
                 </button>
             }
             else
             {
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="save-router-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="Save">
                     Save
                 </button>
-                <button class="text-zinc-500 hover:text-zinc-300"
+
+                <button data-testid="cancel-router-button"
+                        class="text-zinc-500 hover:text-zinc-300"
                         @onclick="Cancel">
                     Cancel
                 </button>
@@ -63,41 +73,42 @@
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- Model -->
-        <div>
+        <div data-testid="router-model-section">
             <div class="text-zinc-400 mb-1">Model</div>
+
             @if (_isEditing)
             {
-                <input
-                    class="w-full px-3 py-2 rounded-md
-                           bg-zinc-800 text-zinc-100
-                           border border-zinc-600
-                           placeholder-zinc-500
-                           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
-                           hover:border-zinc-400
-                           transition-colors duration-150
-                           cursor-text"
-                    @bind="_edit.Model"/>
+                <input data-testid="router-model-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Model" />
             }
             else if (!string.IsNullOrWhiteSpace(Router.Model))
             {
-                <div class="text-zinc-300">@Router.Model</div>
+                <div class="text-zinc-300"
+                     data-testid="router-model-value">
+                    @Router.Model
+                </div>
             }
         </div>
 
         <!-- Features -->
-        <div>
+        <div data-testid="router-features-section">
             <div class="text-zinc-400 mb-1">Features</div>
 
             @if (_isEditing)
             {
                 <div class="flex gap-4">
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Managed"/>
+                        <input type="checkbox"
+                               data-testid="router-managed-checkbox"
+                               @bind="_edit.Managed" />
                         Managed
                     </label>
 
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Poe"/>
+                        <input type="checkbox"
+                               data-testid="router-poe-checkbox"
+                               @bind="_edit.Poe" />
                         PoE
                     </label>
                 </div>
@@ -107,13 +118,15 @@
                 <div class="flex gap-2 flex-wrap">
                     @if (Router.Managed == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="router-managed-badge">
                             Managed
                         </span>
                     }
                     @if (Router.Poe == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="router-poe-badge">
                             PoE
                         </span>
                     }
@@ -122,12 +135,14 @@
         </div>
 
         <!-- Ports -->
-        <div class="md:col-span-2">
+        <div class="md:col-span-2"
+             data-testid="router-ports-section">
+
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     Ports
-                    <button class="hover:text-emerald-400 ml-1"
-                            title="Add Port"
+                    <button data-testid="add-port-button"
+                            class="hover:text-emerald-400 ml-1"
                             @onclick="OpenAddPort">
                         +
                     </button>
@@ -138,10 +153,9 @@
             {
                 @foreach (var port in Router.Ports)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button class="hover:text-emerald-400"
-                                title="Edit Port"
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-port-{port.Type}-{port.Speed}")
+                                class="hover:text-emerald-400"
                                 @onclick="() => OpenEditPort(port)">
                             @port.Count× @port.Type — @port.Speed Gbps
                         </button>
@@ -149,62 +163,64 @@
                 }
             }
         </div>
-        <ResourceTagEditor Resource="Router"/>
 
-        <div class="md:col-span-2">
+        <ResourceTagEditor Resource="Router" />
+
+        <div class="md:col-span-2"
+             data-testid="router-notes-section">
+
             <div class="text-zinc-400 mb-1">Notes</div>
 
             @if (_isEditing)
             {
-                <MarkdownEditor
-                    @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                <MarkdownEditor @bind-Value="_edit.Notes"
+                                ShowActionButtons="false"
+                                TestIdPrefix="router-notes-editor" />
             }
             else
             {
-                <MarkdownViewer
-                    Value="@Router.Notes"
-                    ShowEditButton="false"/>
+                <MarkdownViewer Value="@Router.Notes"
+                                ShowEditButton="false"
+                                TestIdPrefix="router-notes-viewer" />
             }
         </div>
 
     </div>
 </div>
 
-<PortModal
-    IsOpen="@_portModalOpen"
-    IsOpenChanged="v => _portModalOpen = v"
-    Value="@_editingPort"
-    OnSubmit="HandlePortSubmit"
-    OnDelete="HandlePortDelete"/>
-
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete router"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteServer"
-    TestIdPrefix="Router">
+<PortModal IsOpen="@_portModalOpen"
+           IsOpenChanged="v => _portModalOpen = v"
+           Value="@_editingPort"
+           OnSubmit="HandlePortSubmit"
+           OnDelete="HandlePortDelete" />
+
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete router"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteServer"
+              TestIdPrefix="Router">
     Are you sure you want to delete <strong>@Router.Name</strong>?
 </ConfirmModal>
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename router"
-    Description="Enter a new name for this router"
-    Label="New router name"
-    Value="@Router.Name"
-    OnSubmit="HandleRenameSubmit"/>
-
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Router.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename router"
+                  Description="Enter a new name for this router"
+                  Label="New router name"
+                  Value="@Router.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="router-rename" />
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Router.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="router-clone" />
 
 @code {
     [Parameter] [EditorRequired] public Router Router { get; set; } = default!;
@@ -222,7 +238,7 @@
     {
         _isEditing = false;
 
-        await UpdateRouterUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Router.Name,
             _edit.Model,
             _edit.Managed,
@@ -262,7 +278,7 @@
     {
         if (_editingPortIndex < 0)
         {
-            await AddRouterPortUseCase.ExecuteAsync(
+            await AddPortUseCase.ExecuteAsync(
                 Router.Name,
                 port.Type,
                 port.Speed,
@@ -270,7 +286,7 @@
         }
         else
         {
-            await UpdateRouterPortUseCase.ExecuteAsync(
+            await UpdatePortUseCase.ExecuteAsync(
                 Router.Name,
                 _editingPortIndex,
                 port.Type,
@@ -284,7 +300,7 @@
 
     async Task HandlePortDelete(Port _)
     {
-        await RemoveRouterPortUseCase.ExecuteAsync(
+        await RemovePortUseCase.ExecuteAsync(
             Router.Name,
             _editingPortIndex);
 

+ 5 - 5
Shared.Rcl/Servers/ServerCardComponent.razor

@@ -21,8 +21,8 @@
 @inject IRemoveGpuUseCase<Server> RemoveGpuUseCase
 
 @inject IGetResourceByNameUseCase<Server> GetByNameUseCase
-@inject UpdateServerUseCase UpdateServerUseCase
-@inject IDeleteResourceUseCase<Server> DeleteServerUseCase
+@inject UpdateServerUseCase UpdateUseCase
+@inject IDeleteResourceUseCase<Server> DeleteUseCase
 @inject ICloneResourceUseCase<Server> CloneUseCase
 
 @inject IRenameResourceUseCase<Server> RenameUseCase
@@ -334,7 +334,7 @@
     private async Task HandleRamSubmit(Ram value)
     {
         _isRamModalOpen = false;
-        await UpdateServerUseCase.ExecuteAsync(Server.Name, value.Size, value.Mts, Server.Ipmi);
+        await UpdateUseCase.ExecuteAsync(Server.Name, value.Size, value.Mts, Server.Ipmi);
         Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
     }
 
@@ -555,7 +555,7 @@
     {
         _confirmDeleteOpen = false;
 
-        await DeleteServerUseCase.ExecuteAsync(Server.Name);
+        await DeleteUseCase.ExecuteAsync(Server.Name);
 
         if (OnDeleted.HasDelegate)
             await OnDeleted.InvokeAsync(Server.Name);
@@ -620,7 +620,7 @@
     {
         _editingNotes = false;
 
-        await UpdateServerUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Server.Name,
             Server.Ram?.Size,
             Server.Ram?.Mts,

+ 11 - 7
Shared.Rcl/Services/ServiceCardComponent.razor

@@ -1,6 +1,6 @@
-@inject UpdateServiceUseCase UpdateServiceUseCase
+@inject UpdateServiceUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<Service> GetByNameUseCase
-@inject IDeleteResourceUseCase<Service> DeleteServiceUseCase
+@inject IDeleteResourceUseCase<Service> DeleteUseCase
 @inject ICloneResourceUseCase<Service> CloneUseCase
 @inject IRenameResourceUseCase<Service> RenameUseCase
 @inject NavigationManager Nav
@@ -191,13 +191,17 @@
             {
                 <MarkdownEditor
                     @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                    ShowActionButtons="false"
+                    TestIdPrefix="service-notes-editor" />
+
             }
             else
             {
                 <MarkdownViewer
                     Value="@Service.Notes"
-                    ShowEditButton="false"/>
+                    ShowEditButton="false"
+                    TestIdPrefix="service-notes-viewer" />
+
             }
         </div>
     </div>
@@ -274,7 +278,7 @@
     async Task Save()
     {
         _isEditing = false;
-        await UpdateServiceUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             _edit.Name,
             _edit.Ip,
             _edit.Port,
@@ -298,7 +302,7 @@
     async Task HandleParentSelected(string? name)
     {
         SelectedParentName = name;
-        await UpdateServiceUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Service.Name,
             Service.Network?.Ip,
             Service.Network?.Port,
@@ -326,7 +330,7 @@
     {
         _confirmDeleteOpen = false;
 
-        await DeleteServiceUseCase.ExecuteAsync(Service.Name);
+        await DeleteUseCase.ExecuteAsync(Service.Name);
 
         if (OnDeleted.HasDelegate)
             await OnDeleted.InvokeAsync(Service.Name);

+ 104 - 89
Shared.Rcl/Switches/SwitchCardComponent.razor

@@ -1,22 +1,25 @@
 @using RackPeek.Domain.Resources.SubResources
 @using RackPeek.Domain.Resources.Switches
 @using RackPeek.Domain.UseCases.Ports
-@inject UpdateSwitchUseCase UpdateSwitchUseCase
+@inject UpdateSwitchUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<Switch> GetByNameUseCase
-@inject IAddPortUseCase<Switch> AddSwitchPortUseCase
-@inject IUpdatePortUseCase<Switch> UpdateSwitchPortUseCase
-@inject IRemovePortUseCase<Switch> RemoveSwitchPortUseCase
+@inject IAddPortUseCase<Switch> AddPortUseCase
+@inject IUpdatePortUseCase<Switch> UpdatePortUseCase
+@inject IRemovePortUseCase<Switch> RemovePortUseCase
 @inject IDeleteResourceUseCase<Switch> DeleteUseCase
 @inject ICloneResourceUseCase<Switch> CloneUseCase
-
 @inject IRenameResourceUseCase<Switch> RenameUseCase
 @inject NavigationManager Nav
 
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"switch-item-{Switch.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
-        <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Switch.Name}")" class="block">
 
+        <div class="text-zinc-100 hover:text-emerald-300">
+            <NavLink href="@($"resources/hardware/{Switch.Name}")"
+                     class="block"
+                     data-testid="open-switch-link">
                 @Switch.Name
             </NavLink>
         </div>
@@ -24,35 +27,40 @@
         <div class="flex gap-3 text-xs">
             @if (!_isEditing)
             {
-                <button class="text-zinc-400 hover:text-zinc-200"
+                <button data-testid="edit-switch-button"
+                        class="text-zinc-400 hover:text-zinc-200"
                         @onclick="BeginEdit">
                     Edit
                 </button>
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
+
+                <button data-testid="rename-switch-button"
+                        class="text-blue-400 hover:text-blue-300 transition"
                         @onclick="OpenRename">
                     Rename
                 </button>
-                <button
-                    class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
-                    @onclick="OpenClone">
+
+                <button data-testid="clone-switch-button"
+                        class="text-emerald-400 hover:text-emerald-300 transition"
+                        @onclick="OpenClone">
                     Clone
                 </button>
-                <button
-                    class="text-xs text-red-400 hover:text-red-300 transition"
-                    title="Delete server"
-                    @onclick="ConfirmDelete">
+
+                <button data-testid="delete-switch-button"
+                        class="text-red-400 hover:text-red-300 transition"
+                        @onclick="ConfirmDelete">
                     Delete
                 </button>
             }
             else
             {
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="save-switch-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="Save">
                     Save
                 </button>
-                <button class="text-zinc-500 hover:text-zinc-300"
+
+                <button data-testid="cancel-switch-button"
+                        class="text-zinc-500 hover:text-zinc-300"
                         @onclick="Cancel">
                     Cancel
                 </button>
@@ -63,41 +71,42 @@
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- Model -->
-        <div>
+        <div data-testid="switch-model-section">
             <div class="text-zinc-400 mb-1">Model</div>
+
             @if (_isEditing)
             {
-                <input
-                    class="w-full px-3 py-2 rounded-md
-                           bg-zinc-800 text-zinc-100
-                           border border-zinc-600
-                           placeholder-zinc-500
-                           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
-                           hover:border-zinc-400
-                           transition-colors duration-150
-                           cursor-text"
-                    @bind="_edit.Model"/>
+                <input data-testid="switch-model-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Model" />
             }
             else if (!string.IsNullOrWhiteSpace(Switch.Model))
             {
-                <div class="text-zinc-300">@Switch.Model</div>
+                <div class="text-zinc-300"
+                     data-testid="switch-model-value">
+                    @Switch.Model
+                </div>
             }
         </div>
 
         <!-- Features -->
-        <div>
+        <div data-testid="switch-features-section">
             <div class="text-zinc-400 mb-1">Features</div>
 
             @if (_isEditing)
             {
                 <div class="flex gap-4">
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Managed"/>
+                        <input type="checkbox"
+                               data-testid="switch-managed-checkbox"
+                               @bind="_edit.Managed" />
                         Managed
                     </label>
 
                     <label class="flex items-center gap-2 text-zinc-300">
-                        <input type="checkbox" @bind="_edit.Poe"/>
+                        <input type="checkbox"
+                               data-testid="switch-poe-checkbox"
+                               @bind="_edit.Poe" />
                         PoE
                     </label>
                 </div>
@@ -107,13 +116,15 @@
                 <div class="flex gap-2 flex-wrap">
                     @if (Switch.Managed == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="switch-managed-badge">
                             Managed
                         </span>
                     }
                     @if (Switch.Poe == true)
                     {
-                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300">
+                        <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-300"
+                              data-testid="switch-poe-badge">
                             PoE
                         </span>
                     }
@@ -122,12 +133,14 @@
         </div>
 
         <!-- Ports -->
-        <div class="md:col-span-2">
+        <div class="md:col-span-2"
+             data-testid="switch-ports-section">
+
             <div class="flex items-center justify-between mb-1 group">
                 <div class="text-zinc-400">
                     Ports
-                    <button class="hover:text-emerald-400 ml-1"
-                            title="Add Port"
+                    <button data-testid="add-port-button"
+                            class="hover:text-emerald-400 ml-1"
                             @onclick="OpenAddPort">
                         +
                     </button>
@@ -138,10 +151,9 @@
             {
                 @foreach (var port in Switch.Ports)
                 {
-                    <div
-                        class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
-                        <button class="hover:text-emerald-400"
-                                title="Edit Port"
+                    <div class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
+                        <button data-testid=@($"edit-port-{port.Type}-{port.Speed}")
+                                class="hover:text-emerald-400"
                                 @onclick="() => OpenEditPort(port)">
                             @port.Count× @port.Type — @port.Speed Gbps
                         </button>
@@ -149,62 +161,65 @@
                 }
             }
         </div>
-        <ResourceTagEditor Resource="Switch"/>
 
-        <div class="md:col-span-2">
+        <ResourceTagEditor Resource="Switch" />
+
+        <div class="md:col-span-2"
+             data-testid="switch-notes-section">
+
             <div class="text-zinc-400 mb-1">Notes</div>
 
             @if (_isEditing)
             {
-                <MarkdownEditor
-                    @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                <MarkdownEditor @bind-Value="_edit.Notes"
+                                ShowActionButtons="false"
+                                TestIdPrefix="switch-notes-editor" />
             }
             else
             {
-                <MarkdownViewer
-                    Value="@Switch.Notes"
-                    ShowEditButton="false"/>
+                <MarkdownViewer Value="@Switch.Notes"
+                                ShowEditButton="false"
+                                TestIdPrefix="switch-notes-viewer" />
             }
         </div>
 
     </div>
 </div>
 
-<PortModal
-    IsOpen="@_portModalOpen"
-    IsOpenChanged="v => _portModalOpen = v"
-    Value="@_editingPort"
-    OnSubmit="HandlePortSubmit"
-    OnDelete="HandlePortDelete"/>
-
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete switch"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteServer"
-    TestIdPrefix="Switch">>
+<PortModal IsOpen="@_portModalOpen"
+           IsOpenChanged="v => _portModalOpen = v"
+           Value="@_editingPort"
+           OnSubmit="HandlePortSubmit"
+           OnDelete="HandlePortDelete" />
+
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete switch"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteServer"
+              TestIdPrefix="Switch">
     Are you sure you want to delete <strong>@Switch.Name</strong>?
 </ConfirmModal>
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename switch"
-    Description="Enter a new name for this switch"
-    Label="New switch name"
-    Value="@Switch.Name"
-    OnSubmit="HandleRenameSubmit"/>
-
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Switch.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename switch"
+                  Description="Enter a new name for this switch"
+                  Label="New switch name"
+                  Value="@Switch.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="switch-rename" />
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Switch.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="switch-clone" />
+
 
 @code {
     [Parameter] [EditorRequired] public Switch Switch { get; set; } = default!;
@@ -222,7 +237,7 @@
     {
         _isEditing = false;
 
-        await UpdateSwitchUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Switch.Name,
             _edit.Model,
             _edit.Managed,
@@ -262,7 +277,7 @@
     {
         if (_editingPortIndex < 0)
         {
-            await AddSwitchPortUseCase.ExecuteAsync(
+            await AddPortUseCase.ExecuteAsync(
                 Switch.Name,
                 port.Type,
                 port.Speed,
@@ -270,7 +285,7 @@
         }
         else
         {
-            await UpdateSwitchPortUseCase.ExecuteAsync(
+            await UpdatePortUseCase.ExecuteAsync(
                 Switch.Name,
                 _editingPortIndex,
                 port.Type,
@@ -284,7 +299,7 @@
 
     async Task HandlePortDelete(Port _)
     {
-        await RemoveSwitchPortUseCase.ExecuteAsync(
+        await RemovePortUseCase.ExecuteAsync(
             Switch.Name,
             _editingPortIndex);
 

+ 10 - 6
Shared.Rcl/Systems/SystemCardComponent.razor

@@ -2,12 +2,12 @@
 @using RackPeek.Domain.Resources.SystemResources
 @using RackPeek.Domain.Resources.SystemResources.UseCases
 @using RackPeek.Domain.UseCases.Drives
-@inject UpdateSystemUseCase UpdateSystemUseCase
+@inject UpdateSystemUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<SystemResource> GetByNameUseCase
 @inject IAddDriveUseCase<SystemResource> AddDriveUseCase
 @inject IUpdateDriveUseCase<SystemResource> UpdateDriveUseCase
 @inject IRemoveDriveUseCase<SystemResource> RemoveDriveUseCase
-@inject IDeleteResourceUseCase<SystemResource> DeleteSystemUseCase
+@inject IDeleteResourceUseCase<SystemResource> DeleteUseCase
 @inject ICloneResourceUseCase<SystemResource> CloneUseCase
 @inject NavigationManager Nav
 @inject IRenameResourceUseCase<SystemResource> RenameUseCase
@@ -235,13 +235,17 @@
             {
                 <MarkdownEditor
                     @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                    ShowActionButtons="false"
+                    TestIdPrefix="system-notes-editor" />
+
             }
             else
             {
                 <MarkdownViewer
                     Value="@System.Notes"
-                    ShowEditButton="false"/>
+                    ShowEditButton="false"
+                    TestIdPrefix="system-notes-viewer" />
+
             }
         </div>
 
@@ -314,7 +318,7 @@
     async Task HandleParentSelected(string? name)
     {
         SelectedParentName = name;
-        await UpdateSystemUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             System.Name,
             System.Type,
             System.Os,
@@ -400,7 +404,7 @@
     {
         _confirmDeleteOpen = false;
 
-        await DeleteSystemUseCase.ExecuteAsync(System.Name);
+        await DeleteUseCase.ExecuteAsync(System.Name);
 
         if (OnDeleted.HasDelegate)
             await OnDeleted.InvokeAsync(System.Name);

+ 79 - 71
Shared.Rcl/Ups/UpsCardComponent.razor

@@ -1,16 +1,20 @@
 @using RackPeek.Domain.Resources.UpsUnits
-@inject UpdateUpsUseCase UpdateUpsUseCase
+@inject UpdateUpsUseCase UpdateUseCase
 @inject IGetResourceByNameUseCase<Ups> GetByNameUseCase
 @inject IDeleteResourceUseCase<Ups> DeleteUseCase
 @inject IRenameResourceUseCase<Ups> RenameUseCase
 @inject ICloneResourceUseCase<Ups> CloneUseCase
-
 @inject NavigationManager Nav
-<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900"
+     data-testid=@($"ups-item-{Ups.Name.Replace(" ", "-")}")>
+
     <div class="flex justify-between items-center mb-3">
 
         <div class="text-zinc-100 hover:text-emerald-300">
-            <NavLink href="@($"resources/hardware/{Ups.Name}")" class="block">
+            <NavLink href="@($"resources/hardware/{Ups.Name}")"
+                     class="block"
+                     data-testid="open-ups-link">
                 @Ups.Name
             </NavLink>
         </div>
@@ -18,35 +22,40 @@
         <div class="flex gap-3 text-xs">
             @if (!_isEditing)
             {
-                <button class="text-zinc-400 hover:text-zinc-200"
+                <button data-testid="edit-ups-button"
+                        class="text-zinc-400 hover:text-zinc-200"
                         @onclick="BeginEdit">
                     Edit
                 </button>
-                <button class="text-xs text-blue-400 hover:text-blue-300 transition"
-                        title="Rename service"
+
+                <button data-testid="rename-ups-button"
+                        class="text-blue-400 hover:text-blue-300 transition"
                         @onclick="OpenRename">
                     Rename
                 </button>
-                <button
-                    class="text-xs text-emerald-400 hover:text-emerald-300 transition"
-                    title="Clone service"
-                    @onclick="OpenClone">
+
+                <button data-testid="clone-ups-button"
+                        class="text-emerald-400 hover:text-emerald-300 transition"
+                        @onclick="OpenClone">
                     Clone
                 </button>
-                <button class="text-red-400 hover:text-red-300 transition"
-                        title="Delete UPS"
+
+                <button data-testid="delete-ups-button"
+                        class="text-red-400 hover:text-red-300 transition"
                         @onclick="ConfirmDelete">
                     Delete
                 </button>
             }
             else
             {
-                <button class="text-emerald-400 hover:text-emerald-300"
+                <button data-testid="save-ups-button"
+                        class="text-emerald-400 hover:text-emerald-300"
                         @onclick="Save">
                     Save
                 </button>
 
-                <button class="text-zinc-500 hover:text-zinc-300"
+                <button data-testid="cancel-ups-button"
+                        class="text-zinc-500 hover:text-zinc-300"
                         @onclick="Cancel">
                     Cancel
                 </button>
@@ -57,95 +66,94 @@
     <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
 
         <!-- Model -->
-        <div>
+        <div data-testid="ups-model-section">
             <div class="text-zinc-400 mb-1">Model</div>
+
             @if (_isEditing)
             {
-                <input
-                    class="w-full px-3 py-2 rounded-md
-                           bg-zinc-800 text-zinc-100
-                           border border-zinc-600
-                           placeholder-zinc-500
-                           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
-                           hover:border-zinc-400
-                           transition-colors duration-150
-                           cursor-text"
-                    @bind="_edit.Model"/>
+                <input data-testid="ups-model-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Model" />
             }
             else if (!string.IsNullOrWhiteSpace(Ups.Model))
             {
-                <div class="text-zinc-300">@Ups.Model</div>
+                <div class="text-zinc-300"
+                     data-testid="ups-model-value">
+                    @Ups.Model
+                </div>
             }
         </div>
 
         <!-- Capacity -->
-        <div>
+        <div data-testid="ups-capacity-section">
             <div class="text-zinc-400 mb-1">Capacity</div>
+
             @if (_isEditing)
             {
                 <input type="number"
-                       class="w-full px-3 py-2 rounded-md
-                           bg-zinc-800 text-zinc-100
-                           border border-zinc-600
-                           placeholder-zinc-500
-                           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
-                           hover:border-zinc-400
-                           transition-colors duration-150
-                           cursor-text"
-                       @bind="_edit.Va"/>
+                       data-testid="ups-capacity-input"
+                       class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
+                       @bind="_edit.Va" />
             }
             else if (Ups.Va is not null)
             {
-                <div class="text-zinc-300">@Ups.Va VA</div>
+                <div class="text-zinc-300"
+                     data-testid="ups-capacity-value">
+                    @Ups.Va VA
+                </div>
             }
         </div>
-        <ResourceTagEditor Resource="Ups"/>
 
-        <div class="md:col-span-2">
+        <ResourceTagEditor Resource="Ups" />
+
+        <div class="md:col-span-2"
+             data-testid="ups-notes-section">
+
             <div class="text-zinc-400 mb-1">Notes</div>
 
             @if (_isEditing)
             {
-                <MarkdownEditor
-                    @bind-Value="_edit.Notes"
-                    ShowActionButtons="false"/>
+                <MarkdownEditor @bind-Value="_edit.Notes"
+                                ShowActionButtons="false"
+                                TestIdPrefix="ups-notes-editor" />
             }
             else
             {
-                <MarkdownViewer
-                    Value="@Ups.Notes"
-                    ShowEditButton="false"/>
+                <MarkdownViewer Value="@Ups.Notes"
+                                ShowEditButton="false"
+                                TestIdPrefix="ups-notes-viewer" />
             }
         </div>
     </div>
 </div>
 
-<ConfirmModal
-    IsOpen="_confirmDeleteOpen"
-    IsOpenChanged="v => _confirmDeleteOpen = v"
-    Title="Delete UPS"
-    ConfirmText="Delete"
-    ConfirmClass="bg-red-600 hover:bg-red-500"
-    OnConfirm="DeleteUps"
-    TestIdPrefix="Ups">>
+<ConfirmModal IsOpen="_confirmDeleteOpen"
+              IsOpenChanged="v => _confirmDeleteOpen = v"
+              Title="Delete UPS"
+              ConfirmText="Delete"
+              ConfirmClass="bg-red-600 hover:bg-red-500"
+              OnConfirm="DeleteUps"
+              TestIdPrefix="Ups">
     Are you sure you want to delete <strong>@Ups.Name</strong>?
 </ConfirmModal>
-<StringValueModal
-    IsOpen="_renameOpen"
-    IsOpenChanged="v => _renameOpen = v"
-    Title="Rename Ups"
-    Description="Enter a new name for this Ups"
-    Label="New Ups name"
-    Value="@Ups.Name"
-    OnSubmit="HandleRenameSubmit"/>
-<StringValueModal
-    IsOpen="_cloneOpen"
-    IsOpenChanged="v => _cloneOpen = v"
-    Title="Clone resource"
-    Description="Enter a name for the cloned resource"
-    Label="New resource name"
-    Value="@($"{Ups.Name}-copy")"
-    OnSubmit="HandleCloneSubmit"/>
+
+<StringValueModal IsOpen="_renameOpen"
+                  IsOpenChanged="v => _renameOpen = v"
+                  Title="Rename Ups"
+                  Description="Enter a new name for this Ups"
+                  Label="New Ups name"
+                  Value="@Ups.Name"
+                  OnSubmit="HandleRenameSubmit"
+                  TestIdPrefix="ups-rename" />
+
+<StringValueModal IsOpen="_cloneOpen"
+                  IsOpenChanged="v => _cloneOpen = v"
+                  Title="Clone resource"
+                  Description="Enter a name for the cloned resource"
+                  Label="New resource name"
+                  Value="@($"{Ups.Name}-copy")"
+                  OnSubmit="HandleCloneSubmit"
+                  TestIdPrefix="ups-clone" />
 
 @code {
     [Parameter] [EditorRequired] public Ups Ups { get; set; } = default!;
@@ -167,7 +175,7 @@
     {
         _isEditing = false;
 
-        await UpdateUpsUseCase.ExecuteAsync(
+        await UpdateUseCase.ExecuteAsync(
             Ups.Name,
             _edit.Model,
             _edit.Va,

+ 61 - 0
Tests.E2e/AccessPointTests.cs

@@ -0,0 +1,61 @@
+using Tests.E2e.Infra;
+using Tests.E2e.PageObjectModels;
+using Xunit.Abstractions;
+
+namespace Tests.E2e;
+
+public class AccessPointTests(
+    PlaywrightFixture fixture,
+    ITestOutputHelper output) : E2ETestBase(fixture, output)
+{
+    private readonly ITestOutputHelper _output = output;
+
+    [Fact]
+    public async Task User_Can_Add_And_Delete_AccessPoint()
+    {
+        var (context, page) = await CreatePageAsync();
+        var resourceName = $"e2e-ap-{Guid.NewGuid():N}"[..16];
+
+        try
+        {
+            // Go home
+            await page.GotoAsync(fixture.BaseUrl);
+
+            _output.WriteLine($"URL after Goto: {page.Url}");
+
+            var layout = new MainLayoutPom(page);
+            await layout.AssertLoadedAsync();
+            await layout.GotoHardwareAsync();
+
+            var hardwarePage = new HardwareTreePom(page);
+            await hardwarePage.AssertLoadedAsync();
+            await hardwarePage.GotoAccessPointsListAsync();
+
+            var listPage = new AccessPointsListPom(page);
+            await listPage.AssertLoadedAsync();
+            await listPage.AddAccessPointAsync(resourceName);
+            await listPage.AssertAccessPointExists(resourceName);
+            await listPage.DeleteAccessPointAsync(resourceName);
+            await listPage.AssertAccessPointDoesNotExist(resourceName);
+
+            await context.CloseAsync();
+        }
+        catch (Exception ex)
+        {
+            _output.WriteLine("TEST FAILED — Capturing diagnostics");
+
+            _output.WriteLine($"Current URL: {page.Url}");
+
+            var html = await page.ContentAsync();
+            _output.WriteLine("==== DOM SNAPSHOT START ====");
+            _output.WriteLine(html);
+            _output.WriteLine("==== DOM SNAPSHOT END ====");
+
+            throw;
+        }
+        finally
+        {
+            await context.CloseAsync();
+        }
+    }
+}

+ 118 - 0
Tests.E2e/PageObjectModels/AccessPointListPom.cs

@@ -0,0 +1,118 @@
+using Microsoft.Playwright;
+
+namespace Tests.E2e.PageObjectModels;
+
+public class AccessPointsListPom(IPage page)
+{
+    public AddResourceComponent AddAccessPoint => new(page, "accesspoint");
+
+    public ILocator PageRoot => page.GetByTestId("accesspoints-page-root");
+    public ILocator PageTitle => page.GetByTestId("accesspoints-page-title");
+
+    public ILocator Loading => page.GetByTestId("accesspoints-loading");
+    public ILocator EmptyState => page.GetByTestId("accesspoints-empty");
+    public ILocator AccessPointsList => page.GetByTestId("accesspoints-list");
+
+    public ILocator AddSection => page.GetByTestId("accesspoints-add-section");
+
+    // These must match your AddResourceComponent test IDs
+    public ILocator AddInput => page.GetByTestId("add-accesspoint-input");
+    public ILocator AddButton => page.GetByTestId("add-accesspoint-button");
+
+    // -------------------------------------------------
+    // Dynamic AccessPoint Items
+    // -------------------------------------------------
+
+    public ILocator AccessPointItem(string name)
+    {
+        return page.GetByTestId($"accesspoint-item-{Sanitize(name)}");
+    }
+
+    public ILocator DeleteButton(string name)
+    {
+        return AccessPointItem(name)
+            .GetByTestId("delete-accesspoint-button");
+    }
+
+    public ILocator EditButton(string name)
+    {
+        return AccessPointItem(name)
+            .GetByTestId("edit-accesspoint-button");
+    }
+
+    public ILocator RenameButton(string name)
+    {
+        return AccessPointItem(name)
+            .GetByTestId("rename-accesspoint-button");
+    }
+
+    public ILocator CloneButton(string name)
+    {
+        return AccessPointItem(name)
+            .GetByTestId("clone-accesspoint-button");
+    }
+
+    // -------------------------------------------------
+    // Navigation
+    // -------------------------------------------------
+
+    public async Task GotoAsync(string baseUrl)
+    {
+        await page.GotoAsync($"{baseUrl}/accesspoints/list");
+        await AssertLoadedAsync();
+    }
+
+    public async Task AssertLoadedAsync()
+    {
+        await Assertions.Expect(PageRoot).ToBeVisibleAsync();
+        await Assertions.Expect(PageTitle).ToBeVisibleAsync();
+    }
+
+    public async Task WaitForListAsync()
+    {
+        await Assertions.Expect(AccessPointsList).ToBeVisibleAsync();
+    }
+
+    // -------------------------------------------------
+    // Actions
+    // -------------------------------------------------
+
+    public async Task AddAccessPointAsync(string name)
+    {
+        await AddAccessPoint.AddAsync(name);
+        await Assertions.Expect(AccessPointItem(name))
+            .ToBeVisibleAsync();
+    }
+
+    public async Task DeleteAccessPointAsync(string name)
+    {
+        await DeleteButton(name).ClickAsync();
+        await page.GetByTestId("AccessPoint-confirm-modal-confirm").ClickAsync();
+
+        await Assertions.Expect(AccessPointItem(name))
+            .Not.ToBeVisibleAsync();
+    }
+
+    public async Task OpenAccessPointAsync(string name)
+    {
+        await AccessPointItem(name).ClickAsync();
+        await page.WaitForURLAsync($"**/resources/hardware/{name}");
+    }
+
+    public async Task AssertAccessPointExists(string name)
+    {
+        await Assertions.Expect(AccessPointItem(name))
+            .ToBeVisibleAsync();
+    }
+
+    public async Task AssertAccessPointDoesNotExist(string name)
+    {
+        await Assertions.Expect(AccessPointItem(name))
+            .Not.ToBeVisibleAsync();
+    }
+
+    private static string Sanitize(string value)
+    {
+        return value.Replace(" ", "-");
+    }
+}