diff --git a/Directory.Packages.props b/Directory.Packages.props
index 18afbe64a4..1a0afff7a4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,52 +1,45 @@
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Terminal.Gui/FileServices/FileDialogStyle.cs b/Terminal.Gui/FileServices/FileDialogStyle.cs
index aac806de81..34804775ee 100644
--- a/Terminal.Gui/FileServices/FileDialogStyle.cs
+++ b/Terminal.Gui/FileServices/FileDialogStyle.cs
@@ -151,14 +151,11 @@ private Dictionary DefaultTreeRootGetter ()
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 @@ private Dictionary DefaultTreeRootGetter ()
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 ());
}
diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs
index cdeb939a82..2f1aa21abc 100644
--- a/Terminal.Gui/Views/FileDialog.cs
+++ b/Terminal.Gui/Views/FileDialog.cs
@@ -103,6 +103,8 @@ internal FileDialog (IFileSystem fileSystem)
return;
}
+ e.Cancel = true;
+
if (Modal)
{
Application.RequestStop ();
@@ -111,15 +113,27 @@ internal FileDialog (IFileSystem fileSystem)
_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 @@ internal FileDialog (IFileSystem fileSystem)
_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 override void OnLoaded ()
// 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 @@ private IDirectoryInfo StringToDirectoryInfo (string path)
// 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);
diff --git a/Terminal.Gui/Views/SaveDialog.cs b/Terminal.Gui/Views/SaveDialog.cs
index 61ffb88e47..3c7f52be59 100644
--- a/Terminal.Gui/Views/SaveDialog.cs
+++ b/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
{
/// Initializes a new .
- public SaveDialog () { Style.OkButtonText = Strings.btnSave; }
+ public SaveDialog ()
+ {
+ Style.OkButtonText = Strings.btnSave;
+ }
+ internal SaveDialog (IFileSystem fileSystem) : base (fileSystem)
+ {
+ Style.OkButtonText = Strings.btnSave;
+ }
///
/// Gets the name of the file the user selected for saving, or null if the user canceled the
/// .
diff --git a/Terminal.sln b/Terminal.sln
index b255d94598..1456297b83 100644
--- a/Terminal.sln
+++ b/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
diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs
index 1a3274cd32..9b9fe7af4d 100644
--- a/TerminalGuiFluentTesting/GuiTestContext.cs
+++ b/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 topLevelBuilder, int width, int height, V2TestDriver driver)
{
@@ -62,7 +65,7 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height,
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 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height,
{
ApplicationImpl.ChangeInstance (origApp);
Logging.Logger = origLogger;
+ _finished = true;
}
},
_cts.Token);
@@ -111,7 +115,7 @@ public GuiTestContext Stop ()
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 GuiTestContext Stop ()
return this;
}
+ ///
+ /// Hard stops the application and waits for the background thread to exit.
+ ///
+ public void HardStop ()
+ {
+ _hardStop.Cancel ();
+ Stop ();
+ }
+
///
/// Cleanup to avoid state bleed between tests
///
@@ -213,6 +226,12 @@ public GuiTestContext WriteOutLogs (TextWriter writer)
///
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 GuiTestContext Then (Action doAction)
}
catch(Exception)
{
- Stop ();
- _hardStop.Cancel();
+ HardStop ();
throw;
@@ -259,6 +277,7 @@ public GuiTestContext Then (Action doAction)
return this;
}
+
///
/// 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 GuiTestContext Then (Action doAction)
/// 0 indexed screen coordinates
/// 0 indexed screen coordinates
///
- 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 (Func evaluator) where T : View
+ {
+ return Click (WindowsConsole.ButtonState.Button1Pressed,evaluator);
+ }
+
+ private GuiTestContext Click (WindowsConsole.ButtonState btn, Func 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 GuiTestContext Enter ()
return this;
}
+
+ ///
+ /// Simulates pressing the Esc (Escape) key.
+ ///
+ ///
+ ///
+ 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;
+ }
+
+
+
+ ///
+ /// Simulates pressing the Tab key.
+ ///
+ ///
+ ///
+ 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;
+ }
+
///
/// Registers a right click handler on the added view (or root view) that
/// will open the supplied .
@@ -583,4 +685,119 @@ public GuiTestContext Focus (View toFocus)
return WaitIteration ();
}
+
+ ///
+ /// Tabs through the UI until a View matching the
+ /// is found (of Type T) or all views are looped through (back to the beginning)
+ /// in which case triggers hard stop and Exception
+ ///
+ ///
+ ///
+ public GuiTestContext Focus (Func evaluator) where T:View
+ {
+ var t = Application.Top;
+
+ HashSet 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 (Func 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 (View current, Func 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;
+ }
}
diff --git a/TerminalGuiFluentTesting/With.cs b/TerminalGuiFluentTesting/With.cs
index b65d832385..078fdb1896 100644
--- a/TerminalGuiFluentTesting/With.cs
+++ b/TerminalGuiFluentTesting/With.cs
@@ -19,8 +19,23 @@ public static class With
return new (() => new T (), width, height,v2TestDriver);
}
+ ///
+ /// Overload that takes an existing instance
+ /// instead of creating one.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver)
+ {
+ return new (()=>toplevel, width, height, v2TestDriver);
+ }
///
/// The global timeout to allow for any given application to run for before shutting down.
///
public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30);
+
+
}
diff --git a/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj
new file mode 100644
index 0000000000..454cc7bf7f
--- /dev/null
+++ b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ netstandard2.0
+ Latest
+ enable
+ enable
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs
new file mode 100644
index 0000000000..5c0427d115
--- /dev/null
+++ b/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
+{
+ ///
+ public void Initialize (IncrementalGeneratorInitializationContext context)
+ {
+ IncrementalValuesProvider provider = context.SyntaxProvider.CreateSyntaxProvider (
+ static (node, _) => IsClass (node, "XunitContextExtensions"),
+ static (ctx, _) =>
+ (ClassDeclarationSyntax)ctx.Node)
+ .Where (m => m is { });
+
+ IncrementalValueProvider<(Compilation Left, ImmutableArray 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 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 signaturesDone = new ();
+
+ List methods = assertType
+ .GetMembers (methodName)
+ .OfType ()
+ .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 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 here
+ dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters)));
+
+ // Handle type parameter constraints
+ List constraintClauses = methodSymbol.TypeParameters
+ .Where (tp => tp.ConstraintTypes.Length > 0)
+ .Select (
+ tp =>
+ SyntaxFactory.TypeParameterConstraintClause (tp.Name)
+ .WithConstraints (
+ SyntaxFactory
+ .SeparatedList (
+ tp.ConstraintTypes.Select (
+ constraintType =>
+ SyntaxFactory.TypeConstraint (
+ SyntaxFactory.ParseTypeName (
+ constraintType
+ .ToDisplayString ()))
+ )
+ )
+ )
+ )
+ .ToList ();
+
+ if (constraintClauses.Any ())
+ {
+ dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses));
+ }
+
+ // Add the 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;
+ }
+
+ ///
+ /// Creates a from a discovered parameter on real xunit method parameter
+ ///
+ ///
+ ///
+ ///
+ 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 ();
+
+ 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"); }
+}
diff --git a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj
new file mode 100644
index 0000000000..e9e661df26
--- /dev/null
+++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ CS8714
+
+
+
+
+
+
+
+
+
diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs
new file mode 100644
index 0000000000..a007dbbc1b
--- /dev/null
+++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs
@@ -0,0 +1,9 @@
+using TerminalGuiFluentTesting;
+using Xunit;
+
+namespace TerminalGuiFluentTestingXunit;
+
+public static partial class XunitContextExtensions
+{
+ // Placeholder
+}
diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs
new file mode 100644
index 0000000000..2eeb59484e
--- /dev/null
+++ b/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 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