# 使用 PutppiteerSharp *-* 适当替代的终端-@to @-_End测试

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

<!--category-- PuppeteerSharp, E2E Testing, xUnit, Testing -->
这个全面的指南展示了您如何使用 Putsepeer Sharp 在 @.NET}可靠的浏览器自动化在 @MS K17}(如果你需要 Firefox 和Safari Asistance 支持MS18_BAR_)中满足我的需求。 [剧人指南](/blog/playwright-e2e-testing) 取代.

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

如果你曾经和他一起工作过 [](https://www.selenium.dev/) 和驱动程序版本的摔跤 . 处理在您的机器上工作但无其它功能的滑稽测试 以及WebDriver协议的普遍迟钝性 MSMK7\ 足够让你想要将其全部切入并手动测试

输入 [夏尔普](https://www.puppeteersharp.com/) Google的 'NET 端口 [木偶剧](https://pptr.dev/) library . @ it' @%s like Selenium@' @s youngreen_,} 更快的表弟,他实际上忙着准时到场 而且不要求您下载17个不同的浏览器驱动器=.{}

在本文中, I'IQ,}(I'_Putseer Sharp)是如何在博客“%2#E”上使用 PutppeteerSharp 进行测试的?

[TOC]

## 什么? 木偶夏尔普 然后呢?

[夏尔普](https://www.puppeteersharp.com/) 是 Q.NET 库,提供高端的 - API 用于控制使用该设备的铬或铬浏览器 [《铬化脱贫工具议定书》](https://chromedevtools.github.io/devtools-protocol/)与众不同 [](https://www.selenium.dev/)使用此功能的, [WebDriver 协议](https://www.w3.org/TR/webdriver/) 以Json为基地的 JSonson 电线协议 @),_Puptipeer Sharp 通过 DevTools@.直接与浏览器交谈

以这种方式想一想

- ****: 像是通过邮件发送信件, 与您的浏览器通信@ info: whatsthis
- **夏尔普**@: 喜欢有直线的电话线 到浏览器@'s大脑

```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%}@ MSKO} 您的測試中的 ): fast, 便宜, 測試單獨功能@. 但是他們不告訴您系統是否真的有效 *.
- **整合试验** “(15%”测试您测试的“):”测试 不同部件是如何一起工作的“MS K2”比 EZ2“E”更快,但“Don'”测试整个UI @.“
- **E2E 测试** @.}这是小木偶Sharp shays@.}在此测试系统是否与用户体验的完全吻合?

**当你需要 EQ2E 测试时\ :**

1. **关键用户旅程** -Login,校验,付款处理}♪.如果这些中断...
2. **JavaScript - 重度 UIs** -=YTET -伊甸园字幕组=- 翻译:[反应反应](https://react.dev/), [颜色](https://vuejs.org/), [角形](https://angular.dev/)) 介于 UI 的客戶端@-side @.
3. **Cross- 浏览器问题** “-”不同的浏览器使事情变得不同,“(”虽然和小木偶Sharp你 're Chrome-只@).
4. **复杂互动** - 多-step 向导@,拖曳 @-}和{-%droot,文件上传MS K6

**当您不' 需要 E2E 测试@:\Q时**

1. **CRUD简单操作** @-集成测试足够了#.
2. **纯粹逻辑** “-”这个'什么单位测试是“.”的单位测试?
3. **每个边缘情况** - E2E测试太慢和昂贵,无法进行彻底的检测

### 为什么玩偶沙尔普 超过银? ?

让我来计算一下这些方法

1. **无司机管理 Faff**: PuputeeerSharp 为您下载并管理铬浏览器@. 不再用ChromeDriver 版本来迷惑您的已安装的铬版本 @.}

2. **更快执行**“DevTools协议”比WebDriver“.”要快得多。 您的测试会更快地运行“ , ” , 而您在等待事情发生时花费的时间更少了。 “. ”

3. **更好的API**API更现代、更直观,

4. **PDF 新一代**@ :_ 想要在测试失败时截图@ MS K1} It'_ 与 Putchapeer Sharp+.一起死得简单 *

5. **拦截网络请求**“: ” 您可以截取“ , 修改”\ , 或块状网络请求, 可以轻松地测试离线情景或模拟 API 响应@ MS K4 。

6. **正确的 JavaScript 执行**: 执行 JavaScript 在页面的上下文中并获得结果 。

## 设置开胃乐Sharp

第一, 添加 [夏尔普](https://www.nuget.org/packages/PuppeteerSharp) NuGet 软件包@: @% 1

```bash
dotnet add package PuppeteerSharp
```

这里 @ ' @\ title: project`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>
```

I'% 使用 [x 单位](https://xunit.net/) (ASP.NET Core'}默认MS K3,但小木偶夏尔普(Putcheer Sharp)同样工作良好 [N单位](https://nunit.org/) 或 [微测试](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest).

## 创建基准测试类

而不是在每次测试中重复设置@/ @ teardown 代码@ ,_ I'# 已创建了基级@ I(}`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 Unit, 提供 Async 设置@ / teardown @ MS K2 与传统构建器不同 @ I, 这让我们可以适当等待浏览器初始化@ MPK4

### 浏览器初始化

```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 environments\ . 需要的旗帜

### 清理

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

妥善处置对于避免内存泄漏至关重要, 每个浏览器实例都使用 RAM @. 的“ 100-200” MB 。

## 帮助方法

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

这些选项可以处理 - @ 等待元素存在 ,} 优雅的超时处理@ MSC2} 以及测试失败后自动登录 CIQ.}

## 实际写作测试

右@, @ let @ '} 让 @ MPK1 @ 开始写好东西 @ I-_ 撰写实际测试@ MS K3 这里 @ O'}这是在我的博客“ 'Qs 过滤栏功能” @(}上一个真正的测试。`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");
}
```

此测试正在检查我的语言下调功能是否正常@ . @ Let'_%s 看看是什么让它滴答 @ MS K2}

### 跳过属性

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

I' @ve 默认跳过此测试, 因为它要求网站运行本地端@ . @ 用于 E2_ E 測試@ MPK3}您通常想要在-}需求上运行它们, 而不是用每座建筑来运行 MS K5{ 您可以卸载它们, 当您 ' ready to running them@ I,}或者在一个单独的 CID 工作中运行时, 您可以在您的% MSKO8 已经将站点调高了@.}

### 执行 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 type @ MS K1” 来获取。 在这样的情况下, ,} I'm 检查下调是否实际可见 {(}不是仅仅出现在 DOMQZ)}(通过查看其计算样式 <%. > ) _

相比之下,您需要使用“'”来比对到“Setium”, 您需要用“ :” 来比较此值 。

1. 查找元素
2. 获取其显示属性
3. 分析字符串结果
4. 但愿你检查时它不会腐烂

### HTMX 测试相互作用

我的博客使用 [HTMX](https://htmx.org/) @ ( @ sperver@ MS K1} 未写入 JavaScript)._ 此处 @ MPK3{ 检查功能排序的测试 *\ (}`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.在一个完美的世界中 @, we}'d 等待特定的网络请求以完成\,;但对于简单的案例_,一个短暂的延迟是罚款{.}

### 测试响应设计

检查我的过滤栏是否正确隐藏在移动设备 @(}`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
    });
}
```

您可以随时更改视图端口@ , @ {, 它在测试响应性布局时非常出色 @ MS K1}\} 比手动调整浏览器窗口大小要容易得多 @ MPK2} *

## 高级木偶剧

### 网络拦截

我最喜欢的特征之一是能够截取和修改网络请求@.}这对测试错误状态或离线假想情况非常宝贵 :}

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

### 截图

当测试失败时 , a屏幕截图值为千对数消息 :\ {}

```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 页面@ , @ 用于测试服务器@ MPK1} 侧面翻譯或打印样式表\ MS K2}

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

### 代码覆盖率

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

## 夏尔普与竞争

让' @%s 能够正确查看 Putppiteer Sharp 堆叠如何与其他 E2_E 测试工具对比@:}

```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”以来就一直在周围活动, 它展示了“ .” 和 “ I'” 是成熟的 @ , 和“ 好” MS K4” 有记录地@, 支持日光下的所有浏览器@ MSKO6 \ 但它也显示了它的年龄Q: *

**Pros:**

- 支持所有浏览器@ @Chrome @,_Firefox_,Safari},Edge=,IE 如果你是受虐狂的话
- 工具和扩展的大规模生态系统
- 众所周知并被广泛接受
- 用于 Cross @-}浏览器测试

**:**

- WebDriver 协议进度缓慢
- 虽然WebDriverManager帮助),
- 与现代替代品相比,API感觉已经过时
- 由于时间问题,赤基测试很常见
- 没有已建的网络截取@- @in network

**何时使用@ : @ {}** 当您绝对需要测试多个浏览器@ , @ 或当您的\ MS K1 re 已投资到“ 生态系统” =.}

### lay人赖

**街上的新小子**

[lay人赖](https://playwright.dev/) 是 微软 '}%s 回答 Putplayeer\ MPK1}以 [.NET支持](https://playwright.dev/dotnet/) 以多@ - browser 支持 < MS K3 > 来烘烤@ it'} 基本上由 Putppiteeer Sharp 组成,但有多个@ IMSK2_ browerser 支援 *:} @

**Pros:**

- 支持 Chrome,FirefoxMS K1Safari @(WebKit})
- 类似“傀儡”的现代API
- 自动=- download 浏览器
- 网络截取@,_屏幕截图 @,}等#.
- 卓越的 M.NET 支持

**:**

- 如此小的生态系统
- 如果你只需要铬,就可能超杀法
- 由于多@ -_ browser 支持, 小化的复杂设置

**何时使用@ : @ {}** 当您需要多@ - browser 支持时, 如果您想要一个现代的 API @ MS K1} 如果您正在启动一个新工程, 并且需要 cross@ I- 浏览器测试@ MPK4 Playwright 可能是您的最佳赌注: MSSK5}

### 闪压缩

**JavaScript 开发者 @' @s Darling**

如果您在 JavaScript@ / @ TypeScript},, Cypress 是卓越的。 但是它不是“ MS K4” 的启动器。

**Pros:**

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

**:**

- 仅 JavaScript@/ @ type
- 没有 & M.NET 支持
- CanM' @ t 测试多个标签或窗口
- 限制测试您自己的应用程序@( @no 跨域的测试@ )

**何时使用@ : @ {}** 您正在写 @ .@NET code @.}继续使用与您的技术堆集整合的东西\ .}

### 那么,你应该使用什么 ?

在这里,'我采取 :

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

建立现代网络应用程序#:

- **仅测试@? @%** MPK0木偶夏尔普
- **多@ - @ browser 测试 @ MS K1 @** →播放器
- **已经使用 elenium @ ?** @→ 考虑移民到玩家~,}但是不要急着去

## 在 CI/}CD 中运行测试

E2E测试在您的本地机器上 都很好,好。 但是它们需要运行 在 CI/CD 管道 tooMS K3}这里这是I' [吉特胡布行动组织](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

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

总是等待您@ ' re 将要互动的元素存在并显示为可见@.\ {}

### 测试孤立

每个测试都应该是完全独立的@ . @ Don' @ t依赖先前测试的状态@ MPK2}

**巴德:**

```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测试慢于单位测试@, there @'}无法绕过它,但你可以让它们更快地Q:

### 平行运行测试

x unit 在默认情况下平行运行测试@ , @ but you need to be careful about shared state:}但您需要谨慎对待共享状态@ MPK1 @%

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

### 无法禁用不必要的特性

使用禁用功能进行加速测试 您不需要@' @ tdeed @ : @

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

测试失败时, 您需要调试它们@ ( @ 和他们将会 MS K1} 您可以调试它@ I. @ 这里有一些技巧 @ : @

### 以非 - 无头模式运行

设置套设 `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_Reorder_Items()
{
    await NavigateAsync("/admin/posts");

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

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

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

    await WaitAsync(500);

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

## 与ASP=.NET核心测试相结合

您可以将 Putpuppeteer Sharp 和 [ASP.NET 核心}'Web应用工具](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 生成和自动化时超越测试@- PutppiteerSharp

虽然E2E测试非常出色, ,PuptipeerSharp是瑞士军队的一把刀, 它最常用的用途之一是从网络内容中生成 PDF {-}_ItMS K4虽然对于这个%SK5}来说是非常有用的,不过如果你没有光着它的话。 如果您在建造发票\□,′MSK8}报告>,}或任何文件生成系统 <,}本部分可以节省你的时间去调试*.}

### 产生PDFs “承诺与痛苦”

此想法很简单, @: 以铬格式创建网页, 并保存为 PDF_ . 完美地生成发票@, 报告}{, 证书}, 或需要以PDF格式发布的任何动态内容\.}

在这里,'_BARBAR_基本方法_:}

```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.字嵌入的噩梦

**问题:** 您美丽的自定义字体@ ' @ t 在 PDF MS K1} 或更糟糕的pDU ,} 它们@ MPK3re 但看起来绝对垃圾_ .}

**为何发生这种事?** 在 PDF 生成期间, Chrome 需要访问字体文件@ . @ 如果您的字体是通过外部 CDN 装入的, Chrome can\ '}% t toweld 它们 @ MS K2fire wall_ ,} 网络问题 @ MPK4 timetime), you @ OMK6re stupped+.}

**解决方案#:**

```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 @. 是 @ , it@'}它是一个faff,, 但是它是可靠的\ K6_ s weivious *.

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

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

这实际上是 **正确行为** @-_PDFs是平面媒体@.}但是它第一次吸引了每个人

**解决方案#:**

使用 `@media print` CSS 适当规则 *:*

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

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

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

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

若您想要您的 PDF 中的屏幕版本 @ ( @ useful 来生成 pDFs @ MPK4}的 & MS K2 @ screenshots@ "

```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. 标题和页脚 @- 比您更复杂吗?'d think

您可以添加页眉和页脚@ , @ but API is a bit wonky:\ {}

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

**加起来:**

- 页眉% / @ footer 模板必须是有效的 HTML, 但必须是极有限的@ MS K1} 没有外部的 CSS,}没有 JavaScript
- 您只能获得特定的变量@ : @ `date`, `title`, `url`, `pageNumber`, `totalPages`
- 字符串只有内嵌
- 边距必须大到足以容纳信头@/ @otofers 或者它们#'}会重叠您的内容

#### 5. 背景图形

默认情况下@ , @ chrome does\ @ MS K1} t 打印背景图像或颜色 #% (} * 这是保存墨水的浏览器默认值 。 **必须** 启用该选项@: @}{}

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

#### 6.有大文档的记忆泄漏

**问题:** 正在生成许多 PDF 导致您的应用程序@' @%s memory to ballo 并最终崩溃\ .{}

**为什么:** 每个浏览器实例都使用重要的内存@ (100-200MB),}如果您没有正确处理 @ MS K3},它们就会堆叠\ .}

**解决方案#:**

总是使用 `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();
    }
}
```

对于高Q-p 批量的 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的产生模式

@'@sthis:我如何在制作中实际生产 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 Droup +, Puppiteeer Sharp 在其他几项自动化任务中成绩优异...". Let's探索最常见的“-world applications”_.

### 数据提取网页处理

校对:Portnoy

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

**何时使用 It: @ {}**

- 正在擦除单{-}页面应用程序 @ (_ react MSQK2Vue,角MS K4
- 无限滚动或懒惰装入的站点
- 当您在切除前需要与页面 {(} 互动时单击按钮 ,}\ 填充表格@)
- 登录墙后面的内容

**当不使用它时@ : @ {}**

- 简单静态 HTML 抓取@ ( @ use [Html 灵活性包](https://html-agility-pack.net/) 或 [闪烁角](https://anglesharp.github.io/) @-# 速度快得多, 打火机轻一点 #)@
- High- 批量刮 (browser 间接费用相当可观
- 当有“'” API可用时, “(”总是喜欢官方的API而不是刮!)

### 自动截图生成

超過測試@, @% 抓图對縮略圖 @ ,} 預覽@ MS K2} 或归档@ MPK3} 有用

```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 世代成本

使用 Putppiteer Sharp 来制作 PDF 的版本@- @ it'}%s @"{free}"}(因为您不支付PDF图书馆许可证=,}但是它') **资源无自由**.

每个浏览器实例@: @%

- 使用 RAM 的 100-200MB
- 需要相当的 CPU 设定
- 生成 PDF & ( 取决于复杂程度 @)}

比较此选项与专用的 PDF 库如 @ : @ {}

- **[i Text 内容](https://itextpdf.com/)** “(”前的 iTextSharp @)}- 商务许可证要求“(~£500-3000/ year_ZAY ),”,但生成微小内存足迹的毫秒PDF
- **[获取PDF](https://www.questpdf.com/)** -Free and open source 依据MIT 牌照,CMS K2編碼(no HTMLMPK4快速燃燒,
- **[PdfSharp煤炭](https://github.com/ststeiger/PdfSharpCore)** @-_Free MIT 牌照@,}但能力更有限

**何时对 PDFs 使用傀儡Sharp@ :**

- 您已经拥有了 HTML 模板, 而 Don' @\} 不想在 PDF 布局代码中重写
- 您需要像素@ - @ perperfect 复杂的网络布局
- 音量是低的 @ (< @ MPK1PDFs/ 小时@ MS K2
- 您需要从外部网站生成 PDF , 您可以在您的 DZ @ ' @ t

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

- 高容量生成 (>}MS K1 PDFs / hour )
- 简单布局@( @invoices @,_收件},}报告{)}
- 资源@-_受限制的环境
- 您需要高级 PDF 功能@ ( @ forms @ MS K1} 签名@ MPK2_ 加密@ I)

### 混合方法

有时最好的办法就是同时使用:

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

## 结论结论

校对:Portnoy

这是I' 推荐的:

1. **以 PutppiteerSharp 开始** 若您只测试 Chrome@ / @ chromium% MS K2 It'}比替代品更简单、更快

2. **使用 Playwright** 如果您需要多@ -browser 支持 @ MS K1 It's 得到了木偶夏尔普+ Firefox和Safari=.的所有好处

3. **避免** 用于新项目, 除非您有特定的理由使用@ ( @ 喜欢 IEQ11} 支持 @ MS K2 希望您能用到% '_ t).}

4. **谨慎写作测试**“. ” E2E 测试缓慢,且可能不太顺利。使用它们来进行关键用户旅程;“,”不是用来检测每一个小细节。“MS K4”

5. **保持独立测试**“.”每个测试都应该建立自己的数据,

6. **使用助手方法** 我展示的基本等级模式 保持你实际测试代码的干净 并关注您在测试什么

E2E 測試必須是痛苦的 @'}用正確的工具和樣式 @MS K3}它實際上可以很愉快的@.}讓 PutsuiteerSharp 開始您的下一個專案 -我認為你會很驚訝地驚喜 @.

右@, @ I'#% 关闭以写入更多测试@ . @ 快乐测试 @ MS K3} @

## 继续阅读

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