diff --git a/.github/workflows/System.Waf.CI.yml b/.github/workflows/System.Waf.CI.yml index 1ca11385..954ca688 100644 --- a/.github/workflows/System.Waf.CI.yml +++ b/.github/workflows/System.Waf.CI.yml @@ -27,7 +27,7 @@ jobs: src/System.Waf/System.Waf/**/*.snupkg - name: UI Test - run: dotnet test ./src/Samples.UITest/Samples.UITest.sln --logger "console;verbosity=detailed" + run: dotnet test ./src/Samples.UITest/Samples.UITest.sln --logger "console;verbosity=detailed" -maxCpuCount:1 - name: Upload UI Test results uses: actions/upload-artifact@v4 if: always() diff --git a/src/Samples.UITest/BookLibrary.Test/BookLibrary.Test.csproj b/src/Samples.UITest/BookLibrary.Test/BookLibrary.Test.csproj new file mode 100644 index 00000000..e46fb508 --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/BookLibrary.Test.csproj @@ -0,0 +1,19 @@ + + + net8.0-windows + UITest.BookLibrary + + enable + enable + + + + + + + + + + + + diff --git a/src/Samples.UITest/BookLibrary.Test/Tests/BookLibraryTest.cs b/src/Samples.UITest/BookLibrary.Test/Tests/BookLibraryTest.cs new file mode 100644 index 00000000..afba64cf --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/Tests/BookLibraryTest.cs @@ -0,0 +1,93 @@ +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Capturing; +using UITest.BookLibrary.Views; +using UITest.SystemViews; +using Xunit; +using Xunit.Abstractions; + +namespace UITest.BookLibrary.Tests; + +public class BookLibraryTest(ITestOutputHelper log) : UITest(log) +{ + [Fact] + public void AboutTest() => Run(() => + { + Launch(); + var window = GetShellWindow(); + + var helpMenu = window.HelpMenu; + helpMenu.Click(); + helpMenu.AboutMenuItem.Click(); + + var messageBox = window.FirstModalWindow().As(); + Assert.Equal("Waf Book Library", messageBox.Title); + Log.WriteLine(messageBox.Message); + Assert.StartsWith("Waf Book Library ", messageBox.Message); + Capture.Screen().ToFile(GetScreenshotFile("About.png")); + messageBox.Buttons[0].Click(); + + var dataMenu = window.DataMenu; + dataMenu.Click(); + dataMenu.ExitMenuItem.Click(); + }); + + [Fact] + public void SearchBookListAndChangeEntriesTest() => Run(() => + { + Launch(); + var window = GetShellWindow(); + var bookListView = window.TabControl.BookLibraryTabItem.BookListView; + var bookView = window.TabControl.BookLibraryTabItem.BookView; + + Assert.Equal(41, bookListView.BookDataGrid.RowCount); + bookListView.SearchBox.Text = "Ha"; + Assert.Equal(13, bookListView.BookDataGrid.RowCount); + bookListView.SearchBox.Text = "Harr"; + Assert.Equal(7, bookListView.BookDataGrid.RowCount); + var bookRow2 = bookListView.BookDataGrid.GetRowByIndex(1).As(); + bookRow2.Select(); + + AssertEqual("Harry Potter and the Deathly Hallows", bookRow2.TitleCell.Name, bookView.TitleTextBox.Text); + AssertEqual("J.K. Rowling", bookRow2.AuthorCell.Name, bookView.AuthorTextBox.Text); + Assert.Equal("Bloomsbury", bookView.PublisherTextBox.Text); + Assert.Equal("1/1/2007", bookRow2.PublishDateCell.Name); + Assert.Equal(new DateTime(2007, 1, 1), bookView.PublishDatePicker.SelectedDate); + Assert.Equal("9780747591054", bookView.IsbnTextBox.Text); + Assert.Equal("English", bookView.LanguageComboBox.SelectedItem.Text); + Assert.Equal("607", bookView.PagesTextBox.Text); + AssertEqual("Ginny Weasley", bookRow2.LendToCell.LendToLabel.Name, bookView.LendToTextBox.Text); + + bookView.TitleTextBox.Text = "Test Title"; + Assert.Equal("Test Title", bookRow2.TitleCell.Name); + bookView.AuthorTextBox.Text = "TAuthor"; + Assert.Equal("TAuthor", bookRow2.AuthorCell.Name); + bookView.PublishDatePicker.SelectedDate = new DateTime(2024, 3, 2); + Assert.Equal("3/2/2024", bookRow2.PublishDateCell.Name); + Assert.Equal(["Undefined", "English", "German", "French", "Spanish", "Chinese", "Japanese"], bookView.LanguageComboBox.Items.Select(x => x.Name)); + bookView.LanguageComboBox.Select(2); + bookView.LanguageComboBox.Click(); // To close the combo box popup + Assert.Equal("German", bookView.LanguageComboBox.SelectedItem.Text); + + bookView.LendToButton.Click(); + var lendToWindow = window.FirstModalWindow().As(); + Assert.True(lendToWindow.WasReturnedRadioButton.IsChecked); + Assert.False(lendToWindow.LendToRadioButton.IsChecked); + Assert.False(lendToWindow.PersonListBox.IsEnabled); + lendToWindow.LendToRadioButton.Click(); + Assert.True(lendToWindow.PersonListBox.IsEnabled); + Assert.Equal(["Ginny", "Hermione", "Harry", "Ron"], lendToWindow.PersonListBox.Items.Select(x => x.Text)); + lendToWindow.PersonListBox.Items[2].Select(); + lendToWindow.OkButton.Click(); + AssertEqual("Harry Potter", bookRow2.LendToCell.LendToLabel.Name, bookView.LendToTextBox.Text); + + window.Close(); + var messageBox = window.FirstModalWindow().As(); // MessageBox that asks user to save the changes + messageBox.Buttons[1].Click(); // No button + + void AssertEqual(string expected, string actual1, string actual2) + { + Assert.Equal(expected, actual1); + Assert.Equal(expected, actual2); + } + }); +} diff --git a/src/Samples.UITest/BookLibrary.Test/UITest.cs b/src/Samples.UITest/BookLibrary.Test/UITest.cs new file mode 100644 index 00000000..54afc756 --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/UITest.cs @@ -0,0 +1,41 @@ +using FlaUI.Core; +using FlaUI.Core.AutomationElements; +using System.Diagnostics; +using UITest.BookLibrary.Views; +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace UITest.BookLibrary; + +public abstract class UITest(ITestOutputHelper log) : UITestBase(log, "BookLibrary.exe", + Environment.GetEnvironmentVariable("UITestExePath") ?? "out/BookLibrary/Release/net8.0-windows/", + Environment.GetEnvironmentVariable("UITestOutputPath") ?? "out/Samples.UITest/BookLibrary/") +{ + public Application Launch(LaunchArguments? arguments = null, bool resetSettings = true) + { + Log.WriteLine(""); + if (resetSettings) + { + var productName = FileVersionInfo.GetVersionInfo(Executable).ProductName ?? throw new InvalidOperationException("Could not read the ProductName from the exe."); + var settingsFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), productName, "Settings", "Settings.xml"); + if (File.Exists(settingsFile)) File.Delete(settingsFile); + Log.WriteLine($"Delete settings: {settingsFile}"); + } + var args = (arguments ?? new LaunchArguments()).ToArguments(); + Log.WriteLine($"Launch: {args}"); + return App = Application.Launch(Executable, args); + } + + public ShellWindow GetShellWindow() => App!.GetMainWindow(Automation).As(); +} + +public record LaunchArguments(string? UICulture = "en-US", string? Culture = "en-US", string? AdditionalArguments = null) : LaunchArgumentsBase +{ + public override string ToArguments() + { + string?[] args = [CreateArg(UICulture), CreateArg(Culture), AdditionalArguments]; + return string.Join(" ", args.Where(x => !string.IsNullOrEmpty(x))); + } +} \ No newline at end of file diff --git a/src/Samples.UITest/BookLibrary.Test/Views/BookListView.cs b/src/Samples.UITest/BookLibrary.Test/Views/BookListView.cs new file mode 100644 index 00000000..652ae4be --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/Views/BookListView.cs @@ -0,0 +1,29 @@ +using FlaUI.Core; +using FlaUI.Core.AutomationElements; + +namespace UITest.BookLibrary.Views; + +public class BookListView(FrameworkAutomationElementBase element) : AutomationElement(element) +{ + public TextBox SearchBox => this.Find("SearchBox").AsTextBox(); + + public Grid BookDataGrid => this.Find("BookDataGrid").AsGrid(); +} + +public class BookGridRow(FrameworkAutomationElementBase element) : GridRow(element) +{ + public GridCell TitleCell => Cells[0]; + + public GridCell AuthorCell => Cells[1]; + + public GridCell PublishDateCell => Cells[2]; + + public LendToGridCell LendToCell => Cells[3].As(); +} + +public class LendToGridCell(FrameworkAutomationElementBase element) : GridCell(element) +{ + public Label LendToLabel => this.Find("LendToLabel").AsLabel(); + + public Button LendToButton => this.Find("LendToButton").AsButton(); +} \ No newline at end of file diff --git a/src/Samples.UITest/BookLibrary.Test/Views/BookView.cs b/src/Samples.UITest/BookLibrary.Test/Views/BookView.cs new file mode 100644 index 00000000..f4276d2b --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/Views/BookView.cs @@ -0,0 +1,25 @@ +using FlaUI.Core.AutomationElements; +using FlaUI.Core; + +namespace UITest.BookLibrary.Views; + +public class BookView(FrameworkAutomationElementBase element) : AutomationElement(element) +{ + public TextBox TitleTextBox => this.Find("TitleTextBox").AsTextBox(); + + public TextBox AuthorTextBox => this.Find("AuthorTextBox").AsTextBox(); + + public TextBox PublisherTextBox => this.Find("PublisherTextBox").AsTextBox(); + + public DateTimePicker PublishDatePicker => this.Find("PublishDatePicker").AsDateTimePicker(); + + public TextBox IsbnTextBox => this.Find("IsbnTextBox").AsTextBox(); + + public ComboBox LanguageComboBox => this.Find("LanguageComboBox").AsComboBox(); + + public TextBox PagesTextBox => this.Find("PagesTextBox").AsTextBox(); + + public TextBox LendToTextBox => this.Find("LendToTextBox").AsTextBox(); + + public Button LendToButton => this.Find("LendToButton").AsButton(); +} diff --git a/src/Samples.UITest/BookLibrary.Test/Views/LendToWindow.cs b/src/Samples.UITest/BookLibrary.Test/Views/LendToWindow.cs new file mode 100644 index 00000000..48fd4e4a --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/Views/LendToWindow.cs @@ -0,0 +1,17 @@ +using FlaUI.Core; +using FlaUI.Core.AutomationElements; + +namespace UITest.BookLibrary.Views; + +public class LendToWindow(FrameworkAutomationElementBase element) : Window(element) +{ + public RadioButton WasReturnedRadioButton => this.Find("WasReturnedRadioButton").AsRadioButton(); + + public RadioButton LendToRadioButton => this.Find("LendToRadioButton").AsRadioButton(); + + public ListBox PersonListBox => this.Find("PersonListBox").AsListBox(); + + public Button OkButton => this.Find("OkButton").AsButton(); + + public Button CancelButton => this.Find("CancelButton").AsButton(); +} diff --git a/src/Samples.UITest/BookLibrary.Test/Views/ShellWindow.cs b/src/Samples.UITest/BookLibrary.Test/Views/ShellWindow.cs new file mode 100644 index 00000000..4537016d --- /dev/null +++ b/src/Samples.UITest/BookLibrary.Test/Views/ShellWindow.cs @@ -0,0 +1,41 @@ +using FlaUI.Core; +using FlaUI.Core.AutomationElements; + +namespace UITest.BookLibrary.Views; + +public class ShellWindow(FrameworkAutomationElementBase element) : Window(element) +{ + public DataMenu DataMenu => this.Find("DataMenu").As(); + + public HelpMenu HelpMenu => this.Find("HelpMenu").As(); + + public TabControl TabControl => this.Find("TabControl").As(); +} + +public class DataMenu(FrameworkAutomationElementBase element) : Menu(element) +{ + public MenuItem SaveMenuItem => this.Find("SaveMenuItem").AsMenuItem(); + + public MenuItem ExitMenuItem => this.Find("ExitMenuItem").AsMenuItem(); +} + +public class HelpMenu(FrameworkAutomationElementBase element) : Menu(element) +{ + public MenuItem AboutMenuItem => this.Find("AboutMenuItem").AsMenuItem(); +} + +public class TabControl(FrameworkAutomationElementBase element) : Tab(element) +{ + public BookLibraryTabItem BookLibraryTabItem => this.Find("BookLibraryTabItem").As(); + + public TabItem AddressBookTabItem => this.Find("AddressBookTabItem").AsTabItem(); + + public TabItem ReportingTabItem => this.Find("ReportingTabItem").AsTabItem(); +} + +public class BookLibraryTabItem(FrameworkAutomationElementBase element) : TabItem(element) +{ + public BookListView BookListView => this.Find("BookListView").As(); + + public BookView BookView => this.Find("BookView").As(); +} \ No newline at end of file diff --git a/src/Samples.UITest/Samples.UITest.sln b/src/Samples.UITest/Samples.UITest.sln index 9d83d639..cb64ab9f 100644 --- a/src/Samples.UITest/Samples.UITest.sln +++ b/src/Samples.UITest/Samples.UITest.sln @@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34714.143 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Writer.Test", "Writer.Test\Writer.Test.csproj", "{8FBAB64A-51F5-40F8-8B56-4AEE7BE49838}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Writer.Test", "Writer.Test\Writer.Test.csproj", "{8FBAB64A-51F5-40F8-8B56-4AEE7BE49838}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookLibrary.Test", "BookLibrary.Test\BookLibrary.Test.csproj", "{A239DB72-F7D9-4DE9-83F9-E0D852FA5F8F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.Core", "UITest.Core\UITest.Core.csproj", "{4B2174D9-B723-42AA-90DA-2F556409F19A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +19,14 @@ Global {8FBAB64A-51F5-40F8-8B56-4AEE7BE49838}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FBAB64A-51F5-40F8-8B56-4AEE7BE49838}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FBAB64A-51F5-40F8-8B56-4AEE7BE49838}.Release|Any CPU.Build.0 = Release|Any CPU + {A239DB72-F7D9-4DE9-83F9-E0D852FA5F8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A239DB72-F7D9-4DE9-83F9-E0D852FA5F8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A239DB72-F7D9-4DE9-83F9-E0D852FA5F8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A239DB72-F7D9-4DE9-83F9-E0D852FA5F8F}.Release|Any CPU.Build.0 = Release|Any CPU + {4B2174D9-B723-42AA-90DA-2F556409F19A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B2174D9-B723-42AA-90DA-2F556409F19A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B2174D9-B723-42AA-90DA-2F556409F19A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B2174D9-B723-42AA-90DA-2F556409F19A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Samples.UITest/UITest.Core/LaunchArgumentsBase.cs b/src/Samples.UITest/UITest.Core/LaunchArgumentsBase.cs new file mode 100644 index 00000000..eb5200d2 --- /dev/null +++ b/src/Samples.UITest/UITest.Core/LaunchArgumentsBase.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +namespace UITest; + +public abstract record LaunchArgumentsBase +{ + protected string? CreateArg(object? value, [CallerArgumentExpression(nameof(value))] string propertyName = null!) => value is null ? null : $"--{propertyName}=\"{value}\""; + + public abstract string ToArguments(); +} diff --git a/src/Samples.UITest/Writer.Test/Views/MessageBox.cs b/src/Samples.UITest/UITest.Core/SystemViews/MessageBox.cs similarity index 66% rename from src/Samples.UITest/Writer.Test/Views/MessageBox.cs rename to src/Samples.UITest/UITest.Core/SystemViews/MessageBox.cs index 524c373e..501548b8 100644 --- a/src/Samples.UITest/Writer.Test/Views/MessageBox.cs +++ b/src/Samples.UITest/UITest.Core/SystemViews/MessageBox.cs @@ -2,7 +2,7 @@ using FlaUI.Core.AutomationElements; using FlaUI.Core.Definitions; -namespace UITest.Writer.Views; +namespace UITest.SystemViews; public class MessageBox(FrameworkAutomationElementBase element) : AutomationElement(element) { @@ -10,5 +10,5 @@ public class MessageBox(FrameworkAutomationElementBase element) : AutomationElem public string Message => this.Find(x => x.ByControlType(ControlType.Text)).Name; - public Button OkButton => this.Find(x => x.ByControlType(ControlType.Button)).AsButton(); + public Button[] Buttons => this.FindAll(x => x.ByControlType(ControlType.Button)).Select(x => x.AsButton()).ToArray(); } diff --git a/src/Samples.UITest/Writer.Test/Views/SaveFileDialog.cs b/src/Samples.UITest/UITest.Core/SystemViews/SaveFileDialog.cs similarity index 96% rename from src/Samples.UITest/Writer.Test/Views/SaveFileDialog.cs rename to src/Samples.UITest/UITest.Core/SystemViews/SaveFileDialog.cs index d1c5642f..d2096e2e 100644 --- a/src/Samples.UITest/Writer.Test/Views/SaveFileDialog.cs +++ b/src/Samples.UITest/UITest.Core/SystemViews/SaveFileDialog.cs @@ -4,7 +4,7 @@ using FlaUI.Core.Input; using FlaUI.Core.WindowsAPI; -namespace UITest.Writer.Views; +namespace UITest.SystemViews; public class SaveFileDialog(FrameworkAutomationElementBase element) : Window(element) { diff --git a/src/Samples.UITest/Writer.Test/UIAssert.cs b/src/Samples.UITest/UITest.Core/UIAssert.cs similarity index 97% rename from src/Samples.UITest/Writer.Test/UIAssert.cs rename to src/Samples.UITest/UITest.Core/UIAssert.cs index 6d739fb9..7281377e 100644 --- a/src/Samples.UITest/Writer.Test/UIAssert.cs +++ b/src/Samples.UITest/UITest.Core/UIAssert.cs @@ -1,7 +1,7 @@ using FlaUI.Core.Tools; using System.Runtime.CompilerServices; -namespace UITest.Writer; +namespace UITest; public class ElementFoundException(string message, Exception? innerException = null) : Exception(message, innerException) { } diff --git a/src/Samples.UITest/UITest.Core/UITest.Core.csproj b/src/Samples.UITest/UITest.Core/UITest.Core.csproj new file mode 100644 index 00000000..69ec98c9 --- /dev/null +++ b/src/Samples.UITest/UITest.Core/UITest.Core.csproj @@ -0,0 +1,14 @@ + + + net8.0-windows + UITest + + enable + enable + + + + + + + diff --git a/src/Samples.UITest/UITest.Core/UITestBase.cs b/src/Samples.UITest/UITest.Core/UITestBase.cs new file mode 100644 index 00000000..03ec4cbf --- /dev/null +++ b/src/Samples.UITest/UITest.Core/UITestBase.cs @@ -0,0 +1,114 @@ +using FlaUI.Core; +using FlaUI.Core.Capturing; +using FlaUI.Core.Input; +using FlaUI.Core.Tools; +using FlaUI.UIA3; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using Xunit.Abstractions; + +namespace UITest; + +public abstract class UITestBase : IDisposable +{ + private readonly List usedFiles = []; + private string? testMethodName; + + static UITestBase() + { + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + + Mouse.MovePixelsPerMillisecond = 2; + Retry.DefaultTimeout = TimeSpan.FromSeconds(5); + Retry.DefaultInterval = TimeSpan.FromMilliseconds(250); + } + + protected UITestBase(ITestOutputHelper log, string executableFileName, string executablePath, string testOutputPath) + { + Log = log; + var assemblyPath = Assembly.GetAssembly(typeof(UITestBase))!.Location; + var rootPath = Path.GetFullPath(Path.Combine(assemblyPath, "../../../../../../../")); + Executable = Path.GetFullPath(Path.Combine(Path.IsPathFullyQualified(executablePath) ? executablePath : Path.Combine(rootPath, executablePath), executableFileName)); + TestOutPath = Path.GetFullPath(Path.IsPathFullyQualified(testOutputPath) ? testOutputPath : Path.Combine(rootPath, testOutputPath)); + Directory.CreateDirectory(TestOutPath); + Log.WriteLine($"OSVersion: {Environment.OSVersion}"); + Log.WriteLine($"ProcessorCount: {Environment.ProcessorCount}"); + Log.WriteLine($"MachineName: {Environment.MachineName}"); + Log.WriteLine($"UserInteractive: {Environment.UserInteractive}"); + Log.WriteLine($"Executable: {Executable}"); + Log.WriteLine($"TestOutPath: {TestOutPath}"); + Automation = new() + { + ConnectionTimeout = TimeSpan.FromSeconds(5) + }; + } + + public ITestOutputHelper Log { get; } + + public string Executable { get; } + + public string TestOutPath { get; } + + public string TestMethodName => testMethodName ?? throw new InvalidOperationException("Test context not available. Use the Run method for your test code."); + + public UIA3Automation Automation { get; } + + public Application? App { get; protected set; } + + public bool SkipAppClose { get; set; } = false; + + public void Run(Action action, [CallerMemberName] string? memberName = null) + { + try + { + testMethodName = memberName; + action(); + } + catch (Exception) + { + TryGetScreenshot(); + throw; + } + finally + { + testMethodName = null; + } + + void TryGetScreenshot() + { + try + { + Capture.Screen().ToFile(GetScreenshotFile("Fail")); + } + catch { } + } + } + + public string GetTempFileName(string fileExtension) + { + var file = $"UITest_{Path.GetRandomFileName()}.{fileExtension}"; + file = Path.Combine(Path.GetTempPath(), file); + usedFiles.Add(file); + Log.WriteLine($"TempFile: {file}"); + return file; + } + + public string GetScreenshotFile(string fileName) + { + var file = Path.Combine(TestOutPath, string.Join("-", TestMethodName, fileName)); + if (string.IsNullOrEmpty(Path.GetExtension(file))) file += ".png"; + return file; + } + + public void Dispose() + { + if (!SkipAppClose) App?.Close(); + App?.Dispose(); + Automation.Dispose(); + foreach (var file in usedFiles) + { + if (File.Exists(file)) File.Delete(file); + } + } +} diff --git a/src/Samples.UITest/Writer.Test/UITestHelper.cs b/src/Samples.UITest/UITest.Core/UITestHelper.cs similarity index 99% rename from src/Samples.UITest/Writer.Test/UITestHelper.cs rename to src/Samples.UITest/UITest.Core/UITestHelper.cs index 479ffb79..c0e06383 100644 --- a/src/Samples.UITest/Writer.Test/UITestHelper.cs +++ b/src/Samples.UITest/UITest.Core/UITestHelper.cs @@ -4,7 +4,7 @@ using FlaUI.Core.Tools; using System.Text; -namespace UITest.Writer; +namespace UITest; public class ElementNotFoundException(string message, Exception? innerException = null) : Exception(message, innerException) { } diff --git a/src/Samples.UITest/Writer.Test/LaunchArguments.cs b/src/Samples.UITest/Writer.Test/LaunchArguments.cs deleted file mode 100644 index d4d29e1b..00000000 --- a/src/Samples.UITest/Writer.Test/LaunchArguments.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace UITest.Writer; - -public record LaunchArguments(string? UICulture = "en-US", string? Culture = "en-US", bool? DefaultSettings = true, string? AdditionalArguments = null) -{ - private string? CreateArg(object? value, [CallerArgumentExpression(nameof(value))] string propertyName = null!) => value is null ? null : $"--{propertyName}=\"{value}\""; - - public string ToArguments() - { - string?[] args = [ CreateArg(UICulture), CreateArg(Culture), CreateArg(DefaultSettings), CreateArg(AdditionalArguments) ]; - return string.Join(" ", args.Where(x => !string.IsNullOrEmpty(x))); - } -} diff --git a/src/Samples.UITest/Writer.Test/Tests/WriterTest.cs b/src/Samples.UITest/Writer.Test/Tests/WriterTest.cs index 37c76366..afafe4ae 100644 --- a/src/Samples.UITest/Writer.Test/Tests/WriterTest.cs +++ b/src/Samples.UITest/Writer.Test/Tests/WriterTest.cs @@ -2,6 +2,7 @@ using FlaUI.Core.Capturing; using FlaUI.Core.Definitions; using FlaUI.Core.Tools; +using UITest.SystemViews; using UITest.Writer.Views; using Xunit; using Xunit.Abstractions; @@ -21,7 +22,7 @@ public WriterTest(ITestOutputHelper log) : base(log) protected void AssertTextEqual(string expected, TextBox actual) { if (!SkipTextBoxReadText) Assert.Equal(expected, actual.Text); } [Fact] - public void AboutTest() + public void AboutTest() => Run(() => { Launch(); var window = GetShellWindow(); @@ -33,15 +34,15 @@ public void AboutTest() Log.WriteLine(messageBox.Message); Assert.StartsWith("Waf Writer ", messageBox.Message); Capture.Screen().ToFile(GetScreenshotFile("About.png")); - messageBox.OkButton.Click(); + messageBox.Buttons[0].Click(); var fileRibbonMenu = window.FileRibbonMenu; fileRibbonMenu.MenuButton.Click(); fileRibbonMenu.ExitMenuItem.Invoke(); - } + }); [Fact] - public void NewZoomWritePrintPreviewExitWithoutSave() + public void NewZoomWritePrintPreviewExitWithoutSave() => Run(() => { Launch(); var window = GetShellWindow(); @@ -85,17 +86,17 @@ public void NewZoomWritePrintPreviewExitWithoutSave() var firstItem = saveChangesWindow.FilesToSaveList.Items.Single(); Assert.Equal("Document 1.rtf", firstItem.Text); saveChangesWindow.NoButton.Click(); - } + }); [Fact] - public void MultipleNewSaveRestartOpenRecentChangeAskToSave() + public void MultipleNewSaveRestartOpenRecentChangeAskToSave() => Run(() => { Launch(); var window = GetShellWindow(); var startView = window.StartView; Assert.False(startView.IsOffscreen); - + // Create new document, add text, check tab name with dirty flag indicator '*' startView.NewButton.Click(); var tab1 = window.DocumentTabItems.Single(); @@ -146,20 +147,20 @@ public void MultipleNewSaveRestartOpenRecentChangeAskToSave() // Restart the app and check recent file list, pin second item -> moved to top - Launch(new LaunchArguments(DefaultSettings: false)); + Launch(resetSettings: false); window = GetShellWindow(); startView = window.StartView; Capture.Screen().ToFile(GetScreenshotFile("RestartScreen.png")); - Assert.Equal([ fileName2, fileName ], startView.RecentFileListItems.Select(x => x.ToolTip)); + Assert.Equal([fileName2, fileName], startView.RecentFileListItems.Select(x => x.ToolTip)); startView.RecentFileListItems[1].PinButton.Click(); - Assert.Equal([ fileName, fileName2 ], startView.RecentFileListItems.Select(x => x.ToolTip)); + Assert.Equal([fileName, fileName2], startView.RecentFileListItems.Select(x => x.ToolTip)); Assert.True(startView.RecentFileListItems[0].PinButton.IsToggled); Assert.False(startView.RecentFileListItems[1].PinButton.IsToggled); // Check that the recent file list within the ribbon menu has the same content fileRibbonMenu = window.FileRibbonMenu; fileRibbonMenu.MenuButton.Click(); - Assert.Equal([ fileName, fileName2 ], fileRibbonMenu.RecentFileListItems.Select(x => x.ToolTip)); + Assert.Equal([fileName, fileName2], fileRibbonMenu.RecentFileListItems.Select(x => x.ToolTip)); Assert.True(fileRibbonMenu.RecentFileListItems[0].PinButton.IsToggled); Assert.False(fileRibbonMenu.RecentFileListItems[1].PinButton.IsToggled); fileRibbonMenu.MenuButton.Toggle(); @@ -188,27 +189,27 @@ public void MultipleNewSaveRestartOpenRecentChangeAskToSave() Assert.Equal(fileName, firstItem.Text); saveChangesWindow.NoButton.Click(); - + // Restart the app and check recent file list, use context menu to unpin, remove and open - Launch(new LaunchArguments(DefaultSettings: false)); + Launch(resetSettings: false); window = GetShellWindow(); startView = window.StartView; - Assert.Equal([ fileName, fileName2 ], startView.RecentFileListItems.Select(x => x.ToolTip)); + Assert.Equal([fileName, fileName2], startView.RecentFileListItems.Select(x => x.ToolTip)); Assert.True(startView.RecentFileListItems[0].PinButton.IsToggled); var contextMenu = startView.RecentFileListItems[0].ShowContextMenu(); - UIAssert.NotExists(() => _ = contextMenu.PinFileMenuItem); + UIAssert.NotExists(() => _ = contextMenu.PinFileMenuItem); contextMenu.UnpinFileMenuItem.Invoke(); Assert.False(startView.RecentFileListItems[0].PinButton.IsToggled); - + contextMenu = startView.RecentFileListItems[0].ShowContextMenu(); UIAssert.NotExists(() => _ = contextMenu.UnpinFileMenuItem); contextMenu.RemoveFileMenuItem.Invoke(); - Assert.Equal([ fileName2 ], startView.RecentFileListItems.Select(x => x.ToolTip)); + Assert.Equal([fileName2], startView.RecentFileListItems.Select(x => x.ToolTip)); contextMenu = startView.RecentFileListItems[0].ShowContextMenu(); contextMenu.OpenFileMenuItem.Invoke(); tab1 = window.DocumentTab.SelectedTabItem.As(); Assert.Equal(Path.GetFileName(fileName2), tab1.TabName); AssertTextEqual("Hello World 2", tab1.RichTextView.RichTextBox); - } + }); } diff --git a/src/Samples.UITest/Writer.Test/UITest.cs b/src/Samples.UITest/Writer.Test/UITest.cs index caf17c7e..6bee544a 100644 --- a/src/Samples.UITest/Writer.Test/UITest.cs +++ b/src/Samples.UITest/Writer.Test/UITest.cs @@ -1,10 +1,6 @@ using FlaUI.Core; using FlaUI.Core.AutomationElements; -using FlaUI.Core.Input; -using FlaUI.Core.Tools; -using FlaUI.UIA3; -using System.Reflection; -using System.Runtime.CompilerServices; +using System.Diagnostics; using UITest.Writer.Views; using Xunit; using Xunit.Abstractions; @@ -13,77 +9,33 @@ namespace UITest.Writer; -public class UITest : IDisposable +public abstract class UITest(ITestOutputHelper log) : UITestBase(log, "Writer.exe", + Environment.GetEnvironmentVariable("UITestExePath") ?? "out/Writer/Release/net8.0-windows/", + Environment.GetEnvironmentVariable("UITestOutputPath") ?? "out/Samples.UITest/Writer/") { - private readonly string outPath; - private readonly string executable; - private readonly string testOutPath; - private readonly List usedFiles = []; - private Application? app; - - static UITest() + public Application Launch(LaunchArguments? arguments = null, bool resetSettings = true) { - Mouse.MovePixelsPerMillisecond = 2; - Retry.DefaultTimeout = TimeSpan.FromSeconds(5); - Retry.DefaultInterval = TimeSpan.FromMilliseconds(250); - } - - public UITest(ITestOutputHelper log) - { - Log = log; - var assemblyPath = Assembly.GetAssembly(typeof(UITest))!.Location; - outPath = Path.GetFullPath(Path.Combine(assemblyPath, "../../../../../../../out/")); - executable = Path.Combine(outPath, "Writer/Release/net8.0-windows/writer.exe"); - testOutPath = Path.Combine(outPath, "Samples.UITest/Writer/"); - Directory.CreateDirectory(testOutPath); - Log.WriteLine($"OSVersion: {Environment.OSVersion}"); - Log.WriteLine($"ProcessorCount: {Environment.ProcessorCount}"); - Log.WriteLine($"MachineName: {Environment.MachineName}"); - Log.WriteLine($"UserInteractive: {Environment.UserInteractive}"); - Log.WriteLine($"AssemblyPath: {assemblyPath}"); - Log.WriteLine($"Executable: {executable}"); - Log.WriteLine($"TestOutPath: {testOutPath}"); - Automation = new() + Log.WriteLine(""); + if (resetSettings) { - ConnectionTimeout = TimeSpan.FromSeconds(5) - }; - } - - public ITestOutputHelper Log { get; } - - public UIA3Automation Automation { get; } - - public bool SkipAppClose { get; set; } = false; - - public Application Launch(LaunchArguments? arguments = null) - { + var productName = FileVersionInfo.GetVersionInfo(Executable).ProductName ?? throw new InvalidOperationException("Could not read the ProductName from the exe."); + var settingsFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), productName, "Settings", "Settings.xml"); + if (File.Exists(settingsFile)) File.Delete(settingsFile); + Log.WriteLine($"Delete settings: {settingsFile}"); + } var args = (arguments ?? new LaunchArguments()).ToArguments(); Log.WriteLine($"Launch: {args}"); - return app = Application.Launch(executable, args); - } - - public ShellWindow GetShellWindow() => app!.GetMainWindow(Automation).As(); - - public string GetTempFileName(string fileExtension) - { - var file = $"UITest_{Path.GetRandomFileName()}.{fileExtension}"; - file = Path.Combine(Path.GetTempPath(), file); - usedFiles.Add(file); - Log.WriteLine($"TempFile: {file}"); - return file; + return App = Application.Launch(Executable, args); } - public string GetScreenshotFile(string fileName, [CallerMemberName]string? memberName = null) - => Path.Combine(testOutPath, string.Join("-", new[] { memberName, fileName }.Where(x => !string.IsNullOrEmpty(x)))); + public ShellWindow GetShellWindow() => App!.GetMainWindow(Automation).As(); +} - public void Dispose() +public record LaunchArguments(string? UICulture = "en-US", string? Culture = "en-US", string? AdditionalArguments = null) : LaunchArgumentsBase +{ + public override string ToArguments() { - if (!SkipAppClose) app?.Close(); - app?.Dispose(); - Automation.Dispose(); - foreach (var file in usedFiles) - { - if (File.Exists(file)) File.Delete(file); - } + string?[] args = [CreateArg(UICulture), CreateArg(Culture), AdditionalArguments]; + return string.Join(" ", args.Where(x => !string.IsNullOrEmpty(x))); } -} +} \ No newline at end of file diff --git a/src/Samples.UITest/Writer.Test/Writer.Test.csproj b/src/Samples.UITest/Writer.Test/Writer.Test.csproj index eb8f2100..8c0708d6 100644 --- a/src/Samples.UITest/Writer.Test/Writer.Test.csproj +++ b/src/Samples.UITest/Writer.Test/Writer.Test.csproj @@ -8,9 +8,12 @@ - - - - + + + + + + + diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.config b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.config index a62adb65..7ead96e9 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.config +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.config @@ -14,8 +14,7 @@ - + Waf.BookLibrary.Reporting.Applications.dll Waf.BookLibrary.Reporting.Presentation.dll diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.xaml.cs b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.xaml.cs index e2941c4d..ea4533da 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.xaml.cs +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/App.xaml.cs @@ -1,4 +1,5 @@ -using NLog; +using Microsoft.Extensions.Configuration; +using NLog; using NLog.Targets; using NLog.Targets.Wrappers; using System.ComponentModel.Composition; @@ -30,7 +31,7 @@ private static readonly (string loggerNamePattern, LogLevel minLevel)[] logSetti private AggregateCatalog? catalog; private CompositionContainer? container; - private IEnumerable moduleControllers = Array.Empty(); + private IEnumerable moduleControllers = []; public App() { @@ -73,19 +74,36 @@ protected override void OnStartup(StartupEventArgs e) DispatcherUnhandledException += AppDispatcherUnhandledException; AppDomain.CurrentDomain.UnhandledException += AppDomainUnhandledException; #endif + AppConfig appConfig; + try + { + var config = new ConfigurationBuilder().AddCommandLine(Environment.GetCommandLineArgs()).Build(); + appConfig = config.Get() ?? new AppConfig(); + } + catch (Exception ex) + { + Log.Default.Error(ex, "Command line parsing error"); + appConfig = new AppConfig(); + } + catalog = new(); catalog.Catalogs.Add(new AssemblyCatalog(typeof(IMessageService).Assembly)); // WinApplicationFramework catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly())); // Waf.BookLibrary.Library.Presentation catalog.Catalogs.Add(new AssemblyCatalog(typeof(ShellViewModel).Assembly)); // Waf.BookLibrary.Library.Applications // Load module assemblies as well (e.g. Reporting extension). See App.config file. - foreach (var x in Settings.Default.ModuleAssemblies) catalog.Catalogs.Add(new AssemblyCatalog(x)); + var baseDir = AppContext.BaseDirectory; + foreach (var x in Settings.Default.ModuleAssemblies) + { + catalog.Catalogs.Add(new AssemblyCatalog(Path.Combine(baseDir, x!))); + } container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection); var batch = new CompositionBatch(); batch.AddExportedValue(container); container.Compose(batch); + InitializeCultures(appConfig); FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); moduleControllers = container.GetExportedValues(); @@ -102,6 +120,19 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } + private static void InitializeCultures(AppConfig appConfig) + { + try + { + if (!string.IsNullOrEmpty(appConfig.Culture)) Thread.CurrentThread.CurrentCulture = CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(appConfig.Culture); + if (!string.IsNullOrEmpty(appConfig.UICulture)) Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(appConfig.UICulture); + } + catch (Exception ex) + { + Log.Default.Error(ex, "The specified culture code is invalid"); + } + } + private void AppDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) => HandleException(e.Exception, false); private static void AppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) => HandleException(e.ExceptionObject as Exception, e.IsTerminating); diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/BookLibrary.Library.Presentation.csproj b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/BookLibrary.Library.Presentation.csproj index c01f0883..7479b4d8 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/BookLibrary.Library.Presentation.csproj +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/BookLibrary.Library.Presentation.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/AppConfig.cs b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/AppConfig.cs new file mode 100644 index 00000000..3c37d697 --- /dev/null +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/AppConfig.cs @@ -0,0 +1,8 @@ +namespace Waf.BookLibrary.Library.Presentation.Properties; + +public class AppConfig +{ + public string? Culture { get; init; } + + public string? UICulture { get; init; } +} diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.Designer.cs b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.Designer.cs index 77ead131..f69d25d4 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.Designer.cs +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Waf.BookLibrary.Library.Presentation.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -26,7 +26,7 @@ public static Settings Default { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute(@" - + Waf.BookLibrary.Reporting.Applications.dll Waf.BookLibrary.Reporting.Presentation.dll ")] diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.settings b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.settings index 3f7046f3..ee0ba01e 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.settings +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Properties/Settings.settings @@ -4,7 +4,7 @@ <?xml version="1.0" encoding="utf-16"?> -<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> +<ArrayOfString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <string>Waf.BookLibrary.Reporting.Applications.dll</string> <string>Waf.BookLibrary.Reporting.Presentation.dll</string> </ArrayOfString> diff --git a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Views/BookListView.xaml b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Views/BookListView.xaml index fdbb23dd..71c788be 100644 --- a/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Views/BookListView.xaml +++ b/src/System.Waf/Samples/BookLibrary/BookLibrary.Library.Presentation/Views/BookListView.xaml @@ -7,7 +7,7 @@ xmlns:waf="http://waf.codeplex.com/schemas" xmlns:dd="clr-namespace:Waf.BookLibrary.Library.Presentation.DesignData" mc:Ignorable="d" d:DataContext="{d:DesignInstance dd:SampleBookListViewModel, IsDesignTimeCreatable=True}" - d:DesignWidth="550" d:DesignHeight="150" + d:DesignWidth="550" d:DesignHeight="150" AutomationProperties.AutomationId="BookListView" waf:ValidationHelper.IsEnabled="true" waf:ValidationHelper.IsValid="{Binding IsValid, Mode=OneWayToSource}"> @@ -23,12 +23,12 @@