# End-to-End Testing with PuppeterSharp - Ett riktigt alternativ till Selenium

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
Modern E2E (End-To-End, använda din webbplats som användare skulle) testning behöver inte vara smärtsam. Denna omfattande guide visar dig hur du använder PuppeterSharp för snabb, tillförlitlig webbläsarautomation i .NET-täcka allt från grundläggande testning till PDF-generering och webbskrapning. Medan Microsofts Playwright är den modernare multi-browser lösning, Jag valde PuppeterSharp för den här bloggen eftersom det är vad jag visste och Chrome-bara testning var tillräckligt för mina behov. Om du behöver Firefox och Safari stöd, kolla in min [Uppspelningsguide](/blog/playwright-e2e-testing) istället.

## Inledning

Om du någonsin har jobbat med [Med en tjocklek av mer än 0,15 mm men högst 0,15 mm](https://www.selenium.dev/) För end-to-end testing, du vet att det kan vara en rätt smärta på baksidan. Mellan brottning med förare versioner, hantera flagnande tester som fungerar på din maskin men ingen annanstans, och den allmänna tröghet i WebDriver protokollet, det är tillräckligt för att få dig att vilja kasta allt i och testa manuellt i stället.

Ange [PuppeterSharp](https://www.puppeteersharp.com/) - .NET-porten för Googles [PuppeterName](https://pptr.dev/) Det är som Seleniums yngre, snabbare kusin som faktiskt bryr sig om att dyka upp i tid och inte kräver att du hämtar sjutton olika webbläsardrivrutiner.

I denna artikel, Jag ska gå igenom hur jag har genomfört PuppeterSharp för E2E testning på denna blogg, komplett med verkliga kod exempel från repo. Vi ska täcka testning, PDF-generering, webbskrapning, och jämföra det med alternativen.

[TOC]

## Vad är PuppeterSharp då?

[PuppeterSharp](https://www.puppeteersharp.com/) är ett .NET-bibliotek som tillhandahåller en hög nivå API för att styra Chrome eller krom webbläsare med hjälp av [Chrome DevTools-protokollet](https://chromedevtools.github.io/devtools-protocol/). Till skillnad från [Med en tjocklek av mer än 0,15 mm men högst 0,15 mm](https://www.selenium.dev/), som använder [WebDriver- protokoll](https://www.w3.org/TR/webdriver/) (en ganska klumpig HTTP-baserad JSON wire protokoll), PuppeteerSharp talar direkt till webbläsaren via DevTools.

Tänk på det så här:

- **Med en tjocklek av mer än 0,15 mm men högst 0,15 mm**: Som att skicka brev genom inlägget för att kommunicera med din webbläsare
- **PuppeterSharp**: Som att ha en direkt telefonlinje till webbläsarens hjärna

```mermaid
graph LR
    A[Test Code] -->|WebDriver Protocol| B[Selenium]
    B -->|JSON Wire Protocol| C[Browser Driver]
    C -->|Commands| D[Browser]

    E[Test Code] -->|DevTools Protocol| F[PuppeteerSharp]
    F -->|Direct Connection| G[Chrome/Chromium]

    style A stroke:#333,stroke-width:2px
    style E stroke:#333,stroke-width:2px
    style F stroke:#0066cc,stroke-width:3px
    style G stroke:#0066cc,stroke-width:3px
```

### Där E2E-testning passar in i din teststrategi

Innan vi dyker djupare, låt oss tala om var E2E-testning passar in i det stora schemat av saker. Du har förmodligen hört talas om testpyramid - här är hur det faktiskt fungerar i praktiken:

```mermaid
graph TB
    subgraph "Testing Pyramid"
        E2E[E2E Tests<br/>Few, Slow, High Confidence<br/>Test full user journeys]
        INT[Integration Tests<br/>Medium number, Medium speed<br/>Test component interactions]
        UNIT[Unit Tests<br/>Many, Fast, Low Cost<br/>Test individual functions]
    end

    subgraph "Trade-offs"
        SPEED[Speed]
        CONF[Confidence]
        COST[Cost]
    end

    subgraph "When to Use E2E"
        W1[Critical user journeys<br/>e.g. checkout, login]
        W2[Cross-browser compatibility]
        W3[JavaScript-heavy UIs]
        W4[Complex user interactions]
    end

    E2E -.->|Slow but high confidence| CONF
    INT -.->|Balanced| SPEED
    UNIT -.->|Fast and cheap| SPEED

    E2E -.->|Expensive to run| COST
    UNIT -.->|Cheap to run| COST

    style E2E stroke:#cc0000,stroke-width:3px
    style INT stroke:#ff9900,stroke-width:2px
    style UNIT stroke:#00aa00,stroke-width:2px
    style CONF stroke:#0066cc,stroke-width:2px
    style SPEED stroke:#00aa00,stroke-width:2px
    style COST stroke:#cc0000,stroke-width:2px
```

**Verklighetens kontroll:**

- **Enhetstester** (80% av dina tester): Snabba, billiga, testa individuella funktioner. Men de berättar inte om systemet faktiskt fungerar som en helhet.
- **Integreringstest** (15% av dina tester): Testa hur olika delar fungerar tillsammans. Snabbare än E2E men testa inte hela UI.
- **E2E-provningar** (5% av dina tester): Långsamt, dyrt, men testa systemet exakt som användarna upplever det. Det är här PuppeterSharp lyser.

**När du behöver E2E-test:**

1. **Kritiska användarresor** Inloggning, utcheckning, betalningshantering.
2. **JavaScript-tunga användargränssnitt** - Moderna SPA ([Reagera](https://react.dev/), [Vy](https://vuejs.org/), [Angulära](https://angular.dev/)) där användargränssnittet görs till kundsida.
3. **Gränsöverskridande frågor** - Olika webbläsare gör saker annorlunda (men med PuppeterSharp är du bara Chrome).
4. **Komplexa interaktioner** - Flerstegsguider, dra och släpp, filuppladdningar.

**När du inte behöver E2E-test:**

1. **Enkla CRUD-transaktioner** - Integreringstest räcker.
2. **Ren logik** - Det är vad enhetstester är till för.
3. **Varje kantfodral** - E2E-tester är för långsamma och dyra för uttömmande tester.

### Varför PuppeterSharp över Selenium?

Låt mig räkna sätt:

1. **Ingen drivrutinshantering Faff**: PuppeterSharp hämtar och hanterar webbläsaren Chrome åt dig. Inget mer mucking om med ChromeDriver versioner som inte matchar din installerade Chrome-version.

2. **Snabbare körning**: DevTools Protocol är betydligt snabbare än WebDriver. Dina tester kommer att gå snabbare, och du kommer att spendera mindre tid väntar på saker att hända.

3. **Bättre API**: API är mer modern och intuitiv. Det är async / avänta hela vägen ner, som passar vackert med modern .NET-utveckling.

4. **Inbyggd skärmdump och PDF-generering**: Vill du ha en skärmbild när ett test misslyckas? Det är helt enkelt med PuppeterSharp.

5. **Interceptera nätverksförfrågningar**: Du kan snappa upp, ändra eller blockera nätverksförfrågningar med lätthet - lysande för att testa offline-scenarier eller håna API-svar.

6. **Korrekt JavaScript- körning**: Kör JavaScript i sidsammanhang och få resultat tillbaka på ett sätt som inte gör att du vill gråta.

## Ställa in PuppeterSharp

Lägg först till [PuppeterSharp](https://www.nuget.org/packages/PuppeteerSharp) NuGet-paket:

```bash
dotnet add package PuppeteerSharp
```

Här är min testprojektkonfiguration (`Mostlylucid.Test/Mostlylucid.Test.csproj:23`):

```xml
<PackageReference Include="PuppeteerSharp" Version="20.2.4" />
<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>
```

Jag använder [xUnit](https://xunit.net/) (ASP.NET Core standard), men PuppeteerSharp fungerar lika bra med [Nunit Ordförande](https://nunit.org/) eller [MSTest Ordförande](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest).

## Skapa en testklass i basen

I stället för att upprepa setup/inardown-kod i varje test, har jag skapat en basklass (`Mostlylucid.Test/E2E/E2ETestBase.cs:12`) som hanterar hantering av webbläsares livscykel:

### Klassstrukturen

```csharp
using PuppeteerSharp;
using Xunit.Abstractions;

namespace Mostlylucid.Test.E2E;

public abstract class E2ETestBase : IAsyncLifetime
{
    protected readonly ITestOutputHelper Output;
    protected IBrowser Browser = null!;
    protected IPage Page = null!;

    protected const string BaseUrl = "http://localhost:8080";
    protected const int DefaultTimeout = 30000;

    protected E2ETestBase(ITestOutputHelper output)
    {
        Output = output;
    }
```

Vi genomför [IAsyncLivstid](https://xunit.net/docs/shared-context#async-lifetime) från xUnit, som tillhandahåller async setup/teardown. Till skillnad från traditionella konstruktörer, gör detta att vi på rätt sätt väntar på webbläsarens initiering.

### Initiering av webbläsare

```csharp
    public async Task InitializeAsync()
    {
        // Download Chromium on first run
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

        // Launch browser with sensible defaults
        Browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true, // Set false for debugging
            DefaultViewport = new ViewPortOptions
            {
                Width = 1400,
                Height = 900
            },
            Args = new[]
            {
                "--no-sandbox",
                "--disable-setuid-sandbox"
            }
        });

        Page = await Browser.NewPageAsync();
        Page.DefaultTimeout = DefaultTimeout;
    }
```

och `BrowserFetcher` Hämtar automatiskt en kompatibel Chromium-version vid första körningen - ingen manuell drivrutinshantering behövs. `--no-sandbox` Flaggningar krävs för Docker/CI-miljöer.

### Rengöring

```csharp
    public async Task DisposeAsync()
    {
        if (Page != null) await Page.CloseAsync();
        if (Browser != null) await Browser.CloseAsync();
    }
}
```

Korrekt bortskaffande är viktigt för att undvika minnesläckage. Varje webbläsarinstans använder 100-200 MB RAM.

## Metoder för hjälp

Basklassen innehåller hjälpmetoder för att minska pannplattan (`Mostlylucid.Test/E2E/E2ETestBase.cs:72-172`):

```csharp
// Navigation with automatic network idle waiting
protected async Task NavigateAsync(string path)
{
    var url = path.StartsWith("http") ? path : $"{BaseUrl}{path}";
    await Page.GoToAsync(url, new NavigationOptions
    {
        WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
    });
}

// Safe element waiting with timeout handling
protected async Task<IElementHandle?> WaitForSelectorAsync(string selector, int timeout = 5000)
{
    try
    {
        return await Page.WaitForSelectorAsync(selector, new WaitForSelectorOptions
        {
            Timeout = timeout,
            Visible = true
        });
    }
    catch (WaitTaskTimeoutException)
    {
        return null; // Graceful degradation
    }
}

// Common element operations
protected async Task<bool> ElementExistsAsync(string selector) =>
    await Page.QuerySelectorAsync(selector) != null;

protected async Task<string?> GetTextContentAsync(string selector)
{
    var element = await Page.QuerySelectorAsync(selector);
    return element == null ? null :
        await Page.EvaluateFunctionAsync<string>("el => el.textContent", element);
}

protected async Task TypeAsync(string selector, string text, int delay = 50)
{
    await Page.WaitForSelectorAsync(selector);
    await Page.TypeAsync(selector, text, new TypeOptions { Delay = delay });
}

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

Dessa hanterar de tråkiga bitarna - väntar på element att existera, graciös timeout hantering, och automatisk loggning för när tester misslyckas i CI.

## Skriva faktiska tester

Okej, låt oss komma till de bra sakerna - skriva faktiska tester. Här är ett riktigt test från min blogg filter bar funktionalitet (`Mostlylucid.Test/E2E/FilterBarTests.cs:20-50`):

```csharp
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_LanguageDropdown_ShowsLanguages()
{
    // Arrange
    await NavigateAsync("/blog");

    // Act - Click the language dropdown button
    var dropdownButton = await WaitForSelectorAsync("#LanguageDropDown button");
    Assert.NotNull(dropdownButton);

    await ClickAsync("#LanguageDropDown button");
    await WaitAsync(300);

    // Assert - Dropdown menu should be visible with language options
    var dropdownOpen = await EvaluateFunctionAsync<bool>(@"() => {
        const dropdown = document.querySelector('#LanguageDropDown div[x-show]');
        if (!dropdown) return false;
        const style = window.getComputedStyle(dropdown);
        return style.display !== 'none';
    }");

    Assert.True(dropdownOpen, "Language dropdown should be open");

    // Check that English option exists
    var hasEnglish = await EvaluateFunctionAsync<bool>(@"() => {
        const options = document.querySelectorAll('#LanguageDropDown li a');
        return Array.from(options).some(opt => opt.textContent.toLowerCase().includes('english'));
    }");

    Assert.True(hasEnglish, "Language dropdown should contain English option");
    Output.WriteLine("✅ Language dropdown shows languages correctly");
}
```

Detta test kontrollerar att mitt språk dropdown fungerar korrekt. Låt oss titta på vad som får det att ticka:

### Hoppa över attributet

```csharp
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
```

Jag har hoppat över detta test som standard eftersom det kräver att webbplatsen körs lokalt. För E2E tester, du vanligtvis vill köra dem on-demand snarare än med varje bygge. Du kan packa upp dem när du är redo att köra dem, eller köra dem i en separat CI jobb där du har webbplatsen spunnen.

### Kör JavaScript

```csharp
var dropdownOpen = await EvaluateFunctionAsync<bool>(@"() => {
    const dropdown = document.querySelector('#LanguageDropDown div[x-show]');
    if (!dropdown) return false;
    const style = window.getComputedStyle(dropdown);
    return style.display !== 'none';
}");
```

Detta är ett av de områden där PuppeteerSharp absolut lyser. `EvaluateFunctionAsync` metoden låter dig köra JavaScript i webbläsaren sammanhang och få resultatet tillbaka som en riktig .NET-typ. I detta fall, Jag kontrollerar om en dropdown är faktiskt synlig (inte bara närvarande i DOM) genom att titta på dess beräknade stilar.

Jämför detta med Selenium där du behöver:

1. Hitta elementet
2. Hämta dess visningsegenskaper
3. Tolkning av strängresultatet
4. Hoppas det inte är gammalt när du kollar det.

### Test av HTMX-interaktioner

Min blogg använder [HTMX Ordförande](https://htmx.org/) I stor utsträckning (server-side rendering utan att skriva JavaScript). Här är ett test som kontrollerar sorteringsfunktionalitet (`Mostlylucid.Test/E2E/FilterBarTests.cs:98-126`):

```csharp
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_SortOrder_ChangesPostOrder()
{
    // Arrange
    await NavigateAsync("/blog");

    // Get the first post title before sorting
    var firstPostBefore = await EvaluateFunctionAsync<string>(@"() => {
        const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
        return postLink?.textContent?.trim() || '';
    }");
    Output.WriteLine($"First post before sort: {firstPostBefore}");

    // Act - Change sort order to "Oldest first"
    await Page.SelectAsync("#orderSelect", "date_asc");
    await WaitAsync(1000); // Wait for HTMX to update

    // Assert - Post order should have changed
    var firstPostAfter = await EvaluateFunctionAsync<string>(@"() => {
        const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
        return postLink?.textContent?.trim() || '';
    }");
    Output.WriteLine($"First post after sort: {firstPostAfter}");

    var selectValue = await EvaluateFunctionAsync<string>("() => document.querySelector('#orderSelect')?.value");
    Assert.Equal("date_asc", selectValue);
    Output.WriteLine("✅ Sort order selection works correctly");
}
```

Nyckeln här är `await WaitAsync(1000)` Efter att ha ändrat valvärdet. HTMX behöver ett ögonblick för att göra sin begäran och uppdatera DOM. I en perfekt värld, skulle vi vänta på en specifik nätverksbegäran att slutföra, men för enkla fall, en kort fördröjning är bra.

### Test av responsiv design

Här är ett fräckt test som kontrollerar att min filter bar är ordentligt gömd på mobila enheter (`Mostlylucid.Test/E2E/FilterBarTests.cs:216-245`):

```csharp
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_ResponsiveDesign_HiddenOnMobile()
{
    // Arrange - Set mobile viewport
    await Page.SetViewportAsync(new ViewPortOptions
    {
        Width = 375,
        Height = 667
    });

    await NavigateAsync("/blog");
    await WaitAsync(500);

    // Assert - Filter bar should be hidden on mobile
    var filterBarVisible = await EvaluateFunctionAsync<bool>(@"() => {
        const filterBar = document.querySelector('.hidden.lg\\:flex');
        if (!filterBar) return true;
        const rect = filterBar.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }");

    Assert.False(filterBarVisible, "Filter bar should be hidden on mobile viewport");
    Output.WriteLine("✅ Filter bar correctly hidden on mobile");

    // Reset viewport
    await Page.SetViewportAsync(new ViewPortOptions
    {
        Width = 1400,
        Height = 900
    });
}
```

Du kan ändra visningen när som helst, vilket är lysande för att testa responsiva layouter. Mycket enklare än att ändra storlek på webbläsarens fönster manuellt!

## Avancerade PuppeterSharp-funktioner

### Nätverksinterception

En av mina favoritfunktioner är möjligheten att avlyssna och ändra nätverksförfrågningar. Detta är ovärderligt för att testa feltillstånd eller offline-scenarier:

```csharp
await Page.SetRequestInterceptionAsync(true);

Page.Request += async (sender, e) =>
{
    // Block all image requests to speed up tests
    if (e.Request.ResourceType == ResourceType.Image)
    {
        await e.Request.AbortAsync();
    }
    // Mock API responses
    else if (e.Request.Url.Contains("/api/posts"))
    {
        await e.Request.RespondAsync(new ResponseData
        {
            Status = HttpStatusCode.OK,
            ContentType = "application/json",
            Body = "{\"posts\": []}"
        });
    }
    else
    {
        await e.Request.ContinueAsync();
    }
};
```

### Ta skärmdumpar

När ett test misslyckas är en skärmdump värd tusen loggmeddelanden:

```csharp
try
{
    // Your test code here
    await Page.ClickAsync("#someButton");
}
catch (Exception)
{
    // Take a screenshot on failure
    await Page.ScreenshotAsync("test-failure.png");
    throw; // Re-throw to fail the test
}
```

### PDF- generering

Du kan även generera PDF-filer av sidor, som är användbart för att testa server-side rendering eller skriva ut stilmallar:

```csharp
await Page.PdfAsync("page.pdf", new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true
});
```

### Kodtäckning

PuppeterSharp kan även samla JavaScript kod täckningsdata:

```csharp
await Page.Coverage.StartJSCoverageAsync();
await Page.GoToAsync("http://localhost:8080");

var coverage = await Page.Coverage.StopJSCoverageAsync();
var totalBytes = coverage.Sum(c => c.Text.Length);
var usedBytes = coverage.Sum(c => c.Ranges.Sum(r => r.End - r.Start));
var percentUsed = usedBytes / (double)totalBytes * 100;

Output.WriteLine($"JavaScript coverage: {percentUsed:F2}%");
```

## PuppeterSharp mot tävlingen

Låt oss ta en ordentlig titt på hur PuppeterSharp staplar upp mot andra E2E-testverktyg:

```mermaid
graph TD
    A[E2E Testing Tools] --> B[Selenium WebDriver]
    A --> C[PuppeteerSharp]
    A --> D[Playwright]
    A --> E[Cypress]

    B --> B1[❌ Slow WebDriver protocol]
    B --> B2[❌ Driver management hassle]
    B --> B3[✅ Multi-browser support]
    B --> B4[✅ Mature ecosystem]

    C --> C1[✅ Fast DevTools protocol]
    C --> C2[✅ Auto browser management]
    C --> C3[❌ Chrome/Chromium only]
    C --> C4[✅ Great .NET integration]

    D --> D1[✅ Fast DevTools protocol]
    D --> D2[✅ Auto browser management]
    D --> D3[✅ Multi-browser support]
    D --> D4[⚠️ Newer to .NET ecosystem]

    E --> E1[✅ Great developer experience]
    E --> E2[❌ JavaScript only]
    E --> E3[❌ Not for .NET]
    E --> E4[✅ Excellent documentation]

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

### Selenium WebDriver

**Det gamla gardet**

Selen har funnits sedan 2004 och det visar. Det är mogen, väldokumenterad, och stöder varje webbläsare under solen. Men det visar också sin ålder:

**Förmåner:**

- Stöder alla webbläsare (Chrome, Firefox, Safari, Edge, IE om du är en masochist)
- Massiv ekosystem av verktyg och utökningar
- Välkänd och allmänt antagen
- Bra för kors-browser-testning

**Minus:**

- WebDriver-protokollet är långsamt
- Driver management är en smärta (även om WebDriverManager hjälper)
- API känns daterat jämfört med moderna alternativ
- Flaky tester är vanliga på grund av timing frågor
- Ingen inbyggd nätverksavlyssning

**När du ska använda den:** När du absolut behöver testa över flera webbläsare, eller när du redan är investerad i Selenium ekosystem.

### Uppspelningsrätt

**Det nya barnet på blocket**

[Uppspelningsrätt](https://playwright.dev/) är Microsofts svar på Puppeter, med [Stöd för .NET](https://playwright.dev/dotnet/) bakad från början. Det är i huvudsak PuppeterSharp men med multi-browser stöd:

**Förmåner:**

- Stöder Chrome, Firefox, Safari (Webkit)
- Modernt API som liknar Puppeter
- Laddar ner webbläsare automatiskt
- Inbyggd nätverksavlyssning, skärmdumpar, etc.
- Utmärkt stöd för .NET

**Minus:**

- Nyare, mindre ekosystem
- Kan vara överdöda om du bara behöver Chrome
- Något mer komplex inställning på grund av stöd för multi-browser

**När du ska använda den:** När du behöver multi-browser stöd men vill ha en modern API. Om du startar ett nytt projekt och behöver kors-browser testning, Playwright är förmodligen din bästa insats.

### Cypress Ordförande

**JavaScript-utvecklarens älskling**

Cypress är lysande om du arbetar i JavaScript/TypeScript, men det är en icke-startare för .NET-utvecklare:

**Förmåner:**

- Fantastisk erfarenhet av utvecklare
- Avlusning av tidsresor
- Automatisk väntan
- Stor dokumentation

**Minus:**

- Endast JavaScript/TypeScript
- Inget stöd för .NET
- Kan inte testa flera flikar eller fönster
- Begränsat till att testa din egen applikation (ingen testning över domäner)

**När du ska använda den:** Håll dig till nåt som integrerar med din tech stack.

### Vad bör du då använda?

Här är min tagning:

```mermaid
graph TD
    A[What E2E tool?] --> B{Need multi-browser testing?}
    B -->|Yes| C{Starting new project?}
    B -->|No| D[PuppeteerSharp]

    C -->|Yes| E[Playwright]
    C -->|No| F{Invested in Selenium?}

    F -->|Yes| G[Stick with Selenium]
    F -->|No| E

    D --> H[✅ Fast, simple, reliable]
    E --> I[✅ Modern, flexible]
    G --> J[⚠️ Consider migrating]

    style D stroke:#0066cc,stroke-width:3px
    style H stroke:#00aa00,stroke-width:2px
```

För de flesta .NET-utvecklare bygga moderna webbapplikationer:

- **Bara Chrome-testning?** → PuppeterSharp
- **Flerbrowser-testning?** → Spelawright
- **Har du redan använt Selenium?** → Överväg att migrera till Playwright, men inte rusa det

## Körprov i CI/CD

E2E tester är alla bra och bra på din lokala maskin, men de måste köras i CI / CD rörledningar också. Så här har jag satt upp saker för [GitHub-åtgärder](https://github.com/features/actions):

```yaml
name: E2E Tests

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

jobs:
  e2e-tests:
    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: Start application
      run: |
        dotnet run --project Mostlylucid/Mostlylucid.csproj &
        echo $! > app.pid

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

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

    - name: Upload screenshots on failure
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: test-screenshots
        path: '**/test-failure-*.png'

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

Nyckelbitarna:

1. Starta ansökan i bakgrunden
2. Vänta tills det är hälsosamt (med hjälp av en hälsokontroll endpoint)
3. Kör E2E-testen
4. Ladda upp skärmdumpar om några tester misslyckas
5. Stoppa alltid programmet, även om tester misslyckas

## Vanliga fallgropar och hur man undviker dem

### Flaky-tester

E2E tester kan vara flagnande - de passerar ibland och misslyckas andra. Detta är oftast ner till timing frågor. Här är hur man undviker dem:

**Dåligt.**

```csharp
await Page.ClickAsync("#button");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);
```

**Bra.**

```csharp
await Page.ClickAsync("#button");
await Page.WaitForSelectorAsync("#result");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);
```

Vänta alltid på att det element du ska interagera med ska existera och vara synligt.

### Testisolering

Varje test bör vara helt oberoende. Lita inte på tillstånd från tidigare tester:

**Dåligt.**

```csharp
[Fact]
public async Task Test1_Login()
{
    await LoginAsync("user", "password");
    // User is now logged in for subsequent tests
}

[Fact]
public async Task Test2_ViewDashboard()
{
    // Assumes user is still logged in from Test1
    await NavigateAsync("/dashboard");
}
```

**Bra.**

```csharp
[Fact]
public async Task Test1_Login()
{
    await LoginAsync("user", "password");
    await LogoutAsync(); // Clean up
}

[Fact]
public async Task Test2_ViewDashboard()
{
    await LoginAsync("user", "password"); // Set up needed state
    await NavigateAsync("/dashboard");
    await LogoutAsync(); // Clean up
}
```

### Sidobjektsmönster

För komplexa sidor, använd mönstret Page Object för att hålla dina tester underhållbara:

```csharp
public class BlogPageObject
{
    private readonly IPage _page;

    public BlogPageObject(IPage page)
    {
        _page = page;
    }

    public async Task SelectLanguageAsync(string language)
    {
        await _page.ClickAsync("#LanguageDropDown button");
        await _page.WaitAsync(300);
        await _page.ClickAsync($"#LanguageDropDown a:has-text('{language}')");
    }

    public async Task<string[]> GetPostTitlesAsync()
    {
        return await _page.EvaluateFunctionAsync<string[]>(@"() => {
            return Array.from(document.querySelectorAll('.post-title'))
                        .map(el => el.textContent.trim());
        }");
    }
}

// Usage in tests
[Fact]
public async Task Can_Filter_By_Language()
{
    var blogPage = new BlogPageObject(Page);
    await NavigateAsync("/blog");

    await blogPage.SelectLanguageAsync("Spanish");
    var titles = await blogPage.GetPostTitlesAsync();

    Assert.All(titles, title => Assert.NotEmpty(title));
}
```

## Prestandaöverväganden

E2E tester är långsammare än enhet tester, det finns ingen att komma runt det. Men du kan göra dem snabbare:

### Kör tester parallellt

xUnit kör tester parallellt som standard, men du måste vara försiktig med delat tillstånd:

```csharp
[Collection("E2E Tests")] // Tests in same collection run sequentially
public class FilterBarTests : E2ETestBase
{
    // Tests here share resources
}

[Collection("Blog Tests")] // Different collection runs in parallel
public class BlogTests : E2ETestBase
{
    // Tests here run in parallel with FilterBarTests
}
```

### Inaktivera onödiga funktioner

Snabba upp tester genom att inaktivera funktioner du inte behöver:

```csharp
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = true,
    Args = new[]
    {
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage", // Overcome limited resource problems
        "--disable-accelerated-2d-canvas",
        "--disable-gpu", // Not needed for headless
        "--disable-images", // Don't load images if you don't need them
        "--disable-javascript", // Only if testing static content
    }
});
```

### Använd nätverksinterception på ett förståndigt sätt

Blockera onödiga resurser för att påskynda saker och ting:

```csharp
await Page.SetRequestInterceptionAsync(true);
Page.Request += async (sender, e) =>
{
    var blockedResourceTypes = new[]
    {
        ResourceType.Image,
        ResourceType.Media,
        ResourceType.Font,
        ResourceType.StyleSheet // If you don't need to test styling
    };

    if (blockedResourceTypes.Contains(e.Request.ResourceType))
    {
        await e.Request.AbortAsync();
    }
    else
    {
        await e.Request.ContinueAsync();
    }
};
```

## Felsökning av E2E-tester

När tester misslyckas (och de kommer), måste du felsöka dem. Här är några tekniker:

### Kör i icke- huvudlöst läge

Ställ in `Headless = false` för att titta på webbläsaren i funktion:

```csharp
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = false,
    SlowMo = 100, // Slow down by 100ms to see what's happening
});
```

### Använd DevTools

Du kan faktiskt öppna DevTools programmatiskt:

```csharp
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = false,
    Devtools = true, // Auto-open DevTools
});
```

### Konsolloggning

Fånga konsolmeddelanden från webbläsaren:

```csharp
Page.Console += (sender, e) =>
{
    Output.WriteLine($"Browser console: {e.Message.Text}");
};
```

### Begär loggning

Logga alla nätverksförfrågningar:

```csharp
Page.Request += (sender, e) =>
{
    Output.WriteLine($"Request: {e.Request.Method} {e.Request.Url}");
};

Page.Response += (sender, e) =>
{
    Output.WriteLine($"Response: {e.Response.Status} {e.Response.Url}");
};
```

## Verkliga världens testmönster

Här är några mönster som jag använder regelbundet i mina E2E tester:

### Testformulär för inlämningar

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

    // Fill in the comment form
    await TypeAsync("#comment-name", "Test User");
    await TypeAsync("#comment-email", "test@example.com");
    await TypeAsync("#comment-content", "This is a test comment");

    // Submit the form
    await ClickAsync("#comment-submit");

    // Wait for success message
    await WaitForSelectorAsync(".comment-success");

    // Verify the comment appears
    var commentText = await GetTextContentAsync(".comment-list .comment:last-child .comment-content");
    Assert.Contains("test comment", commentText.ToLower());
}
```

### Testa tangentbordsinteraktioner

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

    // Focus the search box
    await Page.FocusAsync("#search");

    // Type a search query
    await Page.Keyboard.TypeAsync("testing");

    // Press arrow down to select first result
    await Page.Keyboard.PressAsync("ArrowDown");

    // Press enter to navigate
    await Page.Keyboard.PressAsync("Enter");

    // Verify we navigated to the right page
    await WaitAsync(1000);
    Assert.Contains("/blog/", Page.Url);
}
```

### Testa filuppladdningar

```csharp
[Fact]
public async Task Can_Upload_Image()
{
    await NavigateAsync("/admin/upload");

    // Create a test file
    var testFilePath = Path.Combine(Path.GetTempPath(), "test-image.jpg");
    File.WriteAllBytes(testFilePath, new byte[] { 0xFF, 0xD8, 0xFF }); // JPEG header

    // Upload the file
    var fileInput = await Page.QuerySelectorAsync("input[type=file]");
    await fileInput.UploadFileAsync(testFilePath);

    await ClickAsync("#upload-submit");

    // Verify upload succeeded
    await WaitForSelectorAsync(".upload-success");

    // Clean up
    File.Delete(testFilePath);
}
```

### Pröva dra och släpp

```csharp
[Fact]
public async Task Can_teAsync("/admin/posts");

    var dragSource = await Page.QuerySelectorAsync(".post-item[data-id='1']");
    var dropTarget = await Page.QuerySelectorAsync(".post-item[data-id='3']");

    var sourceBox = await dragSource.BoundingBoxAsync();
    var targetBox = await dropTarget.BoundingBoxAsync();

    // Perform drag and drop
    await Page.Mouse.MoveAsync(sourceBox.X + sourceBox.Width / 2, sourceBox.Y + sourceBox.Height / 2);
    await Page.Mouse.DownAsync();
    await Page.Mouse.MoveAsync(targetBox.X + targetBox.Width / 2, targetBox.Y + targetBox.Height / 2);
    await Page.Mouse.UpAsync();

    await WaitAsync(500);

    // Verify new order
    var firstItemId = await Page.EvaluateFunctionAsync<string>(
        "() => document.querySelector('.post-item').dataset.id"
    );
    Assert.Equal("1", firstItemId);
}
```

## Integrering med ASP.NET Core Testing

Du kan integrera PuppeterSharp med [ASP.NET Core WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests) För en mer integrerad testupplevelse:

```csharp
public class E2EWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseUrls("http://localhost:5050");

        builder.ConfigureServices(services =>
        {
            // Override services for testing
            // For example, use in-memory database
            services.RemoveAll<DbContextOptions<MostlylucidDbContext>>();
            services.AddDbContext<MostlylucidDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDb");
            });
        });
    }
}

public abstract class IntegratedE2ETestBase : E2ETestBase, IClassFixture<E2EWebApplicationFactory>
{
    protected E2EWebApplicationFactory Factory { get; }

    protected IntegratedE2ETestBase(E2EWebApplicationFactory factory, ITestOutputHelper output)
        : base(output)
    {
        Factory = factory;
    }

    public override async Task InitializeAsync()
    {
        await base.InitializeAsync();

        // Application is automatically started by WebApplicationFactory
        // Override BaseUrl to use the factory's address
        BaseUrl = "http://localhost:5050";
    }
}
```

## Bortom testning - PuppeterSharp för PDF-generering och automatisering

Medan E2E-testning är lysande, är PuppeteerSharp en schweizisk armékniv som kan göra mycket mer. En av dess mest populära användningsområden är att generera PDF-filer från webbinnehåll - det är otroligt användbart för detta, men inte utan dess getchas. Om du bygger fakturor, rapporter, eller något dokumentgenereringssystem, kommer detta avsnitt spara timmar av felsökning.

### Skapa PDF-filer - Löftet och smärtan

Idén är enkel: gör en webbsida i Chrome och spara den som en PDF. Perfekt för att generera fakturor, rapporter, certifikat, eller något dynamiskt innehåll som behöver distribueras i PDF-format.

Här är det grundläggande tillvägagångssättet:

```csharp
public class PdfGeneratorService
{
    public async Task<byte[]> GeneratePdfFromUrlAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
        });

        var pdfData = await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            MarginOptions = new MarginOptions
            {
                Top = "20mm",
                Right = "20mm",
                Bottom = "20mm",
                Left = "20mm"
            }
        });

        return pdfData;
    }
}
```

Jag följer dig genom landminorna.

### PDF Generation Gotchas - Vad ingen säger till dig

#### 1. Teckensnitt inbäddade mardrömmar

**Problemet:** Dina vackra anpassade teckensnitt visas inte i PDF, eller värre, de är där men ser helt skräp.

**Varför det händer:** Chrome behöver tillgång till teckensnittsfilerna under PDF-genereringen. Om dina teckensnitt laddas via extern CDN och Chrome inte kan nå dem (brandvägg, nätverksproblem, timing), är du stoppad.

**Lösningen:**

```csharp
await page.GoToAsync(url, new NavigationOptions
{
    WaitUntil = new[]
    {
        WaitUntilNavigation.Networkidle0,  // Wait for network to be idle
        WaitUntilNavigation.Load           // Wait for fonts to load
    },
    Timeout = 60000  // Give it time to load fonts
});

// Extra insurance - wait for fonts to actually load
await page.EvaluateFunctionAsync(@"async () => {
    await document.fonts.ready;
}");

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true  // CRUCIAL for @font-face fonts
});
```

Ännu bättre, värd dina teckensnitt lokalt eller bädda in dem som bas64 i din CSS. Ja, det är en faff, men det är pålitlig.

#### 2. CSS Skriv ut mediefrågor

**Problemet:** Din PDF ser inte alls ut som din webbsida eftersom Chrome tillämpar tryck mediefrågor.

Det här är faktiskt **korrekt beteende** PDF:er är tryckta medier, men det fångar upp alla första gången.

**Lösningen:**

Användning `@media print` CSS-regler på lämpligt sätt:

```css
/* Show on screen, hide in PDF */
.no-print {
    display: block;
}

@media print {
    .no-print {
        display: none !important;
    }

    /* Prevent page breaks inside elements */
    .keep-together {
        page-break-inside: avoid;
        break-inside: avoid;
    }

    /* Force page breaks */
    .page-break {
        page-break-before: always;
    }
}
```

Eller, om du vill ha skärmversionen i din PDF (användbar för att generera "screenshots" som PDF:er):

```csharp
await page.EmulateMediaTypeAsync(MediaType.Screen);  // Force screen media
var pdfData = await page.PdfDataAsync();
```

#### 3. Sidan bryter - Bane of Your Existence

**Problemet:** Ditt innehåll blir pinsamt delat på sidor, med rubriker föräldralösa längst ner eller tabeller halverade.

**Verkligheten:** Du kämpar mot Chromes interna pagineringsalgoritm, och den kommer att vinna för det mesta.

**Vad du kan göra:**

```css
@media print {
    h1, h2, h3, h4, h5, h6 {
        page-break-after: avoid;
        break-after: avoid;
    }

    table, figure, img {
        page-break-inside: avoid;
        break-inside: avoid;
    }

    /* Force specific breaks */
    .new-page {
        page-break-before: always;
    }
}
```

Och i din PuppeterSharp-kod:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true,
    PreferCSSPageSize = true,  // Respect CSS @page rules
    DisplayHeaderFooter = false
});
```

**Pro-tips:** För komplexa layouter, ibland är det lättare att strukturera din HTML med explicita sidbrytningar snarare än att bekämpa webbläsaren:

```html
<div class="page">
    <!-- First page content -->
</div>
<div class="page-break"></div>
<div class="page">
    <!-- Second page content -->
</div>
```

#### 4. Huvuden och sidfot - mer komplex än du tror

Du kan lägga till headers och sidfotar, men API är lite wonky:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    DisplayHeaderFooter = true,
    HeaderTemplate = @"
        <div style='font-size: 10px; text-align: center; width: 100%;'>
            <span class='title'></span>
        </div>
    ",
    FooterTemplate = @"
        <div style='font-size: 10px; text-align: center; width: 100%;'>
            Page <span class='pageNumber'></span> of <span class='totalPages'></span>
        </div>
    ",
    MarginOptions = new MarginOptions
    {
        Top = "30mm",     // Must be larger to accommodate header
        Bottom = "25mm"   // Must be larger to accommodate footer
    }
});
```

**Jag fattar.**

- Header/footer-mallar måste vara giltiga HTML men är extremt begränsade - ingen extern CSS, inget JavaScript
- Du får bara specifika variabler: `date`, `title`, `url`, `pageNumber`, `totalPages`
- Styling är endast inline
- Marginalerna måste vara tillräckligt stora för att rymma headers/footers, annars överlappar de ditt innehåll.

#### 5. Bakgrundsgrafik

Som standard, Chrome inte skriva ut bakgrundsbilder eller färger (detta är en webbläsare standard för att spara bläck). **måste** aktivera den:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    PrintBackground = true  // Without this, your beautiful backgrounds vanish
});
```

#### 6. Minnesluckor med stora dokument

**Problemet:** Generera massor av PDF-filer orsakar din ansökan minne ballong och så småningom krascha.

**Varför:** Varje webbläsarinstans använder betydande minne (100-200MB), och om du inte kasserar ordentligt, de staplar upp.

**Lösningen:**

Använd alltid `await using` eller lämpligt bortskaffande:

```csharp
// Good - automatic disposal
await using var browser = await Puppeteer.LaunchAsync(options);
await using var page = await browser.NewPageAsync();

// Or manually
IBrowser? browser = null;
try
{
    browser = await Puppeteer.LaunchAsync(options);
    // ... use browser
}
finally
{
    if (browser != null)
    {
        await browser.CloseAsync();
        await browser.DisposeAsync();
    }
}
```

För hög volym PDF-generering, överväga att återanvända webbläsarinstanser:

```csharp
public class PdfGeneratorService : IDisposable
{
    private IBrowser? _browser;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public async Task<byte[]> GeneratePdfAsync(string url)
    {
        await _semaphore.WaitAsync();
        try
        {
            // Reuse browser instance
            _browser ??= await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true
            });

            await using var page = await _browser.NewPageAsync();
            await page.GoToAsync(url);
            return await page.PdfDataAsync();
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_browser != null)
        {
            await _browser.CloseAsync();
            await _browser.DisposeAsync();
        }
        _semaphore.Dispose();
    }

    public void Dispose()
    {
        DisposeAsync().AsTask().Wait();
    }
}
```

#### 7. Skala alternativet - Mindre text, mer innehåll

Ibland behöver du få mer innehåll på en sida:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    Scale = 0.8m,  // 80% scale - fits more content
    PrintBackground = true
});
```

Men var försiktig - för liten och det är oläsligt.

### Real-World PDF Generation Mönster

Så här gör jag faktiskt PDF-generering i produktionen:

```csharp
public class InvoicePdfGenerator
{
    private readonly ILogger<InvoicePdfGenerator> _logger;

    public InvoicePdfGenerator(ILogger<InvoicePdfGenerator> logger)
    {
        _logger = logger;
    }

    public async Task<byte[]> GenerateInvoicePdfAsync(Invoice invoice)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--disable-dev-shm-usage"  // Overcome limited resource problems
            }
        });

        await using var page = await browser.NewPageAsync();

        // Set up console logging to debug issues
        page.Console += (_, e) =>
        {
            _logger.LogInformation("Browser console: {Message}", e.Message.Text);
        };

        try
        {
            // Generate HTML content (using Razor, or however you do it)
            var htmlContent = await GenerateInvoiceHtmlAsync(invoice);

            // Set content directly rather than navigating to URL
            await page.SetContentAsync(htmlContent, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
            });

            // Wait for fonts to load
            await page.EvaluateFunctionAsync("() => document.fonts.ready");

            // Force screen media type to avoid print media queries changing layout
            await page.EmulateMediaTypeAsync(MediaType.Screen);

            // Generate PDF
            var pdfData = await page.PdfDataAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "10mm",
                    Right = "10mm",
                    Bottom = "10mm",
                    Left = "10mm"
                },
                PreferCSSPageSize = false
            });

            _logger.LogInformation("Generated PDF for invoice {InvoiceId}, size: {Size} bytes",
                invoice.Id, pdfData.Length);

            return pdfData;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to generate PDF for invoice {InvoiceId}", invoice.Id);

            // Take a screenshot for debugging
            try
            {
                var screenshot = await page.ScreenshotDataAsync();
                _logger.LogWarning("Captured screenshot of failed PDF generation: {Size} bytes",
                    screenshot.Length);
                // Could save this to blob storage for debugging
            }
            catch
            {
                // Swallow screenshot errors
            }

            throw;
        }
    }

    private async Task<string> GenerateInvoiceHtmlAsync(Invoice invoice)
    {
        // Your HTML generation logic here
        // Could use Razor views, or any templating engine
        return $@"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');

        body {{
            font-family: 'Inter', sans-serif;
            margin: 0;
            padding: 20px;
            color: #333;
        }}

        @media print {{
            .page-break {{
                page-break-before: always;
            }}

            .no-break {{
                page-break-inside: avoid;
            }}
        }}
    </style>
</head>
<body>
    <div class='no-break'>
        <h1>Invoice #{invoice.Number}</h1>
        <p>Date: {invoice.Date:yyyy-MM-dd}</p>
    </div>

    <!-- Invoice content -->
</body>
</html>";
    }
}
```

### Landskap vs Porträtt

Enkelt men ofta nödvändigt:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    Landscape = true,  // Horizontal orientation
    PrintBackground = true
});
```

### Anpassade sidstorlekar

Inte begränsat till standardformat:

```csharp
var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Width = "210mm",   // Custom width
    Height = "297mm",  // Custom height (this is A4, but you can use any size)
    PrintBackground = true
});
```

## Andra praktiska användningsområden för PuppeterSharp

Utöver testning och PDF-generering utmärker sig PuppeteerSharp på flera andra automationsuppgifter. Låt oss utforska de vanligaste verkliga applikationerna.

### Webbklipp för dataextraktion

PuppeterSharp är lysande för att skrapa JavaScript-tunga webbplatser där traditionella HTML-tolkar faller kort:

```csharp
public class ProductScraper
{
    public async Task<List<Product>> ScrapeProductsAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });

        // Wait for products to render (adjust selector as needed)
        await page.WaitForSelectorAsync(".product-item");

        // Extract product data using JavaScript
        var products = await page.EvaluateFunctionAsync<List<Product>>(@"() => {
            return Array.from(document.querySelectorAll('.product-item')).map(item => ({
                name: item.querySelector('.product-name')?.textContent?.trim(),
                price: parseFloat(item.querySelector('.product-price')?.textContent?.replace('£', '')),
                imageUrl: item.querySelector('img')?.src,
                inStock: !item.querySelector('.out-of-stock')
            }));
        }");

        return products;
    }
}
```

**När du ska använda den:**

- Scraping ensidiga program (React, Vue, Angular)
- Platser med oändlig rullning eller lat lastning
- När du behöver interagera med sidan (klicka på knappar, fyll i formulär) innan du skrapar
- Innehåll bakom inloggningsväggar

**När du inte ska använda den:**

- Enkel statisk HTML-skrapning (använd [HtmlAgilityPack](https://html-agility-pack.net/) eller [Vinkelsharp](https://anglesharp.github.io/) istället - mycket snabbare och lättare)
- Högvolymsskrapning (Browser overhead är betydande)
- När det finns ett API tillgängligt (alltid föredrar officiella API:er framför skrapning!)

### Automatiserad skärmdumpsgenerering

Utöver testning är skärmdumpar användbara för miniatyrbilder, förhandsgranskningar eller arkivering:

```csharp
public class ScreenshotService
{
    public async Task<byte[]> CaptureWebsiteAsync(string url, int width = 1920, int height = 1080)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.SetViewportAsync(new ViewPortOptions
        {
            Width = width,
            Height = height
        });

        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });

        // Full page screenshot
        return await page.ScreenshotDataAsync(new ScreenshotOptions
        {
            FullPage = true,
            Type = ScreenshotType.Png
        });
    }

    public async Task<byte[]> CaptureElementAsync(string url, string selector)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url);

        var element = await page.WaitForSelectorAsync(selector);
        if (element == null)
        {
            throw new InvalidOperationException($"Element {selector} not found");
        }

        // Screenshot of specific element
        return await element.ScreenshotDataAsync();
    }
}
```

**Praktiska användningsområden:**

- Skapar og: image taggar för blogginlägg
- Skapa miniatyrbilder för webbplatsgallerier
- Arkivera webbsidor för efterlevnad
- Skapar förhandsgranskningsbilder för länkdelning

### Prestandaövervakning

Mät sidbelastningens prestanda:

```csharp
public class PerformanceMonitor
{
    public async Task<PerformanceMetrics> MeasurePagePerformanceAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();

        var stopwatch = Stopwatch.StartNew();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });
        stopwatch.Stop();

        // Get performance metrics from the browser
        var metrics = await page.MetricsAsync();

        // Get performance timing data
        var performanceTiming = await page.EvaluateExpressionAsync<PerformanceTiming>(@"
            JSON.parse(JSON.stringify(performance.timing))
        ");

        return new PerformanceMetrics
        {
            TotalLoadTime = stopwatch.ElapsedMilliseconds,
            DomContentLoaded = performanceTiming.DomContentLoadedEventEnd - performanceTiming.NavigationStart,
            FirstPaint = metrics["FirstPaint"],
            LayoutCount = (int)metrics["LayoutCount"],
            ScriptDuration = metrics["ScriptDuration"]
        };
    }
}
```

### Automatiserad rapportgenerering

Kombinera HTML-templering med PDF-generering för automatiserad rapportering:

```csharp
public class MonthlyReportGenerator
{
    public async Task<byte[]> GenerateMonthlyReportAsync(ReportData data)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();

        // Generate HTML report using your preferred templating engine
        var html = GenerateReportHtml(data);
        await page.SetContentAsync(html);

        // Wait for any charts to render (if using Chart.js, D3.js, etc.)
        await Task.Delay(2000);

        return await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            DisplayHeaderFooter = true,
            HeaderTemplate = $@"
                <div style='font-size: 9px; margin: 0 auto; text-align: center;'>
                    Monthly Report - {data.Month:MMMM yyyy}
                </div>
            ",
            FooterTemplate = @"
                <div style='font-size: 9px; margin: 0 auto; text-align: center;'>
                    Page <span class='pageNumber'></span> of <span class='totalPages'></span>
                </div>
            ",
            MarginOptions = new MarginOptions
            {
                Top = "25mm",
                Bottom = "20mm",
                Left = "15mm",
                Right = "15mm"
            }
        });
    }
}
```

### Kostnaden för "fri" PDF-generering

Nu, här är grejen med att använda PuppeterSharp för PDF-generering - det är "gratis" i den meningen att du inte betalar för en PDF-bibliotek licens, men det är **inte fri när det gäller resurser**.

Varje browser instans:

- Använder 100-200 MB RAM
- Kräver betydande CPU för rendering
- Det tar 2-5 sekunder att generera en PDF (beroende på komplexitet)

Jämför detta med dedikerade PDF-bibliotek som:

- **[iText av betydelse för EES](https://itextpdf.com/)** (tidigare iTextSharp) - Kommersiell licens krävs (~£500-3000/år), men genererar PDF-filer i millisekunder med små minnesavtryck
- **[QuestPDF](https://www.questpdf.com/)** - Fri och öppen källkod under MIT-licens, genererar PDF-filer från flytande C#-kod (ingen HTML), flammande snabbt
- **[PdfSharpCore Ordförande](https://github.com/ststeiger/PdfSharpCore)** - Gratis MIT licens, men mer begränsad i kapacitet

**När du ska använda PuppeterSharp för PDF-filer:**

- Du har redan HTML-mallar och vill inte skriva om i PDF-layoutkod
- Du behöver pixel-perfect rendering av komplexa webblayouter
- Volymen är låg (< 100 PDF-filer per timme)
- Du måste generera PDF-filer från externa webbplatser som du inte kontrollerar

**När du ska använda dedikerade PDF-bibliotek:**

- Högvolymgenerering (> 100 PDF-filer per timme)
- Enkla layouter (fakturor, kvitton, rapporter)
- Resursbegränsade miljöer
- Du behöver avancerade PDF-funktioner (former, signaturer, kryptering)

### En hybridmetod

Ibland är den bästa lösningen att använda båda:

```csharp
public class PdfService
{
    private readonly ILogger<PdfService> _logger;

    public async Task<byte[]> GeneratePdfAsync(PdfRequest request)
    {
        // Simple documents - use QuestPDF (fast, low resources)
        if (request.IsSimpleLayout)
        {
            return GenerateWithQuestPdf(request);
        }

        // Complex documents with web content - use PuppeteerSharp
        return await GenerateWithPuppeteerAsync(request);
    }

    private byte[] GenerateWithQuestPdf(PdfRequest request)
    {
        // QuestPDF code here - much faster for simple layouts
        return Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Size(PageSizes.A4);
                page.Margin(2, Unit.Centimetre);
                page.Content().Text(request.Content);
            });
        }).GeneratePdf();
    }

    private async Task<byte[]> GenerateWithPuppeteerAsync(PdfRequest request)
    {
        // PuppeteerSharp code for complex layouts
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.SetContentAsync(request.HtmlContent);
        return await page.PdfDataAsync();
    }
}
```

## Slutsatser

PuppeterSharp har varit en absolut game-changer för E2E-testning i mina .NET-projekt. Det är snabbare än Selenium, har ett modernare API, och bara generellt gör testning mindre av en uppgift.

Här är vad jag skulle rekommendera:

1. **Börja med PuppeterSharp** Om du bara testar Chrome/krom. Det är enklare och snabbare än alternativen.

2. **Använd spelrätt** om du behöver stöd för multi-browser. Det har alla fördelar med PuppeterSharp plus Firefox och Safari.

3. **Undvik Selen** för nya projekt om du inte har en särskild anledning att använda det (som IE11 stöd, vilket förhoppningsvis du inte).

4. **Skriv tester på ett klokt sätt**. E2E tester är långsamma och kan vara sprött. Använd dem för kritiska användarresor, inte för att testa varje liten detalj. Det är vad enhet och integration tester är för.

5. **Håll testerna isolerade**. Varje test bör ställa in sina egna data och städa upp efter sig själv.

6. **Använd hjälpmetoder** Basklassens mönster håller din testkod ren och fokuserad på det du testar, inte hur du testar den.

E2E-testning behöver inte vara smärtsam. Med rätt verktyg och mönster kan det faktiskt vara ganska trevligt. Ge PuppeterSharp ett försök på ditt nästa projekt - jag tror att du kommer att bli glatt överraskad.

Jag ska skriva fler tester.

## Ytterligare läsning

- [Dokumentation av PuppeteerSharp](https://www.puppeteersharp.com/)
- [Puppeter- API](https://pptr.dev/) (JavaScript, men de flesta begrepp gäller)
- [Spelrätt för .NET](https://playwright.dev/dotnet/)
- [xUnit Dokumentation](https://xunit.net/)
- [ASP.NET Core Integration Tests](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)