# 使用 " 木偶夏尔普 " 进行端至端测试 -- -- 适当替代品

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
现代 E2E (End- to-End, 使用您的网站,像用户一样使用您的网站) 测试不必是痛苦的。 这个全面的指南展示了您如何使用 Putopiteer Sharp 来快速、可靠的浏览器自动化. NET 覆盖从基本测试到 PDF 生成和网络切除等所有内容。 尽管微软的Playwright 是更现代的多浏览器解决方案, 我选择了 Putopiteeer Sharp 来使用这个博客, 因为它是我所知道的, 而只用 Chrome 测试足以满足我的需求。 如果您需要 Firefox 和 Safari 支持, 请查看我的博客。 [剧人指南](/blog/playwright-e2e-testing) 取而代之。

## 一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

如果你曾经合作过 [](https://www.selenium.dev/) 对于端到端的测试, 你会知道这可能是一个正确的问题。 在与驱动程序版本摔跤, 处理对机器有效但无其它功能的片面测试之间, 以及 WebDriver 协议的总体缓慢性, 足以让你想要将它全部扔进车内, 然后手动测试。

输入 [夏尔普(Sharp)](https://www.puppeteersharp.com/) - 谷歌网网港 [木偶剧](https://pptr.dev/) 这就像Selenium的更年轻、更快的表弟, 他实际上忙着准时出现, 不需要你下载17个不同的浏览器驱动器。

在本篇文章中,我将介绍我是如何在这个博客上应用 Putppieer Sharp 来进行E2E测试的, 完整地用回购中的真实代码示例来完成。 我们将覆盖测试、 PDF 生成、 网络报废, 并将它与替代品进行比较 。

[TOC]

## 那"傀儡沙普"是什么?

[夏尔普(Sharp)](https://www.puppeteersharp.com/) 是一个.NET 库,它提供一个高级 API ,用于控制使用该设备控制铬或铬浏览器的铬或铬浏览器。 [《铬化脱贫工具议定书》](https://chromedevtools.github.io/devtools-protocol/)不同 [](https://www.selenium.dev/),其中使用 [WebDriver 协议](https://www.w3.org/TR/webdriver/) 木偶夏尔普通过DevTools直接与浏览器交谈。

这样想吧:

- ****:比如通过邮件发送信件,与浏览器通信。
- **夏尔普(Sharp)**像是直通浏览器大脑的电话线

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

**现实检查:**

- **单位测试** (80%的测试):快速、廉价、测试单个功能。但是他们不告诉你系统是否真正能整体运作。
- **整合试验** 测试不同部件是如何一起工作的。 比 E2E 更快, 但不要测试完整的 UI 。
- **E2E 测试** 缓慢、昂贵,但测试系统要和用户的体验一样。这是普迪茨夏尔普闪耀的地方。

**当你需要E2E测试时:**

1. **关键用户旅程** 登入、结账、付款处理 如果这些中断,你的生意就停止了
2. **JavaScript 重重 UIs** - 现代证交所(现代证交所)[反应反应](https://react.dev/), [颜色](https://vuejs.org/), [角角](https://angular.dev/)用户界面为客户端。
3. **交叉浏览器问题** - 不同的浏览器使事情变得不同(虽然与PutputeeerSharp相比,你是只有铬的)。
4. **复杂互动** - 多步向导,拖放,文件上传。

**当你不需要E2E测试时:**

1. **CRUUD简单操作** - 融合测试就足够了。
2. **纯粹逻辑逻辑** - 单位测试就是用来做这个的
3. **每个边缘情况** - E2E测试太慢,成本太高,无法进行详尽的测试。

### 为什么开胃菜沙普 超过?

让我计算一下方法:

1. **无司机管理 Faff**: PutopieerSharp 下载并管理您的铬浏览器。不再用与您安装的铬版本不匹配的铬版本的铬Driver版本来搅拌 。

2. **更快执行**: DevTools 协议比WebDriver要快得多。你的测试会更快进行, 你会花更少的时间等待事情的发生。

3. **更好的API**API更现代,更直观。它是Async/await all the way down, 它与现代.NET的开发非常吻合。

4. **内置屏幕截图和 PDF 生成**测试失败时要截图吗?

5. **拦截网络请求**:您可以轻松地拦截、修改或封锁网络请求----在测试离线情景或模拟 API 响应方面非常出色。

6. **正确的 JavaScript 执行**:在页面的上下文中执行 JavaScript,然后以不会让你想哭的方式获得结果。

## 设置开胃乐Sharp

第一,增加 [夏尔普(Sharp)](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>
```

我在用 [x 单位](https://xunit.net/) (ASP.NET Core的违约),但 " 傀儡夏尔普 " 与 [N单位单位](https://nunit.org/) 或 [微测试](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;
    }
```

我们执行 [IAsync 生命时间](https://xunit.net/docs/shared-context#async-lifetime) x United , 它提供同步设置/ 离线 。 与传统构建器不同, 这让我们可以适当等待浏览器初始化 。

### 浏览器初始化

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

缩略 `BrowserFetcher` 自动在第一次运行时下载兼容的铬版本 - 不需要手动驱动程序管理 。 `--no-sandbox` Docker/CI 环境需要悬挂旗帜。

### 清理清理

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

正确处置对于避免内存泄漏至关重要。 每个浏览器实例都使用100 - 200MB的内存管理 。

## 帮助方法

基本等级包括削减锅炉板的助手方法(`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);
}
```

它们处理乏味的比特-- 等待元素的存在, 优雅的超时处理, 和自动记录 当测试失败在 CI 。

## 实际写作测试

对,让我们来谈谈好东西-- 写实际的测试。这是我博客的过滤栏功能(这是真实的测试) 。`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 测试中, 您通常想要按需运行, 而不是每栋建筑都运行。 您可以在您准备运行时卸载它们, 或者在您已经启动网站的地方运行它们 。

### 执行 JavaScript 执行中的 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';
}");
```

这是"傀儡夏尔普"绝对闪耀的地方之一。 `EvaluateFunctionAsync` 方法可以让您在浏览器上下文中运行 JavaScript , 并将结果作为适当的.NET 类型返回。 在这种情况下, 我通过查看其计算样式来检查下调是否实际可见( 不只是在 DOM 中存在) 。

将这个比作 , 在那里你需要:

1. 查找元素
2. 获取其显示属性
3. 分析字符串结果
4. 希望你检查的时候不会变老

### HTMX 测试相互作用

我的博客使用 [HTMX](https://htmx.org/) 宽度( 服务器侧面布局, 不写入 JavaScript) 。 这是用来检查排序功能的测试 (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 。 在完美的世界中, 我们会等待特定的网络请求完成, 但对于简单案例, 短暂的延迟是正常的 。

### 测试响应设计

检查我的过滤栏是否妥善隐藏在移动设备上(请查看InfoFinland上的 ) 。`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
    });
}
```

您可以随时更改视图站, 它对于测试响应性布局非常出色。 比手动调整浏览器窗口大小要容易得多 !

## 高级木偶剧 Sharp 特性

### 网络拦截

我最喜欢的特征之一是能够拦截和修改网络请求。这对于测试错误状态或离线情景非常宝贵:

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

您甚至可以生成 PDFs 页面, 用于测试服务器侧面显示或打印样式表 :

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

### 代码覆盖率

PutcheeerSharp甚至可以收集 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}%");
```

## Sharp 与竞争

让我们好好看看PutopieeerSharp的堆叠是如何与其他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
```

### WebDriver

**旧卫队**

从2004年起就一直存在,它显示出来。它是成熟的,有详细记录, 并支撑着太阳下的所有浏览器。 但是它也显示了它的年龄:

**专业:**

- 支持所有浏览器( Chhrome、 Firefox、Safari、Edge、IE, 如果你是受虐者)
- 工具和扩展的大规模生态系统
- 众所周知和广泛采用
- 可用于交叉浏览器测试

**关节 :**

- WebDriver 协议进度缓慢
- 司机管理是一种痛苦(虽然WebDriverManager帮助)
- 与现代替代品相比,API感觉已经过时
- 由于时间问题,赤基测试很常见
- 没有内置网络拦截

**何时使用:** 当您绝对需要通过多个浏览器测试时, 或者当您已经投入到 银河生态系统中时。

### lay人赖

**街上的新小子**

[lay人赖](https://playwright.dev/) 是 微软对 Putputeer 的回答 [. NET 支持](https://playwright.dev/dotnet/) 从一开始就被烤熟了。 它基本上是 Putputeeer Sharp , 但有多个浏览器支持 :

**专业:**

- 支持铬、Firefox、Safari(WebKit)
- 与 " 傀儡 " 相似的现代API
- 自动下载浏览器
- 内置网络拦截、截图等
- 优秀的.NET支持

**关节 :**

- 更新、更小的生态系统
- 如果你只需要铬的话, 可能会过份的
- 由于多浏览器支持而略为复杂的设置

**何时使用:** 当您需要多浏览器支持但需要现代 API 时。 如果您正在启动一个新项目, 需要交叉浏览器测试, 玩家可能是您的最佳赌注 。

### 结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结结

**JavaScript 开发者亲爱的**

在JavaScript/TypeScript工作的话, Cypress是辉煌的,

**专业:**

- 出色的开发者经验
- 时间旅行调试
- 自动等待
- 特大文件文件

**关节 :**

- JavaScript / TypeScript 只为 JavaScript / TypeScript 只为 JavaScript
- 否. 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
```

对于大多数网络开发商来说,

- **色片测试?** 
- **多浏览器测试?** 播放器
- **已经使用了吗?** 考虑移民到普林斯莱特 但不要匆忙

## 在 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测试慢于单位测试,无法绕过它。但你可以让它们更快:

### 平行运行测试

x unit 在默认情况下平行运行测试, 但是您需要谨慎对待共享状态 :

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

### 使用 DevTools 工具

您可以在程序上打开 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核心测试结合

您可以将 Putputeeer Sharp 和 [ASP.NET 核心网络应用工具](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";
    }
}
```

## 超测试 - PDF 生成和自动化的傀儡

虽然E2E测试非常出色,但傀儡夏尔普(Putputeeer Sharp)是瑞士军队的一把刀,可以做得更多。它最常用的用途之一是从网络内容中生成PDF(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 无法到达这些字体文件( 防火墙、 网络问题、 时间) , 您会被填充 。

**解决方案:**

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

更好的是,在本地安放您的字体, 或者在您的 CSS 中以 base64 嵌入字体 。 是的, 它是一个假的, 但它是可靠的 。

#### 2. CSS 印刷媒体查询

**问题:** 您的 PDF 与您的网页完全不一样, 因为 Chrome 应用印刷媒体查询 。

这实际上是 **右对右行为** PDF是印刷媒体 但它第一次抓住所有人

**解决方案:**

使用使用 `@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. 断页断页 -- -- 你的存在之宝座

**问题:** 你的内容被尴尬地分成各页, 标题在底部成为孤儿或表格被切成两半。

**现实:** 你正在和Chrome的 内部人造算法抗争 而且它会赢得大部分时间

**你们能做的是:**

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

在你的开胃菜代码中:

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

**Pro Tip :** 对于复杂的布局来说,有时更容易用明确的分页符来构建您的 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`
- 字符串只有内嵌
- 边距必须足够大, 以容纳页头/ 脚脚头的人, 否则他们会重叠您的内容 。

#### 5. 背景图

默认情况下, 铬不会打印背景图像或颜色( 这是保存墨水的浏览器默认值 ) 。 您 **必须** 启用该功能:

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

#### 6. 有大文档的内存漏漏漏

**问题:** 生成大量 PDF 导致您的应用程序内存为气球, 并最终崩溃 。

**为什么:** 每个浏览器实例都使用重要的内存(100-200MB),如果你处理不当,它们就会堆积起来。

**解决方案:**

总是使用 `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 生成, 考虑重新使用浏览器实例 :

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

## 木偶人的其他实用用途

除了测试和PDF生成之外, PutopieeerSharp还擅长其他几个自动化任务。让我们探索现实世界中最常见的应用。

### 数据提取网页处理

传统 HTML 解析器不足的 JavaScript-heavy 站点:

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

**何时使用 :**

- 剪裁单页应用程序( React、 Vue、 Agle)
- 无限滚动或懒惰装入的站点
- 剪贴前需要与页面互动(单击按钮,填充表格)
- 登录墙后面的内容

**不使用时 :**

- 简单静态 HTML 抓取(使用) [Html 灵活性纸包](https://html-agility-pack.net/) 或 [闪闪角角](https://anglesharp.github.io/) 而不是 - 更快和轻得多)
- 高容量报废(借款人间接费用很大)
- 当有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:image 标签
- 为网站画廊创建缩略图
- 为遵约目的存档网页
- 正在生成用于链接共享的预览图像

### 业绩监测

测量页面负载性能:

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

### 自动生成报告

将 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生产成本

使用Putoptipeer Sharp作为PDF一代的PDF用户, 这是“免费”的意思, 意思是您不支付PDF图书馆许可证, **资源无自由**.

每个浏览器实例 :

- 使用 RAM 100-200MB
- 需要相当的 CPU 设定
- 生成一个PDF需要2-5秒(视复杂程度而定)

比较于专用 PDF 库, 例如 :

- **[i Text i 文字](https://itextpdf.com/)** (原iTextSharrp) - 需要商业许可证(500-3000/年),但生成的PDF以毫秒计,内存足迹微小
- **[获取PDF](https://www.questpdf.com/)** - 根据麻省理工学院许可证自由开放源码,使用流利的 C# 代码(没有 HTML)生成PDF, 快速燃烧
- **[PdfSharp煤炭](https://github.com/ststeiger/PdfSharpCore)** - 免费MIT许可证,但能力更有限

**对 PDFs 使用 Putppeteer Sharp 时:**

- 您已经有 HTML 模板, 并且不想在 PDF 版式代码中重写
- 您需要像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 像素/ 需要复杂的网络布局
- 音量低(低于每小时100个PDFs/小时)
- 您需要从外部网站生成您无法控制的 PDF 文件

**何时使用专用 PDF 库 :**

- 高容量生产(大于每小时100个PDFs/小时)
- 简单布局(发票、收据、报告)
- 受资源制约的环境
- 您需要高级 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();
    }
}
```

## 结论 结论 结论 结论 结论

PutopieerSharp 在我的.NET 工程中是 E2E 测试的绝对游戏交换器。 它比更快, 具有更现代化的API, 通常测试比杂技要少。

下面我建议:

1. **以 Putputeeer 开始 Sharp 开始** 如果你只是测试铬/铬。它比替代品简单快捷。

2. **使用 Playwright** 需要多浏览器支持时,可以享受木偶夏尔普加Firefox和Safari的好处。

3. **避免** 用于新项目,除非有特定理由使用(如IE11支持,希望你不会使用)。

4. **谨慎写作测试**E2E 测试缓慢,而且可能易碎。使用它们进行关键的用户旅程,而不是测试每一个小细节。这就是单元和集成测试的目的。

5. **保持独立测试**. 每次测试应建立自己的数据,然后进行清理。

6. **使用助手方法** 我展示的基本级图案 让你的实际测试代码保持干净 专注于测试内容 而不是测试方式

E2E 测试不必是痛苦的。 有了正确的工具和模式, 它实际上是相当愉快的。 给 Putppiteeer Sharp 来一个您的下一个项目- 我想你会很惊讶的。

好了,我要去写更多的测试了 快乐的测试!

## 继续阅读

- [Sharp 文档](https://www.puppeteersharp.com/)
- [开胃的 API](https://pptr.dev/) (JavaScript,但大多数概念适用)
- [.NET 的播放器](https://playwright.dev/dotnet/)
- [x 单位文件](https://xunit.net/)
- [ASP.NET 核心一体化测试](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)