fix bug - popup windows show hiding logic
Pull request / build (pull_request) Successful in 39s

This commit is contained in:
2026-08-12 18:12:08 +05:00
parent 6259dbd6b3
commit 87266e6c13
13 changed files with 503 additions and 442 deletions
@@ -17,7 +17,6 @@ public sealed class KeyboardLayoutServiceTests
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()
@@ -52,7 +51,6 @@ public sealed class KeyboardLayoutServiceTests
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);
@@ -70,7 +68,6 @@ public sealed class KeyboardLayoutServiceTests
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);
@@ -125,91 +122,10 @@ public sealed class KeyboardLayoutServiceTests
// 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);
}
/// <summary>
/// A value the switch is only passing through never reaches the subscribers.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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);
}
/// <summary>
/// A window the switch passes through does not make it an application switch.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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()
{
@@ -221,7 +137,6 @@ public sealed class KeyboardLayoutServiceTests
world.LocaleId = Russian;
Pump.Run(service.Poll);
Pump.Run(service.Poll);
Pump.Run(service.Poll);
Assert.Single(world.Changes);
}
@@ -344,11 +259,9 @@ public sealed class KeyboardLayoutServiceTests
internal KeyboardLayoutService CreateService(TimeSpan? pollInterval = null)
{
TimeSpan interval = pollInterval ?? TimeSpan.FromHours(1);
var options = new KeyboardLayoutOptions
{
PollInterval = interval,
SettleInterval = interval,
PollInterval = pollInterval ?? TimeSpan.FromHours(1),
};
_service = Pump.Run(() => new KeyboardLayoutService(
@@ -10,6 +10,7 @@ namespace CursorLang.Core.Tests.Services;
public sealed class LayoutNotificationCoordinatorTests
{
private static readonly KeyboardLayout Russian = KeyboardLayout.FromLocaleId(0x0419);
private static readonly KeyboardLayout English = KeyboardLayout.FromLocaleId(0x0409);
[Fact]
public void Starting_turns_on_the_layout_watch()
@@ -0,0 +1,129 @@
using CursorLang.Core.Threading;
using CursorLang.Tests.Shared;
namespace CursorLang.Core.Tests.Threading;
/// <summary>
/// The timer the agent has instead of a dispatcher timer.
/// </summary>
/// <remarks>
/// It ticks on the message loop, so everything here runs on the pump thread — a timer
/// started on one thread and awaited on another would never be seen to fire.
/// </remarks>
public sealed class MessageTimerTests
{
[Fact]
public void A_started_timer_ticks()
{
int ticks = 0;
Pump.Run(() =>
{
using var timer = new MessageTimer();
timer.Interval = TimeSpan.FromMilliseconds(15);
timer.Tick += (_, _) => ticks++;
timer.Start();
Pump.WaitFor(() => ticks > 0, "the timer ticked");
});
}
[Fact]
public void A_timer_keeps_ticking_until_it_is_stopped()
{
var ticks = 0;
Pump.Run(() =>
{
using var timer = new MessageTimer();
timer.Interval = TimeSpan.FromMilliseconds(15);
timer.Tick += (_, _) => ticks++;
timer.Start();
Pump.WaitFor(() => ticks >= 3, "the timer ticked more than once");
});
}
// A stopped countdown does not go off, however long the loop runs afterwards
[Fact]
public void A_stopped_timer_does_not_tick()
{
var ticks = 0;
Pump.Run(() =>
{
using var timer = new MessageTimer();
timer.Interval = TimeSpan.FromMilliseconds(10);
timer.Tick += (_, _) => ticks++;
timer.Start();
timer.Stop();
Pump.Pause(TimeSpan.FromMilliseconds(80));
});
Assert.Equal(0, ticks);
}
// Restarting means from zero, so a countdown kept short by repeated restarts
// never reaches its end
[Fact]
public void Restarting_begins_the_countdown_again()
{
var ticks = 0;
Pump.Run(() =>
{
using var timer = new MessageTimer();
timer.Interval = TimeSpan.FromMilliseconds(60);
timer.Tick += (_, _) => ticks++;
for (var i = 0; i < 6; i++)
{
timer.Start();
Pump.Pause(TimeSpan.FromMilliseconds(20));
}
Assert.Equal(0, ticks);
Pump.WaitFor(() => ticks > 0, "left alone, the timer reached its end");
});
}
[Fact]
public void Closing_the_timer_ends_the_ticking()
{
var ticks = 0;
Pump.Run(() =>
{
var timer = new MessageTimer { Interval = TimeSpan.FromMilliseconds(15) };
timer.Tick += (_, _) => ticks++;
timer.Start();
Pump.WaitFor(() => ticks > 0, "the timer ticked");
timer.Dispose();
int seen = ticks;
Pump.Pause(TimeSpan.FromMilliseconds(80));
Assert.Equal(seen, ticks);
});
}
[Fact]
public void A_timer_that_was_never_started_says_so()
{
Pump.Run(() =>
{
using var timer = new MessageTimer();
Assert.False(timer.IsRunning);
timer.Start();
Assert.True(timer.IsRunning);
timer.Stop();
Assert.False(timer.IsRunning);
});
}
}