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']"))