Skip to content

Commit 2a514d4

Browse files
Port over original test code from llama/input-injection-test
See: CommunityToolkit/Labs-Windows#183
1 parent 58649b8 commit 2a514d4

8 files changed

+280
-9
lines changed

CommunityToolkit.Tests.Shared/App.xaml.cs

+11-9
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
using Windows.UI.Xaml.Input;
1616
using Windows.UI.Xaml.Media;
1717
using Windows.UI.Xaml.Navigation;
18+
using System.Runtime.InteropServices;
1819
#else
1920
using Microsoft.UI.Dispatching;
21+
using Microsoft.UI.Windowing;
2022
using Microsoft.UI.Xaml;
2123
using Microsoft.UI.Xaml.Controls;
2224
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -37,16 +39,16 @@ public sealed partial class App : Application
3739
// MacOS and iOS don't know the correct type without a full namespace declaration, confusing it with NSWindow and UIWindow.
3840
// Using static will not work.
3941
#if WINAPPSDK
40-
private static Microsoft.UI.Xaml.Window currentWindow = Microsoft.UI.Xaml.Window.Current;
42+
public static Microsoft.UI.Xaml.Window CurrentWindow { get; private set; } = Microsoft.UI.Xaml.Window.Current;
4143
#else
42-
private static Windows.UI.Xaml.Window currentWindow = Windows.UI.Xaml.Window.Current;
44+
public static Windows.UI.Xaml.Window CurrentWindow { get; private set; } = Windows.UI.Xaml.Window.Current;
4345
#endif
4446

4547
// Holder for test content to abstract Window.Current.Content
4648
public static FrameworkElement? ContentRoot
4749
{
48-
get => currentWindow.Content as FrameworkElement;
49-
set => currentWindow.Content = value;
50+
get => CurrentWindow.Content as FrameworkElement;
51+
set => CurrentWindow.Content = value;
5052
}
5153

5254
// Abstract CoreApplication.MainView.DispatcherQueue
@@ -57,7 +59,7 @@ public static DispatcherQueue DispatcherQueue
5759
#if !WINAPPSDK
5860
return CoreApplication.MainView.DispatcherQueue;
5961
#else
60-
return currentWindow.DispatcherQueue;
62+
return CurrentWindow.DispatcherQueue;
6163
#endif
6264
}
6365
}
@@ -79,23 +81,23 @@ public App()
7981
protected override void OnLaunched(LaunchActivatedEventArgs e)
8082
{
8183
#if WINAPPSDK
82-
currentWindow = new Window();
84+
CurrentWindow = new Window();
8385
#endif
8486

8587
// Do not repeat app initialization when the Window already has content,
8688
// just ensure that the window is active
87-
if (currentWindow.Content is not Frame rootFrame)
89+
if (CurrentWindow.Content is not Frame rootFrame)
8890
{
8991
// Create a Frame to act as the navigation context and navigate to the first page
90-
currentWindow.Content = rootFrame = new Frame();
92+
CurrentWindow.Content = rootFrame = new Frame();
9193

9294
rootFrame.NavigationFailed += OnNavigationFailed;
9395
}
9496

9597
////Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
9698

9799
// Ensure the current window is active
98-
currentWindow.Activate();
100+
CurrentWindow.Activate();
99101

100102
#if !WINAPPSDK
101103
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(e.Arguments);

CommunityToolkit.Tests.Shared/CommunityToolkit.Tests.Shared.projitems

+7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@
1818
<Compile Include="$(MSBuildThisFileDirectory)App.xaml.cs">
1919
<DependentUpon>App.xaml</DependentUpon>
2020
</Compile>
21+
<Compile Include="$(MSBuildThisFileDirectory)Input\InputHelpers.cs" />
22+
<Compile Include="$(MSBuildThisFileDirectory)Input\InputSimulator.cs" />
23+
<Compile Include="$(MSBuildThisFileDirectory)Input\InputSimulator.Bounds.cs" />
24+
<Compile Include="$(MSBuildThisFileDirectory)Input\InputSimulator.Touch.cs" />
2125
<Compile Include="$(MSBuildThisFileDirectory)Internal\CompositionTargetHelper.cs" />
2226
<Compile Include="$(MSBuildThisFileDirectory)Log.cs" />
2327
<Compile Include="$(MSBuildThisFileDirectory)VisualUITestBase.cs" />
2428
</ItemGroup>
29+
<ItemGroup>
30+
<AdditionalFiles Include="$(MSBuildThisFileDirectory)NativeMethods.txt" />
31+
</ItemGroup>
2532
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
#if WINAPPSDK
6+
using MainWindow = Microsoft.UI.Xaml.Window;
7+
#else
8+
using MainWindow = Windows.UI.Xaml.Window;
9+
#endif
10+
11+
namespace CommunityToolkit.Tests.Input;
12+
13+
public static class InputHelpers
14+
{
15+
/// <summary>
16+
/// Helper extension method to create a new <see cref="InputSimulator"/> chain for the current window.
17+
/// </summary>
18+
/// <param name="window"><see cref="Window"/> class for your application.</param>
19+
/// <returns>A new <see cref="InputSimulator"/> instance for that window.</returns>
20+
public static InputSimulator InjectInput(this MainWindow window)
21+
{
22+
return new InputSimulator(window);
23+
}
24+
25+
public static Point CoordinatesToCenter(this UIElement parent, UIElement target)
26+
{
27+
var location = target.TransformToVisual(parent).TransformPoint(default(Point));
28+
return new(location.X + target.ActualSize.X / 2, location.Y + target.ActualSize.Y / 2);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
#if WINAPPSDK
6+
using Windows.Win32;
7+
using Windows.Win32.Foundation;
8+
using Win32Rect = Windows.Win32.Foundation.RECT;
9+
using Win32Point = System.Drawing.Point;
10+
#endif
11+
12+
namespace CommunityToolkit.Tests.Input;
13+
14+
//// This polyfill is needed as the WindowsAppSDK doesn't provide client based coordinates. See Issue: TODO: File bug currentWindow.Bounds should be the same between the two platforms. Should AppWindow also have Bounds?
15+
16+
public partial class InputSimulator
17+
{
18+
#if WINAPPSDK
19+
private Rect Bounds
20+
{
21+
get
22+
{
23+
if (_currentWindowRef.TryGetTarget(out var currentWindow))
24+
{
25+
var hWnd = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(currentWindow);
26+
27+
// Get client area position
28+
Win32Point[] points = new Win32Point[1];
29+
PInvoke.MapWindowPoints(hWnd, HWND.Null, points);
30+
31+
// TODO: Check LastError?
32+
33+
// And size
34+
if (points.Length == 1 && PInvoke.GetClientRect(hWnd, out Win32Rect size))
35+
{
36+
return new Rect(points[0].X, points[0].Y, size.right - size.left, size.bottom - size.top);
37+
}
38+
}
39+
40+
return default;
41+
}
42+
}
43+
#else
44+
private Rect Bounds => _currentWindowRef.TryGetTarget(out Window window) ? window.Bounds : default;
45+
#endif
46+
47+
private Point TranslatePointForWindow(Point point)
48+
{
49+
// TODO: Do we want a ToPoint extension in the Toolkit? (is there an existing enum we can use to specify which corner/point of the rect? e.g. topleft, center, middleright, etc...?
50+
51+
// Get the top left screen coordinates of the app window rect.
52+
var bounds = Bounds; // Don't double-retrieve the calculated property, TODO: Just make some point helpers (or use ones in Toolkit)
53+
Point appBoundsTopLeft = new Point(bounds.Left, bounds.Top);
54+
55+
// Create the point for input injection and calculate its screen location.
56+
return new Point(
57+
appBoundsTopLeft.X + point.X,
58+
appBoundsTopLeft.Y + point.Y);
59+
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 Windows.UI.Input.Preview.Injection;
6+
7+
namespace CommunityToolkit.Tests.Input;
8+
9+
public partial class InputSimulator
10+
{
11+
public void StartTouch()
12+
{
13+
Assert.IsNotNull(_input);
14+
15+
_input.InitializeTouchInjection(
16+
InjectedInputVisualizationMode.Default);
17+
}
18+
19+
/// <summary>
20+
/// Simulates a touch press on screen at the coordinates provided, in app-local coordinates. For instance use <code>App.ContentRoot.CoordinatesTo(element)</code>.
21+
/// </summary>
22+
/// <param name="point"></param>
23+
/// <returns></returns>
24+
public uint TouchDown(Point point)
25+
{
26+
// Create a unique pointer ID for the injected touch pointer.
27+
// Multiple input pointers would require more robust handling.
28+
uint pointerId = _currentPointerId++;
29+
30+
var injectionPoint = TranslatePointForWindow(point);
31+
32+
// Create a touch data point for pointer down.
33+
// Each element in the touch data list represents a single touch contact.
34+
// For this example, we're mirroring a single mouse pointer.
35+
List<InjectedInputTouchInfo> touchData = new()
36+
{
37+
new()
38+
{
39+
Contact = new InjectedInputRectangle
40+
{
41+
Left = 30, Top = 30, Bottom = 30, Right = 30
42+
},
43+
PointerInfo = new InjectedInputPointerInfo
44+
{
45+
PointerId = pointerId,
46+
PointerOptions =
47+
InjectedInputPointerOptions.PointerDown |
48+
InjectedInputPointerOptions.InContact |
49+
InjectedInputPointerOptions.New,
50+
TimeOffsetInMilliseconds = 0,
51+
PixelLocation = new InjectedInputPoint
52+
{
53+
PositionX = (int)injectionPoint.X ,
54+
PositionY = (int)injectionPoint.Y
55+
}
56+
},
57+
Pressure = 1.0,
58+
TouchParameters =
59+
InjectedInputTouchParameters.Pressure |
60+
InjectedInputTouchParameters.Contact
61+
}
62+
};
63+
64+
// Inject the touch input.
65+
_input.InjectTouchInput(touchData);
66+
67+
return pointerId;
68+
}
69+
70+
public void TouchMove(uint pointerId, int cX, int cY)
71+
{
72+
// Create a touch data point for pointer up.
73+
List<InjectedInputTouchInfo> touchData = new()
74+
{
75+
new()
76+
{
77+
Contact = new InjectedInputRectangle
78+
{
79+
Left = 30, Top = 30, Bottom = 30, Right = 30
80+
},
81+
PointerInfo = new InjectedInputPointerInfo
82+
{
83+
PointerId = pointerId,
84+
PointerOptions =
85+
InjectedInputPointerOptions.InRange |
86+
InjectedInputPointerOptions.InContact,
87+
TimeOffsetInMilliseconds = 0,
88+
PixelLocation = new InjectedInputPoint
89+
{
90+
PositionX = (int)cX ,
91+
PositionY = (int)cY
92+
}
93+
},
94+
Pressure = 1.0,
95+
TouchParameters =
96+
InjectedInputTouchParameters.Pressure |
97+
InjectedInputTouchParameters.Contact
98+
}
99+
};
100+
101+
// Inject the touch input.
102+
_input.InjectTouchInput(touchData);
103+
}
104+
105+
public void TouchUp(uint pointerId)
106+
{
107+
Assert.IsNotNull(_input);
108+
109+
// Create a touch data point for pointer up.
110+
List<InjectedInputTouchInfo> touchData = new()
111+
{
112+
new()
113+
{
114+
PointerInfo = new InjectedInputPointerInfo
115+
{
116+
PointerId = pointerId,
117+
PointerOptions = InjectedInputPointerOptions.PointerUp
118+
}
119+
}
120+
};
121+
122+
// Inject the touch input.
123+
_input.InjectTouchInput(touchData);
124+
}
125+
126+
public void StopTouch()
127+
{
128+
// Shut down the virtual input device.
129+
_input.UninitializeTouchInjection();
130+
}
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 Windows.UI.Input.Preview.Injection;
6+
7+
#if WINAPPSDK
8+
using MainWindow = Microsoft.UI.Xaml.Window;
9+
#else
10+
using MainWindow = Windows.UI.Xaml.Window;
11+
#endif
12+
13+
namespace CommunityToolkit.Tests.Input;
14+
15+
public partial class InputSimulator
16+
{
17+
private static InputInjector _input = InputInjector.TryCreate();
18+
private static uint _currentPointerId = 0;
19+
20+
private WeakReference<MainWindow> _currentWindowRef;
21+
22+
/// <summary>
23+
/// Create a new <see cref="InputSimulator"/> helper class for the current window. All positions provided to this API will use the client space of the provided window's top-left as an origin point.
24+
/// </summary>
25+
/// <param name="currentWindow">Window to simulate input on, used as a reference for client-space coordinates.</param>
26+
public InputSimulator(MainWindow currentWindow)
27+
{
28+
_currentWindowRef = new(currentWindow);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GetClientRect
2+
MapWindowPoints

ProjectHeads/Tests.Head.WinAppSdk.props

+8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@
99
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="17.11.1">
1010
<ExcludeAssets>build</ExcludeAssets>
1111
</PackageReference>
12+
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183" PrivateAssets="all"/>
1213
</ItemGroup>
1314

1415
<ItemGroup>
1516
<ProjectCapability Include="TestContainer" />
1617
</ItemGroup>
1718

19+
<!-- Removed in 1.1+, see this bug: https://developercommunity.visualstudio.com/t/Windows-App-SDK-11-Unit-Tests-not-laun/10192460 -->
20+
<!-- Is this fixed now??? -->
21+
<!--<PropertyGroup>
22+
<IsTestProject>true</IsTestProject>
23+
<WindowsAppContainer>true</WindowsAppContainer>
24+
</PropertyGroup>-->
25+
1826
</Project>

0 commit comments

Comments
 (0)