Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions Assets/Tests/PlayMode/SimulateMouseUiInputSystemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#if ULOOP_HAS_INPUT_SYSTEM
#nullable enable
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.Runtime;
using io.github.hatayama.UnityCliLoop.ToolContracts;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.TestTools;
using UnityEngine.UI;
using Object = UnityEngine.Object;

namespace io.github.hatayama.UnityCliLoop.Tests.PlayMode
{
/// <summary>
/// Test fixture that verifies mouse UI drags keep Mouse.current aligned with the simulated pointer position.
/// </summary>
public class SimulateMouseUiInputSystemTests : InputTestFixture
{
private const float PositionTolerance = 0.01f;

private GameObject canvasGo = null!;
private GameObject eventSystemGo = null!;
private ExistingEventSystemDisableScope eventSystemDisableScope = null!;
private SimulateMouseUiTool tool = null!;
private SimulateMouseUiResponse lastResponse = null!;

public override void Setup()
{
base.Setup();

eventSystemDisableScope = new ExistingEventSystemDisableScope();

canvasGo = new GameObject("TestCanvas");
Canvas canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.AddComponent<CanvasScaler>();
canvasGo.AddComponent<GraphicRaycaster>();

eventSystemGo = new GameObject("TestEventSystem");
eventSystemGo.AddComponent<EventSystem>();
eventSystemGo.AddComponent<StandaloneInputModule>();

tool = new SimulateMouseUiTool();
InputSystem.AddDevice<Mouse>();
}

public override void TearDown()
{
MouseDragState.Clear();
Object.DestroyImmediate(canvasGo);
Object.DestroyImmediate(eventSystemGo);
eventSystemDisableScope.Restore();
base.TearDown();
}

[UnityTest]
public IEnumerator DragOneShot_Should_UpdateMouseCurrentPositionToDragPosition()
{
// Verifies a one-shot UI drag keeps Mouse.current aligned with PointerEventData at the drag end.
MouseAwareDragTracker tracker = CreateDraggableElement(
"DragTarget", new Vector2(120f, 80f), new Vector2(200f, 100f));
yield return null;

Vector2 startScreenPosition = GetScreenPosition(tracker.gameObject);
Vector2 endScreenPosition = startScreenPosition + new Vector2(140f, -60f);
Vector2 startInputPosition = ScreenToInput(startScreenPosition);
Vector2 endInputPosition = ScreenToInput(endScreenPosition);
SetMousePosition(Vector2.zero);

yield return RunTool(new JObject
{
["action"] = MouseAction.Drag.ToString(),
["fromX"] = startInputPosition.x,
["fromY"] = startInputPosition.y,
["x"] = endInputPosition.x,
["y"] = endInputPosition.y,
["dragSpeed"] = 0f
});

Assert.IsTrue(lastResponse.Success);
AssertPositionEquals(endScreenPosition, tracker.LastPointerPosition, "PointerEventData position should reach the drag end.");
AssertPositionEquals(endScreenPosition, tracker.LastMousePosition, "Mouse.current position should match PointerEventData during drag.");
}

[UnityTest]
public IEnumerator DragSplit_Should_UpdateMouseCurrentPositionOnEachDragStep()
{
// Verifies the incremental drag (DragStart/DragMove/DragEnd) path keeps Mouse.current aligned too,
// covering the InitiateDrag and InterpolateDragPosition sync points independently of one-shot drag.
MouseAwareDragTracker tracker = CreateDraggableElement(
"DragTarget", Vector2.zero, new Vector2(200f, 100f));
yield return null;

Vector2 startScreenPosition = GetScreenPosition(tracker.gameObject);
Vector2 moveScreenPosition = startScreenPosition + new Vector2(50f, 0f);
Vector2 endScreenPosition = startScreenPosition + new Vector2(100f, 0f);
Vector2 startInputPosition = ScreenToInput(startScreenPosition);
Vector2 moveInputPosition = ScreenToInput(moveScreenPosition);
Vector2 endInputPosition = ScreenToInput(endScreenPosition);
SetMousePosition(Vector2.zero);

yield return RunTool(new JObject
{
["action"] = MouseAction.DragStart.ToString(),
["x"] = startInputPosition.x,
["y"] = startInputPosition.y
});
Assert.IsTrue(lastResponse.Success);
AssertPositionEquals(startScreenPosition, GetMousePosition(), "Mouse.current position should match the drag start position.");

yield return RunTool(new JObject
{
["action"] = MouseAction.DragMove.ToString(),
["x"] = moveInputPosition.x,
["y"] = moveInputPosition.y,
["dragSpeed"] = 0f
});
Assert.IsTrue(lastResponse.Success);
AssertPositionEquals(moveScreenPosition, GetMousePosition(), "Mouse.current position should match the drag move position.");

yield return RunTool(new JObject
{
["action"] = MouseAction.DragEnd.ToString(),
["x"] = endInputPosition.x,
["y"] = endInputPosition.y,
["dragSpeed"] = 0f
});
Assert.IsTrue(lastResponse.Success);
AssertPositionEquals(endScreenPosition, GetMousePosition(), "Mouse.current position should match the drag end position.");
}

private IEnumerator RunTool(JObject parameters)
{
Task<UnityCliLoopToolResponse> task = tool.ExecuteAsync(parameters, CancellationToken.None);
float timeoutAt = Time.realtimeSinceStartup + 5f;
yield return new WaitUntil(() =>
task.IsCompleted || Time.realtimeSinceStartup >= timeoutAt);
Assert.IsTrue(task.IsCompleted, "Tool execution timed out.");
Assert.IsFalse(task.IsFaulted, $"Tool execution should not fault: {task.Exception}");
lastResponse = (SimulateMouseUiResponse)task.Result;
}

private MouseAwareDragTracker CreateDraggableElement(
string name, Vector2 anchoredPosition, Vector2 sizeDelta)
{
GameObject go = new GameObject(name);
go.transform.SetParent(canvasGo.transform, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = sizeDelta;
go.AddComponent<Image>();
return go.AddComponent<MouseAwareDragTracker>();
}

private Vector2 GetScreenPosition(GameObject go)
{
return (Vector2)go.GetComponent<RectTransform>().position;
}

private Vector2 ScreenToInput(Vector2 screenPosition)
{
float targetHeight = Handles.GetMainGameViewSize().y;
return new Vector2(screenPosition.x, targetHeight - screenPosition.y);
}

private void SetMousePosition(Vector2 position)
{
Mouse? currentMouse = Mouse.current;
Assert.IsNotNull(currentMouse, "Mouse.current should exist after adding a Mouse device.");
Set(currentMouse!.position, position);
}

private Vector2 GetMousePosition()
{
Mouse? currentMouse = Mouse.current;
Assert.IsNotNull(currentMouse, "Mouse.current should exist after adding a Mouse device.");
return currentMouse!.position.ReadValue();
}

private void AssertPositionEquals(Vector2 expected, Vector2 actual, string message)
{
float distance = Vector2.Distance(expected, actual);
Assert.LessOrEqual(distance, PositionTolerance, $"{message} Expected {expected}, got {actual}.");
}
}

/// <summary>
/// Test support type that records both PointerEventData and Mouse.current positions observed during a drag.
/// </summary>
public class MouseAwareDragTracker : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public Vector2 LastPointerPosition { get; private set; }
public Vector2 LastMousePosition { get; private set; }

public void OnBeginDrag(PointerEventData eventData)
{
}

public void OnDrag(PointerEventData eventData)
{
Mouse? currentMouse = Mouse.current;
Assert.IsNotNull(currentMouse, "Mouse.current should exist for this test.");

LastPointerPosition = eventData.position;
LastMousePosition = currentMouse!.position.ReadValue();
}

public void OnEndDrag(PointerEventData eventData)
{
}
}
}
#endif
11 changes: 11 additions & 0 deletions Assets/Tests/PlayMode/SimulateMouseUiInputSystemTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.ReplayInput.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.SimulateKeyboard.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.SimulateMouseInput.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.SimulateMouseUi.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.PlayMode")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Demo.Editor")]
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal static PointerEventData InitiateDrag(
pointerDrag = dragTarget,
rawPointerPress = raycastResult.gameObject
};
MouseUiInputSystemSync.SyncMousePosition(screenPos);

// Slider.OnPointerDown initializes m_Offset for handle positioning
GameObject? pressTarget = ExecuteEvents.ExecuteHierarchy(
Expand Down Expand Up @@ -105,6 +106,7 @@ internal static async Task<bool> InterpolateDragPosition(
Vector2 previousPosition = pointerData.position;
pointerData.position = endPos;
pointerData.delta = endPos - previousPosition;
MouseUiInputSystemSync.SyncMousePosition(endPos);
ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler);

SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(endPos));
Expand All @@ -131,6 +133,7 @@ internal static async Task<bool> InterpolateDragPosition(
pointerData.position = currentPosition;
pointerData.delta = currentPosition - previousPosition;

MouseUiInputSystemSync.SyncMousePosition(currentPosition);
ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler);

SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(currentPosition));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#nullable enable
using UnityEngine;
#if ULOOP_HAS_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Keeps Mouse.current aligned with the pointer position driven by mouse UI simulation.
/// </summary>
internal static class MouseUiInputSystemSync
{
// UI handlers can read Mouse.current alongside PointerEventData, so both paths must observe one position.
internal static void SyncMousePosition(Vector2 screenPos)
{
#if ULOOP_HAS_INPUT_SYSTEM
Mouse? mouse = Mouse.current;
if (mouse == null)
{
return;
}

InputSystem.QueueDeltaStateEvent(mouse.position, screenPos);
InputSystemUpdateHelper.RunExplicitUpdate(InputUpdateTypeResolver.Resolve());
#endif
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal static async Task<SimulateMouseUiResponse> ExecuteClick(
Vector2 inputPos = new(parameters.X, parameters.Y);
Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos);
PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button);
MouseUiInputSystemSync.SyncMousePosition(screenPos);
ResolvedPointerTargets resolvedTargets =
MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click);
if (resolvedTargets.FailureResponse != null)
Expand Down Expand Up @@ -90,6 +91,7 @@ internal static async Task<SimulateMouseUiResponse> ExecuteLongPress(
Vector2 inputPos = new(parameters.X, parameters.Y);
Vector2 screenPos = MouseUiCoordinateConverter.InputToScreen(inputPos);
PointerEventData pointerData = MouseUiPointerTargetResolver.CreatePointerPressData(eventSystem, screenPos, parameters.Button);
MouseUiInputSystemSync.SyncMousePosition(screenPos);
ResolvedPointerTargets resolvedTargets =
MouseUiPointerTargetResolver.ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress);
if (resolvedTargets.FailureResponse != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"GUID:a2d87883023de4cf59b7d1962d5dd8aa",
"GUID:aa7cf56cc5f074e57ba35272b877e116",
"GUID:c956a21f824994ef087b6de566690b3d",
"GUID:33fef506e6744f2982e8c13b1196f696"
"GUID:33fef506e6744f2982e8c13b1196f696",
"GUID:45d3617c96fa64d3e95383450e67f25a",
"GUID:75469ad4d38634e559750d17036d5f7c"
],
"includePlatforms": [
"Editor"
Expand All @@ -19,6 +21,12 @@
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"versionDefines": [
{
"name": "com.unity.inputsystem",
"expression": "",
"define": "ULOOP_HAS_INPUT_SYSTEM"
}
],
"noEngineReferences": false
}
Loading