dotnet-ui-testing-core
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-ui-testing-core
.NET UI测试核心(dotnet-ui-testing-core)
Core UI testing patterns applicable across .NET UI frameworks (Blazor, MAUI, Uno Platform). Covers the page object model for maintainable test structure, test selector strategies for reliable element identification, async wait patterns for non-deterministic UI, and accessibility testing approaches.
Version assumptions: .NET 8.0+ baseline. Framework-specific details are delegated to dedicated skills.
适用于所有.NET UI框架(Blazor、MAUI、Uno Platform)的核心UI测试模式。包含用于可维护测试结构的页面对象模型、可靠元素定位的测试选择器策略、针对非确定性UI的异步等待模式,以及可访问性测试方法。
版本要求: 以.NET 8.0+为基准。各框架的具体细节请参考对应的专属技能。
Scope
适用范围
- Page object model for maintainable test structure
- Test selector strategies for reliable element identification
- Async wait patterns for non-deterministic UI
- Accessibility testing approaches
- 用于可维护测试结构的页面对象模型
- 用于可靠元素定位的测试选择器策略
- 针对非确定性UI的异步等待模式
- 可访问性测试方法
Out of scope
不适用范围
- Blazor component testing (bUnit) -- see [skill:dotnet-blazor-testing]
- MAUI UI testing (Appium/XHarness) -- see [skill:dotnet-maui-testing]
- Uno Platform WASM testing -- see [skill:dotnet-uno-testing]
- Browser automation specifics -- see [skill:dotnet-playwright]
- Test project scaffolding -- see [skill:dotnet-add-testing]
Prerequisites: A test project scaffolded via [skill:dotnet-add-testing]. Familiarity with test strategy decisions from [skill:dotnet-testing-strategy].
Cross-references: [skill:dotnet-testing-strategy] for deciding when UI tests are appropriate, [skill:dotnet-playwright] for browser-based E2E automation, [skill:dotnet-blazor-testing] for Blazor component testing, [skill:dotnet-maui-testing] for mobile/desktop UI testing, [skill:dotnet-uno-testing] for Uno Platform testing.
- Blazor组件测试(bUnit)——请参考[skill:dotnet-blazor-testing]
- MAUI UI测试(Appium/XHarness)——请参考[skill:dotnet-maui-testing]
- Uno Platform WASM测试——请参考[skill:dotnet-uno-testing]
- 浏览器自动化细节——请参考[skill:dotnet-playwright]
- 测试项目脚手架搭建——请参考[skill:dotnet-add-testing]
前置条件: 已通过[skill:dotnet-add-testing]搭建测试项目。熟悉[skill:dotnet-testing-strategy]中的测试策略决策。
交叉参考:[skill:dotnet-testing-strategy]用于判断何时适合进行UI测试,[skill:dotnet-playwright]用于基于浏览器的端到端自动化,[skill:dotnet-blazor-testing]用于Blazor组件测试,[skill:dotnet-maui-testing]用于移动/桌面UI测试,[skill:dotnet-uno-testing]用于Uno Platform测试。
Page Object Model
页面对象模型
The page object model (POM) encapsulates page structure and interactions behind a class, isolating tests from UI implementation details. When the UI changes, only the page object needs updating -- not every test that touches that page.
页面对象模型(POM)将页面结构和交互封装在类中,使测试与UI实现细节解耦。当UI发生变化时,仅需更新页面对象,而非所有涉及该页面的测试。
Structure
结构
PageObjects/
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 modalPageObjects/
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 modalExample: Generic Page Object Base
示例:通用页面对象基类
csharp
/// <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();
}csharp
/// <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();
}Example: Playwright Page Object
示例:Playwright页面对象
csharp
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);
}csharp
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);
}Page Object Principles
页面对象原则
- Return the next page object from navigation actions. returns
LoginAsync, guiding test authors through the application flow.DashboardPage - Never expose raw selectors from page objects. Tests call , not
LoginAsync().ClickAsync("#submit") - Keep assertions in tests, not page objects. Page objects provide data (e.g., ); tests make assertions on that data.
GetErrorMessageAsync() - Compose page objects from reusable components. A component object can be embedded in every page that has a nav bar.
NavigationMenu
- 导航操作返回下一个页面对象。返回
LoginAsync,引导测试人员遵循应用流程。DashboardPage - 绝不要从页面对象中暴露原始选择器。测试调用,而非
LoginAsync()。ClickAsync("#submit") - 将断言放在测试中,而非页面对象里。页面对象提供数据(如);测试对这些数据进行断言。
GetErrorMessageAsync() - 用可复用组件组合页面对象。组件对象可以嵌入到所有包含导航栏的页面中。
NavigationMenu
Test Selector Strategies
测试选择器策略
Selectors determine how tests find UI elements. Fragile selectors are the leading cause of flaky UI tests.
选择器决定了测试如何定位UI元素。脆弱的选择器是UI测试不稳定的主要原因。
Selector Priority (Most to Least Reliable)
选择器优先级(从最可靠到最不可靠)
| 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 |
| 优先级 | 选择器类型 | 示例 | 可靠性 |
|---|---|---|---|
| 1 | | | 最高 —— 不受CSS/布局变化影响 |
| 2 | 无障碍角色+名称 | | 高 —— 与可见行为绑定 |
| 3 | 标签文本 | | 高 —— 文本变更时会变化 |
| 4 | 占位符文本 | | 中 —— 通常会本地化 |
| 5 | CSS类 | | 低 —— 随样式变化 |
| 6 | XPath / DOM结构 | | 最低 —— 任何布局变化都会使其失效 |
Adding Test IDs
添加测试ID
Add attributes to elements that tests interact with. They are invisible to users and stable across refactors:
data-testidBlazor:
razor
<button data-testid="submit-order" @onclick="SubmitOrder">Place Order</button>
<input data-testid="search-input" @bind="SearchTerm" />MAUI XAML:
xml
<Button AutomationId="submit-order" Text="Place Order" Clicked="OnSubmit" />
<Entry AutomationId="search-input" Text="{Binding SearchTerm}" />Uno Platform XAML:
xml
<Button AutomationProperties.AutomationId="submit-order" Content="Place Order" />为测试需要交互的元素添加属性。这些属性对用户不可见,且在重构过程中保持稳定:
data-testidBlazor:
razor
<button data-testid="submit-order" @onclick="SubmitOrder">Place Order</button>
<input data-testid="search-input" @bind="SearchTerm" />MAUI XAML:
xml
<Button AutomationId="submit-order" Text="Place Order" Clicked="OnSubmit" />
<Entry AutomationId="search-input" Text="{Binding SearchTerm}" />Uno Platform XAML:
xml
<Button AutomationProperties.AutomationId="submit-order" Content="Place Order" />Selector Anti-Patterns
选择器反模式
csharp
// 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();csharp
// 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();Async Wait Strategies
异步等待策略
UI tests deal with asynchronous rendering, network requests, and animations. Hardcoded delays cause flaky tests and slow suites.
UI测试需要处理异步渲染、网络请求和动画。硬编码延迟会导致测试不稳定且运行缓慢。
Wait Strategy Decision Tree
等待策略决策树
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元素是否已在DOM中?
|
+-- 是 --> 是否可见且可交互?
| |
| +-- 是 --> 立即交互
| +-- 否 --> 等待可见/启用状态
|
+-- 否 --> 等待元素出现在DOM中
|
是否通过网络请求加载?
|
+-- 是 --> 等待网络空闲或特定API响应
+-- 否 --> 等待渲染周期完成Framework-Specific Wait Patterns
框架特定的等待模式
Playwright (browser-based):
csharp
// 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");bUnit (Blazor component testing):
csharp
// 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));Playwright(基于浏览器):
csharp
// 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");bUnit(Blazor组件测试):
csharp
// 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));Wait Anti-Patterns
等待反模式
csharp
// 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");csharp
// 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");Accessibility Testing
可访问性测试
Accessibility testing verifies that UI components are usable by people with disabilities and compatible with assistive technologies. Automated checks catch common issues; manual review is still needed for subjective criteria.
可访问性测试用于验证UI组件是否能被残障人士使用,且兼容辅助技术。自动化检查可发现常见问题;但主观标准仍需人工评审。
Automated Accessibility Checks with Playwright
使用Playwright进行自动化可访问性检查
csharp
// 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);
}csharp
// 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);
}Accessibility Checklist for UI Tests
UI测试的可访问性检查清单
| 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 |
| 检查项 | 测试方法 | 工具 |
|---|---|---|
| 颜色对比度 | axe-core自动化规则 | Deque.AxeCore.Playwright |
| 键盘导航 | 按Tab键遍历所有交互元素 | Playwright |
| ARIA标签 | 验证 | Playwright定位器+断言 |
| 焦点管理 | 验证焦点是否移动到对话框/模态框 | Playwright |
| 屏幕阅读器文本 | 验证 | 人工测试+ARIA属性断言 |
Keyboard Navigation Test Example
键盘导航测试示例
csharp
[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();
}csharp
[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();
}Key Principles
核心原则
- Use the page object model for any UI test suite with more than a handful of tests. The upfront cost pays for itself quickly in reduced maintenance.
- Prefer or accessibility-based selectors over CSS or DOM-structure selectors. Stable selectors are the single most effective defense against flaky tests.
data-testid - Never use or
Thread.Sleepas a wait strategy. Use framework-native waits that poll for conditions with timeouts.Task.Delay - Run accessibility checks as part of the standard test suite, not as a separate audit. Catching violations early prevents accessibility debt.
- Keep page objects framework-agnostic where possible. The patterns (POM, selector strategy, wait patterns) are universal; only the driver API changes between Playwright, bUnit, and Appium.
- 对于包含多个测试的UI测试套件,使用页面对象模型。前期的投入会很快在减少维护成本上得到回报。
- 优先使用或基于可访问性的选择器,而非CSS或DOM结构选择器。稳定的选择器是防止测试不稳定的最有效手段。
data-testid - 绝不要使用或
Thread.Sleep作为等待策略。使用框架原生的等待机制,带超时的条件轮询。Task.Delay - 将可访问性检查作为标准测试套件的一部分,而非单独的审计。尽早发现违规问题可避免可访问性债务。
- 尽可能保持页面对象与框架无关。这些模式(POM、选择器策略、等待模式)是通用的;仅驱动API在Playwright、bUnit和Appium之间有所不同。
Agent Gotchas
Agent注意事项
- Do not add attributes to production code without team agreement. Some teams strip them in production builds; others keep them. Check the project's conventions first.
data-testid - Do not use (hardcoded delay) in Playwright tests. It masks timing issues and makes tests slow. Use
WaitForTimeout,WaitForSelectorAsync, orExpect(...).ToBeVisibleAsync()instead.WaitForResponseAsync - Do not assert on element count without waiting for the list to load. returns zero if the component has not finished rendering. Use
FindAll("[data-testid='row']").CountorWaitForStatefirst.WaitForAssertion - Do not skip accessibility testing because "it's not a requirement." WCAG compliance is increasingly a legal requirement. Automated checks catch the low-hanging fruit at near-zero cost.
- Do not create deeply nested page objects. If a page object has page objects inside page objects, flatten the hierarchy. One level of component composition (page -> components) is sufficient.
- 未经团队同意,不要在生产代码中添加属性。有些团队会在生产构建中移除它们;有些团队则保留。请先确认项目的约定。
data-testid - 不要在Playwright测试中使用(硬编码延迟)。它会掩盖时序问题,且使测试变慢。请改用
WaitForTimeout、WaitForSelectorAsync或Expect(...).ToBeVisibleAsync()。WaitForResponseAsync - 不要在未等待列表加载完成的情况下断言元素数量。如果组件尚未完成渲染,会返回0。请先使用
FindAll("[data-testid='row']").Count或WaitForState。WaitForAssertion - 不要因为“不是要求”就跳过可访问性测试。WCAG合规性正日益成为法律要求。自动化检查几乎零成本就能发现大部分常见问题。
- 不要创建深度嵌套的页面对象。如果页面对象中包含页面对象,请扁平化层级。一层组件组合(页面->组件)就足够了。