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); } } } }