لا يجب أن يكون الأمر مؤلماً. هذا دليل شامل يعرض لكم كيفية استخدام Puppeteer Sharp لسرعة، التشغيل الآلي الموثوق به في. NET تغطية كل شيء من الاختبارات الأساسية إلى توليد PDF وخليط الويب. في حين أن لعبة ميكروسوفت هي الحل الأكثر حداثة متعدد المتصفح، اخترت Puppeteer Sharp لهذه المدونة لأنه ما أعرفه واختبار الكروم كان كافياً لاحتياجاتي. إذا كنت بحاجة لدعم فايرفوكس وسفاري، تحقق من بلدي. دليل بدلاً من ذلك.
إذا كنت قد عملت من أي وقت مضى مع سِِِِِِِِِِِِِسِِلِِِِسِِِِِِِِِِِِِِِِِ فيما بين المصارعة مع إصدارات السائقين، التعامل مع الإختبارات الفضفاضة التي تعمل على آلتك ولكن في أي مكان آخر، والبطء العام في بروتوكول مفك ويب، انها كافية لجعلك تريد رمي كل شيء في واختبار يدويا بدلا من ذلك.
النتِيْتِيَر Sarp - ميناء شبكة الإنترنت لغوغل مقدم المُقِل المكتبة، مثل ابن عم (سيلينيوم) الأصغر والأسرع الذي يزعج نفسه بالظهور في الوقت المحدد ولا يتطلب منك تحميل سبعة عشر سائق متصفح مختلف.
في هذه المقالة، سأطلعكم على كيفية تنفيذي لـ إختبارات E2E على هذه المدونة، كاملة بأمثلة رمزية حقيقية من Repo. سنغطي الاختبار، توليد PDF، تفكيك الويب، ومقارنتها مع البدائل.
النتِيْتِيَر Sarp هو مكتبة على الشبكة العالمية تقدم API عالي المستوى للتحكم في متصفحات الكروم أو الكروم باستخدام البروتوكول الملحق ببروتوكولعلى خلاف خلاف خلاف سِِِِِِِِِِِِِسِِلِِِِسِِِِِِِِِِِِِِِِِالتي تستخدم نظام الأمم المتحدة ويتحدث Puppeteer Sharp مباشرة إلى المتصفح من خلال Devtools.
فكر في الأمر بهذه الطريقة:
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 في المخطط الكبير للأشياء. ربما سمعت عن الهرم الاختباري -- هنا كيف يعمل في الواقع في الممارسة العملية:
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
الـ افتراض التحقق:
عندما تحتاج إلى اختبارات E2E:
عندما لا تحتاج إلى اختبارات E2E:
اسمحوا لي أن أعد الطرق:
لا يوجد موظف شؤون موظفينBuppeteer Sharp تنزيلات وإدارة متصفح الكروم بالنسبة لك. لا مزيد من العبث مع إصدارات CromeDriver التي لا تتطابق مع النسخة الكرومية المثبتة.
مُكِرْرِم من التنفيذبروتوكول Devtools أسرع بكثير من محرّك ويب. اختباراتك سوف تعمل بشكل أسرع، وسوف تقضي وقتاً أقل في انتظار حدوث الأشياء.
أفضل معدلاً أدنىالـ API هو أكثر حداثة وبدائية. هو async/await على طول الطريق إلى أسفل، الذي يلائم بشكل جميل مع التطور الحديث.NET.
أنشئت في شاشة الشاشات و توليد PDهل تريد لقطة عندما يفشل الاختبار؟ انها بسيطة جدا مع Puppeteersharp.
طلبات الشبكة: يمكنك ان تعترض او تعدل او توقف طلبات الشبكة بسهولة — عبقرية لاختبار السيناريوهات غير المباشرة او الاستهزاء باستجابات API .
تنفيذتنفيذ جافاسكربت في سياق الصفحة والحصول على النتائج مرة أخرى بطريقة لا تجعلك تريد البكاء.
أولا، يضاف ما يلي: النتِيْتِيَر Sarp المجموعة::
dotnet add package PuppeteerSharp
هذا هو إعدادة مشروع اختباري (Mostlylucid.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>
أنا أستعمل الوحدة (ASP.net's core's dibudor)، ولكن PuppeteerSharp يعمل بنفس القدر من الجودة مع نون الوحدة أو MSTSTST.
بدلاً من تكرار التثبيت/الكود التنازلي في كل اختبار، لقد خلقت درجة أساسية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;
}
نحن ننفذ وقت الحياة على عكس البناءات التقليدية، هذا يجعلنا ننتظر بشكل صحيح بدء المتصفح.
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.
public async Task DisposeAsync()
{
if (Page != null) await Page.CloseAsync();
if (Browser != null) await Browser.CloseAsync();
}
}
والتصريف السليم أمر بالغ الأهمية لتجنب تسربات الذاكرة، وتستخدم كل حالة من حالات المتصفح 100-200 ميغابايت من إدارة السجلات والمحفوظات.
وتشمل الدرجة الأساسية طرق المساعدة على تقليل طواحين الغلاية (الصفائح الأساسية).Mostlylucid.Test/E2E/E2ETestBase.cs:72-172):
// Navigation with automatic network idle waiting
protected async Task NavigateAsync(string path)
{
var url = path.StartsWith("http") ? path : $"{BaseUrl}{path}";
await Page.GoToAsync(url, new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
});
}
// Safe element waiting with timeout handling
protected async Task<IElementHandle?> WaitForSelectorAsync(string selector, int timeout = 5000)
{
try
{
return await Page.WaitForSelectorAsync(selector, new WaitForSelectorOptions
{
Timeout = timeout,
Visible = true
});
}
catch (WaitTaskTimeoutException)
{
return null; // Graceful degradation
}
}
// Common element operations
protected async Task<bool> ElementExistsAsync(string selector) =>
await Page.QuerySelectorAsync(selector) != null;
protected async Task<string?> GetTextContentAsync(string selector)
{
var element = await Page.QuerySelectorAsync(selector);
return element == null ? null :
await Page.EvaluateFunctionAsync<string>("el => el.textContent", element);
}
protected async Task TypeAsync(string selector, string text, int delay = 50)
{
await Page.WaitForSelectorAsync(selector);
await Page.TypeAsync(selector, text, new TypeOptions { Delay = delay });
}
protected async Task ClickAsync(string selector)
{
await Page.WaitForSelectorAsync(selector);
await Page.ClickAsync(selector);
}
وَهٰذَا يَتَعَلَّمُ مِنَ ٱلْأَمْرِ ٱلْمَضِيقِيَّةِ ٱلَّتِي تَتَوَقَّعُ أَنْ تَجِدَ ٱلْعَزَائِزُ وَتَحْتَمِلُ ٱلْوَقْتِ ٱلْمُسْتَقْبَلِ ، وَتَسْجِلُ ٱلْأَتْوَاتِيَّةُ حِينَ تَخْسُرُ ٱلْإِخْتِبَارَاتُ فِي ٱلْمُخْتَارِ .
حسناً، دعونا نذهب إلى الأشياء الجيدة - كتابة الاختبارات الفعلية. ها هو اختبار حقيقي من وظيفة مرشّح مدوّنتي.Mostlylucid.Test/E2E/FilterBarTests.cs:20-50):
[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");
}
هذا الاختبار هو التحقق من أن هبوط لغتي يعمل بشكل صحيح. دعونا ننظر إلى ما يجعلها مؤكّدة:
[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
لقد تخطيت هذا الاختبار افتراضياً لأنه يتطلب من الموقع أن يعمل محلياً. بالنسبة لاختبارات E2E، أنت عادة تريد تشغيلها بناء على الطلب بدلاً من كل بناء. يمكنك فك التزحلق عليها عندما تكون مستعداً لتشغيلها، أو تشغيلها في وظيفة مستقلة CI حيث كنت قد حصلت على الموقع ملفقة.
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 يسمح لك بطريقة تشغيل جافاسكربت في سياق المتصفح والحصول على النتيجة مرة أخرى كنوع مناسب. Net. في هذه الحالة، أنا أتحقق من ما إذا كان القطر للأسفل ظاهراً فعلاً (ليس فقط موجود في DOM) بالنظر إلى أساليبه المحسوبة.
قارن هذا بالسيلينيوم حيث تحتاج إلى:
1. 2. احصل عليه عرض خاصّي 3. 4. آمل أنه ليس مبتلاً في الوقت الذي تتحقق منه
استخدامات مدوّاني XXXX هذا اختبار يفحص وظيفة الفرز (Server-side exsion بدون كتابة JavaScript). هذا هو الاختبار الذي يفحص وظيفة الفرز (The this a text this a this this this this this this this this this this this this this this this this this this this that that this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this thisMostlylucid.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. في عالم مثالي، كنا ننتظر
هذا هو الاختبار الوهمي الذي يفحص شريط ترشيحي مخبأ بشكل صحيح على الأجهزة المحمولة (الأجهزة المحمولة)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
});
}
يمكنك تغيير موقع العرض في أي وقت، والذي هو رائع لاختبار التصميمات المستجيبة. أسهل بكثير من إعادة تعريف نافذة متصفحك يدويا!
واحدة من ميزاتي المفضلة هي القدرة على اعتراض وتعديل طلبات الشبكة. هذه قيمة جداً لاختبار حالات الخطأ أو سيناريوهات الازالة:
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();
}
};
عندما يفشل الاختبار، تكون الشاشات مساوية لألف من رسائل اللوغارتم:
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
}
يمكنك حتى توليد PDFs من الصفحات، والذي هو مفيد لإختبار جانب الخادم أو طباعة أوراق الأسلوب:
await Page.PdfAsync("page.pdf", new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true
});
يمكن لـ Puppeteer Sharp أن يجمع JavaSccript code covernment data:
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}%");
دعونا نلقي نظرة دقيقة على كيفية تكديس Puppeteer Sharp ضد أدوات اختبار أخرى E2E:
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
الحرس القديم
السيلينيوم موجود منذ عام 2004 وهو يظهر. إنه ناضج، موثق جيداً، ويدعم كل متصفح تحت الشمس. لكنه أيضاً يظهر عمره:
Pros:
كون:
متى تستخدمه: عندما تحتاج إلى اختبار عبر متصفحات متعددة، أو عندما تكون قد استثمرت بالفعل في نظام سيلينيوم البيئي.
الطفل الجديد على كتلة المبنى
جاري كتابة هو جواب مايكروسوفت لـ Puppeteer، مع صافي الدعم خبزت من البداية. إنها في الأساس PuppeteerSharp ولكن مع دعم متعدد المستعرضين:
Pros:
كون:
متى تستخدمه: عندما تحتاج إلى دعم متعدد المتصفح ولكن تريد API الحديث. إذا كنت تبدأ مشروعاً جديداً وتحتاج إلى اختبار المستعرض المتقاطع، فإن اللاعب هو على الأرجح أفضل رهان لديك.
مُطوِّر المُطوِّر المُنَجِّزName
السايبرس هو رائع إذا كنت تعمل في جافاسكربت / TypeScript، لكنه غير بدء ل.Net المطورين:
Pros:
كون:
متى تستخدمه: لا تفعل، أنت تكتب رمزاً لشبكة الإنترنت، إلتزم بشيء يتكامل مع كومة تقنياتك.
هذه هي حصتي:
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
بالنسبة لمعظم المطورين الذين يبنون تطبيقات شبكية حديثة:
اختبارات E2E كلها جيدة وجيدة على آلتك المحلية، لكنها تحتاج إلى تشغيل في خطوط أنابيب CI/CD أيضا. جو:
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. جاري التنفيذ 4. يجري التحميل 5. أوقف التطبيق دائماً، حتى لو فشلت الاختبارات
اختبارات 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);
دائماً انتظر العنصر الذي ستتفاعل معه لكي يكون موجوداً ويكون مرئياً
يجب أن يكون كل اختبار مستقل تماماً. لا تعتمد على الحالة من الاختبارات السابقة:
&::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
[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 أبطأ من اختبارات الوحدة، لا يمكن التفاف حولها. ولكن يمكنك جعلها أسرع:
يُجري (xunt) اختبارات بالتوازي مع افتراضي، لكن يجب أن تكون حذراً حول الحالة المشتركة:
[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
}
اختبارات السرعة عن طريق ميزات معوقة لا تحتاجها:
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();
}
};
عندما تفشل الاختبارات (وسوف تفشل)، تحتاج إلى تصحيحها. هنا بعض التقنيات:
Headless = false إلى مراقب المتصفح في العمل:
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = false,
SlowMo = 100, // Slow down by 100ms to see what's happening
});
يمكنك في الواقع فتح 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_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);
}
يمكنك دمج Puppeteter Shar مع (بآلاف دولارات الولايات المتحدة) لتجربة اختبار أكثر تكاملا:
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";
}
}
في حين أن اختبار E2E رائع، فإن PuppeteerSharp هو سكين الجيش السويسري الذي يمكن أن تفعل أكثر من ذلك بكثير. واحد من أكثر الاستخدامات شعبية لها هو توليد PDFs من محتوى الويب - إنه مفيد بشكل لا يصدق لهذا، على الرغم من أنه ليس بدون واتشاه. إذا كنت تبني الفواتير، التقارير، أو أي نظام لتوليد الوثائق، فإن هذا القسم سيوفر عليك ساعات من التصحيح.
والفكرة بسيطة: توفير صفحة على الشبكة العالمية في الكروم وادخارها كنموذج لـ PDF. وهو مثالي لإنتاج الفواتير، والتقارير، والشهادات، أو أي محتوى ديناميكي يحتاج إلى التوزيع في شكل PDF.
هذا هو النهج الأساسي:
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، أو أسوأ من ذلك، أنها هناك ولكن تبدو قمامة تماما.
لماذا يحدث : يحتاج الكروم إلى الوصول إلى ملفات الخط أثناء توليد PDF. إذا كانت خطوطك مُحمّلة عن طريق CDN الخارجية وChrome لا تستطيع الوصول إليها (حوار النار، إصدارات الشبكة، التوقيت)، فأنت محشوة.
الحل:
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
});
أفضل من ذلك، استضافة خطوطك محلياً أو دمجها على أنها أساس 64 في CSS. نعم، هو faff، لكنه موثوق به.
المشكلة: PDF الخاص بك يبدو لا شيء مثل صفحتك على الإنترنت لأن Chrome يطبق إستفسارات وسائل الإعلام المطبوعة.
هذا سلوك صحيح - قوات الدفاع الشعبي هي وسائل الإعلام المطبوعة، لكنها تجذب الجميع للخارج في المرة الأولى
الحل:
@media print قواعد CFSS بشكل مناسب:
/* 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 الخاص بك (مفيدة لتوليد "مقذوفات الشاشة" مثل PDFs):
await page.EmulateMediaTypeAsync(MediaType.Screen); // Force screen media
var pdfData = await page.PdfDataAsync();
المشكلة: محتوياتك تُقسّم بشكل مُحرج عبر الصفحات، مع العناوين التي يُتِمّت في الأسفل أو الطاولات تُقطّع إلى نصفين.
الواقع: أنت تحارب ضد خوارزمية (كروم) الداخلية للمضاجعة، وهي ستربح معظم الوقت.
ما يمكنك فعله :
@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
});
برو تلميج: بالنسبة للتخطيطات المعقدة، في بعض الأحيان يكون من الأسهل هيكلة HTML الخاص بك مع فواصل الصفحات الصريحة بدلا من محاربة المتصفح:
<div class="page">
<!-- First page content -->
</div>
<div class="page-break"></div>
<div class="page">
<!-- Second page content -->
</div>
يمكنك إضافة الواجهات والأحذية، ولكن API هو قليلا من المنطاد:
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
}
});
الإحزات:
date, title, url, pageNumber, totalPagesافتراضياً، الكروم لا يطبع صور الخلفية أو الألوان (هذا هو متصفح افتراضي لحفظ الحبر). أنت. لا يجب هو:
var pdfData = await page.PdfDataAsync(new PdfOptions
{
PrintBackground = true // Without this, your beautiful backgrounds vanish
});
المشكلة: تَخْلقُ الكثير مِنْ PDFs يَجْعلُ ذاكرةَ تطبيقِكَ إلى بالونِ وفي النهاية تَحطّمَ.
السبب: كل حالة من حالات المتصفح تستخدم ذاكرة كبيرة (100-200MB)، وإذا كنت لا تتخلص بشكل صحيح، فإنها تتراكم.
الحل:
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();
}
}
بالنسبة لجيل 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();
}
}
في بعض الأحيان تحتاج إلى وضع المزيد من المحتوى في صفحة:
var pdfData = await page.PdfDataAsync(new PdfOptions
{
Format = PaperFormat.A4,
Scale = 0.8m, // 80% scale - fits more content
PrintBackground = true
});
لكن كن حذراً - صغير جداً وهو غير قابل للقراءة
هذه هي الطريقة التي أقوم بها في الواقع توليد 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، فإن PuppeteerSharp يتفوق في العديد من مهام التشغيل الآلي الأخرى. دعونا نستكشف التطبيقات الأكثر شيوعاً في العالم الحقيقي.
Puppeteer Sharp هو رائع لتخريد جافاScript-heavy المواقع حيث يقصر HTML prosers التقليدية:
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;
}
}
متى تستخدمه:
عندما لا تستخدمه:
بعد الاختبار، تكون شاشات المقذوفات مفيدة للإبهامات، أو المعاينة، أو الأرشيف:
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();
}
}
استخدامات في الأغراض العملية:
أداء أحمال مقياس الصفحة:
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"
}
});
}
}
الآن، هنا الشيء عن استخدام Puppeteer Sharp لـ PDF جيل - هو "مجاني" بمعنى أنك لا تدفع مقابل ترخيص مكتبة PDF، لكنه مـن مبلـغ.
كل حالة من حالات المتصفح:
قارن هذا إلى مخصّص PDF مثل:
متى إلى استخدام PDFs لـ:
متى إلى استخدام PDF مكتبات:
وفي بعض الأحيان يستخدم الحل الأفضل كلاً من:
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();
}
}
Puppeteer Sharp كان مُغير لعبة مطلق لاختبار E2E في مشاريعي. NET. إنه أسرع من السيلينيوم، لديه API أكثر حداثة،
وهذا ما أوصي به:
ابدأ مع Puppiteer Shar إذا كنت فقط تختبر الكروم/الكروميوم. إنه أبسط وأسرع من البدائل.
**** إذا كنت بحاجة إلى دعم متعدد المتصفحين، فلديها كل الفوائد من (بوليبيتر شارب) بالإضافة إلى (فايرفوكس) و (سافاري)
مُنْعِج لمشاريع جديدة ما لم يكن لديك سبب محدد لاستخدامه (مثل دعم IE11، والذي نأمل أنك لا تفعل).
& كتابة الإختباراتاختبارات E2E بطيئة ويمكن أن تكون هشّة. استخدمها لرحلات المستخدم الحرجة، وليس لاختبار كل تفصيل صغير. هذا هو الغرض من اختبارات الوحدة والتكامل.
(أ) الاختباراتوينبغي لكل اختبار أن يضع بياناته الخاصة به وأن ينظفه من تلقاء نفسه.
**** بشكل ليبرالي، نمط الدرجة الأساسية الذي عرضته يبقي رمز اختبارك الفعلي نظيفاً
اختبار E2E ليس من الضروري أن يكون مؤلماً. مع الأدوات والأنماط الصحيحة، يمكن أن يكون في الواقع لطيفاً جداً. أعطِ Puppeteersharp a تشغيل على مشروعك التالي - أعتقد أنك سوف تكون مفاجئاً بسرور.
حسناً، أنا ذاهب لكتابة المزيد من الاختبارات، اختبار سعيد!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.