Back to "Тестування наприкінці до кінця за допомогою PuppeterSap - Належна альтернатива селенію"

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

Тестування наприкінці до кінця за допомогою PuppeterSap - Належна альтернатива селенію

Thursday, 27 November 2025

Сучасна E2E (кінець до кінця, використання вашого сайту, як це робили б користувачі) тестування не має бути болісною. Цей універсальний посібник показує вам, як використовувати PuppeterSharp для швидкого, надійного обробника навігатора у. NET, що визначає все від базового тестування до створення PDF і роботи з комп' ютером. Wilst, Playwright, є сучаснішим багатобровим рішенням, я обрав PuppeteerShide для цього блогу, тому що достатньо того, що я знав і Chrome для моїх потреб. Якщо вам потрібна підтримка Firefox і Safri, перевірте мою точність. Довідник з Playwright Замість цього.

Вступ

Якщо ви коли-небудь працювали з Селен У випадку боротьби з версіями драйверів, з тестами, які працюють на вашому комп'ютері, але ніде інше, і з загальною лінивістю протоколу WebDriver, достатньо, щоб ви могли викинути все і перевірити його вручну.

Ввести PuppeterSap - порт Google.NET Puppeer Бібліотека. і не потребує завантаження сімнадцять різних водіїв браузера.

У цій статті я познайомлю вас з тим, як я реалізував PuppeterSapper для тестування E2E на цьому блозі, повністю зі справжніми прикладами коду з експропріатора. Ми охопимо тестування, створення PDF, веб- екранування і порівняємо його з альтернативами.

Що тоді таке PuppeterSap?

PuppeterSap є бібліотекою.NET, яка надає високоякісний API для керування Chrome або Chromium за допомогою Протокол Chrome DevToolsName. На відміну від Селен, який використовує Протокол WebDriverName (краще незграбний протокол JSON дроту на основі HTTP), PuppeerShalp розмовляє безпосередньо з навігатором через DevTools.

Подумайте про це так:

  • Селен: Як надсилання листів через допис для спілкування з браузером
  • PuppeterSap: Наче мати пряму телефонну лінію до мозку браузера.
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

Коли тест E2E підходить до вашої стратегії тестування

Перш ніж ми зануримося глибше, давайте поговоримо про те, де E2E тестування вписується у грандіозну схему речей.

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

Перевірка реальності

  • Одиничні тести Швидкі, дешеві окремі функції, але вони не кажуть вам, чи система працює взагалі.
  • Перевірки інтеграції (15% від ваших тестів): Перевірте як різні частини працюють разом. Швидше за E2E, але не випробовуйте повний інтерфейс.
  • E2E тести (5% тестів) Повільна, дорога, але випробуйте систему точно так, як це відчувають користувачі.

Коли ви потребуєте тестів E2E:

  1. Критичні подорожі користувача - Вхід, отримання, платежна обробка.
  2. Складний інтерфейс JavaScript - Сучасна САПА (Реагувати, Веfrance. kgm, Кутовий) де інтерфейс користувача передається клієнтською стороною.
  3. Проблеми інструменту перетину - Різні браузери все по-іншому (хоча з PuppeterSap ви тільки Хром-ли).
  4. Складна взаємодія - Майстер з декількома кроками, перетягування зі скиданням, вивантаження файлів.

Коли вам не потрібно тестів E2E:

  1. Проста операція CRUD - Интеграции достаточно.
  2. Чиста логіка - Ось для чого потрібні одиничні тести.
  3. Кожен регістр краю - Е2Е тесты слишком медленные и дорогое для проверки.

Чому PuppeerSharm over Cerenium?

Дозвольте мені порахувати шляхи:

  1. Faff не керує драйверами: Звантажується PuppeterSharp і керує переглядачем Chrom. Більше ніяких версій з версіями RhromeDriver, які не збігаються з вашою встановленою версією Chrome.

  2. Швидке виконання: Протокол DevTools значно швидший за WebDriver. Ваші тести будуть працювати швидше, і ви витрачатимете менше часу на те, щоб все сталося.

  3. Кращий API: API є більш сучасним і інтуїтивним. Це синхронний/зачеканий весь шлях вниз, який чудово пасує до сучасного розвитку.NET.

  4. Знімок вікна створення & PDF: Хочеш зробити знімок вікна, коли тест зазнає невдачі? Його просто використовують з PuppeterSap.

  5. Запити мережі для перевіркиComment: Ви можете перехоплювати, змінювати або блокувати запити у мережі з легкістю - блискуче для перевірки автономних сценаріїв або глумливих відповідей API.

  6. Належне виконання JavaScript: Виконайте JavaScript у контексті сторінки і повертайте результати так, щоб ви не захотіли плакати.

Налаштування PuppeterSap

Спочатку додайте PuppeterSap Пакунок NuGet:

dotnet add package PuppeteerSharp

Ось моя конфігурація тестового проекту (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>

Я використовую xUnit (типово для ядра ASP. NET), але PuppeterSap також працює з NUnit або MSTest.

Створення базового тесту класу

Замість повторювати код налаштовування/вниз під час кожного тесту, я створив базовий клас (Mostlylucid.Test/E2E/E2ETestBase.cs:12) з керуванням життєвим циклом у браузері:

Структура класу

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

Ми реалізуємо Несинхронний час життя за допомогою xUnit, за допомогою якого можна виконати асинхронне налаштування/ рівень вниз. На відміну від традиційних конструкторів, за його допомогою ми належним чином очікуємо ініціалізації навігатора.

Ініціалізація переглядача

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

The BrowserFetcher автоматично звантажить сумісну версію Chromium під час першого запуску - не потрібно буде керувати драйвером вручну. --no-sandbox Для середовищ Docker/CI потрібні прапорці.

Очищення

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

Правильне складання є важливим для того, щоб уникнути витікання пам' яті. Кожен з екземплярів переглядача використовує 100- 200МБ оперативної пам' яті.

Допоміжні методи

У базовий клас входять допоміжні методи, які допомагають зменшити кількість бойлерів (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);
}

Вони мають справу з стомливими бітами - чекаючи на існування елементів, граціозне проведення часу очікування, а також автоматичний лісозаготівель, коли в СІ не буде тестів.

Записування поточних тестів

Так, давайте перейдемо до хороших речей - написання справжніх тестів.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");
}

Ця перевірка перевіряє, чи правильно працює зі спадним списком слів. Погляньмо, що робить так:

Атрибут Пропустити

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

Типово, програма пропустив перевірку, оскільки для виконання тестів з E2E потрібно, щоб сайт було запущено локально. Для тестів E2E ви, зазвичай, бажаєте запускати його на комп' ютері, а не під час кожного збирання. Ви можете розблокувати їх, коли ви готові їх запускати, або запустити їх на окремій роботі CI, де ви маєте створити сайт.

Виконання 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';
}");

Це одна з ділянок, де PuppeterSap повністю сяє. EvaluateFunctionAsync За допомогою методу ви можете запустити JavaScript у контексті переглядача і повернути результат до відповідного типу. NET. У такому випадку я перевіряю, чи справді ви бачите спадний список (не лише у DOM) за допомогою обчислених вами стилів.

Порівняйте це з Селенієм, де вам потрібно:

  1. Знайти елемент
  2. Отримати властивість відображення
  3. Аналіз результату рядка
  4. Надеюсь, к тому времени, когда ты проверишь это, оно уже не действует.

Перевірка взаємодії з HTMX

Використання мого блогу HTMX Широко (відтворення боку сервера без запису JavaScript). Ось тест, за допомогою якого можна перевірити порядок функціональних можливостей (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");
}

Ключ тут await WaitAsync(1000) Після зміни вибраного значення. HTMX потребує часу, щоб зробити свій запит і оновити DOM. У досконалому світі, ми чекали б на завершення вказаного мережевого запиту, але для простих випадків, не затримуйся.

Перевірка складного дизайну

Ось щогла перевірка, яка перевіряє мою панель фільтрування, правильно прихована на мобільних пристроях.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
    });
}

У будь- який момент ви можете змінити область перегляду, яка є блискучою для перевірки розкладок реакції. Значно простішою за зміну розмірів вікна навігатора вручну!

Додаткові можливості PuppeterSharp

Переговори з мережею

Однією з моїх улюблених можливостей є здатність перехоплювати і змінювати мережеві запити. Ця можливість є безцінною для станів перевірки помилок або автономних сценаріїв:

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

Як робити знімки

Якщо спроба перевірки зазнала невдачі, знімок екрана вартує тисячі повідомлень журналу:

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

Ви навіть можете створювати PDF сторінок, які можуть бути корисними для перевірки показу на стороні сервера або друку таблиць стилів:

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

Обкладинка коду

PuppeterSharp навіть може збирати дані покриття коду 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}%");

PuppeterSaption/ The Conjection

Давайте поглянемо, як складатися PuppeterSharp з іншими інструментами для тестування E2E:

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

Zelenium WebDride

Стара вартова

Він зрілий, добре документований і підтримує кожен браузер під сонцем, але також показує свій вік:

Прос:

  • Підтримує всі навігатори (Rhom, Firefox, Safari, Edge, IE, якщо ви мазохіст)
  • Масивна екосистема інструментів і розширень
  • Відомий і широко прийнятий
  • Отримуємо результати перевірки між броузерами

Збори:

  • Протокол WebDriver повільний
  • Керування драйверами - це біль (хоча і допомагає веб-директора)
  • Здається, що API датований порівняно з сучасними альтернативами
  • Тести на флаксацію є звичайними через те, що виникають часові проблеми.
  • Немає вбудованої мережі перехоплення

Коли ним користуватися: Коли вам абсолютно потрібно перевірити декілька браузерів, або коли ви вже інвестували в екосистему Селенію.

Playwright

Новий малюк на блоці

Playwright Microsoft відповідає Puppeter за допомогою Підтримка. NET випалений з початку. Це, по суті, PuppeterSap, але з підтримкою декількох гравців:

Прос:

  • Підтримує Хром, Firefox, Safari (WebKit)
  • Сучасний API подібний до PuppeerName
  • Автоматично завантажувати переглядачі
  • Вбудоване перехоплення мережі, знімки вікон тощо.
  • Чудова підтримка. NET

Збори:

  • Новіша, менша екосистема.
  • Може бути забагато, якщо тобі потрібен тільки крим.
  • Легке складніше налаштування через підтримку багатоброймера

Коли ним користуватися: Якщо вам потрібна багаторядкова підтримка, але вам потрібна сучасна API. Якщо ви запускаєте новий проект і потребуєте тестування між броузерами, Playwright є, ймовірно, найкращим варіантом вашої справи.

Cypress

Дорога розробника JavaScript

Cypress є блискучим, якщо ви працюєте у JavaScript/ TypeScript, але це не перший інструмент для розробників. NET:

Прос:

  • Дивовижний досвід розробника
  • Зневаджування з часовими подорожами
  • Автоматичне очікування
  • Велика документація

Збори:

  • Лише JavaScript/TypeScript
  • Немає підтримки. NET
  • Неможливо протестувати декілька вкладок або вікон
  • Обмежено для тестування вашої власної програми (без тестування у доменах)

Коли ним користуватися: Нет, ты пишешь код NET, придерживайся чего-то, что соединяется с техническим стосом.

Отже, що вам слід використовувати?

Ось моя частка:

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

Для більшості розробників. NET побудовано сучасні веб- програми:

  • Проверка только на Хроме? → Дисперсія Puppeter
  • Мульти-бритуатр-тест? → Playwright
  • Уже использую Селиний? → Подумайте про міграцію до Playwright, але не поспішайте її.

Виконання тестів у CI/CD

E2E-тести добре працюють на вашому локальному комп'ютері, але вони повинні працювати і на каналах CI/CD. Дії 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

Біти ключа:

  1. Запустити програму у тлі
  2. Чекайте на його здоров'я (за допомогою кінцевої точки перевірки здоров'я)
  3. Запустити тести E2E
  4. Вивантажити знімки екрана, якщо спроба виконання будь- яких тестів зазнала невдачі
  5. Завжди зупиняйте програму, навіть якщо програма зазнає невдачі

Поширені пастки й як уникати їх

Гнучкий тест

E2E- тести можуть бути неточними - іноді вони проходять і зазнають невдачі від інших. Зазвичай, такі тести можна використовувати до часових питань. Ось як їх уникати:

Погана:

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

Добре:

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

Завжди чекати на елемент, з яким ви будете взаємодіяти, щоб існувати і бути видимим.

Випробовування

Кожен тест має бути повністю незалежним. Не покладайтеся на стан з попередніх тестів:

Погана:

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

Добре:

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

Шаблон об' єкта сторінки

Для створення складних сторінок скористайтеся шаблоном об' єкта сторінки, щоб зберігати ваші тести:

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

Обмірковування швидкодії

E2E- тести повільніші за одиничні тести, їх неможливо пройти. Але ви можете зробити їх швидшими:

Виконання тестів паралельно

Типово, xUnit виконує тести паралельно, але вам слід бути обережними з станом спільного ресурсу:

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

Вимкнути непотрібні можливості

Прискорити перевірку за допомогою вимикання можливостей, які вам не потрібні:

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

Мудро використовуйте обмін мережею

Блокувати непотрібні ресурси, щоб пришвидшити роботу:

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

Зневадження тестів E2E

Якщо спроба виконання тестів зазнала невдачі (і вони це зроблять), вам слід зневадити їх. Ось декілька методів:

Запуск у режимі безсилля

Встановити Headless = false для спостереження за переглядачем у дії:

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

Використовувати DaveTools

Насправді, ви можете відкрити програму DevTools:

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

Консольний журнал

Захоплення повідомлень консолі з переглядача:

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

Запитати журналювання

Записувати до журналу всі запити на мережу:

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

Шаблони тестування реального світу

Ось декілька шаблонів, які я регулярно використовую у тестах E2E:

Перевірка підходів форми

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

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

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

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

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

Перевірка дій за допомогою клавіатури

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

Перевірка вивантаження файлів

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

Перевірка перетягування зі скиданням

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

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

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

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

    await WaitAsync(500);

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

Інтеграція з тестуванням ядра ASP.NET

Ви можете інтегрувати PuppeterSharp за допомогою АСП.NET Core WebApplicationFactory для більш інтегрованого досвіду тестування:

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

Безперервне тестування - арфа Puppeter для створення і автоматизації PDF

Перевірка Whilst E2E є блискучим, PuppeterSap - це ніж зі швейцарської армії, який може робити набагато більше. Одним з найпопулярніших способів його використання є створення PDF з веб- вмісту - це неймовірно корисно для цього, хоча і не без його стволів. Якщо ви будуєте рахунки, звіти або будь- яку іншу систему створення документів, цей розділ збереже вам час усування вад.

Створення PDF.

Ідея проста: показати веб- сторінку у Хромі і зберегти її як PDF. Досконало для створення озвучень, звітів, сертифікатів або будь- якого динамічного вмісту, який слід розподілити у форматі PDF.

Ось основний підхід:

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

Я проведу тебе через міни.

Покоління PDF.

1. Вбудовані до шрифтів нічні марки

Проблема: Ваші чудові нетипові шрифти не з'являються у PDF, або ще гірше, вони там, але виглядають абсолютно безглуздо.

Чому це стається: Під час створення PDF програмі слід мати доступ до файлів шрифтів. Якщо ваші шрифти завантажуються за допомогою зовнішніх CDN і Chrome не зможуть досягти їх (firefall, network things, time), ви заповнюєте дані.

Вирішення:

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

Навіть краще, отримайте ваші шрифти локально або вбудуйте їх як base64 у ваш CSS. Так, це faff, але він надійний.

2. CSS друкує мультимедійні запити

Проблема: Ваша PDF зовсім не схожа на веб- сторінку, оскільки Кром застосовує запити на друк.

Це насправді виправлення поведінки - PDFs - це друковані носії, але вони виловлюють усіх вперше.

Вирішення:

Користування @media print Відповідні правила CSS:

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

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

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

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

Або, якщо вам потрібна версія екрана у вашій PDF (корисно для створення " знімків екрана " у форматі PDF):

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

3 Переривання сторінки.

Проблема: Ваш зміст дивно розколюється на сторінки, а заголовки осиротілі внизу або столи розрізані навпіл.

Реальність: Ви боретеся з внутрішнім алгоритмом пагінії Крома, і він виграє більшість часу.

Що ви можете зробити:

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

І у вашому коді PuppeerSharp:

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

Підказка: Для складних розкладок іноді простіше структурувати ваш HTML чіткими розривами сторінок, а не боротися з переглядачем:

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

4 Заголовки і нижні колонтитули.

Ви можете додавати заголовки і нижні колонтитули, але API є трохи популярним:

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

Попався:

  • Шаблони заголовків і підвалів повинні бути коректними HTML, але дуже обмежені - немає зовнішнього CSS, без JavaScript
  • Ви отримуєте лише специфічні змінні: date, title, url, pageNumber, totalPages
  • Styling є лише в рядку
  • Поля мають бути достатньо великими, щоб розмістити заголовки/ підніжки, інакше вони перекриють вміст.

5. Гравітація тла

Типово, Хром не друкує зображень тла або кольорів (це типовий переглядач для збереження чорнила). Ви має увімкнути:

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

6. Крила пам'яті з великими документами

Проблема: Створення багатьох PDF- програм призводить до аварійного завершення роботи у пам' яті вашої програми.

Чому: Кожен екземпляр переглядача використовує важливу пам' ять (100- 200МБ), і, якщо ви неправильно спростовуєте її належним чином, вони складаються.

Вирішення:

Завжди використовувати await using або відповідне усування:

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

Для створення PDF з високими volume вам слід повторно використовувати екземпляри переглядачів:

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. Параметр масштабу - менший текст, більше вмісту

Іноді вам потрібно вмістити більше інформації на сторінку:

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

Але будьте обережні - надто малі і їх неможливо прочитати.

Шаблон створення PDF у реальному світі

Ось як я розробляю створення 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>";
    }
}

Альбомна/ Книжкова

Просте, але часто потрібне:

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

Нетипові розміри сторінки

Не обмежується стандартними форматами:

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

Інші практичні поради щодо оргазму PuppeterSap

Окрім тестування і створення PDF, PuppeterSharp можна скористатися декількома іншими задачами для автоматизації. Давайте дослідимо найпоширеніші програми у реальному світі.

Веб-raping для видобування даних

PuppeterSharp чудово пасує до зіпсованих сайтів JavaScript, у яких не вистачає традиційних засобів обробки HTML:

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

Коли його використовувати:

  • Програма для роботи з однією сторінкою (Реакція, Вуе, Кутовий)
  • Сайти з нескінченним сувоєм або лінивим завантаженням
  • Якщо вам потрібно взаємодіяти зі сторінкою (клацання кнопками, заповнення форм) перед вилученням
  • Вміст за стінками входу

Якщо НЕ використовувати його:

  • Просте статичне знімання HTML (використовувати) HtmlAglibibilityPack або Кут & різкості замість цього - набагато швидше і світліше)
  • Знімання з високою кількістю volume (накладне накладне значення)
  • Коли доступний API (завжди надаєте перевагу офіційному API над скребками!)

Автоматизоване створення знімків екрана

За межами тестування знімки вікон можуть бути корисними для мініатюр, попереднього перегляду або архівування:

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

Практичне використання:

  • Створення теґів og: зображень для дописів блогів
  • Створення мініатюр для галерей веб- сайтів
  • Архівування веб- сторінок для виконання
  • Створення зображень попереднього перегляду для спільного використання посилань

Спостереження за швидкодією

Вимірювання швидкодії завантаження сторінки:

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

Покоління звітів Automed

Об' єднати зміни HTML з створенням PDF для автоматичного звіту:

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

Вартість створення PDF " вільного "

Ось що можна сказати про використання PuppeterSapper для створення PDF - це "безкоштовно" в тому розумінні, що ви не платите за ліцензію з бібліотеки PDF, але це - не вільна для ресурсів.

Кожен екземпляр переглядача:

  • Використовує 100- 200МБ ОЗП
  • Вимагає значного процесора для показу
  • Забирається 2- 5 секунд для створення PDF (залежить від складності)

Порівняйте це з відзначеними бібліотеками PDF на зразок:

  • iText (колишній iTextSharp) - Потрібна комерційна ліцензія (~ фунтів 500-3000/рік), але створює PDF в мілісекундах з невеликими відбитками пам'яті
  • WickPDF - Вільне і відкрите програмне забезпечення з ліцензією MIT, створення PDF з коду Free C# (без HTML), палахкотіння швидко
  • PdfSapCore - Вільна ліцензія MIT, але обмежені можливості

Якщо слід використовувати PuppeterSharp для PDFs:

  • Ви вже маєте шаблони HTML і не хочете перезаписувати у коді розкладки PDF
  • Вам потрібне максимізоване відображення складних веб- розкладок
  • Гучність мала ( < 100 PDFs на годину)
  • Вам слід створити PDF з зовнішніх веб-сайтів, якими ви не керуєте

Коли використовувати застарілі бібліотеки PDF:

  • Створення високої гучності ( > 100 PDF на годину)
  • Просте компонування (записи, квитанції, звіти)
  • Об' єднані для ресурсів середовища
  • Вам потрібні додаткові можливості PDF (форми, підписи, шифрування)

Гібридний підхід

Іноді найкращим вирішенням є використання обох варіантів:

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

Висновки

PuppeterSapper - це абсолютний ігровий інструмент для тестування E2E в моїх проектах. NET. Він швидший за Celenium, має більш сучасний API, і, як правило, робить тестування менш важким.

Ось що я пропоную:

  1. Почати з PuppeterSap Якщо ви тестуєте тільки Хром/Хромій, він простіший і швидший за альтернативи.

  2. Використовувати Playwright якщо вам потрібна підтримка декількох бровів. вона має всі переваги PuppeterSap плюс Firefox і Safari.

  3. Уникайте селенію у нових проектах, якщо у вас не є специфічних причин для його використання (таких, як підтримка IE11, які, на щастя, ви не використовуєте).

  4. Тести запису з розсудливістю. E2E- тести повільні і можуть бути крихкими. Скористайтеся ними для критичних подорожей користувачів, а не для тестування кожної дрібної деталі. Ось для чого потрібні одиниці і перевірки інтеграції.

  5. Зберігати аналізи. Кожен тест повинен налаштувати власні дані і очиститися після себе.

  6. Використовувати допоміжні методи Шаблон базового класу, який я показав, зберігає ваш тестовий код чистим і зосереджений на тому, що ви перевіряєте, а не на тому, як ви його тестуєте.

Тестування E2E не обов'язково буде болісним. що ви будете приємно здивовані.

Так, я хочу написати більше тестів.

Подальше читання

logo

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