using System.Collections.Concurrent;
using CursorLang.Core.Models;
using CursorLang.Core.Services;
using CursorLang.Tests.Shared;
namespace CursorLang.Core.Tests.Services;
///
/// 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.
///
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;
private const int German = 0x0407;
[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);
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);
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);
Pump.Run(service.Poll);
Assert.Single(world.Changes);
}
///
/// A value the switch is only passing through never reaches the subscribers.
///
///
/// A layout change is rarely a single step: the Windows language switcher takes the
/// focus while it is up, and applications with a rendering engine of their own move
/// a helper thread first. Reporting what polling caught in between meant the popup
/// coming up with one layout and turning into another in front of the user.
///
[Fact]
public void A_layout_the_switch_only_passes_through_is_never_reported()
{
using var world = new World { LocaleId = English };
KeyboardLayoutService service = world.CreateService();
Pump.Run(service.Start);
world.LocaleId = German;
Pump.Run(service.Poll);
Assert.Empty(world.Changes);
world.LocaleId = Russian;
Pump.Run(service.Poll);
Pump.Run(service.Poll);
LayoutChangedEventArgs change = Assert.Single(world.Changes);
Assert.Equal("RU", change.Layout.ShortName);
}
// A switch that goes out and comes back is not a switch at all
[Fact]
public void A_layout_that_returns_to_where_it_was_yields_no_events()
{
using var world = new World { LocaleId = English };
KeyboardLayoutService service = world.CreateService();
Pump.Run(service.Start);
world.LocaleId = Russian;
Pump.Run(service.Poll);
world.LocaleId = English;
Pump.Run(service.Poll);
Pump.Run(service.Poll);
Assert.Empty(world.Changes);
}
///
/// A window the switch passes through does not make it an application switch.
///
///
/// The Windows language switcher is a window of its own and holds the foreground
/// while it is up. Judging by the window seen on the previous tick would call that
/// an application switch and swallow the popup, so the judgement is made against
/// the window that was there before the whole transition started.
///
[Fact]
public void A_window_the_switch_passes_through_is_not_an_application_switch()
{
using var world = new World { LocaleId = English };
KeyboardLayoutService service = world.CreateService();
Pump.Run(service.Start);
world.ForegroundWindow = SecondWindow;
world.LocaleId = German;
Pump.Run(service.Poll);
world.ForegroundWindow = FirstWindow;
world.LocaleId = Russian;
Pump.Run(service.Poll);
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 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);
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));
}
///
/// The state of the system as the service sees it, and everything the
/// service reported about it.
///
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 Changes { get; } = new();
internal KeyboardLayoutService CreateService(TimeSpan? pollInterval = null)
{
TimeSpan interval = pollInterval ?? TimeSpan.FromHours(1);
var options = new KeyboardLayoutOptions
{
PollInterval = interval,
SettleInterval = interval,
};
_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);
}
}
}
}