AGENT SKILL

tunit-playwright-ui-testing

Comprehensive guidance for writing coded end-to-end UI tests in C# using TUnit.Playwright. Tests inherit from PageTest — the base class manages all browser lifecycle. Covers NuGet setup, PageTest inheritance, Page Object Model, browser configuration, parallelism limiting, and CI integration. Based on the official TUnit docs at https://tunit.dev/docs/examples/playwright.

适用职业设计师
使用场景测试与质量
License 许可
GPL-3.0
最近更新
2026-06-06
Stars
★ 0
Forks
0

01 前言 Front Matter

name
tunit-playwright-ui-testing
description
Comprehensive guidance for writing coded end-to-end UI tests in C# using TUnit.Playwright. Tests inherit from PageTest — the base class manages all browser lifecycle. Covers NuGet setup, PageTest inheritance, Page Object Model, browser configuration, parallelism limiting, and CI integration. Based on the official TUnit docs at https://tunit.dev/docs/examples/playwright.

03 SKILL.md 正文

tunit-playwright-ui-testing

This skill guides the sqa-engineer to write coded, repeatable UI tests in C# for web applications in HypermediaEngine using TUnit.Playwright — where tests inherit from PageTest and the base class manages browser lifecycle automatically.

Source: Official TUnit docs — https://tunit.dev/docs/examples/playwright

When to use: Use this skill for end-to-end tests that must be committed as .cs test files and run in CI via dotnet test. For one-time AI-driven exploratory browser tests (no .cs files), use playwright-mcp-ui-testing instead. For Blazor component-level tests, use bunit-blazor-testing.


Phase 0 — Context Load (silent)

  1. Read CLAUDE.md and project conventions.
  2. Load memory: Skill("manage-memory", args: "sqa-engineer").
  3. Identify the test project that will host Playwright tests. Create a new *.PlaywrightTests csproj if one doesn't exist.
  4. Read the spec file at docs/specs/<feature-slug>.spec.md if it exists.

Phase 1 — NuGet Setup

Add to Directory.Packages.props (if not already present):

<PackageReference Include="TUnit.Playwright" Version="*" />

Add to the Playwright test project's .csproj:

<PackageReference Include="TUnit.Playwright" />

Run Playwright browser install after building:

# Windows (PowerShell):
pwsh bin/Debug/net10.0/playwright.ps1 install chromium

# macOS/Linux:
./bin/Debug/net10.0/playwright.sh install chromium

TUnit.Playwright transitively brings in Microsoft.Playwright. No need to reference Microsoft.Playwright directly.


Phase 2 — Simplest Possible Test (PageTest)

By inheriting from PageTest, the base class sets up and disposes all Playwright objects. The following properties are available in every test:

Property Type Description
Page IPage The current browser page (tab)
Context IBrowserContext The browser context
Browser IBrowser The browser instance
Playwright IPlaywright The Playwright instance

Minimal test:

using TUnit.Playwright;

public class Tests : PageTest
{
    [Test]
    public async Task NavigatesToGitHub()
    {
        await Page.GotoAsync("https://www.github.com/thomhurst/TUnit");
    }
}

No setup code needed. No fixture classes. No ClassDataSource. Just inherit and write tests.


Phase 3 — Real Page Interaction Tests

using TUnit.Core;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Playwright;

public class LoginPageTests : PageTest
{
    [Test]
    [DisplayName("[AC-1] Login button is visible on the login page")]
    public async Task Login_Button_Is_Visible()
    {
        await Page.GotoAsync("https://example.com/login");
        ILocator loginButton = Page.Locator("button#login");
        await Assert.That(await loginButton.IsVisibleAsync()).IsTrue();
    }

    [Test]
    [DisplayName("[AC-2] Successful login redirects to dashboard")]
    public async Task Successful_Login_Redirects_To_Dashboard()
    {
        await Page.GotoAsync("https://example.com/login");
        await Page.FillAsync("#username", "testuser");
        await Page.FillAsync("#password", "password123");
        await Page.ClickAsync("button#login");
        await Page.WaitForURLAsync("**/dashboard");
        string? heading = await Page.Locator("h1").TextContentAsync();
        await Assert.That(heading).IsEqualTo("Dashboard");
    }
}

Phase 4 — Configuring Browser Options

Override BrowserName and pass BrowserTypeLaunchOptions to the base constructor:

using Microsoft.Playwright;
using TUnit.Playwright;

public class HeadlessChromeTests : PageTest
{
    public HeadlessChromeTests() : base(new BrowserTypeLaunchOptions
    {
        Headless = Environment.GetEnvironmentVariable("PLAYWRIGHT_HEADLESS") != "false",
        SlowMo = 0  // set to 100 for debugging — adds 100ms delay between actions
    }) { }

    public override string BrowserName => "chromium";  // chromium | firefox | webkit

    [Test]
    public async Task Page_Title_Matches()
    {
        await Page.GotoAsync("https://example.com");
        string title = await Page.TitleAsync();
        await Assert.That(title).Contains("Example");
    }
}

Browser options:

  • BrowserName values: "chromium" (default), "firefox", "webkit"
  • Headless = true for CI; read from env var for local flexibility
  • SlowMo — milliseconds to add between Playwright actions (useful for debugging)

Phase 5 — Page Object Model

Keep pages assertion-free. Page Objects return data; tests assert on it.

public class WeatherPage(IPage page)
{
    private const string Url = "http://localhost:5000/weather";

    public async Task NavigateAsync()
        => await page.GotoAsync(Url);

    public async Task<IReadOnlyList<ILocator>> GetForecastRowsAsync()
        => await page.Locator("table tbody tr").AllAsync();

    public async Task<string?> GetTitleAsync()
        => await page.TitleAsync();
}

public class WeatherPageTests : PageTest
{
    [Test]
    [DisplayName("[AC-3] Weather page shows forecast rows")]
    public async Task WeatherPage_WhenLoaded_ShowsForecastRows()
    {
        WeatherPage weatherPage = new(Page);

        // Arrange + Act
        await weatherPage.NavigateAsync();
        IReadOnlyList<ILocator> rows = await weatherPage.GetForecastRowsAsync();

        // Assert
        await Assert.That(rows.Count).IsGreaterThan(0);
    }
}

Page Object rules:

  • One class per page or major component.
  • No assertions inside page objects — they return data; tests assert.
  • Use Locator over QuerySelector — locators are lazy and auto-retry.
  • Prefer GetByRole, GetByLabel, GetByText, GetByTestId over CSS selectors.

Phase 6 — Controlling Parallelism

Browser tests are resource-intensive. Use [ParallelLimiter<T>] to cap concurrency:

using TUnit.Core;
using TUnit.Playwright;

public class BrowserParallelLimit : IParallelLimit
{
    public int Limit => 2;  // at most 2 browser tests run at once
}

[ParallelLimiter<BrowserParallelLimit>]
public class HeavyBrowserTests : PageTest
{
    [Test]
    public async Task Test_A()
    {
        await Page.GotoAsync("https://example.com/a");
        await Assert.That(await Page.TitleAsync()).IsNotNull();
    }

    [Test]
    public async Task Test_B()
    {
        await Page.GotoAsync("https://example.com/b");
        await Assert.That(await Page.TitleAsync()).IsNotNull();
    }
}

Phase 7 — Async Patterns

// ✅ Always await Playwright calls
await Page.GotoAsync(url);
await Page.ClickAsync("button[type=submit]");
ILocator input = Page.GetByLabel("Email");
await input.FillAsync("user@example.com");

// ✅ Use locators with built-in retry and wait
ILocator successMsg = Page.GetByText("Order confirmed");
await successMsg.WaitForAsync(new LocatorWaitForOptions { Timeout = 5000 });

// ❌ Never use .Result / .Wait() — deadlocks in async context
Page.GotoAsync(url).Result;

// ❌ Never use Thread.Sleep
Thread.Sleep(2000);

Phase 8 — Running Tests

# Run only Playwright tests
dotnet test --filter "FullyQualifiedName~PlaywrightTests"

# Run in headed mode (visible browser window) for local debugging:
# Windows PowerShell:
$env:PLAYWRIGHT_HEADLESS="false"; dotnet test --filter "FullyQualifiedName~PlaywrightTests"
# macOS/Linux:
PLAYWRIGHT_HEADLESS=false dotnet test --filter "FullyQualifiedName~PlaywrightTests"

Phase 9 — CI Integration (GitHub Actions)

- name: Install Playwright browsers
  run: pwsh tests/YourProject.PlaywrightTests/bin/Release/net10.0/playwright.ps1 install --with-deps chromium

- name: Run Playwright tests
  run: dotnet test tests/YourProject.PlaywrightTests/ --configuration Release
  env:
    PLAYWRIGHT_HEADLESS: "true"

Best Practices

Do Don't
Inherit from PageTest Create manual browser fixture classes
Use GetByRole, GetByLabel, GetByTestId Reach for CSS selectors first
Use Locator.WaitForAsync() to stabilize Use await Task.Delay()
Keep assertions in test methods Put Assert.That() in page objects
Override BrowserName per test class Leave browser type implicit
Use [ParallelLimiter<T>] on heavy suites Let all tests run in unlimited parallel
Read Headless from env var Hardcode Headless = true or false
Use Assert.Multiple() for multi-property assertions Let first failure stop the test
Trace each test to an AC ID in [DisplayName] Write untargeted tests

References

04 同仓库更多 Skills

rafsanulhasan
rafsanulhasan/HypermediaEngine

write-tests

Test implementation skill for HypermediaEngine. Takes a confirmed test plan from design-test-cases and produces compilable, runnable xUnit tests following project conventions. Validates coverage quality through mutation testing. Invoked by the sqa-engineer agent after the test plan is approved.

测试与质量
★ 0
rafsanulhasan
rafsanulhasan/HypermediaEngine

write-documentation

Structured documentation writing workflow for HypermediaEngine. Writes or updates README.md files at the repo root and in each project directory (src/, tests/, samples/, etc.) based on current implementation, ADRs, and architecture decisions.

★ 0
rafsanulhasan
rafsanulhasan/HypermediaEngine

write-adr

Writes a numbered Architecture Decision Record (ADR) for a significant architectural choice in the HypermediaEngine project. Invoked by the software-architect agent whenever a consequential design decision is made or ratified. Saves the ADR to docs/architecture/decisions/.

★ 0
rafsanulhasan
rafsanulhasan/HypermediaEngine

triage

Task classification, decomposition, dependency mapping, and routing skill. Invoked by the triage-agent to break any user request into discrete, independently-routable work items with assigned priorities and agent chains.

Skill与智能体开发
★ 0
rafsanulhasan
rafsanulhasan/HypermediaEngine

system-design

Expert system design skill. Use PROACTIVELY to design systems, review architectures, and assist software engineers in learning design principles and implementing them in the code with design patterns.

设计师视觉设计
★ 0
rafsanulhasan
rafsanulhasan/HypermediaEngine

spec-driven-development

Manages the full lifecycle of a feature specification — from collaborative drafting to enforcement during implementation and testing.

★ 0