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:
@@ -27,6 +27,23 @@ public class PlaywrightFixture : IAsyncLifetime
|
|||||||
public IPlaywright Playwright { get; private set; } = null!;
|
public IPlaywright Playwright { get; private set; } = null!;
|
||||||
public IBrowser Browser { 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()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
|
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
|
||||||
@@ -40,12 +57,36 @@ public class PlaywrightFixture : IAsyncLifetime
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public async Task<IPage> NewPageAsync()
|
public async Task<IPage> NewPageAsync()
|
||||||
{
|
{
|
||||||
var context = await Browser.NewContextAsync();
|
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>
|
/// <summary>
|
||||||
|
|||||||
+4
-2
@@ -190,7 +190,7 @@ public class TransportImportTests
|
|||||||
await CliRunner.AddAttributeAsync(tmplId, "Value", "Double");
|
await CliRunner.AddAttributeAsync(tmplId, "Value", "Double");
|
||||||
await CliRunner.BundleExportAsync(bundlePath, tmplId, "correct-passphrase-1", "src-env");
|
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 ────────────────────────────────────────────────────────
|
// ── STEP 1: Upload ────────────────────────────────────────────────────────
|
||||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import");
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import");
|
||||||
@@ -213,7 +213,9 @@ public class TransportImportTests
|
|||||||
await Assertions.Expect(page.Locator("[data-testid='error-message']"))
|
await Assertions.Expect(page.Locator("[data-testid='error-message']"))
|
||||||
.ToContainTextAsync("Wrong passphrase. Please try again.", new() { Timeout = 10_000 });
|
.ToContainTextAsync("Wrong passphrase. Please try again.", new() { Timeout = 10_000 });
|
||||||
await Assertions.Expect(page.Locator("#import-passphrase")).ToBeVisibleAsync();
|
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).
|
// Secondary indicator: one failed attempt recorded (1 of MaxUnlockAttempts).
|
||||||
await Assertions.Expect(page.Locator("[data-testid='unlock-attempts']"))
|
await Assertions.Expect(page.Locator("[data-testid='unlock-attempts']"))
|
||||||
|
|||||||
Reference in New Issue
Block a user