This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
使用 PutppiteerSharp - 适当替代的终端-@to @-_End测试
Thursday, 27 November 2025
这个全面的指南展示了您如何使用 Putsepeer Sharp 在 @.NET}可靠的浏览器自动化在 @MS K17}(如果你需要 Firefox 和Safari Asistance 支持MS18_BAR_)中满足我的需求。 剧人指南 取代.
一. 导言导言 导言 导言 导言
如果你曾经和他一起工作过 和驱动程序版本的摔跤 . 处理在您的机器上工作但无其它功能的滑稽测试 以及WebDriver协议的普遍迟钝性 MSMK7\ 足够让你想要将其全部切入并手动测试
输入 夏尔普 Google的 'NET 端口 木偶剧 library . @ it' @%s like Selenium@' @s youngreen_,} 更快的表弟,他实际上忙着准时到场 而且不要求您下载17个不同的浏览器驱动器=.
在本文中, I'IQ,}(I'_Putseer Sharp)是如何在博客“%2#E”上使用 PutppeteerSharp 进行测试的?
什么? 木偶夏尔普 然后呢?
夏尔普 是 Q.NET 库,提供高端的 - API 用于控制使用该设备的铬或铬浏览器 《铬化脱贫工具议定书》与众不同 使用此功能的, WebDriver 协议 以Json为基地的 JSonson 电线协议 @),_Puptipeer Sharp 通过 DevTools@.直接与浏览器交谈
以这种方式想一想
- ****: 像是通过邮件发送信件, 与您的浏览器通信@ info: whatsthis
- 夏尔普@: 喜欢有直线的电话线 到浏览器@'s大脑
graph LR
A[Test Code] -->|WebDriver Protocol| B[Selenium]
B -->|JSON Wire Protocol| C[Browser Driver]
C -->|Commands| D[Browser]
E[Test Code] -->|DevTools Protocol| F[PuppeteerSharp]
F -->|Direct Connection| G[Chrome/Chromium]
style A stroke:#333,stroke-width:2px
style E stroke:#333,stroke-width:2px
style F stroke:#0066cc,stroke-width:3px
style G stroke:#0066cc,stroke-width:3px
E2E测试适合您的测试战略的地方
在我们潜入深水前 ,,让我们来讨论一下 E2E测试在大计划中的位置 你可能听说过这个测试金字塔的“-”,这里是'
graph TB
subgraph "Testing Pyramid"
E2E[E2E Tests<br/>Few, Slow, High Confidence<br/>Test full user journeys]
INT[Integration Tests<br/>Medium number, Medium speed<br/>Test component interactions]
UNIT[Unit Tests<br/>Many, Fast, Low Cost<br/>Test individual functions]
end
subgraph "Trade-offs"
SPEED[Speed]
CONF[Confidence]
COST[Cost]
end
subgraph "When to Use E2E"
W1[Critical user journeys<br/>e.g. checkout, login]
W2[Cross-browser compatibility]
W3[JavaScript-heavy UIs]
W4[Complex user interactions]
end
E2E -.->|Slow but high confidence| CONF
INT -.->|Balanced| SPEED
UNIT -.->|Fast and cheap| SPEED
E2E -.->|Expensive to run| COST
UNIT -.->|Cheap to run| COST
style E2E stroke:#cc0000,stroke-width:3px
style INT stroke:#ff9900,stroke-width:2px
style UNIT stroke:#00aa00,stroke-width:2px
style CONF stroke:#0066cc,stroke-width:2px
style SPEED stroke:#00aa00,stroke-width:2px
style COST stroke:#cc0000,stroke-width:2px
真实检查@: @
- 单位测试 @(80%}@ MSKO} 您的測試中的 ): fast, 便宜, 測試單獨功能@. 但是他們不告訴您系統是否真的有效 *.
- 整合试验 “(15%”测试您测试的“):”测试 不同部件是如何一起工作的“MS K2”比 EZ2“E”更快,但“Don'”测试整个UI @.“
- E2E 测试 @.}这是小木偶Sharp shays@.}在此测试系统是否与用户体验的完全吻合?
当你需要 EQ2E 测试时\ :
- 关键用户旅程 -Login,校验,付款处理}♪.如果这些中断...
- JavaScript - 重度 UIs -=YTET -伊甸园字幕组=- 翻译:反应反应, 颜色, 角形) 介于 UI 的客戶端@-side @.
- Cross- 浏览器问题 “-”不同的浏览器使事情变得不同,“(”虽然和小木偶Sharp你 're Chrome-只@).
- 复杂互动 - 多-step 向导@,拖曳 @-}和{-%droot,文件上传MS K6
当您不' 需要 E2E 测试@:\Q时
- CRUD简单操作 @-集成测试足够了#.
- 纯粹逻辑 “-”这个'什么单位测试是“.”的单位测试?
- 每个边缘情况 - E2E测试太慢和昂贵,无法进行彻底的检测
为什么玩偶沙尔普 超过银? ?
让我来计算一下这些方法
无司机管理 Faff: PuputeeerSharp 为您下载并管理铬浏览器@. 不再用ChromeDriver 版本来迷惑您的已安装的铬版本 @.}
更快执行“DevTools协议”比WebDriver“.”要快得多。 您的测试会更快地运行“ , ” , 而您在等待事情发生时花费的时间更少了。 “. ”
更好的APIAPI更现代、更直观,
PDF 新一代@ :_ 想要在测试失败时截图@ MS K1} It'_ 与 Putchapeer Sharp+.一起死得简单 *
拦截网络请求“: ” 您可以截取“ , 修改”\ , 或块状网络请求, 可以轻松地测试离线情景或模拟 API 响应@ MS K4 。
正确的 JavaScript 执行: 执行 JavaScript 在页面的上下文中并获得结果 。
设置开胃乐Sharp
第一, 添加 夏尔普 NuGet 软件包@: @% 1
dotnet add package PuppeteerSharp
这里 @ ' @\ title: projectMostlylucid.Test/Mostlylucid.Test.csproj:23):
<PackageReference Include="PuppeteerSharp" Version="20.2.4" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
I'% 使用 x 单位 (ASP.NET Core'}默认MS K3,但小木偶夏尔普(Putcheer Sharp)同样工作良好 N单位 或 微测试.
创建基准测试类
而不是在每次测试中重复设置@/ @ teardown 代码@ ,_ I'# 已创建了基级@ I(}Mostlylucid.Test/E2E/E2ETestBase.cs:12管理浏览器生命周期管理的)
阶级结构
using PuppeteerSharp;
using Xunit.Abstractions;
namespace Mostlylucid.Test.E2E;
public abstract class E2ETestBase : IAsyncLifetime
{
protected readonly ITestOutputHelper Output;
protected IBrowser Browser = null!;
protected IPage Page = null!;
protected const string BaseUrl = "http://localhost:8080";
protected const int DefaultTimeout = 30000;
protected E2ETestBase(ITestOutputHelper output)
{
Output = output;
}
我们执行 IAsync 寿命 从 x Unit, 提供 Async 设置@ / teardown @ MS K2 与传统构建器不同 @ I, 这让我们可以适当等待浏览器初始化@ MPK4
浏览器初始化
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\ . 需要的旗帜
清理
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):
// 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):
[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}
跳过属性
[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
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”, 您需要用“ :” 来比较此值 。
- 查找元素
- 获取其显示属性
- 分析字符串结果
- 但愿你检查时它不会腐烂
HTMX 测试相互作用
我的博客使用 HTMX @ ( @ sperver@ MS K1} 未写入 JavaScript)._ 此处 @ MPK3{ 检查功能排序的测试 *\ (}Mostlylucid.Test/E2E/FilterBarTests.cs:98-126):
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_SortOrder_ChangesPostOrder()
{
// Arrange
await NavigateAsync("/blog");
// Get the first post title before sorting
var firstPostBefore = await EvaluateFunctionAsync<string>(@"() => {
const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
return postLink?.textContent?.trim() || '';
}");
Output.WriteLine($"First post before sort: {firstPostBefore}");
// Act - Change sort order to "Oldest first"
await Page.SelectAsync("#orderSelect", "date_asc");
await WaitAsync(1000); // Wait for HTMX to update
// Assert - Post order should have changed
var firstPostAfter = await EvaluateFunctionAsync<string>(@"() => {
const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
return postLink?.textContent?.trim() || '';
}");
Output.WriteLine($"First post after sort: {firstPostAfter}");
var selectValue = await EvaluateFunctionAsync<string>("() => document.querySelector('#orderSelect')?.value");
Assert.Equal("date_asc", selectValue);
Output.WriteLine("✅ Sort order selection works correctly");
}
关键是 await WaitAsync(1000) 在更改了选定值@.HTMX后,需要一点时间来提出请求并更新 DOM.在一个完美的世界中 @, we}'d 等待特定的网络请求以完成,;但对于简单的案例_,一个短暂的延迟是罚款
测试响应设计
检查我的过滤栏是否正确隐藏在移动设备 @(}Mostlylucid.Test/E2E/FilterBarTests.cs:216-245):
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_ResponsiveDesign_HiddenOnMobile()
{
// Arrange - Set mobile viewport
await Page.SetViewportAsync(new ViewPortOptions
{
Width = 375,
Height = 667
});
await NavigateAsync("/blog");
await WaitAsync(500);
// Assert - Filter bar should be hidden on mobile
var filterBarVisible = await EvaluateFunctionAsync<bool>(@"() => {
const filterBar = document.querySelector('.hidden.lg\\:flex');
if (!filterBar) return true;
const rect = filterBar.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}");
Assert.False(filterBarVisible, "Filter bar should be hidden on mobile viewport");
Output.WriteLine("✅ Filter bar correctly hidden on mobile");
// Reset viewport
await Page.SetViewportAsync(new ViewPortOptions
{
Width = 1400,
Height = 900
});
}
您可以随时更改视图端口@ , @ {, 它在测试响应性布局时非常出色 @ MS K1}} 比手动调整浏览器窗口大小要容易得多 @ MPK2} *
高级木偶剧
网络拦截
我最喜欢的特征之一是能够截取和修改网络请求@.}这对测试错误状态或离线假想情况非常宝贵 :}
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屏幕截图值为千对数消息 :\
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}
await Page.PdfAsync("page.pdf", new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true
});
代码覆盖率
PuppiteerSharp甚至可以收集 JavaScript 代码覆盖数据@: @%
await Page.Coverage.StartJSCoverageAsync();
await Page.GoToAsync("http://localhost:8080");
var coverage = await Page.Coverage.StopJSCoverageAsync();
var totalBytes = coverage.Sum(c => c.Text.Length);
var usedBytes = coverage.Sum(c => c.Ranges.Sum(r => r.End - r.Start));
var percentUsed = usedBytes / (double)totalBytes * 100;
Output.WriteLine($"JavaScript coverage: {percentUsed:F2}%");
夏尔普与竞争
让' @%s 能够正确查看 Putppiteer Sharp 堆叠如何与其他 E2_E 测试工具对比@:}
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人赖 是 微软 '}%s 回答 Putplayeer\ MPK1}以 .NET支持 以多@ - 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 @.}继续使用与您的技术堆集整合的东西\ .}
那么,你应该使用什么 ?
在这里,'我采取 :
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' 吉特胡布行动组织:
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
- 在背景中启动应用程序
- 使用健康检查端点#)
- 运行 E2E 测试
- 如果测试失败, 上传屏幕抓图
- 即使测试失败, 也总是停止应用程序@ , @ 即使检测失败
常见的瀑布和如何避免它们
光基测试
E2E 测试可以是片面的 @-它们有时通过并且失败了其他人@. 这通常归根结底是计时问题_.这里#'如何避免这些:
巴德:
await Page.ClickAsync("#button");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);
古德:
await Page.ClickAsync("#button");
await Page.WaitForSelectorAsync("#result");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);
总是等待您@ ' re 将要互动的元素存在并显示为可见@.\
测试孤立
每个测试都应该是完全独立的@ . @ Don' @ t依赖先前测试的状态@ MPK2}
巴德:
[Fact]
public async Task Test1_Login()
{
await LoginAsync("user", "password");
// User is now logged in for subsequent tests
}
[Fact]
public async Task Test2_ViewDashboard()
{
// Assumes user is still logged in from Test1
await NavigateAsync("/dashboard");
}
古德:
[Fact]
public async Task Test1_Login()
{
await LoginAsync("user", "password");
await LogoutAsync(); // Clean up
}
[Fact]
public async Task Test2_ViewDashboard()
{
await LoginAsync("user", "password"); // Set up needed state
await NavigateAsync("/dashboard");
await LogoutAsync(); // Clean up
}
页面对象模式
对于复杂的页面 {,} 使用“ 页面对象” 模式来保持您的测试可维持@ :
public class BlogPageObject
{
private readonly IPage _page;
public BlogPageObject(IPage page)
{
_page = page;
}
public async Task SelectLanguageAsync(string language)
{
await _page.ClickAsync("#LanguageDropDown button");
await _page.WaitAsync(300);
await _page.ClickAsync($"#LanguageDropDown a:has-text('{language}')");
}
public async Task<string[]> GetPostTitlesAsync()
{
return await _page.EvaluateFunctionAsync<string[]>(@"() => {
return Array.from(document.querySelectorAll('.post-title'))
.map(el => el.textContent.trim());
}");
}
}
// Usage in tests
[Fact]
public async Task Can_Filter_By_Language()
{
var blogPage = new BlogPageObject(Page);
await NavigateAsync("/blog");
await blogPage.SelectLanguageAsync("Spanish");
var titles = await blogPage.GetPostTitlesAsync();
Assert.All(titles, title => Assert.NotEmpty(title));
}
业绩考量
E2E测试慢于单位测试@, there @'}无法绕过它,但你可以让它们更快地Q:
平行运行测试
x unit 在默认情况下平行运行测试@ , @ but you need to be careful about shared state:}但您需要谨慎对待共享状态@ MPK1 @%
[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 @ : @
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = new[]
{
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage", // Overcome limited resource problems
"--disable-accelerated-2d-canvas",
"--disable-gpu", // Not needed for headless
"--disable-images", // Don't load images if you don't need them
"--disable-javascript", // Only if testing static content
}
});
明智使用网络拦截
屏蔽不必要资源以加快进度@: @%
await Page.SetRequestInterceptionAsync(true);
Page.Request += async (sender, e) =>
{
var blockedResourceTypes = new[]
{
ResourceType.Image,
ResourceType.Media,
ResourceType.Font,
ResourceType.StyleSheet // If you don't need to test styling
};
if (blockedResourceTypes.Contains(e.Request.ResourceType))
{
await e.Request.AbortAsync();
}
else
{
await e.Request.ContinueAsync();
}
};
正在调试 E2E 测试
测试失败时, 您需要调试它们@ ( @ 和他们将会 MS K1} 您可以调试它@ I. @ 这里有一些技巧 @ : @
以非 - 无头模式运行
设置套设 Headless = false 以在动作中查看浏览器@:
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = false,
SlowMo = 100, // Slow down by 100ms to see what's happening
});
使用 DevTools
您实际上可以用程序打开 DevTools 。
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = false,
Devtools = true, // Auto-open DevTools
});
控制台日志
从浏览器中抓取控制台信息@: @%
Page.Console += (sender, e) =>
{
Output.WriteLine($"Browser console: {e.Message.Text}");
};
请求日志
记录所有网络请求@: @%
Page.Request += (sender, e) =>
{
Output.WriteLine($"Request: {e.Request.Method} {e.Request.Url}");
};
Page.Response += (sender, e) =>
{
Output.WriteLine($"Response: {e.Response.Status} {e.Response.Url}");
};
世界测试模式
以下是我经常在 E2E 测试中使用的一些图案 :
测试表格提交情况
[Fact]
public async Task Can_Submit_Comment()
{
await NavigateAsync("/blog/some-post");
// Fill in the comment form
await TypeAsync("#comment-name", "Test User");
await TypeAsync("#comment-email", "[email protected]");
await TypeAsync("#comment-content", "This is a test comment");
// Submit the form
await ClickAsync("#comment-submit");
// Wait for success message
await WaitForSelectorAsync(".comment-success");
// Verify the comment appears
var commentText = await GetTextContentAsync(".comment-list .comment:last-child .comment-content");
Assert.Contains("test comment", commentText.ToLower());
}
测试键盘互动
[Fact]
public async Task Can_Navigate_With_Keyboard()
{
await NavigateAsync("/blog");
// Focus the search box
await Page.FocusAsync("#search");
// Type a search query
await Page.Keyboard.TypeAsync("testing");
// Press arrow down to select first result
await Page.Keyboard.PressAsync("ArrowDown");
// Press enter to navigate
await Page.Keyboard.PressAsync("Enter");
// Verify we navigated to the right page
await WaitAsync(1000);
Assert.Contains("/blog/", Page.Url);
}
测试文件上传
[Fact]
public async Task Can_Upload_Image()
{
await NavigateAsync("/admin/upload");
// Create a test file
var testFilePath = Path.Combine(Path.GetTempPath(), "test-image.jpg");
File.WriteAllBytes(testFilePath, new byte[] { 0xFF, 0xD8, 0xFF }); // JPEG header
// Upload the file
var fileInput = await Page.QuerySelectorAsync("input[type=file]");
await fileInput.UploadFileAsync(testFilePath);
await ClickAsync("#upload-submit");
// Verify upload succeeded
await WaitForSelectorAsync(".upload-success");
// Clean up
File.Delete(testFilePath);
}
拖放测试
[Fact]
public async Task Can_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应用工具 更综合的测试经验 :
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_基本方法:}
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+.}
解决方案#:
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 适当规则 :
/* 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@ "
await page.EmulateMediaTypeAsync(MediaType.Screen); // Force screen media
var pdfData = await page.PdfDataAsync();
3. 断页 - 你的存在贝奈
问题: 您的内容会尴尬地在页面上分割@ , @ 在底部成为孤儿的标题或表格被切成半.}#
**《现实》 : *** 你正在和Chrome '抗争... ...内部的排程算法... ...,...
你能做什么? :吗?
@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;
}
}
在你的木偶剧《夏尔普代码》里
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
PreferCSSPageSize = true, // Respect CSS @page rules
DisplayHeaderFooter = false
});
Pro Tip: 对于复杂的布局来说, @,}有时它会更容易用明确的分页符来构建您的 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:\
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 打印背景图像或颜色 #% (} * 这是保存墨水的浏览器默认值 。 必须 启用该选项@: @}
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 或适当处置@:
// 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 生成@,考虑重新使用浏览器实例 @:
public class PdfGeneratorService : IDisposable
{
private IBrowser? _browser;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task<byte[]> GeneratePdfAsync(string url)
{
await _semaphore.WaitAsync();
try
{
// Reuse browser instance
_browser ??= await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await _browser.NewPageAsync();
await page.GoToAsync(url);
return await page.PdfDataAsync();
}
finally
{
_semaphore.Release();
}
}
public async ValueTask DisposeAsync()
{
if (_browser != null)
{
await _browser.CloseAsync();
await _browser.DisposeAsync();
}
_semaphore.Dispose();
}
public void Dispose()
{
DisposeAsync().AsTask().Wait();
}
}
7. 缩放选项 @- 小文本@,更多内容
有时您需要在页面中添加更多内容@: @%
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Format = PaperFormat.A4,
Scale = 0.8m, // 80% scale - fits more content
PrintBackground = true
});
但是要小心: “-”太小了, 无法读取“.”
世界PDF的产生模式
@'@sthis:我如何在制作中实际生产 PDF 一代?
public class InvoicePdfGenerator
{
private readonly ILogger<InvoicePdfGenerator> _logger;
public InvoicePdfGenerator(ILogger<InvoicePdfGenerator> logger)
{
_logger = logger;
}
public async Task<byte[]> GenerateInvoicePdfAsync(Invoice invoice)
{
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = new[]
{
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage" // Overcome limited resource problems
}
});
await using var page = await browser.NewPageAsync();
// Set up console logging to debug issues
page.Console += (_, e) =>
{
_logger.LogInformation("Browser console: {Message}", e.Message.Text);
};
try
{
// Generate HTML content (using Razor, or however you do it)
var htmlContent = await GenerateInvoiceHtmlAsync(invoice);
// Set content directly rather than navigating to URL
await page.SetContentAsync(htmlContent, new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});
// Wait for fonts to load
await page.EvaluateFunctionAsync("() => document.fonts.ready");
// Force screen media type to avoid print media queries changing layout
await page.EmulateMediaTypeAsync(MediaType.Screen);
// Generate PDF
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
MarginOptions = new MarginOptions
{
Top = "10mm",
Right = "10mm",
Bottom = "10mm",
Left = "10mm"
},
PreferCSSPageSize = false
});
_logger.LogInformation("Generated PDF for invoice {InvoiceId}, size: {Size} bytes",
invoice.Id, pdfData.Length);
return pdfData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate PDF for invoice {InvoiceId}", invoice.Id);
// Take a screenshot for debugging
try
{
var screenshot = await page.ScreenshotDataAsync();
_logger.LogWarning("Captured screenshot of failed PDF generation: {Size} bytes",
screenshot.Length);
// Could save this to blob storage for debugging
}
catch
{
// Swallow screenshot errors
}
throw;
}
}
private async Task<string> GenerateInvoiceHtmlAsync(Invoice invoice)
{
// Your HTML generation logic here
// Could use Razor views, or any templating engine
return $@"
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body {{
font-family: 'Inter', sans-serif;
margin: 0;
padding: 20px;
color: #333;
}}
@media print {{
.page-break {{
page-break-before: always;
}}
.no-break {{
page-break-inside: avoid;
}}
}}
</style>
</head>
<body>
<div class='no-break'>
<h1>Invoice #{invoice.Number}</h1>
<p>Date: {invoice.Date:yyyy-MM-dd}</p>
</div>
<!-- Invoice content -->
</body>
</html>";
}
}
景观与纵向
简单但经常需要的@: @%
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Format = PaperFormat.A4,
Landscape = true, // Horizontal orientation
PrintBackground = true
});
自定义页面大小
不限于标准格式@:
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Width = "210mm", // Custom width
Height = "297mm", // Custom height (this is A4, but you can use any size)
PrintBackground = true
});
木偶人的其他实用用途
超过测试后, PDF Droup +, Puppiteeer Sharp 在其他几项自动化任务中成绩优异...". Let's探索最常见的“-world applications”_.
数据提取网页处理
校对:Portnoy
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 灵活性包 或 闪烁角 @-# 速度快得多, 打火机轻一点 #)@
- High- 批量刮 (browser 间接费用相当可观
- 当有“'” API可用时, “(”总是喜欢官方的API而不是刮!)
自动截图生成
超過測試@, @% 抓图對縮略圖 @ ,} 預覽@ MS K2} 或归档@ MPK3} 有用
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 图像标记
- 为网站画廊创建缩略图
- 为遵约目的存档网页
- 正在生成用于链接共享的预览图像
业绩监测
测量页面负荷性能@:
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 生成组合在一起,用于自动报告@:
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 @""}(因为您不支付PDF图书馆许可证=,}但是它') 资源无自由.
每个浏览器实例@: @%
- 使用 RAM 的 100-200MB
- 需要相当的 CPU 设定
- 生成 PDF & ( 取决于复杂程度 @)}
比较此选项与专用的 PDF 库如 @ : @
- i Text 内容 “(”前的 iTextSharp @)}- 商务许可证要求“(~£500-3000/ year_ZAY ),”,但生成微小内存足迹的毫秒PDF
- 获取PDF -Free and open source 依据MIT 牌照,CMS K2編碼(no HTMLMPK4快速燃燒,
- PdfSharp煤炭 @-_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)
混合方法
有时最好的办法就是同时使用:
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' 推荐的:
以 PutppiteerSharp 开始 若您只测试 Chrome@ / @ chromium% MS K2 It'}比替代品更简单、更快
使用 Playwright 如果您需要多@ -browser 支持 @ MS K1 It's 得到了木偶夏尔普+ Firefox和Safari=.的所有好处
避免 用于新项目, 除非您有特定的理由使用@ ( @ 喜欢 IEQ11} 支持 @ MS K2 希望您能用到% '_ t).}
谨慎写作测试“. ” E2E 测试缓慢,且可能不太顺利。使用它们来进行关键用户旅程;“,”不是用来检测每一个小细节。“MS K4”
保持独立测试“.”每个测试都应该建立自己的数据,
使用助手方法 我展示的基本等级模式 保持你实际测试代码的干净 并关注您在测试什么
E2E 測試必須是痛苦的 @'}用正確的工具和樣式 @MS K3}它實際上可以很愉快的@.}讓 PutsuiteerSharp 開始您的下一個專案 -我認為你會很驚訝地驚喜 @.
右@, @ I'#% 关闭以写入更多测试@ . @ 快乐测试 @ MS K3} @
继续阅读
- Sharp 文档
- 玩偶的API @(JavaScript,但大多数概念都适用#)
- 用于 @ .+NET 的播放器
- x 单位文件
- ASP.NET核心集成测试