| 1 | using CascadeIDE.Cockpit.Graph; |
| 2 | using CascadeIDE.Cockpit.Graph.Layout; |
| 3 | using CascadeIDE.Features.WorkspaceNavigation.Application; |
| 4 | using CascadeIDE.Models; |
| 5 | using Xunit; |
| 6 | |
| 7 | namespace CascadeIDE.Tests; |
| 8 | |
| 9 | public sealed class GraphRelatedFileHierarchyLayoutEngineTests |
| 10 | { |
| 11 | /// <summary> |
| 12 | /// Регрессия 2026-05: <c>Math.Clamp(step, 38, innerH/(n+1))</c> при <c>38 > innerH/(n+1)</c> |
| 13 | /// (типично ~32 при умеренной высоте) кидал ArgumentException → «Не удалось разобрать ответ навигации». |
| 14 | /// </summary> |
| 15 | [Theory] |
| 16 | [InlineData(5, 200)] |
| 17 | [InlineData(37, 520)] |
| 18 | public void Layout_top_down_regression_inverted_math_clamp_bounds_does_not_throw(int satelliteCount, double height) |
| 19 | { |
| 20 | var doc = BuildStarDoc(satelliteCount); |
| 21 | var engine = new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: true); |
| 22 | |
| 23 | var ex = Record.Exception(() => |
| 24 | engine.Layout(doc, width: 280, height, CodeNavigationMapDetailLevel.Normal)); |
| 25 | |
| 26 | Assert.Null(ex); |
| 27 | } |
| 28 | |
| 29 | [Fact] |
| 30 | public void Layout_many_satellites_top_down_places_all_nodes() |
| 31 | { |
| 32 | var doc = BuildStarDoc(satelliteCount: 37); |
| 33 | var engine = new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: true); |
| 34 | |
| 35 | var scene = engine.Layout(doc, width: 280, height: 520, CodeNavigationMapDetailLevel.Normal); |
| 36 | |
| 37 | Assert.Equal(38, scene.Nodes.Count); |
| 38 | Assert.All(scene.Nodes, n => Assert.True(n.Radius > 0)); |
| 39 | Assert.All(scene.Nodes, n => Assert.Equal(GraphNodeShape.Rectangle, n.Shape)); |
| 40 | } |
| 41 | |
| 42 | [Fact] |
| 43 | public void Compositor_file_top_down_many_related_nodes_does_not_throw() |
| 44 | { |
| 45 | var nodes = new List<GraphNode> |
| 46 | { |
| 47 | new() { Id = "n0", Path = @"D:\w\A.cs", Kind = "anchor", Label = "A.cs" } |
| 48 | }; |
| 49 | for (var i = 1; i <= 37; i++) |
| 50 | { |
| 51 | nodes.Add(new GraphNode |
| 52 | { |
| 53 | Id = $"n{i}", |
| 54 | Path = $@"D:\w\B{i}.cs", |
| 55 | Kind = "project_peer", |
| 56 | Label = $"B{i}.cs" |
| 57 | }); |
| 58 | } |
| 59 | |
| 60 | var doc = new GraphDocument |
| 61 | { |
| 62 | AnchorPath = @"D:\w\A.cs", |
| 63 | Kind = GraphKind.RelatedFiles, |
| 64 | Nodes = nodes, |
| 65 | Edges = nodes.Skip(1).Select(n => new GraphEdge { FromId = "n0", ToId = n.Id, Kind = "related_to" }).ToList() |
| 66 | }; |
| 67 | |
| 68 | var compositor = new CodeNavigationMapCompositor(); |
| 69 | var result = compositor.Compose( |
| 70 | new CodeNavigationMapCompositionIntent( |
| 71 | doc, |
| 72 | CodeNavigationMapLevelKind.File, |
| 73 | CodeNavigationMapDetailLevel.Normal, |
| 74 | CodeNavigationMapRelatedGraphLayoutKind.TopDown, |
| 75 | CodeNavigationMapControlFlowMainAxisKind.Auto), |
| 76 | new Services.SkiaInstruments.SkiaInstrumentViewport(280, 120)); |
| 77 | |
| 78 | Assert.True(result.PreferredHeight >= 120); |
| 79 | Assert.Equal(38, result.ToSceneVm(280, result.PreferredHeight).Nodes.Count); |
| 80 | } |
| 81 | |
| 82 | private static GraphDocument BuildStarDoc(int satelliteCount) |
| 83 | { |
| 84 | var nodes = new List<GraphNode> |
| 85 | { |
| 86 | new() { Id = "n0", Path = @"D:\w\A.cs", Kind = "anchor", Label = "A.cs" } |
| 87 | }; |
| 88 | for (var i = 1; i <= satelliteCount; i++) |
| 89 | { |
| 90 | nodes.Add(new GraphNode |
| 91 | { |
| 92 | Id = $"n{i}", |
| 93 | Path = $@"D:\w\B{i}.cs", |
| 94 | Kind = "project_peer", |
| 95 | Label = $"B{i}.cs" |
| 96 | }); |
| 97 | } |
| 98 | |
| 99 | return new GraphDocument |
| 100 | { |
| 101 | AnchorPath = @"D:\w\A.cs", |
| 102 | Nodes = nodes, |
| 103 | Edges = nodes.Skip(1).Select(n => new GraphEdge { FromId = "n0", ToId = n.Id }).ToList() |
| 104 | }; |
| 105 | } |
| 106 | } |
| 107 | |