본문으로 건너뛰기
cccskills
무료GitHub에 공개

dotnet-playwright

Automates browser tests in .NET. Playwright E2E, CI browser caching, trace viewer, codegen.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구GitHubPlaywright
설치 방법 보기

포함된 파일(1)

  • SKILL.md19.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

dotnet-playwright

Playwright for .NET: browser automation and end-to-end testing. Covers browser lifecycle management, page interactions, assertions, CI caching of browser binaries, trace viewer for debugging failures, and codegen for rapid test scaffolding.

Version assumptions: Playwright 1.40+ for .NET, .NET 8.0+ baseline. Playwright supports Chromium, Firefox, and WebKit browsers.

Scope

  • Browser lifecycle management (Chromium, Firefox, WebKit)
  • Page interactions and locator-based assertions
  • CI caching of browser binaries
  • Trace viewer for debugging test failures
  • Codegen for rapid test scaffolding

Out of scope

  • Shared UI testing patterns (page object model, selectors, wait strategies) -- see [skill:dotnet-ui-testing-core]
  • Testing strategy (when E2E vs unit vs integration) -- see [skill:dotnet-testing-strategy]
  • Test project scaffolding -- see [skill:dotnet-add-testing]

Prerequisites: Test project scaffolded via [skill:dotnet-add-testing] with Playwright packages referenced. Browsers installed via pwsh bin/Debug/net8.0/playwright.ps1 install or dotnet tool run playwright install.

Cross-references: [skill:dotnet-ui-testing-core] for page object model and selector strategies, [skill:dotnet-testing-strategy] for deciding when E2E tests are appropriate.


Package Setup


<PackageReference Include="Microsoft.Playwright" Version="1.*" />
<!-- For xUnit integration: -->
<PackageReference Include="Microsoft.Playwright.Xunit" Version="1.*" />
<!-- For NUnit integration: -->
<!-- <PackageReference Include="Microsoft.Playwright.NUnit" Version="1.*" /> -->

```text

### Installing Browsers

Playwright requires downloading browser binaries before tests can run:

```bash

# After building the test project:
pwsh bin/Debug/net8.0/playwright.ps1 install

# Or install specific browsers:
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
pwsh bin/Debug/net8.0/playwright.ps1 install firefox

# Using dotnet tool:
dotnet tool install --global Microsoft.Playwright.CLI
playwright install

```text

---

## Basic Test Structure

### With Playwright xUnit Base Class

```csharp

using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;

// PageTest provides Page, Browser, BrowserContext, and Playwright properties
public class HomePageTests : PageTest
{
    [Fact]
    public async Task HomePage_Title_ContainsAppName()
    {
        await Page.GotoAsync("https://localhost:5001");

        await Expect(Page).ToHaveTitleAsync(new Regex("My App"));
    }

    [Fact]
    public async Task HomePage_NavLinks_AreVisible()
    {
        await Page.GotoAsync("https://localhost:5001");

        var nav = Page.Locator("nav");
        await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "Home" }))
            .ToBeVisibleAsync();
        await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "About" }))
            .ToBeVisibleAsync();
    }
}

```text

### Manual Setup (Without Base Class)

```csharp

public class ManualSetupTests : IAsyncLifetime
{
    private IPlaywright _playwright = null!;
    private IBrowser _browser = null!;
    private IBrowserContext _context = null!;
    private IPage _page = null!;

    public async ValueTask InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
        _context = await _browser.NewContextAsync(new BrowserNewContextOptions
        {
            ViewportSize = new ViewportSize { Width = 1280, Height = 720 },
            Locale = "en-US"
        });
        _page = await _context.NewPageAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await _page.CloseAsync();
        await _context.CloseAsync();
        await _browser.CloseAsync();
        _playwright.Dispose();
    }

    [Fact]
    public async Task Login_ValidUser_RedirectsToDashboard()
    {
        await _page.GotoAsync("https://localhost:5001/login");

        await _page.FillAsync("[data-testid='email']", "user@example.com");
        // Use placeholder password in docs/examples
        await _page.FillAsync("[data-testid='password']", "<TEST_PASSWORD_PLACEHOLDER>");
        await _page.ClickAsync("[data-testid='login-btn']");

        await Expect(_page).ToHaveURLAsync(new Regex("/dashboard"));
    }
}

```text

---

## Locators and Interactions

### Recommended Locator Strategies

```csharp

// BEST: Role-based (accessible and semantic)
var submitBtn = Page.GetByRole(AriaRole.Button, new() { Name = "Submit Order" });

// GOOD: Test ID (stable, explicit)
var emailInput = Page.Locator("[data-testid='email-input']");

// GOOD: Label text (user-visible, accessible)
var nameField = Page.GetByLabel("Full Name");

// GOOD: Placeholder (user-visible)
var searchBox = Page.GetByPlaceholder("Search products...");

// AVOID: CSS class (fragile, changes with styling)
var card = Page.Locator(".card-primary");

// AVOID: XPath (brittle, hard to read)
var cell = Page.Locator("//table/tbody/tr[1]/td[2]");

```text

### Common Interactions

```csharp

// Text input
await Page.FillAsync("[data-testid='name']", "Alice Johnson");

// Click
await Page.ClickAsync("[data-testid='submit']");

// Select dropdown
await Page.SelectOptionAsync("[data-testid='country']", "US");

// Checkbox / radio
await Page.CheckAsync("[data-testid='agree-terms']");

// File upload
await Page.SetInputFilesAsync("[data-testid='avatar']", "testdata/photo.jpg");

// Keyboard
await Page.Keyboard.PressAsync("Enter");
await Page.Keyboard.TypeAsync("search query");

// Hover (for dropdowns, tooltips)
await Page.HoverAsync("[data-testid='user-menu']");

```text

### Assertions (Expect API)

Playwright assertions auto-retry until the condition is met or the timeout expires:

```csharp

// Element visibility
await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
await Expect(Page.Locator("[data-testid='spinner']")).ToBeHiddenAsync();

// Text content
await Expect(Page.Locator("[data-testid='total']")).ToHaveTextAsync("$99.99");
await Expect(Page.Locator("[data-testid='status']")).ToContainTextAsync("Completed");

// Attribute
await Expect(Page.Locator("[data-testid='submit']")).ToBeEnabledAsync();
await Expect(Page.Locator("[data-testid='email']")).ToHaveValueAsync("user@example.com");

// Page-level
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));
await Expect(Page).ToHaveTitleAsync("Order Details - My App");

// Count
await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(5);

```text

---

## Network Interception

### Mocking API Responses

```csharp

[Fact]
public async Task OrderList_WithMockedApi_DisplaysOrders()
{
    // Intercept API calls and return mock data
    await Page.RouteAsync("**/api/orders", async route =>
    {
        var json = JsonSerializer.Serialize(new[]
        {
            new { Id = 1, CustomerName = "Alice", Total = 99.99 },
            new { Id = 2, CustomerName = "Bob", Total = 149.50 }
        });
        await route.FulfillAsync(new RouteFulfillOptions
        {
            Status = 200,
            ContentType = "application/json",
            Body = json
        });
    });

    await Page.GotoAsync("https://localhost:5001/orders");

    await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(2);
}

```text

### Waiting for Network Requests

```csharp

[Fact]
public async Task CreateOrder_SubmitForm_WaitsForApiResponse()
{
    await Page.GotoAsync("https://localhost:5001/orders/new");

    await Page.FillAsync("[data-testid='customer']", "Alice");
    await Page.FillAsync("[data-testid='amount']", "99.99");

    // Wait for the API call triggered by form submission
    var responseTask = Page.WaitForResponseAsync(
        response => response.Url.Contains("/api/orders") && response.Status == 201);

    await Page.ClickAsync("[data-testid='submit']");

    var response = await responseTask;
    Assert.Equal(201, response.Status);
}

```text

---

## CI Browser Caching

Downloading browser binaries on every CI run is slow (500MB+). Cache them to speed up builds.

### GitHub Actions Caching

```yaml

# .github/workflows/e2e-tests.yml
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Build
        run: dotnet build tests/MyApp.E2E/

      - name: Cache Playwright browsers
        id: playwright-cache
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('tests/MyApp.E2E/MyApp.E2E.csproj') }}

      - name: Install Playwright browsers
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps

      - name: Install Playwright system deps
        if: steps.playwright-cache.outputs.cache-hit == 'true'
        run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps

      - name: Run E2E tests
        run: dotnet test tests/MyApp.E2E/

```powershell

### Azure DevOps Caching

```yaml

# azure-pipelines.yml
steps:
  - task: Cache@2
    inputs:
      key: 'playwright | "$(Agent.OS)" | tests/MyApp.E2E/MyApp.E2E.csproj'
      path: $(HOME)/.cache/ms-playwright
      restoreKeys: |
        playwright | "$(Agent.OS)"
      cacheHitVar: PLAYWRIGHT_CACHE_RESTORED
    displayName: Cache Playwright browsers

  - script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps
    condition: ne(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
    displayName: Install Playwright browsers

  - script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps
    condition: eq(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
    displayName: Install Playwright system deps (cached browsers)

  - script: dotnet test tests/MyApp.E2E/
    displayName: Run E2E tests

```text

### Cache Key Strategy

The cache key should include:
- **OS:** Browser binaries are platform-specific
- **Project file hash:** Playwright version determines browser versions; changing the package version invalidates the cache
- **Fallback key:** Allows partial cache restoration when the project file changes

---

## Trace Viewer

Playwright's trace viewer captures a full recording of test execution for debugging failures. Each trace includes screenshots, DOM snapshots, network logs, and console output.

### Enabling Traces

```csharp

public class TracedTests : IAsyncLifetime
{
    private IPlaywright _playwright = null!;
    private IBrowser _browser = null!;
    private IBrowserContext _context = null!;

    public IPage Page { get; private set; } = null!;

    public async ValueTask InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync();
        _context = await _browser.NewContextAsync();

        // Start tracing before each test
        await _context.Tracing.StartAsync(new TracingStartOptions
        {
            Screenshots = true,
            Snapshots = true,
            Sources = true
        });

        Page = await _context.NewPageAsync();
    }

    public async ValueTask DisposeAsync()
    {
        // Save trace on failure (check test result in xUnit requires custom wrapper)
        await _context.Tracing.StopAsync(new TracingStopOptions
        {
            Path = Path.Combine("test-results", "traces",
                $"trace-{DateTime.UtcNow:yyyyMMdd-HHmmss}.zip")
        });

        await Page.CloseAsync();
        await _context.CloseAsync();
        await _browser.CloseAsync();
        _playwright.Dispose();
    }
}

```text

### Viewing Traces

```bash

# Open trace file in browser
pwsh bin/Debug/net8.0/playwright.ps1 show-trace test-results/traces/trace-20260101-120000.zip

# Or use the online trace viewer
# Upload the .zip to https://trace.playwright.dev/

```powershell

### Trace on Failure Only

Save traces only when tests fail to reduce storage:

```csharp

// In a custom test class or middleware
public async Task RunWithTrace(Func<IPage, Task> testAction, string testName)
{
    await _context.Tracing.StartAsync(new TracingStartOptions
    {
        Screenshots = true,
        Snapshots = true,
        Sources = true
    });

    try
    {
        await testAction(Page);
        // Test passed -- discard trace
        await _context.Tracing.StopAsync();
    }
    catch
    {
        // Test failed -- save trace for debugging
        await _context.Tracing.StopAsync(new TracingStopOptions
        {
            Path = $"test-results/traces/{testName}.zip"
        });
        throw;
    }
}

```text

---

## Codegen

Playwright's code generator records browser interactions and generates test code. Use it to scaffold tests quickly, then refine the generated code.

### Running Codegen

```bash

# Open codegen with your app URL
pwsh bin/Debug/net8.0/playwright.ps1 codegen https://localhost:5001

# With specific browser
pwsh bin/Debug/net8.0/playwright.ps1 codegen --browser firefox https://localhost:5001

# With device emulation
pwsh bin/Debug/net8.0/playwright.ps1 codegen --device "iPhone 15" https://localhost:5001

# With saved authentication state
pwsh bin/Debug/net8.0/playwright.ps1 codegen --save-storage auth.json https://localhost:5001

```json

### Codegen Best Practices

1. **Use codegen as a starting point,** not the final test. Generated code often uses fragile selectors and lacks proper assertions.
2. **Replace generated selectors** with `data-testid` or role-based locators immediately after generating.
3. **Add meaningful assertions.** Codegen records actions but does not know what to verify. Add `Expect()` calls for expected outcomes.
4. **Extract page objects** from generated code. Group related interactions into page object methods.

### Before and After Codegen Refinement

```csharp

// GENERATED by codegen (fragile, no assertions):
await page.GotoAsync("https://localhost:5001/orders");
await page.Locator("#root > div > main > div:nth-child(2) > button").ClickAsync();
await page.GetByPlaceholder("Customer name").FillAsync("Alice");
await page.GetByPlaceholder("Amount").FillAsync("99.99");
await page.Locator("form > button[type='submit']").ClickAsync();

// REFINED (stable selectors, proper assertions):
await Page.GotoAsync("https://localhost:5001/orders");
await Page.ClickAsync("[data-testid='new-order-btn']");
await Page.FillAsync("[data-testid='customer-name']", "Alice");
await Page.FillAsync("[data-testid='amount']", "99.99");
await Page.ClickAsync("[data-testid='submit-order']");

await Expect(Page.Locator("[data-testid='success-toast']"))
    .ToBeVisibleAsync();
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));

```text

---

## Multi-Browser Testing

### Running Tests Across Browsers

```csharp

// Using Playwright xUnit base class with environment variable
// Set BROWSER=chromium|firefox|webkit via CLI or CI config
public class CrossBrowserTests : PageTest
{
    [Fact]
    public async Task OrderFlow_WorksAcrossBrowsers()
    {
        // This test runs in whichever browser BROWSER env var specifies
        await Page.GotoAsync("https://localhost:5001/orders/new");
        await Page.FillAsync("[data-testid='customer']", "Alice");
        await Page.ClickAsync("[data-testid='submit']");

        await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
    }
}

```text

```bash

# Run tests in each browser
BROWSER=chromium dotnet test
BROWSER=firefox dotnet test
BROWSER=webkit dotnet test

```text

### CI Matrix Strategy

```yaml

# GitHub Actions matrix for multi-browser
strategy:
  matrix:
    browser: [chromium, firefox, webkit]
steps:
  - name: Run E2E tests
    run: dotnet test tests/MyApp.E2E/
    env:
      BROWSER: ${{ matrix.browser }}

```text

---

## Key Principles

- **Use Playwright assertions (`Expect`) instead of raw xUnit `Assert`.** Playwright assertions auto-retry with configurable timeouts, eliminating flaky timing issues.
- **Cache browser binaries in CI.** Downloading 500MB+ of browsers per run wastes time and bandwidth. Cache by OS + Playwright version.
- **Enable trace viewer for debugging CI failures.** Traces capture everything needed to reproduce a failure without re-running the test.
- **Use codegen to bootstrap tests, then refine.** Generated code gets you started fast; manual refinement makes tests maintainable.
- **Prefer role-based or `data-testid` locators** over CSS classes or XPath. See [skill:dotnet-ui-testing-core] for the full selector priority guide.

---

## Agent Gotchas

1. **Do not forget to install browsers after adding the Playwright package.** The NuGet package does not include browser binaries. Run the install script after building.
2. **Do not use `Task.Delay` for waiting.** Playwright's auto-waiting and `Expect` assertions handle timing automatically. Adding delays makes tests slow and still flaky.
3. **Do not hardcode `localhost` ports.** Use configuration or environment variables for the base URL. CI environments may use different ports than local development.
4. **Do not skip `--with-deps` on first CI install.** Playwright browsers need system libraries (libgbm, libasound, etc.) on Linux. The `--with-deps` flag installs them. Subsequent cached runs only need `install-deps`.
5. **Do not store trace files in the repository.** Traces are large binary files. Write them to a `test-results/` directory that is git-ignored, and upload them as CI artifacts.
6. **Do not create a new browser instance per test.** Browser launch is expensive. Use `IClassFixture` or the Playwright xUnit base class to share a browser across tests in a class. Create a new `BrowserContext` per test for isolation.

---

## References

- [Playwright for .NET Documentation](https://playwright.dev/dotnet/)
- [Playwright Locators](https://playwright.dev/dotnet/docs/locators)
- [Playwright Assertions](https://playwright.dev/dotnet/docs/test-assertions)
- [Playwright Trace Viewer](https://playwright.dev/dotnet/docs/trace-viewer)
- [Playwright Codegen](https://playwright.dev/dotnet/docs/codegen)
- [Playwright CI Configuration](https://playwright.dev/dotnet/docs/ci)
- [Playwright Browser Downloads](https://playwright.dev/dotnet/docs/browsers)

Code Navigation (Serena MCP)

Primary approach: Use Serena symbol operations for efficient code navigation:

  1. Find definitions: serena_find_symbol instead of text search
  2. Understand structure: serena_get_symbols_overview for file organization
  3. Track references: serena_find_referencing_symbols for impact analysis
  4. Precise edits: serena_replace_symbol_body for clean modifications

When to use Serena vs traditional tools:

  • Use Serena: Navigation, refactoring, dependency analysis, precise edits
  • Use Read/Grep: Reading full files, pattern matching, simple text operations
  • Fallback: If Serena unavailable, traditional tools work fine

Example workflow:

# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"

# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Patterns and techniques for evaluating and improving AI agent outputs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Comprehensive AI prompt engineering safety review and improvement prompt. Analyzes prompts for safety, bias, security vulnerabilities, and effectiveness while providing detailed improvement recommendations.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Use when user requests research requiring multiple sources, comprehensive analysis, or synthesis across topics - technical research, domain knowledge gathering, market analysis, or learning about complex subjects

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

deep-wiki

무료

AI-powered wiki generation for code repositories with commands, agents, and skills

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Use when building .NET 10 or C# 14 applications; when using minimal APIs, modular monolith patterns, or feature folders; when implementing HTTP resilience, Options pattern, Channels, or validation; when seeing outdated patterns like old extension method syntax

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Implements accessible .NET UI. SemanticProperties, ARIA, AutomationPeer, testing per platform.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

rudironsoni의 스킬 모두 보기

이 스킬의 문제 신고