added tests to project
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
|
||||
namespace CursorLang.Tests.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Vetting the caret position. Some applications report it in their own
|
||||
/// coordinate system, and such answers have to be sifted out by the bounds
|
||||
/// of the input window.
|
||||
/// </summary>
|
||||
public sealed class CaretNativeTests
|
||||
{
|
||||
private static readonly PopupWindowNative.Rect Window = new()
|
||||
{
|
||||
Left = 100,
|
||||
Top = 100,
|
||||
Right = 900,
|
||||
Bottom = 700,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void A_caret_inside_the_window_is_taken_as_is()
|
||||
{
|
||||
var caret = new PopupWindowNative.Rect { Left = 200, Top = 300, Right = 202, Bottom = 320 };
|
||||
|
||||
Assert.Equal(caret, CaretNative.Validate(caret, Window, scale: 1.5));
|
||||
}
|
||||
|
||||
// The application reported the coordinates without the screen scale: on
|
||||
// their own they sit above and to the left of the input window, and after
|
||||
// being brought to the scale they land inside it
|
||||
[Fact]
|
||||
public void An_unscaled_caret_is_brought_to_the_screen_scale()
|
||||
{
|
||||
var caret = new PopupWindowNative.Rect { Left = 80, Top = 80, Right = 81, Bottom = 90 };
|
||||
|
||||
PopupWindowNative.Rect? validated = CaretNative.Validate(caret, Window, scale: 1.5);
|
||||
|
||||
Assert.NotNull(validated);
|
||||
Assert.Equal(120, validated.Value.Left);
|
||||
Assert.Equal(120, validated.Value.Top);
|
||||
Assert.Equal(121, validated.Value.Right);
|
||||
Assert.Equal(135, validated.Value.Bottom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_caret_far_from_the_window_is_discarded()
|
||||
{
|
||||
var caret = new PopupWindowNative.Rect { Left = 5000, Top = 5000, Right = 5002, Bottom = 5020 };
|
||||
|
||||
Assert.Null(CaretNative.Validate(caret, Window, scale: 1.5));
|
||||
}
|
||||
|
||||
// At the ordinary scale there is nothing to fix: wrong coordinates stay wrong
|
||||
[Fact]
|
||||
public void At_scale_one_a_caret_outside_the_window_is_discarded()
|
||||
{
|
||||
var caret = new PopupWindowNative.Rect { Left = 10, Top = 10, Right = 12, Bottom = 30 };
|
||||
|
||||
Assert.Null(CaretNative.Validate(caret, Window, scale: 1.0));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(100, 100, 900, 700, true)]
|
||||
[InlineData(99, 100, 900, 700, false)]
|
||||
[InlineData(100, 99, 900, 700, false)]
|
||||
[InlineData(100, 100, 901, 700, false)]
|
||||
[InlineData(100, 100, 900, 701, false)]
|
||||
[InlineData(400, 400, 402, 420, true)]
|
||||
public void Inside_the_window_means_entirely_inside(
|
||||
int left, int top, int right, int bottom, bool expected)
|
||||
{
|
||||
var caret = new PopupWindowNative.Rect { Left = left, Top = top, Right = right, Bottom = bottom };
|
||||
|
||||
Assert.Equal(expected, CaretNative.IsInside(caret, Window));
|
||||
}
|
||||
|
||||
// When there is no caret, its rectangle comes back with zero height. Zero
|
||||
// coordinates, on the other hand, are the ordinary start of an empty field
|
||||
[Theory]
|
||||
[InlineData(0, 0, 0, 0, true)]
|
||||
[InlineData(0, 0, 2, 0, true)]
|
||||
[InlineData(0, 10, 2, 5, true)]
|
||||
[InlineData(0, 0, 0, 1, false)]
|
||||
[InlineData(0, 0, 0, 20, false)]
|
||||
public void A_caret_without_height_counts_as_empty(
|
||||
int left, int top, int right, int bottom, bool expected)
|
||||
{
|
||||
var rect = new PopupWindowNative.Rect { Left = left, Top = top, Right = right, Bottom = bottom };
|
||||
|
||||
Assert.Equal(expected, CaretNative.IsEmpty(rect));
|
||||
}
|
||||
|
||||
// The answer depends on what is on screen right now, but it has no right to
|
||||
// throw: the tooltip is shown whatever the answer
|
||||
[Fact]
|
||||
public void Asking_the_system_for_the_caret_goes_without_errors()
|
||||
{
|
||||
PopupWindowNative.Rect? caret = Sta.Run(CaretNative.TryGetCaretRect);
|
||||
|
||||
if (caret is not null)
|
||||
{
|
||||
Assert.True(caret.Value.Bottom > caret.Value.Top);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Models;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
using CursorLang.ViewModels;
|
||||
using CursorLang.Views;
|
||||
|
||||
namespace CursorLang.Tests.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that need a real foreground window with an input field: the caret
|
||||
/// and the layout switch live exactly there.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Windows does not always allow a window to come forward — when the screen is
|
||||
/// locked, say, or when the run happens in a session without a desktop. In
|
||||
/// those cases the check reports itself as skipped rather than failed: there
|
||||
/// would be nothing to verify.
|
||||
/// </remarks>
|
||||
public sealed class ForegroundWindowTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_caret_in_an_input_field_is_found()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var input = new InputWindow();
|
||||
input.RequireForeground();
|
||||
|
||||
PopupWindowNative.Rect? caret = CaretNative.TryGetCaretRect();
|
||||
|
||||
if (caret is null)
|
||||
{
|
||||
Assert.Skip("The input field did not report the caret position");
|
||||
}
|
||||
|
||||
// The caret has to sit inside the input window and to have a height
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(input.Handle)!.Value;
|
||||
|
||||
Assert.True(caret.Value.Bottom > caret.Value.Top);
|
||||
Assert.InRange(caret.Value.Left, bounds.Left, bounds.Right);
|
||||
Assert.InRange(caret.Value.Top, bounds.Top, bounds.Bottom);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_tooltip_at_the_caret_lands_next_to_it()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var input = new InputWindow();
|
||||
input.RequireForeground();
|
||||
|
||||
PopupWindowNative.Rect? caret = CaretNative.TryGetCaretRect();
|
||||
if (caret is null)
|
||||
{
|
||||
Assert.Skip("The input field did not report the caret position");
|
||||
}
|
||||
|
||||
var settings = new AppSettings
|
||||
{
|
||||
PlacementMode = PopupPlacementMode.AtCaret,
|
||||
CaretSide = AnchorSide.BottomRight,
|
||||
CaretOffset = 8,
|
||||
};
|
||||
|
||||
var viewModel = new LayoutPopupViewModel(settings) { ShortName = "RU" };
|
||||
var popup = new LayoutPopupWindow(viewModel, settings);
|
||||
|
||||
try
|
||||
{
|
||||
popup.ShowPopup();
|
||||
|
||||
PopupWindowNative.Rect bounds =
|
||||
WindowPlacementNative.TryGetBounds(new WindowInteropHelper(popup).Handle)!.Value;
|
||||
|
||||
// The tooltip landed to the right of and below the caret — as asked
|
||||
Assert.True(bounds.Left >= caret.Value.Right);
|
||||
Assert.True(bounds.Top >= caret.Value.Bottom);
|
||||
}
|
||||
finally
|
||||
{
|
||||
popup.Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The request to switch the layout goes to the window holding the input
|
||||
// focus, so it only concerns the test window itself
|
||||
[Fact]
|
||||
public void The_request_to_change_the_layout_reaches_its_own_window()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var input = new InputWindow();
|
||||
input.RequireForeground();
|
||||
|
||||
int before = KeyboardLayoutNative.GetActiveLocaleId();
|
||||
|
||||
KeyboardLayoutNative.RequestNextLayout();
|
||||
Sta.Pause(TimeSpan.FromMilliseconds(150));
|
||||
|
||||
int after = KeyboardLayoutNative.GetActiveLocaleId();
|
||||
|
||||
Assert.InRange(after, 1, 0xFFFF);
|
||||
|
||||
if (after == before)
|
||||
{
|
||||
// The system may hold a single layout — there is nothing to switch to
|
||||
return;
|
||||
}
|
||||
|
||||
// Bring the layout back around the circle to where it was
|
||||
for (int i = 0; i < 8 && KeyboardLayoutNative.GetActiveLocaleId() != before; i++)
|
||||
{
|
||||
KeyboardLayoutNative.RequestNextLayout();
|
||||
Sta.Pause(TimeSpan.FromMilliseconds(150));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>A window with an input field brought to the foreground.</summary>
|
||||
private sealed class InputWindow : IDisposable
|
||||
{
|
||||
private readonly Window _window;
|
||||
|
||||
internal InputWindow()
|
||||
{
|
||||
var box = new TextBox { Text = "check", FontSize = 20 };
|
||||
|
||||
_window = new Window
|
||||
{
|
||||
Width = 400,
|
||||
Height = 200,
|
||||
ShowInTaskbar = false,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
Left = 100,
|
||||
Top = 100,
|
||||
Topmost = true,
|
||||
Content = box,
|
||||
};
|
||||
|
||||
_window.Show();
|
||||
Handle = new WindowInteropHelper(_window).Handle;
|
||||
|
||||
// Windows grants the right to bring a window forward neither to
|
||||
// everyone nor at once, so it takes a few attempts
|
||||
for (int attempt = 0; attempt < 10; attempt++)
|
||||
{
|
||||
_window.Activate();
|
||||
box.Focus();
|
||||
box.CaretIndex = box.Text.Length;
|
||||
|
||||
Sta.Pause(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
if (KeyboardLayoutNative.GetForegroundWindow() == Handle)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The caret does not appear at the same instant as the focus
|
||||
Sta.Pause(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
internal IntPtr Handle { get; }
|
||||
|
||||
/// <summary>Skips the check if the window never became the foreground one.</summary>
|
||||
internal void RequireForeground()
|
||||
{
|
||||
if (KeyboardLayoutNative.GetForegroundWindow() != Handle)
|
||||
{
|
||||
Assert.Skip("The window could not be brought to the foreground");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _window.Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
|
||||
namespace CursorLang.Tests.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Making sense of the events of the system keyboard hook.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The events are fed straight into the handler the way Windows sends them:
|
||||
/// the tests have no right to press keys for real — the interception is shared
|
||||
/// by the whole system, and a real press would land in someone else's window.
|
||||
/// </remarks>
|
||||
public sealed class LowLevelKeyboardHookTests
|
||||
{
|
||||
private const int HcAction = 0;
|
||||
private const int WmKeyDown = 0x0100;
|
||||
private const int WmKeyUp = 0x0101;
|
||||
private const int WmSysKeyDown = 0x0104;
|
||||
private const int WmSysKeyUp = 0x0105;
|
||||
private const int WmMouseMove = 0x0200;
|
||||
|
||||
private const uint Injected = 0x10;
|
||||
private const int CapsLock = 0x14;
|
||||
|
||||
[Theory]
|
||||
[InlineData(WmKeyDown, true)]
|
||||
[InlineData(WmSysKeyDown, true)]
|
||||
[InlineData(WmKeyUp, false)]
|
||||
[InlineData(WmSysKeyUp, false)]
|
||||
public void Presses_and_releases_reach_the_handler(int message, bool expectedKeyDown)
|
||||
{
|
||||
List<(int Key, bool IsDown)> events = [];
|
||||
var hook = new LowLevelKeyboardHook((key, isDown) =>
|
||||
{
|
||||
events.Add((key, isDown));
|
||||
return false;
|
||||
});
|
||||
|
||||
using (hook)
|
||||
{
|
||||
Send(hook, HcAction, message, CapsLock, flags: 0);
|
||||
}
|
||||
|
||||
Assert.Equal([(CapsLock, expectedKeyDown)], events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_swallowed_event_goes_no_further()
|
||||
{
|
||||
using var hook = new LowLevelKeyboardHook(static (_, _) => true);
|
||||
|
||||
IntPtr result = Send(hook, HcAction, WmKeyDown, CapsLock, flags: 0);
|
||||
|
||||
// A non-zero answer breaks the chain: neither the application nor the
|
||||
// case handler in Windows will see the event
|
||||
Assert.Equal(new IntPtr(1), result);
|
||||
}
|
||||
|
||||
// Synthetic input comes from on-screen keyboards and automation tools
|
||||
[Fact]
|
||||
public void Synthetic_input_is_not_intercepted()
|
||||
{
|
||||
List<int> keys = [];
|
||||
using var hook = new LowLevelKeyboardHook((key, _) =>
|
||||
{
|
||||
keys.Add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
IntPtr result = Send(hook, HcAction, WmKeyDown, CapsLock, Injected);
|
||||
|
||||
Assert.Empty(keys);
|
||||
Assert.NotEqual(new IntPtr(1), result);
|
||||
}
|
||||
|
||||
// Windows asks for events below zero not to be inspected but simply passed on
|
||||
[Fact]
|
||||
public void Events_not_meant_for_inspection_are_passed_on()
|
||||
{
|
||||
List<int> keys = [];
|
||||
using var hook = new LowLevelKeyboardHook((key, _) =>
|
||||
{
|
||||
keys.Add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
Send(hook, code: -1, WmKeyDown, CapsLock, flags: 0);
|
||||
|
||||
Assert.Empty(keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_other_messages_are_not_shown_to_the_handler()
|
||||
{
|
||||
List<int> keys = [];
|
||||
using var hook = new LowLevelKeyboardHook((key, _) =>
|
||||
{
|
||||
keys.Add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
Send(hook, HcAction, WmMouseMove, CapsLock, flags: 0);
|
||||
|
||||
Assert.Empty(keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_handler_sees_the_code_of_the_pressed_key()
|
||||
{
|
||||
List<int> keys = [];
|
||||
using var hook = new LowLevelKeyboardHook((key, _) =>
|
||||
{
|
||||
keys.Add(key);
|
||||
return false;
|
||||
});
|
||||
|
||||
Send(hook, HcAction, WmKeyDown, virtualKey: 0x41, flags: 0);
|
||||
Send(hook, HcAction, WmKeyDown, virtualKey: 0x1B, flags: 0);
|
||||
|
||||
Assert.Equal([0x41, 0x1B], keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_interception_is_installed_and_removed()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var hook = new LowLevelKeyboardHook(static (_, _) => false);
|
||||
|
||||
Assert.False(hook.IsInstalled);
|
||||
|
||||
Assert.True(hook.Install());
|
||||
Assert.True(hook.IsInstalled);
|
||||
|
||||
hook.Uninstall();
|
||||
Assert.False(hook.IsInstalled);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Installing_again_changes_nothing()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var hook = new LowLevelKeyboardHook(static (_, _) => false);
|
||||
|
||||
Assert.True(hook.Install());
|
||||
Assert.True(hook.Install());
|
||||
Assert.True(hook.IsInstalled);
|
||||
|
||||
hook.Uninstall();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Removing_without_installing_passes_silently()
|
||||
{
|
||||
var hook = new LowLevelKeyboardHook(static (_, _) => false);
|
||||
|
||||
hook.Uninstall();
|
||||
hook.Uninstall();
|
||||
|
||||
Assert.False(hook.IsInstalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Closing_removes_the_interception()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var hook = new LowLevelKeyboardHook(static (_, _) => false);
|
||||
hook.Install();
|
||||
|
||||
hook.Dispose();
|
||||
|
||||
Assert.False(hook.IsInstalled);
|
||||
});
|
||||
}
|
||||
|
||||
// The event arrives from Windows as a structure in unmanaged memory
|
||||
private static IntPtr Send(
|
||||
LowLevelKeyboardHook hook, int code, int message, int virtualKey, uint flags)
|
||||
{
|
||||
// vkCode, scanCode, flags and time take four bytes each, then a pointer
|
||||
const int Size = 24;
|
||||
IntPtr data = Marshal.AllocHGlobal(Size);
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.WriteInt32(data, 0, virtualKey);
|
||||
Marshal.WriteInt32(data, 4, 0);
|
||||
Marshal.WriteInt32(data, 8, (int)flags);
|
||||
Marshal.WriteInt32(data, 12, 0);
|
||||
Marshal.WriteIntPtr(data, 16, IntPtr.Zero);
|
||||
|
||||
MethodInfo handler = typeof(LowLevelKeyboardHook)
|
||||
.GetMethod("OnHookEvent", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
return (IntPtr)handler.Invoke(hook, [code, new IntPtr(message), data])!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
|
||||
namespace CursorLang.Tests.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// The Win32 wrappers: what is checked is that the calls are put together
|
||||
/// right — structures of the expected size, flags in place, and the answers
|
||||
/// of the system read correctly.
|
||||
/// </summary>
|
||||
public sealed class NativeWrappersTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_cursor_position_is_read()
|
||||
{
|
||||
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
|
||||
|
||||
// The virtual screen may run into negative coordinates, but not beyond
|
||||
// reason: a misread structure would give garbage
|
||||
Assert.InRange(cursor.X, -32_000, 32_000);
|
||||
Assert.InRange(cursor.Y, -32_000, 32_000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_scale_of_the_monitor_under_the_cursor_is_positive()
|
||||
{
|
||||
double scale = PopupWindowNative.GetScaleAt(PopupWindowNative.GetCursorPosition());
|
||||
|
||||
Assert.InRange(scale, 0.5, 8.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_work_area_of_the_active_monitor_is_not_empty()
|
||||
{
|
||||
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
||||
|
||||
Assert.True(work.Right > work.Left);
|
||||
Assert.True(work.Bottom > work.Top);
|
||||
Assert.InRange(scale, 0.5, 8.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_bounds_are_read_from_the_system()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
PopupWindowNative.Rect? bounds = WindowPlacementNative.TryGetBounds(window.Handle);
|
||||
|
||||
Assert.NotNull(bounds);
|
||||
Assert.True(bounds.Value.Right > bounds.Value.Left);
|
||||
Assert.True(bounds.Value.Bottom > bounds.Value.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_window_that_does_not_exist_has_no_bounds()
|
||||
{
|
||||
Assert.Null(WindowPlacementNative.TryGetBounds(IntPtr.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_window_is_moved_to_the_given_point()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
PopupWindowNative.MoveTo(window.Handle, 120, 90);
|
||||
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
||||
Assert.Equal(120, bounds.Left);
|
||||
Assert.Equal(90, bounds.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Moving_does_not_change_the_window_size()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
PopupWindowNative.Rect before = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
||||
PopupWindowNative.MoveTo(window.Handle, 200, 150);
|
||||
PopupWindowNative.Rect after = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
||||
|
||||
Assert.Equal(before.Right - before.Left, after.Right - after.Left);
|
||||
Assert.Equal(before.Bottom - before.Top, after.Bottom - after.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_bounds_are_set_as_a_whole()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
PopupWindowNative.SetBounds(window.Handle, 60, 70, 320, 240);
|
||||
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
||||
Assert.Equal(60, bounds.Left);
|
||||
Assert.Equal(70, bounds.Top);
|
||||
Assert.Equal(380, bounds.Right);
|
||||
Assert.Equal(310, bounds.Bottom);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_work_area_is_found_by_the_rectangle_of_the_window()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(window.Handle)!.Value;
|
||||
|
||||
PopupWindowNative.Rect? work = WindowPlacementNative.TryGetWorkAreaNear(bounds);
|
||||
|
||||
Assert.NotNull(work);
|
||||
Assert.True(work.Value.Right > work.Value.Left);
|
||||
Assert.True(work.Value.Bottom > work.Value.Top);
|
||||
});
|
||||
}
|
||||
|
||||
// The nearest monitor is picked, so an area is found even for a point far off screen
|
||||
[Fact]
|
||||
public void For_a_rectangle_off_every_screen_the_nearest_monitor_is_taken()
|
||||
{
|
||||
var far = new PopupWindowNative.Rect { Left = 30_000, Top = 30_000, Right = 30_100, Bottom = 30_100 };
|
||||
|
||||
PopupWindowNative.Rect? work = WindowPlacementNative.TryGetWorkAreaNear(far);
|
||||
|
||||
Assert.NotNull(work);
|
||||
Assert.True(work.Value.Right > work.Value.Left);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_window_becomes_invisible_to_the_focus_and_the_switcher()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
PopupWindowNative.MakePassive(window.Handle);
|
||||
|
||||
// Checked through the same wrapper: the style has to stick and not
|
||||
// be reset by a repeated call
|
||||
PopupWindowNative.MakePassive(window.Handle);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_layout_of_the_foreground_window_is_read()
|
||||
{
|
||||
int localeId = KeyboardLayoutNative.GetActiveLocaleId();
|
||||
|
||||
// The low word of the HKL is the locale identifier, and it is never zero
|
||||
Assert.NotEqual(0, localeId);
|
||||
Assert.InRange(localeId, 1, 0xFFFF);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_layout_is_read_for_any_window()
|
||||
{
|
||||
int localeId = KeyboardLayoutNative.GetLocaleIdOf(KeyboardLayoutNative.GetForegroundWindow());
|
||||
|
||||
Assert.InRange(localeId, 0, 0xFFFF);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_input_state_of_the_foreground_is_read()
|
||||
{
|
||||
bool received = ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info);
|
||||
|
||||
if (received)
|
||||
{
|
||||
// The structure size is filled in by the wrapper itself, and it has
|
||||
// to match what Windows expects
|
||||
Assert.Equal(System.Runtime.InteropServices.Marshal.SizeOf<ForegroundInputNative.GuiThreadInfo>(),
|
||||
info.cbSize);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_right_to_show_a_window_is_given_away_without_errors()
|
||||
{
|
||||
ForegroundPermissionNative.GrantToAnyProcess();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_title_bar_is_repainted()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var window = new HandleWindow();
|
||||
|
||||
WindowThemeNative.SetDarkTitleBar(window.Handle, isDark: true);
|
||||
WindowThemeNative.SetDarkTitleBar(window.Handle, isDark: false);
|
||||
});
|
||||
}
|
||||
|
||||
// The window may be gone by the time of the repaint
|
||||
[Fact]
|
||||
public void Repainting_a_window_that_does_not_exist_passes_silently()
|
||||
{
|
||||
WindowThemeNative.SetDarkTitleBar(IntPtr.Zero, isDark: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_package_flag_is_computed_once_and_does_not_change()
|
||||
{
|
||||
bool first = PackageIdentityNative.IsPackaged;
|
||||
|
||||
Assert.Equal(first, PackageIdentityNative.IsPackaged);
|
||||
}
|
||||
|
||||
/// <summary>A window with a created handle that never appears on screen.</summary>
|
||||
private sealed class HandleWindow : IDisposable
|
||||
{
|
||||
private readonly Window _window;
|
||||
|
||||
internal HandleWindow()
|
||||
{
|
||||
_window = new Window
|
||||
{
|
||||
Width = 300,
|
||||
Height = 200,
|
||||
ShowInTaskbar = false,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
};
|
||||
|
||||
Handle = new WindowInteropHelper(_window).EnsureHandle();
|
||||
}
|
||||
|
||||
internal IntPtr Handle { get; }
|
||||
|
||||
public void Dispose() => _window.Close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user