# 多浏览器 E2E 测试,使用 .NET 的 Playwright 使用 Playwright 测试 。

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

<!--category-- Playwright, E2E Testing, xUnit, Testing, Multi-Browser -->
Playwright 是微软的现代浏览器自动化官方解决方案, 提供多浏览器支持( Chroome、 Firefox、 Safari) , 提供单一的 API 、 内嵌追踪调试和移动模拟。 本指南覆盖了从设置到高级测试模式的所有内容, 将其与 PutputeerSharp 比较, 以帮助您选择正确的工具 。 如果您只需要 Chrome 和想要更简单的设置, 请考虑 [夏尔普(Sharp)](/blog/puppeteersharp-e2e-testing) 但是如果你需要全面的交叉浏览器测试 普莱特是前进的方向

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

如果你读过我的文章 [夏尔普(Sharp)](/blog/puppeteersharp-e2e-testing),你会知道我是现代E2E测试工具的粉丝,这些工具不会让你想撕掉你的头发。PutputeeerSharp非常适合只用铬的测试, 但如果你需要通过多个浏览器进行测试呢? [lay人赖](https://playwright.dev/) 进来

Playwright是微软对浏览器自动化问题的答案, 他们从以前的一切中都学到了教训。就像 PutppeeeerSharp 更雄心勃勃的搭桥人一样, 它做了木偶人所做的一切, 但是增加了Firefox和Safari的支持, 更好的自动等待, 以及大量的调试工具, 使得发现问题成为绝对的托盘。

在本篇文章中,我将展示如何使用Playwright来为.NET测试铬、Firefox和Safari, 并使用今天可以使用的真实代码实例和实际模式。

[TOC]

## 为什么是《玩偶剧剧本》?

别误会我 [夏尔普(Sharp)](/blog/puppeteersharp-e2e-testing) 如果你只需要铬,就太好了。但现在,普赖特说得通:

### 多浏览器问题

您的用户并非全部使用铬。 他们使用 :

- **铬/日**:~65%的用户(铬基)
- **萨法里**: ~20%的用户(特别是移动用户)
- **消防**: ~5% 的用户(但往往是不同的成份)

仅出现在 Safari 的错误会失去20%的潜在用户 。 玩家让您用相同的 API 测试所有三个 。

### 更好的开发者经验

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

### 关键优点

1. **多浏览器支持** - 测试铬、Firefox、Safari等同代码
2. **更好的自动等待** - 更可靠地从盒子里出来,减少片片状测试
3. **追踪查看器** - 记录用于调试的测试运行的全部痕迹
4. **腐素** - 通过与网站互动生成测试代码
5. **网络拦截** - 比木偶更强大 夏尔普
6. **现代网络功能** - 更好地支持暗影DOM, iframes,等等。

## 为.NET 设置 Playwright 设置 。

安装 [微软. laywright](https://www.nuget.org/packages/Microsoft.Playwright) NuGet 软件包 :

```bash
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit  # Or use xUnit
```

然后安装浏览器( 此下载) [铬](https://www.chromium.org/), [消防](https://www.mozilla.org/firefox/), 和 [WebKit 网络文件](https://webkit.org/)):

```bash
pwsh bin/Debug/net9.0/playwright.ps1 install
```

或Linux/Mac上:

```bash
playwright install
```

**注:** 第一个浏览器安装了大约400MB的下载。 随后的更新要小得多 。

以下是我的测试工程配置:

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

我在用 [x 单位](https://xunit.net/),但Playwright 工作与 [N单位单位](https://nunit.org/) 或 [微测试](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest)事实上, Playwright 拥有一个 [N单位一体化](https://playwright.dev/dotnet/docs/test-runners#nunit) 并配备辅助人员。

## 创建基准测试类

类似 [夏尔普(Sharp)](/blog/puppeteersharp-e2e-testing#creating-a-base-test-class),但有多个浏览器支持:

### 阶级结构

```csharp
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`。这是一个玩耍者的概念, 傀儡夏尔普没有这个概念:

- **背景背景背景背景** = 孤立的浏览器会话( 如未知模式)
- **页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次 页次** = 上下文中的标签

上下文允许您:

- 同时测试不同的用户会话
- 每个上下文设定不同的 cookie、 权限或位置
- 完全分离测试( 即使在同一浏览器实例中)

### 帮助方法

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

### locators - 玩弄者的秘密武器

通知通知 `Page.Locator()` 这和小木偶夏尔普的电话不一样 `QuerySelector`定位器为:

- **懒懒** - 在你使用之前,他们不会询问DOM
- **重试** - 自动等待和重试操作
- **严格** - 如果多个元素匹配(防止弹片测试),则错误( 防止弹片测试)

```csharp
// PuppeteerSharp way (manual waiting)
await Page.WaitForSelectorAsync("#button");
await Page.ClickAsync("#button");

// Playwright way (auto-waiting)
await Page.Locator("#button").ClickAsync(); // Waits automatically!
```

## 多浏览器测试

这里就是Playwright发光的地方。 您可以对所有浏览器进行相同的测试 :

### 多浏览测试使用 x 单位理论

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

### 浏览器- 特定浏览器测试类

或为每个浏览器创建单独的测试类 :

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

## 写作测试 - laywright 路径

让我们看看真实的例子, 显示Playwright的优势:

### 用自动等待测试 HTMX

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

### Alpine.js相互作用测试

Playwright 手柄 [阿尔卑山](https://alpinejs.dev/) 活动与动画天衣无缝:

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

### 带有校验的测试表格

```csharp
[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("test@example.com");
    await Page.Locator("#comment-content").FillAsync("Great article!");

    // Submit
    await Page.Locator("#comment-submit").ClickAsync();

    // Wait for success
    await Page.Locator(".comment-success").WaitForAsync();
}
```

## 高级 Playwright 特征

### 网络拦截 - 比傀儡更强大

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

### 追踪查看器 - 调试失败测试

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

然后查看追踪 :

```bash
playwright show-trace traces/TestName.zip
```

这将打开一个 UI 显示 :

- 你每做一次测试
- 每一步的截图截图
- 联网请求
- 控制台日志
- DOM快照

这是绝对辉煌的调试。

### 失败的截图截图

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

### 移动模拟

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

游戏剧作家随而来 [设备描述器](https://playwright.dev/dotnet/docs/emulation#devices) 用于:

- iPhone 13, 13 pro, 12, 11, SE iPhone 13, 13 pro, 12, 11, SE
- iPad、iPad Pro
- 三星银银河,像素
- 还有更多

### 测试黑暗模式

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

## Playwright 和 Putppeteer Sharp - 边边对边

以下是两个图书馆的相同测试, 以显示差异:

### Putpeeer Putpeer Sharp 版本

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

### Playwright 版本

```csharp
[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` 所需
- 更清洁的主张 `Expect`
- 更易读用 `has-text` 选择器

## PDF 使用 Playwright 的 PDF 生成

Playwright可以生成 PDF。 API几乎相同 :

```csharp
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 从 PutputeeerSharp 文章中获取的查斯( PDF)](/blog/puppeteersharp-e2e-testing#pdf-generation-gotchas) 这里也适用。

## 选择 Playwrright 何时选择

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

**选择 Playwright 时 :**

- 您需要多浏览器测试
- 您想要更好的调试工具( 跟踪查看器)
- 你在测试复杂的现代网络应用程序
- 你需要移动模拟
- 你想从盒子里得到更可靠的测试

**选择开胃菜 Sharp 时 :**

- 只有铬/铬才算罚款
- 你想要更简单的设置
- 你已经熟悉"傀儡"了
- 您需要略微少一点资源管理管理费

## 在 CI/ CD 中运行

### GitHub 行动实例

Playwright在CI/CD管道中无缝运作。 [GitHub 行动组织](https://github.com/features/actions) 工作流程 :

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

### 嵌套支持

正在运行 Playwright 测试 [嵌嵌入器](https://www.docker.com/) 需要安装系统依赖性:

```dockerfile
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"]
```

## 业绩比较

从我在这个博客上的测试来看:

主角: 小木偶 夏尔普 剧作家:
|---------|---------------|------------|
| **铬测试速度** ~~ 2.5s~~~~ 2.8s~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| **多浏览器** * * * * * * * * * * (每浏览器增加~1s) * * * * * * (每浏览器增加~1s) * * * * * (每浏览器增加~1s) * * * * * (每浏览器增加~1s) * * * * (每浏览器增加~1s) * * * * (每浏览器增加~1s) * * * * (每浏览器增加~) * * * * (每浏览器增加~1s) * * * * (每浏览器增加~) * * * * * (每浏览器增加~) * * * * * (每浏览器增加~1) * * * * * * * * (每浏览器增加~) * * * * *
| **自动等待可靠性** 棒极了(自动) 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了 (自动) 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了 (自动) 棒极了 棒极了 棒极了 棒极了 棒极了 棒极了
| **每个浏览器的内存** ~ 150MB~~ 180MB~ ~ 150MB~ ~ 180MB~ ~ 150MB~
| **设置复杂设置** 简单 * 中度 *
| **调试工具** 
| **移动模拟** * 基础 * * 优秀 * * 基本 * * 优秀 * * * 基本 * * 优秀 * * * 基本 * * 优秀 * * 基本 * * 优秀 * * 基本 *

Playwright稍慢一点,使用记忆略多一点,但可靠性和调试的好处通常大于此。

## 共犯组织

### 1. 严格模式

Playwright 的定位器默认是严格的 :

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

### 2. 浏览器安装

别忘了跑,别忘了跑 `playwright install` 添加软件包后。 浏览器不包括在 NuGet 软件包中 。

### 3. 背景与页

记住背景分离测试 :

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

### 4. Webkit 差异

Safari (WebKit) 的行为可能不同:

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

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

Playwright 代表了.NET 开发者浏览器自动化的进化。 如果您需要多浏览器支持, 这是明确的选择。 是的, 它比 [夏尔普(Sharp)](/blog/puppeteersharp-e2e-testing)但好处是巨大的:

- 在所有主要浏览器中以相同的代码测试
- 自动等待的可靠性提高
- 使用跟踪查看器进行极佳的调试
- 盒子外的移动和平板仿冒
- 更能应对时间安排问题

**我的建议:**

- **需要交叉浏览器测试的新项目**: 以剧作家开始
- **仅铬项目**: Putputeeer Sharp 比较简单
- **现有木偶的Sharp项目**:只有在需要多浏览器时才迁移:
- **复杂的现代网络应用程序**: 玩家的工具值高空 。

光是微量浏览器就让我节省了调试时间。 能够重放一个失败的测试并查看浏览器在每一步所看到的确切内容是非常宝贵的。

让Playwright去你的下一个项目吧 你会很惊讶它能让交叉浏览器测试容易得多

## 继续阅读

- [.NET 文档的 Playwright 播放器](https://playwright.dev/dotnet/)
- [Playwright API 引用工具](https://playwright.dev/dotnet/docs/api/class-playwright)
- [Playwright 和 Playwright 组合乐 组合乐 Sharp 和 Playwright 组合乐器](/blog/puppeteersharp-e2e-testing)
- [追踪查看指南](https://playwright.dev/dotnet/docs/trace-viewer)
- [浏览器上下文解释](https://playwright.dev/dotnet/docs/browser-contexts)