LANCommander/LANCommander.Server.UI.Tests/Pages/UsersPage.cs
Pat Hartl 9b5195f1d6 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>
2026-06-23 17:51:34 -05:00

100 lines
3.9 KiB
C#

using Microsoft.Playwright;
namespace LANCommander.Server.UI.Tests.Pages;
/// <summary>
/// Page object for the Settings > Users page at /Settings/Users.
/// </summary>
public class UsersPage
{
private readonly IPage _page;
public UsersPage(IPage page)
{
_page = page;
}
/// <summary>
/// Navigates to the Users page via the Settings sidebar menu and waits for the table to load.
/// </summary>
public async Task NavigateAsync()
{
await _page.GetByRole(AriaRole.Button, new() { Name = "Settings" }).ClickAsync();
await _page.GetByRole(AriaRole.Link, new() { Name = "Users", Exact = true }).ClickAsync();
await _page.WaitForURLAsync("**/Settings/Users", new() { Timeout = 10000 });
// Wait for table data rows to render (Blazor SSR + async data load)
await WaitForTableDataAsync();
}
/// <summary>
/// Returns the number of data rows in the users table.
/// </summary>
public async Task<int> GetUserCountAsync()
{
return await _page.Locator("tr.ant-table-row").CountAsync();
}
/// <summary>
/// Searches for users by typing into the DataTable search input and waiting for results.
/// </summary>
public async Task SearchUsersAsync(string query)
{
var searchInput = _page.GetByPlaceholder("Search");
await searchInput.FillAsync(query);
// Press Enter to ensure the search triggers via Blazor's event pipeline
await searchInput.PressAsync("Enter");
// Wait for the server-side search to complete and table to re-render
await _page.WaitForLoadStateAsync(LoadState.NetworkIdle, new() { Timeout = 10000 });
await _page.WaitForTimeoutAsync(1000);
}
/// <summary>
/// Locator for a table row containing the given username.
/// Use with web-first assertions, e.g. Expect(page.User("admin")).ToBeVisibleAsync().
/// </summary>
public ILocator User(string username)
{
return _page.Locator("tr.ant-table-row", new() { HasTextString = username });
}
/// <summary>
/// Gets the text content of the Roles column for a given user.
/// </summary>
public async Task<string> GetUserRolesTextAsync(string username)
{
var row = _page.Locator("tr.ant-table-row", new() { HasTextString = username });
// Roles are rendered as <span class="ant-tag"> inside the row
var tags = row.Locator(".ant-tag");
var count = await tags.CountAsync();
var roles = new List<string>();
for (int i = 0; i < count; i++)
{
var text = await tags.Nth(i).TextContentAsync();
if (!string.IsNullOrWhiteSpace(text))
roles.Add(text.Trim());
}
return string.Join(", ", roles);
}
/// <summary>
/// Deletes a user by clicking the delete button on their row and confirming the popconfirm.
/// </summary>
public async Task DeleteUserAsync(string username)
{
var row = _page.Locator("tr.ant-table-row", new() { HasTextString = username });
// Click the danger button (delete icon) in the row - AntDesign renders it as ant-btn-dangerous
await row.Locator("button.ant-btn-dangerous").ClickAsync();
// Wait for the Popconfirm overlay to appear and click OK
var okButton = _page.GetByRole(AriaRole.Button, new() { Name = "OK" });
await okButton.WaitForAsync(new() { Timeout = 5000 });
await okButton.ClickAsync();
// Wait for the deletion round-trip and table re-render
await _page.WaitForLoadStateAsync(LoadState.NetworkIdle, new() { Timeout = 10000 });
await _page.WaitForTimeoutAsync(1000);
}
private async Task WaitForTableDataAsync()
{
await _page.Locator("tr.ant-table-row").First.WaitForAsync(new() { Timeout = 15000 });
}
}