Skip to content

Commit bb42665

Browse files
authored
Add breakpoint UI with F9 keyboard shortcut, visual indicator, and E2E tests (#83)
1 parent 2d82fa0 commit bb42665

13 files changed

Lines changed: 489 additions & 29 deletions

File tree

src/NodeDev.Blazor/Components/ClassExplorer.razor

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,16 @@
202202

203203
private async Task ShowCreateMethodDialog()
204204
{
205-
var result = await DialogService.Show<CreateMethodDialog>("Create New Method", new()
205+
var dialogReference = await DialogService.ShowAsync<CreateMethodDialog>("Create New Method", new()
206206
{
207207
[nameof(CreateMethodDialog.Class)] = Class
208208
}, new DialogOptions()
209209
{
210210
MaxWidth = MaxWidth.Small,
211211
FullWidth = true
212-
}).Result;
212+
});
213+
214+
var result = await dialogReference.Result;
213215

214216
if (result != null && !result.Canceled && result.Data is NodeDev.Core.Class.NodeClassMethod method)
215217
{
@@ -229,7 +231,7 @@
229231
{
230232
if (item.Method == null) return;
231233

232-
var result = await DialogService.Show<RenameDialog>("Rename Method", new()
234+
var dialogReference = await DialogService.ShowAsync<RenameDialog>("Rename Method", new()
233235
{
234236
[nameof(RenameDialog.CurrentName)] = item.Method.Name,
235237
[nameof(RenameDialog.Label)] = "Method Name",
@@ -238,12 +240,15 @@
238240
{
239241
MaxWidth = MaxWidth.Small,
240242
FullWidth = true
241-
}).Result;
243+
});
244+
245+
var result = await dialogReference.Result;
242246

243247
if (result != null && !result.Canceled && result.Data is string newName)
244248
{
245249
var oldName = item.Method.Name;
246250
item.Method.Rename(newName);
251+
item.Name = newName; // Update the tree item name for display
247252
Snackbar.Add($"Method renamed from '{oldName}' to '{newName}'", Severity.Success);
248253
StateHasChanged();
249254
}

src/NodeDev.Blazor/Components/GraphCanvas.razor.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,16 @@ private void Diagram_KeyDown(global::Blazor.Diagrams.Core.Events.KeyboardEventAr
533533
node.Refresh();
534534
}
535535
}
536+
// Detect F9 key to toggle breakpoint on the selected node
537+
else if (obj.Key == "F9")
538+
{
539+
var node = Diagram.Nodes.Where(x => x.Selected).OfType<GraphNodeModel>().FirstOrDefault();
540+
if (node != null && !node.Node.CanBeInlined)
541+
{
542+
node.Node.ToggleBreakpoint();
543+
node.Refresh();
544+
}
545+
}
536546
}
537547

538548
#endregion
@@ -567,6 +577,20 @@ private void CancelPopup()
567577

568578
#endregion
569579

580+
#region ToggleBreakpoint
581+
582+
public void ToggleBreakpointOnSelectedNode()
583+
{
584+
var node = Diagram.Nodes.Where(x => x.Selected).OfType<GraphNodeModel>().FirstOrDefault();
585+
if (node != null && !node.Node.CanBeInlined)
586+
{
587+
node.Node.ToggleBreakpoint();
588+
node.Refresh();
589+
}
590+
}
591+
592+
#endregion
593+
570594
#region RemoveNode
571595

572596
public void RemoveNode(Node node)

src/NodeDev.Blazor/Components/ProjectExplorer.razor

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
Class
4040
}
4141

42-
private record class TreeItem(string Name, TreeItemType Type, NodeDev.Core.Class.NodeClass? Class)
42+
private record class TreeItem(TreeItemType Type, NodeDev.Core.Class.NodeClass? Class)
4343
{
44+
public string Name { get; set; } = "";
4445
public bool IsExpanded { get; set; } = true;
4546
}
4647

@@ -93,7 +94,7 @@
9394
{
9495
folder = new TreeItemData<TreeItem>()
9596
{
96-
Value = new(folders[i], TreeItemType.Folder, null),
97+
Value = new(TreeItemType.Folder, null) { Name = folders[i] },
9798
Children = [],
9899
Expanded = true
99100
};
@@ -106,7 +107,7 @@
106107

107108
folder.Children.Add(new()
108109
{
109-
Value = new(nodeClass.Name, TreeItemType.Class, nodeClass)
110+
Value = new(TreeItemType.Class, nodeClass) { Name = nodeClass.Name }
110111
});
111112
}
112113

@@ -133,7 +134,7 @@
133134
{
134135
if (item.Class == null) return;
135136

136-
var result = await DialogService.Show<RenameDialog>("Rename Class", new()
137+
var dialogReference = DialogService.Show<RenameDialog>("Rename Class", new()
137138
{
138139
[nameof(RenameDialog.CurrentName)] = item.Class.Name,
139140
[nameof(RenameDialog.Label)] = "Class Name",
@@ -142,12 +143,15 @@
142143
{
143144
MaxWidth = MaxWidth.Small,
144145
FullWidth = true
145-
}).Result;
146+
});
147+
148+
var result = await dialogReference.Result;
146149

147150
if (result != null && !result.Canceled && result.Data is string newName)
148151
{
149152
var oldName = item.Class.Name;
150-
item.Class.Name = newName; // Name is a settable property
153+
item.Class.Name = newName; // Update the class name
154+
item.Name = newName; // Update the tree item name for display
151155
Snackbar.Add($"Class renamed from '{oldName}' to '{newName}'", Severity.Success);
152156
StateHasChanged();
153157
}

src/NodeDev.Blazor/Components/ProjectToolbar.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@
1717
<MudButton OnClick="Add" Class="ml-3">Add node</MudButton>
1818
<MudButton OnClick="Run" Class="ml-3">Run</MudButton>
1919
<MudButton OnClick="SwitchLiveDebugging">@(Project.IsLiveDebuggingEnabled ? "Stop Live Debugging" : "Start Live Debugging")</MudButton>
20+
<MudButton OnClick="ToggleBreakpoint" Class="ml-3" data-test-id="toggle-breakpoint" Title="Toggle Breakpoint (F9)">
21+
<MudIcon Icon="@Icons.Material.Filled.FiberManualRecord" Color="Color.Error" Size="Size.Small" />
22+
</MudButton>
2023
<MudSpacer />
2124
<MudButton OnClick="OpenOptionsDialogAsync" Class="ml-3" data-test-id="options">Options</MudButton>
2225
<MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End" />
2326

2427

2528
@code {
2629

30+
[CascadingParameter]
31+
public NodeDev.Blazor.Index? IndexPage { get; set; }
32+
2733
private Project Project => ProjectService.Project;
2834

2935
private DialogOptions DialogOptions => new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
@@ -110,6 +116,11 @@
110116
Project.StartLiveDebugging();
111117
}
112118

119+
private void ToggleBreakpoint()
120+
{
121+
IndexPage?.ToggleBreakpointOnSelectedNode();
122+
}
123+
113124
private Task OpenOptionsDialogAsync()
114125
{
115126
return DialogService.ShowAsync<OptionsDialog>("Options", DialogOptions);

src/NodeDev.Blazor/DiagramsModels/GraphNodeWidget.razor

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<div class="drop-shadow-lg bg-white border my-node" @ondblclick="OnDoubleClick" data-test-id="graph-node" data-test-node-name="@Node.Node.Name">
44

55
<div class="bg-main pa-2 mb-1 font-semibold text-white title relative">
6+
@if (Node.Node.HasBreakpoint)
7+
{
8+
<div class="breakpoint-indicator" title="Breakpoint"></div>
9+
}
610
@if (Node.Node.AlternatesOverloads.Take(2).Count() == 2)
711
{
812
<MudIconButton Class="overload-icon" Icon="@Icons.Material.Filled.ChangeCircle" OnClick="() => GraphCanvas.OnOverloadSelectionRequested(Node)" />

src/NodeDev.Blazor/Index.razor

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
<MudDialogProvider />
1111
<MudSnackbarProvider />
1212

13-
<MudLayout Style="height: 100vh; width: 100%; overflow: hidden">
14-
<MudAppBar Elevation="1" data-test-id="appBar">
15-
<ProjectToolbar />
16-
</MudAppBar>
17-
<MudMainContent Style="width: 100%; height: 100%; overflow-y: hidden">
13+
<CascadingValue Value="this" IsFixed="true">
14+
<MudLayout Style="height: 100vh; width: 100%; overflow: hidden">
15+
<MudAppBar Elevation="1" data-test-id="appBar">
16+
<ProjectToolbar />
17+
</MudAppBar>
18+
<MudMainContent Style="width: 100%; height: 100%; overflow-y: hidden">
1819

1920
<MudExtensions.MudSplitter EnableSlide="true" Sensitivity="0.01" @bind-Dimension="ProjectExplorerGraphPercentage" Class="wh100 overflow-hidden relative">
2021
<StartContent>
@@ -77,6 +78,7 @@
7778
</MudExtensions.MudSplitter>
7879
</MudMainContent>
7980
</MudLayout>
81+
</CascadingValue>
8082

8183
@code {
8284

@@ -163,6 +165,16 @@
163165
StateHasChanged();
164166
}
165167

168+
public void ToggleBreakpointOnSelectedNode()
169+
{
170+
if (OpenedMethods.Count > 0 && ActivePanelIndex < OpenedMethods.Count)
171+
{
172+
var method = OpenedMethods[ActivePanelIndex];
173+
var graphCanvas = method.Graph.GraphCanvas as GraphCanvas;
174+
graphCanvas?.ToggleBreakpointOnSelectedNode();
175+
}
176+
}
177+
166178
public void Dispose()
167179
{
168180
ProjectService.ProjectChanged -= OnProjectChanged;

src/NodeDev.Blazor/wwwroot/styles.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@
137137
padding: 0px;
138138
}
139139

140+
.my-node .breakpoint-indicator {
141+
position: absolute;
142+
right: 8px;
143+
top: 50%;
144+
transform: translateY(-50%);
145+
width: 12px;
146+
height: 12px;
147+
background-color: #f44336;
148+
border-radius: 50%;
149+
border: 2px solid white;
150+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
151+
z-index: 10;
152+
}
153+
140154
g.diagram-link path{
141155
transition: 2s linear stroke;
142156
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using NodeDev.Core.Types;
2+
3+
namespace NodeDev.Core.NodeDecorations;
4+
5+
/// <summary>
6+
/// Decoration to mark a node as having a breakpoint for debugging.
7+
/// Only non-inlinable nodes (nodes with exec connections) can have breakpoints.
8+
/// </summary>
9+
public class BreakpointDecoration : INodeDecoration
10+
{
11+
public static BreakpointDecoration Instance { get; } = new();
12+
13+
private BreakpointDecoration() { }
14+
15+
public string Serialize() => "breakpoint";
16+
17+
public static INodeDecoration Deserialize(TypeFactory typeFactory, string serialized)
18+
{
19+
return Instance;
20+
}
21+
}

src/NodeDev.Core/Nodes/Node.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,31 @@ public T GetOrAddDecoration<T>(Func<T> creator) where T : INodeDecoration
250250
return v;
251251
}
252252

253+
public bool HasDecoration<T>() where T : INodeDecoration => Decorations.ContainsKey(typeof(T));
254+
255+
public void RemoveDecoration<T>() where T : INodeDecoration => Decorations.Remove(typeof(T));
256+
257+
/// <summary>
258+
/// Gets whether this node has a breakpoint set.
259+
/// Only non-inlinable nodes can have breakpoints.
260+
/// </summary>
261+
public bool HasBreakpoint => HasDecoration<NodeDecorations.BreakpointDecoration>();
262+
263+
/// <summary>
264+
/// Toggles the breakpoint on this node.
265+
/// Only works for non-inlinable nodes (nodes with exec connections).
266+
/// </summary>
267+
public void ToggleBreakpoint()
268+
{
269+
if (CanBeInlined)
270+
return; // Cannot set breakpoints on inlinable nodes
271+
272+
if (HasBreakpoint)
273+
RemoveDecoration<NodeDecorations.BreakpointDecoration>();
274+
else
275+
AddDecoration(NodeDecorations.BreakpointDecoration.Instance);
276+
}
277+
253278
#endregion
254279

255280
#region Serialization

src/NodeDev.EndToEndTests/Pages/HomePage.cs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ public async Task CreateNewProject()
3535

3636
public async Task HasClass(string name)
3737
{
38-
await SearchProjectExplorerClasses.GetByText(name).WaitForVisible();
38+
await SearchProjectExplorerClasses.GetByText(name, new() { Exact = true }).WaitForVisible();
3939
}
4040

4141
public async Task ClickClass(string name)
4242
{
43-
await SearchProjectExplorerClasses.GetByText(name).ClickAsync();
43+
await SearchProjectExplorerClasses.GetByText(name, new() { Exact = true }).ClickAsync();
4444
}
4545

4646
public async Task OpenProjectExplorerProjectTab()
@@ -755,4 +755,61 @@ public async Task<string[]> GetConsoleOutput()
755755
}
756756
return lines.ToArray();
757757
}
758+
759+
// Breakpoint Operations
760+
761+
public async Task ClickToggleBreakpointButton()
762+
{
763+
var toggleButton = _user.Locator("[data-test-id='toggle-breakpoint']");
764+
await toggleButton.WaitForVisible();
765+
await toggleButton.ClickAsync();
766+
Console.WriteLine("Clicked toggle breakpoint button");
767+
}
768+
769+
public async Task VerifyNodeHasBreakpoint(string nodeName)
770+
{
771+
var node = GetGraphNode(nodeName);
772+
var breakpointIndicator = node.Locator(".breakpoint-indicator");
773+
774+
try
775+
{
776+
await breakpointIndicator.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = 5000 });
777+
Console.WriteLine($"Verified node '{nodeName}' has breakpoint indicator");
778+
}
779+
catch (TimeoutException)
780+
{
781+
throw new Exception($"Node '{nodeName}' does not have a breakpoint indicator");
782+
}
783+
}
784+
785+
public async Task VerifyNodeHasNoBreakpoint(string nodeName)
786+
{
787+
var node = GetGraphNode(nodeName);
788+
var breakpointIndicator = node.Locator(".breakpoint-indicator");
789+
var count = await breakpointIndicator.CountAsync();
790+
791+
if (count > 0)
792+
{
793+
throw new Exception($"Node '{nodeName}' has a breakpoint indicator when it shouldn't");
794+
}
795+
796+
Console.WriteLine($"Verified node '{nodeName}' has no breakpoint indicator");
797+
}
798+
799+
public async Task AddNodeToCanvas(string nodeType)
800+
{
801+
// Click the add node button
802+
var addNodeButton = _user.Locator("[data-test-id='node-search']");
803+
await addNodeButton.WaitForVisible();
804+
await addNodeButton.ClickAsync();
805+
await Task.Delay(200);
806+
807+
// Find and click the node type in the list
808+
var nodeItem = _user.GetByText(nodeType, new() { Exact = true });
809+
await nodeItem.WaitForVisible();
810+
await nodeItem.ClickAsync();
811+
await Task.Delay(300);
812+
813+
Console.WriteLine($"Added node '{nodeType}' to canvas");
814+
}
758815
}

0 commit comments

Comments
 (0)