Back to "Fin-to-End Testing with PuppeteerSharp - Une solution de rechange adéquate à Selenium"

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

Fin-to-End Testing with PuppeteerSharp - Une solution de rechange adéquate à Selenium

Thursday, 27 November 2025

Moderne E2E (EndM SK2To-End~, Utilisant votre site comme les utilisateurs le feraientMSC5essais n’ont pas à être douloureux+'This comprehensive guide shows you how to use PuppeteerSharp for fast=, reliable browser automation in . NET -covering everything from basic testing to PDF generation and web scraping=. Whilst Microsoft MSC12 Playwright is the more modern multi MSC13 browser solution=MSC14 J’ai choisi PuppeterSharp pour ce blog parce que c’est ce que je savais et Chrome S’il suffit d’effectuer des tests seulement pour répondre à mes besoins SFC17 Si vous avez besoin de support Firefox et Safari Guide du dramaturge plutôt.

Introduction

Si vous avez déjà travaillé avec Selenium pour la fin-to-tests de fin , vousM SK3pourriez savoir qu’il peut être une douleur correcte à l’arrière-planMSC4 Entre le combat avec les versions des conducteursMST5 traitant d’essais fléchiers qui fonctionnent sur votre machine mais n’y sont pas ailleurs MST6 et la lenteur générale du protocole WebDriverMst7 ilM st8 est suffisant pour que vous voulez le mettre tout en place et tester manuellement au lieu de celaMSt9

Inscrire Écarpe de marionnettes - le port .NET de Google's Le marionnier library. C’est un cousin plus rapide, ' comme Selenium' plus jeuneM SK3 qui s’oblige à comparaître en temps opportun et ne vous oblige pas à télécharger dix-sept différents pilotes de navigateur.

Dans cet article, je vous expliquerai comment j’ai mis en œuvre PuppeteerSharp pour E sur ce blog.

Qu’est-ce qu’on entend par "puppeteerSharp" ?

Écarpe de marionnettes est une bibliothèque .NET qui fournit un haut niveau d’API pour contrôler les navigateurs Chrome ou Chromium en utilisant Chrome DevTools Protocole. À l'inverse Selenium, qui utilise le Protocole WebDriver (un protocole de câblodistribution JSON basé sur HTTP qui est plutôt inquiétant -), PuppeteerSharp parle directement au navigateur par l’entremise des DevToolsM SK3

Pensez-en ainsi:

  • Selenium: Comme envoyer des lettres par la poste pour communiquer avec votre navigateur
  • Écarpe de marionnettes: Comme avoir une ligne téléphonique directe avec le navigateur
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

Où le test E2E s’inscrit dans votre stratégie d’essai

Avant de plonger plus loin, laissez-nous dire où le test E s’inscrit dans le grand schéma des choses. Vous avez probablement entendu parler de la pyramide du test.

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

Examen de la réalité:

  • Essais d'unité (80% de vos essais ): FastM SK2 Cheap, Tester les fonctions individuellesMNK4 Mais ils ne vous disent pasMRK5 si le système fonctionne réellement dans son ensembleMMK6
  • Essais d'intégration (15% de vos essais ): Testez la façon dont les différentes parties travaillent ensemble. Plus vite que EM SK3E mais ne testez pas l’interface intégraleMSC4
  • Essais E2E (5% de vos essais ): Sous-couplé, coûteuxM SK3 mais tester le système exactement comme les utilisateurs l'experimentMSC4 C'est là où le PuppeteerSharp brilleMST5

Lorsque vous avez besoin d’un test E2E

  1. Voyages d'utilisateurs critiques - EnregistrementM SK1 Dépôt de commandes , Traitement des paiements. Si ces interruptions se produisent,
  2. JavaScript-UIs lourds - Spas modernes Réaction, Vue, Angulaire) où l'interface est rendue client
  3. Cross-problemes de navigateur - Différents navigateurs rendent les choses différemment.
  4. Interactions complexes - Multi-wizards de étapesM SK2 drag -andMNK4dropMRK5 téléchargements de fichiers MNK6

Lorsque vous DONT'T Besoin d’EM SK1E Tests:

  1. Opérations CRUD simples - Les essais d'intégration sont suffisants
  2. Logique pure - C'est ce que les essais unitaires sont pour le '
  3. Toutes les cases à bord - E 2 Les essais de l’E sont trop lentes et coûteux pour des essais exhaustifs

Pourquoi PuppeteerSharp sur Selenium?

Permettez-moi de compter les chemins:

  1. Aucun faf de gestion du conducteur: PuppeteerSharp télécharge et gère le navigateur Chrome pour vous . Il n’y a plus de camouflage avec les versions du ChromeDriver qui ne correspondent pas à la version Chrome installée'

  2. Exécution plus rapide: Le protocole DevTools est nettement plus rapide que WebDriver . Vos essais se dérouleront plus rapidement, et vousM SK3 passerez moins de temps à attendre des choses à se produire

  3. Améliorer l'API: L'API est plus moderne et intuitive . IlM SK2 est async/ attend tout le long de la routeMSC4 qui se prête parfaitement à l'évolution moderne

  4. Générée -dans le cadre d’un screenshot & génération PDF: Besoin d’un screenshot quand un test échoue ? C’est le plus simple avec PuppeteerSharp.

  5. Demandes d'interception du réseau: Vous pouvez intercepter les demandes de réseau avec facilité , modifier , ou bloquer des demandes

  6. Exécution appropriée de JavaScript: Exécutez JavaScript dans le contexte de la page et reçoisez les résultats d’une manière qui ne vous fait pas vouloir pleurer.

Établissement d'un sculpteur de marionnettes

Premièrement, ajouter le Écarpe de marionnettes NuGet package:

dotnet add package PuppeteerSharp

Voici la configuration de mon projet d’essai '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 utilisant xUnit (ASP.NET Core's defaultM SK3 mais PuppeteerSharp fonctionne aussi bien avec NUnit ou Essai MS.

Création d'une classe d'essai de base

Plutôt que de répéter l’installation/teardown code dans chaque essai , IM SK2 j’ai créé une classe de base (Mostlylucid.Test/E2E/E2ETestBase.cs:12) qui gère la gestion du cycle de vie des navigateurs

Structure de la classe

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

Nous mettons en œuvre IAsyncLifetime de xUnit, qui fournit une mise en place asynque/teardownM SK2 Contrairement aux constructeurs traditionnelsMSC3 cela nous permet d'attendre correctement l'initialisation du navigateurMNK4

Initialisation du navigateur

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

L'article 5 de la Loi sur l'immigration et le statut des réfugiés BrowserFetcher télécharge automatiquement une version compatible Chromium au premier démarrage - il n'est pas nécessaire de gérer le pilote manuellement . --no-sandbox Des drapeaux sont nécessaires pour les environnements Docker/CI.

Nettoyage

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

L’élimination appropriée est essentielle pour éviter les fuites de mémoire. Chaque instance de navigateur utilise 100-200MB de RAM .

Méthodes d'aide

La classe de base comprend les méthodes d'aide pour réduire la plaque de chaudière (Mostlylucid.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);
}

Ils manipulent les bits onéreux - attendant l’existence d’éléments , manipulant le temps d’arrêt gracieusement

Écrire des tests réels

A droite, laissez's aller aux bonnes choses - écrire des essais réelsM SK3 iciMSC4 c'est un test réel de mon blogMST5 sa fonctionnalité de barre filtrante MSP6Mostlylucid.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");
}

Ce test permet de vérifier que ma mise à jour linguistique fonctionne correctement.

Attribut « Passer »

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

I' j’ai passé cet essai par défaut parce qu’il faut que le site soit exploité localement . Pour les tests EM SK2E, vous voulez normalement les exécuter sur la demande, plutôt qu’avec tous les édifices.

Exécution de 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';
}");

Il s’agit d’une des régions où PuppeteerSharp brille absolument. Le EvaluateFunctionAsync method vous permet de faire fonctionner JavaScript dans le contexte du navigateur et d'obtenir le résultat en tant que type NET approprié.

Comparez cela à Selenium où vous avez besoin de

  1. Trouver l'élément
  2. Obtenir sa propriété d'affichage
  3. Tracer le résultat de la chaîne
  4. J’espère que ' n’est pas désuet au moment de le vérifier.

Évaluation des interactions HTMX

Mon blog utilise HTMX extensivement (server-réalisation des côtés sans écrire JavaScriptM SK2Cette section de test qui vérifie la fonctionnalité du tri (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");
}

La clé est la suivante : await WaitAsync(1000) après avoir changé la valeur de sélection. HTMX a besoin d’un moment pour faire sa demande et mettre à jour le DOM . Dans un monde parfait, nousM SK3d attendons qu’une requête spécifique du réseau soit remplieMSC4 mais dans des cas simplesMST5 un bref délai est bonMst6

Évaluation de la conception réactive

Voici'un test qui vérifie si ma barre de filtre est bien cachée sur les appareils mobiles.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
    });
}

Vous pouvez changer le port de visualisation à tout moment, ce qui est excellent pour tester les layouts réceptifs. beaucoup plus facile que la redimension manuelle de votre fenêtre de navigateur !

Caractéristiques avancées de PuppeteerSharp

Interception du réseau

L’une de mes caractéristiques préférées est la capacité d’intercepter et de modifier les demandes de réseau. C’est inestimable pour tester des états d’erreur ou des scénarios hors ligne:

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

Prendre des photos d'écran

Lorsqu'un essai échoue, un screenshot vaut mille messages de log:

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
}

Production de PDF

Vous pouvez même générer des fichiers PDF de pages, qui sont utiles pour tester le serveur- Rendement à côté ou imprimer les feuilles de styleM SK2

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

Couverture du code

PuppeteerSharp peut même recueillir des données sur la couverture par code JavaScript:

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 vs The Competition

Let's ont un bon aperçu de la façon dont PuppeteerSharp s’associe à d’autres outils d’essai.

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

La vieille Garde

Le Selenium est présent depuis 2004 et il montreM SK1 Il ' s’est matured, bien,- documentéMSC5 et appuie tous les navigateurs sous le soleil.

Pros:

  • Appuie tous les navigateurs (Chrome, FirefoxM SK2 Safari , EdgeMST4 IE si vous êtes un masochisteMSC6
  • Écosystème massif d'outils et de prolongements
  • Well- connu et largement adopté
  • Bon pour les tests de cross-browser

Cons:

  • Le protocole WebDriver est lent
  • La gestion des conducteurs est une douleur (même si WebDriverManager aide)
  • L'API se sent dépassé par rapport aux solutions de rechange modernes
  • Les tests d'échantillonnage sont fréquents en raison des problèmes de calendrier.
  • Aucune interception de réseau construite-

Quand l'utiliser: Lorsque vous avez absolument besoin de tester à travers plusieurs navigateurs, ou lorsque vous ' êtes déjà investi dans l’écosystème Selenium.

Playwright

The New Kid on the Block

Playwright est Microsoft' la réponse à Puppeteer, avec Soutien .NET fondé depuis le début. Il' est essentiellement PuppeteerSharp mais avec plusieurs supports de navigateur

Pros:

  • Appuie Chrome, Firefox, Safari (WebKitM SK3
  • API moderne similaire à Puppeteer
  • Auto-downloads les navigateurs
  • Élaboré-interception du réseau, screenshots , etcM SK3
  • Excellent soutien .NET

Cons:

  • Nouveaux, écosystème aussi petit
  • Peut être exagéré si vous n'avez besoin que de Chrome
  • Un réglage légèrement plus complexe en raison du support multi-browser

Quand l'utiliser: Lorsque vous avez besoin de multi-support pour le navigateur mais que vous voulez une API moderne . Si vous'entreprendrez un nouveau projet et avez besoin d’un croisementM SK3essai du navigateurMSC4 Playwright est probablement votre meilleure option

Cypresse

The JavaScript Developer's Darling

Cypress est brillant si vous ' travaillez en JavaScript/TypeScriptM SK2 mais ilMSC3est un non-starter pour les développeurs de .NETMNK6

Pros:

  • Une expérience fantastique pour les développeurs
  • Temps-débugging de voyage
  • Heure d'attente automatique
  • Une excellente documentation

Cons:

  • JavaScript/TypeScript seulement
  • Aucun soutien .NET
  • Peut't tester plusieurs onglets ou fenêtres
  • Limitée à l'essai de votre propre application (pas d'essais dans les domaines )

Quand l'utiliser: Don'tM SK1 vous'vous écrivez MSC3code NET . Stick to something that integrates with your tech stack

Qu’est-ce que vous devez utiliser ?

Voici ce que je prends :

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

Pour la plupart des .NET développeurs qui construisent des applications Web modernes

  • Chrome-tests seulement? → PuppeteerSharp
  • Multi-tests de navigateur? → Playwright
  • Utilisez déjà Selenium? → Pensez à migrer vers Playwright, mais ne le faites pas

Essais en cours dans CI/CD

E2E les essais sont tous bien et bon sur votre machine locale , mais ils doivent être effectués dans des pipelines de CI/CD aussiM SK3 VoiciMSC4 c’est comment j’ai établi les choses pour Actions de GitHub:

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

Les bits clés:

  1. Démarrer l'application dans le fond
  2. Il faut attendre qu’il soit sain (à l’aide d’un point final de contrôle de la santé
  3. Effectuer les tests E2E
  4. Télécharger les screenshots si des essais ont échoué
  5. Poursuivre toujours l’application, même si les essais ont échoué

Cas communs et comment les éviter

Évaluation de la qualité des aliments

E2E les essais peuvent être floqués - ils réussissent parfois et faillent parfois d’autres M SK2 Cela dépend généralement des questions de temps . Voici MSC4 comment les éviter MST5

Bad:

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

Bon:

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

Veuillez toujours attendre que l’élément avec lequel vous êtes en train d’interagir existe et soit visible.

Essai d'isolement

Chaque essai devrait être complètement indépendant. DonM SK1 ne dépendre pas de l’état des tests précédents :

Bad:

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

Bon:

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

Modèle d'objet de la page

Pour les pages complexes, utilisez le modèle Page Object pour que vos tests soient maintenus.

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

Considérations relatives au rendement

E2Les essais d’E sont plus lentes que les essais de l’unité.

Effectuer des essais parallèles

xUnit effectue des essais parallèles par défaut, mais vous devez être prudents quant à l'état partagé:

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

Disabler les fonctionnalités inutiles

Accélérer les essais en désactivant des fonctions que vous n’avez pas à utiliser

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

Utiliser judicieusement l'interception du réseau

bloquer les ressources inutiles pour accélérer les choses:

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

Débugging des essais E2E

Lorsque les essais ont échoué ( et qu'ils se produiront), vous devez les débugerM SK2 Voici quelques techniquesMSC3

Exécuter en mode non -Headless

Établissement Headless = false pour regarder le navigateur en action:

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

Utiliser DevTools

Vous pouvez en fait ouvrir DevTools programmatiquement:

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

Enregistrement de la console

Capture des messages de console du navigateur:

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

Demande d'enregistrement

Enregistrez toutes les demandes de réseau:

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

Modèles d'essai réels

Voici quelques exemples que j’utilise régulièrement dans mes essais E2E

Présentation du formulaire d'essai

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

Évaluation des interactions de clavier

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

Essai des téléchargements de fichiers

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

Essai de Drag and 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);
}

Intégration avec ASP.NET Core Testing

Vous pouvez intégrer PuppeteerSharp avec ASP.NET Core's WebApplicationFactory pour une expérience d’essai plus intégrée:

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

Au-delà des essais - PuppeteerSharp pour la production et l'automatisation de PDF

Alors que E2E essais est brillant, PuppeteerSharp est un couteau de l'Armée suisse qui peut faire beaucoup plusM SK2 Une des utilisations les plus populaires est la production de PDFs à partir du contenu Web. - c’est une utilité inoubliable pour ce genre d’outil.

Gérer des fichiers PDF - La promesse et la douleur

L'idée est simple: Rendre une page Web en Chrome et la sauvegarder comme un PDF. Perfect pour produire des facturesM SK2 Rapports , CertificatsMSC4 ou tout contenu dynamique qui doit être distribué dans le format PDF

Voici l’approche de base :

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

Il semble tout à fait simple.

PDF Generation Gotchas - Ce que personne ne vous dit

1. Font Embedding Nightmares

Le problème: Vos belles polices personnalisées ne apparaissent pas dans le PDF, ou pireM SK2 elles 'y sont, mais elles ressemblent absolument à de l’erreur.

Pourquoi cela se produit: Chrome a besoin d'avoir accès aux fichiers de caractères pendant la génération de PDF. Si vos caractères sont chargés via CDN externe et Chrome ne peut' les atteindre (firewallMSC3 problèmes de réseauM SK4 horaireMST5 vousMSL6vous êtes chargéeMSP7

La solution:

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

Même mieux, hébergez vos caractères localement ou les emplacez comme base 64 dans votre CSSM SK2 Oui, c'est une faffMSC5 mais c' est une fiabilité

2. Demandes de médias d'impression CSS

Le problème: Votre PDF ne ressemble rien à votre page Web parce que Chrome applique les demandes de médias imprimés.

Il s’agit en fait de comportement correct - Les PDF sont des médias imprimés. Mais ils atteignent tout le monde pour la première fois

La solution:

Utilisation @media print Règles de CSS appropriées:

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

Ou, si vous voulez la version écran dans votre PDF.

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

3. Interruptions de page - L'interdiction d'exister

Le problème: Votre contenu se divise mal entre les pages, avec des titres orphelins au bas ou des tableaux coupés en moitié.

La réalité: Vous combattez contre l’algorithme de pagination interne du Chrome, le ' et celui-ci gagnera la plupart des temps.

Ce que vous pouvez faire:

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

Et dans votre code PuppeteerSharp:

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

Conseil Pro: Pour les layouts complexes, il est parfois plus facile de structurer votre HTML avec des interruptions explicites plutôt que de lutter contre le navigateur:

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

4. Entêtes et crochets - Plus complexe que vous ne le pensez

Vous pouvez ajouter des en-têtes et des footers, mais l'API est un peu inquiétant:

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 templates doivent être valides HTML mais sont extrêmement limitées - pas de CSS externe , pas d'JavaScript
  • Vous obtiendrez uniquement des variables spécifiques: date, title, url, pageNumber, totalPages
  • Le style est en ligne seulement
  • Les marges doivent être suffisamment grandes pour accueillir les en-têtes/footers ou elles ' se chevauchent avec votre contenu

5. Graphique de fond

Par défaut, Chrome ne print pas les images de fond ou les couleurs (ce n'est un navigateur par défaut pour sauvegarder l'encreM SK3 Vous Il faut enablez-le:

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

6. fuites de mémoire avec de grands documents

Le problème: La production d’un grand nombre de fichiers PDF conduit à ce que votre application' se retrouve dans la mémoire du ballon et que celle-ci déclinne en bout de ligne.

Pourquoi: Chaque instance de navigateur utilise une mémoire significative (100-200MB), et si vous 'ne disposez pas correctementM SK3 elles se montent à l’avanceMSC4

La solution:

Utiliser toujours await using ou l’élimination appropriée:

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

Pour des formats PDF volumineux-génération de volumeM SK1 envisagez de réutiliser les instances du navigateur:

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. L'option d'étalonnage - Texte plus petitM SK2 Plus de contenu

Parfois, il faut placer plus de contenu sur une page:

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

Mais soyez prudents - trop petit et il ' est impréciseM SK2

Réel-Patern de génération PDF mondial

Voici'de quelle façon je réalise la production de PDF:

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

Landscape vs Portrait

Simple mais souvent nécessaire:

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

Taille de la page personnalisée

Non limité aux formats standard:

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

Autres utilisations pratiques pour PuppeteerSharp

Outre les tests et la production de PDF, PuppeteerSharp excelle dans plusieurs autres tâches d’automatisationM SK1 Let's explorer les applications réelles les plus courantes -world

Web Scraping pour l'extraction de données

PuppeteerSharp est parfait pour décraper JavaScript- sites lourds où les parsateurs HTML traditionnels ne sont pas à même de le faire:

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

Quand l'utiliser:

  • Applications de balayage unique-page MSC1ReactM SK2 Vue, AngularMST4
  • Sites avec une roulée infinie ou un chargement lazi
  • Lorsque vous avez besoin d'interagir avec la page (cliquez sur les boutonsM SK1 remplissez les formulaires ) avant de jeter
  • Contenu derrière les parois d'accès

Quand ne pas l'utiliser:

  • Traitement statique simple HTML (utiliser HtmlAgilityPack ou Écarpe d'angle plutôt - beaucoup plus rapide et plus légerM SK1
  • Haute -scrapage de volume (coût d’exploitation du navigateur est significatifM SK2
  • Lorsque l’API est disponible ', ( préfère toujours les API officiels plutôt que le triage

Production automatique de screenshots

Au-delà de la vérification, les screenshots sont utiles pour les aperçus d'images miniaturesM SK1 les préviews , ou l'archivage:

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

Usages pratiques:

  • Generating og:image tags for blog posts
  • Création de vignettes pour les galeries Web
  • Archivage des pages Web pour la conformité
  • Établissement d'images de prévision pour le partage des liens

Surveillance du rendement

Mesure de la performance du chargement des pages:

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

Production de rapports automatisés

Combiner le templage HTML avec la production PDF pour des rapports automatisés:

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

Coût de la génération PDF "Free"

Maintenant, , ici ' c'est le problème de l'utilisation du PuppeteerSharp pour la génération PDF - il est ' Il n'y a pas d'exemption de " vous ne payez pas pour une licence de bibliothèque PDF \ mais cela \ ' non gratuits en termes de ressources.

Chaque instance de navigateur:

  • Utilise 100-200MB de RAM
  • Besoins de CPU significatifs pour la traduction
  • Il faut 2-5 secondes pour produire un PDF (selon la complexitéM SK2

Comparez cela à des bibliothèques PDF dédiées comme:

  • iText (anciennement iTextSharpM SK1 - Licence commerciale requise (~£500-3000/année), mais produit des PDF en millisecondes avec une petite empreinte de mémoire
  • QuestPDF - Source libre et ouverte sous licence de l'MITM SK1 produit des PDF à partir d'un code fluide C# aucun HTML
  • PdfSharpCore - Licence MIT gratuite, mais plus limitée en capacités

Quand utiliser PuppeteerSharp pour PDF:

  • Vous avez déjà des modèles HTML et ne voulez pas réécrire dans le code de layout PDF
  • Vous avez besoin de pixel-réalisation parfaite des layouts Web complexes
  • Volume est faible (< 100 PDFs par heureM SK2
  • Vous devez générer des PDF provenant de sites externes que vous ne contrôlez pas.

Quand utiliser les bibliothèques PDF dédiées:

  • Production de volume élevé (> 100 PDFs par heure)
  • Logiciels simples (invoies, reçusM SK2 rapports )
  • Ressources-environnements restreints
  • Vous avez besoin de fonctionnalités PDF avancées (formsM SK1 signatures , encryption)

Une approche hybride

Parfois, la meilleure solution consiste à utiliser les deux :

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

Conclusion

PuppeteerSharp a été un jeu absolu.

Voici ce que je recommande :

  1. Démarrer avec PuppeteerSharp si vous testez seulement Chrome/Chromium. C’est plus simple et plus rapide que les solutions de remplacement

  2. Utilisez le Playwright si vous avez besoin de multi-support du navigateurM SK1 Il' a tous les avantages de PuppeteerSharp plus Firefox et Safari .

  3. Évitez le Selenium pour les nouveaux projets à moins que vous ayez une raison précise d'en utiliser.

  4. Rédiger les tests judicieusement. E 2 Les essais de l’E sont lentes et peuvent être fragilesM SK2 Utiliser ces tests pour les voyages critiques des utilisateurs, non pas pour tester tous les petits détailsMSC4 C’est ce que les essais d’unité et d’intégration sont pour

  5. Garder les essais isolés. Chaque essai doit établir ses propres données et nettoyer après lui-même

  6. Utiliser les méthodes d'aide libéralement. Le modèle de classe de base que j’ai montré maintient votre code d’essai propre et concentre ses efforts sur ce que vous testez, non sur la façon dont vous le testez.

E2Les essais en ligne ne doivent pas être douloureux 'il n’est pas nécessaire que les outils et les modèles appropriés soient utilisés pour le dépistage de . Il peut s’avérer très agréable avec ce type d’outils et de modèles.

Droit, JeM SK1Je suis à l’extérieur pour faire plus d’essais. Bonnes essais !

Autres lectures

logo

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