287 lines
8.2 KiB
C#
287 lines
8.2 KiB
C#
using System.Collections.Concurrent;
|
|
using CursorLang.Core.Models;
|
|
using CursorLang.Core.Services;
|
|
using CursorLang.Tests.Shared;
|
|
|
|
namespace CursorLang.Core.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// Watching the layout of the foreground window. The test supplies what the
|
|
/// system reports: what is under test is the decision about what counts as
|
|
/// a layout change.
|
|
/// </summary>
|
|
public sealed class KeyboardLayoutServiceTests
|
|
{
|
|
private static readonly IntPtr FirstWindow = new(1000);
|
|
private static readonly IntPtr SecondWindow = new(2000);
|
|
|
|
private const int English = 0x0409;
|
|
private const int Russian = 0x0419;
|
|
|
|
[Fact]
|
|
public void The_current_layout_is_taken_from_the_foreground_window()
|
|
{
|
|
using var world = new World { LocaleId = Russian };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Assert.Equal("RU", service.Current.ShortName);
|
|
|
|
world.LocaleId = English;
|
|
Assert.Equal("EN", service.Current.ShortName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Switching_asks_the_system_to_change_the_layout()
|
|
{
|
|
using var world = new World();
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
service.SwitchToNext();
|
|
service.SwitchToNext();
|
|
|
|
Assert.Equal(2, world.SwitchRequests);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_layout_change_in_the_same_window_counts_as_the_users_doing()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
world.LocaleId = Russian;
|
|
Pump.Run(service.Poll);
|
|
|
|
LayoutChangedEventArgs change = Assert.Single(world.Changes);
|
|
Assert.Equal(LayoutChangeReason.UserSwitched, change.Reason);
|
|
Assert.Equal("RU", change.Layout.ShortName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Moving_to_another_application_differs_from_switching()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
|
|
world.ForegroundWindow = SecondWindow;
|
|
world.LocaleId = Russian;
|
|
Pump.Run(service.Poll);
|
|
|
|
LayoutChangedEventArgs change = Assert.Single(world.Changes);
|
|
Assert.Equal(LayoutChangeReason.ApplicationSwitched, change.Reason);
|
|
}
|
|
|
|
// Moving to an application with the same layout changes nothing
|
|
[Fact]
|
|
public void Moving_without_a_layout_change_yields_no_events()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
|
|
world.ForegroundWindow = SecondWindow;
|
|
Pump.Run(service.Poll);
|
|
|
|
Assert.Empty(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void An_unchanged_layout_yields_no_events()
|
|
{
|
|
using var world = new World { LocaleId = Russian };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
Pump.Run(service.Poll);
|
|
}
|
|
|
|
Assert.Empty(world.Changes);
|
|
}
|
|
|
|
// There is no foreground window — during a desktop switch, for one
|
|
[Fact]
|
|
public void Without_a_foreground_window_the_poll_is_skipped()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
|
|
world.ForegroundWindow = IntPtr.Zero;
|
|
world.LocaleId = Russian;
|
|
Pump.Run(service.Poll);
|
|
|
|
Assert.Empty(world.Changes);
|
|
|
|
// And the layout was not remembered: the change is noticed once a window is back
|
|
world.ForegroundWindow = FirstWindow;
|
|
Pump.Run(service.Poll);
|
|
|
|
Assert.Single(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void One_change_yields_exactly_one_event()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
Pump.Run(service.Start);
|
|
|
|
world.LocaleId = Russian;
|
|
Pump.Run(service.Poll);
|
|
Pump.Run(service.Poll);
|
|
|
|
Assert.Single(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_layout_change_before_the_watch_starts_goes_unnoticed()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService();
|
|
|
|
world.LocaleId = Russian;
|
|
Pump.Run(service.Start);
|
|
Pump.Run(service.Poll);
|
|
|
|
// Start remembered the layout that was in place at that moment
|
|
Assert.Empty(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_watch_runs_on_a_timer()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService(TimeSpan.FromMilliseconds(15));
|
|
|
|
Pump.Run(service.Start);
|
|
world.LocaleId = Russian;
|
|
|
|
Pump.WaitFor(() => !world.Changes.IsEmpty, "the timer noticed the layout change");
|
|
}
|
|
|
|
[Fact]
|
|
public void Stopping_ends_the_polling()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService(TimeSpan.FromMilliseconds(15));
|
|
|
|
Pump.Run(service.Start);
|
|
Pump.Run(service.Stop);
|
|
|
|
world.LocaleId = Russian;
|
|
Pump.Pause(TimeSpan.FromMilliseconds(120));
|
|
|
|
Assert.Empty(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Closing_ends_the_polling()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService(TimeSpan.FromMilliseconds(15));
|
|
|
|
Pump.Run(service.Start);
|
|
Pump.Run(service.Dispose);
|
|
|
|
world.LocaleId = Russian;
|
|
Pump.Pause(TimeSpan.FromMilliseconds(120));
|
|
|
|
Assert.Empty(world.Changes);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_watch_can_be_resumed()
|
|
{
|
|
using var world = new World { LocaleId = English };
|
|
KeyboardLayoutService service = world.CreateService(TimeSpan.FromMilliseconds(15));
|
|
|
|
Pump.Run(service.Start);
|
|
Pump.Run(service.Stop);
|
|
Pump.Run(service.Start);
|
|
|
|
world.LocaleId = Russian;
|
|
|
|
Pump.WaitFor(() => !world.Changes.IsEmpty, "the watch resumed");
|
|
}
|
|
|
|
// The ordinary service asks Windows itself about the layout
|
|
[Fact]
|
|
public void The_service_can_work_with_the_real_system()
|
|
{
|
|
KeyboardLayoutService service = Pump.Run(() =>
|
|
new KeyboardLayoutService(new KeyboardLayoutOptions { PollInterval = TimeSpan.FromHours(1) }));
|
|
|
|
try
|
|
{
|
|
Pump.Run(service.Start);
|
|
Pump.Run(service.Poll);
|
|
|
|
Assert.InRange(service.Current.LocaleId, 1, 0xFFFF);
|
|
|
|
Pump.Run(service.Stop);
|
|
}
|
|
finally
|
|
{
|
|
Pump.Run(service.Dispose);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void By_default_the_poll_runs_more_than_six_times_a_second()
|
|
{
|
|
// Any rarer and the tooltip would visibly lag behind the keystroke
|
|
Assert.True(new KeyboardLayoutOptions().PollInterval <= TimeSpan.FromMilliseconds(150));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The state of the system as the service sees it, and everything the
|
|
/// service reported about it.
|
|
/// </summary>
|
|
private sealed class World : IDisposable
|
|
{
|
|
private KeyboardLayoutService? _service;
|
|
|
|
internal IntPtr ForegroundWindow { get; set; } = FirstWindow;
|
|
|
|
internal int LocaleId { get; set; } = English;
|
|
|
|
internal int SwitchRequests { get; private set; }
|
|
|
|
internal ConcurrentQueue<LayoutChangedEventArgs> Changes { get; } = new();
|
|
|
|
internal KeyboardLayoutService CreateService(TimeSpan? pollInterval = null)
|
|
{
|
|
var options = new KeyboardLayoutOptions
|
|
{
|
|
PollInterval = pollInterval ?? TimeSpan.FromHours(1),
|
|
};
|
|
|
|
_service = Pump.Run(() => new KeyboardLayoutService(
|
|
options,
|
|
() => ForegroundWindow,
|
|
() => LocaleId,
|
|
() => SwitchRequests++));
|
|
|
|
_service.LayoutChanged += (_, e) => Changes.Enqueue(e);
|
|
|
|
return _service;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_service is not null)
|
|
{
|
|
Pump.Run(_service.Dispose);
|
|
}
|
|
}
|
|
}
|
|
}
|