الـ مسرحيّة هو الحل الرسمي مايكروسوفت لـ عصريّة المتصفح التشغيل الآلي، يعرض دعم متعدد المتصفحين (Chrome، Firefox، Safari) مع a واحد API، مُنتَج مُنتقَل مُنتقَأ و مُحَازَل. هذا الدليل يغطّي كلّ شيء من الإعداد إلى أنماط اختبار متقدمة، يقارنه مع PuppeteerSharp ليساعدك على اختيار الأداة الصحيحة. إذا كنت فقط بحاجة إلى الكروم و أبسط، فكّر في الإعداد، النتِيْتِيَر Sarp -بدلاً من ذلك، ولكن إذا كنت بحاجة إلى اختبار شامل للمستعرضين، فالكتاب المسرحي هو الطريق إلى الأمام.
إذا كنت قد قرأت مقالتي على النتِيْتِيَر Sarpأنا معجب بأدوات اختبار E2E الحديثة التي لا تجعلك ترغب في تمزيق شعرك خارج. Puppeteer Sharp هو رائع لاختبار الكروم فقط، ولكن ماذا لو كنت بحاجة إلى اختبار عبر المتصفحات المتعددة؟ هذا هو المكان الذي جاري كتابة يأتي في الداخل في.
والمسرحية هي رد مايكروسوفت على مشكلة أتمتة المتصفح، وقد تعلموا من كل ما جاء من قبل. إنها مثل شقيقة بوبيتير شارب الأكثر طموحاً - إنها تفعل كل شيء يقوم به Puppeteer، ولكن تضيف دعم فايرفوكس وسافاري، وانتظار أفضل للسيارات، ومجموعة من أدوات التنقية التي تجعل من إيجاد القضايا أمراً مُطلقاً.
في هذه المقالة، سأريكم كيف تستخدمون بلايرايت ل.NET لاختبار عبر الكروم، فايرفوكس، وسفاري، مع أمثلة رمزية حقيقية وأنماط عملية يمكنك استخدامها اليوم.
لا تفهمني خطأ النتِيْتِيَر Sarp لكن هنا عندما يصبح الكتاب المسرحي منطقياً:
مستخدموك لا يستخدمون الكروم بل يستخدمون:
الحشرة التي تظهر فقط في سفاري يمكن أن تفقدك 20% من مستخدميك المحتملين. حقوق اللعب تسمح لك بإختبار كل ثلاثة بنفس API.
graph TB
subgraph "PuppeteerSharp"
PS[Chrome Only]
PS --> PS1[Fast setup]
PS --> PS2[Simple API]
PS --> PS3[Chrome DevTools]
end
subgraph "Playwright"
PW[Multi-Browser]
PW --> PW1[Chrome, Firefox, Safari]
PW --> PW2[Auto-waiting built-in]
PW --> PW3[Trace viewer]
PW --> PW4[Codegen tool]
PW --> PW5[Better error messages]
end
style PW stroke:#00aa00,stroke-width:3px
style PW1 stroke:#0066cc,stroke-width:2px
style PW3 stroke:#0066cc,stroke-width:2px
style PW4 stroke:#0066cc,stroke-width:2px
(أ) تثبيت: ميكروسوفt.playwwight المجموعة::
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit # Or use xUnit
ثم تثبيت المتصفحات (هذه تنزيلات) , Fox، و ، ، ، ، ، ، ، ، ، ، ، و/ب/ كِتِكِكِت):
pwsh bin/Debug/net9.0/playwright.ps1 install
أو على لينكس/ماك:
playwright install
ملاحظة: أول متصفح يقوم بالتنزيل حوالي 400 ميغابايت. التحديثات اللاحقة أصغر بكثير.
هذا هو إعدادات مشروع اختباري:
<PackageReference Include="Microsoft.Playwright" Version="1.48.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.48.0" />
<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>
أنا أستعمل الوحدةلكن اللاعب يعمل بشكل جيد مع نون الوحدة أو MSTSTSTفي الواقع، في الواقع، لمسرحية لَهُ a يُكرّسُ الوحدة ادمج مع مساعدين إضافيين.
من طراز S- النتِيْتِيَر Sarpولكن مع دعم متعدد الأحماض:
using Microsoft.Playwright;
using Xunit.Abstractions;
namespace Mostlylucid.Test.E2E;
public abstract class PlaywrightTestBase : IAsyncLifetime
{
protected IPlaywright Playwright = null!;
protected IBrowser Browser = null!;
protected IBrowserContext Context = null!;
protected IPage Page = null!;
protected readonly ITestOutputHelper Output;
protected const string BaseUrl = "http://localhost:8080";
// Override this in derived classes to test different browsers
protected virtual BrowserType BrowserType => BrowserType.Chromium;
protected PlaywrightTestBase(ITestOutputHelper output)
{
Output = output;
}
public async Task InitializeAsync()
{
// Create Playwright instance
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
// Launch the specified browser
Browser = await LaunchBrowserAsync();
// Create a new context (like an incognito window)
Context = await Browser.NewContextAsync(new BrowserNewContextOptions
{
ViewportSize = new ViewportSize { Width = 1400, Height = 900 },
IgnoreHTTPSErrors = true
});
// Create a new page
Page = await Context.NewPageAsync();
// Set default timeout
Page.SetDefaultTimeout(30000);
}
private async Task<IBrowser> LaunchBrowserAsync()
{
var options = new BrowserTypeLaunchOptions
{
Headless = true, // Set to false for debugging
};
return BrowserType switch
{
BrowserType.Chromium => await Playwright.Chromium.LaunchAsync(options),
BrowserType.Firefox => await Playwright.Firefox.LaunchAsync(options),
BrowserType.Webkit => await Playwright.Webkit.LaunchAsync(options),
_ => throw new ArgumentException($"Unknown browser type: {BrowserType}")
};
}
public async Task DisposeAsync()
{
await Page?.CloseAsync()!;
await Context?.CloseAsync()!;
await Browser?.CloseAsync()!;
Playwright?.Dispose();
}
}
public enum BrowserType
{
Chromium,
Firefox,
Webkit
}
مُلاحظ نُنشئ Context قبل إنشاء Pageهذا مفهوم مسرحي لا يملكه (بوبيتيه شارب)
تَتْركُك المُؤطراتُ:
// Navigation with better waiting
protected async Task NavigateAsync(string path)
{
var url = path.StartsWith("http") ? path : $"{BaseUrl}{path}";
await Page.GotoAsync(url, new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle
});
}
// Playwright has better built-in waiting, but these are still useful
protected async Task<IElementHandle?> WaitForSelectorAsync(string selector)
{
try
{
return await Page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions
{
State = WaitForSelectorState.Visible
});
}
catch (TimeoutException)
{
return null;
}
}
// Improved element operations with auto-waiting
protected async Task<bool> ElementExistsAsync(string selector)
{
return await Page.Locator(selector).CountAsync() > 0;
}
protected async Task<string?> GetTextContentAsync(string selector)
{
var locator = Page.Locator(selector);
return await locator.TextContentAsync();
}
protected async Task TypeAsync(string selector, string text)
{
await Page.Locator(selector).FillAsync(text);
}
protected async Task ClickAsync(string selector)
{
await Page.Locator(selector).ClickAsync();
}
(ب) أن Page.Locator() -هذا مختلف عن (بوفيتير شارب ) QuerySelectorالمسالك هي:
// PuppeteerSharp way (manual waiting)
await Page.WaitForSelectorAsync("#button");
await Page.ClickAsync("#button");
// Playwright way (auto-waiting)
await Page.Locator("#button").ClickAsync(); // Waits automatically!
هنا حيث يضيء بلايرايت. يمكنك إجراء نفس الاختبار على جميع المتصفحات:
public class CrossBrowserTests : PlaywrightTestBase
{
public CrossBrowserTests(ITestOutputHelper output) : base(output) { }
[Theory]
[InlineData(BrowserType.Chromium)]
[InlineData(BrowserType.Firefox)]
[InlineData(BrowserType.Webkit)]
public async Task HomePage_LoadsCorrectly_InAllBrowsers(BrowserType browserType)
{
// Override the browser type for this test
BrowserType = browserType;
await InitializeAsync();
// Arrange & Act
await NavigateAsync("/");
// Assert
var title = await Page.TitleAsync();
Assert.Contains("Mostlylucid", title);
Output.WriteLine($"✓ Test passed in {browserType}");
}
}
أو إ_ نشئ فئات اختبار منفصلة لكل متصفح:
public class ChromiumTests : PlaywrightTestBase
{
protected override BrowserType BrowserType => BrowserType.Chromium;
public ChromiumTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task FilterBar_WorksInChrome()
{
await NavigateAsync("/blog");
await Page.Locator("#LanguageDropDown button").ClickAsync();
await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();
var url = Page.Url;
Assert.Contains("language=es", url);
}
}
public class FirefoxTests : PlaywrightTestBase
{
protected override BrowserType BrowserType => BrowserType.Firefox;
public FirefoxTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task FilterBar_WorksInFirefox()
{
// Same test, different browser!
await NavigateAsync("/blog");
await Page.Locator("#LanguageDropDown button").ClickAsync();
await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();
var url = Page.Url;
Assert.Contains("language=es", url);
}
}
public class SafariTests : PlaywrightTestBase
{
protected override BrowserType BrowserType => BrowserType.Webkit;
public SafariTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task FilterBar_WorksInSafari()
{
await NavigateAsync("/blog");
await Page.Locator("#LanguageDropDown button").ClickAsync();
await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();
var url = Page.Url;
Assert.Contains("language=es", url);
}
}
دعونا ننظر إلى أمثلة حقيقية تظهر مزايا بلايرايت:
[Fact]
public async Task SortingDropdown_UpdatesContentViaHTMX()
{
// Arrange
await NavigateAsync("/blog");
// Get first post title before sorting
var firstPostBefore = await Page.Locator("article h2 a").First.TextContentAsync();
Output.WriteLine($"First post before: {firstPostBefore}");
// Act - Change sort order
await Page.Locator("#orderSelect").SelectOptionAsync("date_asc");
// Playwright automatically waits for HTMX to update the DOM!
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Assert - Content should have changed
var firstPostAfter = await Page.Locator("article h2 a").First.TextContentAsync();
Output.WriteLine($"First post after: {firstPostAfter}");
var selectValue = await Page.Locator("#orderSelect").InputValueAsync();
Assert.Equal("date_asc", selectValue);
}
جاري تشغيل مُم م م م م م م م الألابين (أ) النشاط والتحرك بسلاسة:
[Fact]
public async Task AlpineDropdown_OpensAndCloses()
{
await NavigateAsync("/blog");
// Click to open dropdown (Alpine.js controlled)
await Page.Locator("#LanguageDropDown button").ClickAsync();
// Wait for Alpine.js animation
await Page.WaitForSelectorAsync("#LanguageDropDown div[x-show]", new()
{
State = WaitForSelectorState.Visible
});
// Check dropdown is visible
var isVisible = await Page.Locator("#LanguageDropDown div[x-show]").IsVisibleAsync();
Assert.True(isVisible);
// Click outside to close
await Page.Locator("body").ClickAsync(new LocatorClickOptions
{
Position = new Position { X = 0, Y = 0 }
});
// Verify closed
await Page.WaitForSelectorAsync("#LanguageDropDown div[x-show]", new()
{
State = WaitForSelectorState.Hidden
});
isVisible = await Page.Locator("#LanguageDropDown div[x-show]").IsVisibleAsync();
Assert.False(isVisible);
}
[Fact]
public async Task CommentForm_ValidatesRequiredFields()
{
await NavigateAsync("/blog/some-post");
// Try to submit without filling fields
await Page.Locator("#comment-submit").ClickAsync();
// Check HTML5 validation messages
var nameInput = Page.Locator("#comment-name");
var validationMessage = await nameInput.EvaluateAsync<string>("el => el.validationMessage");
Assert.NotEmpty(validationMessage);
Output.WriteLine($"Validation message: {validationMessage}");
// Fill form properly
await nameInput.FillAsync("Test User");
await Page.Locator("#comment-email").FillAsync("[email protected]");
await Page.Locator("#comment-content").FillAsync("Great article!");
// Submit
await Page.Locator("#comment-submit").ClickAsync();
// Wait for success
await Page.Locator(".comment-success").WaitForAsync();
}
[Fact]
public async Task PageLoadsWithoutImages_ToTestPerformance()
{
// Block all image requests
await Page.RouteAsync("**/*.{png,jpg,jpeg,gif,webp}", route => route.AbortAsync());
await NavigateAsync("/blog");
// Page should still function without images
var title = await Page.Locator("h1").TextContentAsync();
Assert.NotEmpty(title);
}
[Fact]
public async Task MockAPIResponse_ForTesting()
{
// Intercept API calls and return mock data
await Page.RouteAsync("**/api/posts", route => route.FulfillAsync(new()
{
Status = 200,
ContentType = "application/json",
Body = System.Text.Json.JsonSerializer.Serialize(new
{
posts = new[]
{
new { id = 1, title = "Mock Post 1" },
new { id = 2, title = "Mock Post 2" }
}
})
}));
await NavigateAsync("/blog");
// Should show mock data
var posts = await Page.Locator(".post-title").CountAsync();
Assert.Equal(2, posts);
}
public async Task InitializeAsync()
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
Browser = await LaunchBrowserAsync();
// Start tracing
Context = await Browser.NewContextAsync();
await Context.Tracing.StartAsync(new()
{
Screenshots = true,
Snapshots = true,
Sources = true
});
Page = await Context.NewPageAsync();
}
public async Task DisposeAsync()
{
// Save trace on test completion
var tracePath = Path.Combine("traces", $"{TestContext.TestName}.zip");
await Context.Tracing.StopAsync(new()
{
Path = tracePath
});
// View trace with: playwright show-trace traces/TestName.zip
await Page?.CloseAsync()!;
await Context?.CloseAsync()!;
await Browser?.CloseAsync()!;
Playwright?.Dispose();
}
ثمّ يَرى التتبعَ:
playwright show-trace traces/TestName.zip
هذا يَفْتحُ a:
إنها رائعة تماماً من أجل التصحيح.
public async Task DisposeAsync()
{
// Take screenshot if test failed
if (TestContext.CurrentTestOutcome != TestOutcome.Passed)
{
var screenshot = await Page.ScreenshotAsync();
File.WriteAllBytes($"failure-{TestContext.TestName}.png", screenshot);
Output.WriteLine($"Screenshot saved: failure-{TestContext.TestName}.png");
}
await Page?.CloseAsync()!;
await Context?.CloseAsync()!;
await Browser?.CloseAsync()!;
Playwright?.Dispose();
}
[Fact]
public async Task BlogPage_WorksOnMobile()
{
// Create context with mobile emulation
var iPhone = Playwright.Devices["iPhone 13"];
await using var context = await Browser.NewContextAsync(iPhone);
await using var page = await context.NewPageAsync();
await page.GotoAsync($"{BaseUrl}/blog");
// Check mobile menu is visible
var mobileMenu = page.Locator(".mobile-menu");
await Expect(mobileMenu).ToBeVisibleAsync();
// Desktop menu should be hidden
var desktopMenu = page.Locator(".desktop-menu");
await Expect(desktopMenu).Not.ToBeVisibleAsync();
}
يجيء مع جهاز في:
[Fact]
public async Task DarkMode_TogglesCorrectly()
{
// Start with dark color scheme
await using var context = await Browser.NewContextAsync(new()
{
ColorScheme = ColorScheme.Dark
});
await using var page = await context.NewPageAsync();
await page.GotoAsync($"{BaseUrl}");
// Check dark mode is active
var isDark = await page.EvaluateAsync<bool>(
"() => window.matchMedia('(prefers-color-scheme: dark)').matches"
);
Assert.True(isDark);
// Check background color reflects dark mode
var bgColor = await page.Locator("body").EvaluateAsync<string>(
"el => getComputedStyle(el).backgroundColor"
);
Assert.Contains("rgb(0, 0, 0)", bgColor); // Dark background
}
وهنا نفس الاختبار في المكتبتين لإظهار الاختلافات:
[Fact]
public async Task FilterTest_PuppeteerSharp()
{
await Page.GoToAsync("http://localhost:8080/blog");
// Manual waiting required
await Page.WaitForSelectorAsync("#LanguageDropDown button");
await Page.ClickAsync("#LanguageDropDown button");
// Wait for dropdown animation
await Task.Delay(300);
// Click Spanish option
await Page.WaitForSelectorAsync("#LanguageDropDown li:nth-child(2) a");
await Page.ClickAsync("#LanguageDropDown li:nth-child(2) a");
// Wait for navigation
await Task.Delay(500);
// Check URL
var url = Page.Url;
Assert.Contains("language=", url);
}
[Fact]
public async Task FilterTest_Playwright()
{
await Page.GotoAsync("http://localhost:8080/blog");
// Auto-waiting built in
await Page.Locator("#LanguageDropDown button").ClickAsync();
// Click Spanish option (waits automatically for visibility)
await Page.Locator("#LanguageDropDown li:has-text('Spanish')").ClickAsync();
// Check URL (waits automatically for navigation)
await Expect(Page).ToHaveURLAsync(new Regex(".*language=.*"));
}
ملحوظة:
WaitForSelectorAsync الحاجة إلى الحاجةTask.Delay الحاجة إلى الحاجةExpecthas-text الانتقاءمثل PuppeteerSharp، يمكن أن ينتج Buppeteer Sharp PDS. الـ API متطابق تقريباً:
public async Task<byte[]> GeneratePdfAsync(string url)
{
await using var browser = await Playwright.Chromium.LaunchAsync();
await using var page = await browser.NewPageAsync();
await page.GotoAsync(url);
return await page.PdfAsync(new()
{
Format = "A4",
PrintBackground = true,
Margin = new()
{
Top = "20mm",
Right = "20mm",
Bottom = "20mm",
Left = "20mm"
}
});
}
نفس الـ PDF حصلت على شاشا من مقالة Puppeteer Sharp هنا أيضاً.
graph TD
A[Need E2E Testing?] --> B{Multi-browser required?}
B -->|Yes| C[Playwright]
B -->|No| D{Chrome only?}
D -->|Yes| E[PuppeteerSharp or Playwright]
D -->|No| C
C --> F[Benefits]
F --> F1[Test all major browsers]
F --> F2[Better debugging tools]
F --> F3[More reliable waiting]
F --> F4[Mobile emulation]
E --> G[PuppeteerSharp Benefits]
G --> G1[Simpler setup]
G --> G2[Smaller overhead]
G --> G3[Chromium focused]
style C stroke:#00aa00,stroke-width:3px
style E stroke:#0066cc,stroke-width:2px
تنفيذ::::::::::::::::
دعم::::::::::::::::::::::::::::::::::::::::::::::
يعمل بلايرايت بسلاسة في خطوط أنابيب CI/ CD. ها هو اكمال جو سير العمل:
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
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: Install Playwright browsers
run: pwsh Mostlylucid.Test/bin/Debug/net9.0/playwright.ps1 install --with-deps
- name: Start application
run: |
dotnet run --project Mostlylucid/Mostlylucid.csproj &
echo $! > app.pid
- name: Wait for application
run: |
timeout 60 bash -c 'until curl -f http://localhost:8080/health; do sleep 2; done'
- name: Run Playwright tests
run: |
dotnet test Mostlylucid.Test/Mostlylucid.Test.csproj \
--filter "Category=E2E" \
--logger "console;verbosity=detailed"
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-results
path: |
**/test-results/
**/traces/
- name: Stop application
if: always()
run: kill $(cat app.pid) || true
جاري جاري جاري تنفيذ جاريت مُصد مُصد مُصد مُكر يتطلّب تثبيت مُعتمدات النظام:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
# Install Playwright dependencies
RUN apt-get update && apt-get install -y \
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet build
RUN pwsh Mostlylucid.Test/bin/Debug/net9.0/playwright.ps1 install
CMD ["dotnet", "test"]
من اختباري على هذه المدونة:
□ الصورة u puppeteer shrp o blaywright o
| --------- | --------------- | ------------ |
|---|---|---|
| المعالجات المتعددة المحاجج (الآدز = 1s لكل مُسْتَرْجِح) □ □ □ (الآدز = 1s لكل مُسْتَرْحِل) □ □ | ||
| الموثوقية في انتظار التشغيل □ جيد (دليل) أو ممتاز (أتوماتيكي) □ جيد (دليل) □ ممتاز (أتوماتيكي) □ | ||
| ذاكرة متصفح □ ~ 1.50 umB | ||
| عقد التهيئة / بسيط / / / / / / / / / / / / / / بسيط / / / / / / / / / / / / / / / | ||
| جاري جاري تنفيذ جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري جاري تنفيذفكفكفكفكفكفك جاري جاري جاري جاري جاري جاريفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفك جاري جاري جاري جاريفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفكفك محلات العرض الأثري + DevTools o | ||
| **** الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي أو الأساسي |
والكتاب المسرحي أبطأ قليلاً قليلاً ويستخدم ذاكرة أكثر قليلاً، ولكن فوائد الموثوقية والتنقيح عادة ما تفوق هذا.
مُحدّدات موقع مسرحيّات مسرحيّة مُصَدّرة بالافتراض:
// This errors if multiple buttons exist
await Page.Locator("button").ClickAsync();
// Be specific
await Page.Locator("button#submit").ClickAsync();
// Or use .First if you really want the first match
await Page.Locator("button").First.ClickAsync();
لا تنسى أن تركض playwright install بعد إضافة الحزمة، المتصفحات غير مُضمّنة في حزمة النوتات.
تذكّر السياقات:
// Bad - state leaks between tests
await Page.GotoAsync("/login");
// Do stuff...
await Page.GotoAsync("/dashboard");
// Previous login state might still exist
// Good - fresh context per test
await using var context = await Browser.NewContextAsync();
await using var page = await context.NewPageAsync();
// Completely isolated
يمكن أن يتصرف السفاري (WebKit) بشكل مختلف:
// May work in Chrome but fail in WebKit
await Page.Locator(".dropdown").Hover();
await Page.Locator(".dropdown-item").ClickAsync();
// More reliable across browsers
await Page.Locator(".dropdown").ClickAsync();
await Page.WaitForSelectorAsync(".dropdown-item");
await Page.Locator(".dropdown-item").ClickAsync();
يُمَثّل دور الـ تطور من متصفح التشغيل التشغيل التشغيل الآلي لـ net لـ إذا كنت بحاجة إلى دعم متعدد المتصفح، فهو الخيار الواضح. نعم، هو أكثر تعقيداً قليلاً من النتِيْتِيَر Sarpولكن الفوائد كبيرة:
التوصية:
التتبع المُشاهد وحده وفّر لي ساعات من التصحيح. القدرة على إعادة تكرار اختبار فاشل ورؤية بالضبط ما رأى المتصفح في كل خطوة قيمة.
أعطي الكاتب المسرحي فرصة لمشروعك التالي - ستتفاجئ بسرور كم يجعل من السهل إجراء اختبار المستعرضين.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.