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<bool>, 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 <noreply@anthropic.com>
This commit is contained in:
Pat Hartl 2026-06-23 17:51:34 -05:00
parent 77b0f04e4d
commit 9b5195f1d6
18 changed files with 109 additions and 122 deletions

View file

@ -11,8 +11,8 @@ namespace LANCommander.Server.UI.Tests;
/// <summary>
/// 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&lt;ConfiguredServerFixture&gt;.
/// the admin user, and makes it available for all tests in the collection.
/// Shared across the "Server" collection via ICollectionFixture&lt;ConfiguredServerFixture&gt;.
/// </summary>
public class ConfiguredServerFixture : IAsyncLifetime
{

View file

@ -44,11 +44,14 @@ public class MetadataPage
await _page.WaitForSelectorAsync(".ant-modal", new() { State = WaitForSelectorState.Hidden, Timeout = 5000 });
}
public async Task<bool> IsItemVisibleAsync(string name)
/// <summary>
/// Locator for an item row's name cell within the table body.
/// Use with web-first assertions, e.g. Expect(page.Item("Action")).ToBeVisibleAsync().
/// </summary>
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)

View file

@ -28,11 +28,14 @@ public class ProfilePage
return await input.InputValueAsync();
}
public async Task<bool> HasFieldAsync(string label)
/// <summary>
/// Locator for a form field (text or password) with the given label.
/// Use with web-first assertions, e.g. Expect(page.Field("Username")).ToBeVisibleAsync().
/// </summary>
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<bool> HasPasswordFieldAsync(string label)
{
return await _page.Locator(".ant-form-item")
.Filter(new() { HasText = label })
.IsVisibleAsync();
}
/// <summary>
/// Fills the change password form and submits it.
/// </summary>

View file

@ -47,12 +47,12 @@ public class RolesPage
}
/// <summary>
/// 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().
/// </summary>
public async Task<bool> 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 });
}
/// <summary>

View file

@ -49,11 +49,12 @@ public class UsersPage
}
/// <summary>
/// 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().
/// </summary>
public async Task<bool> 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 });
}
/// <summary>

View file

@ -26,12 +26,12 @@ public class PlaywrightFixture : IAsyncLifetime
Playwright.Dispose();
}
public async Task<IBrowserContext> NewContextAsync(string? baseUrl = null)
public async Task<IBrowserContext> NewContextAsync(string baseUrl)
{
return await Browser.NewContextAsync(new BrowserNewContextOptions
{
IgnoreHTTPSErrors = true,
BaseURL = baseUrl ?? TestConstants.BaseUrl,
BaseURL = baseUrl,
});
}
}

View file

@ -5,8 +5,10 @@ using Xunit.Abstractions;
namespace LANCommander.Server.UI.Tests;
/// <summary>
/// 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.)
/// </summary>
public static class ScreenshotHelper
{
@ -16,17 +18,14 @@ public static class ScreenshotHelper
string.Empty);
/// <summary>
/// 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.
/// </summary>
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

View file

@ -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}";
}

View file

@ -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.
/// </summary>
[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();
}
}
}

View file

@ -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")]

View file

@ -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();
}

View file

@ -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]

View file

@ -7,7 +7,7 @@ namespace LANCommander.Server.UI.Tests.Tests;
/// <summary>
/// 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.
/// </summary>
[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]

View file

@ -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();
}
}

View file

@ -6,7 +6,7 @@ namespace LANCommander.Server.UI.Tests.Tests;
/// <summary>
/// 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.
/// </summary>
[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]

View file

@ -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]

View file

@ -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();
}
}

View file

@ -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();
}
}