diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PlaywrightFixture.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PlaywrightFixture.cs
index c3cec195..8b8fff3c 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PlaywrightFixture.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PlaywrightFixture.cs
@@ -27,6 +27,23 @@ public class PlaywrightFixture : IAsyncLifetime
public IPlaywright Playwright { get; private set; } = null!;
public IBrowser Browser { get; private set; } = null!;
+ ///
+ /// Live browser contexts created by , oldest first.
+ /// Capped to so finished tests' contexts are
+ /// closed eagerly rather than leaking for the whole run (see ).
+ ///
+ private readonly List _contexts = new();
+
+ ///
+ /// 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 Playwright 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.
+ ///
+ private const int MaxRetainedContexts = 4;
+
public async Task InitializeAsync()
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
@@ -40,12 +57,36 @@ public class PlaywrightFixture : IAsyncLifetime
}
///
- /// 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
+ /// are open, to bound the number of concurrent Blazor
+ /// Server circuits the run holds on the Central UI.
///
public async Task NewPageAsync()
{
var context = await Browser.NewContextAsync();
- return await context.NewPageAsync();
+ var page = await context.NewPageAsync();
+
+ List 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;
}
///
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs
index 29353784..1b3eb7cb 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs
@@ -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']"))