فهرست منبع

Add Playwright E2E tests for Other hardware UI

Mirrors the UPS E2E coverage: list add/delete, card
rename/clone/delete, edit/save, cancel-edit, and tag flows, with
OtherListPom/OtherCardPom page object models and an 'Other' nav
entry on HardwareTreePom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tim Jones 15 ساعت پیش
والد
کامیت
f5633ee45d

+ 225 - 0
Tests.E2e/OtherCardTests.cs

@@ -0,0 +1,225 @@
+using Microsoft.Playwright;
+using Tests.E2e.Infra;
+using Tests.E2e.PageObjectModels;
+using Xunit.Abstractions;
+
+namespace Tests.E2e;
+
+public class OtherCardTests(
+    PlaywrightFixture fixture,
+    ITestOutputHelper output) : E2ETestBase(fixture, output) {
+    private readonly PlaywrightFixture _fixture = fixture;
+    private readonly ITestOutputHelper _output = output;
+
+    // =============================================================
+    // Rename + Clone + Delete Flow
+    // =============================================================
+
+    [Fact]
+    public async Task User_Can_Rename_Clone_And_Delete_Other_From_Details_Page() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+
+        var originalName = $"e2e-oth-{Guid.NewGuid():N}"[..16];
+        var renamedName = $"e2e-oth-rn-{Guid.NewGuid():N}"[..16];
+        var cloneName = $"e2e-oth-cl-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync($"{_fixture.BaseUrl}/other/list");
+
+            var list = new OtherListPom(page);
+            await list.AddOtherAsync(originalName);
+
+            if (!page.Url.Contains($"/resources/hardware/{originalName}",
+                    StringComparison.OrdinalIgnoreCase))
+                await list.OpenOtherAsync(originalName);
+
+            var card = new OtherCardPom(page);
+            await card.AssertVisibleAsync(originalName);
+
+            // -------------------------
+            // Rename
+            // -------------------------
+            await card.RenameAsync(originalName, renamedName);
+            await card.AssertVisibleAsync(renamedName);
+
+            // -------------------------
+            // Clone
+            // -------------------------
+            await card.CloneAsync(renamedName, cloneName);
+            await card.AssertVisibleAsync(cloneName);
+
+            // -------------------------
+            // Delete clone
+            // -------------------------
+            await card.DeleteAsync(cloneName);
+            await page.WaitForURLAsync("**/hardware/tree");
+
+            // Navigate back and delete renamed
+            await page.GotoAsync($"{_fixture.BaseUrl}/resources/hardware/{renamedName}");
+            await card.AssertVisibleAsync(renamedName);
+
+            await card.DeleteAsync(renamedName);
+            await page.WaitForURLAsync("**/hardware/tree");
+        }
+        catch (Exception) {
+            _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();
+        }
+    }
+
+    // =============================================================
+    // Edit + Save Flow
+    // =============================================================
+
+    [Fact]
+    public async Task User_Can_Edit_And_Save_Other() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        var name = $"e2e-oth-edit-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync($"{_fixture.BaseUrl}/other/list");
+
+            var list = new OtherListPom(page);
+            await list.AddOtherAsync(name);
+
+            if (!page.Url.Contains($"/resources/hardware/{name}",
+                    StringComparison.OrdinalIgnoreCase))
+                await list.OpenOtherAsync(name);
+
+            var card = new OtherCardPom(page);
+            await card.AssertVisibleAsync(name);
+
+            await card.BeginEditAsync(name);
+
+            await card.ModelInput(name).FillAsync("Building Bridge XG");
+            await card.DescriptionInput(name).FillAsync("Microwave radio bridge");
+
+            await card.SaveAsync(name);
+
+            await Assertions.Expect(
+                card.ModelValue(name)
+            ).ToContainTextAsync("Building Bridge XG");
+
+            await Assertions.Expect(
+                card.DescriptionValue(name)
+            ).ToContainTextAsync("Microwave radio bridge");
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+
+    // =============================================================
+    // Cancel Edit Flow
+    // =============================================================
+
+    [Fact]
+    public async Task User_Can_Cancel_Other_Edit_Without_Saving() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        var name = $"e2e-oth-cancel-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync($"{_fixture.BaseUrl}/other/list");
+
+            var list = new OtherListPom(page);
+            await list.AddOtherAsync(name);
+
+            if (!page.Url.Contains($"/resources/hardware/{name}",
+                    StringComparison.OrdinalIgnoreCase))
+                await list.OpenOtherAsync(name);
+
+            var card = new OtherCardPom(page);
+            await card.AssertVisibleAsync(name);
+
+            await card.BeginEditAsync(name);
+
+            await card.ModelInput(name).FillAsync("ShouldNotPersist");
+            await card.DescriptionInput(name).FillAsync("ShouldNotPersist");
+
+            await card.CancelAsync(name);
+
+            // Verify edit mode exited
+            await Assertions.Expect(
+                card.EditButton(name)
+            ).ToBeVisibleAsync();
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+
+
+    [Fact]
+    public async Task User_Can_Add_And_Remove_Tags_From_Other_Card() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        var name = $"e2e-oth-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync(_fixture.BaseUrl);
+
+            var layout = new MainLayoutPom(page);
+            await layout.AssertLoadedAsync();
+            await layout.GotoHardwareAsync();
+
+            var hardwareTree = new HardwareTreePom(page);
+            await hardwareTree.AssertLoadedAsync();
+            await hardwareTree.GotoOtherListAsync();
+
+            var list = new OtherListPom(page);
+            await list.AssertLoadedAsync();
+
+            await list.AddOtherAsync(name);
+            await page.WaitForURLAsync($"**/resources/hardware/{name}");
+
+            var card = new OtherCardPom(page);
+            await card.AssertVisibleAsync(name);
+
+            TagsPom tags = card.Tags;
+
+            // -------------------------------------------------
+            // Add multiple tags in one modal interaction
+            // -------------------------------------------------
+
+            await tags.AddTagsAsync("other", "Foo", "Bar", "Baz");
+
+            await tags.AssertTagVisibleAsync("other", "Foo");
+            await tags.AssertTagVisibleAsync("other", "Bar");
+            await tags.AssertTagVisibleAsync("other", "Baz");
+
+            // -------------------------------------------------
+            // Remove a single tag
+            // -------------------------------------------------
+
+            await tags.RemoveTagAsync("other", "Bar");
+
+            await tags.AssertTagNotVisibleAsync("other", "Bar");
+            await tags.AssertTagVisibleAsync("other", "Foo");
+            await tags.AssertTagVisibleAsync("other", "Baz");
+
+            // -------------------------------------------------
+            // Reload to verify persistence
+            // -------------------------------------------------
+
+            await page.ReloadAsync();
+
+            await tags.AssertTagVisibleAsync("other", "Foo");
+            await tags.AssertTagVisibleAsync("other", "Baz");
+            await tags.AssertTagNotVisibleAsync("other", "Bar");
+
+            await context.CloseAsync();
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+}

+ 58 - 0
Tests.E2e/OtherTests.cs

@@ -0,0 +1,58 @@
+using Microsoft.Playwright;
+using Tests.E2e.Infra;
+using Tests.E2e.PageObjectModels;
+using Xunit.Abstractions;
+
+namespace Tests.E2e;
+
+public class OtherTests(
+    PlaywrightFixture fixture,
+    ITestOutputHelper output) : E2ETestBase(fixture, output) {
+    private readonly PlaywrightFixture _fixture = fixture;
+    private readonly ITestOutputHelper _output = output;
+
+    [Fact]
+    public async Task User_Can_Add_And_Delete_Other() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        var resourceName = $"e2e-other-{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.GotoOtherListAsync();
+
+            var listPage = new OtherListPom(page);
+            await listPage.AssertLoadedAsync();
+            await listPage.AddOtherAsync(resourceName);
+            await listPage.AssertOtherExists(resourceName);
+            await listPage.DeleteOtherAsync(resourceName);
+            await listPage.AssertOtherDoesNotExist(resourceName);
+
+            await context.CloseAsync();
+        }
+        catch (Exception) {
+            _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();
+        }
+    }
+}

+ 6 - 0
Tests.E2e/PageObjectModels/HardwareTreePom.cs

@@ -27,6 +27,7 @@ public class HardwareTreePom(IPage page) {
     public ILocator NavUps => page.GetByTestId("nav-ups");
     public ILocator NavDesktops => page.GetByTestId("nav-desktops");
     public ILocator NavLaptops => page.GetByTestId("nav-laptops");
+    public ILocator NavOther => page.GetByTestId("nav-other");
 
     public async Task GotoAsync(string baseUrl) {
         await page.GotoAsync($"{baseUrl}/hardware/tree");
@@ -145,4 +146,9 @@ public class HardwareTreePom(IPage page) {
         await NavLaptops.ClickAsync();
         await page.WaitForURLAsync("**/laptops/list");
     }
+
+    public async Task GotoOtherListAsync() {
+        await NavOther.ClickAsync();
+        await page.WaitForURLAsync("**/other/list");
+    }
 }

+ 142 - 0
Tests.E2e/PageObjectModels/OtherCardPom.cs

@@ -0,0 +1,142 @@
+using Microsoft.Playwright;
+
+namespace Tests.E2e.PageObjectModels;
+
+public class OtherCardPom(IPage page) {
+    public TagsPom Tags => new(page);
+    public LabelsPom Labels => new(page);
+
+    // -------------------------------------------------
+    // Notes
+    // -------------------------------------------------
+
+    public ILocator NotesViewer
+        => page.GetByTestId("other-notes-viewer-container");
+
+    public ILocator NotesEditor
+        => page.GetByTestId("other-notes-editor-container");
+
+    // -------------------------------------------------
+    // Confirm Modal (TestIdPrefix="Other")
+    // -------------------------------------------------
+
+    public ILocator ConfirmDeleteButton
+        => page.GetByTestId("Other-confirm-modal-confirm");
+
+    // -------------------------------------------------
+    // Rename Modal (TestIdPrefix="other-rename")
+    // -------------------------------------------------
+
+    public ILocator RenameInput
+        => page.GetByTestId("other-rename-string-value-modal-input");
+
+    public ILocator RenameSubmit
+        => page.GetByTestId("other-rename-string-value-modal-submit");
+
+    // -------------------------------------------------
+    // Clone Modal (TestIdPrefix="other-clone")
+    // -------------------------------------------------
+
+    public ILocator CloneInput
+        => page.GetByTestId("other-clone-string-value-modal-input");
+
+    public ILocator CloneSubmit
+        => page.GetByTestId("other-clone-string-value-modal-submit");
+
+    // -------------------------------------------------
+    // Root
+    // -------------------------------------------------
+
+    private static string Sanitize(string value)
+        => value.Replace(" ", "-");
+
+    public ILocator Card(string name)
+        => page.GetByTestId($"other-item-{Sanitize(name)}");
+
+    public ILocator Link(string name)
+        => page.GetByTestId($"other-item-{Sanitize(name)}-link");
+
+    // -------------------------------------------------
+    // Action Buttons
+    // -------------------------------------------------
+
+    public ILocator EditButton(string name)
+        => Card(name).GetByTestId("edit-other-button");
+
+    public ILocator SaveButton(string name)
+        => Card(name).GetByTestId("save-other-button");
+
+    public ILocator CancelButton(string name)
+        => Card(name).GetByTestId("cancel-other-button");
+
+    public ILocator RenameButton(string name)
+        => Card(name).GetByTestId("rename-other-button");
+
+    public ILocator CloneButton(string name)
+        => Card(name).GetByTestId("clone-other-button");
+
+    public ILocator DeleteButton(string name)
+        => Card(name).GetByTestId("delete-other-button");
+
+    // -------------------------------------------------
+    // Edit Inputs
+    // -------------------------------------------------
+
+    public ILocator ModelInput(string name)
+        => Card(name).GetByTestId("other-model-input");
+
+    public ILocator DescriptionInput(string name)
+        => Card(name).GetByTestId("other-description-input");
+
+    // -------------------------------------------------
+    // View Values
+    // -------------------------------------------------
+
+    public ILocator ModelValue(string name)
+        => Card(name).GetByTestId("other-model-value");
+
+    public ILocator DescriptionValue(string name)
+        => Card(name).GetByTestId("other-description-value");
+
+    // -------------------------------------------------
+    // Assertions
+    // -------------------------------------------------
+
+    public async Task AssertVisibleAsync(string name) => await Assertions.Expect(Card(name)).ToBeVisibleAsync();
+
+    // -------------------------------------------------
+    // High-Level Actions
+    // -------------------------------------------------
+
+    public async Task BeginEditAsync(string name)
+        => await EditButton(name).ClickAsync();
+
+    public async Task SaveAsync(string name)
+        => await SaveButton(name).ClickAsync();
+
+    public async Task CancelAsync(string name)
+        => await CancelButton(name).ClickAsync();
+
+    public async Task RenameAsync(string currentName, string newName) {
+        await RenameButton(currentName).ClickAsync();
+
+        await RenameInput.FillAsync(newName);
+        await RenameSubmit.ClickAsync();
+
+        await page.WaitForURLAsync($"**/resources/hardware/{newName}");
+    }
+
+    public async Task CloneAsync(string currentName, string cloneName) {
+        await CloneButton(currentName).ClickAsync();
+
+        await CloneInput.FillAsync(cloneName);
+        await CloneSubmit.ClickAsync();
+
+        await page.WaitForURLAsync($"**/resources/hardware/{cloneName}");
+    }
+
+    public async Task DeleteAsync(string name) {
+        await DeleteButton(name).ClickAsync();
+        await ConfirmDeleteButton.ClickAsync();
+    }
+}

+ 112 - 0
Tests.E2e/PageObjectModels/OtherListPom.cs

@@ -0,0 +1,112 @@
+using Microsoft.Playwright;
+
+namespace Tests.E2e.PageObjectModels;
+
+public class OtherListPom(IPage page) {
+    public AddResourceComponent AddOther => new(page, "other");
+
+    public ILocator PageRoot => page.GetByTestId("other-page-root");
+    public ILocator PageTitle => page.GetByTestId("other-page-title");
+
+    public ILocator Loading => page.GetByTestId("other-loading");
+    public ILocator EmptyState => page.GetByTestId("other-empty");
+    public ILocator OtherList => page.GetByTestId("other-list");
+
+    public ILocator AddSection => page.GetByTestId("other-add-section");
+
+    // Must match AddResourceComponent test IDs
+    public ILocator AddInput => page.GetByTestId("add-other-input");
+    public ILocator AddButton => page.GetByTestId("add-other-button");
+
+    // -------------------------------------------------
+    // Dynamic Other Items
+    // -------------------------------------------------
+
+    public ILocator OtherItem(string name) => page.GetByTestId($"other-item-{Sanitize(name)}");
+
+    public ILocator OpenLink(string name) {
+        return OtherItem(name)
+            .GetByTestId($"other-item-{Sanitize(name)}-link");
+    }
+
+    public ILocator EditButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("edit-other-button");
+    }
+
+    public ILocator SaveButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("save-other-button");
+    }
+
+    public ILocator CancelButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("cancel-other-button");
+    }
+
+    public ILocator RenameButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("rename-other-button");
+    }
+
+    public ILocator CloneButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("clone-other-button");
+    }
+
+    public ILocator DeleteButton(string name) {
+        return OtherItem(name)
+            .GetByTestId("delete-other-button");
+    }
+
+    // -------------------------------------------------
+    // Navigation
+    // -------------------------------------------------
+
+    public async Task GotoAsync(string baseUrl) {
+        await page.GotoAsync($"{baseUrl}/other/list");
+        await AssertLoadedAsync();
+    }
+
+    public async Task AssertLoadedAsync() {
+        await Assertions.Expect(PageRoot).ToBeVisibleAsync();
+        await Assertions.Expect(PageTitle).ToBeVisibleAsync();
+    }
+
+    public async Task WaitForListAsync() => await Assertions.Expect(OtherList).ToBeVisibleAsync();
+
+    // -------------------------------------------------
+    // Actions
+    // -------------------------------------------------
+
+    public async Task AddOtherAsync(string name) {
+        await AddOther.AddAsync(name);
+        await Assertions.Expect(OtherItem(name))
+            .ToBeVisibleAsync();
+    }
+
+    public async Task DeleteOtherAsync(string name) {
+        await DeleteButton(name).ClickAsync();
+        await page.GetByTestId("Other-confirm-modal-confirm").ClickAsync();
+
+        await Assertions.Expect(OtherItem(name))
+            .Not.ToBeVisibleAsync();
+    }
+
+    public async Task OpenOtherAsync(string name) {
+        await OpenLink(name).ClickAsync();
+        await page.WaitForURLAsync($"**/resources/hardware/{name}");
+    }
+
+    public async Task AssertOtherExists(string name) {
+        await Assertions.Expect(OtherItem(name))
+            .ToBeVisibleAsync();
+    }
+
+    public async Task AssertOtherDoesNotExist(string name) {
+        await Assertions.Expect(OtherItem(name))
+            .Not.ToBeVisibleAsync();
+    }
+
+    private static string Sanitize(string value) => value.Replace(" ", "-");
+}