From 00e5965bf7655bf4e747bce14d3ecff6d284fcf8 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 9 Feb 2021 16:15:15 -0800 Subject: [PATCH 1/3] Add an initial Logical Tree Extension FindParent method test Required some new Test Infrastructure adapted from WinUI setup, but simplified. However, running into deadlock with TaskCompletionSource after test execution (test itself is succeeding) This may help? https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ --- .../Extensions/Tree/LogicalTree.cs | 6 +- .../Helpers/CompositionTargetHelper.cs | 7 +- .../Extensions/Test_LogicalTreeExtensions.cs | 66 ++++++++++++++ .../Controls/Test_TextToolbar_Localization.cs | 2 +- UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs | 35 +++++++- UnitTests/UnitTests.UWP/UnitTests.UWP.csproj | 6 +- UnitTests/UnitTests.UWP/VisualUITestBase.cs | 87 +++++++++++++++++++ 7 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs create mode 100644 UnitTests/UnitTests.UWP/VisualUITestBase.cs diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Tree/LogicalTree.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Tree/LogicalTree.cs index df77539061b..a09471a4edb 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/Tree/LogicalTree.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Tree/LogicalTree.cs @@ -200,7 +200,8 @@ public static IEnumerable FindChildren(this FrameworkElement element) } /// - /// Finds the logical parent element with the given name or returns null. + /// Finds the logical parent element with the given name or returns null. Note: Parent may only be set when the control is added to the VisualTree. + /// /// /// Child element. /// Name of the control to find. @@ -226,7 +227,8 @@ public static FrameworkElement FindParentByName(this FrameworkElement element, s } /// - /// Find first logical parent control of a specified type. + /// Find first logical parent control of a specified type. Note: Parent may only be set when the control is added to the VisualTree. + /// /// /// Type to search for. /// Child element. diff --git a/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs b/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs index 24a7bc18801..f718d650f78 100644 --- a/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs +++ b/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs @@ -16,17 +16,20 @@ public static class CompositionTargetHelper /// /// Provides a method to execute code after the rendering pass is completed. /// + /// /// /// Action to be executed after render pass + /// for how to handle async calls with . /// Awaitable Task - public static Task ExecuteAfterCompositionRenderingAsync(Action action) + public static Task ExecuteAfterCompositionRenderingAsync(Action action, TaskCreationOptions? options = null) { if (action is null) { ThrowArgumentNullException(); } - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = options.HasValue ? new TaskCompletionSource(options.Value) + : new TaskCompletionSource(); try { diff --git a/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs b/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs new file mode 100644 index 00000000000..d7f567954b8 --- /dev/null +++ b/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs @@ -0,0 +1,66 @@ +using Microsoft.Toolkit.Uwp.Extensions; +using Microsoft.Toolkit.Uwp.UI.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Markup; + +namespace UnitTests.Extensions +{ + [TestClass] + public class Test_LogicalTreeExtensions : VisualUITestBase + { + [TestCategory("LogicalTree")] + [TestMethod] + public async Task Test_LogicalTree_FindParent_Exists() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var outerGrid = treeRoot.Content as Grid; + + Assert.IsNotNull(outerGrid, "Couldn't find Page content."); + + var targetGrid = outerGrid.Children.FirstOrDefault() as Grid; + Assert.IsNotNull(targetGrid, "Couldn't find Target Grid"); + Assert.AreEqual(2, targetGrid.Children.Count, "Grid doesn't have right number of children."); + + var secondBorder = targetGrid.Children[1] as Border; + Assert.IsNotNull(secondBorder, "Border not found."); + + var startingPoint = secondBorder.Child as FrameworkElement; + Assert.IsNotNull(startingPoint, "Could not find starting element."); + + // Main Test + var grid = startingPoint.FindParent(); + + Assert.IsNotNull(grid, "Expected to find Grid"); + Assert.AreEqual(targetGrid, grid, "Grid didn't match expected."); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_TextToolbar_Localization.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_TextToolbar_Localization.cs index 97de47cb3a9..6d9dbe48dff 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_TextToolbar_Localization.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_TextToolbar_Localization.cs @@ -79,7 +79,7 @@ public void Test_TextToolbar_Localization_Override() [TestMethod] public async Task Test_TextToolbar_Localization_Override_Fr() { - await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(async () => + await App.DispatcherQueue.EnqueueAsync(async () => { // Just double-check we've got the right environment setup in our tests. CollectionAssert.AreEquivalent(new string[] { "en-US", "fr" }, ApplicationLanguages.ManifestLanguages.ToArray(), "Missing locales for test"); diff --git a/UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs b/UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs index 2091342e30d..c7092bb133b 100644 --- a/UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs +++ b/UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs @@ -3,11 +3,14 @@ // See the LICENSE file in the project root for more information. using System; - +using UnitTests.Extensions; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; +using Windows.ApplicationModel.Core; +using Windows.System; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace UnitTests @@ -17,6 +20,31 @@ namespace UnitTests /// public partial class App : Application { + // Holder for test content to abstract Window.Current.Content + public static FrameworkElement ContentRoot + { + get + { + var rootFrame = Window.Current.Content as Frame; + return rootFrame.Content as FrameworkElement; + } + + set + { + var rootFrame = Window.Current.Content as Frame; + rootFrame.Content = value; + } + } + + // Abstract CoreApplication.MainView.DispatcherQueue + public static DispatcherQueue DispatcherQueue + { + get + { + return CoreApplication.MainView.DispatcherQueue; + } + } + /// /// Initializes a new instance of the class. /// Initializes the singleton application object. This is the first line of authored code @@ -50,7 +78,10 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) if (rootFrame == null) { // Create a Frame to act as the navigation context and navigate to the first page - rootFrame = new Frame(); + rootFrame = new Frame() + { + CacheSize = 0 // Prevent any test pages from being cached + }; rootFrame.NavigationFailed += OnNavigationFailed; diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index ce29975833b..b55d63b2122 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -114,10 +114,10 @@ 6.2.10 - 2.1.0 + 2.1.2 - 2.1.0 + 2.1.2 10.0.3 @@ -150,6 +150,7 @@ + @@ -208,6 +209,7 @@ UnitTestApp.xaml + diff --git a/UnitTests/UnitTests.UWP/VisualUITestBase.cs b/UnitTests/UnitTests.UWP/VisualUITestBase.cs new file mode 100644 index 00000000000..222e56396f5 --- /dev/null +++ b/UnitTests/UnitTests.UWP/VisualUITestBase.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Toolkit.Uwp.Extensions; +using Microsoft.Toolkit.Uwp.UI.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; +using Windows.UI.Xaml; + +namespace UnitTests +{ + /// + /// Base class to be used in API tests which require UI layout or rendering to occur first. + /// For more E2E scenarios or testing components for user interation, see integration test suite instead. + /// Use this class when an API needs direct access to test functions of the UI itself in more simplistic scenarios (i.e. visual tree helpers). + /// + public class VisualUITestBase + { + /// + /// Sets the content of the test app to a simple to load into the visual tree. + /// Waits for that element to be loaded and rendered before returning. + /// + /// Content to set in test app. + /// When UI is loaded. + protected Task SetTestContentAsync(FrameworkElement content, TaskCreationOptions? options = null) + { + var taskCompletionSource = options.HasValue ? new TaskCompletionSource(options.Value) + : new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + App.DispatcherQueue.EnqueueAsync(() => + { + async void Callback(object sender, RoutedEventArgs args) + { + content.Loaded -= Callback; + + // Wait for first Render pass + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }, TaskCreationOptions.RunContinuationsAsynchronously); + + taskCompletionSource.SetResult(true); + } + + // Going to wait for our original content to unload + content.Loaded += Callback; + + // Trigger that now + try + { + App.ContentRoot = content; + } + catch (Exception e) + { + taskCompletionSource.SetException(e); + } + }); + + return taskCompletionSource.Task; + } + + [TestCleanup] + public async Task Cleanup() + { + var taskCompletionSource = new TaskCompletionSource(); + + // Need to await TaskCompletionSource before Task + // See https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + await taskCompletionSource.Task; + + await App.DispatcherQueue.EnqueueAsync(() => + { + void Callback(object sender, RoutedEventArgs args) + { + App.ContentRoot.Unloaded -= Callback; + + taskCompletionSource.SetResult(true); + } + + // Going to wait for our original content to unload + App.ContentRoot.Unloaded += Callback; + + // Trigger that now + App.ContentRoot = null; + }); + } + } +} From bd5cd48ea182f03f35a6d8366ef66bbaf8df0a8f Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 9 Feb 2021 20:19:31 -0800 Subject: [PATCH 2/3] Fixed deadlock issues with TaskCompletionSource, Test passes locally now! --- UnitTests/UnitTests.UWP/UnitTests.UWP.csproj | 2 +- UnitTests/UnitTests.UWP/VisualUITestBase.cs | 30 +++++++------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index b55d63b2122..3d06f84563d 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -18,7 +18,7 @@ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} $(VisualStudioVersion) false - 8.0 + 9.0 diff --git a/UnitTests/UnitTests.UWP/VisualUITestBase.cs b/UnitTests/UnitTests.UWP/VisualUITestBase.cs index 222e56396f5..33338e92ac0 100644 --- a/UnitTests/UnitTests.UWP/VisualUITestBase.cs +++ b/UnitTests/UnitTests.UWP/VisualUITestBase.cs @@ -24,19 +24,18 @@ public class VisualUITestBase /// /// Content to set in test app. /// When UI is loaded. - protected Task SetTestContentAsync(FrameworkElement content, TaskCreationOptions? options = null) + protected Task SetTestContentAsync(FrameworkElement content) { - var taskCompletionSource = options.HasValue ? new TaskCompletionSource(options.Value) - : new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - App.DispatcherQueue.EnqueueAsync(() => + return App.DispatcherQueue.EnqueueAsync(() => { + var taskCompletionSource = new TaskCompletionSource(); + async void Callback(object sender, RoutedEventArgs args) { content.Loaded -= Callback; // Wait for first Render pass - await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }, TaskCreationOptions.RunContinuationsAsynchronously); + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); taskCompletionSource.SetResult(true); } @@ -53,9 +52,9 @@ async void Callback(object sender, RoutedEventArgs args) { taskCompletionSource.SetException(e); } - }); - return taskCompletionSource.Task; + return taskCompletionSource.Task; + }); } [TestCleanup] @@ -63,25 +62,16 @@ public async Task Cleanup() { var taskCompletionSource = new TaskCompletionSource(); - // Need to await TaskCompletionSource before Task - // See https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ - await taskCompletionSource.Task; - await App.DispatcherQueue.EnqueueAsync(() => { - void Callback(object sender, RoutedEventArgs args) - { - App.ContentRoot.Unloaded -= Callback; - - taskCompletionSource.SetResult(true); - } - // Going to wait for our original content to unload - App.ContentRoot.Unloaded += Callback; + App.ContentRoot.Unloaded += (_, _) => taskCompletionSource.SetResult(true); // Trigger that now App.ContentRoot = null; }); + + await taskCompletionSource.Task; } } } From e6a0257eb75a084003a7ae4092594f13cbdaeef2 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 10 Feb 2021 00:24:20 -0800 Subject: [PATCH 3/3] Add Missing Header --- .../Extensions/Test_LogicalTreeExtensions.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs b/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs index d7f567954b8..a6bdf804be1 100644 --- a/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs +++ b/UnitTests/UnitTests.UWP/Extensions/Test_LogicalTreeExtensions.cs @@ -1,12 +1,12 @@ -using Microsoft.Toolkit.Uwp.Extensions; -using Microsoft.Toolkit.Uwp.UI.Extensions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System.Linq; -using System.Text; using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp.Extensions; +using Microsoft.Toolkit.Uwp.UI.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup;