# End-~-~Endtestning med PuppeteerSharp | - En lämplig alternativ till Selenium

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
Moderna E2E M SK1End-Vad -End~, genom att använda din sida som användaren skulle göra.) Testing behöver inte vara smärtsamt, ' Det här kompletta handboken visar dig hur man använder PuppeteerSharp för en snabb, pålitlig och tillförlitlig webblärares automatisering i ., NET, M SK10, som täcker allt från grundtestning till PDF-generation och web scraping.M SK11. Medan Microsofts Playwright är den mer moderna multi- -browserlösningen.,. Jag valde PuppeterSharp till denna blogg eftersom det är vad jag visste och Chrome var. [Playwright guide](/blog/playwright-e2e-testing) istället.

## Inleiding

Om du någonsin arbetat med [Selenium](https://www.selenium.dev/) för slutändan.- till-. slutända tester.,. du kommer veta att det kan vara en rätt smärta på baksidan. .. Mellan att brottas med drivarnas versioner, ,. hantera fläckiga tester som fungerar på din maskin men ingen annanstans.

Kom in [Puppeteer-särp](https://www.puppeteersharp.com/) - Googles NET port . [Poppatör](https://pptr.dev/) bibliotek. Det är som Selenium ' den yngre, snabbare kusin som faktiskt bryr sig om att dyka upp i tid och inte kräver att du ladda ner sjutton olika webbläsares driver

I denna artikel kommer jag att visa er hur jag implementerade PuppeteerSharp för E-testning på den här bloggen. , komplett med verkliga kodexexempla från repon.

[TOC]

## Vad är "'", PuppeteerSharp , då?

[Puppeteer-särp](https://www.puppeteersharp.com/) är en .NET bibliotek som tillhandahåller en API på hög nivå för att kontrollera Chrome eller Chromium-browser med hjälp av [Chrome DevTools-protokoll](https://chromedevtools.github.io/devtools-protocol/). Olikt [Selenium](https://www.selenium.dev/), som använder [WebDriver-protocol](https://www.w3.org/TR/webdriver/) ( en ganska stökig HTTP-baserad JSON-drátprotokoll, - PuppeteerSharp pratar direkt med webbläsaren via DevTools.

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

- **Selenium**: Som att skicka brev genom post för att kommunicera med din webblader
- **Puppeteer-särp**: Som att ha en direkt telefon till webbläsaren

```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
```

### Var E2E-testing passar in i ditt teststrategi

Innan vi dyker djupare, låt oss prata om var E-testet passar in i det stora schemat av saker och ting. Ni har förmodligen hört talas om testpyramiden.

```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 prov**

- **Unittests** (80% av dina tester, ): snabb, , billig, M SK3 testar individuella funktioner,. Men de säger inte till dig om systemet faktiskt fungerar som ett helt system.
- **Integrationstest** (15% av dina tester
- **E2E tester** (5% av dina tester

**När du behöver E2E tester:**

1. **Kritiska användarfärder** - logga in , betala ut M SK2 processa betalningen . om dessa bryter sig
2. **JavaScript- tunga gränssnitt** - Moderna SPAs [reagera](https://react.dev/), [Vue](https://vuejs.org/), [Längsvinkel](https://angular.dev/)) där gränssnittet presenteras som klient
3. **Cross-browserproblem** - Skillnade webblader skiljer saker på olika sätt.  fast med PuppeteerSharp så är du m.a.
4. **Kompleksa interaktioner** - Multi

**När du inte behöver E-test**

1. **Enkel CRUD-operationer** - Integreringstest är tillräckligt
2. **Rent logik** - Det är vad enhetens tester är för
3. **Varje rand i fallet** - EM SK1E-tester är för långsamma och dyra för uttömmande tester

### Varför PuppeteerSharp över Selenium?

Låt mig räkna sättet.

1. **Ingen drivkraftskontroll faff**: PuppeteerSharp laddar ner och sköter Chrome-brausern för dig . Ingen mer röra sig om ChromeDriver-versioner som inte matchar Chromeversionen som du installerat

2. **snabbare utföring**: DevTools-protocoln är betydligt snabbare än WebDriver

3. **Bättre API**: API:n är mer modern och intuitivt . Det' är asynkt / väntar hela vägen ner M SK4 som passar vackert med den moderna ♫ . NET utvecklingen ♫

4. **Inbyggda - i bildbild & PDF-generering**: Vill du ha en skärmdump när ett test misslyckas ? Det är dödligt enkelt med PuppeteerSharp

5. **Interceptera nätverksbehov**: Du kan interceptera , modifiera , eller blockera nätverksbehov på ett enkelt sätt. ♫ - lysande för att testa offline-scenarier eller skämta API-responser ♫

6. **Rättvis implementerad JavaScript**: utför JavaScript i sidans kontext och får resultat tillbaka på ett sätt som inte gör att du vill gråta

## Att sätta upp en poppenskapare

Först, ",", lägger du till [Puppeteer-särp](https://www.nuget.org/packages/PuppeteerSharp) NuGet package:

```bash
dotnet add package PuppeteerSharp
```

Här är min testprojekts konfiguration (`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>
```

I'm använder [xUnit](https://xunit.net/) (ASP .NET Core's standardinställningM SK3 men PuppeteerSharp fungerar lika bra med [NUnit](https://nunit.org/) eller [MStest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest).

## Att skapa en testklass

Istället för att upprepa inställningen, har jag skapat en basklass.`Mostlylucid.Test/E2E/E2ETestBase.cs:12`) som hanterar webbläsarens livscykel

### Klassenstrukturen

```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 implementerar [IAsyncLifetime](https://xunit.net/docs/shared-context#async-lifetime) från xUnit, som ger en async konfiguration /teardown. Till skillnad från traditionella konstruktorerM SK3 låter det oss vänta ordentligt på webblagrens inicialisering

### Initialisering av webbläsaren

```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;
    }
```

Den `BrowserFetcher` laddar automatiskt ner en kompatible Chromiumversion på den första versionen - inget drivermanagement krävs `--no-sandbox` flaggor är nödvändiga för Docker

### Sko

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

Ett korrekt avfallsmedel är viktigt för att undvika minnesmängder. Varje webblader instans använder 100-200MB RAM

## hjälpmetoder

Basisklassen innefattar hjälpmedel för att minska boilerplatten (`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);
}
```

De hanterar tråkiga biter - som väntar på att elementet ska existera , elegant tidningstillståndshantering , och automatiskt registrera när tester misslyckas i CI

## Att skriva faktiska tester

Okej,Let'S få tag i de bra grejerna-S skriva riktiga tester. Här' är en riktig test från min blogg'S filterbarfunktionalitet(`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");
}
```

Den här testen tar reda på att min dropdown fungerar ordentligt.

### Attityden  Skip 

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

I' har hoppat över den här testen standardmässigt eftersom det kräver att platsen kör lokalt . För E2E-testar M SK3 vill du vanligtvis köra dem på ♫ - ♫ krav snarare än med varje byggande ♫. Du kan avskipa dem när du ♫ ' ♫ är redo att köra dem ♫ МSK7 ♫ eller köra dem i en separat CI-uppdrag där du ♪ ' ♫ har fått platsen att snurra upp ♫

### Att utföra 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 en av områdena där PuppeteerSharp absolut lyser `EvaluateFunctionAsync` metoden låter dig köra JavaScript i webbläsarens kontext och få resulten tillbaka som en korrekt .NET-typ. I detta fall , IM SK3 m kolla om en dropdown faktiskt är synlig | ( | inte bara närvarande i DOM | МSK5 | genom att titta på dess programmerade style

Jämför detta med Selenium där du

1. Hitta elementet
2. få dess display egenskap
3. Förefatta stringresultatet
4. Förhoppningsvis är ' inte död när du kollat den

### Att testa HTMX-interaktioner

Min blogg använder [HTMX](https://htmx.org/) i stor utsträckning (server`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 är `await WaitAsync(1000)` efter att ha ändrat valet.

### Att testa responsiv design

Här är en hemsk test som kollar att min filterbar ä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 porten när som helst, vilket är lysande för att testa responsiva layouts. Mycket enklare än att skala upp din webbsida manuellt

## Utvecklade figurfigur-fenskaper

### nätverksövervakning

En av mina favoritfunktioner är förmågan att interceptera och modifiera nätverksbehov. Detta är ovärderligt för att testa fel tillstånd eller offline-scenarior:

```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();
    }
};
```

### Att ta bilder

När ett test misslyckas, är en skärmdump värd tusen logmeddelanden

```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 till och med generera PDF-versioner av sidor.

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

### Kodskydd

PuppeteerSharp kan till och med samla in data om JavaScripts kodbeläggning:

```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}%");
```

## PuppeteerSharp mot konkurrensen

Låt oss titta på hur PuppeteerSharp staplar upp mot andra 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

**Gamla vakterna**

Selenium har funnits i världen sedan 2004 och det visar . det älsklingande, ' bra, - dokumenterat och ,, och stödjer alla webbsidor under solen.

**Pros:**

- Ondersteuner alla webblader (Chrome, FirefoxM SK2 Safari , EdgeMSC4 IE om du är en masochist
- Ett massivt ekosystem av verktyg och förlängningar
- Bra-känd och allmänt tillämpad
- Bra för cross-browsertester

**Cons:**

- WebDriver-protokollet är långsamt
- Att sköta drivaren är smärtsamt, fast WebDriverManager hjälper.
- API känns förlegat jämfört med moderna alternativ
- Flaky-tester är vanliga på grund av timingsproblem
- Inget byggt-in nätverksinterception

**När man ska använda den:** När du absolut behöver testa över flera webblader , eller när du' är redan inblandad i Seleniums ekosystem

### Dramatiker

**Den nya killen i kvarteret**

[Dramatiker](https://playwright.dev/) är Microsofts svar på Puppeteer, med [.NET stöd](https://playwright.dev/dotnet/) baked in från början. Det ' är i grunden PuppeteerSharp men med flera supporter

**Pros:**

- Ondersteuner Chrome, Firefox, Safari M SK2WebKitMSC3
- Modern API som liknar Puppeteer
- Auto-downloads webblader
- Inbyggt-in nätverksinterceptering,bildskärmdumpar , etcM SK3
- Utmärkt .NET stöd

**Cons:**

- Nyare, så mindre ekosystem
- Kan vara överkill om du bara behöver Chrome
- Lite mer komplex inställning på grund av multi-browser support

**När man ska använda den:** När du behöver multi-browser support men vill ha en modern API

### Cypress

**Den JavaScript-utvecklare**

Cypress är lysande om du arbetar med JavaScript.

**Pros:**

- Fantastiskt utvecklande upplevelse
- Tid-travel-debugging
- Automatiskt vänta
- Lysande dokumentation

**Cons:**

- JavaScript/TypScript bara
- Ingen support för .NET
- Kan' inte testa flera tabs eller fönster
- Begränsat med att testa din egen applikation ( inga test över alla domains )

**När man ska använda den:** Don't, du ' skriver M SK3 NET-koden . Sticka till något som integreras med din teknikstack

### Så vad ska du använda?

Här är mitt exempel.

```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 som bygger moderna webbapplikationer

- **Chrome-only testing?** → PuppeteerSharp
- **Multi-browsertestning?** → Dramatiker
- **Already using Selenium?** → överväg att migra till Playwright , men tänk inte på

## Att köra tester i CI/CD

E2E-tester är alla bra på din lokala maskin, men de måste fungera i CI, /, CD-rör också. [GitHub-aktioner](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
```

Nyckelbiterna:

1. Starta applikationen i bakgrunden
2. Vänta till att den är frisk ( med hjälp av en hälsokontroll
3. köra E2E tester
4. Uploada bilder om några tester misslyckas
5. Stoppa alltid applikationen, även om tester misslyckas

## Vanliga gropar och hur man kan undvika dem

### fläckande tester

E2E-tester kan vara fläckiga - de passerar ibland och misslyckas ibland med andra M SK2 Det beror oftast på timingsproblem

**Okej**

```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 till elementet du är på väg att interagera med för att existera och bli synligt

### Isolering

Varje test måste vara helt oberoende

**Okej**

```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
}
```

### Page Object Pattern

För komplexa sidor, använda Page Objects mönster för att hålla dina tester underhållningsbara

```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));
}
```

## Tillvägagångspunkter

E2E-tester är långsammare än enhetens-testers.

### Att köra tester parallellt

xUnit kör standardmässigt parallella tester, men du behöver vara försiktig med delade tillstånden:

```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
}
```

### Deaktivera onödiga egenskaper

snabba upp tester genom att avskaffa egenskaper 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 klokt

Blockera onödiga resurser för att accelerera saker

```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();
    }
};
```

## Debugging av E2E tester

När tester misslyckas ( och de kommer att göra det), behöver du avbuga demM SK2 Här är några tekniker

### Loopa i icke-Headless Mod

Bestämda `Headless = false` att titta på webbläsaren i aktivitet:

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

### Använd DevTools

Man kan faktiskt öppna DevTools programmerat:

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

### Konsoleloggning

Ta upp konsol- meddelanden från webbläsaren:

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

### Att be om inlogning

Protokollera alla nätverksbehov:

```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}");
};
```

## Verklig-Worlda testmönster

Här är några mönster jag använder regelbundet i mina E-tester

### Försöksformen för bidrag

```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());
}
```

### Att 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);
}
```

### Att testa upplappar av filer

```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);
}
```

### Att testa Drag and Drop

```csharp
[Fact]
public async Task Can_Reorder_Items()
{
    await NavigateAsync("/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);
}
```

## Integration med ASP.NET Core Testing

Du kan integrera PuppeteerSharp med [ASP.NET Core's 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";
    }
}
```

## Förutom att testa - PuppeteerSharp för PDF generering och automatisering

Medan E2E-testerning är lysande , PuppeteerSharp är en Schweizisk armé kniv som kan göra mycket mer . En av dess vanligaste användningsområden är att generera PDF från webbinhoud. - det M SK4 är otroligt användbart för detta MSC5 även om inte utan dess gotchas.

### Generera PDFs - Fördraget och smärtan

Tanken är enkel: att göra en webbsida i Chrome och spara den som en PDF. Perfekt för att generera fakturor , rapporter

Här är den 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;
    }
}
```

Det ser väldigt enkelt ut.

### PDF-generation fick tag i - Vad ingen säger

#### 1. Font Embedding Nightmares

**Problemet:** Dina vackra customfonts dyker inte upp i PDF, eller ännu värre, de finns där, men ser absolut skräp ut.

**Varför det händer:** Chrome behöver tillgång till font-filer under PDF-generationen. Om dina fonter är laddade via ett externt CDN och Chrome kan' inte nå dem | (skyltväggen , nätverksproblem M SK4 timing ), duM SK6 är fulla

**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, hosta dina fonter lokalt eller bädda in dem som baser64 i din CSSM SK2 JaMSC3 det ' är ett skämt

#### 2. CSS Mediasökningar för skrift

**Problemet:** Din PDF ser inte ut som din webbsida eftersom Chrome använder skriftsmediasökningar.

Det här är **korrekt beteende** - PDF är tryckmedier . Men det fångar alla först

**Lösningen:**

Använda `@media print` CSS-regler ordentligt:

```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ärmenversionen i din PDF | ( | användbar för att generera |" | skärmarbilder | МSK3 | som PDFs

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

#### 3. Page Breaks - Banet på din existens

**Problemet:** Ni får otrevligt splittrade innehåll på sidorna, med underkategorier som är föräldralösa på botten eller tabeller som skärs i två

**Verkligheten:** Du kämpar mot Chromes inre paginationsalgoritm och det kommer att vinna större delen av tiden.

**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 PuppeteerSharp-kod:

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

**Pro Tipp:** När det gäller komplexa layouts, är det ibland lättare att strukturera HTML med uppenklada sidor än att slåss mot webbläsaren.

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

#### 4. Titel och footer - Mer komplex än du tror

Du kan lägga till opskrifte och footer, men API är lite klurigt

```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
    }
});
```

**Gotchas:**

- Header/footer-templater måste vara korrekta HTML men är extremt begränsade - ingen extern CSS , ingen JavaScript
- Du får bara specifika variabler: `date`, `title`, `url`, `pageNumber`, `totalPages`
- Stylering är bara inlyn
- Marginerna måste vara stora nog att tilvara opskrifte/footers eller så kommer de ' överlappa din innehåll

#### 5. bakgrundsgrafik

Standardmässigt, Chrome kan inte printa ut bakgrundsbilder eller färger | ( | detta är en standardmässig webblader för att spara färg **måste** att aktivera det:

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

#### 6. Memory Leaks with Large Documents

**Problemet:** Att generera massor av PDF orsakar att din applications minne till ballong och slutligen kraschar

**Varför:** Varje webbläsares instans använder ett signifikant minne (100-200MB), och om du ' inte tar bort det ordentligt

**Lösningen:**

Använd alltid `await using` eller rätt hantering:

```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öga versioner

```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. Skaleringsalternativet - Mindre text, Mer innehåll

Ibland behöver du få plats med 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örsiktiga. - är för liten och det

### Verklig-World PDF Generation Pattern

Här är hur jag gör PDF-generering i produktion.

```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

Enkel men ofta nödvändig:

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

### Custom Bladsystorlekar

Inte begränsad 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 PuppeteerSharp

Förutom att testa och generera PDF, PuppeteerSharp är bättre på flera andra automatiserade uppgifter . Låt' utforska de vanligaste verkliga applikationerna

### Web Scraping för datautvinning

PuppeteerSharp är lysande för att skrapa JavaScript- tunga webbplatser där traditionella HTML parser inte klarar sig

```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 man ska använda den:**

- Scraping single-page applications (ReactMSC2 VueM SK3 Angular)
- Stätter med oändlig scroll eller lata laddande
- När du behöver interagera med sidan (klicka på knapparna, fylla i formernaM SK2 innan du skrapar
- innehåll bakom inlogeringsväggar

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

- Enkel statisk HTML-skrapning (använda [HtmlAgilityPack](https://html-agility-pack.net/) eller [Kampsform](https://anglesharp.github.io/) istället - mycket snabbare och lättare
- Höga-volumenskrapning M SK1browser överhead är signifikant
- När det finns ett API, prefererar man alltid officiella API istället för att skrapa.

### Automatiserad bildskapande

Förutom att testa, är bilderna användbara för tumbenälningar , previews

```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 tillämpningar:**

- Generera og:bildtag för bloggposter
- Att skapa tumbnailer för webbsidors gallerier
- Archivering av webbsidor för conformitet
- Generera previewbilder för att dela länkar

### Performanceövervakning

mäta sidans belastning:

```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-Tempelering 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"
            }
        });
    }
}
```

### Bekostnaden av "Free" PDF-generering

Nu är det viktigt att använda "" PuppeteerSharp "" för PDF-generation. Det är så att du inte behöver betala för en PDF-bibliotekslicens, men det är så **inte fria i avseende på resurser**.

Varje webbläsares instans:

- Använder 100-200mb RAM
- Benöt stor CPU för rendering
- Det tar 2-5 sekunder att generera en PDF ( beroende på komplexiteten)

jämför detta med dedikerade PDF bibliotek som:

- **[iText](https://itextpdf.com/)** (formerly iTextSharp) - Commercial license required ♫ (~£500-3000/ ♫ year ♫ ), ♫ men genererar PDFs i millisekund med en liten minnesfußavtryck
- **[QuestPDF](https://www.questpdf.com/)** - Gratis och öppen källkod under MIT-licensen, genererar PDFs från flytande C
- **[PdfSharpCore](https://github.com/ststeiger/PdfSharpCore)** - Gratis MIT-licension, men mer begränsad i möjligheter

**När man använder PuppeteerSharp för PDFs:**

- Du har redan HTML-templater och du vill inte skriva om i PDF-koden
- Du behöver pixel-perfect rendering of complex web layouts
- Volumen är låg (< 100 PDF per timme)
- Du behöver generera PDF från externa webbsidor du inte kan kontrollera

**När man ska använda dedikerade PDF bibliotek:**

- Hög volym generering (> 100 PDF per timme)
- Enkela layouts ( fakturor , insamlingar M SK2 rapporter
- Ressource-begränsade miljöer
- Du behöver avancerade PDF-fenslor (former, signeringar , kryptering

### Ett hybridt angreppssätt

Ibland är det bäst 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();
    }
}
```

## Slutsatsen

PuppeteerSharp har varit ett absolut spel.

Här är vad jag rekommenderar.

1. **Börja med PuppeteerSharp** om du bara testar Chrome

2. **Använd spelförfattare** om du behöver multi-browser support. DetM SK2 har alla fördelarna med PuppeteerSharp plus Firefox och SafariMSC3

3. **undvika Selenium** för nya projekt om du inte har någon särskild anledning att använda den. ( som IE 11 stöd

4. **Skriva tester noggrant**., E, 2, E-tester är långsamma och kan vara brittliga, ., Använd dem för kritiska användarfärder.

5. **Håll tester isolerade**. Varje test måste sätta upp sina egna data och rengöra efter sig själv

6. **Använd hjälpmetoder** liberalt. Patternet i grundklassen som jag visade håller din riktiga testkod ren och fokuserar på vad du testar, inte hur du testar det.

E2E-testning behöver inte vara smärtsamt ' med rätt verktyg och mönster, kan det faktiskt vara ganska trevligt . Ge PuppeteerSharp ett steg i ditt nästa projekt M SK5 Jag tror du kommer att bli pleasantly överraskad

Regt, Jag ' Jag är ute för att skriva fler tester

## Fortsättning avläsning

- [PuppeteerSharp Dokumentation](https://www.puppeteersharp.com/)
- [Puppeteer API](https://pptr.dev/) (JavaScript , men de flesta koncept appliceras
- [Dramatiker 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)