Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/**/coverage.cobertura.xml
97 changes: 97 additions & 0 deletions Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Pointframe.Models;
using Pointframe.Services;
using Xunit;

namespace Pointframe.Tests.Services;

public sealed class GlobalHotkeyServiceTests
{
private static GlobalHotkeyService CreateService(UserSettings? settings = null)
{
var mock = new Mock<IUserSettingsService>();
mock.SetupGet(s => s.Current).Returns(settings ?? new UserSettings());
return new GlobalHotkeyService(mock.Object, NullLogger<GlobalHotkeyService>.Instance);
}

[Fact]
public void BeginKeyCaptureMode_StoresCallback()
{
var svc = CreateService();
Action<uint, HotkeyModifiers> callback = (_, _) => { };

svc.BeginKeyCaptureMode(callback);

var field = typeof(GlobalHotkeyService)
.GetField("_keyCaptureCallback", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(field);
Assert.Same(callback, field!.GetValue(svc));
}

[Fact]
public void EndKeyCaptureMode_ClearsCallback()
{
var svc = CreateService();
svc.BeginKeyCaptureMode((_, _) => { });

svc.EndKeyCaptureMode();

var field = typeof(GlobalHotkeyService)
.GetField("_keyCaptureCallback", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(field);
Assert.Null(field!.GetValue(svc));
}

[Fact]
public void Dispose_CanBeCalledMultipleTimes_DoesNotThrow()
{
var svc = CreateService();
svc.Dispose();
var ex = Record.Exception(svc.Dispose);
Assert.Null(ex);
}

[Theory]
[InlineData(false, false, false, HotkeyModifiers.None, true)]
[InlineData(true, false, false, HotkeyModifiers.Ctrl, true)]
[InlineData(true, true, false, HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, true)]
[InlineData(true, true, true, HotkeyModifiers.Ctrl | HotkeyModifiers.Shift | HotkeyModifiers.Alt, true)]
[InlineData(true, false, false, HotkeyModifiers.None, false)]
[InlineData(false, false, false, HotkeyModifiers.Ctrl, false)]
[InlineData(true, true, false, HotkeyModifiers.Ctrl, false)]
public void ModifiersMatch_ReturnsExpected(bool ctrl, bool shift, bool alt, HotkeyModifiers required, bool expected)
{
var method = typeof(GlobalHotkeyService)
.GetMethod("ModifiersMatch", BindingFlags.Static | BindingFlags.NonPublic);
Assert.NotNull(method);

var result = (bool)method!.Invoke(null, [required, ctrl, shift, alt])!;

Assert.Equal(expected, result);
}

[Theory]
[InlineData(NativeMethods.VK_SHIFT, true)]
[InlineData(NativeMethods.VK_LSHIFT, true)]
[InlineData(NativeMethods.VK_RSHIFT, true)]
[InlineData(NativeMethods.VK_LCONTROL, true)]
[InlineData(NativeMethods.VK_RCONTROL, true)]
[InlineData(NativeMethods.VK_LMENU, true)]
[InlineData(NativeMethods.VK_RMENU, true)]
[InlineData(NativeMethods.VK_LWIN, true)]
[InlineData(NativeMethods.VK_RWIN, true)]
[InlineData(0x41u, false)] // 'A' — not a modifier
[InlineData(NativeMethods.VK_ESCAPE, false)]
public void IsModifierVk_ReturnsExpected(uint vk, bool expected)
{
var method = typeof(GlobalHotkeyService)
.GetMethod("IsModifierVk", BindingFlags.Static | BindingFlags.NonPublic);
Assert.NotNull(method);

var result = (bool)method!.Invoke(null, [vk])!;

Assert.Equal(expected, result);
}
}
151 changes: 142 additions & 9 deletions Pointframe.Tests/SettingsWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Windows.Interop;
using System.Windows.Media;
using Moq;
using Pointframe;
using Pointframe.Models;
using Pointframe.Services;
using Pointframe.Tests.Services.Handlers;
Expand Down Expand Up @@ -75,38 +76,163 @@ public void DoubleInput_Pasting_AllowsValidPaste()
}

[Fact]
public void HotkeyCapture_PreviewKeyDown_Escape_CancelsRecording()
public void OnCaptureHotkeyKeyPressed_Escape_CancelsRecording()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsRecordingHotkey = true;
var args = CreateKeyArgs(Key.Escape);

InvokePrivateHandler(window, "HotkeyCapture_PreviewKeyDown", window, args);
InvokeCallback(window, "OnCaptureHotkeyKeyPressed", NativeMethods.VK_ESCAPE, HotkeyModifiers.None);

Assert.True(args.Handled);
Assert.False(viewModel.IsRecordingHotkey);
});
}

[Fact]
public void HotkeyCapture_PreviewKeyDown_StoresNewHotkey()
public void OnCaptureHotkeyKeyPressed_StoresNewHotkey()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsRecordingHotkey = true;
var args = CreateKeyArgs(Key.A);

InvokePrivateHandler(window, "HotkeyCapture_PreviewKeyDown", window, args);
InvokeCallback(window, "OnCaptureHotkeyKeyPressed", (uint)KeyInterop.VirtualKeyFromKey(Key.A), HotkeyModifiers.None);

Assert.True(args.Handled);
Assert.Equal((uint)KeyInterop.VirtualKeyFromKey(Key.A), viewModel.RegionCaptureHotkey);
Assert.False(viewModel.IsRecordingHotkey);
});
Comment on lines 92 to 104

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests cover that a captured key is stored, but don’t verify the new modifier behavior introduced by the callback signature. Consider asserting that RegionCaptureHotkeyModifiers is updated when a non-None modifier set is passed, and adding a similar test for OnRecordHotkeyKeyPressed to ensure record hotkey modifiers are persisted to the view model.

Copilot uses AI. Check for mistakes.
}

[Fact]
public void OnCaptureHotkeyKeyPressed_StoresModifiers()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsRecordingHotkey = true;

InvokeCallback(window, "OnCaptureHotkeyKeyPressed", (uint)KeyInterop.VirtualKeyFromKey(Key.A), HotkeyModifiers.Ctrl | HotkeyModifiers.Shift);

Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, viewModel.RegionCaptureHotkeyModifiers);
Assert.False(viewModel.IsRecordingHotkey);
});
}

[Fact]
public void OnRecordHotkeyKeyPressed_StoresNewHotkey()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsCapturingWholeScreenRecordHotkey = true;

InvokeCallback(window, "OnRecordHotkeyKeyPressed", (uint)KeyInterop.VirtualKeyFromKey(Key.R), HotkeyModifiers.None);

Assert.Equal((uint)KeyInterop.VirtualKeyFromKey(Key.R), viewModel.WholeScreenRecordHotkey);
Assert.False(viewModel.IsCapturingWholeScreenRecordHotkey);
});
}

[Fact]
public void OnRecordHotkeyKeyPressed_StoresModifiers()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsCapturingWholeScreenRecordHotkey = true;

InvokeCallback(window, "OnRecordHotkeyKeyPressed", (uint)KeyInterop.VirtualKeyFromKey(Key.R), HotkeyModifiers.Ctrl | HotkeyModifiers.Alt);

Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Alt, viewModel.WholeScreenRecordHotkeyModifiers);
Assert.False(viewModel.IsCapturingWholeScreenRecordHotkey);
});
}

[Fact]
public void OnRecordHotkeyKeyPressed_Escape_CancelsCapture()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsCapturingWholeScreenRecordHotkey = true;

InvokeCallback(window, "OnRecordHotkeyKeyPressed", NativeMethods.VK_ESCAPE, HotkeyModifiers.None);

Assert.False(viewModel.IsCapturingWholeScreenRecordHotkey);
});
}

[Fact]
public void WholeScreenRecordHotkeyRecordingPanel_IsVisibleChanged_WhenVisible_FocusesPanel()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow();
var panel = (StackPanel)window.FindName("RecordHotkeyRecordingPanel");
Assert.NotNull(panel);
var args = new DependencyPropertyChangedEventArgs(UIElement.IsVisibleProperty, false, true);

InvokePrivateHandler(window, "WholeScreenRecordHotkeyRecordingPanel_IsVisibleChanged", panel!, args);

Assert.True(panel!.Focusable);
});
}

[Fact]
public void HotkeyCapture_PreviewKeyUp_UpdatesLiveDisplay()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsRecordingHotkey = true;
window.Show();
window.UpdateLayout();
var args = CreateKeyArgs(Key.LeftCtrl);

InvokePrivateHandler(window, "HotkeyCapture_PreviewKeyUp", window, args);

Assert.True(args.Handled);
window.Close();
});
}

[Fact]
public void RecordHotkeyCapture_PreviewKeyDown_UpdatesLiveDisplay()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsCapturingWholeScreenRecordHotkey = true;
window.Show();
window.UpdateLayout();
var args = CreateKeyArgs(Key.LeftCtrl);

InvokePrivateHandler(window, "RecordHotkeyCapture_PreviewKeyDown", window, args);

Assert.True(args.Handled);
window.Close();
});
}

[Fact]
public void RecordHotkeyCapture_PreviewKeyUp_UpdatesLiveDisplay()
{
StaTestHelper.Run(() =>
{
var window = CreateWindow(out var viewModel);
viewModel.IsCapturingWholeScreenRecordHotkey = true;
window.Show();
window.UpdateLayout();
var args = CreateKeyArgs(Key.LeftCtrl);

InvokePrivateHandler(window, "RecordHotkeyCapture_PreviewKeyUp", window, args);

Assert.True(args.Handled);
window.Close();
});
}

[Fact]
public void HotkeyCapture_PreviewKeyDown_IgnoresModifierKeys()
{
Expand Down Expand Up @@ -190,7 +316,7 @@ private static SettingsWindow CreateWindow(out SettingsViewModel viewModel)
Mock.Of<IMicrophoneDeviceService>(service =>
service.GetAvailableCaptureDeviceNames() == new[] { "Studio Mic", "USB Mic" } &&
service.GetDefaultCaptureDeviceName() == "Studio Mic"));
return new SettingsWindow(viewModel);
return new SettingsWindow(viewModel, Mock.Of<IGlobalHotkeyService>());
}

private static SettingsWindow CreateWindow() => CreateWindow(out _);
Expand All @@ -202,6 +328,13 @@ private static void InvokePrivateHandler(object target, string methodName, objec
method.Invoke(target, [sender, args]);
}

private static void InvokeCallback(object target, string methodName, uint vk, HotkeyModifiers modifiers)
{
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(method);
method.Invoke(target, [vk, modifiers]);
}

private static TextCompositionEventArgs CreateTextInputArgs(TextBox textBox, string text)
{
var composition = new TextComposition(InputManager.Current, textBox, text);
Expand Down
Loading
Loading