• 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

98.9
/src/KeenEyes.UI/Systems/UIDatePickerSystem.cs
1
using System.Globalization;
2

3
using KeenEyes.UI.Abstractions;
4

5
namespace KeenEyes.UI;
6

7
/// <summary>
8
/// System that handles date/time picker interaction and calendar navigation.
9
/// </summary>
10
/// <remarks>
11
/// <para>
12
/// This system manages:
13
/// <list type="bullet">
14
/// <item>Calendar day selection</item>
15
/// <item>Month/year navigation</item>
16
/// <item>Time spinner interactions</item>
17
/// <item>Calendar grid updates</item>
18
/// </list>
19
/// </para>
20
/// </remarks>
21
public sealed class UIDatePickerSystem : SystemBase
22
{
23
    private EventSubscription? clickSubscription;
24

25
    /// <inheritdoc />
26
    protected override void OnInitialize()
27
    {
28
        // Subscribe to click events for calendar days and navigation
29
        clickSubscription = World.Subscribe<UIClickEvent>(OnClick);
1✔
30
    }
1✔
31

32
    /// <inheritdoc />
33
    protected override void Dispose(bool disposing)
34
    {
35
        if (disposing)
1✔
36
        {
37
            clickSubscription?.Dispose();
1✔
38
            clickSubscription = null;
1✔
39
        }
40
        base.Dispose(disposing);
1✔
41
    }
1✔
42

43
    /// <inheritdoc />
44
    public override void Update(float deltaTime)
45
    {
46
        // Most work is event-driven
47
    }
1✔
48

49
    private void OnClick(UIClickEvent evt)
50
    {
51
        // Handle calendar day click
52
        if (World.Has<UICalendarDay>(evt.Element))
1✔
53
        {
54
            HandleDayClick(evt.Element);
1✔
55
            return;
1✔
56
        }
57

58
        // Handle time spinner click
59
        if (World.Has<UITimeSpinner>(evt.Element))
1✔
60
        {
61
            HandleTimeSpinnerClick(evt.Element);
1✔
62
            return;
1✔
63
        }
64

65
        // Handle navigation button clicks
66
        HandleNavigationClick(evt.Element);
1✔
67
    }
1✔
68

69
    private void HandleDayClick(Entity dayEntity)
70
    {
71
        ref readonly var calendarDay = ref World.Get<UICalendarDay>(dayEntity);
1✔
72
        var pickerEntity = calendarDay.DatePicker;
1✔
73

74
        if (!World.IsAlive(pickerEntity) || !World.Has<UIDatePicker>(pickerEntity))
1✔
75
        {
76
            return;
1✔
77
        }
78

79
        if (calendarDay.IsDisabled)
1✔
80
        {
81
            return;
1✔
82
        }
83

84
        ref var picker = ref World.Get<UIDatePicker>(pickerEntity);
1✔
85
        var oldValue = picker.Value;
1✔
86

87
        // Create new date keeping the time component
88
        var newDate = new DateTime(
1✔
89
            calendarDay.Year,
1✔
90
            calendarDay.Month,
1✔
91
            calendarDay.Day,
1✔
92
            picker.Value.Hour,
1✔
93
            picker.Value.Minute,
1✔
94
            picker.Value.Second,
1✔
95
            picker.Value.Kind);
1✔
96

97
        // Check min/max constraints
98
        if (picker.MinDate.HasValue && newDate < picker.MinDate.Value)
1✔
99
        {
100
            return;
1✔
101
        }
102

103
        if (picker.MaxDate.HasValue && newDate > picker.MaxDate.Value)
1✔
104
        {
105
            return;
1✔
106
        }
107

108
        picker.Value = newDate;
1✔
109

110
        // Update display month/year if needed (for overflow days)
111
        if (calendarDay.Month != picker.DisplayMonth || calendarDay.Year != picker.DisplayYear)
1✔
112
        {
113
            picker.DisplayMonth = calendarDay.Month;
1✔
114
            picker.DisplayYear = calendarDay.Year;
1✔
115
            UpdateCalendarGrid(pickerEntity, ref picker);
1✔
116
            World.Send(new UICalendarNavigatedEvent(pickerEntity, picker.DisplayYear, picker.DisplayMonth));
1✔
117
        }
118
        else
119
        {
120
            UpdateDaySelection(pickerEntity, ref picker);
1✔
121
        }
122

123
        UpdateHeaderDisplay(pickerEntity, ref picker);
1✔
124

125
        if (oldValue != newDate)
1✔
126
        {
127
            World.Send(new UIDateChangedEvent(pickerEntity, oldValue, newDate));
1✔
128
        }
129
    }
1✔
130

131
    private void HandleTimeSpinnerClick(Entity spinnerEntity)
132
    {
133
        ref readonly var spinner = ref World.Get<UITimeSpinner>(spinnerEntity);
1✔
134
        var pickerEntity = spinner.DatePicker;
1✔
135

136
        if (!World.IsAlive(pickerEntity) || !World.Has<UIDatePicker>(pickerEntity))
1✔
137
        {
138
            return;
1✔
139
        }
140

141
        ref var picker = ref World.Get<UIDatePicker>(pickerEntity);
1✔
142
        var oldValue = picker.Value;
1✔
143

144
        // Get the text from the spinner to determine if this is an increment or decrement
145
        // For now, we'll just increment by 1 on click (full implementation would have up/down buttons)
146
        DateTime newValue;
147

148
        switch (spinner.Field)
1✔
149
        {
150
            case TimeField.Hour:
151
                newValue = picker.Value.AddHours(1);
1✔
152
                // Wrap around if needed
153
                if (newValue.Day != picker.Value.Day)
1✔
154
                {
155
                    newValue = new DateTime(picker.Value.Year, picker.Value.Month, picker.Value.Day, 0, picker.Value.Minute, picker.Value.Second, picker.Value.Kind);
1✔
156
                }
157
                break;
1✔
158

159
            case TimeField.Minute:
160
                newValue = picker.Value.AddMinutes(1);
1✔
161
                // Keep same hour
162
                if (newValue.Hour != picker.Value.Hour)
1✔
163
                {
164
                    newValue = new DateTime(picker.Value.Year, picker.Value.Month, picker.Value.Day, picker.Value.Hour, 0, picker.Value.Second, picker.Value.Kind);
1✔
165
                }
166
                break;
1✔
167

168
            case TimeField.Second:
169
                newValue = picker.Value.AddSeconds(1);
1✔
170
                // Keep same minute
171
                if (newValue.Minute != picker.Value.Minute)
1✔
172
                {
173
                    newValue = new DateTime(picker.Value.Year, picker.Value.Month, picker.Value.Day, picker.Value.Hour, picker.Value.Minute, 0, picker.Value.Kind);
1✔
174
                }
175
                break;
1✔
176

177
            case TimeField.AmPm:
178
                newValue = picker.Value.AddHours(12);
1✔
179
                // Keep same day
180
                if (newValue.Day != picker.Value.Day)
1✔
181
                {
182
                    newValue = picker.Value.AddHours(-12);
1✔
183
                }
184
                break;
185

186
            default:
187
                return;
188
        }
189

190
        picker.Value = newValue;
1✔
191
        UpdateTimeDisplay(pickerEntity, ref picker);
1✔
192

193
        if (oldValue != newValue)
1✔
194
        {
195
            World.Send(new UIDateChangedEvent(pickerEntity, oldValue, newValue));
1✔
196
        }
197
    }
1✔
198

199
    private void HandleNavigationClick(Entity entity)
200
    {
201
        // Find which date picker this button belongs to
202
        foreach (var pickerEntity in World.Query<UIDatePicker>())
1✔
203
        {
204
            ref var picker = ref World.Get<UIDatePicker>(pickerEntity);
1✔
205

206
            if (entity == picker.PrevMonthButton)
1✔
207
            {
208
                NavigatePreviousMonth(pickerEntity, ref picker);
1✔
209
                return;
1✔
210
            }
211

212
            if (entity == picker.NextMonthButton)
1✔
213
            {
214
                NavigateNextMonth(pickerEntity, ref picker);
1✔
215
                return;
1✔
216
            }
217
        }
218
    }
1✔
219

220
    private void NavigatePreviousMonth(Entity pickerEntity, ref UIDatePicker picker)
221
    {
222
        picker.DisplayMonth--;
1✔
223
        if (picker.DisplayMonth < 1)
1✔
224
        {
225
            picker.DisplayMonth = 12;
1✔
226
            picker.DisplayYear--;
1✔
227
        }
228

229
        UpdateCalendarGrid(pickerEntity, ref picker);
1✔
230
        UpdateHeaderDisplay(pickerEntity, ref picker);
1✔
231
        World.Send(new UICalendarNavigatedEvent(pickerEntity, picker.DisplayYear, picker.DisplayMonth));
1✔
232
    }
1✔
233

234
    private void NavigateNextMonth(Entity pickerEntity, ref UIDatePicker picker)
235
    {
236
        picker.DisplayMonth++;
1✔
237
        if (picker.DisplayMonth > 12)
1✔
238
        {
239
            picker.DisplayMonth = 1;
1✔
240
            picker.DisplayYear++;
1✔
241
        }
242

243
        UpdateCalendarGrid(pickerEntity, ref picker);
1✔
244
        UpdateHeaderDisplay(pickerEntity, ref picker);
1✔
245
        World.Send(new UICalendarNavigatedEvent(pickerEntity, picker.DisplayYear, picker.DisplayMonth));
1✔
246
    }
1✔
247

248
    private void UpdateCalendarGrid(Entity pickerEntity, ref UIDatePicker picker)
249
    {
250
        // Update all calendar day entities
251
        foreach (var dayEntity in World.Query<UICalendarDay>())
1✔
252
        {
253
            ref var calendarDay = ref World.Get<UICalendarDay>(dayEntity);
1✔
254

255
            if (calendarDay.DatePicker != pickerEntity)
1✔
256
            {
257
                continue;
258
            }
259

260
            // Recalculate which day this cell should display
261
            // This would typically be done by the factory, but we update existing cells
262
            UpdateDayCell(dayEntity, ref calendarDay, ref picker);
1✔
263
        }
264
    }
1✔
265

266
    private void UpdateDayCell(Entity dayEntity, ref UICalendarDay calendarDay, ref UIDatePicker picker)
267
    {
268
        var today = DateTime.Today;
1✔
269

270
        calendarDay.IsToday =
1✔
271
            calendarDay.Year == today.Year &&
1✔
272
            calendarDay.Month == today.Month &&
1✔
273
            calendarDay.Day == today.Day;
1✔
274

275
        calendarDay.IsSelected =
1✔
276
            calendarDay.Year == picker.Value.Year &&
1✔
277
            calendarDay.Month == picker.Value.Month &&
1✔
278
            calendarDay.Day == picker.Value.Day;
1✔
279

280
        calendarDay.IsCurrentMonth =
1✔
281
            calendarDay.Month == picker.DisplayMonth &&
1✔
282
            calendarDay.Year == picker.DisplayYear;
1✔
283

284
        // Check if disabled
285
        var cellDate = new DateTime(calendarDay.Year, calendarDay.Month, calendarDay.Day, 0, 0, 0, DateTimeKind.Unspecified);
1✔
286
        calendarDay.IsDisabled =
1✔
287
            (picker.MinDate.HasValue && cellDate < picker.MinDate.Value.Date) ||
1✔
288
            (picker.MaxDate.HasValue && cellDate > picker.MaxDate.Value.Date);
1✔
289

290
        // Update visual styling
291
        if (World.Has<UIStyle>(dayEntity))
1✔
292
        {
293
            ref var style = ref World.Get<UIStyle>(dayEntity);
1✔
294

295
            if (calendarDay.IsDisabled)
1✔
296
            {
297
                style.BackgroundColor = new System.Numerics.Vector4(0.15f, 0.15f, 0.15f, 1f);
1✔
298
            }
299
            else if (calendarDay.IsSelected)
1✔
300
            {
301
                style.BackgroundColor = new System.Numerics.Vector4(0.3f, 0.5f, 0.9f, 1f);
×
302
            }
303
            else if (calendarDay.IsToday)
1✔
304
            {
305
                style.BackgroundColor = new System.Numerics.Vector4(0.4f, 0.4f, 0.4f, 1f);
×
306
            }
307
            else if (!calendarDay.IsCurrentMonth)
1✔
308
            {
309
                style.BackgroundColor = new System.Numerics.Vector4(0.2f, 0.2f, 0.2f, 0.5f);
1✔
310
            }
311
            else
312
            {
313
                style.BackgroundColor = new System.Numerics.Vector4(0.25f, 0.25f, 0.25f, 1f);
1✔
314
            }
315
        }
316

317
        // Update text content
318
        if (World.Has<UIText>(dayEntity))
1✔
319
        {
320
            ref var text = ref World.Get<UIText>(dayEntity);
1✔
321
            text.Content = calendarDay.Day.ToString();
1✔
322

323
            if (calendarDay.IsDisabled || !calendarDay.IsCurrentMonth)
1✔
324
            {
325
                text.Color = new System.Numerics.Vector4(0.5f, 0.5f, 0.5f, 1f);
1✔
326
            }
327
            else
328
            {
329
                text.Color = System.Numerics.Vector4.One;
1✔
330
            }
331
        }
332
    }
1✔
333

334
    private void UpdateDaySelection(Entity pickerEntity, ref UIDatePicker picker)
335
    {
336
        foreach (var dayEntity in World.Query<UICalendarDay>())
1✔
337
        {
338
            ref var calendarDay = ref World.Get<UICalendarDay>(dayEntity);
1✔
339

340
            if (calendarDay.DatePicker != pickerEntity)
1✔
341
            {
342
                continue;
343
            }
344

345
            bool wasSelected = calendarDay.IsSelected;
1✔
346
            calendarDay.IsSelected =
1✔
347
                calendarDay.Year == picker.Value.Year &&
1✔
348
                calendarDay.Month == picker.Value.Month &&
1✔
349
                calendarDay.Day == picker.Value.Day;
1✔
350

351
            if (wasSelected != calendarDay.IsSelected && World.Has<UIStyle>(dayEntity))
1✔
352
            {
353
                ref var style = ref World.Get<UIStyle>(dayEntity);
1✔
354

355
                if (calendarDay.IsSelected)
1✔
356
                {
357
                    style.BackgroundColor = new System.Numerics.Vector4(0.3f, 0.5f, 0.9f, 1f);
1✔
358
                }
359
                else if (calendarDay.IsToday)
1✔
360
                {
361
                    style.BackgroundColor = new System.Numerics.Vector4(0.4f, 0.4f, 0.4f, 1f);
1✔
362
                }
363
                else if (!calendarDay.IsCurrentMonth)
1✔
364
                {
365
                    style.BackgroundColor = new System.Numerics.Vector4(0.2f, 0.2f, 0.2f, 0.5f);
×
366
                }
367
                else
368
                {
369
                    style.BackgroundColor = new System.Numerics.Vector4(0.25f, 0.25f, 0.25f, 1f);
1✔
370
                }
371
            }
372
        }
373
    }
1✔
374

375
    private void UpdateHeaderDisplay(Entity pickerEntity, ref UIDatePicker picker)
376
    {
377
        if (!World.IsAlive(picker.HeaderEntity) || !World.Has<UIText>(picker.HeaderEntity))
1✔
378
        {
379
            return;
1✔
380
        }
381

382
        ref var headerText = ref World.Get<UIText>(picker.HeaderEntity);
1✔
383
        headerText.Content = $"{CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(picker.DisplayMonth)} {picker.DisplayYear}";
1✔
384
    }
1✔
385

386
    private void UpdateTimeDisplay(Entity pickerEntity, ref UIDatePicker picker)
387
    {
388
        // Update hour display
389
        if (World.IsAlive(picker.HourEntity) && World.Has<UIText>(picker.HourEntity))
1✔
390
        {
391
            ref var hourText = ref World.Get<UIText>(picker.HourEntity);
1✔
392
            int displayHour = picker.Value.Hour;
1✔
393

394
            if (picker.TimeFormat == TimeFormat.Hour12)
1✔
395
            {
396
                displayHour = displayHour % 12;
1✔
397
                if (displayHour == 0)
1✔
398
                {
399
                    displayHour = 12;
1✔
400
                }
401
            }
402

403
            hourText.Content = displayHour.ToString("D2");
1✔
404
        }
405

406
        // Update minute display
407
        if (World.IsAlive(picker.MinuteEntity) && World.Has<UIText>(picker.MinuteEntity))
1✔
408
        {
409
            ref var minuteText = ref World.Get<UIText>(picker.MinuteEntity);
1✔
410
            minuteText.Content = picker.Value.Minute.ToString("D2");
1✔
411
        }
412

413
        // Update second display
414
        if (picker.ShowSeconds && World.IsAlive(picker.SecondEntity) && World.Has<UIText>(picker.SecondEntity))
1✔
415
        {
416
            ref var secondText = ref World.Get<UIText>(picker.SecondEntity);
1✔
417
            secondText.Content = picker.Value.Second.ToString("D2");
1✔
418
        }
419

420
        // Update AM/PM display
421
        if (picker.TimeFormat == TimeFormat.Hour12 && World.IsAlive(picker.AmPmEntity) && World.Has<UIText>(picker.AmPmEntity))
1✔
422
        {
423
            ref var ampmText = ref World.Get<UIText>(picker.AmPmEntity);
1✔
424
            ampmText.Content = picker.Value.Hour >= 12 ? "PM" : "AM";
1✔
425
        }
426
    }
1✔
427

428
    /// <summary>
429
    /// Sets the date/time value of a date picker.
430
    /// </summary>
431
    /// <param name="entity">The date picker entity.</param>
432
    /// <param name="value">The new date/time value.</param>
433
    public void SetValue(Entity entity, DateTime value)
434
    {
435
        if (!World.IsAlive(entity) || !World.Has<UIDatePicker>(entity))
1✔
436
        {
437
            return;
1✔
438
        }
439

440
        ref var picker = ref World.Get<UIDatePicker>(entity);
1✔
441
        var oldValue = picker.Value;
1✔
442

443
        // Apply constraints
444
        if (picker.MinDate.HasValue && value < picker.MinDate.Value)
1✔
445
        {
446
            value = picker.MinDate.Value;
1✔
447
        }
448

449
        if (picker.MaxDate.HasValue && value > picker.MaxDate.Value)
1✔
450
        {
451
            value = picker.MaxDate.Value;
1✔
452
        }
453

454
        picker.Value = value;
1✔
455
        picker.DisplayMonth = value.Month;
1✔
456
        picker.DisplayYear = value.Year;
1✔
457

458
        UpdateCalendarGrid(entity, ref picker);
1✔
459
        UpdateHeaderDisplay(entity, ref picker);
1✔
460
        UpdateTimeDisplay(entity, ref picker);
1✔
461

462
        if (oldValue != value)
1✔
463
        {
464
            World.Send(new UIDateChangedEvent(entity, oldValue, value));
1✔
465
        }
466
    }
1✔
467

468
    /// <summary>
469
    /// Gets the current date/time value from a date picker.
470
    /// </summary>
471
    /// <param name="entity">The date picker entity.</param>
472
    /// <returns>The current date/time value, or DateTime.MinValue if invalid.</returns>
473
    public DateTime GetValue(Entity entity)
474
    {
475
        if (!World.IsAlive(entity) || !World.Has<UIDatePicker>(entity))
1✔
476
        {
477
            return DateTime.MinValue;
1✔
478
        }
479

480
        return World.Get<UIDatePicker>(entity).Value;
1✔
481
    }
482

483
    /// <summary>
484
    /// Navigates the calendar to a specific month/year.
485
    /// </summary>
486
    /// <param name="entity">The date picker entity.</param>
487
    /// <param name="year">The year to navigate to.</param>
488
    /// <param name="month">The month to navigate to (1-12).</param>
489
    public void NavigateTo(Entity entity, int year, int month)
490
    {
491
        if (!World.IsAlive(entity) || !World.Has<UIDatePicker>(entity))
1✔
492
        {
493
            return;
1✔
494
        }
495

496
        if (month < 1 || month > 12)
1✔
497
        {
498
            return;
1✔
499
        }
500

501
        ref var picker = ref World.Get<UIDatePicker>(entity);
1✔
502
        picker.DisplayYear = year;
1✔
503
        picker.DisplayMonth = month;
1✔
504

505
        UpdateCalendarGrid(entity, ref picker);
1✔
506
        UpdateHeaderDisplay(entity, ref picker);
1✔
507
        World.Send(new UICalendarNavigatedEvent(entity, year, month));
1✔
508
    }
1✔
509

510
    /// <summary>
511
    /// Navigates the calendar to today's date.
512
    /// </summary>
513
    /// <param name="entity">The date picker entity.</param>
514
    public void NavigateToToday(Entity entity)
515
    {
516
        var today = DateTime.Today;
1✔
517
        NavigateTo(entity, today.Year, today.Month);
1✔
518
    }
1✔
519

520
    /// <summary>
521
    /// Gets the number of days in a specific month.
522
    /// </summary>
523
    /// <param name="year">The year.</param>
524
    /// <param name="month">The month (1-12).</param>
525
    /// <returns>The number of days in the month.</returns>
526
    public static int GetDaysInMonth(int year, int month)
527
    {
528
        return DateTime.DaysInMonth(year, month);
1✔
529
    }
530

531
    /// <summary>
532
    /// Gets the day of week for the first day of a month.
533
    /// </summary>
534
    /// <param name="year">The year.</param>
535
    /// <param name="month">The month (1-12).</param>
536
    /// <returns>The day of week (0 = Sunday, 6 = Saturday).</returns>
537
    public static DayOfWeek GetFirstDayOfMonth(int year, int month)
538
    {
539
        return new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Unspecified).DayOfWeek;
1✔
540
    }
541
}
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