From 9b5195f1d6ffd81ad957616cedcecd35382bedac Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Tue, 23 Jun 2026 17:51:34 -0500 Subject: [PATCH] Use web-first Playwright assertions in UI tests Replace non-waiting Assert.True(await locator.IsVisibleAsync()) checks with auto-retrying Assertions.Expect(locator).ToBeVisibleAsync()/ToBeHiddenAsync() to remove flakiness against Blazor's async rendering. Table-row/field page objects (Metadata/Roles/Users/Profile) now expose ILocator helpers instead of Task, so deletion checks wait for the element to disappear. Also: rename ScreenshotHelper.CaptureIfFailedAsync to CaptureAsync (xUnit v2 cannot expose the test outcome to DisposeAsync, so it always captures the final page state), drop the unused TestConstants.ServerPort/BaseUrl now that ports bind dynamically, and fix IClassFixture doc comments to ICollectionFixture. Co-Authored-By: Claude Opus 4.6 --- .../ConfiguredServerFixture.cs | 4 +-- .../Pages/MetadataPage.cs | 9 ++++-- .../Pages/ProfilePage.cs | 18 +++++------ .../Pages/RolesPage.cs | 8 ++--- .../Pages/UsersPage.cs | 7 +++-- .../PlaywrightFixture.cs | 4 +-- .../ScreenshotHelper.cs | 15 +++++----- LANCommander.Server.UI.Tests/TestConstants.cs | 2 -- .../Tests/AdminNavigationTests.cs | 30 +++++++++---------- .../Tests/FirstTimeSetupTests.cs | 16 +++++----- .../Tests/GameEditTests.cs | 2 +- .../Tests/GameImportTests.cs | 8 ++--- .../Tests/LoginTests.cs | 6 ++-- .../Tests/MetadataTests.cs | 26 ++++++++-------- .../Tests/ProfileTests.cs | 24 +++++++-------- .../Tests/RoleManagementTests.cs | 14 ++++----- .../Tests/SettingsTests.cs | 26 ++++++++-------- .../Tests/UserManagementTests.cs | 12 ++++---- 18 files changed, 109 insertions(+), 122 deletions(-) diff --git a/LANCommander.Server.UI.Tests/ConfiguredServerFixture.cs b/LANCommander.Server.UI.Tests/ConfiguredServerFixture.cs index a35ddd6a..5d316ea6 100644 --- a/LANCommander.Server.UI.Tests/ConfiguredServerFixture.cs +++ b/LANCommander.Server.UI.Tests/ConfiguredServerFixture.cs @@ -11,8 +11,8 @@ namespace LANCommander.Server.UI.Tests; /// /// Shared fixture that starts the server via WebApplicationFactory, programmatically creates -/// the admin user, and makes it available for all tests in a class. -/// Used via IClassFixture<ConfiguredServerFixture>. +/// the admin user, and makes it available for all tests in the collection. +/// Shared across the "Server" collection via ICollectionFixture<ConfiguredServerFixture>. /// public class ConfiguredServerFixture : IAsyncLifetime { diff --git a/LANCommander.Server.UI.Tests/Pages/MetadataPage.cs b/LANCommander.Server.UI.Tests/Pages/MetadataPage.cs index 1c684a58..de01d321 100644 --- a/LANCommander.Server.UI.Tests/Pages/MetadataPage.cs +++ b/LANCommander.Server.UI.Tests/Pages/MetadataPage.cs @@ -44,11 +44,14 @@ public class MetadataPage await _page.WaitForSelectorAsync(".ant-modal", new() { State = WaitForSelectorState.Hidden, Timeout = 5000 }); } - public async Task IsItemVisibleAsync(string name) + /// + /// Locator for an item row's name cell within the table body. + /// Use with web-first assertions, e.g. Expect(page.Item("Action")).ToBeVisibleAsync(). + /// + public ILocator Item(string name) { // Look for the name text within the table body - var cell = _page.Locator(".ant-table-tbody").GetByText(name, new() { Exact = true }); - return await cell.IsVisibleAsync(); + return _page.Locator(".ant-table-tbody").GetByText(name, new() { Exact = true }); } public async Task EditItemAsync(string oldName, string newName) diff --git a/LANCommander.Server.UI.Tests/Pages/ProfilePage.cs b/LANCommander.Server.UI.Tests/Pages/ProfilePage.cs index 9b63d075..ee2c1238 100644 --- a/LANCommander.Server.UI.Tests/Pages/ProfilePage.cs +++ b/LANCommander.Server.UI.Tests/Pages/ProfilePage.cs @@ -28,11 +28,14 @@ public class ProfilePage return await input.InputValueAsync(); } - public async Task HasFieldAsync(string label) + /// + /// Locator for a form field (text or password) with the given label. + /// Use with web-first assertions, e.g. Expect(page.Field("Username")).ToBeVisibleAsync(). + /// + public ILocator Field(string label) { - return await _page.Locator(".ant-form-item") - .Filter(new() { HasText = label }) - .IsVisibleAsync(); + return _page.Locator(".ant-form-item") + .Filter(new() { HasText = label }); } public async Task SetAliasAsync(string alias) @@ -69,13 +72,6 @@ public class ProfilePage await _page.WaitForSelectorAsync("text=Change Password", new() { Timeout = 10000 }); } - public async Task HasPasswordFieldAsync(string label) - { - return await _page.Locator(".ant-form-item") - .Filter(new() { HasText = label }) - .IsVisibleAsync(); - } - /// /// Fills the change password form and submits it. /// diff --git a/LANCommander.Server.UI.Tests/Pages/RolesPage.cs b/LANCommander.Server.UI.Tests/Pages/RolesPage.cs index ea18d4cf..608504b6 100644 --- a/LANCommander.Server.UI.Tests/Pages/RolesPage.cs +++ b/LANCommander.Server.UI.Tests/Pages/RolesPage.cs @@ -47,12 +47,12 @@ public class RolesPage } /// - /// Check whether a role with the given name appears in the table. + /// Locator for a role row with the given name. + /// Use with web-first assertions, e.g. Expect(page.Role("Administrator")).ToBeVisibleAsync(). /// - public async Task IsRoleVisibleAsync(string name) + public ILocator Role(string name) { - var row = _page.Locator("table tbody tr").Filter(new() { HasText = name }); - return await row.CountAsync() > 0; + return _page.Locator("table tbody tr").Filter(new() { HasText = name }); } /// diff --git a/LANCommander.Server.UI.Tests/Pages/UsersPage.cs b/LANCommander.Server.UI.Tests/Pages/UsersPage.cs index 763cb713..2bdb7d47 100644 --- a/LANCommander.Server.UI.Tests/Pages/UsersPage.cs +++ b/LANCommander.Server.UI.Tests/Pages/UsersPage.cs @@ -49,11 +49,12 @@ public class UsersPage } /// - /// Checks whether a given username appears in a table row. + /// Locator for a table row containing the given username. + /// Use with web-first assertions, e.g. Expect(page.User("admin")).ToBeVisibleAsync(). /// - public async Task IsUserVisibleAsync(string username) + public ILocator User(string username) { - return await _page.Locator("tr.ant-table-row", new() { HasTextString = username }).CountAsync() > 0; + return _page.Locator("tr.ant-table-row", new() { HasTextString = username }); } /// diff --git a/LANCommander.Server.UI.Tests/PlaywrightFixture.cs b/LANCommander.Server.UI.Tests/PlaywrightFixture.cs index 7acd04f2..ddcb9199 100644 --- a/LANCommander.Server.UI.Tests/PlaywrightFixture.cs +++ b/LANCommander.Server.UI.Tests/PlaywrightFixture.cs @@ -26,12 +26,12 @@ public class PlaywrightFixture : IAsyncLifetime Playwright.Dispose(); } - public async Task NewContextAsync(string? baseUrl = null) + public async Task NewContextAsync(string baseUrl) { return await Browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true, - BaseURL = baseUrl ?? TestConstants.BaseUrl, + BaseURL = baseUrl, }); } } diff --git a/LANCommander.Server.UI.Tests/ScreenshotHelper.cs b/LANCommander.Server.UI.Tests/ScreenshotHelper.cs index 6785d0ab..1863b27d 100644 --- a/LANCommander.Server.UI.Tests/ScreenshotHelper.cs +++ b/LANCommander.Server.UI.Tests/ScreenshotHelper.cs @@ -5,8 +5,10 @@ using Xunit.Abstractions; namespace LANCommander.Server.UI.Tests; /// -/// Captures a full-page screenshot when a test fails. -/// Screenshots are saved to a "Screenshots" directory that CI uploads as an artifact. +/// Captures a full-page screenshot of the final page state at the end of each test. +/// Screenshots are saved to a "Screenshots" directory that CI uploads as an artifact, +/// making failures easy to diagnose. (xUnit v2 does not expose the test outcome to +/// DisposeAsync, so we capture unconditionally and name each file after the test.) /// public static class ScreenshotHelper { @@ -16,17 +18,14 @@ public static class ScreenshotHelper string.Empty); /// - /// Captures a screenshot of the current page state. - /// Call this in DisposeAsync — it extracts the test name from ITestOutputHelper. + /// Captures a screenshot of the current page state, named after the running test. + /// Call this from DisposeAsync — it extracts the test name from ITestOutputHelper. /// - public static async Task CaptureIfFailedAsync(IPage? page, ITestOutputHelper? output) + public static async Task CaptureAsync(IPage? page, ITestOutputHelper? output) { if (page == null || output == null) return; - // xUnit only writes to ITestOutputHelper when a test fails or produces output. - // We always capture since DisposeAsync doesn't know the test result, - // but name the file so it's easy to correlate. var testName = GetTestDisplayName(output) ?? $"Unknown_{Guid.NewGuid():N}"; try diff --git a/LANCommander.Server.UI.Tests/TestConstants.cs b/LANCommander.Server.UI.Tests/TestConstants.cs index 6e191890..6c4f0d95 100644 --- a/LANCommander.Server.UI.Tests/TestConstants.cs +++ b/LANCommander.Server.UI.Tests/TestConstants.cs @@ -4,6 +4,4 @@ public static class TestConstants { public const string AdminUserName = "admin"; public const string AdminPassword = "Password1234!"; - public const int ServerPort = 1337; - public static string BaseUrl => $"http://localhost:{ServerPort}"; } diff --git a/LANCommander.Server.UI.Tests/Tests/AdminNavigationTests.cs b/LANCommander.Server.UI.Tests/Tests/AdminNavigationTests.cs index bd6e6715..4232abb2 100644 --- a/LANCommander.Server.UI.Tests/Tests/AdminNavigationTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/AdminNavigationTests.cs @@ -8,7 +8,7 @@ namespace LANCommander.Server.UI.Tests.Tests; /// Tests for navigating around key parts of the admin application. /// These tests verify that the main admin pages are accessible and render correctly /// after logging in as an administrator. -/// Uses IClassFixture to start the server once for all tests in this class. +/// Uses the shared "Server" collection fixture so the server starts once for the whole collection. /// [Collection("Server")] public class AdminNavigationTests : IAsyncLifetime @@ -31,7 +31,7 @@ public class AdminNavigationTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -43,9 +43,9 @@ public class AdminNavigationTests : IAsyncLifetime Assert.True(await dashboard.IsDisplayedAsync()); // Dashboard should show playtime charts - Assert.True(await _page.GetByText("Top 10 Total Playtime (By Player)").IsVisibleAsync()); - Assert.True(await _page.GetByText("Top 10 Total Playtime (By Game)").IsVisibleAsync()); - Assert.True(await _page.GetByText("Top Average Session Length (By Game)").IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Top 10 Total Playtime (By Player)")).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByText("Top 10 Total Playtime (By Game)")).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByText("Top Average Session Length (By Game)")).ToBeVisibleAsync(); } [Fact] @@ -71,11 +71,11 @@ public class AdminNavigationTests : IAsyncLifetime await dashboard.NavigateToGamesAsync(); Assert.Contains("/Games", _page.Url); - Assert.True(await _page.GetByText("Games").First.IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Add Game" }).IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Import" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Games").First).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Add Game" })).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Import" })).ToBeVisibleAsync(); // Empty table should show "No data" - Assert.True(await _page.GetByText("No data").IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("No data")).ToBeVisibleAsync(); } [Fact] @@ -85,7 +85,7 @@ public class AdminNavigationTests : IAsyncLifetime await dashboard.NavigateToRedistributablesAsync(); Assert.Contains("/Redistributables", _page.Url); - Assert.True(await _page.GetByText("Redistributables").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Redistributables").First).ToBeVisibleAsync(); } [Fact] @@ -95,7 +95,7 @@ public class AdminNavigationTests : IAsyncLifetime await dashboard.NavigateToToolsAsync(); Assert.Contains("/Tools", _page.Url); - Assert.True(await _page.GetByText("Tools").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Tools").First).ToBeVisibleAsync(); } [Fact] @@ -105,7 +105,7 @@ public class AdminNavigationTests : IAsyncLifetime await dashboard.NavigateToServersAsync(); Assert.Contains("/Servers", _page.Url); - Assert.True(await _page.GetByText("Servers").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Servers").First).ToBeVisibleAsync(); } [Fact] @@ -134,7 +134,7 @@ public class AdminNavigationTests : IAsyncLifetime Assert.Contains("/Settings/General", _page.Url); // Verify settings-specific content is visible (Database Provider is unique to General settings) - Assert.True(await _page.GetByText("Database Provider").IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Database Provider")).ToBeVisibleAsync(); } [Fact] @@ -151,9 +151,7 @@ public class AdminNavigationTests : IAsyncLifetime foreach (var setting in expectedSettings) { - Assert.True( - await _page.GetByRole(AriaRole.Link, new() { Name = setting, Exact = true }).IsVisibleAsync(), - $"Settings menu should contain '{setting}'"); + await Assertions.Expect(_page.GetByRole(AriaRole.Link, new() { Name = setting, Exact = true })).ToBeVisibleAsync(); } } } diff --git a/LANCommander.Server.UI.Tests/Tests/FirstTimeSetupTests.cs b/LANCommander.Server.UI.Tests/Tests/FirstTimeSetupTests.cs index ee50dd30..adcec7bd 100644 --- a/LANCommander.Server.UI.Tests/Tests/FirstTimeSetupTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/FirstTimeSetupTests.cs @@ -30,7 +30,7 @@ public class FirstTimeSetupTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -54,11 +54,11 @@ public class FirstTimeSetupTests : IAsyncLifetime await setupPage.NavigateAsync(); // Verify all 4 steps are visible in the wizard - Assert.True(await _page.GetByText("Database").First.IsVisibleAsync()); - Assert.True(await _page.GetByText("Paths").IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Database").First).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByText("Paths")).ToBeVisibleAsync(); // "Metadata" may be truncated in UI to "Metad" but the text node still exists - Assert.True(await _page.Locator("text=/Metad/").First.IsVisibleAsync()); - Assert.True(await _page.GetByText("Administrator").IsVisibleAsync()); + await Assertions.Expect(_page.Locator("text=/Metad/").First).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByText("Administrator")).ToBeVisibleAsync(); } [Fact] @@ -74,9 +74,9 @@ public class FirstTimeSetupTests : IAsyncLifetime await _page.WaitForSelectorAsync("[role='listbox']", new() { Timeout = 5000 }); // Verify all expected providers are shown - Assert.True(await _page.GetByRole(AriaRole.Option, new() { Name = "SQLite" }).IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Option, new() { Name = "MySQL" }).IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Option, new() { Name = "PostgreSQL" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByRole(AriaRole.Option, new() { Name = "SQLite" })).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Option, new() { Name = "MySQL" })).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Option, new() { Name = "PostgreSQL" })).ToBeVisibleAsync(); } [Fact(Skip = "Requires real database and file I/O - not supported with in-memory WebApplicationFactory")] diff --git a/LANCommander.Server.UI.Tests/Tests/GameEditTests.cs b/LANCommander.Server.UI.Tests/Tests/GameEditTests.cs index 292ed2b3..9527c5aa 100644 --- a/LANCommander.Server.UI.Tests/Tests/GameEditTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/GameEditTests.cs @@ -29,7 +29,7 @@ public class GameEditTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } diff --git a/LANCommander.Server.UI.Tests/Tests/GameImportTests.cs b/LANCommander.Server.UI.Tests/Tests/GameImportTests.cs index c15922a5..41459e4f 100644 --- a/LANCommander.Server.UI.Tests/Tests/GameImportTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/GameImportTests.cs @@ -32,7 +32,7 @@ public class GameImportTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -44,8 +44,8 @@ public class GameImportTests : IAsyncLifetime await gamesPage.NavigateAsync(); // Verify the page structure is correct (table area and buttons are present) - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Add Game" }).IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Import" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Add Game" })).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Import" })).ToBeVisibleAsync(); // The table should render (with "No data" if empty, or rows if a prior test imported) var count = await gamesPage.GetGameCountAsync(); @@ -59,7 +59,7 @@ public class GameImportTests : IAsyncLifetime await gamesPage.NavigateAsync(); var importButton = _page.GetByRole(AriaRole.Button, new() { Name = "Import" }); - Assert.True(await importButton.IsVisibleAsync()); + await Assertions.Expect(importButton).ToBeVisibleAsync(); } [Fact] diff --git a/LANCommander.Server.UI.Tests/Tests/LoginTests.cs b/LANCommander.Server.UI.Tests/Tests/LoginTests.cs index 58ea0b3a..f6fc544a 100644 --- a/LANCommander.Server.UI.Tests/Tests/LoginTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/LoginTests.cs @@ -7,7 +7,7 @@ namespace LANCommander.Server.UI.Tests.Tests; /// /// Tests for the login flow against a server that has already been configured. /// These tests assume the server is running with a known admin user. -/// Uses IClassFixture to start the server once for all tests in this class. +/// Uses the shared "Server" collection fixture so the server starts once for the whole collection. /// [Collection("Server")] public class LoginTests : IAsyncLifetime @@ -30,7 +30,7 @@ public class LoginTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -54,7 +54,7 @@ public class LoginTests : IAsyncLifetime Assert.True(await loginPage.IsDisplayedAsync()); Assert.True(await loginPage.HasRegisterLinkAsync()); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Login" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Login" })).ToBeVisibleAsync(); } [Fact] diff --git a/LANCommander.Server.UI.Tests/Tests/MetadataTests.cs b/LANCommander.Server.UI.Tests/Tests/MetadataTests.cs index 1fef30b1..66dc6c7b 100644 --- a/LANCommander.Server.UI.Tests/Tests/MetadataTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/MetadataTests.cs @@ -31,7 +31,7 @@ public class MetadataTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -54,7 +54,7 @@ public class MetadataTests : IAsyncLifetime await tagsPage.NavigateAsync(); Assert.Equal(0, await tagsPage.GetItemCountAsync()); - Assert.True(await _page.GetByText("No data").IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("No data")).ToBeVisibleAsync(); } [Fact] @@ -65,7 +65,7 @@ public class MetadataTests : IAsyncLifetime await tagsPage.AddItemAsync("Action"); - Assert.True(await tagsPage.IsItemVisibleAsync("Action")); + await Assertions.Expect(tagsPage.Item("Action")).ToBeVisibleAsync(); } [Fact] @@ -75,10 +75,10 @@ public class MetadataTests : IAsyncLifetime await tagsPage.NavigateAsync(); await tagsPage.AddItemAsync("Puzzle"); - Assert.True(await tagsPage.IsItemVisibleAsync("Puzzle")); + await Assertions.Expect(tagsPage.Item("Puzzle")).ToBeVisibleAsync(); await tagsPage.EditItemAsync("Puzzle", "Puzzle Games"); - Assert.True(await tagsPage.IsItemVisibleAsync("Puzzle Games")); + await Assertions.Expect(tagsPage.Item("Puzzle Games")).ToBeVisibleAsync(); } [Fact] @@ -88,10 +88,10 @@ public class MetadataTests : IAsyncLifetime await tagsPage.NavigateAsync(); await tagsPage.AddItemAsync("Temporary"); - Assert.True(await tagsPage.IsItemVisibleAsync("Temporary")); + await Assertions.Expect(tagsPage.Item("Temporary")).ToBeVisibleAsync(); await tagsPage.DeleteItemAsync("Temporary"); - Assert.False(await tagsPage.IsItemVisibleAsync("Temporary")); + await Assertions.Expect(tagsPage.Item("Temporary")).ToBeHiddenAsync(); } // --- Genres --- @@ -104,7 +104,7 @@ public class MetadataTests : IAsyncLifetime await genresPage.AddItemAsync("RPG"); - Assert.True(await genresPage.IsItemVisibleAsync("RPG")); + await Assertions.Expect(genresPage.Item("RPG")).ToBeVisibleAsync(); } [Fact] @@ -114,10 +114,10 @@ public class MetadataTests : IAsyncLifetime await genresPage.NavigateAsync(); await genresPage.AddItemAsync("Strategy"); - Assert.True(await genresPage.IsItemVisibleAsync("Strategy")); + await Assertions.Expect(genresPage.Item("Strategy")).ToBeVisibleAsync(); await genresPage.DeleteItemAsync("Strategy"); - Assert.False(await genresPage.IsItemVisibleAsync("Strategy")); + await Assertions.Expect(genresPage.Item("Strategy")).ToBeHiddenAsync(); } // --- Platforms --- @@ -130,7 +130,7 @@ public class MetadataTests : IAsyncLifetime await platformsPage.AddItemAsync("Windows"); - Assert.True(await platformsPage.IsItemVisibleAsync("Windows")); + await Assertions.Expect(platformsPage.Item("Windows")).ToBeVisibleAsync(); } [Fact] @@ -140,9 +140,9 @@ public class MetadataTests : IAsyncLifetime await platformsPage.NavigateAsync(); await platformsPage.AddItemAsync("Linux"); - Assert.True(await platformsPage.IsItemVisibleAsync("Linux")); + await Assertions.Expect(platformsPage.Item("Linux")).ToBeVisibleAsync(); await platformsPage.DeleteItemAsync("Linux"); - Assert.False(await platformsPage.IsItemVisibleAsync("Linux")); + await Assertions.Expect(platformsPage.Item("Linux")).ToBeHiddenAsync(); } } diff --git a/LANCommander.Server.UI.Tests/Tests/ProfileTests.cs b/LANCommander.Server.UI.Tests/Tests/ProfileTests.cs index 5ffd34b5..ec16f13e 100644 --- a/LANCommander.Server.UI.Tests/Tests/ProfileTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/ProfileTests.cs @@ -6,7 +6,7 @@ namespace LANCommander.Server.UI.Tests.Tests; /// /// Tests for the user profile and change password pages. -/// Uses IClassFixture to share the server instance across all tests in this class. +/// Uses the shared "Server" collection fixture so the server instance is shared across the collection. /// [Collection("Server")] public class ProfileTests : IAsyncLifetime @@ -29,7 +29,7 @@ public class ProfileTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -51,12 +51,10 @@ public class ProfileTests : IAsyncLifetime var profilePage = new ProfilePage(_page); await profilePage.NavigateAsync(); - Assert.True(await profilePage.HasFieldAsync("Username"), "Username field should be visible"); - Assert.True(await profilePage.HasFieldAsync("Alias"), "Alias field should be visible"); - Assert.True(await profilePage.HasFieldAsync("Email Address"), "Email Address field should be visible"); - Assert.True( - await _page.GetByRole(AriaRole.Button, new() { Name = "Save" }).IsVisibleAsync(), - "Save button should be visible"); + await Assertions.Expect(profilePage.Field("Username")).ToBeVisibleAsync(); + await Assertions.Expect(profilePage.Field("Alias")).ToBeVisibleAsync(); + await Assertions.Expect(profilePage.Field("Email Address")).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Save" })).ToBeVisibleAsync(); } [Fact] @@ -88,12 +86,10 @@ public class ProfileTests : IAsyncLifetime var profilePage = new ProfilePage(_page); await profilePage.NavigateToChangePasswordAsync(); - Assert.True(await profilePage.HasPasswordFieldAsync("Current Password"), "Current Password field should be visible"); - Assert.True(await profilePage.HasPasswordFieldAsync("New Password"), "New Password field should be visible"); - Assert.True(await profilePage.HasPasswordFieldAsync("Confirm Password"), "Confirm Password field should be visible"); - Assert.True( - await _page.GetByRole(AriaRole.Button, new() { Name = "Change" }).IsVisibleAsync(), - "Change button should be visible"); + await Assertions.Expect(profilePage.Field("Current Password")).ToBeVisibleAsync(); + await Assertions.Expect(profilePage.Field("New Password")).ToBeVisibleAsync(); + await Assertions.Expect(profilePage.Field("Confirm Password")).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Change" })).ToBeVisibleAsync(); } [Fact] diff --git a/LANCommander.Server.UI.Tests/Tests/RoleManagementTests.cs b/LANCommander.Server.UI.Tests/Tests/RoleManagementTests.cs index 78ed57d0..9bc2f978 100644 --- a/LANCommander.Server.UI.Tests/Tests/RoleManagementTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/RoleManagementTests.cs @@ -30,7 +30,7 @@ public class RoleManagementTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -41,8 +41,7 @@ public class RoleManagementTests : IAsyncLifetime var rolesPage = new RolesPage(_page); await rolesPage.NavigateAsync(); - Assert.True(await rolesPage.IsRoleVisibleAsync("Administrator"), - "Administrator role should be visible in the roles table"); + await Assertions.Expect(rolesPage.Role("Administrator")).ToBeVisibleAsync(); } [Fact] @@ -53,8 +52,7 @@ public class RoleManagementTests : IAsyncLifetime await rolesPage.AddRoleAsync("TestRole"); - Assert.True(await rolesPage.IsRoleVisibleAsync("TestRole"), - "Newly added TestRole should appear in the roles table"); + await Assertions.Expect(rolesPage.Role("TestRole")).ToBeVisibleAsync(); } [Fact] @@ -65,14 +63,12 @@ public class RoleManagementTests : IAsyncLifetime // Add a role to delete await rolesPage.AddRoleAsync("RoleToDelete"); - Assert.True(await rolesPage.IsRoleVisibleAsync("RoleToDelete"), - "RoleToDelete should be visible before deletion"); + await Assertions.Expect(rolesPage.Role("RoleToDelete")).ToBeVisibleAsync(); // Delete the role await rolesPage.DeleteRoleAsync("RoleToDelete"); - Assert.False(await rolesPage.IsRoleVisibleAsync("RoleToDelete"), - "RoleToDelete should no longer appear after deletion"); + await Assertions.Expect(rolesPage.Role("RoleToDelete")).ToBeHiddenAsync(); } [Fact] diff --git a/LANCommander.Server.UI.Tests/Tests/SettingsTests.cs b/LANCommander.Server.UI.Tests/Tests/SettingsTests.cs index 636ce9ca..2e8b2871 100644 --- a/LANCommander.Server.UI.Tests/Tests/SettingsTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/SettingsTests.cs @@ -29,7 +29,7 @@ public class SettingsTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -41,9 +41,9 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToGeneralAsync(); Assert.Contains("/Settings/General", _page.Url); - Assert.True(await _page.GetByText("Database Provider").IsVisibleAsync()); - Assert.True(await _page.GetByText("Port").First.IsVisibleAsync()); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Save" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Database Provider")).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByText("Port").First).ToBeVisibleAsync(); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Save" })).ToBeVisibleAsync(); } [Fact] @@ -58,7 +58,7 @@ public class SettingsTests : IAsyncLifetime // The admin user created during fixture setup should appear in the table var adminCell = _page.Locator("table").GetByText(TestConstants.AdminUserName).First; await adminCell.WaitForAsync(new() { Timeout = 15000 }); - Assert.True(await adminCell.IsVisibleAsync()); + await Assertions.Expect(adminCell).ToBeVisibleAsync(); } [Fact] @@ -68,12 +68,12 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToRolesAsync(); Assert.Contains("/Settings/Roles", _page.Url); - Assert.True(await _page.GetByRole(AriaRole.Button, new() { Name = "Add Role" }).IsVisibleAsync()); + await Assertions.Expect(_page.GetByRole(AriaRole.Button, new() { Name = "Add Role" })).ToBeVisibleAsync(); // Wait for the data table to render then check for the Administrator role await _page.Locator("table").First.WaitForAsync(new() { Timeout = 15000 }); var adminRole = _page.Locator("table").GetByText("Administrator"); await adminRole.WaitForAsync(new() { Timeout = 15000 }); - Assert.True(await adminRole.IsVisibleAsync()); + await Assertions.Expect(adminRole).ToBeVisibleAsync(); } [Fact] @@ -83,7 +83,7 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToAuthenticationAsync(); Assert.Contains("/Settings/Authentication", _page.Url); - Assert.True(await _page.GetByText("Authentication").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Authentication").First).ToBeVisibleAsync(); } [Fact] @@ -93,7 +93,7 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToArchivesAsync(); Assert.Contains("/Settings/Archives", _page.Url); - Assert.True(await _page.GetByText("Archives").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Archives").First).ToBeVisibleAsync(); } [Fact] @@ -103,7 +103,7 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToMediaAsync(); Assert.Contains("/Settings/Media", _page.Url); - Assert.True(await _page.GetByText("Media").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Media").First).ToBeVisibleAsync(); } [Fact] @@ -114,7 +114,7 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToBeaconAsync(); Assert.Contains("/Settings/Beacon", _page.Url); - Assert.True(await _page.GetByText("Beacon").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Beacon").First).ToBeVisibleAsync(); } [Fact] @@ -124,7 +124,7 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToUpdatesAsync(); Assert.Contains("/Settings/Updates", _page.Url); - Assert.True(await _page.GetByText("Updates").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Updates").First).ToBeVisibleAsync(); } [Fact] @@ -134,6 +134,6 @@ public class SettingsTests : IAsyncLifetime await settings.NavigateToAppearanceAsync(); Assert.Contains("/Settings/Appearance", _page.Url); - Assert.True(await _page.GetByText("Appearance").First.IsVisibleAsync()); + await Assertions.Expect(_page.GetByText("Appearance").First).ToBeVisibleAsync(); } } diff --git a/LANCommander.Server.UI.Tests/Tests/UserManagementTests.cs b/LANCommander.Server.UI.Tests/Tests/UserManagementTests.cs index 71c31ab6..a6837ba1 100644 --- a/LANCommander.Server.UI.Tests/Tests/UserManagementTests.cs +++ b/LANCommander.Server.UI.Tests/Tests/UserManagementTests.cs @@ -28,7 +28,7 @@ public class UserManagementTests : IAsyncLifetime public async Task DisposeAsync() { - await ScreenshotHelper.CaptureIfFailedAsync(_page, _output); + await ScreenshotHelper.CaptureAsync(_page, _output); if (_page != null) await _page.CloseAsync(); if (_context != null) await _context.DisposeAsync(); } @@ -39,7 +39,7 @@ public class UserManagementTests : IAsyncLifetime var usersPage = new UsersPage(_page); await usersPage.NavigateAsync(); - Assert.True(await usersPage.IsUserVisibleAsync(TestConstants.AdminUserName)); + await Assertions.Expect(usersPage.User(TestConstants.AdminUserName)).ToBeVisibleAsync(); } [Fact] @@ -50,7 +50,7 @@ public class UserManagementTests : IAsyncLifetime await usersPage.SearchUsersAsync(TestConstants.AdminUserName); - Assert.True(await usersPage.IsUserVisibleAsync(TestConstants.AdminUserName)); + await Assertions.Expect(usersPage.User(TestConstants.AdminUserName)).ToBeVisibleAsync(); // After searching for "admin", the admin user must be in the results var count = await usersPage.GetUserCountAsync(); Assert.True(count >= 1, $"Expected at least 1 user matching 'admin', got {count}"); @@ -96,7 +96,7 @@ public class UserManagementTests : IAsyncLifetime var usersPage = new UsersPage(_page); await usersPage.NavigateAsync(); - Assert.True(await usersPage.IsUserVisibleAsync(testUserName)); + await Assertions.Expect(usersPage.User(testUserName)).ToBeVisibleAsync(); } [Fact] @@ -129,12 +129,12 @@ public class UserManagementTests : IAsyncLifetime await usersPage.NavigateAsync(); // Verify user exists before deletion - Assert.True(await usersPage.IsUserVisibleAsync(testUserName)); + await Assertions.Expect(usersPage.User(testUserName)).ToBeVisibleAsync(); // Delete the user await usersPage.DeleteUserAsync(testUserName); // Verify user is gone - Assert.False(await usersPage.IsUserVisibleAsync(testUserName)); + await Assertions.Expect(usersPage.User(testUserName)).ToBeHiddenAsync(); } }