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>
140 lines
5.1 KiB
C#
140 lines
5.1 KiB
C#
using LANCommander.Server.UI.Tests.Pages;
|
|
using Microsoft.Playwright;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace LANCommander.Server.UI.Tests.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for the Settings > Users page and user registration flow.
|
|
/// </summary>
|
|
[Collection("Server")]
|
|
public class UserManagementTests : IAsyncLifetime
|
|
{
|
|
private readonly ConfiguredServerFixture _fixture;
|
|
private readonly ITestOutputHelper _output;
|
|
private IBrowserContext _context = null!;
|
|
private IPage _page = null!;
|
|
|
|
public UserManagementTests(ConfiguredServerFixture fixture, ITestOutputHelper output)
|
|
{
|
|
_fixture = fixture;
|
|
_output = output;
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
(_context, _page) = await _fixture.CreateLoggedInPageAsync();
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await ScreenshotHelper.CaptureAsync(_page, _output);
|
|
if (_page != null) await _page.CloseAsync();
|
|
if (_context != null) await _context.DisposeAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UsersPage_ShowsAdminUser()
|
|
{
|
|
var usersPage = new UsersPage(_page);
|
|
await usersPage.NavigateAsync();
|
|
|
|
await Assertions.Expect(usersPage.User(TestConstants.AdminUserName)).ToBeVisibleAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UsersPage_CanSearchUsers()
|
|
{
|
|
var usersPage = new UsersPage(_page);
|
|
await usersPage.NavigateAsync();
|
|
|
|
await usersPage.SearchUsersAsync(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}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UsersPage_ShowsUserRoles()
|
|
{
|
|
var usersPage = new UsersPage(_page);
|
|
await usersPage.NavigateAsync();
|
|
|
|
var roles = await usersPage.GetUserRolesTextAsync(TestConstants.AdminUserName);
|
|
Assert.Contains("Administrator", roles);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Register_NewUser_AppearsInUserList()
|
|
{
|
|
var testUserName = $"testuser_{Guid.NewGuid().ToString()[..8]}";
|
|
|
|
// Register a new user via the public Register page using a separate anonymous context
|
|
var (anonContext, anonPage) = await _fixture.CreateAnonymousPageAsync();
|
|
try
|
|
{
|
|
await anonPage.GotoAsync("/Register");
|
|
await anonPage.WaitForSelectorAsync("#login-submit", new() { Timeout = 10000 });
|
|
|
|
await anonPage.Locator("input[name='Model.UserName']").FillAsync(testUserName);
|
|
await anonPage.Locator("input[name='Model.Password']").FillAsync(TestConstants.AdminPassword);
|
|
await anonPage.Locator("input[name='Model.PasswordConfirmation']").FillAsync(TestConstants.AdminPassword);
|
|
await anonPage.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync();
|
|
|
|
// Wait for registration to complete (redirects to home)
|
|
await anonPage.WaitForURLAsync("**/", new() { Timeout = 10000 });
|
|
}
|
|
finally
|
|
{
|
|
await anonPage.CloseAsync();
|
|
await anonContext.DisposeAsync();
|
|
}
|
|
|
|
// Navigate to users page as admin and verify the new user appears
|
|
var usersPage = new UsersPage(_page);
|
|
await usersPage.NavigateAsync();
|
|
|
|
await Assertions.Expect(usersPage.User(testUserName)).ToBeVisibleAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UsersPage_CanDeleteUser()
|
|
{
|
|
var testUserName = $"deluser_{Guid.NewGuid().ToString()[..8]}";
|
|
|
|
// Create a user via the Register page
|
|
var (anonContext, anonPage) = await _fixture.CreateAnonymousPageAsync();
|
|
try
|
|
{
|
|
await anonPage.GotoAsync("/Register");
|
|
await anonPage.WaitForSelectorAsync("#login-submit", new() { Timeout = 10000 });
|
|
|
|
await anonPage.Locator("input[name='Model.UserName']").FillAsync(testUserName);
|
|
await anonPage.Locator("input[name='Model.Password']").FillAsync(TestConstants.AdminPassword);
|
|
await anonPage.Locator("input[name='Model.PasswordConfirmation']").FillAsync(TestConstants.AdminPassword);
|
|
await anonPage.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync();
|
|
|
|
await anonPage.WaitForURLAsync("**/", new() { Timeout = 10000 });
|
|
}
|
|
finally
|
|
{
|
|
await anonPage.CloseAsync();
|
|
await anonContext.DisposeAsync();
|
|
}
|
|
|
|
// Navigate to users page as admin
|
|
var usersPage = new UsersPage(_page);
|
|
await usersPage.NavigateAsync();
|
|
|
|
// Verify user exists before deletion
|
|
await Assertions.Expect(usersPage.User(testUserName)).ToBeVisibleAsync();
|
|
|
|
// Delete the user
|
|
await usersPage.DeleteUserAsync(testUserName);
|
|
|
|
// Verify user is gone
|
|
await Assertions.Expect(usersPage.User(testUserName)).ToBeHiddenAsync();
|
|
}
|
|
}
|