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; [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)); } /// /// 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) { 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); } } } }