test(e2e): cap live browser contexts to bound Blazor circuit pressure (fixes full-suite timeouts); import negative-test review fixes

This commit is contained in:
Joseph Doherty
2026-06-06 12:33:06 -04:00
parent b52f7281aa
commit 09f14f18ea
2 changed files with 47 additions and 4 deletions
@@ -27,6 +27,23 @@ public class PlaywrightFixture : IAsyncLifetime
public IPlaywright Playwright { get; private set; } = null!;
public IBrowser Browser { get; private set; } = null!;
/// <summary>
/// Live browser contexts created by <see cref="NewPageAsync"/>, oldest first.
/// Capped to <see cref="MaxRetainedContexts"/> so finished tests' contexts are
/// closed eagerly rather than leaking for the whole run (see <see cref="NewPageAsync"/>).
/// </summary>
private readonly List<IBrowserContext> _contexts = new();
/// <summary>
/// Maximum number of browser contexts kept open at once. Each context holds a live
/// Blazor Server SignalR circuit on the Central UI; the full suite runs serially within
/// the <c>Playwright</c> collection, so contexts from already-finished tests can be closed
/// safely. Leaving every context open accumulated ~one circuit per test and slowed late
/// tests into navigation/visibility timeouts. A small cap (covering any single test that
/// opens more than one page) keeps server-side circuit pressure flat across the run.
/// </summary>
private const int MaxRetainedContexts = 4;
public async Task InitializeAsync()
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
@@ -40,12 +57,36 @@ public class PlaywrightFixture : IAsyncLifetime
}
/// <summary>
/// Create a new browser context and page. Each test gets an isolated session.
/// Create a new browser context and page. Each test gets an isolated session. Contexts
/// from already-finished tests are closed eagerly once more than
/// <see cref="MaxRetainedContexts"/> are open, to bound the number of concurrent Blazor
/// Server circuits the run holds on the Central UI.
/// </summary>
public async Task<IPage> NewPageAsync()
{
var context = await Browser.NewContextAsync();
return await context.NewPageAsync();
var page = await context.NewPageAsync();
List<IBrowserContext> toClose = new();
lock (_contexts)
{
_contexts.Add(context);
int excess = _contexts.Count - MaxRetainedContexts;
if (excess > 0)
{
toClose = _contexts.GetRange(0, excess);
_contexts.RemoveRange(0, excess);
}
}
foreach (var old in toClose)
{
// Best-effort: a finished test's context may already be gone; never fail a test
// (or the next page creation) on teardown of a stale context.
try { await old.CloseAsync(); } catch { /* best-effort */ }
}
return page;
}
/// <summary>
@@ -190,7 +190,7 @@ public class TransportImportTests
await CliRunner.AddAttributeAsync(tmplId, "Value", "Double");
await CliRunner.BundleExportAsync(bundlePath, tmplId, "correct-passphrase-1", "src-env");
var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
var page = await _fixture.NewAuthenticatedPageAsync();
// ── STEP 1: Upload ────────────────────────────────────────────────────────
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import");
@@ -213,7 +213,9 @@ public class TransportImportTests
await Assertions.Expect(page.Locator("[data-testid='error-message']"))
.ToContainTextAsync("Wrong passphrase. Please try again.", new() { Timeout = 10_000 });
await Assertions.Expect(page.Locator("#import-passphrase")).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("[data-testid='diff-summary']")).ToBeHiddenAsync();
// Assert the diff step is genuinely absent (the @switch never rendered it), not merely
// hidden — ToBeHiddenAsync is vacuously true for an element that doesn't exist.
await Assertions.Expect(page.Locator("[data-testid='diff-summary']")).ToHaveCountAsync(0);
// Secondary indicator: one failed attempt recorded (1 of MaxUnlockAttempts).
await Assertions.Expect(page.Locator("[data-testid='unlock-attempts']"))