Skip to content

Commit

Permalink
Merge pull request #65 from unoplatform/dev/dr/pointers
Browse files Browse the repository at this point in the history
feat: Add support of pointers injection on supported platforms
  • Loading branch information
dr1rrb authored Dec 13, 2022
2 parents b13962b + 0d1f1d1 commit 6a2c15b
Show file tree
Hide file tree
Showing 23 changed files with 654 additions and 86 deletions.
79 changes: 49 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,37 +109,56 @@ Placing this attribute on a test or test class will force the tests to run on th
This attribute configures the type of the pointer that is simulated when using helpers like `App.TapCoordinates()`. You can define the attribute more than once, the test will then be run for each configured type. When not defined, the test engine will use the common pointer type of the platform (touch for mobile OS like iOS and Android, mouse for browsers, skia and other desktop platforms).

## Placing tests in a separate assembly
- In a separate assembly, add the following attributes code:
```csharp
using System;
using Windows.Devices.Input;

namespace Uno.UI.RuntimeTests;

public sealed class RequiresFullWindowAttribute : Attribute { }

public sealed class RunsOnUIThreadAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class InjectedPointerAttribute : Attribute
{
public PointerDeviceType Type { get; }

public InjectedPointerAttribute(PointerDeviceType type)
{
Type = type;
}
}
```
- Then define the following in your `csproj`:
```xml
<PropertyGroup>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_INJECTEDPOINTERATTRIBUTE</DefineConstants>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_REQUIRESFULLWINDOWATTRIBUTE</DefineConstants>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_RUNSONUITHREADATTRIBUTE</DefineConstants>
</PropertyGroup>
- In your separated test assembly
- Add a reference to the "Uno.UI.RuntimeTests.Engine" package
- Then define the following in your `csproj`:
```xml
<PropertyGroup>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_UI</DefineConstants>
</PropertyGroup>
```

- In your test application
- Add a reference to the "Uno.UI.RuntimeTests.Engine" package
- Then define the following in your `csproj`:
```xml
<PropertyGroup>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_LIBRARY</DefineConstants>
</PropertyGroup>
```
### Alternative method
Alternatively, if you have only limited needs, in your separated test assembly, add the following attributes code:
```csharp
using System;
using Windows.Devices.Input;

namespace Uno.UI.RuntimeTests;

public sealed class RequiresFullWindowAttribute : Attribute { }

public sealed class RunsOnUIThreadAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class InjectedPointerAttribute : Attribute
{
public PointerDeviceType Type { get; }

public InjectedPointerAttribute(PointerDeviceType type)
{
Type = type;
}
}
```

and define the following in your `csproj`:
```xml
<PropertyGroup>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_UI</DefineConstants>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_INJECTEDPOINTERATTRIBUTE</DefineConstants>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_REQUIRESFULLWINDOWATTRIBUTE</DefineConstants>
<DefineConstants>$(DefineConstants);UNO_RUNTIMETESTS_DISABLE_RUNSONUITHREADATTRIBUTE</DefineConstants>
</PropertyGroup>
```
These attributes will ask for the runtime test engine to replace the ones defined by the `Uno.UI.RuntimeTests.Engine` package.

## Running the tests automatically during CI
Expand Down
42 changes: 42 additions & 0 deletions src/TestApp/shared/PointersInjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;

#if HAS_UNO_WINUI || WINDOWS_WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#else
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#endif

namespace Uno.UI.RuntimeTests.Engine;

[TestClass]
[RunsOnUIThread]
public class PointersInjectionTests
{
[TestMethod]
[InjectedPointer(PointerDeviceType.Mouse)]
[InjectedPointer(PointerDeviceType.Touch)]
#if !HAS_UNO_SKIA && !WINDOWS
[ExpectedException(typeof(NotSupportedException))]
#endif
public async Task When_TapCoordinates()
{
var elt = new Button { Content = "Tap me" };
var clicked = false;
elt.Click += (snd, e) => clicked = true;

UnitTestsUIContentHelper.Content = elt;

await UnitTestsUIContentHelper.WaitForLoaded(elt);

InputInjectorHelper.Current.Tap(elt);

Assert.IsTrue(clicked);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Uno.UI.RuntimeTests;

using Windows.Devices.Input;

#if !UNO_RUNTIMETESTS_DISABLE_INJECTEDPOINTERATTRIBUTE
#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY && !UNO_RUNTIMETESTS_DISABLE_INJECTEDPOINTERATTRIBUTE
/// <summary>
/// Specify the type of pointer to use for that test.
/// WARNING: This has no effects on UI tests, cf. remarks.
Expand All @@ -30,4 +30,4 @@ public InjectedPointerAttribute(PointerDeviceType type)
Type = type;
}
}
#endif
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Windows.Foundation;
using Windows.UI.Input;
using Windows.UI.Input.Preview.Injection;

namespace Uno.UI.RuntimeTests;

public partial class InputInjectorHelper
{
#pragma warning disable CA1822 // Mark members as static
public class MouseHelper
{
#if HAS_UNO
public MouseHelper(InputInjector input)
{
var getCurrent = typeof(InputInjector).GetProperty("Mouse", BindingFlags.Instance | BindingFlags.NonPublic)?.GetMethod
?? throw new NotSupportedException("This version of uno is not supported for pointer injection.");
var currentType = getCurrent.Invoke(input, null)!.GetType();
var getCurrentPosition = currentType.GetProperty("Position", BindingFlags.Instance | BindingFlags.Public)?.GetMethod
?? throw new NotSupportedException("This version of uno is not supported for pointer injection.");
var getCurrentProperties = currentType.GetProperty("Properties", BindingFlags.Instance | BindingFlags.Public)?.GetMethod
?? throw new NotSupportedException("This version of uno is not supported for pointer injection.");

CurrentPosition = () => (Point)getCurrentPosition.Invoke(getCurrent.Invoke(input, null)!, null)!;
CurrentProperties = () => (PointerPointProperties)getCurrentProperties.Invoke(getCurrent.Invoke(input, null)!, null)!;
}

private Func<PointerPointProperties> CurrentProperties;

private Func<Point> CurrentPosition;
#else
public MouseHelper(InputInjector input)
{
}

private Point CurrentPosition()
=> Windows.UI.Core.CoreWindow.GetForCurrentThread().PointerPosition;
#endif

/// <summary>
/// Create an injected pointer info which presses the left button
/// </summary>
public InjectedInputMouseInfo Press()
=> new()
{
TimeOffsetInMilliseconds = 1,
MouseOptions = InjectedInputMouseOptions.LeftDown,
};

/// <summary>
/// Create an injected pointer info which release the left button
/// </summary>
public InjectedInputMouseInfo Release()
=> new()
{
TimeOffsetInMilliseconds = 1,
MouseOptions = InjectedInputMouseOptions.LeftUp,
};

/// <summary>
/// Create an injected pointer info which releases any pressed button
/// </summary>
public InjectedInputMouseInfo? ReleaseAny()
{
var options = default(InjectedInputMouseOptions);

#if HAS_UNO
var currentProps = CurrentProperties();
if (currentProps.IsLeftButtonPressed)
{
options |= InjectedInputMouseOptions.LeftUp;
}

if (currentProps.IsMiddleButtonPressed)
{
options |= InjectedInputMouseOptions.MiddleUp;
}

if (currentProps.IsRightButtonPressed)
{
options |= InjectedInputMouseOptions.RightUp;
}

if (currentProps.IsXButton1Pressed)
{
options |= InjectedInputMouseOptions.XUp;
}
#else
options = InjectedInputMouseOptions.LeftUp
| InjectedInputMouseOptions.MiddleUp
| InjectedInputMouseOptions.RightUp
| InjectedInputMouseOptions.XUp;
#endif

return options is default(InjectedInputMouseOptions)
? null
: new()
{
TimeOffsetInMilliseconds = 1,
MouseOptions = options
};
}

/// <summary>
/// Create an injected pointer info which moves the mouse by the given offests
/// </summary>
public InjectedInputMouseInfo MoveBy(int deltaX, int deltaY)
=> new()
{
DeltaX = deltaX,
DeltaY = deltaY,
TimeOffsetInMilliseconds = 1,
MouseOptions = InjectedInputMouseOptions.MoveNoCoalesce,
};

/// <summary>
/// Create some injected pointer infos which moves the mouse to the given coordinates
/// </summary>
/// <param name="x">The target x position</param>
/// <param name="y">The traget y position</param>
/// <param name="steps">Number injected pointer infos to generate to simutale a smooth manipulation.</param>
public IEnumerable<InjectedInputMouseInfo> MoveTo(double x, double y, int? steps = null)
{
var deltaX = x - CurrentPosition().X;
var deltaY = y - CurrentPosition().Y;

steps ??= (int)Math.Min(Math.Max(Math.Abs(deltaX), Math.Abs(deltaY)), 512);
if (steps is 0)
{
yield break;
}

var stepX = deltaX / steps.Value;
var stepY = deltaY / steps.Value;

stepX = stepX is > 0 ? Math.Ceiling(stepX) : Math.Floor(stepX);
stepY = stepY is > 0 ? Math.Ceiling(stepY) : Math.Floor(stepY);

for (var step = 0; step <= steps && (stepX is not 0 || stepY is not 0); step++)
{
yield return MoveBy((int)stepX, (int)stepY);

if (Math.Abs(CurrentPosition().X - x) < stepX)
{
stepX = 0;
}

if (Math.Abs(CurrentPosition().Y - y) < stepY)
{
stepY = 0;
}
}
}
}
}

#endif
Loading

0 comments on commit 6a2c15b

Please sign in to comment.