Back to "Ende des Tests mit PuppeteerSharp, eine richtige Alternative zu Selen"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

E2E Testing PuppeteerSharp Testing xUnit

Ende des Tests mit PuppeteerSharp, eine richtige Alternative zu Selen

Thursday, 27 November 2025

Modernes E-2-Testen muss schmerzhaft sein Dieses umfassende Handbuch zeigt Ihnen, wie man PuppeteerSharp für schnelles verwenden ?,-Verlässlichkeit Browser-Automatisierung in .-NET--Entdeckung alles von der grundlegenden Prüfung zu PDF-Generation und Web-Scraping ? .-Während Microsoft ? '-Playwright ist die modernere Multi---Browser-Lösung ?,-Ich wählte PuppeteerShorp für diesen Blog, weil es '-s, was ich wusste und Chrome .--nur Test war genug für meine Bedürfnisse , wenn Sie Firefox und Safari Unterstützung benötigen ? Playwright Guide stattdessen ,.'

Einleitung

Wenn du jemals mit gearbeitet hast Selen für end---to---End-Tests - ,-You - '-ll wissen, dass es ein richtiger Schmerz in der Rückseite sein kann..-Zwischen Wrestling mit Treiberversionen - ,- Umgang mit schuppigen Tests, die auf Ihrer Maschine arbeiten, aber nirgendwo sonst...,-und die allgemeine Trägheit des WebDriver-Protokolls -,- es ist genug, um Sie wollen werfen alle in und manuell testen statt ?.

Eingabe PuppeteerSharp Der Port von Google ist der . NET von Google. Puppeteer Bibliothek - 's wie Selenium -'s jüngere, schnellere Cousine, die sich wirklich Mühe macht, rechtzeitig zu zeigen und nicht erfordern, dass Sie siebzehn verschiedene Browser-Treiber herunterladen

In diesem Artikel gehen Sie I''ll durch, wie IMSC2«ve PuppeteerSharp implementierte PuppeteerShorp für E-2«E Testen auf diesem sehr Blog .,« komplett mit echten Codebeispielen aus dem repo ?. .

Was ist ' ?s PuppenspielerSharp Dann ? ?

PuppeteerSharp ist eine Bibliothek, die eine API auf hoher Ebene zur Steuerung von Chrome oder Chromium-Browsern mit dem Chrome DevTools ProtokollIm Gegensatz zu Selen, die die WebDriver-Protokoll Das JSON-Drahtprotokoll ist ziemlich klobig und spricht direkt mit dem Browser über DevTools.

Denken Sie daran auf diese Weise.

  • SelenWie das Senden von Briefen durch den Post, um mit Ihrem Browser zu kommunizieren
  • PuppeteerSharpWie eine direkte Telefonleitung zum Browser zu haben, das Gehirn von '
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

Wo E-2-E-Testing in Ihre Teststrategie passt

Bevor wir tiefer tauchen, reden wir über die Prüfungspyramide, wo das Testen von E2E in das große Schema der Dinge passt. Sie haben wahrscheinlich gehört, wie es tatsächlich in der Praxis funktioniert

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

Der Realitätscheck

  • Einzelprüfungen (80%-Test Ihrer Tests ):-Schnell - billig ,-Test individuelle Funktionen Aber sie nicht sagen Ihnen, ob das System tatsächlich als Ganzes funktioniert .-Test
  • Integrationsprüfungen Testen Sie, wie verschiedene Teile zusammen arbeiten, schneller als E-2.E, aber don.'.t testen Sie die vollständige UI-.
  • E-2.E Prüfungen Besprechen Sie Ihre Tests Bestimmen Sie langsame, kostenintensive, aber testen Sie das System genau so, wie Benutzer es erleben.

Wenn du E-2-Tests brauchst

  1. Kritische Benutzerreisen --Login ,-Checkout,-Zahlungsverarbeitung .-Wenn diese Break-,-Ihr Geschäft beendet
  2. UMWELTBESCHÄFTIGUNG Moderne SPAsReagieren, Vue, Winkel), wo die Benutzeroberfläche client; -,side, ., gerendert wird
  3. Probleme mit Cross---Browsern Verschiedene Browser rendern die Dinge anders.
  4. Komplexe Wechselwirkungen Mehrstufige Assistenten von -Drag--Drag und -Drop-,Drop Uploads der Datei.Drag

Wenn du ON-'-T brauchst, müssen E-2-E-Tests

  1. Einfache CRUD-Operationen Integrationstests reichen aus.
  2. Reine Logik Dass die Einheitenprüfungen für die .
  3. Jeder Randfall E-Tests sind zu langsam und teuer für umfassende Tests.

Warum PuppeteerSharp über Selen?

Lassen Sie mich die Weisen zählen

  1. Kein Fahrermanagement-FaffNicht mehr mucking über mit ChromeDriver-Versionen, die don.'.t passen zu Ihrer installierten Chrome-Version.

  2. Schnellere AusführungDas DevTools-Protokoll ist deutlich schneller als WebDriver..Ihre Tests laufen schneller und Sie verbringen weniger Zeit damit, darauf zu warten, dass etwas passiert.

  3. Bessere APIDie API ist moderner und intuitiver als die .Es'Es async./Erwarten Sie den ganzen Weg nach unten,Das passt wunderbar zu modernen .NET-Entwicklung

  4. Errichtet in Screenshot PDF-GenerierungSie wollen einen Screenshot, wenn ein Test fehlschlägt.?. Es ist tot einfach mit PuppeteerSharp.

  5. Netzwerkanfragen abfangenSie können , modifizieren oder Netzwerk-Anfragen mit Leichtigkeit blockieren Brillant für das Testen von Offline-Szenarien oder spöttischen API-Antworten

  6. Richtige JavaScript-AusführungAusführen JavaScript im Seitenkontext und erhalten Ergebnisse zurück in einer Weise, die nicht machen Sie wollen weinen.

Einrichten des PuppenspielersSharp

Zuerst fügen Sie die PuppeteerSharp NuGet-Paket„:"

dotnet add package PuppeteerSharp

Hier ist meine Konfiguration des Testprojekts.Mostlylucid.Test/Mostlylucid.Test.csproj:23):

<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 mit xEinheit PuppeteerSharp funktioniert genauso gut mit NUnit oder MSTest.

Erstellen einer Basistestklasse

Anstatt den Setup-Code in jedem Test zu wiederholen, hat I'«ve eine Basisklasse erstellt.Mostlylucid.Test/E2E/E2ETestBase.cs:12), die Browser-Lebenszyklus-Management behandelt

Die Klassenstruktur

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

Wir implementieren IASyncLifetime von xUnit-, , die async Setup zur Verfügung stellt./ , teardown ,. , im Gegensatz zu traditionellen Konstruktoren , , , so können wir richtig warten Browser Initialisierung , . ,

Browser-Initialisierung

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

Das BrowserFetcher Laden Sie automatisch eine kompatible Chromium-Version auf den ersten Lauf herunter. Keine manuelle Treiberverwaltung erforderlich. --no-sandbox Flags sind für Docker-Umgebungen erforderlich

Reinigung

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

Die richtige Entsorgung ist entscheidend, um Speicherlecks zu vermeiden.Jede Browser-Instanz verwendet RAM..

Hilfsmethoden

Die Basisklasse umfasst Helfer-Methoden, um Kesselplatte zu reduzierenMostlylucid.Test/E2E/E2ETestBase.cs:72-172):

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

Diese handle die mühsamen Bits warten darauf, dass Elemente vorhanden sind. Anmutige Timeout-Handhabung und automatische Protokollierung für wenn Tests in CI.. versagen

Tatsächliche Tests schreiben

Rechts , , let ,' , um die gute Sache zu erhalten , das Schreiben von tatsächlichen Tests , . , Hier ist ein echter Test aus meinem Blog .' , Filter Bar Funktionalität , ( ,Mostlylucid.Test/E2E/FilterBarTests.cs:20-50):

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

Dieser Test überprüft, dass meine Sprache Dropdown richtig funktioniert ... . . LETTEN ' Blick auf, was macht es tick .

Die Attribute überspringen

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

I'-ve übersprungen diesen Test standardmäßig, weil es erfordert, dass die Website lokal ausgeführt werden muss... Für E-2-E-Tests...,...Sie normalerweise wollen sie auf -.Demand statt mit jedem Build laufen lassen.....Sie können sie ausweichen, wenn Sie '-re bereit sind, sie auszuführen oder sie in einem separaten CI-Job ausführen, bei dem Sie sich mit der Site verdreht haben.MSC9.

Ausführen von JavaScript

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

Dies ist einer der Bereiche, in denen PuppenspielerSharp absolut glänzt EvaluateFunctionAsync Methode lässt Sie JavaScript im Browser-Kontext ausführen und erhalten das Ergebnis zurück als richtiges Beispiel . NET Type®.. In diesem Fall wird überprüft, ob ein Dropdown tatsächlich sichtbar ist ( nicht einfach nur in der DOM.) durch einen Blick auf seine berechneten Stile vorhanden ist.

Vergleichen Sie dies mit Selen, wo du'

  1. Das Element finden
  2. Erhalten Sie seine Anzeigeeigenschaft
  3. Das String-Ergebnis ablesen
  4. Hoffe, dass es nicht abgestanden durch die Zeit, die Sie überprüfen es

HTMX-Interaktionen testen

Mein Blog verwendet HTMX extensive Wiedergabe von (-Servern-'side Rendering ohne Schreiben JavaScript®).« Hier ist ein Test, der die Sortierfunktion überprüft.Mostlylucid.Test/E2E/FilterBarTests.cs:98-126):

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

Der Schlüssel dazu ist die await WaitAsync(1000) nach der Änderung des ausgewählten Wertes . HTMX braucht einen Moment, um seine Anfrage zu machen und aktualisieren Sie die DOM-. In einer perfekten Welt warten , in einem bestimmten Netzwerk auf eine Anforderung zu beenden , aber für einfache Fälle Eine kurze Verzögerung ist fein .

Testen von Responsive Design

Hier ist ein frecher Test, der meine Filterleiste prüft, richtig auf mobilen Geräten versteckt.Mostlylucid.Test/E2E/FilterBarTests.cs:216-245):

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

Sie können das Ansichtsfenster jederzeit ändern, was zum Testen von responsiven Layouts brillant ist... Viel einfacher als die manuelle Größenänderung Ihres Browser-Fensters.

Fortgeschrittene PuppenspielerSharp-Funktionen

Netzabfangen

Eines meiner Lieblings-Features ist die Fähigkeit, Netzwerkanfragen abzufangen und zu modifizieren.Dies ist unschätzbar für das Testen von Fehlerzuständen oder Offline-Szenarien.

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

Screenshots machen

Wenn ein Test fehlschlägt, ist ein Screenshot tausend Log-Nachrichten wert.

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

Sie können sogar PDFs von Seiten erzeugen.Das ist nützlich für das Testen von Servern.

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

Code Erfassungsbereich

PuppeteerSharp kann sogar JavaScript-Code Abdeckungsdaten sammeln

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

PuppenspielerSharp gegen den Wettbewerb

Lassen Sie uns einen richtigen Blick auf, wie PuppenspielerSharp stapelt sich gegen andere Test-Tools E.2.E.

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

Die alte Garde

Selen ist herum gewesen, seit 2004 und es zeigt, dass . es reife ' gut , ist dokumentiert , und unterstützt jeden Browser unter der Sonne . Aber es zeigt auch sein Alter :

Vorbemerkungen

  • Unterstützt alle Browser, wenn du ein Masochist bist
  • Massives Ökosystem von Werkzeugen und Erweiterungen
  • Gut bekannt und weithin angenommen
  • Gut für Cross---Browser-Tests

Verwechslungen

  • WebDriver-Protokoll ist langsam
  • Treiber-Management ist ein Schmerz, obwohl WebDriverManager hilft
  • API fühlt sich im Vergleich zu modernen Alternativen datiert
  • Flaky-Tests sind häufig aufgrund von Zeitproblemen
  • Keine eingebaute ,-'in Netzwerkabhörung

Wann es zu verwenden ist Wenn Sie unbedingt über mehrere Browser testen müssen, oder wenn Sie bereits in das Selenium-Ökosystem investiert haben,

Wiedergaberecht

Das neue Kind auf dem Block

Wiedergaberecht ist Microsoft-Antwort auf Puppeteer-Annullierung mit Unterstützung von -.-NET von Anfang an hineingebacken ,. , es ist im Wesentlichen PuppeteerSharp aber mit Multi-- , Browser Unterstützung , : ,

Vorbemerkungen

  • Unterstützt Chrome , , , Firefox , , , Safari ,( , WebKit , ) ,
  • Moderne API ähnlich Puppeteer
  • Automatisches Herunterladen von Browsern
  • Integrierte Netzwerkabhörung, Screenshots, usw..
  • Exzellente Unterstützung von .-NET

Verwechslungen

  • Neueres, kleineres Ökosystem
  • Kann Overkill sein, wenn Sie nur Chrome benötigen
  • Etwas komplexeres Setup durch Multi---Browser-Unterstützung

Wann es zu verwenden ist Wenn Sie Multi---Browser-Unterstützung benötigen, aber eine moderne API wünschen, wenn Sie ein neues Projekt starten und cross ---Browser testen möchten, ist Playwright wahrscheinlich Ihre beste Wette.

Zypressen

Der JavaScript-Entwickler – 's Liebling

Zypresse ist brillant, wenn du in JavaScript wendest./.TypeScript, aber es ist ein Nicht-- Starter für Entwickler von ..NET.

Vorbemerkungen

  • Fantastische Entwickler-Erfahrung
  • Zeitraffer-Debugging
  • Automatisches Warten
  • Tolle Dokumentation

Verwechslungen

  • Nur JavaScript-Code
  • Keine Unterstützung für -.-NET
  • Kann mehrere Tabs oder Fenster testen
  • Begrenzt auf das Testen Ihrer eigenen Anwendung.

Wann es zu verwenden ist Don-'-t-,-you-'-re Schreiben Sie .-NET-Code, um etwas zu halten, das sich mit Ihrem Tech-Stack integriert

Was sollten Sie also verwenden?

Hier ist meine Einnahme von :.

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 die meisten Entwickler, die moderne Web-Anwendungen aufbauen,

  • Nur Chrome testet? PuppeteerSharp
  • Multi---Browserprüfung Wiedergaberecht
  • Schon mit Selenium-? Betrachten Sie die Migration zum Playwright-Spiel, aber nicht überstürzen es

Laufende Tests in CI-/-CD

E-2-Tests sind alle gut und gut auf Ihrer lokalen Maschine, aber sie müssen in CI-MSC2-CD Pipelines auch laufen . GitHub-Maßnahmen:

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

Die Schlüsselbits

  1. Starten Sie die Anwendung im Hintergrund
  2. Warten Sie darauf, dass es gesund ist.
  3. Führen Sie die Tests aus
  4. Screenshots hochladen, wenn Tests fehlschlagen
  5. Beenden Sie die Anwendung immer, auch wenn Tests fehlschlagen.

Häufige Pitfalls und wie man sie vermeiden

Schrägproben

E-2-E Tests können flaky sein . .- . sie passieren manchmal und andere scheitern .. . . Dies ist in der Regel bis zum Timing Probleme hier .

Schlecht, :

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

Guten Tag.

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

Warten Sie immer, bis das Element you-'-re über die Interaktion mit zu existieren und sichtbar sein

Prüfungsisolierung

Jeder Test sollte völlig unabhängig sein.

Schlecht, :

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

Guten Tag.

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

Objektmuster der Seite

Für komplexe Seiten Verwenden Sie das Page Object-Muster, um Ihre Tests wartungsfähig zu halten.

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

Leistungsbeurteilungen

E-Tests sind langsamer als Einheitentests , , , dort , ' , keine immer um es ,. , Aber Sie können sie schneller machen , : .

Parallele Tests durchführen

xUnit führt Tests standardmäßig parallel durch, aber Sie müssen vorsichtig sein über geteilten Zustand.

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

Unnötige Funktionen deaktivieren

Beschleunigen Sie Tests durch Deaktivieren von Funktionen, die Sie nicht benötigen

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

Netzwerkabfangen weise verwenden

Blockieren Sie unnötige Ressourcen, um die Dinge zu beschleunigen

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

Wenn Tests fehlschlagen, und sie werden ),, müssen Sie sie debuggen.

Im kopflosen Modus ausführen

Satz Headless = false um den Browser in Aktion zu beobachten

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

DevTools verwenden

Du kannst DevTools tatsächlich programmatisch öffnen

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

Protokollierung der Konsole

Konsolen-Nachrichten aus dem Browser erfassen

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

Protokollierung anfordern

Alle Netzwerkanfragen protokollieren

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

Real---Welt-Testmuster

Hier sind einige Muster, die ich regelmäßig in meinen Tests verwenden.

Vorlage des Prüfformulars

[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", "[email protected]");
    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());
}

Testen von Tastaturinteraktionen

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

Dateiuploads testen

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

Testen von Drag & Drop

[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 mit ASP-.-NET Core Testing

Du kannst PuppeteerSharp mit WETTBEWERBSFÄHIGKEIT für ein integrierteres Test-Erlebnis

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

Über die Prüfung hinaus --PuppeteerSharp für PDF Erzeugung und Automatisierung

Während E-2-E-Tests brillant sind, ist PuppeteerSharp ein Schweizer Armeemesser, das weit mehr tun kann..-Eine seiner populärsten Anwendungen ist die Erzeugung von PDFs aus Webinhalten.MSC4 ist unglaublich nützlich für diese , wenn auch nicht ohne seine gotchas , .-Wenn Sie ,'-Rechnungen bauen , ,-Berichte oder jedes Dokument Generation System , wird dieser Abschnitt sparen Sie Stunden debugging ,.-

PDF-Erstellung des Versprechens und der Schmerzen

Die Idee ist einfach, eine Webseite in Chrome rendern und speichern Sie sie als PDF... Perfekt für die Erstellung von Rechnungen.MSC2. reports...,. certificates... oder alle dynamischen Inhalte, die im PDF-Format verteilt werden müssen.

Hier ist der grundlegende Ansatz.

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

Sieht tot einfach aus, bis es ' ist, lass mich dich durch die Landminen gehen

PDF Generierung Gotchas - Was dir niemand sagt

Schriftart Einbetten von Alpträumen

Das Problem Deine schönen benutzerdefinierten Schriftarten erscheinen im PDF-Format oder schlimmer noch, sie sind dort, aber sehen absolut Müll aus.

Warum es passiert ist Chrome benötigt Zugriff auf die Font-Dateien während der PDF-Erzeugung, wenn Ihre Schriftarten über externe CDN und Chrome geladen werden können, erreichen Sie ( firewall, , network issues,, timing, ),

Die Lösung

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

Noch besser hosten Sie Ihre Schriftarten lokal oder sie als Basis einbetten s64e in Ihrem CSS s.e Yes sm,e it sh'es a faff.,e aber es ist zuverlässig

CSS Print Media Queries

Das Problem Ihr PDF sieht nicht wie Ihre Webseite aus, weil Chrome Druckmedienanfragen anwendet.

Das ist eigentlich korrektes Verhalten PDFs sind Printmedien, aber es fängt jeden zum ersten Mal heraus.

Die Lösung

Verwendung @media print CSS-Regeln angemessen

/* 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;
    }
}

Oder, wenn Sie die Bildschirmversion in Ihrem PDF wünschen, ist es nützlich für das Erzeugen von Screenshots.

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

Seite bricht das Bane deines Daseins

Das Problem Ihr Inhalt wird unbequem über Seiten aufgeteilt.... mit Überschriften, die unten verwaist sind oder Tabellen, die halbiert werden.......

Die Realität Du kämpfst gegen Chrome , ' , interne Paginations-Algorithmus , und es wird die meiste Zeit gewinnen ,. ,

Was du tun kannst

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

Und in deinem PuppenspielerSharp-Code

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

Pro-Tipp-: Für komplexe Layouts ist es manchmal einfacher, Ihr HTML mit expliziten Seitenumbrüchen zu strukturieren, anstatt den Browser zu bekämpfen.

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

Kopf- und Fußzeilen komplexer als Sie denken

Sie können Kopf- und Fußzeilen hinzufügen, aber die API ist ein bisschen wonky.

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

Ich weiß nicht, was ich tun soll.

  • Header-Vorlagen müssen valid HTML sein, sind aber extrem begrenzt.
  • Sie erhalten nur bestimmte Variablen. date, title, url, pageNumber, totalPages
  • Styling ist nur Inline
  • Die Ränder müssen groß genug sein, um Kopfzeilen zu beherbergen.

Hintergrundgrafiken

Standardmäßig ist dies ein Browser-Standard für das Speichern von Tinte. muss Aktivieren Sie es.

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

Speicherlecks mit großen Dokumenten

Das Problem Viele PDFs erzeugen bewirkt, dass Ihre Anwendung den Speicher von ' zum Ballon und schließlich zum Absturz bringt

Warum? Jede Browser-Instanz verwendet signifikanten Speicher, und wenn du nicht richtig entsorgst, stapeln sie hoch.

Die Lösung

Immer verwenden await using oder die richtige Entsorgung

// 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 High-- Volume PDF-Generierung, erwägen Sie die Verwendung von Browser-Instanzen

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

Die Skalierungsoption Mehr Inhalt

Manchmal müssen Sie mehr Inhalte auf eine Seite passen.

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

Aber seien Sie vorsichtig, zu klein und es ist nicht lesbar.

Real.-.World PDF-Erstellungsmuster

Hier ,' , wie ich eigentlich tun PDF-Generierung in der Produktion , : ,

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

Landschaft vs Portrait

Einfach, aber oft gebraucht

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

Benutzerdefinierte Seitengrößen

Nicht auf Standardformate beschränkt

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

Andere praktische Anwendungen für PuppenspielerSharp

Über die Test- und PDF-Generation hinaus, zeichnet sich der PuppeteerSharp bei mehreren anderen Automatisierungsaufgaben aus.

Web Scraping für Datenextraktion

PuppeteerSharp ist brillant für das Abkratzen von JavaScript-Websites, wo traditionelle HTML-Parser kurz fallen

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

Wann wird es verwendet?

  • Verschrottung einzelner Anwendungen der --Seite
  • Websites mit unendlicher Scroll oder faulen Laden
  • Wenn Sie mit der Seite interagieren müssen, klicken Sie auf Buttons.
  • Inhalt hinter Anmeldewänden

Wenn Sie es NICHT verwenden möchten

  • Einfaches statisches HTML-Schrotten HtmlAgilityPack oder Winkelscharf Stattdessen ist es viel schneller und leichter.
  • Hochhaltiges Abkratzen von (Browser Overhead ist signifikant
  • Wenn dort eine API verfügbar ist, bevorzugen Sie immer offizielle APIs als Scraping.

Automatisierte Screenshot-Erstellung

Über das Testen hinaus sind Screenshots für Thumbnails nützlich.

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

Praktische Verwendungen

  • Generierung von og-:-Bildtags für Blogposts
  • Erstellen von Thumbnails für Webseiten-Galerien
  • Archivierung von Webseiten für Compliance
  • Erstellen von Vorschaubildern für die Linkfreigabe

Leistungsüberwachung

Messung der Ladeleistung auf der Seite

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"]
        };
    }
}

Automatisierte Berichtserstellung

HTML Templating mit PDF-Generierung für automatisiertes Reporting kombinieren

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

Die Kosten für die Erstellung von PDF-Dateien

Jetzt ist die Sache über die Verwendung von PuppeteerSharp für PDF-Erzeugung.'s Kostenlos "s in dem Sinne, dass Sie nicht zahlen für eine PDF-Bibliothek Lizenz nicht frei in Bezug auf Ressourcen.

Jede Browser-Instanz.

  • Verwendet RAM-Speicher
  • Benötigt signifikante CPU für Rendering
  • dauert Sekunden, um ein PDF zu generieren, abhängig von Komplexität.

Vergleichen Sie dies mit dedizierten PDF-Bibliotheken wie

  • iText Vormals iTextSharp ,) , - , Kommerzielle Lizenz erforderlich , (~£500-3000/ , Jahr .), , erzeugt aber PDFs in Millisekunden mit winzigem Speicherfußabdruck
  • QuestPDF Kostenlos und Open Source unter MIT-Lizenz erzeugt PDFs aus fließendem C.M.S.K.2 code
  • PdfSharpCore Kostenlose MIT-Lizenz, aber eingeschränkter in den Fähigkeiten

Wann wird PuppeteerSharp für PDFs verwendet?

  • Sie haben bereits HTML-Templates und möchten im PDF-Layoutcode neu schreiben
  • Sie benötigen pixel---perfekte Rendering von komplexen Weblayouts
  • Das Volumen ist niedrig.
  • Sie müssen PDFs von externen Websites erzeugen, die Sie don-'-Steuerung

Wann Sie dedizierte PDF-Bibliotheken verwenden können

  • Hochvolumen-Erzeugung PDFs pro Stunde
  • Einfache Layouts Rechnungen Quittungen Berichte
  • Resource---beschränkte Umgebungen
  • Sie benötigen erweiterte PDF-Features.

Ein hybrider Ansatz

Manchmal ist die beste Lösung, beides zu verwenden.

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

Schlußfolgerung

PuppeteerSharp ist ein absolutes Spiel gewesen.--Wechsler für das Testen von E-2-E in meinen .-NET-Projekten, die schneller sind als Selenium., hat eine modernere API.MSC6- und macht im Allgemeinen weniger chore-MSC7-Test

Hier ist, was I'?d empfehlen

  1. Beginnen Sie mit PuppenspielerSharp wenn du nur Chrome testest, /; Chromium; .; es ist einfacher und schneller als die Alternativen

  2. Playwright verwenden wenn Sie Multi---Browser-Unterstützung benötigen - . - es hat alle Vorteile von PuppeteerSharp plus Firefox und Safari - .

  3. Selen vermeiden für neue Projekte, es sei denn, Sie haben einen bestimmten Grund, es zu verwenden IE.11. Unterstützung, die hoffentlich Sie tun ?'.t)..

  4. Schreiben Sie Tests vernünftigDie Tests sind langsam und können brüchig sein.Verwenden Sie sie für kritische Benutzerreisen, nicht zum Testen jedes kleinen Detail.

  5. Tests isoliert haltenJeder Test sollte seine eigenen Daten einrichten und nach sich selbst aufräumen.

  6. Hilfemethoden verwenden Die Basisklasse Muster, die ich zeigte hält Ihren tatsächlichen Testcode sauber und konzentriert sich auf das, was du testest ,e nicht wie Sie testet 'e es

Mit den richtigen Werkzeugen und Mustern kann es wirklich sehr angenehm sein...Give PuppeteerSharp a go on your next project. Ich denke, Sie werden angenehm überrascht sein.

Rechts ,, , I' , um mehr Tests zu schreiben , . , Happy Testing , ! ,

Literaturhinweise

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.