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

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
Сучасна E2E (кінець до кінця, використання вашого сайту, як це робили б користувачі) тестування не має бути болісною. Цей універсальний посібник показує вам, як використовувати PuppeterSharp для швидкого, надійного обробника навігатора у. NET, що визначає все від базового тестування до створення PDF і роботи з комп' ютером. Wilst, Playwright, є сучаснішим багатобровим рішенням, я обрав PuppeteerShide для цього блогу, тому що достатньо того, що я знав і Chrome для моїх потреб. Якщо вам потрібна підтримка Firefox і Safri, перевірте мою точність. [Довідник з Playwright](/blog/playwright-e2e-testing) Замість цього.

## Вступ

Якщо ви коли-небудь працювали з [Селен](https://www.selenium.dev/) У випадку боротьби з версіями драйверів, з тестами, які працюють на вашому комп'ютері, але ніде інше, і з загальною лінивістю протоколу WebDriver, достатньо, щоб ви могли викинути все і перевірити його вручну.

Ввести [PuppeterSap](https://www.puppeteersharp.com/) - порт Google.NET [Puppeer](https://pptr.dev/) Бібліотека. і не потребує завантаження сімнадцять різних водіїв браузера.

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

[TOC]

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

[PuppeterSap](https://www.puppeteersharp.com/) є бібліотекою.NET, яка надає високоякісний API для керування Chrome або Chromium за допомогою [Протокол Chrome DevToolsName](https://chromedevtools.github.io/devtools-protocol/). На відміну від [Селен](https://www.selenium.dev/), який використовує [Протокол WebDriverName](https://www.w3.org/TR/webdriver/) (краще незграбний протокол JSON дроту на основі HTTP), PuppeerShalp розмовляє безпосередньо з навігатором через DevTools.

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

- **Селен**: Як надсилання листів через допис для спілкування з браузером
- **PuppeterSap**: Наче мати пряму телефонну лінію до мозку браузера.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1. **Критичні подорожі користувача** - Вхід, отримання, платежна обробка.
2. **Складний інтерфейс JavaScript** - Сучасна САПА ([Реагувати](https://react.dev/), [Веfrance. kgm](https://vuejs.org/), [Кутовий](https://angular.dev/)) де інтерфейс користувача передається клієнтською стороною.
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](https://www.nuget.org/packages/PuppeteerSharp) Пакунок NuGet:

```bash
dotnet add package PuppeteerSharp
```

Ось моя конфігурація тестового проекту (`Mostlylucid.Test/Mostlylucid.Test.csproj:23`):

```xml
<PackageReference Include="PuppeteerSharp" Version="20.2.4" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
```

Я використовую [xUnit](https://xunit.net/) (типово для ядра ASP. NET), але PuppeterSap також працює з [NUnit](https://nunit.org/) або [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest).

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

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

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

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

namespace Mostlylucid.Test.E2E;

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

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

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

Ми реалізуємо [Несинхронний час життя](https://xunit.net/docs/shared-context#async-lifetime) за допомогою xUnit, за допомогою якого можна виконати асинхронне налаштування/ рівень вниз. На відміну від традиційних конструкторів, за його допомогою ми належним чином очікуємо ініціалізації навігатора.

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

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

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

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

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

### Очищення

```csharp
    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`):

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

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

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

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

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

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

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

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

Так, давайте перейдемо до хороших речей - написання справжніх тестів.`Mostlylucid.Test/E2E/FilterBarTests.cs:20-50`):

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

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

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

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

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

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

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

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

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

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

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

### Виконання JavaScript

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

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

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

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

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

Використання мого блогу [HTMX](https://htmx.org/) Широко (відтворення боку сервера без запису JavaScript). Ось тест, за допомогою якого можна перевірити порядок функціональних можливостей (`Mostlylucid.Test/E2E/FilterBarTests.cs:98-126`):

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

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

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

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

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

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

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

Ось щогла перевірка, яка перевіряє мою панель фільтрування, правильно прихована на мобільних пристроях.`Mostlylucid.Test/E2E/FilterBarTests.cs:216-245`):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Створення PDF

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

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

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

PuppeterSharp навіть може збирати дані покриття коду JavaScript:

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

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

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

## PuppeterSaption/ The Conjection

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

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

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

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

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

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

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

### Zelenium WebDride

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

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

**Прос:**

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

**Збори:**

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

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

### Playwright

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

[Playwright](https://playwright.dev/) Microsoft відповідає Puppeter за допомогою [Підтримка. NET](https://playwright.dev/dotnet/) випалений з початку. Це, по суті, PuppeterSap, але з підтримкою декількох гравців:

**Прос:**

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

**Збори:**

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

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

### Cypress

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

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

**Прос:**

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

**Збори:**

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

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

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

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

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

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

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

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

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

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

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

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

E2E-тести добре працюють на вашому локальному комп'ютері, але вони повинні працювати і на каналах CI/CD. [Дії GitHub](https://github.com/features/actions):

```yaml
name: E2E Tests

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

jobs:
  e2e-tests:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '9.0.x'

    - name: Install dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --no-restore

    - name: Start application
      run: |
        dotnet run --project Mostlylucid/Mostlylucid.csproj &
        echo $! > app.pid

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

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

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

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

Біти ключа:

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

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

### Гнучкий тест

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

**Погана:**

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

**Добре:**

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

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

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

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

**Погана:**

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

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

**Добре:**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    await ClickAsync("#upload-submit");

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

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

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

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

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

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

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

    await WaitAsync(500);

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

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

Ви можете інтегрувати PuppeterSharp за допомогою [АСП.NET Core WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests) для більш інтегрованого досвіду тестування:

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

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

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

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

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

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

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

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

### Створення PDF.

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

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

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

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

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

        return pdfData;
    }
}
```

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

### Покоління PDF.

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

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

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

**Вирішення:**

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

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

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

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

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

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

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

**Вирішення:**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**Попався:**

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

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

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

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

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

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

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

**Вирішення:**

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

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

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

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

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

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

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

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

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

#### 7. Параметр масштабу - менший текст, більше вмісту

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

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

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

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

Ось як я розробляю створення PDF:

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

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

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

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

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

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

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

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

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

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

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

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

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

            throw;
        }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return products;
    }
}
```

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

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

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

- Просте статичне знімання HTML (використовувати) [HtmlAglibibilityPack](https://html-agility-pack.net/) або [Кут & різкості](https://anglesharp.github.io/) замість цього - набагато швидше і світліше)
- Знімання з високою кількістю volume (накладне накладне значення)
- Коли доступний API (завжди надаєте перевагу офіційному API над скребками!)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- **[iText](https://itextpdf.com/)** (колишній iTextSharp) - Потрібна комерційна ліцензія (~ фунтів 500-3000/рік), але створює PDF в мілісекундах з невеликими відбитками пам'яті
- **[WickPDF](https://www.questpdf.com/)** - Вільне і відкрите програмне забезпечення з ліцензією MIT, створення PDF з коду Free C# (без HTML), палахкотіння швидко
- **[PdfSapCore](https://github.com/ststeiger/PdfSharpCore)** - Вільна ліцензія MIT, але обмежені можливості

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

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

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

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

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

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

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

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

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

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

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

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

## Висновки

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

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

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

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

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

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

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

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

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

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

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

- [Документація з PuppeteerSharp](https://www.puppeteersharp.com/)
- [API Puppeer](https://pptr.dev/) (JavaScript, але більшість концепцій застосовуються)
- [Playwright для.NET](https://playwright.dev/dotnet/)
- [Документація xUnit](https://xunit.net/)
- [Тести ядра ASP.NET](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)