Skip to content

API Proposal: Microsoft.AspNetCore.Components.Testing E2E infrastructure #66394

@javiercn

Description

@javiercn

Background and Motivation

Blazor E2E testing currently requires significant boilerplate for process management, readiness detection, proxy routing, interactivity awareness, and tracing. Each test project reimplements this infrastructure. This package provides a Playwright + xUnit v3 + YARP-based E2E testing library that handles all of these concerns, enabling developers to write concise E2E tests for Blazor applications.

PR #65958. No linked issue. Adds Microsoft.AspNetCore.Components.Testing E2E test infrastructure.

Proposed API

namespace Microsoft.AspNetCore.Components.Testing.Infrastructure;

+ public class ServerFixture<TTestAssembly> : IAsyncLifetime, IAsyncDisposable
+ {
+     public string ProxyUrl { get; }
+     public ValueTask InitializeAsync();
+     public ValueTask DisposeAsync();
+     public Task<ServerInstance> StartServerAsync(string appName, Action<ServerStartOptions>? configure = null);
+     public Task<ServerInstance> StartServerAsync<TApp>(Action<ServerStartOptions>? configure = null);
+ }

+ public class ServerInstance : IAsyncDisposable
+ {
+     public string AppName { get; }
+     public string AppUrl { get; }
+     public string Id { get; }
+     public string? PublicUrl { get; }
+     public string TestUrl { get; }
+     public ValueTask DisposeAsync();
+ }

+ public class ServerStartOptions
+ {
+     public Dictionary<string, string> EnvironmentVariables { get; }
+     public int ReadinessTimeoutMs { get; set; }
+     public void ConfigureServices(Type type, string methodName);
+     public void ConfigureServices<T>(string methodName);
+ }

+ public static class PlaywrightExtensions
+ {
+     public static Task<TracedContext> NewTracedContextAsync(this BrowserTest test, ServerInstance server, BrowserNewContextOptions? options = null);
+     public static string SanitizeFileName(string name);
+     public static Task SetTestSession(this IBrowserContext context, ServerInstance server, string sessionId);
+     public static Task<TracingSession> TraceAsync(this IBrowserContext context, string artifactDir);
+     public static Task WaitForBlazorAsync(this IPage page);
+     public static Task WaitForEnhancedNavigationAsync(this IPage page);
+     public static Task WaitForEnhancedNavigationAsync(this IPage page, Func<Task> navigationAction);
+     public static Task WaitForInteractiveAsync(this IPage page, string selector);
+     public static BrowserNewContextOptions WithArtifacts(this BrowserNewContextOptions options, string? artifactDir = null);
+     public static BrowserNewContextOptions WithServerRouting(this BrowserNewContextOptions options, ServerInstance server);
+ }

+ public class TracedContext : IAsyncDisposable
+ {
+     public IBrowserContext Context { get; }
+     public void Deconstruct(out IBrowserContext context);
+     public ValueTask DisposeAsync();
+     public Task<IPage> NewPageAsync();
+ }

+ public class TracingSession : IAsyncDisposable
+ {
+     public ValueTask DisposeAsync();
+     public static Task<TracingSession> StartAsync(IBrowserContext context, string artifactDir, bool recordVideo);
+ }

+ public class RemoteLock : IAsyncDisposable
+ {
+     public ValueTask DisposeAsync();
+     public Task ReleaseAsync();
+ }

+ public class ResourceLock : IAsyncDisposable
+ {
+     public ValueTask DisposeAsync();
+     public Task ReleaseAsync();
+     public Task WaitForRequestAsync();
+     public static Task<ResourceLock> CreateAsync(IPage page, Regex urlPattern);
+ }

+ public class TestLockClient
+ {
+     public RemoteLock Lock(string name);
+     public static Task<TestLockClient> CreateAsync(ServerInstance server, IBrowserContext context);
+ }

+ public class TestLockProvider
+ {
+     public bool Release(string key);
+     public Task WaitOn(string key);
+ }

+ public class TestSessionContext
+ {
+     public string? Id { get; set; }
+ }

+ public class TestReadinessHostingStartup : IHostingStartup
+ {
+     public void Configure(IWebHostBuilder builder);
+ }

+ public interface IE2EServiceOverrideResolver
+ {
+     Action<IServiceCollection>? TryResolve(string assemblyQualifiedTypeName, string methodName);
+ }

Usage Examples

[Fact]
public async Task Counter_IncrementsOnClick()
{
    await using var ctx = await this.NewTracedContextAsync(_server);
    var page = await ctx.NewPageAsync();

    await page.GotoAsync($"{_server.TestUrl}/counter");
    await page.WaitForInteractiveAsync("button.btn-primary");

    await page.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();

    await Expect(page.Locator("p[role='status']")).ToHaveTextAsync("Current count: 1");
}

Alternative Designs

Each test project building its own infrastructure. The unified library reduces duplication and provides consistent patterns.

Risks

Large public API surface. The library depends on Playwright and xUnit v3, which constrains the testing framework choice. This is primarily intended for testing Blazor applications within the ASP.NET Core repo and by library authors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-proposalapi-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsarea-blazorIncludes: Blazor, Razor Components

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions