Loading...
Loading...
Tests UI across frameworks. Page objects, test selectors, async waits, accessibility.
npx skill4agent add novotnyllc/dotnet-artisan dotnet-ui-testing-corePageObjects/
LoginPage.cs -- login form interactions
DashboardPage.cs -- dashboard navigation + widgets
OrderListPage.cs -- order list filtering + selection
Components/
NavigationMenu.cs -- shared nav component
ConfirmDialog.cs -- reusable confirmation modal/// <summary>
/// Base class for page objects. Subclass per framework:
/// Playwright uses IPage, bUnit uses IRenderedComponent, Appium uses AppiumDriver.
/// </summary>
public abstract class PageObjectBase<TDriver>
{
protected TDriver Driver { get; }
protected PageObjectBase(TDriver driver)
{
Driver = driver;
}
/// <summary>
/// Verifies the page/component is in the expected state after navigation.
/// Call this in the constructor or after navigation to fail fast on wrong pages.
/// </summary>
protected abstract void VerifyLoaded();
}public class LoginPage : PageObjectBase<IPage>
{
public LoginPage(IPage page) : base(page)
{
VerifyLoaded();
}
protected override void VerifyLoaded()
{
// Fail fast if not on the login page
Driver.WaitForSelectorAsync("[data-testid='login-form']")
.GetAwaiter().GetResult();
}
public async Task<DashboardPage> LoginAsync(string email, string password)
{
await Driver.FillAsync("[data-testid='email-input']", email);
await Driver.FillAsync("[data-testid='password-input']", password);
await Driver.ClickAsync("[data-testid='login-button']");
await Driver.WaitForURLAsync("**/dashboard");
return new DashboardPage(Driver);
}
public async Task<string> GetErrorMessageAsync()
{
var error = Driver.Locator("[data-testid='login-error']");
return await error.TextContentAsync() ?? "";
}
}
// Usage in test
[Fact]
public async Task Login_ValidCredentials_RedirectsToDashboard()
{
var loginPage = new LoginPage(Page);
var dashboard = await loginPage.LoginAsync("user@example.com", "P@ssw0rd!");
Assert.NotNull(dashboard);
}LoginAsyncDashboardPageLoginAsync()ClickAsync("#submit")GetErrorMessageAsync()NavigationMenu| Priority | Selector Type | Example | Reliability |
|---|---|---|---|
| 1 | | | Highest -- survives CSS/layout changes |
| 2 | Accessibility role + name | | High -- tied to visible behavior |
| 3 | Label text | | High -- changes when copy changes |
| 4 | Placeholder text | | Medium -- often localized |
| 5 | CSS class | | Low -- changes with styling |
| 6 | XPath / DOM structure | | Lowest -- breaks on any layout change |
data-testid<button data-testid="submit-order" @onclick="SubmitOrder">Place Order</button>
<input data-testid="search-input" @bind="SearchTerm" /><Button AutomationId="submit-order" Text="Place Order" Clicked="OnSubmit" />
<Entry AutomationId="search-input" Text="{Binding SearchTerm}" /><Button AutomationProperties.AutomationId="submit-order" Content="Place Order" />// BAD: Tied to CSS implementation
await page.ClickAsync(".MuiButton-root.MuiButton-containedPrimary");
// BAD: Tied to DOM structure
await page.ClickAsync("div > form > div:nth-child(3) > button");
// BAD: Tied to dynamic content
await page.ClickAsync($"text=Order #{orderId}");
// GOOD: Stable test identifier
await page.ClickAsync("[data-testid='submit-order']");
// GOOD: Accessibility-driven (Playwright)
await page.GetByRole(AriaRole.Button, new() { Name = "Place Order" }).ClickAsync();Is the element already in the DOM?
|
+-- YES --> Is it visible and actionable?
| |
| +-- YES --> Interact immediately
| +-- NO --> Wait for visibility/enabled state
|
+-- NO --> Wait for element to appear in DOM
|
Is it loaded via network request?
|
+-- YES --> Wait for network idle or specific API response
+-- NO --> Wait for render cycle to complete// Auto-waiting: Playwright waits for actionability by default
await page.ClickAsync("[data-testid='submit']"); // waits until visible + enabled
// Explicit wait for network-loaded content
await page.WaitForResponseAsync(
response => response.Url.Contains("/api/orders") && response.Status == 200);
// Wait for element state
await page.Locator("[data-testid='results']")
.WaitForAsync(new() { State = WaitForSelectorState.Visible });
// Wait for specific text content
await Expect(page.Locator("[data-testid='status']")).ToHaveTextAsync("Completed");// Wait for async state changes to render
var cut = RenderComponent<OrderList>();
// Wait for component to finish async operations
cut.WaitForState(() => cut.Instance.Orders.Count > 0,
timeout: TimeSpan.FromSeconds(5));
// Wait for specific markup
cut.WaitForAssertion(() =>
Assert.NotEmpty(cut.FindAll("[data-testid='order-row']")),
timeout: TimeSpan.FromSeconds(5));// BAD: Hardcoded delay -- slow and still flaky
await Task.Delay(3000);
await page.ClickAsync("[data-testid='results']");
// BAD: Polling with Thread.Sleep
while (!element.IsVisible)
{
Thread.Sleep(100); // blocks thread, no timeout safety
}
// GOOD: Framework-native wait
await page.Locator("[data-testid='results']")
.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = 5000 });
// GOOD: Assertion with retry (Playwright)
await Expect(page.Locator("[data-testid='count']")).ToHaveTextAsync("5");// NuGet: Deque.AxeCore.Playwright
[Fact]
public async Task HomePage_PassesAccessibilityAudit()
{
await Page.GotoAsync("/");
var results = await Page.RunAxe();
Assert.Empty(results.Violations);
}
[Fact]
public async Task OrderForm_NoAccessibilityViolations()
{
await Page.GotoAsync("/orders/new");
// Scope to specific component
var form = Page.Locator("[data-testid='order-form']");
var results = await Page.RunAxe(new AxeRunOptions
{
// Focus on WCAG 2.1 AA rules
RunOnly = new RunOnlyOptions
{
Type = "tag",
Values = ["wcag2a", "wcag2aa", "wcag21aa"]
}
});
// Report violations with details for debugging
foreach (var violation in results.Violations)
{
// Log: violation.Id, violation.Description, violation.Nodes
}
Assert.Empty(results.Violations);
}| Check | How to Test | Tool |
|---|---|---|
| Color contrast | Automated axe-core rule | Deque.AxeCore.Playwright |
| Keyboard navigation | Tab through all interactive elements | Playwright |
| ARIA labels | Verify | Playwright locators + assertions |
| Focus management | Verify focus moves to dialogs/modals | Playwright |
| Screen reader text | Verify | Manual + assertion on ARIA attributes |
[Fact]
public async Task OrderForm_TabOrder_FollowsLogicalSequence()
{
await Page.GotoAsync("/orders/new");
// Tab through form fields and verify focus order
await Page.Keyboard.PressAsync("Tab");
await Expect(Page.Locator("[data-testid='customer-name']")).ToBeFocusedAsync();
await Page.Keyboard.PressAsync("Tab");
await Expect(Page.Locator("[data-testid='customer-email']")).ToBeFocusedAsync();
await Page.Keyboard.PressAsync("Tab");
await Expect(Page.Locator("[data-testid='order-items']")).ToBeFocusedAsync();
// Verify Enter submits the form
await Page.Keyboard.PressAsync("Tab"); // focus submit button
await Expect(Page.Locator("[data-testid='submit-order']")).ToBeFocusedAsync();
}data-testidThread.SleepTask.Delaydata-testidWaitForTimeoutWaitForSelectorAsyncExpect(...).ToBeVisibleAsync()WaitForResponseAsyncFindAll("[data-testid='row']").CountWaitForStateWaitForAssertion