using Bunit; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared; /// Tests for (Flow Debugger vertical trace list, Gitea #53). public class FlowTraceListTests : BunitContext { private static readonly DateTimeOffset T0 = new(2026, 11, 9, 12, 0, 0, TimeSpan.Zero); private static FlowStep Step(FlowStepKind kind, FlowLane lane, string title, int offsetMs, FlowStepOutcome outcome = FlowStepOutcome.Ok, int? durationMs = 5, IReadOnlyList? logs = null) => new(Guid.NewGuid(), null, null, kind, lane, title, null, T0.AddMilliseconds(offsetMs), durationMs, outcome, null, null, null, null, null, null, null, null, Logs: logs); private static FlowGraph Graph(params FlowStep[] steps) => new(steps, Array.Empty(), Predicted: false, Truncated: false); private static FlowGraph Sample() => Graph( Step(FlowStepKind.InboundRequest, FlowLane.Central, "POST /api/M", 0), Step(FlowStepKind.ScriptRun, FlowLane.Site, "RunIt", 20, FlowStepOutcome.Failed, 80), Step(FlowStepKind.AttributeWrite, FlowLane.Tags, "Inst.Flag", 30)); /// Every step renders as a row in display order with its lane and offset. [Fact] public void Render_ListsStepsInOrderWithLaneColumn() { var cut = Render(p => p.Add(c => c.Graph, Sample())); var rows = cut.FindAll("[data-test='flow-list-row']"); Assert.Equal(3, rows.Count); Assert.Equal(new[] { "Central", "Site", "Tags" }, rows.Select(r => r.GetAttribute("data-lane"))); Assert.Contains("+20 ms", rows[1].TextContent); Assert.Contains("RunIt", rows[1].TextContent); Assert.Contains("Failed", rows[1].TextContent); Assert.Contains("failed", rows[1].ClassList); } /// The lane filter narrows the rows and the count line. [Fact] public void LaneFilter_NarrowsRows() { var cut = Render(p => p.Add(c => c.Graph, Sample())); cut.Find("[data-test='flow-list-lane-filter']").Change("Site"); var row = Assert.Single(cut.FindAll("[data-test='flow-list-row']")); Assert.Equal("Site", row.GetAttribute("data-lane")); Assert.Contains("1 of 3", cut.Find("[data-test='flow-list-count']").TextContent); } /// The failures-only toggle keeps failed and timed-out steps. [Fact] public void FailuresOnly_KeepsFailedSteps() { var cut = Render(p => p.Add(c => c.Graph, Sample())); cut.Find("[data-test='flow-list-failed-only']").Change(true); var row = Assert.Single(cut.FindAll("[data-test='flow-list-row']")); Assert.Equal("ScriptRun", row.GetAttribute("data-kind")); } /// Clicking or pressing Enter on a row raises OnSelect; the selected row is marked. [Fact] public void ClickAndEnter_RaiseSelect_AndSelectedRowIsMarked() { var graph = Sample(); var picked = new List(); var cut = Render(p => p .Add(c => c.Graph, graph) .Add(c => c.SelectedEventId, graph.Steps[2].EventId) .Add(c => c.OnSelect, s => picked.Add(s))); cut.FindAll("[data-test='flow-list-row']")[1].Click(); cut.FindAll("[data-test='flow-list-row']")[0].KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Key = "Enter" }); Assert.Equal(new[] { FlowStepKind.ScriptRun, FlowStepKind.InboundRequest }, picked.Select(s => s.Kind)); var rows = cut.FindAll("[data-test='flow-list-row']"); Assert.Equal("true", rows[2].GetAttribute("aria-selected")); Assert.Contains("selected", rows[2].ClassList); } /// No graph shows the empty note; log lines show a count. [Fact] public void EmptyGraph_ShowsNoSteps_AndLogsAreCounted() { Assert.NotNull(Render().Find("[data-test='flow-list-empty']")); var logs = new[] { new FlowLogLine(Guid.NewGuid(), null, T0, "error", "script", "boom", null, null, null, 1) }; var cut = Render(p => p.Add(c => c.Graph, Graph(Step(FlowStepKind.ScriptRun, FlowLane.Site, "S", 0, logs: logs)))); Assert.Equal("1", cut.Find("[data-test='flow-list-logs']").TextContent.Trim()); } }