using System.Globalization; using Microsoft.Playwright; using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; using Xunit; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; /// /// Real-browser coverage for the Flow Debugger page (follow-ups plan 2026-09-06, Task 9): /// a Live run of an inbound method completes and draws a graph, and the method choice survives /// a reload through the page's localStorage restore path. Both go through the same /// prediction path that used to deadlock the circuit (see ) — /// under that bug the state chip never reached Predicted and every later click was lost, /// so these tests are the browser-level guard for that fix as well. /// [Collection("Playwright")] public class FlowDebuggerTests : IClassFixture { private const string FlowDebuggerUrl = "/deployment/flow-debugger"; private readonly PlaywrightFixture _pw; private readonly FlowDebuggerFixture _flow; public FlowDebuggerTests(PlaywrightFixture pw, FlowDebuggerFixture flow) { _pw = pw; _flow = flow; } [SkippableFact] public async Task LiveRun_InBrowser_CompletesAndDrawsTheGraph() { Skip.IfNot(_flow.Available, ClusterAvailability.SkipReason); var page = await _pw.NewAuthenticatedPageAsync(); var consoleErrors = CaptureConsoleErrors(page); await page.GotoAsync(PlaywrightFixture.BaseUrl + FlowDebuggerUrl); await SelectMethodAsync(page); // The prediction (a Roslyn walk + the route-target resolve) finished: the state chip flips // to Predicted and the typed input form for the method's parameter appears. await Assertions.Expect(page.Locator("[data-test='flow-state']")).ToHaveTextAsync("Predicted", new() { Timeout = 15_000 }); var input = page.Locator("input[data-test='flow-input-Text']"); await Assertions.Expect(input).ToBeVisibleAsync(); // @bind commits on change (blur), so commit the value as its own circuit message before Run. await input.FillAsync("hello"); await input.BlurAsync(); await page.WaitForTimeoutAsync(300); await page.ClickAsync("button[data-test='flow-run']"); await Assertions.Expect(page.Locator("[data-test='flow-state']")).ToHaveTextAsync("Completed", new() { Timeout = 60_000 }); var stepCountText = await page.Locator("[data-test='flow-step-count']").InnerTextAsync(); var stepCount = int.Parse(stepCountText.Trim(), CultureInfo.InvariantCulture); Assert.True(stepCount >= 2, $"expected at least the request + response steps, got {stepCount}"); // Selecting a node fills the detail panel. await page.Locator("g[data-test='flow-node']").Last.ClickAsync(); await Assertions.Expect(page.Locator("[data-test='flow-step-title']")).ToBeVisibleAsync(); Assert.DoesNotContain(consoleErrors, e => e.Contains("JSInterop", StringComparison.OrdinalIgnoreCase)); } [SkippableFact] public async Task MethodChoice_SurvivesReload_ViaLocalStorage() { Skip.IfNot(_flow.Available, ClusterAvailability.SkipReason); var page = await _pw.NewAuthenticatedPageAsync(); var consoleErrors = CaptureConsoleErrors(page); await page.GotoAsync(PlaywrightFixture.BaseUrl + FlowDebuggerUrl); await SelectMethodAsync(page); await Assertions.Expect(page.Locator("[data-test='flow-state']")).ToHaveTextAsync("Predicted", new() { Timeout = 15_000 }); // The page persisted the choice; a fresh circuit restores it in OnAfterRenderAsync // through localStorage and re-runs the prediction. await page.ReloadAsync(); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); var select = page.Locator("select[data-test='flow-method-select']"); await Assertions.Expect(select).ToHaveValueAsync(MethodIdText, new() { Timeout = 15_000 }); await Assertions.Expect(page.Locator("[data-test='flow-state']")).ToHaveTextAsync("Predicted", new() { Timeout = 15_000 }); // And the page is still interactive afterwards (the deadlock left it inert right here). await page.ClickAsync("button[data-test='flow-mode-replay']"); await Assertions.Expect(page.Locator("input[data-test='flow-execution-id']")).ToBeVisibleAsync(); Assert.DoesNotContain(consoleErrors, e => e.Contains("JSInterop", StringComparison.OrdinalIgnoreCase)); } private string MethodIdText => _flow.MethodId.ToString(CultureInfo.InvariantCulture); private async Task SelectMethodAsync(IPage page) { var select = page.Locator("select[data-test='flow-method-select']"); await Assertions.Expect(select).ToBeVisibleAsync(); // The option list is loaded server-side on init; wait for our method to be present. await Assertions.Expect(select.Locator($"option[value='{MethodIdText}']")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); await select.SelectOptionAsync(MethodIdText); } private static List CaptureConsoleErrors(IPage page) { var errors = new List(); page.Console += (_, msg) => { if (string.Equals(msg.Type, "error", StringComparison.OrdinalIgnoreCase)) { lock (errors) errors.Add(msg.Text); } }; return errors; } }