Blazor Interactive Server Render Mode Headless Testing Library
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 276
Description
# Blazor Server Load Testing Library
## Summary
Productize the internal Ignitor tool as an official, supported library for load testing Blazor Server applications. This enables customers to efficiently stress test their Blazor Server apps without requiring thousands of browser instances.
## Motivation
Blazor Server applications require stress testing to validate they can handle production loads. However, existing tools like Playwright/Selenium are expensive for simulating thousands of concurrent connections—each browser instance consumes significant memory and CPU.
A headless client that speaks the Blazor Server SignalR protocol can:
- Simulate thousands of concurrent circuits with minimal resources
- Validate connection scaling and circuit lifecycle behavior
- Measure server-side rendering performance under load
- Complement (not replace) browser-based E2E testing for UX validation
## In Scope
- Programmatic Blazor Server circuit client (SignalR-based)
- Render batch interpretation without a browser
- Element tree navigation and interaction (click, select, dispatch events)
- Connection lifecycle management
- Metrics/operation capture for analysis
- Single-threaded execution model via SynchronizationContext (matching JavaScript client behavior)
## Out of Scope
- Blazor WebAssembly support
- JavaScript execution/interop emulation
- Visual rendering
- Full E2E testing (use Playwright for that)
## Risks / Concerns
- **Limited fidelity**: Cannot execute JavaScript, so JS interop-heavy apps may have gaps
## Design
### Naming
**Namespace**: `Microsoft.AspNetCore.Components.Server.HeadlessTesting`
**Main type**: `BlazorInteractiveServerModeClient`
### API Design Principles
- **Minimal public API surface**: Single public type exposes all functionality
- **Extensibility via hooks**: Allow users to intercept server-initiated JS interop calls
### Public vs Internal Types
| Type | Visibility | Rationale |
|------|------------|-----------|
| `BlazorInteractiveServerModeClient` | **public** | Single entry point—all interactions go through this |
| `ElementNode` | **public** | Return type of `FindElementById` |
| All other types | **internal** | Implementation details, stub types, or functionality exposed via client methods |
The client exposes:
- **Lifecycle**: `ConnectAsync`, `DisposeAsync`
- **Element interaction**: `FindElementById`, `ClickAsync`, `SelectAsync`, `DispatchEventAsync`
- **Synchronization**: `WaitForRenderBatchAsync`
- **JS Interop**: `OnJSInterop` callback for server-initiated JS calls
- **.NET Interop**: `InvokeDotNetAsync` for client-initiated calls to server
### Server-Initiated JS Interop
When the server calls `JSRuntime.InvokeAsync(...)`, the headless client receives it via SignalR. Users can provide a callback to respond:
```csharp
client.OnJSInterop = (string identifier, string argsJson) =>
{
return identifier switch
{
"localStorage.getItem" => "\"mock-value\"",
"Blazor._internal.navigationManager.getBaseURI" => "\"/\"",
_ => null // Default handling
};
};
```
### Client-Initiated .NET Interop
The client can invoke .NET methods on the server, simulating how the JS client calls into .NET:
```csharp
await client.InvokeDotNetAsync("MyAssembly", "MyClass.MyMethod", args);
```
### Execution Model
Internally, the client will use a custom `SynchronizationContext` to match the JavaScript client's single-threaded execution model. This prevents race conditions that wouldn't occur in real Blazor Server usage.
### Basic Usage
```csharp
await using var client = new BlazorInteractiveServerModeClient();
await client.ConnectAsync(new Uri("https://localhost:5001"));
// Find and interact with elements
await client.ClickAsync("submit-btn");
// Wait for render to complete
await client.WaitForRenderBatchAsync();
```
## Packaging
- Ships as a separate NuGet package: `Microsoft.AspNetCore.Components.Server.HeadlessTesting`
- No backward compatibility guarantees—use the library version matching your target framework
## Initial Tasks
- [ ] Refactor API surface: rename namespace to `Microsoft.AspNetCore.Components.Server.HeadlessTesting`, rename types (`BlazorClient` → `BlazorInteractiveServerModeClient`, `ElementHive` → `CircuitState`), make types internal except `BlazorInteractiveServerModeClient` and `ElementNode`, move methods to client
- [ ] Add `SynchronizationContext` for single-threaded execution model
- [ ] Add `OnJSInterop` callback for server-initiated JS interop
- [ ] Add `InvokeDotNetAsync` for client-initiated .NET interop
- [ ] Identify important missing features
- [ ] API review
- [ ] Create NuGet package configuration
Contributor guide
Assessment
This issue has not been assessed yet.