dotnet-ui-testing-core

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-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 modal
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 modal

Example: 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.
    LoginAsync
    returns
    DashboardPage
    , guiding test authors through the application flow.
  • Never expose raw selectors from page objects. Tests call
    LoginAsync()
    , not
    ClickAsync("#submit")
    .
  • Keep assertions in tests, not page objects. Page objects provide data (e.g.,
    GetErrorMessageAsync()
    ); tests make assertions on that data.
  • Compose page objects from reusable components. A
    NavigationMenu
    component object can be embedded in every page that has a nav bar.

  • 导航操作返回下一个页面对象
    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)

选择器优先级(从最可靠到最不可靠)

PrioritySelector TypeExampleReliability
1
data-testid
[data-testid='submit-btn']
Highest -- survives CSS/layout changes
2Accessibility role + name
GetByRole(AriaRole.Button, new() { Name = "Submit" })
High -- tied to visible behavior
3Label text
GetByLabel("Email address")
High -- changes when copy changes
4Placeholder text
GetByPlaceholder("Enter email")
Medium -- often localized
5CSS class
.btn-primary
Low -- changes with styling
6XPath / DOM structure
//div[3]/button[1]
Lowest -- breaks on any layout change
优先级选择器类型示例可靠性
1
data-testid
[data-testid='submit-btn']
最高 —— 不受CSS/布局变化影响
2无障碍角色+名称
GetByRole(AriaRole.Button, new() { Name = "Submit" })
高 —— 与可见行为绑定
3标签文本
GetByLabel("Email address")
高 —— 文本变更时会变化
4占位符文本
GetByPlaceholder("Enter email")
中 —— 通常会本地化
5CSS类
.btn-primary
低 —— 随样式变化
6XPath / DOM结构
//div[3]/button[1]
最低 —— 任何布局变化都会使其失效

Adding Test IDs

添加测试ID

Add
data-testid
attributes to elements that tests interact with. They are invisible to users and stable across refactors:
Blazor:
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-testid
属性。这些属性对用户不可见,且在重构过程中保持稳定:
Blazor:
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测试的可访问性检查清单

CheckHow to TestTool
Color contrastAutomated axe-core ruleDeque.AxeCore.Playwright
Keyboard navigationTab through all interactive elementsPlaywright
page.Keyboard
ARIA labelsVerify
aria-label
/
aria-labelledby
present
Playwright locators + assertions
Focus managementVerify focus moves to dialogs/modalsPlaywright
page.Locator(':focus')
Screen reader textVerify
aria-live
regions update
Manual + assertion on ARIA attributes
检查项测试方法工具
颜色对比度axe-core自动化规则Deque.AxeCore.Playwright
键盘导航按Tab键遍历所有交互元素Playwright
page.Keyboard
ARIA标签验证
aria-label
/
aria-labelledby
是否存在
Playwright定位器+断言
焦点管理验证焦点是否移动到对话框/模态框Playwright
page.Locator(':focus')
屏幕阅读器文本验证
aria-live
区域是否更新
人工测试+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
    data-testid
    or accessibility-based selectors over CSS or DOM-structure selectors.
    Stable selectors are the single most effective defense against flaky tests.
  • Never use
    Thread.Sleep
    or
    Task.Delay
    as a wait strategy.
    Use framework-native waits that poll for conditions with timeouts.
  • 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测试套件,使用页面对象模型。前期的投入会很快在减少维护成本上得到回报。
  • 优先使用
    data-testid
    或基于可访问性的选择器,而非CSS或DOM结构选择器
    。稳定的选择器是防止测试不稳定的最有效手段。
  • 绝不要使用
    Thread.Sleep
    Task.Delay
    作为等待策略
    。使用框架原生的等待机制,带超时的条件轮询。
  • 将可访问性检查作为标准测试套件的一部分,而非单独的审计。尽早发现违规问题可避免可访问性债务。
  • 尽可能保持页面对象与框架无关。这些模式(POM、选择器策略、等待模式)是通用的;仅驱动API在Playwright、bUnit和Appium之间有所不同。

Agent Gotchas

Agent注意事项

  1. Do not add
    data-testid
    attributes to production code without team agreement.
    Some teams strip them in production builds; others keep them. Check the project's conventions first.
  2. Do not use
    WaitForTimeout
    (hardcoded delay) in Playwright tests.
    It masks timing issues and makes tests slow. Use
    WaitForSelectorAsync
    ,
    Expect(...).ToBeVisibleAsync()
    , or
    WaitForResponseAsync
    instead.
  3. Do not assert on element count without waiting for the list to load.
    FindAll("[data-testid='row']").Count
    returns zero if the component has not finished rendering. Use
    WaitForState
    or
    WaitForAssertion
    first.
  4. 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.
  5. 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.

  1. 未经团队同意,不要在生产代码中添加
    data-testid
    属性
    。有些团队会在生产构建中移除它们;有些团队则保留。请先确认项目的约定。
  2. 不要在Playwright测试中使用
    WaitForTimeout
    (硬编码延迟)
    。它会掩盖时序问题,且使测试变慢。请改用
    WaitForSelectorAsync
    Expect(...).ToBeVisibleAsync()
    WaitForResponseAsync
  3. 不要在未等待列表加载完成的情况下断言元素数量。如果组件尚未完成渲染,
    FindAll("[data-testid='row']").Count
    会返回0。请先使用
    WaitForState
    WaitForAssertion
  4. 不要因为“不是要求”就跳过可访问性测试。WCAG合规性正日益成为法律要求。自动化检查几乎零成本就能发现大部分常见问题。
  5. 不要创建深度嵌套的页面对象。如果页面对象中包含页面对象,请扁平化层级。一层组件组合(页面->组件)就足够了。

References

参考资料