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 7e4253cf28.

* 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
This commit is contained in:
Thomas Nind
2025-04-16 16:25:07 +01:00
committed by Tig
parent 4d87d5f249
commit eaa9ee1ef6
14 changed files with 946 additions and 101 deletions

View File

@@ -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>

View File

@@ -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 ());
}

View File

@@ -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);

View File

@@ -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"/>.

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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"); }
}

View File

@@ -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>

View File

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

View File

@@ -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);
}
}

View File

@@ -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 ();

View File

@@ -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" />