I have the following code to start up a kestrel server on a random port.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Abc;
public sealed class KestrelWebAppFactory : WebApplicationFactory<Program>
{
protected override IHost CreateHost(IHostBuilder builder)
{
// Build the TestServer host first (required by WebApplicationFactory)
var testHost = builder.Build();
// Reconfigure the builder to use a real Kestrel server on a dynamic port
builder.ConfigureWebHost(webHost =>
{
webHost.UseKestrel();
webHost.UseUrls("http://127.0.0.1:0");
});
// Build & start the Kestrel host
var kestrelHost = builder.Build();
kestrelHost.Start();
// Wait for Kestrel to replace :0 with the actual port
var server = kestrelHost.Services.GetRequiredService<IServer>();
var addressesFeature = server.Features.Get<IServerAddressesFeature>()!;
// Small spin-wait until Kestrel publishes a concrete port
var sw = System.Diagnostics.Stopwatch.StartNew();
string bound = null;
while (sw.Elapsed < TimeSpan.FromSeconds(5))
{
bound = addressesFeature.Addresses.FirstOrDefault(a => !a.EndsWith(":0", StringComparison.Ordinal));
if (bound is not null) break;
Thread.Sleep(25);
}
if (bound is null)
throw new InvalidOperationException("Kestrel did not publish a bound address.");
// Point the factory’s HttpClient at the real server so Playwright can use it
ClientOptions.BaseAddress = new Uri(bound);
// Start and return the TestServer host (WAF expects this)
testHost.Start();
return testHost;
}
}
Then I have the following fixture that I use on my test
namespace AbcE2ETests;
// Hosts the Kestrel server and Playwright browser for a test class.
public class ServerFixture : IAsyncLifetime
{
private const string PlaywrightContextStoragePath = "/temp/playwrightstate.json";
public KestrelWebAppFactory Factory { get; private set; }
public Uri BaseUrl => Factory.ClientOptions.BaseAddress;
public IPlaywright Playwright { get; private set; }
public IBrowser Browser { get; private set; }
public IBrowserContext Context { get; private set; }
public IPage Page { get; private set; }
public virtual async Task InitializeAsync()
{
Factory = new KestrelWebAppFactory();
await WaitForServerStartAsync();
// Initialize Playwright and a fresh browser context/page for this fixture
await SignInAsync();
}
public string GetFullUrl(string relativeUrl) => new Uri(BaseUrl, relativeUrl).ToString();
public async ValueTask EnterValuesByNameAsync(Dictionary<string, string> namesAndValues)
{
foreach (var kvp in namesAndValues)
{
ILocator input = Page.Locator($"input[name='{kvp.Key}']").First;
await input.WaitForAsync();
await input.FocusAsync();
Task setValueTask = kvp.Value switch
{
BrowserConstants.FormValues.Checkbox.Checked => input.CheckAsync(),
BrowserConstants.FormValues.Checkbox.Unchecked => input.UncheckAsync(),
_ => input.FillAsync(kvp.Value)
};
await setValueTask;
}
}
public async ValueTask PressEnterAsync() =>
await Page.Keyboard.PressAsync(BrowserConstants.Keys.Enter);
public async ValueTask NavigateToAsync(string relativeUrl) =>
await Page.GotoAsync(GetFullUrl(relativeUrl));
private async Task SignInAsync()
{
await InitializePlaywrightAsync(readFromStoragePath: false);
await NavigateToAsync("/Account/LogIn");
await EnterValuesByNameAsync(new Dictionary<string, string>
{
["Input.TenantCode"] = "ASL",
["Input.Email"] = "[email protected]",
["Input.Password"] = "SuperSecretPassword"
});
await PressEnterAsync();
await Page.WaitForURLAsync(GetFullUrl("/"));
await Context.StorageStateAsync(new()
{
// relative to test bin folder
Path = PlaywrightContextStoragePath
});
await DisposePlaywrightAsync();
await InitializePlaywrightAsync(readFromStoragePath: true);
await Task.Delay(300_000);
}
// Let descendants tweak context options (e.g., viewport, locale, storage state)
protected virtual void ConfigureContextOptions(BrowserNewContextOptions options) { }
protected async Task WaitForServerStartAsync()
{
HttpClient http = Factory.CreateClient();
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.Elapsed < TimeSpan.FromSeconds(5))
{
try
{
HttpResponseMessage response = await http.GetAsync(Factory.ClientOptions.BaseAddress);
if (response.IsSuccessStatusCode)
return;
}
catch { }
}
throw new InvalidOperationException("Server failed to start");
}
public virtual async Task DisposeAsync()
{
await DisposePlaywrightAsync();
Factory?.Dispose();
Factory = null;
}
private async ValueTask DisposePlaywrightAsync()
{
try
{
if (Context is not null)
await Context.CloseAsync();
if (Browser is not null)
await Browser.CloseAsync();
}
finally
{
Playwright?.Dispose();
Context = null;
Browser = null;
Playwright = null;
}
}
private async Task InitializePlaywrightAsync(bool readFromStoragePath)
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = false
});
var ctxOptions = new BrowserNewContextOptions
{
BaseURL = Factory.ClientOptions.BaseAddress.ToString(),
StorageStatePath = readFromStoragePath ? PlaywrightContextStoragePath : null
};
ConfigureContextOptions(ctxOptions);
Context = await Browser.NewContextAsync(ctxOptions);
Page = await Context.NewPageAsync();
}
}
This works great, except when the sign-in is completed and I am redirected to / I just get a blank screen.