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