using Xunit.Abstractions;
namespace ApplicationTests.Timeout;
///
/// Tests for timeout behavior with nested Application.Run() calls.
/// These tests verify that timeouts scheduled in a parent run loop continue to fire
/// correctly when a nested modal dialog is shown via Application.Run().
///
public class NestedRunTimeoutTests (ITestOutputHelper output)
{
[Fact]
public void Multiple_Timeouts_Fire_In_Correct_Order_With_Nested_Run ()
{
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
List executionOrder = new ();
var mainWindow = new Window { Title = "Main Window" };
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
var nestedRunCompleted = false;
// Use iteration counter for safety instead of time-based timeout
var iterations = 0;
app.Iteration += IterationHandler;
try
{
// Schedule multiple timeouts
app.AddTimeout (
TimeSpan.FromMilliseconds (100),
() =>
{
executionOrder.Add ("Timeout1-100ms");
output.WriteLine ("Timeout1 fired at 100ms");
return false;
}
);
app.AddTimeout (
TimeSpan.FromMilliseconds (200),
() =>
{
executionOrder.Add ("Timeout2-200ms-StartNestedRun");
output.WriteLine ("Timeout2 fired at 200ms - Starting nested run");
// Start nested run
app.Run (dialog);
executionOrder.Add ("Timeout2-NestedRunEnded");
nestedRunCompleted = true;
output.WriteLine ("Nested run ended");
return false;
}
);
app.AddTimeout (
TimeSpan.FromMilliseconds (300),
() =>
{
executionOrder.Add ("Timeout3-300ms-InNestedRun");
output.WriteLine ($"Timeout3 fired at 300ms - TopRunnable: {app.TopRunnableView?.Title}");
// This should fire while dialog is running
Assert.Equal (dialog, app.TopRunnableView);
return false;
}
);
app.AddTimeout (
TimeSpan.FromMilliseconds (400),
() =>
{
executionOrder.Add ("Timeout4-400ms-CloseDialog");
output.WriteLine ("Timeout4 fired at 400ms - Closing dialog");
// Close the dialog
app.RequestStop (dialog);
return false;
}
);
// Event-driven: Only stop main window AFTER nested run completes
// Use a repeating timeout that checks the condition
app.AddTimeout (
TimeSpan.FromMilliseconds (50),
() =>
{
// Keep checking until nested run completes
if (nestedRunCompleted)
{
executionOrder.Add ("Timeout5-AfterNestedRun-StopMain");
output.WriteLine ("Timeout5 fired after nested run completed - Stopping main window");
app.RequestStop (mainWindow);
return false; // Don't repeat
}
return true; // Keep checking
}
);
// Act
app.Run (mainWindow);
// Assert - Verify all timeouts fired in the correct order
output.WriteLine ($"Execution order: {string.Join (", ", executionOrder)}");
Assert.Equal (6, executionOrder.Count); // 5 timeout events + 1 nested run end marker
Assert.Equal ("Timeout1-100ms", executionOrder [0]);
Assert.Equal ("Timeout2-200ms-StartNestedRun", executionOrder [1]);
Assert.Equal ("Timeout3-300ms-InNestedRun", executionOrder [2]);
Assert.Equal ("Timeout4-400ms-CloseDialog", executionOrder [3]);
Assert.Equal ("Timeout2-NestedRunEnded", executionOrder [4]);
Assert.Equal ("Timeout5-AfterNestedRun-StopMain", executionOrder [5]);
}
finally
{
app.Iteration -= IterationHandler;
dialog.Dispose ();
mainWindow.Dispose ();
}
return;
void IterationHandler (object? s, EventArgs e)
{
iterations++;
// Safety limit - should never be hit with event-driven logic
if (iterations > 2000)
{
output.WriteLine ($"SAFETY: Hit iteration limit. Execution order: {string.Join (", ", executionOrder)}");
app.RequestStop ();
}
}
}
[Fact]
public void Timeout_Fires_In_Nested_Run ()
{
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
var timeoutFired = false;
var nestedRunStarted = false;
var nestedRunEnded = false;
// Create a simple window for the main run loop
var mainWindow = new Window { Title = "Main Window" };
// Create a dialog for the nested run loop
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
app.RequestStop ();
return false;
}
);
// Schedule a timeout that will fire AFTER the nested run starts and stop the dialog
app.AddTimeout (
TimeSpan.FromMilliseconds (200),
() =>
{
output.WriteLine ($"DialogRequestStop Timeout fired! TopRunnable: {app.TopRunnableView?.Title ?? "null"}");
timeoutFired = true;
// Close the dialog when timeout fires
if (app.TopRunnableView == dialog)
{
app.RequestStop (dialog);
}
return false;
}
);
// After 100ms, start the nested run loop
app.AddTimeout (
TimeSpan.FromMilliseconds (100),
() =>
{
output.WriteLine ("Starting nested run...");
nestedRunStarted = true;
// This blocks until the dialog is closed (by the timeout at 200ms)
app.Run (dialog);
output.WriteLine ("Nested run ended");
nestedRunEnded = true;
// Stop the main window after nested run completes
app.RequestStop ();
return false;
}
);
// Act - Start the main run loop
app.Run (mainWindow);
// Assert
Assert.True (nestedRunStarted, "Nested run should have started");
Assert.True (timeoutFired, "Timeout should have fired during nested run");
Assert.True (nestedRunEnded, "Nested run should have ended");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
dialog.Dispose ();
mainWindow.Dispose ();
}
[Fact]
public void Timeout_Fires_With_Single_Session ()
{
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
// Create a simple window for the main run loop
var mainWindow = new Window { Title = "Main Window" };
// Schedule a timeout that will ensure the app quits
var requestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (100),
() =>
{
output.WriteLine ("RequestStop Timeout fired!");
requestStopTimeoutFired = true;
app.RequestStop ();
return false;
}
);
// Act - Start the main run loop
app.Run (mainWindow);
// Assert
Assert.True (requestStopTimeoutFired, "RequestStop Timeout should have fired");
mainWindow.Dispose ();
}
[Fact]
public void Timeout_Queue_Persists_Across_Nested_Runs ()
{
// Verify that the timeout queue is not cleared when nested runs start/end
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
app.RequestStop ();
return false;
}
);
var mainWindow = new Window { Title = "Main Window" };
var dialog = new Dialog { Title = "Dialog", Buttons = [new() { Text = "Ok" }] };
var initialTimeoutCount = 0;
var timeoutCountDuringNestedRun = 0;
var timeoutCountAfterNestedRun = 0;
// Schedule 5 timeouts at different times with wider spacing
for (var i = 0; i < 5; i++)
{
int capturedI = i;
app.AddTimeout (
TimeSpan.FromMilliseconds (150 * (i + 1)), // Increased spacing from 100ms to 150ms
() =>
{
output.WriteLine ($"Timeout {capturedI} fired at {150 * (capturedI + 1)}ms");
if (capturedI == 0)
{
initialTimeoutCount = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Initial timeout count: {initialTimeoutCount}");
}
if (capturedI == 1)
{
// Start nested run
output.WriteLine ("Starting nested run");
app.Run (dialog);
output.WriteLine ("Nested run ended");
timeoutCountAfterNestedRun = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Timeout count after nested run: {timeoutCountAfterNestedRun}");
}
if (capturedI == 2)
{
// This fires during nested run
timeoutCountDuringNestedRun = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Timeout count during nested run: {timeoutCountDuringNestedRun}");
// Close dialog
app.RequestStop (dialog);
}
if (capturedI == 4)
{
// Stop main window
app.RequestStop (mainWindow);
}
return false;
}
);
}
// Act
app.Run (mainWindow);
// Assert
output.WriteLine ($"Final counts - Initial: {initialTimeoutCount}, During: {timeoutCountDuringNestedRun}, After: {timeoutCountAfterNestedRun}");
// The timeout queue should have pending timeouts throughout
Assert.True (initialTimeoutCount >= 0, "Should have timeouts in queue initially");
Assert.True (timeoutCountDuringNestedRun >= 0, "Should have timeouts in queue during nested run");
Assert.True (timeoutCountAfterNestedRun >= 0, "Should have timeouts in queue after nested run");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
dialog.Dispose ();
mainWindow.Dispose ();
}
[Fact]
public void Timeout_Scheduled_Before_Nested_Run_Fires_During_Nested_Run ()
{
// This test specifically reproduces the ESC key issue scenario:
// - Timeouts are scheduled upfront (like demo keys)
// - A timeout fires and triggers a nested run (like Enter opening MessageBox)
// - A subsequent timeout should still fire during the nested run (like ESC closing MessageBox)
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
var enterFired = false;
var escFired = false;
var messageBoxShown = false;
var messageBoxClosed = false;
var mainWindow = new Window { Title = "Login Window" };
var messageBox = new Dialog { Title = "Success", Buttons = [new() { Text = "Ok" }] };
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
app.RequestStop ();
return false;
}
);
// Schedule "Enter" timeout at 100ms
app.AddTimeout (
TimeSpan.FromMilliseconds (100),
() =>
{
output.WriteLine ("Enter timeout fired - showing MessageBox");
enterFired = true;
// Simulate Enter key opening MessageBox
messageBoxShown = true;
app.Run (messageBox);
messageBoxClosed = true;
output.WriteLine ("MessageBox closed");
return false;
}
);
// Schedule "ESC" timeout at 200ms (should fire while MessageBox is running)
app.AddTimeout (
TimeSpan.FromMilliseconds (200),
() =>
{
output.WriteLine ($"ESC timeout fired - TopRunnable: {app.TopRunnableView?.Title}");
escFired = true;
// Simulate ESC key closing MessageBox
if (app.TopRunnableView == messageBox)
{
output.WriteLine ("Closing MessageBox with ESC");
app.RequestStop (messageBox);
}
return false;
}
);
// Increased delay from 300ms to 500ms to ensure nested run completes before stopping main
app.AddTimeout (
TimeSpan.FromMilliseconds (500),
() =>
{
output.WriteLine ("Stopping main window");
app.RequestStop (mainWindow);
return false;
}
);
// Act
app.Run (mainWindow);
// Assert
Assert.True (enterFired, "Enter timeout should have fired");
Assert.True (messageBoxShown, "MessageBox should have been shown");
Assert.True (escFired, "ESC timeout should have fired during MessageBox"); // THIS WAS THE BUG - NOW FIXED!
Assert.True (messageBoxClosed, "MessageBox should have been closed");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
messageBox.Dispose ();
mainWindow.Dispose ();
}
}