Write Playwright-based acceptance tests for migrated Blazor applications. Covers PlaywrightFixture setup, test organization with xUnit collection fixtures, page navigation, element interaction, assertion patterns, environment configuration, CI/CD browser setup, and screenshot capture. Use when adding acceptance tests for benchmark apps, verifying migration quality, or debugging Playwright test failures.
72
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This skill covers writing end-to-end acceptance tests using Playwright (.NET) to verify migrated Blazor applications work correctly.
| Suite | Location | Target App | Env Var |
|---|---|---|---|
| WingtipToys | src/WingtipToys.AcceptanceTests/ | samples/AfterWingtipToys/ | WINGTIPTOYS_BASE_URL |
| ContosoUniversity | src/ContosoUniversity.AcceptanceTests/ | samples/AfterContosoUniversity/ | CONTOSO_BASE_URL |
| Sample App | samples/AfterBlazorServerSide.Tests/ | samples/AfterBlazorServerSide/ | N/A (built inline) |
Every test suite uses the same pattern:
PlaywrightFixture.cs — Shared browser instance:
public class PlaywrightFixture : IAsyncLifetime
{
public IPlaywright Playwright { get; private set; } = null!;
public IBrowser Browser { get; private set; } = null!;
public async Task InitializeAsync()
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true
});
}
public async Task DisposeAsync()
{
await Browser.DisposeAsync();
Playwright.Dispose();
}
public async Task<IPage> NewPageAsync()
{
var context = await Browser.NewContextAsync(new BrowserNewContextOptions
{
IgnoreHTTPSErrors = true
});
return await context.NewPageAsync();
}
}
[CollectionDefinition("Playwright")]
public class PlaywrightCollection : ICollectionFixture<PlaywrightFixture> { }TestConfiguration.cs — Environment-driven base URL:
public static class TestConfiguration
{
public static string BaseUrl =>
Environment.GetEnvironmentVariable("WINGTIPTOYS_BASE_URL")
?? "https://localhost:5001";
}using Microsoft.Playwright;
namespace WingtipToys.AcceptanceTests;
[Collection("Playwright")]
public class MyFeatureTests
{
private readonly PlaywrightFixture _fixture;
public MyFeatureTests(PlaywrightFixture fixture) => _fixture = fixture;
[Fact]
public async Task PageName_Loads_Successfully()
{
var page = await _fixture.NewPageAsync();
var response = await page.GotoAsync($"{TestConfiguration.BaseUrl}/MyPage");
Assert.NotNull(response);
Assert.True(response.Ok, $"Page returned {response.Status}");
}
}Navigation and page load:
var page = await _fixture.NewPageAsync();
await page.GotoAsync($"{TestConfiguration.BaseUrl}/ProductList");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);Click a link by href:
var link = page.Locator("a[href='/ProductList']").First;
await link.ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);Click a link by text:
var link = page.GetByRole(AriaRole.Link, new() { Name = "Products" });
await link.ClickAsync();Fill a form field:
var input = page.Locator("input[type='text']").First;
await input.ClearAsync();
await input.FillAsync("new value");Submit a form:
var submitButton = page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
await submitButton.ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);Assert page content:
// Check URL
Assert.Contains("ProductList", page.Url, StringComparison.OrdinalIgnoreCase);
// Check title
var title = await page.TitleAsync();
Assert.False(string.IsNullOrEmpty(title));
// Check element exists
var count = await page.Locator("table tbody tr").CountAsync();
Assert.True(count > 0, "Table should have data rows");
// Check text content
var content = await page.ContentAsync();
Assert.Contains("expected text", content, StringComparison.OrdinalIgnoreCase);Screenshot capture (for benchmark reports):
await page.ScreenshotAsync(new PageScreenshotOptions
{
Path = "screenshots/my-test.png",
FullPage = true
});Extract reusable navigation sequences:
/// <summary>
/// Helper: navigates to product list and adds the first product to cart.
/// </summary>
private static async Task AddFirstProductToCart(IPage page)
{
await page.GotoAsync($"{TestConfiguration.BaseUrl}/ProductList");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var firstProduct = page.Locator("a[href*='ProductDetails']").First;
await firstProduct.ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var addToCartLink = page.Locator("a[href*='AddToCart']").First;
if (await addToCartLink.CountAsync() > 0)
await addToCartLink.ClickAsync();
else
await page.GetByRole(AriaRole.Button, new() { Name = "Add To Cart" }).ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
}Use [Theory] with [InlineData] for testing multiple similar pages:
[Theory]
[InlineData("About")]
[InlineData("Contact")]
[InlineData("ProductList")]
public async Task NavbarLink_LoadsPage(string pageName)
{
var page = await _fixture.NewPageAsync();
await page.GotoAsync(TestConfiguration.BaseUrl);
var link = page.Locator($"a[href='/{pageName}']").First;
await link.ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
Assert.DoesNotContain("Error", await page.TitleAsync(), StringComparison.OrdinalIgnoreCase);
Assert.True(page.Url.Contains(pageName, StringComparison.OrdinalIgnoreCase));
}| Category | What to Test | Example |
|---|---|---|
| Navigation | All navbar links load without errors | HomePage_Loads, NavbarLink_LoadsPage |
| Static Assets | CSS/JS/images served correctly | StaticAssets_CssLoads, StaticAssets_ImagesLoad |
| Data Display | Data-bound pages show content | ProductList_DisplaysProducts |
| User Flows | Multi-step workflows complete | AddItemToCart_AppearsInCart |
| Authentication | Login/register pages load, forms work | LoginLink_LoadsPage, RegisterLink_LoadsPage |
| Error Handling | 404/500 pages render gracefully | NonexistentPage_Returns404 |
# Build the test project
dotnet build src/WingtipToys.AcceptanceTests
# Install Playwright browsers (first time only)
pwsh src/WingtipToys.AcceptanceTests/bin/Debug/net10.0/playwright.ps1 install chromium
# Start the target app (in another terminal)
dotnet run --project samples/AfterWingtipToys
# Run tests (set base URL if not default)
$env:WINGTIPTOYS_BASE_URL = "https://localhost:5001"
dotnet test src/WingtipToys.AcceptanceTests| Problem | Solution |
|---|---|
Browser not found | Run playwright.ps1 install chromium |
Connection refused | Ensure target app is running on the expected port |
Timeout waiting for selector | Add await page.WaitForLoadStateAsync(LoadState.NetworkIdle) |
SSL error | IgnoreHTTPSErrors = true is set in PlaywrightFixture.NewPageAsync() |
Flaky test | Add explicit waits, avoid timing-dependent assertions |
Element not found | Use page.Locator(...).First and check CountAsync() > 0 before interacting |
[Collection("Playwright")]PlaywrightFixture fixtureasync TaskLoadState.NetworkIdle after navigationTestConfiguration.BaseUrl (not hardcoded)dotnet test passes with target app running3154758
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.