Forráskód Böngészése

Fixes #4022 file dialog tests and bugfix for cancellation (#4024)

* Add class for detecting information about console in extensible way

* WIP - Create test for reordering

* Change Dictionary to List and preserve TreeBuilder order

* Add test to ensure branch expansion/status remains consistent despite reorder

* Cleanup code

* Fix regression when removed child was the selected one

* Revert "Add class for detecting information about console in extensible way"

This reverts commit 7e4253cf28428ea80a4773b137d4bd89cf321746.

* Code cleanup and enable nullable on Branch

* Remove color scheme and driver from Branch draw

* Add xunit context extensions

* Investigate codegen for xunit

* Getting closer to something that works

* Fix code generation

* Further explore code gen

* Generate all methods in single class for easier extensibility

* Simplify code gen by moving parameter creation to its own method

* Implement asserts A-I

* Add remaining assert calls that are not obsolete

* Fix unit test

* Roll back versions to be compatible with CI version of csharp

* Handle params and ref etc

* Fix null warning

* WIP - start to add integration tests for FileDialog

* Add ability to tab focus to specific control with simple one line delegate

* Clarify test criteria

* Add unit tests for Ok and other ways of canceling dialog

* Fix other buttons also triggering save

* Fix for linux environment tests

* Fix for linux again

* Fix application null race condition - add better way of knowing if stuff is finished

* Better fix for shutdown detection

* Add test that shows #4026 is not an issue

* Switch to `_fileSystem.Directory.GetLogicalDrives ()`

* Don't show duplicate MyDocuments etc
Thomas Nind 4 hónapja
szülő
commit
eaa9ee1ef6

+ 43 - 50
Directory.Packages.props

@@ -1,52 +1,45 @@
 <Project>
-	<PropertyGroup>
-		<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
-	</PropertyGroup>
-	<ItemGroup>
-		<!-- Enable Nuget Source Link for github -->
-		<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
-
-		<PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
-		<PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.13,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.13,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.13,5)" />
-		<PackageVersion Include="Microsoft.Extensions.Logging" Version="[9.0.2,10)" />
-		<PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
-		<PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
-		<PackageVersion Include="Wcwidth" Version="[2,3)" />
-
-		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
-		<PackageVersion Include="Serilog" Version="4.2.0" />
-		<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
-		<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
-		<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
-		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.7,4)" />
-		<PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
-		<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
-		<PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />
-
-		<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
-
-		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.4.0,9)" />
-		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[9.0.2,10)" />	
-		<PackageVersion Include="ReactiveUI" Version="[20.1.63,21)" />
-		<PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" />
-		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[2.1.8,3)"/>	
-
-		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.13,18)" />
-		<PackageVersion Include="Moq" Version="[4.20.72,5)" />
-		<PackageVersion Include="ReportGenerator" Version="[5.4.4,6)" />
-		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[22.0.11,23)" />
-		<PackageVersion Include="xunit" Version="[2.9.3,3)" />
-		<PackageVersion Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
-		<PackageVersion Include="xunit.runner.visualstudio" Version="[2.8.2,3)"/> 
-		<PackageVersion Include="coverlet.collector" Version="[6.0.4,7)" />
-		
-	</ItemGroup>
-
-	<ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-        <PackageVersion Include="Terminal.Gui" Version="2.0.0" />
-	</ItemGroup>
-
+  <PropertyGroup>
+    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+  </PropertyGroup>
+  <ItemGroup>
+    <!-- Enable Nuget Source Link for github -->
+    <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
+    <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
+    <PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
+    <PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
+    <PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.11,4.12)" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.11,4.12)" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
+    <PackageVersion Include="Microsoft.Extensions.Logging" Version="[9.0.2,10)" />
+    <PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
+    <PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
+    <PackageVersion Include="Wcwidth" Version="[2,3)" />
+    <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
+    <PackageVersion Include="Serilog" Version="4.2.0" />
+    <PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
+    <PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
+    <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
+    <PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.7,4)" />
+    <PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
+    <PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
+    <PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />
+    <PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.4.0,9)" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[9.0.2,10)" />
+    <PackageVersion Include="ReactiveUI" Version="[20.1.63,21)" />
+    <PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" />
+    <PackageVersion Include="ReactiveUI.SourceGenerators" Version="[2.1.8,3)" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.13,18)" />
+    <PackageVersion Include="Moq" Version="[4.20.72,5)" />
+    <PackageVersion Include="ReportGenerator" Version="[5.4.4,6)" />
+    <PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[22.0.11,23)" />
+    <PackageVersion Include="xunit" Version="[2.9.3,3)" />
+    <PackageVersion Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="[2.8.2,3)" />
+    <PackageVersion Include="coverlet.collector" Version="[6.0.4,7)" />
+  </ItemGroup>
+  <ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <PackageVersion Include="Terminal.Gui" Version="2.0.0" />
+  </ItemGroup>
 </Project>

+ 3 - 6
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -151,14 +151,11 @@ public class FileDialogStyle
 
         try
         {
-            foreach (string d in GetLogicalDrives ())
+            foreach (string d in _fileSystem.Directory.GetLogicalDrives ())
             {
                 IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (d);
 
-                if (!roots.ContainsKey (dir))
-                {
-                    roots.Add (dir, d);
-                }
+                roots.TryAdd (dir, d);
             }
         }
         catch (Exception)
@@ -181,7 +178,7 @@ public class FileDialogStyle
 
                     IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (path);
 
-                    if (!roots.ContainsKey (dir) && dir.Exists)
+                    if (!roots.ContainsKey (dir) && !roots.ContainsValue (special.ToString ()) && dir.Exists)
                     {
                         roots.Add (dir, special.ToString ());
                     }

+ 21 - 5
Terminal.Gui/Views/FileDialog.cs

@@ -103,6 +103,8 @@ public class FileDialog : Dialog, IDesignable
                                         return;
                                     }
 
+                                    e.Cancel = true;
+
                                     if (Modal)
                                     {
                                         Application.RequestStop ();
@@ -111,15 +113,27 @@ public class FileDialog : Dialog, IDesignable
 
         _btnUp = new() { X = 0, Y = 1, NoPadding = true };
         _btnUp.Text = GetUpButtonText ();
-        _btnUp.Accepting += (s, e) => _history.Up ();
+        _btnUp.Accepting += (s, e) =>
+                            {
+                                _history.Up ();
+                                e.Cancel = true;
+                            };
 
         _btnBack = new() { X = Pos.Right (_btnUp) + 1, Y = 1, NoPadding = true };
         _btnBack.Text = GetBackButtonText ();
-        _btnBack.Accepting += (s, e) => _history.Back ();
+        _btnBack.Accepting += (s, e) =>
+                              {
+                                  _history.Back ();
+                                  e.Cancel = true;
+                              };
 
         _btnForward = new() { X = Pos.Right (_btnBack) + 1, Y = 1, NoPadding = true };
         _btnForward.Text = GetForwardButtonText ();
-        _btnForward.Accepting += (s, e) => _history.Forward ();
+        _btnForward.Accepting += (s, e) =>
+                                 {
+                                     _history.Forward();
+                                     e.Cancel = true;
+                                 };
 
         _tbPath = new() { Width = Dim.Fill (), CaptionColor = new (Color.Black) };
 
@@ -199,6 +213,8 @@ public class FileDialog : Dialog, IDesignable
 
         _btnToggleSplitterCollapse.Accepting += (s, e) =>
                                                 {
+                                                    // Required otherwise the Save button clicks itself
+                                                    e.Cancel = true;
                                                     Tile tile = _splitContainer.Tiles.ElementAt (0);
 
                                                     bool newState = !tile.ContentView.Visible;
@@ -490,7 +506,7 @@ public class FileDialog : Dialog, IDesignable
         // if no path has been provided
         if (_tbPath.Text.Length <= 0)
         {
-            Path = Environment.CurrentDirectory;
+            Path = _fileSystem.Directory.GetCurrentDirectory ();
         }
 
         // to streamline user experience and allow direct typing of paths
@@ -1288,7 +1304,7 @@ public class FileDialog : Dialog, IDesignable
         // really not what most users would expect
         if (Regex.IsMatch (path, "^\\w:$"))
         {
-            return _fileSystem.DirectoryInfo.New (path + System.IO.Path.DirectorySeparatorChar);
+            return _fileSystem.DirectoryInfo.New (path + _fileSystem.Path.DirectorySeparatorChar);
         }
 
         return _fileSystem.DirectoryInfo.New (path);

+ 9 - 1
Terminal.Gui/Views/SaveDialog.cs

@@ -9,6 +9,7 @@
 //   * Use a line separator to show the file listing, so we can use same colors as the rest
 //   * DirListView: Add mouse support
 
+using System.IO.Abstractions;
 using Terminal.Gui.Resources;
 
 namespace Terminal.Gui;
@@ -24,8 +25,15 @@ namespace Terminal.Gui;
 public class SaveDialog : FileDialog
 {
     /// <summary>Initializes a new <see cref="SaveDialog"/>.</summary>
-    public SaveDialog () { Style.OkButtonText = Strings.btnSave; }
+    public SaveDialog ()
+    {
+        Style.OkButtonText = Strings.btnSave;
+    }
 
+    internal SaveDialog (IFileSystem fileSystem) : base (fileSystem)
+    {
+        Style.OkButtonText = Strings.btnSave;
+    }
     /// <summary>
     ///     Gets the name of the file the user selected for saving, or null if the user canceled the
     ///     <see cref="SaveDialog"/>.

+ 11 - 5
Terminal.sln

@@ -65,7 +65,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable",
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit", "TerminalGuiFluentTestingXunit\TerminalGuiFluentTestingXunit.csproj", "{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit.Generator", "TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj", "{199F27D8-A905-4DDC-82CA-1FE1A90B1788}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -125,10 +127,14 @@ Global
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU
-		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.Build.0 = Release|Any CPU
+		{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 223 - 6
TerminalGuiFluentTesting/GuiTestContext.cs

@@ -1,7 +1,9 @@
-using System.Text;
+using System.Drawing;
+using System.Text;
 using Microsoft.Extensions.Logging;
 using Terminal.Gui;
 using Terminal.Gui.ConsoleDrivers;
+using static Unix.Terminal.Curses;
 
 namespace TerminalGuiFluentTesting;
 
@@ -21,6 +23,7 @@ public class GuiTestContext : IDisposable
     private View? _lastView;
     private readonly StringBuilder _logsSb;
     private readonly V2TestDriver _driver;
+    private bool _finished=false;
 
     internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
     {
@@ -62,7 +65,7 @@ public class GuiTestContext : IDisposable
                                      booting.Release ();
 
                                      Toplevel t = topLevelBuilder ();
-
+                                     t.Closed += (s, e) => { _finished = true; };
                                      Application.Run (t); // This will block, but it's on a background thread now
 
                                      Application.Shutdown ();
@@ -77,6 +80,7 @@ public class GuiTestContext : IDisposable
                                  {
                                      ApplicationImpl.ChangeInstance (origApp);
                                      Logging.Logger = origLogger;
+                                     _finished = true;
                                  }
                              },
                              _cts.Token);
@@ -111,7 +115,7 @@ public class GuiTestContext : IDisposable
             return this;
         }
 
-        Application.Invoke (() => Application.RequestStop ());
+        Application.Invoke (() => {Application.RequestStop ();});
 
         // Wait for the application to stop, but give it a 1-second timeout
         if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
@@ -134,6 +138,15 @@ public class GuiTestContext : IDisposable
         return this;
     }
 
+    /// <summary>
+    ///     Hard stops the application and waits for the background thread to exit.
+    /// </summary>
+    public void HardStop ()
+    {
+        _hardStop.Cancel ();
+        Stop ();
+    }
+
     /// <summary>
     /// Cleanup to avoid state bleed between tests
     /// </summary>
@@ -213,6 +226,12 @@ public class GuiTestContext : IDisposable
     /// <returns></returns>
     public GuiTestContext WaitIteration (Action? a = null)
     {
+        // If application has already exited don't wait!
+        if (_finished || _cts.Token.IsCancellationRequested || _hardStop.Token.IsCancellationRequested)
+        {
+            return this;
+        }
+
         a ??= () => { };
         var ctsLocal = new CancellationTokenSource ();
 
@@ -249,8 +268,7 @@ public class GuiTestContext : IDisposable
         }
         catch(Exception)
         {
-            Stop ();
-            _hardStop.Cancel();
+            HardStop ();
 
             throw;
 
@@ -259,6 +277,7 @@ public class GuiTestContext : IDisposable
         return this;
     }
 
+
     /// <summary>
     /// Simulates a right click at the given screen coordinates on the current driver.
     /// This is a raw input event that goes through entire processing pipeline as though
@@ -277,8 +296,22 @@ public class GuiTestContext : IDisposable
     /// <param name="screenX">0 indexed screen coordinates</param>
     /// <param name="screenY">0 indexed screen coordinates</param>
     /// <returns></returns>
-    public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
+    public GuiTestContext LeftClick (int screenX, int screenY)
+    {
+        return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY);
+    }
 
+    public GuiTestContext LeftClick<T> (Func<T,bool> evaluator) where T : View
+    {
+        return Click (WindowsConsole.ButtonState.Button1Pressed,evaluator);
+    }
+
+    private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T:View
+    {
+        var v = Find (evaluator);
+        var screen = v.ViewportToScreen (new Point (0, 0));
+        return Click (btn, screen.X, screen.Y);
+    }
     private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
     {
         switch (_driver)
@@ -462,6 +495,75 @@ public class GuiTestContext : IDisposable
         return this;
     }
 
+
+    /// <summary>
+    /// Simulates pressing the Esc (Escape) key.
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Escape ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (
+                                new WindowsConsole.KeyEventRecord
+                                {
+                                    UnicodeChar = '\u001b',
+                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                                    wRepeatCount = 1,
+                                    wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
+                                    wVirtualScanCode = 1
+                                });
+                break;
+            case V2TestDriver.V2Net:
+
+                // Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None
+                // even though you would think it would be Escape - it isn't
+                SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false));
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
+
+
+    /// <summary>
+    /// Simulates pressing the Tab key.
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Tab ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (
+                                new WindowsConsole.KeyEventRecord
+                                {
+                                    UnicodeChar = '\t',
+                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                                    wRepeatCount = 1,
+                                    wVirtualKeyCode = 0,
+                                    wVirtualScanCode = 0
+                                });
+                break;
+            case V2TestDriver.V2Net:
+
+                // Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None
+                // even though you would think it would be Tab - it isn't
+                SendNetKey (new ('\t', ConsoleKey.None, false, false, false));
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
     /// <summary>
     /// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
     /// will open the supplied <paramref name="contextMenu"/>.
@@ -583,4 +685,119 @@ public class GuiTestContext : IDisposable
 
         return WaitIteration ();
     }
+
+    /// <summary>
+    /// Tabs through the UI until a View matching the <paramref name="evaluator"/>
+    /// is found (of Type T) or all views are looped through (back to the beginning)
+    /// in which case triggers hard stop and Exception
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentException"></exception>
+    public GuiTestContext Focus<T> (Func<T,bool> evaluator) where T:View
+    {
+        var t = Application.Top;
+
+        HashSet<View> seen = new ();
+
+        if (t == null)
+        {
+            Fail ("Application.Top was null when trying to set focus");
+            return this;
+        }
+
+        do
+        {
+            var next = t.MostFocused;
+
+            // Is view found?
+            if (next is T v && evaluator (v))
+            {
+                return this;
+            }
+
+            // No, try tab to the next (or first)
+            this.Tab ();
+            WaitIteration ();
+            next = t.MostFocused;
+
+            if (next is null)
+            {
+                Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null");
+                return this;
+            }
+
+            // Track the views we have seen
+            // We have looped around to the start again if it was already there
+            if (!seen.Add (next))
+            {
+                Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View");
+
+                return this;
+            }
+
+        }
+        while (true);
+    }
+
+
+
+    private T Find<T> (Func<T, bool> evaluator) where T : View
+    {
+        var t = Application.Top;
+
+        if (t == null)
+        {
+            Fail ("Application.Top was null when attempting to find view");
+        }
+        var f = FindRecursive(t!, evaluator);
+
+        if (f == null)
+        {
+            Fail ("Failed to tab to a view which matched the Type and evaluator constraints in any SubViews of top");
+        }
+
+        return f!;
+    }
+
+    private T? FindRecursive<T> (View current, Func<T, bool> evaluator) where T : View
+    {
+        foreach (var subview in current.SubViews)
+        {
+            if (subview is T match && evaluator (match))
+            {
+                return match;
+            }
+
+            // Recursive call
+            var result = FindRecursive (subview, evaluator);
+            if (result != null)
+            {
+                return result;
+            }
+        }
+
+        return null;
+    }
+
+    private void Fail (string reason)
+    {
+        Stop ();
+
+        throw new Exception (reason);
+
+    }
+
+    public GuiTestContext Send (Key key)
+    {
+        if (Application.Driver is IConsoleDriverFacade facade)
+        {
+            facade.InputProcessor.OnKeyDown (key);
+            facade.InputProcessor.OnKeyUp (key);
+        }
+        else
+        {
+            Fail ("Expected Application.Driver to be IConsoleDriverFacade");
+        }
+        return this;
+    }
 }

+ 15 - 0
TerminalGuiFluentTesting/With.cs

@@ -19,8 +19,23 @@ public static class With
         return new (() => new T (), width, height,v2TestDriver);
     }
 
+    /// <summary>
+    /// Overload that takes an existing instance <paramref name="toplevel"/>
+    /// instead of creating one.
+    /// </summary>
+    /// <param name="toplevel"></param>
+    /// <param name="width"></param>
+    /// <param name="height"></param>
+    /// <param name="v2TestDriver"></param>
+    /// <returns></returns>
+    public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver)
+    {
+        return new (()=>toplevel, width, height, v2TestDriver);
+    }
     /// <summary>
     ///     The global timeout to allow for any given application to run for before shutting down.
     /// </summary>
     public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30);
+
+    
 }

+ 20 - 0
TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+
+	<PropertyGroup>
+		<TargetFramework>netstandard2.0</TargetFramework>
+		<LangVersion>Latest</LangVersion>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<Nullable>enable</Nullable>
+		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+	</PropertyGroup>
+
+	<ItemGroup>
+		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
+			<PrivateAssets>all</PrivateAssets>
+			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+		</PackageReference>
+		<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
+	</ItemGroup>
+
+</Project>

+ 333 - 0
TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs

@@ -0,0 +1,333 @@
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace TerminalGuiFluentTestingXunit.Generator;
+
+[Generator]
+public class TheGenerator : IIncrementalGenerator
+{
+    /// <inheritdoc/>
+    public void Initialize (IncrementalGeneratorInitializationContext context)
+    {
+        IncrementalValuesProvider<ClassDeclarationSyntax> provider = context.SyntaxProvider.CreateSyntaxProvider (
+                                                                             static (node, _) => IsClass (node, "XunitContextExtensions"),
+                                                                             static (ctx, _) =>
+                                                                                 (ClassDeclarationSyntax)ctx.Node)
+                                                                            .Where (m => m is { });
+
+        IncrementalValueProvider<(Compilation Left, ImmutableArray<ClassDeclarationSyntax> Right)> compilation =
+            context.CompilationProvider.Combine (provider.Collect ());
+        context.RegisterSourceOutput (compilation, Execute);
+    }
+
+    private static bool IsClass (SyntaxNode node, string named) { return node is ClassDeclarationSyntax c && c.Identifier.Text == named; }
+
+    private void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray<ClassDeclarationSyntax> Right) arg2)
+    {
+        INamedTypeSymbol assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert")
+            ?? throw new NotSupportedException("Referencing codebase does not include Xunit, could not find Xunit.Assert");
+
+        GenerateMethods (assertType, context, "Equal", false);
+
+        GenerateMethods (assertType, context, "All", true);
+        GenerateMethods (assertType, context, "Collection", true);
+        GenerateMethods (assertType, context, "Contains", true);
+        GenerateMethods (assertType, context, "Distinct", true);
+        GenerateMethods (assertType, context, "DoesNotContain", true);
+        GenerateMethods (assertType, context, "DoesNotMatch", true);
+        GenerateMethods (assertType, context, "Empty", true);
+        GenerateMethods (assertType, context, "EndsWith", false);
+        GenerateMethods (assertType, context, "Equivalent", true);
+        GenerateMethods (assertType, context, "Fail", true);
+        GenerateMethods (assertType, context, "False", true);
+        GenerateMethods (assertType, context, "InRange", true);
+        GenerateMethods (assertType, context, "IsAssignableFrom", true);
+        GenerateMethods (assertType, context, "IsNotAssignableFrom", true);
+        GenerateMethods (assertType, context, "IsType", true);
+        GenerateMethods (assertType, context, "IsNotType", true);
+
+        GenerateMethods (assertType, context, "Matches", true);
+        GenerateMethods (assertType, context, "Multiple", true);
+        GenerateMethods (assertType, context, "NotEmpty", true);
+        GenerateMethods (assertType, context, "NotEqual", true);
+        GenerateMethods (assertType, context, "NotInRange", true);
+        GenerateMethods (assertType, context, "NotNull", false);
+        GenerateMethods (assertType, context, "NotSame", true);
+        GenerateMethods (assertType, context, "NotStrictEqual", true);
+        GenerateMethods (assertType, context, "Null", false);
+        GenerateMethods (assertType, context, "ProperSubset", true);
+        GenerateMethods (assertType, context, "ProperSuperset", true);
+        GenerateMethods (assertType, context, "Raises", true);
+        GenerateMethods (assertType, context, "RaisesAny", true);
+        GenerateMethods (assertType, context, "Same", true);
+        GenerateMethods (assertType, context, "Single", true);
+        GenerateMethods (assertType, context, "StartsWith", false);
+
+        GenerateMethods (assertType, context, "StrictEqual", true);
+        GenerateMethods (assertType, context, "Subset", true);
+        GenerateMethods (assertType, context, "Superset", true);
+
+//        GenerateMethods (assertType, context, "Throws", true);
+        //      GenerateMethods (assertType, context, "ThrowsAny", true);
+        GenerateMethods (assertType, context, "True", false);
+    }
+
+    private void GenerateMethods (INamedTypeSymbol assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly)
+    {
+        var sb = new StringBuilder ();
+
+        // Create a HashSet to track unique method signatures
+        HashSet<string> signaturesDone = new ();
+
+        List<IMethodSymbol> methods = assertType
+                                      .GetMembers (methodName)
+                                      .OfType<IMethodSymbol> ()
+                                      .ToList ();
+
+        var header = """"
+                     #nullable enable
+                     using TerminalGuiFluentTesting;
+                     using Xunit;
+
+                     namespace TerminalGuiFluentTestingXunit;
+
+                     public static partial class XunitContextExtensions
+                     {
+
+
+                     """";
+
+        var tail = """
+
+                   }
+                   """;
+
+        sb.AppendLine (header);
+
+        foreach (IMethodSymbol? m in methods)
+        {
+            string signature = GetModifiedMethodSignature (m, methodName, invokeTExplicitly, out string [] paramNames, out string typeParams);
+
+            if (!signaturesDone.Add (signature))
+            {
+                continue;
+            }
+
+            var method = $$"""
+                           {{signature}}
+                           {
+                               try
+                               {
+                                   Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}});
+                               }
+                               catch(Exception)
+                               {
+                                   context.HardStop ();
+                                   
+                               
+                                   throw;
+                               
+                               }
+                               
+                               return context;
+                           }
+                           """;
+
+            sb.AppendLine (method);
+        }
+
+        sb.AppendLine (tail);
+
+        context.AddSource ($"XunitContextExtensions{methodName}.g.cs", sb.ToString ());
+    }
+
+    private string GetModifiedMethodSignature (
+        IMethodSymbol methodSymbol,
+        string methodName,
+        bool invokeTExplicitly,
+        out string [] paramNames,
+        out string typeParams
+    )
+    {
+        typeParams = string.Empty;
+
+        // Create the "this GuiTestContext context" parameter
+        ParameterSyntax contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context"))
+                                                    .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext"))
+                                                    .AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword
+
+        // Extract the parameter names (expected and actual)
+        paramNames = new string [methodSymbol.Parameters.Length];
+
+        for (var i = 0; i < methodSymbol.Parameters.Length; i++)
+        {
+            paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name;
+
+            // Check if the parameter name is a reserved keyword and prepend "@" if it is
+            if (IsReservedKeyword (paramNames [i]))
+            {
+                paramNames [i] = "@" + paramNames [i];
+            }
+            else
+            {
+                paramNames [i] = paramNames [i];
+            }
+        }
+
+        // Get the current method parameters and add the context parameter at the start
+        List<ParameterSyntax> parameters = methodSymbol.Parameters.Select (p => CreateParameter (p)).ToList ();
+
+        parameters.Insert (0, contextParam); // Insert 'context' as the first parameter
+
+        // Change the return type to GuiTestContext
+        TypeSyntax returnType = SyntaxFactory.ParseTypeName ("GuiTestContext");
+
+        // Change the method name to AssertEqual
+        SyntaxToken newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}");
+
+        // Handle generic type parameters if the method is generic
+        TypeParameterSyntax [] typeParameters = methodSymbol.TypeParameters.Select (
+                                                                                    tp =>
+                                                                                        SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name))
+                                                                                   )
+                                                            .ToArray ();
+
+        MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName)
+                                                   .WithModifiers (
+                                                                   SyntaxFactory.TokenList (
+                                                                                            SyntaxFactory.Token (SyntaxKind.PublicKeyword),
+                                                                                            SyntaxFactory.Token (SyntaxKind.StaticKeyword)))
+                                                   .WithParameterList (SyntaxFactory.ParameterList (SyntaxFactory.SeparatedList (parameters)));
+
+        if (typeParameters.Any ())
+        {
+            // Add the <T> here
+            dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters)));
+
+            // Handle type parameter constraints
+            List<TypeParameterConstraintClauseSyntax> constraintClauses = methodSymbol.TypeParameters
+                                                                                      .Where (tp => tp.ConstraintTypes.Length > 0)
+                                                                                      .Select (
+                                                                                               tp =>
+                                                                                                   SyntaxFactory.TypeParameterConstraintClause (tp.Name)
+                                                                                                       .WithConstraints (
+                                                                                                            SyntaxFactory
+                                                                                                                .SeparatedList<TypeParameterConstraintSyntax> (
+                                                                                                                     tp.ConstraintTypes.Select (
+                                                                                                                          constraintType =>
+                                                                                                                              SyntaxFactory.TypeConstraint (
+                                                                                                                               SyntaxFactory.ParseTypeName (
+                                                                                                                                constraintType
+                                                                                                                                    .ToDisplayString ()))
+                                                                                                                         )
+                                                                                                                    )
+                                                                                                           )
+                                                                                              )
+                                                                                      .ToList ();
+
+            if (constraintClauses.Any ())
+            {
+                dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses));
+            }
+
+            // Add the <T> here
+            if (invokeTExplicitly)
+            {
+                typeParams = "<" + string.Join (", ", typeParameters.Select (tp => tp.Identifier.ValueText)) + ">";
+            }
+        }
+
+        // Build the method signature syntax tree
+        MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace ();
+
+        // Convert the method syntax to a string
+        var methodString = methodSyntax.ToString ();
+
+        return methodString;
+    }
+
+    /// <summary>
+    ///     Creates a <see cref="ParameterSyntax"/> from a discovered parameter on real xunit method parameter
+    ///     <paramref name="p"/>
+    /// </summary>
+    /// <param name="p"></param>
+    /// <returns></returns>
+    private ParameterSyntax CreateParameter (IParameterSymbol p)
+    {
+        string paramName = p.Name;
+
+        // Check if the parameter name is a reserved keyword and prepend "@" if it is
+        if (IsReservedKeyword (paramName))
+        {
+            paramName = "@" + paramName;
+        }
+
+        // Create the basic parameter syntax with the modified name and type
+        ParameterSyntax parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName))
+                                                       .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ()));
+
+        // Add 'params' keyword if the parameter has the Params modifier
+        var modifiers = new List<SyntaxToken> ();
+
+        if (p.IsParams)
+        {
+            modifiers.Add (SyntaxFactory.Token (SyntaxKind.ParamsKeyword));
+        }
+
+        // Handle ref/out/in modifiers
+        if (p.RefKind != RefKind.None)
+        {
+            SyntaxKind modifierKind = p.RefKind switch
+                                      {
+                                          RefKind.Ref => SyntaxKind.RefKeyword,
+                                          RefKind.Out => SyntaxKind.OutKeyword,
+                                          RefKind.In => SyntaxKind.InKeyword,
+                                          _ => throw new NotSupportedException ($"Unsupported RefKind: {p.RefKind}")
+                                      };
+
+
+            modifiers.Add (SyntaxFactory.Token (modifierKind));
+        }
+
+
+        if (modifiers.Any ())
+        {
+            parameterSyntax = parameterSyntax.WithModifiers (SyntaxFactory.TokenList (modifiers));
+        }
+
+        // Add default value if one is present
+        if (p.HasExplicitDefaultValue)
+        {
+            ExpressionSyntax defaultValueExpression = p.ExplicitDefaultValue switch
+                                                      {
+                                                          null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression),
+                                                          bool b => SyntaxFactory.LiteralExpression (
+                                                                                                     b
+                                                                                                         ? SyntaxKind.TrueLiteralExpression
+                                                                                                         : SyntaxKind.FalseLiteralExpression),
+                                                          int i => SyntaxFactory.LiteralExpression (
+                                                                                                    SyntaxKind.NumericLiteralExpression,
+                                                                                                    SyntaxFactory.Literal (i)),
+                                                          double d => SyntaxFactory.LiteralExpression (
+                                                                                                       SyntaxKind.NumericLiteralExpression,
+                                                                                                       SyntaxFactory.Literal (d)),
+                                                          string s => SyntaxFactory.LiteralExpression (
+                                                                                                       SyntaxKind.StringLiteralExpression,
+                                                                                                       SyntaxFactory.Literal (s)),
+                                                          _ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback
+                                                      };
+
+            parameterSyntax = parameterSyntax.WithDefault (
+                                                           SyntaxFactory.EqualsValueClause (defaultValueExpression)
+                                                          );
+        }
+
+        return parameterSyntax;
+    }
+
+    // Helper method to check if a parameter name is a reserved keyword
+    private bool IsReservedKeyword (string name) { return string.Equals (name, "object"); }
+}

+ 17 - 0
TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+	  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+	  <NoWarn>CS8714</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj" OutputItemType="Analyzer" />
+    <ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
+    <PackageReference Include="xunit" />
+  </ItemGroup>
+
+</Project>

+ 9 - 0
TerminalGuiFluentTestingXunit/XunitContextExtensions.cs

@@ -0,0 +1,9 @@
+using TerminalGuiFluentTesting;
+using Xunit;
+
+namespace TerminalGuiFluentTestingXunit;
+
+public static partial class XunitContextExtensions
+{
+    // Placeholder
+}

+ 197 - 0
Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs

@@ -0,0 +1,197 @@
+using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
+using System.Runtime.InteropServices;
+using Terminal.Gui;
+using TerminalGuiFluentTesting;
+using TerminalGuiFluentTestingXunit;
+using Xunit.Abstractions;
+
+namespace IntegrationTests.FluentTests;
+public class FileDialogFluentTests
+{
+    private readonly TextWriter _out;
+
+    public FileDialogFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
+
+    private MockFileSystem CreateExampleFileSystem ()
+    {
+
+        // Optional: use Ordinal to simulate Linux-style case sensitivity
+        var mockFileSystem = new MockFileSystem (new Dictionary<string, MockFileData> ());
+
+        string testDir = mockFileSystem.Path.Combine ("test-dir");
+        string subDir = mockFileSystem.Path.Combine (testDir, "sub-dir");
+        string logsDir = "logs";
+        string emptyDir = "empty-dir";
+
+        // Add files
+        mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file1.txt"), new MockFileData ("Hello, this is file 1."));
+        mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file2.txt"), new MockFileData ("Hello, this is file 2."));
+        mockFileSystem.AddFile (mockFileSystem.Path.Combine (subDir, "nested-file.txt"), new MockFileData ("This is a nested file."));
+        mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log1.log"), new MockFileData ("Log entry 1"));
+        mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log2.log"), new MockFileData ("Log entry 2"));
+
+        // Create an empty directory
+        mockFileSystem.AddDirectory (emptyDir);
+
+        return mockFileSystem;
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void CancelFileDialog_UsingEscape (V2TestDriver d)
+    {
+        var sd = new SaveDialog ( CreateExampleFileSystem ());
+        using var c = With.A (sd, 100, 20, d)
+            .ScreenShot ("Save dialog",_out)
+            .Escape()
+            .Stop ();
+
+        Assert.True (sd.Canceled);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void CancelFileDialog_UsingCancelButton_TabThenEnter (V2TestDriver d)
+    {
+        var sd = new SaveDialog (CreateExampleFileSystem ());
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .Focus <Button>(b=> b.Text == "_Cancel")
+                          .Enter ()
+                          .Stop ();
+
+        Assert.True (sd.Canceled);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void CancelFileDialog_UsingCancelButton_LeftClickButton (V2TestDriver d)
+    {
+        var sd = new SaveDialog (CreateExampleFileSystem ());
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .LeftClick <Button> (b => b.Text == "_Cancel")
+                          .Stop ()
+                          .WriteOutLogs (_out);
+
+        Assert.True (sd.Canceled);
+    }
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void CancelFileDialog_UsingCancelButton_AltC (V2TestDriver d)
+    {
+        var sd = new SaveDialog (CreateExampleFileSystem ());
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .Send (Key.C.WithAlt)
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.True (sd.Canceled);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void SaveFileDialog_UsingOkButton_Enter (V2TestDriver d)
+    {
+        var fs = CreateExampleFileSystem ();
+        var sd = new SaveDialog (fs);
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .LeftClick<Button> (b => b.Text == "_Save")
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.False (sd.Canceled);
+        AssertIsFileSystemRoot (fs, sd);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void SaveFileDialog_UsingOkButton_AltS (V2TestDriver d)
+    {
+        var fs = CreateExampleFileSystem ();
+        var sd = new SaveDialog (fs);
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .Send (Key.S.WithAlt)
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.False (sd.Canceled);
+        AssertIsFileSystemRoot (fs, sd);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void SaveFileDialog_UsingOkButton_TabEnter (V2TestDriver d)
+    {
+        var fs = CreateExampleFileSystem ();
+        var sd = new SaveDialog (fs);
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .Focus <Button> (b => b.Text == "_Save")
+                          .Enter ()
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.False (sd.Canceled);
+        AssertIsFileSystemRoot (fs,sd);
+    }
+
+    private void AssertIsFileSystemRoot (IFileSystem fs, SaveDialog sd)
+    {
+        var expectedPath =
+            RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
+                $@"C:{fs.Path.DirectorySeparatorChar}" :
+                "/";
+
+        Assert.Equal (expectedPath, sd.FileName);
+
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void SaveFileDialog_PressingPopTree_ShouldNotChangeCancel (V2TestDriver d)
+    {
+        var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = true };
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .AssertTrue (sd.Canceled)
+                          .Focus<Button> (b => b.Text == "►►")
+                          .Enter ()
+                          .ScreenShot ("After pop tree", _out)
+                          .AssertTrue (sd.Canceled)
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.True(sd.Canceled);
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void SaveFileDialog_PopTree_AndNavigate (V2TestDriver d)
+    {
+        var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = true };
+
+        using var c = With.A (sd, 100, 20, d)
+                          .ScreenShot ("Save dialog", _out)
+                          .AssertTrue (sd.Canceled)
+                          .LeftClick <Button> (b => b.Text == "►►")
+                          .ScreenShot ("After pop tree", _out)
+                          .Focus <TreeView<IFileSystemInfo>> (_ => true)
+                          .Right ()
+                          .ScreenShot ("After expand tree", _out)
+                          .Down ()
+                          .ScreenShot ("After navigate down in tree", _out)
+                          .Enter ()
+                          .WaitIteration ()
+                          .AssertFalse (sd.Canceled)
+                          .AssertContains ("empty-dir", sd.FileName)
+                          .WriteOutLogs (_out)
+                          .Stop ();
+
+        Assert.False (sd.Canceled);
+    }
+}

+ 44 - 27
Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs

@@ -1,5 +1,6 @@
 using Terminal.Gui;
 using TerminalGuiFluentTesting;
+using TerminalGuiFluentTestingXunit;
 using Xunit.Abstractions;
 
 namespace IntegrationTests.FluentTests;
@@ -33,7 +34,6 @@ public class TreeViewFluentTests
                 bike = new ("Bike")
             ]
         };
-
         tv.AddObject (root);
 
         using GuiTestContext context =
@@ -46,10 +46,15 @@ public class TreeViewFluentTests
                 .Then (() => Assert.Null (tv.GetObjectOnRow (1)))
                 .Right ()
                 .ScreenShot ("After expanding", _out)
-                .AssertEqual (root, tv.GetObjectOnRow (0))
-                .AssertEqual (car, tv.GetObjectOnRow (1))
-                .AssertEqual (lorry, tv.GetObjectOnRow (2))
-                .AssertEqual (bike, tv.GetObjectOnRow (3))
+                .AssertMultiple (
+                                 () =>
+                                 {
+                                     Assert.Equal (root, tv.GetObjectOnRow (0));
+                                     Assert.Equal (car, tv.GetObjectOnRow (1));
+                                     Assert.Equal (lorry, tv.GetObjectOnRow (2));
+                                     Assert.Equal (bike, tv.GetObjectOnRow (3));
+                                 })
+                .AssertIsAssignableFrom <ITreeNode>(tv.SelectedObject)
                 .Then (
                        () =>
                        {
@@ -59,10 +64,14 @@ public class TreeViewFluentTests
                        })
                 .WaitIteration ()
                 .ScreenShot ("After re-order", _out)
-                .AssertEqual (root, tv.GetObjectOnRow (0))
-                .AssertEqual (bike, tv.GetObjectOnRow (1))
-                .AssertEqual (car, tv.GetObjectOnRow (2))
-                .AssertEqual (lorry, tv.GetObjectOnRow (3))
+                .AssertMultiple (
+                                 () =>
+                                 {
+                                     Assert.Equal (root, tv.GetObjectOnRow (0));
+                                     Assert.Equal (bike, tv.GetObjectOnRow (1));
+                                     Assert.Equal (car, tv.GetObjectOnRow (2));
+                                     Assert.Equal (lorry, tv.GetObjectOnRow (3));
+                                 })
                 .WriteOutLogs (_out);
 
         context.Stop ();
@@ -128,15 +137,19 @@ public class TreeViewFluentTests
                 .Add (tv)
                 .WaitIteration ()
                 .ScreenShot ("Initial State", _out)
-                .AssertEqual (root, tv.GetObjectOnRow (0))
-                .AssertEqual (car, tv.GetObjectOnRow (1))
-                .AssertEqual (mrA, tv.GetObjectOnRow (2))
-                .AssertEqual (mrB, tv.GetObjectOnRow (3))
-                .AssertEqual (lorry, tv.GetObjectOnRow (4))
-                .AssertEqual (mrC, tv.GetObjectOnRow (5))
-                .AssertEqual (bike, tv.GetObjectOnRow (6))
-                .AssertEqual (mrD, tv.GetObjectOnRow (7))
-                .AssertEqual (mrE, tv.GetObjectOnRow (8))
+                .AssertMultiple (
+                                 () =>
+                                 {
+                                     Assert.Equal (root, tv.GetObjectOnRow (0));
+                                     Assert.Equal (car, tv.GetObjectOnRow (1));
+                                     Assert.Equal (mrA, tv.GetObjectOnRow (2));
+                                     Assert.Equal (mrB, tv.GetObjectOnRow (3));
+                                     Assert.Equal (lorry, tv.GetObjectOnRow (4));
+                                     Assert.Equal (mrC, tv.GetObjectOnRow (5));
+                                     Assert.Equal (bike, tv.GetObjectOnRow (6));
+                                     Assert.Equal (mrD, tv.GetObjectOnRow (7));
+                                     Assert.Equal (mrE, tv.GetObjectOnRow (8));
+                                 })
                 .Then (
                        () =>
                        {
@@ -146,15 +159,19 @@ public class TreeViewFluentTests
                        })
                 .WaitIteration ()
                 .ScreenShot ("After re-order", _out)
-                .AssertEqual (root, tv.GetObjectOnRow (0))
-                .AssertEqual (bike, tv.GetObjectOnRow (1))
-                .AssertEqual (mrD, tv.GetObjectOnRow (2))
-                .AssertEqual (mrE, tv.GetObjectOnRow (3))
-                .AssertEqual (car, tv.GetObjectOnRow (4))
-                .AssertEqual (mrA, tv.GetObjectOnRow (5))
-                .AssertEqual (mrB, tv.GetObjectOnRow (6))
-                .AssertEqual (lorry, tv.GetObjectOnRow (7))
-                .AssertEqual (mrC, tv.GetObjectOnRow (8))
+                .AssertMultiple (
+                                 () =>
+                                 {
+                                     Assert.Equal (root, tv.GetObjectOnRow (0));
+                                     Assert.Equal (bike, tv.GetObjectOnRow (1));
+                                     Assert.Equal (mrD, tv.GetObjectOnRow (2));
+                                     Assert.Equal (mrE, tv.GetObjectOnRow (3));
+                                     Assert.Equal (car, tv.GetObjectOnRow (4));
+                                     Assert.Equal (mrA, tv.GetObjectOnRow (5));
+                                     Assert.Equal (mrB, tv.GetObjectOnRow (6));
+                                     Assert.Equal (lorry, tv.GetObjectOnRow (7));
+                                     Assert.Equal (mrC, tv.GetObjectOnRow (8));
+                                 })
                 .WriteOutLogs (_out);
 
         context.Stop ();

+ 1 - 1
Tests/IntegrationTests/IntegrationTests.csproj

@@ -26,7 +26,7 @@
     </ItemGroup>
     <ItemGroup>
         <ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
-        <ProjectReference Include="..\..\TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj" />
+        <ProjectReference Include="..\..\TerminalGuiFluentTestingXunit\TerminalGuiFluentTestingXunit.csproj" />
         <ProjectReference Include="..\..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
         <ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
         <ProjectReference Include="..\UnitTests\UnitTests.csproj" />