1f8c5810ca
An ephemeral inbound method (created and deleted through the CLI, with a never-taken Route.To branch so prediction resolves a route target) is run live in a real browser through the fixture's /auth/login; the reload test drives the OnAfterRenderAsync localStorage restore and then proves the page is still interactive. Both fail on the pre-fix image at the 'Predicted' wait (the circuit deadlock) and pass on the fixed one. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
118 lines
5.4 KiB
C#
118 lines
5.4 KiB
C#
using System.Globalization;
|
|
using Microsoft.Playwright;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
|
|
|
/// <summary>
|
|
/// 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 <c>localStorage</c> restore path. Both go through the same
|
|
/// prediction path that used to deadlock the circuit (see <see cref="FlowDebuggerFixture"/>) —
|
|
/// under that bug the state chip never reached <c>Predicted</c> and every later click was lost,
|
|
/// so these tests are the browser-level guard for that fix as well.
|
|
/// </summary>
|
|
[Collection("Playwright")]
|
|
public class FlowDebuggerTests : IClassFixture<FlowDebuggerFixture>
|
|
{
|
|
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<string> CaptureConsoleErrors(IPage page)
|
|
{
|
|
var errors = new List<string>();
|
|
page.Console += (_, msg) =>
|
|
{
|
|
if (string.Equals(msg.Type, "error", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
lock (errors) errors.Add(msg.Text);
|
|
}
|
|
};
|
|
return errors;
|
|
}
|
|
}
|