# Multi-Browser E2E Testen met Playwright voor .NET

<datetime class="hidden">2025-11-27T14:00</datetime>

<!--category-- Playwright, E2E Testing, xUnit, Testing, Multi-Browser -->
Playwright is Microsoft's officiële oplossing voor moderne browser automatisering, het aanbieden van multi-browser ondersteuning (Chrome, Firefox, Safari) met een enkele API, ingebouwde trace debugging, en mobiele emulatie. Deze gids dekt alles van setup tot geavanceerde testpatronen, te vergelijken met PuppeteerSharp om u te helpen de juiste tool te kiezen. Als u alleen Chrome nodig hebt en wilt eenvoudiger setup, overwegen [PuppeterSharp](/blog/puppeteersharp-e2e-testing) in plaats daarvan - maar als je uitgebreide cross-browser testen nodig hebt, Playwright is de weg vooruit.

## Inleiding

Als je mijn artikel hebt gelezen [PuppeterSharp](/blog/puppeteersharp-e2e-testing), je zult weten dat ik ben een fan van de moderne E2E test tools die niet maken dat je wilt scheuren uw haar uit. PuppeteerSharp is briljant voor Chrome-alleen testen, maar wat als je nodig hebt om te testen over meerdere browsers? Dat is waar [Playwright](https://playwright.dev/) Kom binnen.

Playwright is Microsoft's antwoord op de browser automatisering probleem, en ze hebben geleerd van alles wat kwam voor. Het is als PuppeteerSharp's meer ambitieuze broer - het doet alles Puppeteer doet, maar voegt Firefox en Safari ondersteuning, beter auto-wachten, en een groot aantal debugging tools die het vinden van problemen een absolute doddle.

In dit artikel, Ik zal je laten zien hoe je Playwright voor .NET te testen over Chrome, Firefox en Safari, met echte code voorbeelden en praktische patronen die u kunt gebruiken vandaag.

[TOC]

## Waarom Playwright over PuppeteerSharp?

Begrijp me niet verkeerd [PuppeterSharp](/blog/puppeteersharp-e2e-testing) is uitstekend als je alleen Chrome nodig hebt. Maar hier is wanneer Playwright zinvol is:

### Het Multi-Browser probleem

Je gebruikers gebruiken niet allemaal Chrome. Ze gebruiken:

- **Chrome/rand**: ~65% van de gebruikers (op basis van chromium)
- **Safari**: ~20% van de gebruikers (vooral mobiel)
- **Firefox**: ~5% van de gebruikers (maar vaak verschillende rendering)

Een bug die alleen in Safari verschijnt kan je 20% van je potentiële gebruikers verliezen. Playwright laat je alle drie testen met dezelfde API.

### Betere ervaring met ontwikkelaars

```mermaid
graph TB
    subgraph "PuppeteerSharp"
        PS[Chrome Only]
        PS --> PS1[Fast setup]
        PS --> PS2[Simple API]
        PS --> PS3[Chrome DevTools]
    end

    subgraph "Playwright"
        PW[Multi-Browser]
        PW --> PW1[Chrome, Firefox, Safari]
        PW --> PW2[Auto-waiting built-in]
        PW --> PW3[Trace viewer]
        PW --> PW4[Codegen tool]
        PW --> PW5[Better error messages]
    end

    style PW stroke:#00aa00,stroke-width:3px
    style PW1 stroke:#0066cc,stroke-width:2px
    style PW3 stroke:#0066cc,stroke-width:2px
    style PW4 stroke:#0066cc,stroke-width:2px
```

### Belangrijkste voordelen

1. **Ondersteuning voor multi-browser** - Test Chrome, Firefox, Safari met identieke code
2. **Beter auto-wachten** - Betrouwbaarder uit de doos, minder schilferige testen
3. **Trace viewer** - Record volledige sporen van tests voor debuggen
4. **Codegen** - Genereer testcode door interactie met uw site
5. **Onderschepping van netwerken** - Krachtiger dan PuppeterSharp
6. **Moderne webfuncties** - Betere ondersteuning voor Shadow DOM, iframes, etc.

## Playwright instellen voor .NET

Installeer de [Microsoft.Playwright](https://www.nuget.org/packages/Microsoft.Playwright) NuGet pakket:

```bash
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit  # Or use xUnit
```

Installeer vervolgens de browsers (deze downloads [Chroom](https://www.chromium.org/), [Firefox](https://www.mozilla.org/firefox/), en [WebKit](https://webkit.org/)):

```bash
pwsh bin/Debug/net9.0/playwright.ps1 install
```

Of op Linux/Mac:

```bash
playwright install
```

**Opmerking:** De eerste browser installeert downloads rond 400MB. Latere updates zijn veel kleiner.

Hier is mijn test project configuratie:

```xml
<PackageReference Include="Microsoft.Playwright" Version="1.48.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.48.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
```

Ik gebruik [xEenheid](https://xunit.net/), maar Playwright werkt net zo goed met [NEenheid](https://nunit.org/) of [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest). In feite, Playwright heeft een toegewijde [NUnit-integratie](https://playwright.dev/dotnet/docs/test-runners#nunit) met extra helpers.

## Een basistestklasse aanmaken

Vergelijkbaar met [PuppeterSharp](/blog/puppeteersharp-e2e-testing#creating-a-base-test-class), maar met multi-browser ondersteuning:

### De klassestructuur

```csharp
using Microsoft.Playwright;
using Xunit.Abstractions;

namespace Mostlylucid.Test.E2E;

public abstract class PlaywrightTestBase : IAsyncLifetime
{
    protected IPlaywright Playwright = null!;
    protected IBrowser Browser = null!;
    protected IBrowserContext Context = null!;
    protected IPage Page = null!;

    protected readonly ITestOutputHelper Output;
    protected const string BaseUrl = "http://localhost:8080";

    // Override this in derived classes to test different browsers
    protected virtual BrowserType BrowserType => BrowserType.Chromium;

    protected PlaywrightTestBase(ITestOutputHelper output)
    {
        Output = output;
    }

    public async Task InitializeAsync()
    {
        // Create Playwright instance
        Playwright = await Microsoft.Playwright.Playwright.CreateAsync();

        // Launch the specified browser
        Browser = await LaunchBrowserAsync();

        // Create a new context (like an incognito window)
        Context = await Browser.NewContextAsync(new BrowserNewContextOptions
        {
            ViewportSize = new ViewportSize { Width = 1400, Height = 900 },
            IgnoreHTTPSErrors = true
        });

        // Create a new page
        Page = await Context.NewPageAsync();

        // Set default timeout
        Page.SetDefaultTimeout(30000);
    }

    private async Task<IBrowser> LaunchBrowserAsync()
    {
        var options = new BrowserTypeLaunchOptions
        {
            Headless = true, // Set to false for debugging
        };

        return BrowserType switch
        {
            BrowserType.Chromium => await Playwright.Chromium.LaunchAsync(options),
            BrowserType.Firefox => await Playwright.Firefox.LaunchAsync(options),
            BrowserType.Webkit => await Playwright.Webkit.LaunchAsync(options),
            _ => throw new ArgumentException($"Unknown browser type: {BrowserType}")
        };
    }

    public async Task DisposeAsync()
    {
        await Page?.CloseAsync()!;
        await Context?.CloseAsync()!;
        await Browser?.CloseAsync()!;
        Playwright?.Dispose();
    }
}

public enum BrowserType
{
    Chromium,
    Firefox,
    Webkit
}
```

### Waarom Browser Contexts?

Merk op dat we een `Context` voor het aanmaken van een `Page`Dit is een Playwright concept dat PuppeteerSharp niet heeft:

- **Context** = Geïsoleerde browsersessie (zoals incognito-modus)
- **Blz.** = Een tabblad binnen die context

Contexts laat je:

- Test gelijktijdig met verschillende gebruikerssessies
- Verschillende cookies, rechten of locale per context instellen
- Isoleer tests volledig (zelfs binnen dezelfde browser instantie)

### Hulpmethoden

```csharp
// Navigation with better waiting
protected async Task NavigateAsync(string path)
{
    var url = path.StartsWith("http") ? path : $"{BaseUrl}{path}";
    await Page.GotoAsync(url, new PageGotoOptions
    {
        WaitUntil = WaitUntilState.NetworkIdle
    });
}

// Playwright has better built-in waiting, but these are still useful
protected async Task<IElementHandle?> WaitForSelectorAsync(string selector)
{
    try
    {
        return await Page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions
        {
            State = WaitForSelectorState.Visible
        });
    }
    catch (TimeoutException)
    {
        return null;
    }
}

// Improved element operations with auto-waiting
protected async Task<bool> ElementExistsAsync(string selector)
{
    return await Page.Locator(selector).CountAsync() > 0;
}

protected async Task<string?> GetTextContentAsync(string selector)
{
    var locator = Page.Locator(selector);
    return await locator.TextContentAsync();
}

protected async Task TypeAsync(string selector, string text)
{
    await Page.Locator(selector).FillAsync(text);
}

protected async Task ClickAsync(string selector)
{
    await Page.Locator(selector).ClickAsync();
}
```

### Locators - Playwright's Secret Weapon

Nota van de `Page.Locator()` Dit is anders dan die van PuppeterSharp. `QuerySelector`. Locatoren zijn:

- **Lui** - Ze vragen de DOM pas als je ze gebruikt.
- **Opnieuw proberen** - Ze wachten automatisch en proberen het opnieuw.
- **Strikt** - Ze fout als meerdere elementen overeenkomen (voorkomen schilferige testen)

```csharp
// PuppeteerSharp way (manual waiting)
await Page.WaitForSelectorAsync("#button");
await Page.ClickAsync("#button");

// Playwright way (auto-waiting)
await Page.Locator("#button").ClickAsync(); // Waits automatically!
```

## Multi-browser testen

Hier is waar Playwright schijnt. U kunt dezelfde test uit te voeren in alle browsers:

### Gebruik van xUnit Theory voor Multi-Browser Tests

```csharp
public class CrossBrowserTests : PlaywrightTestBase
{
    public CrossBrowserTests(ITestOutputHelper output) : base(output) { }

    [Theory]
    [InlineData(BrowserType.Chromium)]
    [InlineData(BrowserType.Firefox)]
    [InlineData(BrowserType.Webkit)]
    public async Task HomePage_LoadsCorrectly_InAllBrowsers(BrowserType browserType)
    {
        // Override the browser type for this test
        BrowserType = browserType;
        await InitializeAsync();

        // Arrange & Act
        await NavigateAsync("/");

        // Assert
        var title = await Page.TitleAsync();
        Assert.Contains("Mostlylucid", title);

        Output.WriteLine($"✓ Test passed in {browserType}");
    }
}
```

### Browserspecifieke testklassen

Of maak aparte testklassen voor elke browser:

```csharp
public class ChromiumTests : PlaywrightTestBase
{
    protected override BrowserType BrowserType => BrowserType.Chromium;

    public ChromiumTests(ITestOutputHelper output) : base(output) { }

    [Fact]
    public async Task FilterBar_WorksInChrome()
    {
        await NavigateAsync("/blog");

        await Page.Locator("#LanguageDropDown button").ClickAsync();
        await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();

        var url = Page.Url;
        Assert.Contains("language=es", url);
    }
}

public class FirefoxTests : PlaywrightTestBase
{
    protected override BrowserType BrowserType => BrowserType.Firefox;

    public FirefoxTests(ITestOutputHelper output) : base(output) { }

    [Fact]
    public async Task FilterBar_WorksInFirefox()
    {
        // Same test, different browser!
        await NavigateAsync("/blog");

        await Page.Locator("#LanguageDropDown button").ClickAsync();
        await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();

        var url = Page.Url;
        Assert.Contains("language=es", url);
    }
}

public class SafariTests : PlaywrightTestBase
{
    protected override BrowserType BrowserType => BrowserType.Webkit;

    public SafariTests(ITestOutputHelper output) : base(output) { }

    [Fact]
    public async Task FilterBar_WorksInSafari()
    {
        await NavigateAsync("/blog");

        await Page.Locator("#LanguageDropDown button").ClickAsync();
        await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();

        var url = Page.Url;
        Assert.Contains("language=es", url);
    }
}
```

## Schrijven Tests - De Playwright Way

Laten we eens kijken naar echte voorbeelden die de voordelen van Playwright tonen:

### Testen van HTMX met Auto-Waiting

```csharp
[Fact]
public async Task SortingDropdown_UpdatesContentViaHTMX()
{
    // Arrange
    await NavigateAsync("/blog");

    // Get first post title before sorting
    var firstPostBefore = await Page.Locator("article h2 a").First.TextContentAsync();
    Output.WriteLine($"First post before: {firstPostBefore}");

    // Act - Change sort order
    await Page.Locator("#orderSelect").SelectOptionAsync("date_asc");

    // Playwright automatically waits for HTMX to update the DOM!
    await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);

    // Assert - Content should have changed
    var firstPostAfter = await Page.Locator("article h2 a").First.TextContentAsync();
    Output.WriteLine($"First post after: {firstPostAfter}");

    var selectValue = await Page.Locator("#orderSelect").InputValueAsync();
    Assert.Equal("date_asc", selectValue);
}
```

### Testing Alpine.js Interacties

Playwright-handvatten [Alpine.jsunit synonyms for matching user input](https://alpinejs.dev/) reactiviteit en animaties naadloos:

```csharp
[Fact]
public async Task AlpineDropdown_OpensAndCloses()
{
    await NavigateAsync("/blog");

    // Click to open dropdown (Alpine.js controlled)
    await Page.Locator("#LanguageDropDown button").ClickAsync();

    // Wait for Alpine.js animation
    await Page.WaitForSelectorAsync("#LanguageDropDown div[x-show]", new()
    {
        State = WaitForSelectorState.Visible
    });

    // Check dropdown is visible
    var isVisible = await Page.Locator("#LanguageDropDown div[x-show]").IsVisibleAsync();
    Assert.True(isVisible);

    // Click outside to close
    await Page.Locator("body").ClickAsync(new LocatorClickOptions
    {
        Position = new Position { X = 0, Y = 0 }
    });

    // Verify closed
    await Page.WaitForSelectorAsync("#LanguageDropDown div[x-show]", new()
    {
        State = WaitForSelectorState.Hidden
    });

    isVisible = await Page.Locator("#LanguageDropDown div[x-show]").IsVisibleAsync();
    Assert.False(isVisible);
}
```

### Testformulieren met validatie

```csharp
[Fact]
public async Task CommentForm_ValidatesRequiredFields()
{
    await NavigateAsync("/blog/some-post");

    // Try to submit without filling fields
    await Page.Locator("#comment-submit").ClickAsync();

    // Check HTML5 validation messages
    var nameInput = Page.Locator("#comment-name");
    var validationMessage = await nameInput.EvaluateAsync<string>("el => el.validationMessage");

    Assert.NotEmpty(validationMessage);
    Output.WriteLine($"Validation message: {validationMessage}");

    // Fill form properly
    await nameInput.FillAsync("Test User");
    await Page.Locator("#comment-email").FillAsync("test@example.com");
    await Page.Locator("#comment-content").FillAsync("Great article!");

    // Submit
    await Page.Locator("#comment-submit").ClickAsync();

    // Wait for success
    await Page.Locator(".comment-success").WaitForAsync();
}
```

## Geavanceerde Playwright functies

### Netwerkinterceptie - Krachtiger dan PuppeteerSharp

```csharp
[Fact]
public async Task PageLoadsWithoutImages_ToTestPerformance()
{
    // Block all image requests
    await Page.RouteAsync("**/*.{png,jpg,jpeg,gif,webp}", route => route.AbortAsync());

    await NavigateAsync("/blog");

    // Page should still function without images
    var title = await Page.Locator("h1").TextContentAsync();
    Assert.NotEmpty(title);
}

[Fact]
public async Task MockAPIResponse_ForTesting()
{
    // Intercept API calls and return mock data
    await Page.RouteAsync("**/api/posts", route => route.FulfillAsync(new()
    {
        Status = 200,
        ContentType = "application/json",
        Body = System.Text.Json.JsonSerializer.Serialize(new
        {
            posts = new[]
            {
                new { id = 1, title = "Mock Post 1" },
                new { id = 2, title = "Mock Post 2" }
            }
        })
    }));

    await NavigateAsync("/blog");

    // Should show mock data
    var posts = await Page.Locator(".post-title").CountAsync();
    Assert.Equal(2, posts);
}
```

### Trace Viewer - Debug-testen mislukt

```csharp
public async Task InitializeAsync()
{
    Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
    Browser = await LaunchBrowserAsync();

    // Start tracing
    Context = await Browser.NewContextAsync();
    await Context.Tracing.StartAsync(new()
    {
        Screenshots = true,
        Snapshots = true,
        Sources = true
    });

    Page = await Context.NewPageAsync();
}

public async Task DisposeAsync()
{
    // Save trace on test completion
    var tracePath = Path.Combine("traces", $"{TestContext.TestName}.zip");
    await Context.Tracing.StopAsync(new()
    {
        Path = tracePath
    });

    // View trace with: playwright show-trace traces/TestName.zip

    await Page?.CloseAsync()!;
    await Context?.CloseAsync()!;
    await Browser?.CloseAsync()!;
    Playwright?.Dispose();
}
```

Bekijk dan het spoor:

```bash
playwright show-trace traces/TestName.zip
```

Dit opent een UI tonen:

- Elke actie die je test heeft genomen
- Screenshots bij elke stap
- Netwerkverzoeken
- Console-logs
- DOM snapshots

Het is absoluut briljant voor debuggen.

### Screenshots bij falen

```csharp
public async Task DisposeAsync()
{
    // Take screenshot if test failed
    if (TestContext.CurrentTestOutcome != TestOutcome.Passed)
    {
        var screenshot = await Page.ScreenshotAsync();
        File.WriteAllBytes($"failure-{TestContext.TestName}.png", screenshot);
        Output.WriteLine($"Screenshot saved: failure-{TestContext.TestName}.png");
    }

    await Page?.CloseAsync()!;
    await Context?.CloseAsync()!;
    await Browser?.CloseAsync()!;
    Playwright?.Dispose();
}
```

### Mobiele emulatie

```csharp
[Fact]
public async Task BlogPage_WorksOnMobile()
{
    // Create context with mobile emulation
    var iPhone = Playwright.Devices["iPhone 13"];
    await using var context = await Browser.NewContextAsync(iPhone);
    await using var page = await context.NewPageAsync();

    await page.GotoAsync($"{BaseUrl}/blog");

    // Check mobile menu is visible
    var mobileMenu = page.Locator(".mobile-menu");
    await Expect(mobileMenu).ToBeVisibleAsync();

    // Desktop menu should be hidden
    var desktopMenu = page.Locator(".desktop-menu");
    await Expect(desktopMenu).Not.ToBeVisibleAsync();
}
```

Playwright wordt geleverd met [apparaatdescriptoren](https://playwright.dev/dotnet/docs/emulation#devices) in plaats van:

- iPhone 13, 13 Pro, 12, 11, SE
- iPad, iPad Pro
- Samsung Galaxy, Pixel
- En nog veel meer.

### Donkere modus testen

```csharp
[Fact]
public async Task DarkMode_TogglesCorrectly()
{
    // Start with dark color scheme
    await using var context = await Browser.NewContextAsync(new()
    {
        ColorScheme = ColorScheme.Dark
    });
    await using var page = await context.NewPageAsync();

    await page.GotoAsync($"{BaseUrl}");

    // Check dark mode is active
    var isDark = await page.EvaluateAsync<bool>(
        "() => window.matchMedia('(prefers-color-scheme: dark)').matches"
    );
    Assert.True(isDark);

    // Check background color reflects dark mode
    var bgColor = await page.Locator("body").EvaluateAsync<string>(
        "el => getComputedStyle(el).backgroundColor"
    );
    Assert.Contains("rgb(0, 0, 0)", bgColor); // Dark background
}
```

## Playwright vs PuppeteerSharp - Side by Side

Hier is dezelfde test in beide bibliotheken om de verschillen te tonen:

### PuppeteerSharp-versie

```csharp
[Fact]
public async Task FilterTest_PuppeteerSharp()
{
    await Page.GoToAsync("http://localhost:8080/blog");

    // Manual waiting required
    await Page.WaitForSelectorAsync("#LanguageDropDown button");
    await Page.ClickAsync("#LanguageDropDown button");

    // Wait for dropdown animation
    await Task.Delay(300);

    // Click Spanish option
    await Page.WaitForSelectorAsync("#LanguageDropDown li:nth-child(2) a");
    await Page.ClickAsync("#LanguageDropDown li:nth-child(2) a");

    // Wait for navigation
    await Task.Delay(500);

    // Check URL
    var url = Page.Url;
    Assert.Contains("language=", url);
}
```

### Playwright-versie

```csharp
[Fact]
public async Task FilterTest_Playwright()
{
    await Page.GotoAsync("http://localhost:8080/blog");

    // Auto-waiting built in
    await Page.Locator("#LanguageDropDown button").ClickAsync();

    // Click Spanish option (waits automatically for visibility)
    await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();

    // Check URL (waits automatically for navigation)
    await Expect(Page).ToHaveURLAsync(new Regex(".*language=.*"));
}
```

Opmerking:

- Geen handleiding `WaitForSelectorAsync` nodig
- Nee `Task.Delay` nodig
- Schonere beweringen met `Expect`
- Meer leesbaar met `has-text` selector

## PDF-generatie met Playwright

Net als PuppeteerSharp kan Playwright PDF's genereren. De API is bijna identiek:

```csharp
public async Task<byte[]> GeneratePdfAsync(string url)
{
    await using var browser = await Playwright.Chromium.LaunchAsync();
    await using var page = await browser.NewPageAsync();
    await page.GotoAsync(url);

    return await page.PdfAsync(new()
    {
        Format = "A4",
        PrintBackground = true,
        Margin = new()
        {
            Top = "20mm",
            Right = "20mm",
            Bottom = "20mm",
            Left = "20mm"
        }
    });
}
```

Hetzelfde [PDF kreegchas uit het PuppeteerSharp artikel](/blog/puppeteersharp-e2e-testing#pdf-generation-gotchas) Hier ook toepassen.

## Wanneer Playwright kiezen

```mermaid
graph TD
    A[Need E2E Testing?] --> B{Multi-browser required?}
    B -->|Yes| C[Playwright]
    B -->|No| D{Chrome only?}
    D -->|Yes| E[PuppeteerSharp or Playwright]
    D -->|No| C

    C --> F[Benefits]
    F --> F1[Test all major browsers]
    F --> F2[Better debugging tools]
    F --> F3[More reliable waiting]
    F --> F4[Mobile emulation]

    E --> G[PuppeteerSharp Benefits]
    G --> G1[Simpler setup]
    G --> G2[Smaller overhead]
    G --> G3[Chromium focused]

    style C stroke:#00aa00,stroke-width:3px
    style E stroke:#0066cc,stroke-width:2px
```

**Kies Playwright wanneer:**

- U moet multi-browser testen
- U wilt betere debuggereedschappen (trace viewer)
- U test complexe moderne webapps
- Je hebt mobiele emulatie nodig.
- U wilt meer betrouwbare testen uit de doos

**Kies PuppeteerSharp wanneer:**

- Alleen Chrome/Edge is prima
- U wilt eenvoudiger instellen
- Je bent al bekend met Puppeteer.
- Je hebt iets minder overhead nodig.

## Uitvoeren van CI/CD

### Voorbeeld van GitHub-acties

Playwright werkt naadloos in CI/CD pijpleidingen. Hier is een compleet [GitHub-acties](https://github.com/features/actions) workflow:

```yaml
name: Playwright Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

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

    - name: Install dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --no-restore

    - name: Install Playwright browsers
      run: pwsh Mostlylucid.Test/bin/Debug/net9.0/playwright.ps1 install --with-deps

    - name: Start application
      run: |
        dotnet run --project Mostlylucid/Mostlylucid.csproj &
        echo $! > app.pid

    - name: Wait for application
      run: |
        timeout 60 bash -c 'until curl -f http://localhost:8080/health; do sleep 2; done'

    - name: Run Playwright tests
      run: |
        dotnet test Mostlylucid.Test/Mostlylucid.Test.csproj \
          --filter "Category=E2E" \
          --logger "console;verbosity=detailed"

    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v3
      with:
        name: playwright-results
        path: |
          **/test-results/
          **/traces/

    - name: Stop application
      if: always()
      run: kill $(cat app.pid) || true
```

### Ondersteuning voor dockers

Playwright testen uitvoeren in [Docker](https://www.docker.com/) vereist installatie systeem afhankelijkheden:

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

# Install Playwright dependencies
RUN apt-get update && apt-get install -y \
    libnss3 \
    libnspr4 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libxkbcommon0 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    libasound2

WORKDIR /app
COPY . .

RUN dotnet restore
RUN dotnet build
RUN pwsh Mostlylucid.Test/bin/Debug/net9.0/playwright.ps1 install

CMD ["dotnet", "test"]
```

## Prestatievergelijking

Van mijn testen op deze blog:

PuppeteerSharp Playwright
|---------|---------------|------------|
| **Chrome-testsnelheid** ~2.5's ~2.8's
| **Multi-browser** * * * * *  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
| **Betrouwbaarheid automatisch wachten** Uitstekend (automatisch)
| **Geheugen per browser** 150 MB 180 MB
| **Complexheid instellen** Eenvoudig, voluit
| **Hulpmiddelen voor debuggen** Chrome DevTools Trace Viewer + DevTools
| **Mobiele emulatie** Basic Uitstekend

Playwright is iets langzamer en gebruikt iets meer geheugen, maar de betrouwbaarheid en debugging voordelen wegen meestal op tegen dit.

## Gewone HebbesCity in New Jersey USA

### 1. Strikte modus

Playwright's locators zijn standaard streng:

```csharp
// This errors if multiple buttons exist
await Page.Locator("button").ClickAsync();

// Be specific
await Page.Locator("button#submit").ClickAsync();

// Or use .First if you really want the first match
await Page.Locator("button").First.ClickAsync();
```

### 2. Browserinstallatie

Vergeet niet te rennen. `playwright install` na het toevoegen van het pakket. De browsers zijn niet opgenomen in het NuGet pakket.

### 3. Context vs Page

Onthoud contexten isoleren testen:

```csharp
// Bad - state leaks between tests
await Page.GotoAsync("/login");
// Do stuff...
await Page.GotoAsync("/dashboard");
// Previous login state might still exist

// Good - fresh context per test
await using var context = await Browser.NewContextAsync();
await using var page = await context.NewPageAsync();
// Completely isolated
```

### 4. Webkit verschillen

Safari (WebKit) kan zich anders gedragen:

```csharp
// May work in Chrome but fail in WebKit
await Page.Locator(".dropdown").Hover();
await Page.Locator(".dropdown-item").ClickAsync();

// More reliable across browsers
await Page.Locator(".dropdown").ClickAsync();
await Page.WaitForSelectorAsync(".dropdown-item");
await Page.Locator(".dropdown-item").ClickAsync();
```

## Conclusie

Playwright vertegenwoordigt de evolutie van browserautomatisering voor .NET ontwikkelaars. Als u multi-browser ondersteuning nodig heeft, is het de duidelijke keuze. Ja, het is iets complexer dan [PuppeterSharp](/blog/puppeteersharp-e2e-testing), maar de voordelen zijn aanzienlijk:

- Test in alle grote browsers met identieke code
- Betere betrouwbaarheid met automatisch wachten
- Uitstekend debuggen met trace viewer
- Mobiele en tablet emulatie uit de doos
- Meer bestand tegen timing problemen

**Mijn aanbeveling:**

- **Nieuwe projecten die cross-browser testen nodig hebben**: Begin met Playwright
- **Alleen Chrome-projecten**: PuppeteerSharp is eenvoudiger
- **Bestaande PuppeteerSharp-projecten**: Migreren alleen als u multi-browser nodig hebt
- **Complexe moderne webapps**: Playwright's gereedschap is de overhead waard

De trace viewer alleen al heeft me uren van debuggen bespaard. In staat zijn om een mislukte test opnieuw af te spelen en precies te zien wat de browser zag bij elke stap is van onschatbare waarde.

Geef Playwright een go op uw volgende project - je zult aangenaam verrast zijn hoe veel gemakkelijker het maakt cross-browser testen.

## Meer lezen

- [Playwright voor .NET Documentatie](https://playwright.dev/dotnet/)
- [Playwright API-referentie](https://playwright.dev/dotnet/docs/api/class-playwright)
- [PuppeteerSharp vs Playwright](/blog/puppeteersharp-e2e-testing)
- [Trace Viewer Guide](https://playwright.dev/dotnet/docs/trace-viewer)
- [Browsercontext uitgelegd](https://playwright.dev/dotnet/docs/browser-contexts)