• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 29923911837

22 Jul 2026 01:26PM UTC coverage: 62.597% (-2.6%) from 65.151%
29923911837

push

github

tyevco
test(mcp): Update TestBridge mocks to current interface surface

MockTestBridge and MockCaptureController never implemented the members added
during the MCP Tools Expansion, so tests/KeenEyes.Mcp.TestBridge.Tests did not
compile on main. MockStateController had also drifted (GetComponentAsync and
EntitySnapshot.Components now use JsonElement).

- MockTestBridge: add Window, Time, Systems, Mutation, Profile, Snapshot, AI,
  Replay and InputContext, backed by new hand-written mock controllers plus
  KeenEyes.Testing's MockInputContext, mirroring the existing recording /
  canned-result mock style.
- MockCaptureController: add CaptureRegionAsync, GetRegionScreenshotBytesAsync
  and SaveRegionScreenshotAsync.
- MockStateController: return JsonElement from GetComponentAsync and store
  component data as JsonElement to match the current IStateController.
- Rename pre-existing PascalCase private fields in InputParsingTests to
  camelCase so the project passes dotnet format once it is in CI.

The project was MISSING from KeenEyes.slnx, which is why the compile break went
unnoticed (CI builds the solution). Adding it to the solution is itself the
cheap drift guard requested in the issue: CI now compiles the mocks against the
interfaces, so any future interface addition breaks the build immediately. All
80 tests pass under --max-parallel-test-modules 1.

Fixes #1025

8144 of 12399 branches covered (65.68%)

Branch coverage included in aggregate %.

48975 of 78850 relevant lines covered (62.11%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.63
/src/KeenEyes.Graph/Systems/GraphContextMenuSystem.cs
1
using KeenEyes.Graph.Abstractions;
2
using KeenEyes.Input.Abstractions;
3

4
namespace KeenEyes.Graph;
5

6
/// <summary>
7
/// System that processes context menu interactions for graph editing.
8
/// </summary>
9
/// <remarks>
10
/// <para>
11
/// Handles keyboard navigation (arrow keys, Enter, Escape), search filtering,
12
/// and menu item execution for context menus on graph canvases.
13
/// </para>
14
/// </remarks>
15
public sealed class GraphContextMenuSystem : SystemBase
16
{
17
    private IInputContext? inputContext;
18
    private GraphContext? graphContext;
19
    private PortRegistry? portRegistry;
20
    private NodeTypeRegistry? nodeTypeRegistry;
21

22
    // Key state for debouncing
23
    private readonly HashSet<Key> keysDownLastFrame = [];
1✔
24

25
    /// <inheritdoc />
26
    public override void Update(float deltaTime)
27
    {
28
        // Lazy initialization
29
        if (inputContext is null && !World.TryGetExtension(out inputContext))
×
30
        {
31
            return;
×
32
        }
33

34
        if (graphContext is null && !World.TryGetExtension(out graphContext))
×
35
        {
36
            return;
×
37
        }
38

39
        if (portRegistry is null && !World.TryGetExtension(out portRegistry))
×
40
        {
41
            return;
×
42
        }
43

44
        if (nodeTypeRegistry is null)
×
45
        {
46
            World.TryGetExtension(out nodeTypeRegistry);
×
47
        }
48

49
        var keyboard = inputContext!.Keyboard;
×
50

51
        // Process each canvas with an active context menu
52
        foreach (var canvas in World.Query<GraphCanvas, GraphContextMenu, GraphCanvasTag>())
×
53
        {
54
            ProcessContextMenu(canvas, keyboard);
×
55
        }
56

57
        // Update key state for next frame
58
        UpdateKeyState(keyboard);
×
59
    }
×
60

61
    private void ProcessContextMenu(Entity canvas, IKeyboard keyboard)
62
    {
63
        ref var canvasData = ref World.Get<GraphCanvas>(canvas);
×
64
        ref var menu = ref World.Get<GraphContextMenu>(canvas);
×
65

66
        // Handle Escape - close menu
67
        if (WasKeyJustPressed(keyboard, Key.Escape))
×
68
        {
69
            CloseMenu(canvas, ref canvasData);
×
70
            return;
×
71
        }
72

73
        // Handle menu type-specific logic
74
        switch (menu.MenuType)
×
75
        {
76
            case ContextMenuType.Canvas:
77
                ProcessCanvasMenu(canvas, ref canvasData, ref menu, keyboard);
×
78
                break;
×
79

80
            case ContextMenuType.Node:
81
                ProcessNodeMenu(canvas, ref canvasData, ref menu, keyboard);
×
82
                break;
×
83

84
            case ContextMenuType.Connection:
85
                ProcessConnectionMenu(canvas, ref canvasData, ref menu, keyboard);
×
86
                break;
87
        }
88
    }
×
89

90
    private void ProcessCanvasMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
91
    {
92
        // Get filtered node types
93
        var nodeTypes = GetFilteredNodeTypes(menu.SearchFilter);
×
94

95
        if (nodeTypes.Count == 0)
×
96
        {
97
            return;
×
98
        }
99

100
        // Handle arrow key navigation
101
        if (WasKeyJustPressed(keyboard, Key.Down))
×
102
        {
103
            menu.SelectedIndex = (menu.SelectedIndex + 1) % nodeTypes.Count;
×
104
        }
105
        else if (WasKeyJustPressed(keyboard, Key.Up))
×
106
        {
107
            menu.SelectedIndex = (menu.SelectedIndex - 1 + nodeTypes.Count) % nodeTypes.Count;
×
108
        }
109

110
        // Handle Enter - create selected node
111
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
112
        {
113
            var selectedType = nodeTypes[menu.SelectedIndex];
×
114
            graphContext!.CreateNodeUndoable(canvas, selectedType.TypeId, menu.CanvasPosition);
×
115
            CloseMenu(canvas, ref canvasData);
×
116
        }
117

118
        // Handle alphanumeric input for search filtering
119
        UpdateSearchFilter(ref menu, keyboard);
×
120
    }
×
121

122
    private void ProcessNodeMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
123
    {
124
        // Node menu options: Delete, Duplicate, Copy, Cut
125
        var options = new[] { "Delete", "Duplicate" };
×
126

127
        // Handle arrow key navigation
128
        if (WasKeyJustPressed(keyboard, Key.Down))
×
129
        {
130
            menu.SelectedIndex = (menu.SelectedIndex + 1) % options.Length;
×
131
        }
132
        else if (WasKeyJustPressed(keyboard, Key.Up))
×
133
        {
134
            menu.SelectedIndex = (menu.SelectedIndex - 1 + options.Length) % options.Length;
×
135
        }
136

137
        // Handle Enter - execute selected option
138
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
139
        {
140
            var selectedOption = options[menu.SelectedIndex];
×
141

142
            switch (selectedOption)
143
            {
144
                case "Delete":
145
                    if (menu.TargetEntity.IsValid)
×
146
                    {
147
                        graphContext!.DeleteNodesUndoable([menu.TargetEntity]);
×
148
                    }
149
                    break;
×
150

151
                case "Duplicate":
152
                    if (menu.TargetEntity.IsValid)
×
153
                    {
154
                        graphContext!.DuplicateSelectionUndoable();
×
155
                    }
156
                    break;
157
            }
158

159
            CloseMenu(canvas, ref canvasData);
×
160
        }
161
    }
×
162

163
    private void ProcessConnectionMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
164
    {
165
        // Handle Enter - delete connection
166
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
167
        {
168
            if (menu.TargetEntity.IsValid)
×
169
            {
170
                graphContext!.DeleteConnectionUndoable(menu.TargetEntity);
×
171
            }
172

173
            CloseMenu(canvas, ref canvasData);
×
174
        }
175
    }
×
176

177
    private void CloseMenu(Entity canvas, ref GraphCanvas canvasData)
178
    {
179
        World.Remove<GraphContextMenu>(canvas);
×
180
        canvasData.Mode = GraphInteractionMode.None;
×
181
    }
×
182

183
    private List<PortRegistry.NodeTypeInfo> GetFilteredNodeTypes(string filter)
184
    {
185
        var allTypes = portRegistry!.GetAllNodeTypes().ToList();
×
186

187
        if (string.IsNullOrWhiteSpace(filter))
×
188
        {
189
            // When no filter, sort by category then by name for better organization
190
            return allTypes
×
191
                .OrderBy(t => t.Category)
×
192
                .ThenBy(t => t.Name)
×
193
                .ToList();
×
194
        }
195

196
        var lowerFilter = filter.ToLowerInvariant();
×
197
        return allTypes
×
198
            .Where(t => t.Name.ToLowerInvariant().Contains(lowerFilter) ||
×
199
                        t.Category.ToLowerInvariant().Contains(lowerFilter))
×
200
            .OrderBy(t => t.Category)
×
201
            .ThenBy(t => t.Name)
×
202
            .ToList();
×
203
    }
204

205
    /// <summary>
206
    /// Gets node types organized by category for hierarchical display.
207
    /// </summary>
208
    /// <param name="filter">Optional search filter.</param>
209
    /// <returns>Dictionary of category name to list of node types in that category.</returns>
210
    internal Dictionary<string, List<PortRegistry.NodeTypeInfo>> GetNodeTypesByCategory(string filter)
211
    {
212
        var filtered = GetFilteredNodeTypes(filter);
×
213
        return filtered
×
214
            .GroupBy(t => t.Category)
×
215
            .OrderBy(g => g.Key)
×
216
            .ToDictionary(g => g.Key, g => g.ToList());
×
217
    }
218

219
    /// <summary>
220
    /// Gets all categories with registered node types.
221
    /// </summary>
222
    /// <returns>Sorted list of category names.</returns>
223
    internal IReadOnlyList<string> GetCategories()
224
    {
225
        if (nodeTypeRegistry is not null)
×
226
        {
227
            return nodeTypeRegistry.GetCategories().ToList();
×
228
        }
229

230
        return portRegistry?.GetCategories().ToList() ?? [];
×
231
    }
232

233
    private void UpdateSearchFilter(ref GraphContextMenu menu, IKeyboard keyboard)
234
    {
235
        // Handle backspace
236
        if (WasKeyJustPressed(keyboard, Key.Backspace) && menu.SearchFilter.Length > 0)
×
237
        {
238
            menu.SearchFilter = menu.SearchFilter[..^1];
×
239
            menu.SelectedIndex = 0; // Reset selection when filter changes
×
240
            return;
×
241
        }
242

243
        // Handle alphanumeric keys (simplified - in a real impl, use proper text input).
244
        // The typed character is appended after the scan loops so no string
245
        // concatenation happens inside a loop (S1643).
246
        char? typed = null;
×
247
        for (var key = Key.A; key <= Key.Z; key++)
×
248
        {
249
            if (WasKeyJustPressed(keyboard, key))
×
250
            {
251
                var shift = (keyboard.Modifiers & KeyModifiers.Shift) != 0;
×
252
                typed = shift ? (char)key : char.ToLowerInvariant((char)key);
×
253
                break;
×
254
            }
255
        }
256

257
        // Handle number keys
258
        if (!typed.HasValue)
×
259
        {
260
            for (var key = Key.Number0; key <= Key.Number9; key++)
×
261
            {
262
                if (WasKeyJustPressed(keyboard, key))
×
263
                {
264
                    typed = (char)('0' + (key - Key.Number0));
×
265
                    break;
×
266
                }
267
            }
268
        }
269

270
        if (typed.HasValue)
×
271
        {
272
            menu.SearchFilter += typed.Value;
×
273
            menu.SelectedIndex = 0;
×
274
            return;
×
275
        }
276

277
        // Handle space
278
        if (WasKeyJustPressed(keyboard, Key.Space))
×
279
        {
280
            menu.SearchFilter += ' ';
×
281
            menu.SelectedIndex = 0;
×
282
        }
283
    }
×
284

285
    private bool WasKeyJustPressed(IKeyboard keyboard, Key key)
286
    {
287
        var isDownNow = keyboard.IsKeyDown(key);
×
288
        var wasDownLastFrame = keysDownLastFrame.Contains(key);
×
289
        return isDownNow && !wasDownLastFrame;
×
290
    }
291

292
    // Keys that the context menu system cares about
293
    private static readonly Key[] trackedKeys =
×
294
    [
×
295
        Key.Escape,  // Close menu
×
296
        Key.Enter,   // Confirm selection
×
297
        Key.Up,      // Navigate up
×
298
        Key.Down,    // Navigate down
×
299
        Key.Left,    // Navigate left/collapse
×
300
        Key.Right,   // Navigate right/expand
×
301
    ];
×
302

303
    private void UpdateKeyState(IKeyboard keyboard)
304
    {
305
        keysDownLastFrame.Clear();
×
306

307
        // Only track keys relevant to context menu navigation
308
        foreach (var key in trackedKeys)
×
309
        {
310
            if (keyboard.IsKeyDown(key))
×
311
            {
312
                keysDownLastFrame.Add(key);
×
313
            }
314
        }
315
    }
×
316
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc