diff --git a/src/Avalonia.Samples/Avalonia.Samples.sln b/src/Avalonia.Samples/Avalonia.Samples.sln index eb697de..e0b70d7 100644 --- a/src/Avalonia.Samples/Avalonia.Samples.sln +++ b/src/Avalonia.Samples/Avalonia.Samples.sln @@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RectPainter", "Drawing\Rect EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnowflakesControlSample", "CustomControls\SnowflakesControlSample\SnowflakesControlSample.csproj", "{B7B246E1-26F7-4225-A8FC-4344C1D7BA17}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleToDoListTests", "CompleteApps\SimpleToDoListTests\SimpleToDoListTests.csproj", "{48320694-0277-45E1-9039-00A19F053171}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +145,10 @@ Global {B7B246E1-26F7-4225-A8FC-4344C1D7BA17}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7B246E1-26F7-4225-A8FC-4344C1D7BA17}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7B246E1-26F7-4225-A8FC-4344C1D7BA17}.Release|Any CPU.Build.0 = Release|Any CPU + {48320694-0277-45E1-9039-00A19F053171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48320694-0277-45E1-9039-00A19F053171}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48320694-0277-45E1-9039-00A19F053171}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48320694-0277-45E1-9039-00A19F053171}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -164,11 +170,10 @@ Global {B8CB5C57-07ED-4BC6-ACE8-F05E428E3EB5} = {85B157B3-F701-4F75-B4F1-EC2287729480} {BDA7536E-26FD-436F-AAC8-F8A2B500548E} = {85B157B3-F701-4F75-B4F1-EC2287729480} {F5CB3DA2-EB59-4792-A1B3-49F600F7C130} = {85B157B3-F701-4F75-B4F1-EC2287729480} - {48432457-6A55-4D03-9D40-260CE8E06440} = {2E99F15F-A82A-4734-A837-C0F768702600} - {0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA} = {2E99F15F-A82A-4734-A837-C0F768702600} {7D75B38A-304C-44DE-AC2F-8A461C7FC889} = {50FCF785-BBCF-4AFE-AC72-79EA9E92C43A} {2B746401-384F-484A-810E-7A65288165E0} = {D02161B3-8242-4BF5-96E9-780465A5023B} {B7B246E1-26F7-4225-A8FC-4344C1D7BA17} = {92C71AA7-E791-40C0-9F3A-2A85880B0439} + {48320694-0277-45E1-9039-00A19F053171} = {50FCF785-BBCF-4AFE-AC72-79EA9E92C43A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C246CAB0-0837-4EE4-A22D-28B3C74930B4} diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoList/SimpleToDoList.csproj b/src/Avalonia.Samples/CompleteApps/SimpleToDoList/SimpleToDoList.csproj index fdf49ea..052d78c 100644 --- a/src/Avalonia.Samples/CompleteApps/SimpleToDoList/SimpleToDoList.csproj +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoList/SimpleToDoList.csproj @@ -12,7 +12,6 @@ - @@ -22,4 +21,9 @@ + + + + + diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoList/Views/MainWindow.axaml b/src/Avalonia.Samples/CompleteApps/SimpleToDoList/Views/MainWindow.axaml index 2b6aef7..94484e5 100644 --- a/src/Avalonia.Samples/CompleteApps/SimpleToDoList/Views/MainWindow.axaml +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoList/Views/MainWindow.axaml @@ -51,11 +51,11 @@ - - diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Helper/AvaloniaTestMethodAttribute.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Helper/AvaloniaTestMethodAttribute.cs new file mode 100644 index 0000000..754410d --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Helper/AvaloniaTestMethodAttribute.cs @@ -0,0 +1,28 @@ +using System; +using global::Avalonia.Headless; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine.ClientProtocol; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; + +namespace Avalonia.Headless.MSTest; + +// +// Zusammenfassung: +// Identifies a nunit test that starts on Avalonia Dispatcher such that awaited +// expressions resume on the test's "main thread". +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +public sealed class AvaloniaTestMethodAttribute : TestMethodAttribute +{ + public override TestResult[] Execute(ITestMethod testMethod) + { + using var _session = HeadlessUnitTestSession.GetOrStartForAssembly(testMethod?.MethodInfo.DeclaringType?.Assembly); + { + return _session.Dispatch(() => ExecuteTestMethod(testMethod), default).GetAwaiter().GetResult(); + } + } + + // Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work. + private async Task ExecuteTestMethod(ITestMethod testMethod) + { + return [testMethod.Invoke(null)]; + } +} \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ProgramTests.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ProgramTests.cs new file mode 100644 index 0000000..4c1e333 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ProgramTests.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SimpleToDoList.Tests; + +[TestClass()] +public class ProgramTests +{ + [TestMethod()] + public void BuildAvaloniaAppTest() + { + var app = Program.BuildAvaloniaApp(); + + Assert.IsNotNull(app); + } +} diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Services/ToDoListFileServiceTests.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Services/ToDoListFileServiceTests.cs new file mode 100644 index 0000000..5161fc6 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Services/ToDoListFileServiceTests.cs @@ -0,0 +1,93 @@ +using SimpleToDoList.Models; + +namespace SimpleToDoList.Services.Tests; + +[TestClass()] +public class ToDoListFileServiceTests +{ + // The path to the file we are going to test + private string destPath; + + [TestInitialize] + public void TestInitialize() + { + destPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Avalonia.SimpleToDoList", "MyToDoList.txt"); + + // Move a file if it exists to a backup file + if (File.Exists(destPath)) + { + File.Move(destPath, Path.ChangeExtension(destPath, ".txorg")); + } + } + + [TestCleanup] + public void TestCleanup() + { + // Delete the file we created + if (File.Exists(destPath)) + { + File.Delete(destPath); + } + + // Move the backup file back + if (File.Exists(Path.ChangeExtension(destPath, ".txorg"))) + { + File.Move(Path.ChangeExtension(destPath, ".txorg"),destPath); + } + } + + + [DataTestMethod()] + [DataRow(new[] { "Hello World" }, new[] { true }, "[{\"IsChecked\":true,\"Content\":\"Hello World\"}]")] + [DataRow(new[] { "Get up", "Do chores" }, new[] { true, false }, "[{\"IsChecked\":true,\"Content\":\"Get up\"},{\"IsChecked\":false,\"Content\":\"Do chores\"}]")] + public void SaveToFileAsyncTest(string[] sAct, bool[] xAct, string sExp) + { + // Arrange + var items = sAct.Zip(xAct, (s, x) => new ToDoItem { Content = s, IsChecked = x }); + + // Act + Task t = ToDoListFileService.SaveToFileAsync(items); + t.Wait(); + + // Assert + Assert.IsTrue(File.Exists(destPath)); + Assert.AreEqual(sExp, File.ReadAllText(destPath)); + } + + [DataTestMethod()] + [DataRow(new[] { "Hello World" }, new[] { true }, "[{\"IsChecked\":true,\"Content\":\"Hello World\"}]")] + [DataRow(new[] { "Get up", "Do chores" }, new[] { true, false }, "[{\"IsChecked\":true,\"Content\":\"Get up\"},{\"IsChecked\":false,\"Content\":\"Do chores\"}]")] + public void LoadFromFileAsyncTest(string[] sExp, bool[] xExp, string sAct) + { + // Arrange + File.WriteAllText(destPath, sAct); + + // Act + Task?> t = ToDoListFileService.LoadFromFileAsync(); + t.Wait(); + + // Assert + Assert.IsNotNull(t.Result); + var items = t.Result; + Assert.AreEqual(sExp.Length, items.Count()); + for (int i = 0; i < sExp.Length; i++) + { + Assert.AreEqual(sExp[i], items.ElementAt(i).Content); + Assert.AreEqual(xExp[i], items.ElementAt(i).IsChecked); + } + } + + [TestMethod()] + public void LoadFromFileAsyncTest2() + { + // Act + Task?> t = ToDoListFileService.LoadFromFileAsync(); + t.Wait(); + + // Assert + Assert.IsNull(t.Result); + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/SimpleToDoListTests.csproj b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/SimpleToDoListTests.csproj new file mode 100644 index 0000000..dee3d49 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/SimpleToDoListTests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + latest + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/TestAppBuilder.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/TestAppBuilder.cs new file mode 100644 index 0000000..f70fa7b --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/TestAppBuilder.cs @@ -0,0 +1,31 @@ +// *********************************************************************** +// Assembly : SimpleToDoListTests +// Author : Joe Care +// Created : 01-12-2025 +// +// Last Modified By : Joe Care +// Last Modified On : 01-12-2025 +// *********************************************************************** +// +// Copyright (c) .... All rights reserved. +// +// +// *********************************************************************** +using Avalonia; +using Avalonia.Headless; +using SimpleToDoList; + +/// +/// Class TestAppBuilder. +/// +[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] + +public class TestAppBuilder +{ + /// + /// Builds the avalonia application. + /// + /// AppBuilder. + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()); +} \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/MainViewModelTests.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/MainViewModelTests.cs new file mode 100644 index 0000000..981c413 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/MainViewModelTests.cs @@ -0,0 +1,120 @@ +using Avalonia.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SimpleToDoList.ViewModels; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SimpleToDoList.ViewModels.Tests; + +[TestClass()] +public class MainViewModelTests +{ +#pragma warning disable CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. + private MainViewModel testModel; + private MainViewModel testModel2; +#pragma warning restore CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. + private string sTestLog = ""; + private bool xDesignMode; + + [TestInitialize()] + public void TestInitialize() + { + testModel = new MainViewModel(); + testModel.PropertyChanged += (object? sender, PropertyChangedEventArgs e) + => DoLog($"PropChg(Sender: {sender?.GetType().Name}, Prop: {e.PropertyName}), Value = ${sender?.GetType().GetProperty(e.PropertyName ?? "")?.GetValue(sender)}"); + testModel.AddItemCommand.CanExecuteChanged += (object? sender, EventArgs e) + => DoLog($"CmdChg(Sender: {sender?.GetType().Name}) = {sender?.GetType().GetMethod("CanExecute")!.Invoke(sender,[null])}"); + sTestLog = ""; + xDesignMode = Design.IsDesignMode; + typeof(Design).GetProperty("IsDesignMode")!.SetValue(null, true); + testModel2 = new MainViewModel(); + } + + [TestCleanup] + public void TestCleanup() + { + typeof(Design).GetProperty("IsDesignMode")!.SetValue(null, xDesignMode); + } + + private void DoLog(string v) + { + sTestLog += $"{v}\r\n"; // !! Fixed NewLine-Sequence + } + + [TestMethod()] + public void SetUpTest() + { + Assert.IsNotNull(testModel); + Assert.IsNotNull(testModel2); + Assert.IsInstanceOfType(testModel, typeof(MainViewModel)); + Assert.IsInstanceOfType(testModel, typeof(INotifyPropertyChanged)); + Assert.IsInstanceOfType(testModel2, typeof(MainViewModel)); + Assert.IsInstanceOfType(testModel2, typeof(INotifyPropertyChanged)); + Assert.IsNotNull(testModel.ToDoItems); + Assert.IsNotNull(testModel2.ToDoItems); + Assert.AreEqual(0, testModel.ToDoItems.Count); + Assert.AreEqual(2,testModel2.ToDoItems.Count); + } + + [DataTestMethod()] + [DataRow(new string[0], "")] + [DataRow(new[]{ "Test" }, @"PropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $Test\r\nCmdChg(Sender: RelayCommand) = True\r\n")] + [DataRow(new[]{ "Test2", null },@"PropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $Test2\r\nCmdChg(Sender: RelayCommand) = True\r\nPropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $\r\nCmdChg(Sender: RelayCommand) = False\r\n")] + [DataRow(new[]{ null, "Test3" }, @"PropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $Test3\r\nCmdChg(Sender: RelayCommand) = True\r\n")] + public void SetNewItemTest(string?[] asAct,string sExp) + { + // Act + foreach (string? s in asAct) + { + testModel.NewItemContent = s; + } + + // Assert + Assert.AreEqual(sExp.Replace("\\r\\n","\r\n"), sTestLog); + } + + [DataTestMethod()] + [DataRow("Test", 1, @"PropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $Test\r\nCmdChg(Sender: RelayCommand) = True\r\nPropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $\r\nCmdChg(Sender: RelayCommand) = False\r\n")] + [DataRow("",0, "PropChg(Sender: MainViewModel, Prop: NewItemContent), Value = $\\r\\nCmdChg(Sender: RelayCommand) = False\\r\\n")] + public void AddItemTest(string sAct,int iExp,string sExp) + { + // Arrange + testModel.NewItemContent = sAct; + // Act + if (testModel.AddItemCommand.CanExecute(null)) + testModel.AddItemCommand.Execute(null); + + // Assert + Assert.AreEqual(iExp, testModel.ToDoItems.Count); + if (iExp >0) + Assert.AreEqual(sAct, testModel.ToDoItems[0].Content); + Assert.AreEqual(iExp > 0?null:sAct, testModel.NewItemContent); + Assert.AreEqual( sExp.Replace("\\r\\n", "\r\n"), sTestLog); + } + + [DataTestMethod()] + [DataRow(new[] { "Test", "Test2" }, "Test" )] + [DataRow(new[] { "Test", "Test2" }, "Test2" )] + public void RemoveItemTest(string[] sAct, string sAct2) + { + // Arrange + foreach (string s in sAct) + { + testModel.ToDoItems.Add(new() {IsChecked =false,Content = s }); + } + var _act2 = testModel.ToDoItems.First(s=>s.Content==sAct2); + + // Act + if (testModel.RemoveItemCommand.CanExecute(_act2)) + testModel.RemoveItemCommand.Execute(_act2); + + // Assert + Assert.AreEqual(sAct.Length-1, testModel.ToDoItems.Count); + Assert.AreEqual("", sTestLog); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/ToDoItemViewModelTests.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/ToDoItemViewModelTests.cs new file mode 100644 index 0000000..e7c38a4 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/ViewModels/ToDoItemViewModelTests.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SimpleToDoList.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SimpleToDoList.ViewModels.Tests +{ + [TestClass()] + public class ToDoItemViewModelTests + { +#pragma warning disable CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. + private ToDoItemViewModel testModel; + private ToDoItemViewModel testModel2; +#pragma warning restore CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Fügen Sie ggf. den „erforderlichen“ Modifizierer hinzu, oder deklarieren Sie den Modifizierer als NULL-Werte zulassend. + private string sTestLog=""; + + [TestInitialize()] + public void TestInitialize() + { + testModel = new ToDoItemViewModel(); + testModel2 = new ToDoItemViewModel(new() {Content = "Test2" }); + testModel2.PropertyChanged += (sender, e) + => DoLog($"PropChg(Sender: {sender?.GetType().Name}, Prop: {e.PropertyName}), Value = ${sender?.GetType().GetProperty(e.PropertyName ?? "")?.GetValue(sender)}"); + } + private void DoLog(string v) + { + sTestLog += $"{v}\r\n"; // !! Fixed NewLine-Sequence + } + + [TestMethod()] + public void SetUpTest() + { + Assert.IsNotNull(testModel); + Assert.IsNotNull(testModel2); + Assert.IsInstanceOfType(testModel, typeof(ToDoItemViewModel)); + Assert.IsInstanceOfType(testModel2, typeof(ToDoItemViewModel)); + Assert.AreEqual("Test2", testModel2.Content); + Assert.IsFalse(testModel2.IsChecked); + Assert.AreEqual("", sTestLog); + } + + [DataTestMethod()] + public void GetToDoItemTest2() + { + //Act + var result = testModel2.GetToDoItem(); + + //Assert + Assert.IsNotNull(result); + Assert.AreEqual("Test2", result.Content); + Assert.IsFalse(result.IsChecked); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Views/MainWindowTests.cs b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Views/MainWindowTests.cs new file mode 100644 index 0000000..70c3319 --- /dev/null +++ b/src/Avalonia.Samples/CompleteApps/SimpleToDoListTests/Views/MainWindowTests.cs @@ -0,0 +1,41 @@ +using Avalonia.Headless.MSTest; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SimpleToDoList.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SimpleToDoList.ViewModels; +using Avalonia.Headless; +using Avalonia.Input; + +namespace SimpleToDoList.Views.Tests; + +[TestClass()] +public class MainWindowTests +{ + private MainViewModel _vm; + + [AvaloniaTestMethod()] + public void MainWindowTest() + { + var window = new MainWindow() + { + DataContext = _vm = new MainViewModel() + }; + + window.Show(); + + // Set values to the input boxes by simulating text input: + window.ItemInput.Focus(); + window.KeyTextInput("Do something"); + + // Raise click event on the button: + window.ItemAddButton.Focus(); + window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None); + + Assert.AreEqual(null,window.ItemInput.Text); + Assert.AreEqual(1, _vm.ToDoItems.Count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Samples/Testing/TestableApp.Appium/TestableApp.Appium.csproj b/src/Avalonia.Samples/Testing/TestableApp.Appium/TestableApp.Appium.csproj index 5b41a3f..8eeded5 100644 --- a/src/Avalonia.Samples/Testing/TestableApp.Appium/TestableApp.Appium.csproj +++ b/src/Avalonia.Samples/Testing/TestableApp.Appium/TestableApp.Appium.csproj @@ -1,4 +1,4 @@ - + net8.0