Skip to content

Commit 00e5965

Browse files
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/
1 parent 10fbbad commit 00e5965

File tree

7 files changed

+200
-9
lines changed

7 files changed

+200
-9
lines changed

Microsoft.Toolkit.Uwp.UI/Extensions/Tree/LogicalTree.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ public static IEnumerable<T> FindChildren<T>(this FrameworkElement element)
200200
}
201201

202202
/// <summary>
203-
/// Finds the logical parent element with the given name or returns null.
203+
/// 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.
204+
/// <seealso href="https://docs.microsoft.com/uwp/api/windows.ui.xaml.frameworkelement.parent#remarks"/>
204205
/// </summary>
205206
/// <param name="element">Child element.</param>
206207
/// <param name="name">Name of the control to find.</param>
@@ -226,7 +227,8 @@ public static FrameworkElement FindParentByName(this FrameworkElement element, s
226227
}
227228

228229
/// <summary>
229-
/// Find first logical parent control of a specified type.
230+
/// Find first logical parent control of a specified type. Note: Parent may only be set when the control is added to the VisualTree.
231+
/// <seealso href="https://docs.microsoft.com/uwp/api/windows.ui.xaml.frameworkelement.parent#remarks"/>
230232
/// </summary>
231233
/// <typeparam name="T">Type to search for.</typeparam>
232234
/// <param name="element">Child element.</param>

Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ public static class CompositionTargetHelper
1616
/// <summary>
1717
/// Provides a method to execute code after the rendering pass is completed.
1818
/// <seealso href="https://github.com/microsoft/microsoft-ui-xaml/blob/c045cde57c5c754683d674634a0baccda34d58c4/dev/dll/SharedHelpers.cpp#L399"/>
19+
/// <seealso href="https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/"/>
1920
/// </summary>
2021
/// <param name="action">Action to be executed after render pass</param>
22+
/// <param name="options"><see cref="TaskCreationOptions"/> for how to handle async calls with <see cref="TaskCompletionSource{TResult}"/>.</param>
2123
/// <returns>Awaitable Task</returns>
22-
public static Task<bool> ExecuteAfterCompositionRenderingAsync(Action action)
24+
public static Task<bool> ExecuteAfterCompositionRenderingAsync(Action action, TaskCreationOptions? options = null)
2325
{
2426
if (action is null)
2527
{
2628
ThrowArgumentNullException();
2729
}
2830

29-
var taskCompletionSource = new TaskCompletionSource<bool>();
31+
var taskCompletionSource = options.HasValue ? new TaskCompletionSource<bool>(options.Value)
32+
: new TaskCompletionSource<bool>();
3033

3134
try
3235
{
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.Toolkit.Uwp.Extensions;
2+
using Microsoft.Toolkit.Uwp.UI.Extensions;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Windows.UI.Xaml;
11+
using Windows.UI.Xaml.Controls;
12+
using Windows.UI.Xaml.Markup;
13+
14+
namespace UnitTests.Extensions
15+
{
16+
[TestClass]
17+
public class Test_LogicalTreeExtensions : VisualUITestBase
18+
{
19+
[TestCategory("LogicalTree")]
20+
[TestMethod]
21+
public async Task Test_LogicalTree_FindParent_Exists()
22+
{
23+
await App.DispatcherQueue.EnqueueAsync(async () =>
24+
{
25+
var treeRoot = XamlReader.Load(@"<Page
26+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
27+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
28+
<Grid>
29+
<Grid> <!-- Target -->
30+
<Border/>
31+
<Border>
32+
<TextBlock/> <!-- Starting Point -->
33+
</Border>
34+
</Grid>
35+
</Grid>
36+
</Page>") as Page;
37+
38+
// Test Setup
39+
Assert.IsNotNull(treeRoot, "XAML Failed to Load");
40+
41+
// Initialize Visual Tree
42+
await SetTestContentAsync(treeRoot);
43+
44+
var outerGrid = treeRoot.Content as Grid;
45+
46+
Assert.IsNotNull(outerGrid, "Couldn't find Page content.");
47+
48+
var targetGrid = outerGrid.Children.FirstOrDefault() as Grid;
49+
Assert.IsNotNull(targetGrid, "Couldn't find Target Grid");
50+
Assert.AreEqual(2, targetGrid.Children.Count, "Grid doesn't have right number of children.");
51+
52+
var secondBorder = targetGrid.Children[1] as Border;
53+
Assert.IsNotNull(secondBorder, "Border not found.");
54+
55+
var startingPoint = secondBorder.Child as FrameworkElement;
56+
Assert.IsNotNull(startingPoint, "Could not find starting element.");
57+
58+
// Main Test
59+
var grid = startingPoint.FindParent<Grid>();
60+
61+
Assert.IsNotNull(grid, "Expected to find Grid");
62+
Assert.AreEqual(targetGrid, grid, "Grid didn't match expected.");
63+
});
64+
}
65+
}
66+
}

UnitTests/UnitTests.UWP/UI/Controls/Test_TextToolbar_Localization.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void Test_TextToolbar_Localization_Override()
7979
[TestMethod]
8080
public async Task Test_TextToolbar_Localization_Override_Fr()
8181
{
82-
await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(async () =>
82+
await App.DispatcherQueue.EnqueueAsync(async () =>
8383
{
8484
// Just double-check we've got the right environment setup in our tests.
8585
CollectionAssert.AreEquivalent(new string[] { "en-US", "fr" }, ApplicationLanguages.ManifestLanguages.ToArray(), "Missing locales for test");

UnitTests/UnitTests.UWP/UnitTestApp.xaml.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
6+
using UnitTests.Extensions;
77
using Windows.ApplicationModel;
88
using Windows.ApplicationModel.Activation;
9+
using Windows.ApplicationModel.Core;
10+
using Windows.System;
911
using Windows.UI.Xaml;
1012
using Windows.UI.Xaml.Controls;
13+
using Windows.UI.Xaml.Media;
1114
using Windows.UI.Xaml.Navigation;
1215

1316
namespace UnitTests
@@ -17,6 +20,31 @@ namespace UnitTests
1720
/// </summary>
1821
public partial class App : Application
1922
{
23+
// Holder for test content to abstract Window.Current.Content
24+
public static FrameworkElement ContentRoot
25+
{
26+
get
27+
{
28+
var rootFrame = Window.Current.Content as Frame;
29+
return rootFrame.Content as FrameworkElement;
30+
}
31+
32+
set
33+
{
34+
var rootFrame = Window.Current.Content as Frame;
35+
rootFrame.Content = value;
36+
}
37+
}
38+
39+
// Abstract CoreApplication.MainView.DispatcherQueue
40+
public static DispatcherQueue DispatcherQueue
41+
{
42+
get
43+
{
44+
return CoreApplication.MainView.DispatcherQueue;
45+
}
46+
}
47+
2048
/// <summary>
2149
/// Initializes a new instance of the <see cref="App"/> class.
2250
/// Initializes the singleton application object. This is the first line of authored code
@@ -50,7 +78,10 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
5078
if (rootFrame == null)
5179
{
5280
// Create a Frame to act as the navigation context and navigate to the first page
53-
rootFrame = new Frame();
81+
rootFrame = new Frame()
82+
{
83+
CacheSize = 0 // Prevent any test pages from being cached
84+
};
5485

5586
rootFrame.NavigationFailed += OnNavigationFailed;
5687

UnitTests/UnitTests.UWP/UnitTests.UWP.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@
114114
<Version>6.2.10</Version>
115115
</PackageReference>
116116
<PackageReference Include="MSTest.TestAdapter">
117-
<Version>2.1.0</Version>
117+
<Version>2.1.2</Version>
118118
</PackageReference>
119119
<PackageReference Include="MSTest.TestFramework">
120-
<Version>2.1.0</Version>
120+
<Version>2.1.2</Version>
121121
</PackageReference>
122122
<PackageReference Include="Newtonsoft.Json">
123123
<Version>10.0.3</Version>
@@ -150,6 +150,7 @@
150150
<Compile Include="Extensions\Test_FontIconExtensionMarkupExtension.cs" />
151151
<Compile Include="Extensions\Test_EnumValuesExtension.cs" />
152152
<Compile Include="Extensions\Test_NullableBoolMarkupExtension.cs" />
153+
<Compile Include="Extensions\Test_LogicalTreeExtensions.cs" />
153154
<Compile Include="Geometry\Test_CanvasPathGeometry.cs" />
154155
<Compile Include="Geometry\Test_RegexFactory.cs" />
155156
<Compile Include="Geometry\Test_Utils.cs" />
@@ -208,6 +209,7 @@
208209
<Compile Include="UnitTestApp.xaml.cs">
209210
<DependentUpon>UnitTestApp.xaml</DependentUpon>
210211
</Compile>
212+
<Compile Include="VisualUITestBase.cs" />
211213
</ItemGroup>
212214
<ItemGroup>
213215
<ApplicationDefinition Include="UnitTestApp.xaml">
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Toolkit.Uwp.Extensions;
6+
using Microsoft.Toolkit.Uwp.UI.Helpers;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using System;
9+
using System.Threading.Tasks;
10+
using Windows.UI.Xaml;
11+
12+
namespace UnitTests
13+
{
14+
/// <summary>
15+
/// Base class to be used in API tests which require UI layout or rendering to occur first.
16+
/// For more E2E scenarios or testing components for user interation, see integration test suite instead.
17+
/// 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).
18+
/// </summary>
19+
public class VisualUITestBase
20+
{
21+
/// <summary>
22+
/// Sets the content of the test app to a simple <see cref="FrameworkElement"/> to load into the visual tree.
23+
/// Waits for that element to be loaded and rendered before returning.
24+
/// </summary>
25+
/// <param name="content">Content to set in test app.</param>
26+
/// <returns>When UI is loaded.</returns>
27+
protected Task<bool> SetTestContentAsync(FrameworkElement content, TaskCreationOptions? options = null)
28+
{
29+
var taskCompletionSource = options.HasValue ? new TaskCompletionSource<bool>(options.Value)
30+
: new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
31+
32+
App.DispatcherQueue.EnqueueAsync(() =>
33+
{
34+
async void Callback(object sender, RoutedEventArgs args)
35+
{
36+
content.Loaded -= Callback;
37+
38+
// Wait for first Render pass
39+
await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }, TaskCreationOptions.RunContinuationsAsynchronously);
40+
41+
taskCompletionSource.SetResult(true);
42+
}
43+
44+
// Going to wait for our original content to unload
45+
content.Loaded += Callback;
46+
47+
// Trigger that now
48+
try
49+
{
50+
App.ContentRoot = content;
51+
}
52+
catch (Exception e)
53+
{
54+
taskCompletionSource.SetException(e);
55+
}
56+
});
57+
58+
return taskCompletionSource.Task;
59+
}
60+
61+
[TestCleanup]
62+
public async Task Cleanup()
63+
{
64+
var taskCompletionSource = new TaskCompletionSource<bool>();
65+
66+
// Need to await TaskCompletionSource before Task
67+
// See https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
68+
await taskCompletionSource.Task;
69+
70+
await App.DispatcherQueue.EnqueueAsync(() =>
71+
{
72+
void Callback(object sender, RoutedEventArgs args)
73+
{
74+
App.ContentRoot.Unloaded -= Callback;
75+
76+
taskCompletionSource.SetResult(true);
77+
}
78+
79+
// Going to wait for our original content to unload
80+
App.ContentRoot.Unloaded += Callback;
81+
82+
// Trigger that now
83+
App.ContentRoot = null;
84+
});
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)