# Lopputestaus PuppeteerSharpilla - oikea vaihtoehto seleniumille

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
Modern E2E (End-To-End, käyttäen sivustoasi kuten käyttäjät haluaisivat) -testauksen ei tarvitse olla tuskallista. Tämä kattava opas näyttää, miten PuppeteerSharp -mallia käytetään nopeaan ja luotettavaan selainautomaatioon .NET-verkossa, joka kattaa kaiken perustestauksesta pdf-sukupolveen ja verkkokaapimiseen. Microsoftin Playwright on modernimpi monikirjainratkaisu, mutta valitsin PuppeteerSharpin tähän blogiin, koska se on se, minkä tiesin ja Chrome-testi vain riitti tarpeisiini. Jos tarvitset Firefox- ja Safari-tukea, tarkista, mitä tarvitsen. [Näytelmäkirjailijan opas](/blog/playwright-e2e-testing) Sen sijaan.

## Johdanto

Jos olet koskaan työskennellyt [Seleeni](https://www.selenium.dev/) Lopputestauksesta tiedät, että se voi olla oikea kipu takaraivossa. Välillä painitaan kuljettajaversioiden kanssa, käsitellään hilseileviä testejä, jotka toimivat koneella, mutta eivät missään muualla, ja WebDriverin protokollan yleinen hitaus saa sinut haluamaan heittää kaiken sisään ja testata käsin.

Syötä [PuppeteerSharp](https://www.puppeteersharp.com/) - Googlen .net-portti [Puppeteer](https://pptr.dev/) Kirjasto. Se on kuin Seleniumin nuorempi, nopeampi serkku, joka vaivautuu tulemaan ajoissa eikä vaadi lataamaan seitsemäätoista eri selainajuria.

Tässä artikkelissa kerron, miten olen toteuttanut PuppeteerSharp for E2E -testauksen juuri tässä blogissa, täydennettynä repo-ohjelman todellisilla koodiesimerkeillä. Käsittelemme testausta, PDF-sukupolvea, verkkokaappausta ja vertaamme sitä vaihtoehtoihin.

[TOC]

## Mitä PuppeteerSharp sitten on?

[PuppeteerSharp](https://www.puppeteersharp.com/) on .NET-kirjasto, joka tarjoaa korkean tason API:n Chrome- tai Chrome-selainten hallitsemiseksi [Chrome DevTools -protokolla](https://chromedevtools.github.io/devtools-protocol/)Toisin kuin [Seleeni](https://www.selenium.dev/), jossa käytetään [WebDriver-protokolla](https://www.w3.org/TR/webdriver/) PuppeteerSharp puhuu suoraan selaimelle DevToolsin kautta.

Ajattele asiaa näin:

- **Seleeni**: Kuten kirjeiden lähettäminen postitse kommunikoidaksesi selaimesi kanssa
- **PuppeteerSharp**: Kuin suora puhelinlinja selaimen aivoihin

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

### Missä E2E-testaus sopii testausstrategiaasi

Ennen kuin sukeltamme syvemmälle, puhutaan siitä, missä kohtaa E2E-testit sopivat suureen kokonaisuuteen. Olet luultavasti kuullut testipyramidista - näin se käytännössä toimii:

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

**Todellisuustarkistus:**

- **Yksikkötestit** (80 % kokeista): Nopeat, halvat, yksilölliset toiminnot, mutta ne eivät kerro, toimiiko järjestelmä kokonaisuutena.
- **Kotoutumistestit** (15 % testeistä): Testaa, kuinka eri osat toimivat yhdessä. Nopeampi kuin E2E, mutta älä testaa koko käyttöliittymää.
- **E2E-testit** Viisi prosenttia testeistäsi: Hidas, kallis, mutta testaa järjestelmää juuri niin kuin käyttäjät kokevat sen. PuppeteerSharp loistaa tässä.

**Kun tarvitset E2E-testejä:**

1. **Kriittiset käyttäjämatkat** Kirjautuminen, uloskirjautuminen, maksujen käsittely.
2. **Javascript-raskas UIs** - Nykyaikaiset erityissuojelualueet ([Reaktio](https://react.dev/), [Vue](https://vuejs.org/), [Kulma](https://angular.dev/)), jossa UI on asiakaspuolella.
3. **Ristiin selaamiseen liittyvät kysymykset** - Eri selaimissa asiat muuttuvat (vaikka PuppeteerSharpin kohdalla olet vain Chrome).
4. **Kompleksiset yhteisvaikutukset** Monivaiheiset velhot, raahaus ja pudotus, tiedostojen lataaminen.

**Kun et tarvitse E2E-testejä:**

1. **Yksinkertaiset CRUD-toiminnot** Kotoutumistestit riittävät.
2. **Puhdasta logiikkaa** - Sitä varten on yksikkötestit.
3. **Jokainen reunalaatikko** - E2E-testit ovat liian hitaita ja kalliita tyhjentävään testaukseen.

### Miksi PuppeteerSharppasi seleniumia?

Anna kun lasken tapoja:

1. **Ei kuljettajien hallintafaffia**PuppeteerSharp lataa ja hallinnoi Chrome-selainta puolestasi. ChromeDriverin versiot eivät enää vedä vertoja asennetulle Chrome-versiolle.

2. **Nopeampi suoritus**DevTools Protokolla on huomattavasti nopeampi kuin WebDriver. Testisi sujuvat nopeammin, ja odotat asioita vähemmän.

3. **Parempi API**: API on modernimpi ja intuitiivismpi. Se on async/aodotus koko matkan alaspäin, mikä sopii kauniisti modernin .NET-kehityksen kanssa.

4. **Sisäänrakennettu kuvakaappaus ja PDF-sukupolvi**Haluatko kuvakaappauksen, kun testi epäonnistuu? PuppeteerSharp on yksinkertainen.

5. **Verkkopyynnöt**: Voit siepata, muokata tai estää verkkopyyntöjä helposti – loistavasti offline-skenaarioiden testaamiseen tai API-vastausten pilkkaamiseen.

6. **Oikea javascript-teloitus**: Suorita JavaScript sivukontekstissa ja hanki tulokset takaisin tavalla, joka ei saa sinua itkemään.

## PuppeteerSharpin asennus

Ensinnäkin: [PuppeteerSharp](https://www.nuget.org/packages/PuppeteerSharp) NuGet-paketti:

```bash
dotnet add package PuppeteerSharp
```

Tässä on testiprojektin kokoonpanoni (`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>
```

Käytän [xUnit](https://xunit.net/) (ASP.NET Coren oletus), mutta PuppeteerSharp toimii yhtä hyvin [NUNIT](https://nunit.org/) tai [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest).

## Peruskoeluokan luominen

Sen sijaan, että olisin toistanut asetus- ja kaatokoodia jokaisessa testissä, olen luonut perusluokan (`Mostlylucid.Test/E2E/E2ETestBase.cs:12`), joka käsittelee selaimen elinkaaren hallintaa:

### Luokan rakenne

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

Toteutamme [I AsyncLifetime](https://xunit.net/docs/shared-context#async-lifetime) XUnitista, joka tarjoaa async-asetukset/teroitukset. Toisin kuin perinteiset rakentajat, tämä antaa meidän odottaa kunnolla selaimen alustusta.

### Selainalustaus

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

Erytropoietiini `BrowserFetcher` Lataa automaattisesti yhteensopivan Chromium-version ensimmäisellä kierroksella - manuaalista ohjauksen hallintaa ei tarvita. `--no-sandbox` Docker/CI-ympäristöihin vaaditaan liput.

### Siivous

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

Oikea hävittäminen on tärkeää muistivuotojen välttämiseksi. Jokainen selain-installaatio käyttää 100-200MB RAM-muistia.

## Auttajan menetelmät

Perusluokkaan kuuluu apukeinot kattilalevyn vähentämiseksi (`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);
}
```

Nämä käsittelevät tylsiä kappaleita - odottavat alkuaineiden olemassaoloa, viehkeää aikakatkaisun käsittelyä ja automaattista kirjautumista, kun testit epäonnistuvat CI:ssä.

## Todellisten testien kirjoittaminen

Siirrytään hyviin juttuihin - todellisten testien kirjoittamiseen. Tässä todellinen testi blogini suodatinpalkkitoiminnosta (`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");
}
```

Tässä kokeessa tarkistetaan, että kielenpudotukseni toimii oikein. Katsotaan, mikä saa sen toimimaan:

### Ohitusattribuutti

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

Olen jättänyt tämän testin väliin oletuksena, koska se edellyttää, että sivusto on toiminnassa paikallisesti. E2E-testeissä haluat yleensä ajaa ne tilauksesta eikä joka rakenteella. Voit irrottaa ne, kun olet valmis ajamaan ne, tai tehdä ne erillisessä Cl-tehtävässä, jossa olet saanut sivuston pyörimään.

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

Tämä on yksi niistä alueista, joilla PuppeteerSharp ehdottomasti loistaa. `EvaluateFunctionAsync` Menetelmän avulla voit ajaa JavaScriptin selainkontekstissa ja saada tuloksen takaisin oikeana .NET-tyyppisenä. Tässä tapauksessa tarkistan, näkyykö pudotuspeli (ei vain esiintyä DOM:ssa) katsomalla sen laskelmia tyylejä.

Vertaa tätä Seleniumiin, jossa sinun pitäisi:

1. Etsi elementti
2. Hanki sen näyttöominaisuus
3. Jaa merkkijonon tulos
4. Toivottavasti se ei ole tunkkaista, kun tarkistat sen

### HTMX-yhteisvaikutusten testaus

Blogini käyttää [HTMX](https://htmx.org/) Laajamittaisesti (server-side renderointi ilman kirjoitusta JavaScript). Tässä testi, jossa tarkistetaan lajittelutoiminnot (`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");
}
```

Avain tässä on `await WaitAsync(1000)` Kun valittu arvo on vaihdettu. HTMX tarvitsee hetken aikaa esittääkseen pyyntönsä ja päivittääkseen DOM:n. Täydellisessä maailmassa odottaisimme tietyn verkkopyynnön valmistumista, mutta yksinkertaisissa tapauksissa lyhyt viive on hyvä.

### Testaa vastuullista suunnittelua

Tässä on röyhkeä testi, joka tarkistaa, että suodatinpalkkini on kunnolla piilotettu mobiililaitteisiin (`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
    });
}
```

Voit muuttaa näkymää milloin tahansa, mikä on loistavaa reagoivien asettelujen testaamiseen. Paljon helpompaa kuin selainikkunan uudelleenlaajentaminen manuaalisesti!

## Advanced PuppeteerSharp Features

### Verkkohäirintä

Yksi suosikkiominaisuuksistani on kyky siepata ja muokata verkkopyyntöjä. Tämä on korvaamaton keino testata virhetilanteita tai offline-skenaarioita:

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

### Kuvakaappauksia

Kun testi epäonnistuu, kuvakaappauksen arvo on tuhat lokiviestiä:

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

Voit jopa luoda PDF-sivuja, joista on hyötyä palvelimen renderöinnin tai tyylisivujen testauksessa:

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

### Koodin kattavuus

PuppeteerSharp voi jopa kerätä JavaScript-koodin peittotietoja:

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

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

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

## PuppeteerSharp vs. kilpailu

Katsotaanpa kunnolla, kuinka PuppeteerSharp pinoutuu muihin E2E-testaustyökaluihin:

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

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

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

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

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

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

### Selenium WebDriver

**Vanha kaarti**

Seleniumia on ollut olemassa vuodesta 2004 lähtien, ja se näkyy. Se on kypsä, hyvin dokumentoitu ja tukee jokaista auringon alla olevaa selainta. Mutta se näyttää myös ikänsä:

**Plussat:**

- Tukee kaikkia selaimia (Chrome, Firefox, Safari, Edge, IE, jos olet masokisti)
- Työkalujen ja laajennusten laaja ekosysteemi
- Tunnettu ja laajalti omaksuttu
- Hyvä ristiselkätestiin

**Miinukset:**

- WebDriver-protokolla on hidas
- Kuljettajan hallinta on hankalaa (vaikka WebDriverManager auttaa)
- API tuntuu moderniin vaihtoehtoon verrattuna ajoitetulta
- Flaky-testit ovat yleisiä ajankohtaongelmien vuoksi
- Ei sisäänrakennettua verkkokuuntelua

**Milloin sitä käytetään:** Kun sinun täytyy ehdottomasti testata useita selaimia, tai kun olet jo investoinut Selenium-ekosysteemiin.

### Näytelmäkirjailija

**Uusi poika korttelissa**

[Näytelmäkirjailija](https://playwright.dev/) on Microsoftin vastaus Puppeteerille, [.NET-tuki](https://playwright.dev/dotnet/) Se on pääasiassa PuppeteerSharp, mutta moniselitteisellä tuella:

**Plussat:**

- Tukee Chromea, Firefoxia, Safaria (WebKit)
- Moderni API muistuttaa Puppeteeria
- Lataa selain automaattisesti
- Sisäänrakennettu verkkokuuntelu, kuvakaappaukset jne.
- Erinomainen .NET-tuki

**Miinukset:**

- Uudempi, niin pienempi ekosysteemi
- Voi olla liioittelua, jos tarvitset vain Chromea
- Moniselitteisen tuen vuoksi hieman monimutkaisempi asennus

**Milloin sitä käytetään:** Kun tarvitset multi-selaustukea, mutta haluat modernin API:n. Jos olet aloittamassa uutta projektia ja tarvitset ristiselostustestejä, Playwright on todennäköisesti paras vaihtoehtosi.

### Sypressi

**JavaScript-kehittäjän kulta**

Cypress on loistava, jos olet töissä JavaScript/TypeScriptissä, mutta se ei käynnisty .NET-kehittäjille:

**Plussat:**

- Fantastinen kehittäjäkokemus
- Aikamatkustamisen vianetsintä
- Automaattinen odottelu
- Hienot asiakirjat

**Miinukset:**

- Vain JavaScript/TypeScript
- Ei .NET-tukea
- Useita välilehtiä tai ikkunoita ei voi testata
- Rajoitettu oman sovelluksen testaamiseen (ei testaamista eri toimialueilla)

**Milloin sitä käytetään:** Älä, kirjoitat .net-koodia.

### Mitä sinun sitten pitäisi käyttää?

Tässä osuuteni:

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

Useimmille .NET-kehittäjille, jotka rakentavat nykyaikaisia verkkosovelluksia:

- **Vain Chrome-testi?** → PuppeteerSharp
- **Moniselitteinen testaus?** → Näytelmäkirjailija
- **Käytätkö jo Seleniumia?** → Harkitse muuttoa Playwrightiin, mutta älä kiirehdi sitä

## Suoritetaan CI/CD-testit

E2E-testit ovat hyviä paikallisessa koneessa, mutta niiden pitää toimia myös CI/CD-putkistoissa. [GitHub-toimet](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
```

Avainkohdat:

1. Aloita sovellus taustalla
2. Odota, että se on terve (käyttäen terveystarkastuksen päätepistettä)
3. Suorita E2E-testit
4. Lähetä kuvakaappauksia, jos jokin testi epäonnistuu
5. Keskeytä sovellus aina, vaikka testit eivät onnistuisikaan

## Yleiset ansat ja niiden välttäminen

### Flaky-testit

E2E-testit voivat olla epäluotettavia - ne menevät joskus ohi ja epäonnistuvat toisissa. Yleensä kyse on ajoituksesta. Näin vältät ne:

**Huono:**

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

**Hyvä:**

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

Odota aina, että elementti, jonka kanssa olet vuorovaikutuksessa, on olemassa ja näkyvä.

### Testauksen eristäminen

Jokaisen testin tulisi olla täysin riippumaton. Älä luota aiempien testien tilaan:

**Huono:**

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

**Hyvä:**

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

### Sivun objektikuvio

Monimutkaisille sivuille, käytä Page Object -mallia pitääksesi testisi kunnossa:

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

## Suorituskykyä koskevia huomioita

E2E-testit ovat hitaampia kuin yksikkötestit, niitä ei voi kiertää, mutta niistä voi tehdä nopeampia:

### Suorita rinnakkaistestit

XUnit tekee oletuksena rinnakkaistestejä, mutta jaetun tilan suhteen on oltava varovainen:

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

### Tarpeettomien ominaisuuksien poistaminen käytöstä

Nopeuta testejä poistamalla ominaisuuksia, joita et tarvitse:

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

### Käytä verkon katkaisua viisaasti

Vähennä tarpeettomia resursseja asioiden nopeuttamiseksi:

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

## Vianetsintä E2E-testeissä

Kun testit epäonnistuvat (ja ne epäonnistuvat), sinun on debuggattava ne. Tässä muutamia tekniikoita:

### Suorita ei-päättömässä tilassa

Aseta `Headless = false` Katsoaksesi selainta toiminnassa:

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

### Käytä DevToolsia

Voit avata DevToolsin ohjelmallisesti:

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

### Konsolidointi lokiin

Kaappaa konsoliviestit selaimesta:

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

### Pyydä kirjautumista

Kirjaa kaikki verkkopyynnöt:

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

## Reaalimaailman testimallit

Tässä muutamia malleja, joita käytän säännöllisesti E2E-testeissäni:

### Testauslomakkeet

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

### Näppäimistön vuorovaikutusten testaus

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

### Testataan tiedostojen latausta

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

### Vedon ja pudotuksen testaus

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

## Integrointi ASP.NET-ydintestaukseen

Voit integroida PuppeteerSharpin [ASP.NET Coren Web ApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests) Integroidumpi testauskokemus:

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

## Beyond Testing - PuppeteerSharp for PDF Generation and Automation

Vaikka E2E-testit ovat nerokkaita, PuppeteerSharp on Sveitsin armeijan veitsi, joka voi tehdä paljon enemmän. Yksi sen suosituimmista käyttökohteista on pdf:ien tuottaminen web-sisällöstä - se on uskomattoman hyödyllinen tähän, vaikkakaan ei ilman gotcha-tietoja. Jos rakennat laskuja, raportteja tai minkä tahansa asiakirjasukupolven järjestelmän, tämä osio säästää sinulta tunteja vianetsintää.

### PDF:ien luominen - lupaus ja kipu

Idea on yksinkertainen: tee nettisivu Chromeen ja tallenna se PDF-muodossa. Täydellinen laskujen, raporttien, sertifikaattien tai minkä tahansa dynaamisen sisällön tuottamiseen, joka pitää jakaa pdf-muodossa.

Tässä on peruslähestymistapa:

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

Se näyttää yksinkertaiselta, kunnes ei ole.

### PDF Generation Gotchas - Mitä kukaan ei kerro

#### 1. Fontin upottamat painajaiset

**Ongelma:** Kauniit fonttisi eivät näy pdf:ssä, tai mikä pahempaa, ne ovat siellä, mutta näyttävät aivan surkeilta.

**Miksi näin tapahtuu:** Chrome tarvitsee pääsyn fonttitiedostoihin PDF-sukupolven aikana. Jos fonttisi ladataan ulkoisen CDN:n kautta, eikä Chrome pääse niihin (paloseinä, verkko-ongelmat, ajoitus), olet tukossa.

**Ratkaisu:**

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

Vielä parempi on, että isännöit fonttejasi paikallisesti tai liität ne CSS:ään base64:nä. Kyllä, se on kummallista, mutta luotettavaa.

#### 2. CSS Print Media -kyselyt

**Ongelma:** PDF:si ei näytä lainkaan nettisivuiltasi, koska Chrome soveltaa painettuja mediakyselyitä.

Tämä on itse asiassa **oikea käytös** - PDF:t ovat printtimediaa, - mutta se nappaa kaikki ulos ensimmäisellä kerralla.

**Ratkaisu:**

Käyttö `@media print` CSS-säännöt ovat tarkoituksenmukaiset:

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

Tai, jos haluat näyttöversion pdf-muodossasi (käyttää "kuvakaappauksia" PDF-muodossa):

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

#### 3. Page Breaks - Bane of Your Existing

**Ongelma:** Sisältösi jakautuu oudosti sivuille, ja otsikot ovat alareunassa tai pöydät on leikattu kahtia.

**Todellisuus:** Taistelet Chromen sisäistä paginaatioalgoritmia vastaan, ja se voittaa suurimman osan ajasta.

**Mitä voit tehdä:**

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

Ja PuppeteerSharp-koodissasi:

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

**Pro vinkki:** Monimutkaisissa asetelmissa HTML:ää on joskus helpompi jäsentää selkeillä sivukatkoilla kuin taistella selainta vastaan:

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

#### 4. Otsikko ja Footers - monimutkaisempi kuin luulisi

Otsikkoja ja foottereita voi lisätä, mutta API on hieman vinossa:

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

**Selvä.**

- Otsikko-/footer-mallien on oltava voimassa olevia HTML-malleja, mutta ne ovat erittäin rajallisia - ei ulkoista CSS:ää, ei JavaScriptia
- Saat vain tiettyjä muuttujia: `date`, `title`, `url`, `pageNumber`, `totalPages`
- Styling on vain linjalla
- Marginaalien on oltava riittävän suuret, jotta ne mahtuvat otsaan tai jalkaan, tai ne menevät päällekkäin sisältösi kanssa.

#### 5. Taustagrafiikka

Oletuksena, Chrome ei tulosta taustakuvia tai värejä (tämä on selaimen oletus musteen tallentamiselle). **must** mahdollistaa se:

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

#### 6. Suurilla dokumenteilla varustetut muistivuodot

**Ongelma:** Monien pdf-tiedostojen tuottaminen saa sovelluksen muistin laukeamaan ja lopulta romahtamaan.

**Miksi:** Jokainen selain-installaatio käyttää merkittävää muistia (100-200MB), ja jos et hävitä kunnolla, ne kasaantuvat.

**Ratkaisu:**

Käytä aina `await using` tai kunnollinen hävittäminen:

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

Suuritilavuusiselle PDF-sukupolvelle kannattaa harkita selaintapausten uudelleenkäyttöä:

```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. Skaalavaihtoehto - Pienempi teksti, enemmän sisältöä

Joskus sivulle pitää mahtua lisää sisältöä:

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

Mutta ole varovainen - liian pieni ja se on lukukelvoton.

### Reaalimaailman PDF-sukupolven malli

Näin itse asiassa teen pdf-sukupolven tuotannossa:

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

### Maisema vastaan muotokuva

Yksinkertaista, mutta usein tarpeellista:

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

### Omat sivukoot

Ei rajoituta vakiomuotoihin:

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

## Muita käytännön käyttötapoja PuppeteerSharpille

Testaamisen ja pdf-sukupolven lisäksi PuppeteerSharp on erinomainen useissa muissa automaatiotehtävissä. Tutustutaan yleisimpiin reaalimaailman sovelluksiin.

### Web-kaappaus tietojen keräämistä varten

PuppeteerSharp on loistava raapimaan JavaScript-kovia sivustoja, joissa perinteiset HTML-parserit eivät riitä:

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

**Milloin sitä käytetään:**

- Yhden sivun sovellusten kaappaus (Reakti, Vue, Angular)
- Sivustot, joilla on ääretön käärö tai laiska lataus
- Kun haluat olla vuorovaikutuksessa sivun kanssa (napsauta painiketta, täytä lomakkeet) ennen raaputtamista
- Sisällysluettelo kirjautumisseinien takana

**EI saa käyttää:**

- Yksinkertainen staattinen HTML-kaappaus (käytä [HtmlAgilityPack](https://html-agility-pack.net/) tai [AngleSharp](https://anglesharp.github.io/) Sen sijaan - paljon nopeampi ja kevyempi)
- Suurikokoinen kaavinta (selainten yleiskustannukset ovat merkittävät)
- Kun käytössä on API (mieluummin viralliset API-rajapinnat kuin raapiminen!)

### Automaattinen kuvakaappaussukupolvi

Testaamisen lisäksi kuvakaappauksista on hyötyä esikatselussa, esikatselussa tai arkistoinnissa:

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

**Käytännöllisiä käyttötarkoituksia:**

- Luodaan og:image tageja blogikirjoituksiin
- Pienoiskuvien luominen verkkosivujen gallerioihin
- Arkistoidaan verkkosivuja vaatimusten noudattamista varten
- Luodaan esikatselukuvia linkin jakamiseen

### Suorituskyvyn seuranta

Mittaa sivun kantavuus:

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

### Automatisoitu raporttisukupolvi

Yhdistä HTML-tempaus PDF-sukupolveen automaattiseen raportointiin:

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

### "Ilmaisen" PDF-sukupolven kustannukset

PuppeteerSharpin käyttö PDF-sukupolvena on "ilmaista" siinä mielessä, että pdf-kirjaston käyttöluvasta ei makseta, mutta se on **ei vapaa resurssien suhteen**.

Jokainen selain-installaatio:

- Käyttää 100-200MB RAM-muistia
- Vaatii merkittävän prosessorin renderointiin
- PDF:n luominen kestää 2-5 sekuntia (riippuen monimutkaisuudesta)

Vertaa tätä omistettuihin pdf-kirjastoihin, kuten:

- **[iTeksti](https://itextpdf.com/)** (aiemmin iTextSharp) - Kaupallinen lisenssi vaaditaan ([]500-3000/vuosi), mutta se tuottaa millisekunneissa pdf-muodossa pienen muistijalanjäljen
- **[QuestPDF](https://www.questpdf.com/)** - Vapaa ja avoin lähdekoodi MIT-lisenssin nojalla, luo PDF-tiedostoja sujuvasta C#-koodista (ei HTML:ää), hehkuu nopeasti
- **[PdfSharpCore](https://github.com/ststeiger/PdfSharpCore)** - Ilmaista MIT-lisenssiä, mutta rajallisempia ominaisuuksia

**Milloin PuppeteerSharpia käytetään PDF-muodossa:**

- Sinulla on jo HTML-pohjat, etkä halua kirjoittaa uudelleen PDF-asettelukoodia
- Tarvitset kompleksisten verkkoasettelujen pikselitäydellisen renderoinnin
- Tilavuus on pieni (< 100 PDFä tunnissa)
- Sinun täytyy luoda pdf-tiedostoja ulkopuolisilta sivustoilta, joita et hallitse

**Milloin käyttää omistettuja PDF-kirjastoja:**

- Suuri volyyminen sukupolvi (> 100 PDFä tunnissa)
- Yksinkertaiset asettelut (laskut, kuitit, raportit)
- Resurssirajoitusympäristöt
- Tarvitset kehittyneet PDF-ominaisuudet (lomakkeet, allekirjoitukset, salaus)

### Hybridilähestymistapa

Joskus paras ratkaisu on käyttää molempia:

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

## Päätelmät

PuppeteerSharp on ollut ehdoton pelinvaihtaja E2E-testauksessa .NET-projekteissani. Se on nopeampi kuin Selenium, siinä on modernimpi API, ja se tekee testaamisesta yleensä vähemmän työlästä.

Suosittelisin seuraavaa:

1. **Aloita PuppeteerSharpista** Jos testaat vain Chrome/Chromea, se on yksinkertaisempi ja nopeampi kuin vaihtoehdot.

2. **Käytä Playwrightia** Jos tarvitset usean selaimen tukea, se on saanut kaikki PuppeteerSharpin ja Firefoxin ja Safarin edut.

3. **Vältä seleniumia** Uusiin hankkeisiin, ellei sinulla ole erityistä syytä käyttää sitä (kuten IE11-tukea, jota toivottavasti ei).

4. **Kirjoita testit harkiten**. E2E-testit ovat hitaita ja voivat olla hauraita. Käytä niitä kriittisiin käyttäjämatkoihin, ei jokaisen pienen yksityiskohdan testaamiseen.

5. **Pidä testit erillään**Jokaisen kokeen tulisi laatia omat tietonsa ja siivota jälkensä.

6. **Käytä auttajamenetelmiä** Näyttämäni peruskurssin kaava pitää testikoodisi puhtaana ja keskittyy testaamaansa, ei testaamaansa.

E2E-testien ei tarvitse olla kivuliaita. Oikeiden työkalujen ja mallien avulla ne voivat olla itse asiassa varsin miellyttäviä. Anna PuppeteerSharpille mahdollisuus seuraavaan projektiin - olet varmasti iloisesti yllättynyt.

Menen kirjoittamaan lisää testejä.

## Lisää luettavaa

- [PuppeteerSharp Dokumentaatio](https://www.puppeteersharp.com/)
- [Puppeteerin API](https://pptr.dev/) (JavaScript, mutta useimmat käsitteet pätevät)
- [Pelaaja .NETille](https://playwright.dev/dotnet/)
- [XUnit-dokumentaatio](https://xunit.net/)
- [ASP.NET-ydinintegraatiotestit](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)