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
76 changes: 76 additions & 0 deletions Assets/Tests/PlayMode/SimulateMouseUiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,82 @@ public IEnumerator TearDown()
eventSystemDisableScope.Restore();
}

#region Request Validation Tests

/// <summary>
/// Verifies one-shot drag rejects a negative speed before pointer execution starts.
/// </summary>
[UnityTest]
public IEnumerator DragOneShot_WithNegativeSpeed_Should_ReturnValidationFailure()
{
yield return RunTool(new JObject
{
["action"] = MouseAction.Drag.ToString(),
["dragSpeed"] = -1f
});

Assert.IsFalse(lastResponse.Success);
Assert.AreEqual("DragSpeed must be non-negative, got: -1", lastResponse.Message);
Assert.AreEqual(MouseAction.Drag.ToString(), lastResponse.Action);
}

/// <summary>
/// Verifies drag actions reject non-left buttons before pointer execution starts.
/// </summary>
[UnityTest]
public IEnumerator DragOneShot_WithRightButton_Should_ReturnValidationFailure()
{
yield return RunTool(new JObject
{
["action"] = MouseAction.Drag.ToString(),
["button"] = MouseButton.Right.ToString()
});

Assert.IsFalse(lastResponse.Success);
Assert.AreEqual(
"Drag actions only support Left button (uGUI ignores non-left drags), got: Right",
lastResponse.Message);
Assert.AreEqual(MouseAction.Drag.ToString(), lastResponse.Action);
}

/// <summary>
/// Verifies bypass click requires an explicit target path before pointer execution starts.
/// </summary>
[UnityTest]
public IEnumerator Click_WithBypassRaycastAndNoTargetPath_Should_ReturnValidationFailure()
{
yield return RunTool(new JObject
{
["action"] = MouseAction.Click.ToString(),
["bypassRaycast"] = true
});

Assert.IsFalse(lastResponse.Success);
Assert.AreEqual(
"TargetPath is required when BypassRaycast is true for Click, LongPress, Drag, or DragStart.",
lastResponse.Message);
Assert.AreEqual(MouseAction.Click.ToString(), lastResponse.Action);
}

/// <summary>
/// Verifies click rejects a drop target path that only drag completion can consume.
/// </summary>
[UnityTest]
public IEnumerator Click_WithDropTargetPath_Should_ReturnValidationFailure()
{
yield return RunTool(new JObject
{
["action"] = MouseAction.Click.ToString(),
["dropTargetPath"] = "TestCanvas/DropTarget"
});

Assert.IsFalse(lastResponse.Success);
Assert.AreEqual("DropTargetPath supports Drag and DragEnd only.", lastResponse.Message);
Assert.AreEqual(MouseAction.Click.ToString(), lastResponse.Action);
}

#endregion

#region Click Tests

[UnityTest]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#nullable enable
using System;

using io.github.hatayama.UnityCliLoop.Runtime;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Provides Mouse UI Simulation Command behavior for Unity CLI Loop.
/// </summary>
internal sealed class MouseUiSimulationCommand
{
private MouseUiSimulationCommand(SimulateMouseUiSchema request, MouseAction action)
{
Action = action;
X = request.X;
Y = request.Y;
FromX = request.FromX;
FromY = request.FromY;
DragSpeed = request.DragSpeed;
Duration = request.Duration;
Button = ToRuntimeMouseButton(request.Button);
BypassRaycast = request.BypassRaycast;
TargetPath = request.TargetPath ?? "";
DropTargetPath = request.DropTargetPath ?? "";
}

public MouseAction Action { get; }
public float X { get; }
public float Y { get; }
public float FromX { get; }
public float FromY { get; }
public float DragSpeed { get; }
public float Duration { get; }
public MouseButton Button { get; }
public bool BypassRaycast { get; }
public string TargetPath { get; }
public string DropTargetPath { get; }

// Returns (command, error). Error is non-null iff the caller supplied an out-of-range enum value.
public static (MouseUiSimulationCommand? command, string? errorMessage) TryFromSchema(SimulateMouseUiSchema request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

MouseAction? action = TryConvertMouseAction(request.Action);
if (action == null)
{
return (null, $"Unknown mouse UI action: {request.Action}");
}

return (new MouseUiSimulationCommand(request, action.Value), null);
}

private static MouseAction? TryConvertMouseAction(UnityCliLoopMouseUiAction action)
{
switch (action)
{
case UnityCliLoopMouseUiAction.Click:
return MouseAction.Click;
case UnityCliLoopMouseUiAction.Drag:
return MouseAction.Drag;
case UnityCliLoopMouseUiAction.DragStart:
return MouseAction.DragStart;
case UnityCliLoopMouseUiAction.DragMove:
return MouseAction.DragMove;
case UnityCliLoopMouseUiAction.DragEnd:
return MouseAction.DragEnd;
case UnityCliLoopMouseUiAction.LongPress:
return MouseAction.LongPress;
default:
return null;
}
}

private static MouseButton ToRuntimeMouseButton(UnityCliLoopMouseButton button)
{
switch (button)
{
case UnityCliLoopMouseButton.Right:
return MouseButton.Right;
case UnityCliLoopMouseButton.Middle:
return MouseButton.Middle;
default:
return MouseButton.Left;
}
}
}
}

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
@@ -0,0 +1,127 @@
#nullable enable
using UnityEngine.EventSystems;

using io.github.hatayama.UnityCliLoop.Runtime;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Validates mouse UI simulation preconditions and request options.
/// </summary>
internal static class MouseUiSimulationValidator
{
internal static SimulateMouseUiResponse? ValidateSimulationStart(
MouseUiSimulationCommand parameters,
EventSystem? eventSystem,
string pausedActionDescription)
{
ValidationResult playModeResult =
PlayModeToolPreflightService.RequireActiveAndNotPaused(pausedActionDescription);
if (!playModeResult.IsValid)
{
return CreateFailure(parameters, playModeResult.ErrorMessage);
}

if (eventSystem == null)
{
return CreateFailure(
parameters,
"No EventSystem found in the scene. Ensure an EventSystem GameObject exists.");
}

return ValidateSimulationRequestOptions(parameters);
}

internal static SimulateMouseUiResponse? ValidateActiveDragState(MouseUiSimulationCommand parameters)
{
if (!MouseDragState.IsDragging || !RequiresIdlePointer(parameters.Action))
{
return null;
}

return CreateFailure(
parameters,
$"Cannot {parameters.Action.ToString()} while a split drag is active. Call DragEnd first.");
}

internal static SimulateMouseUiResponse CreateFailure(
MouseUiSimulationCommand parameters,
string message)
{
return new SimulateMouseUiResponse
{
Success = false,
Message = message,
Action = parameters.Action.ToString()
};
}

private static SimulateMouseUiResponse? ValidateSimulationRequestOptions(
MouseUiSimulationCommand parameters)
{
if (parameters.Action != MouseAction.Click && parameters.Action != MouseAction.LongPress && parameters.DragSpeed < 0f)
{
return CreateFailure(parameters, $"DragSpeed must be non-negative, got: {parameters.DragSpeed}");
}

if (IsDragAction(parameters.Action) && parameters.Button != MouseButton.Left)
{
return CreateFailure(
parameters,
$"Drag actions only support Left button (uGUI ignores non-left drags), got: {parameters.Button}");
}

if (parameters.BypassRaycast && !SupportsBypassRaycast(parameters.Action))
Comment thread
hatayama marked this conversation as resolved.
{
return CreateFailure(parameters, "BypassRaycast is not supported for this action.");
}

if (parameters.BypassRaycast &&
RequiresBypassTargetPath(parameters.Action) &&
string.IsNullOrWhiteSpace(parameters.TargetPath))
{
return CreateFailure(
parameters,
"TargetPath is required when BypassRaycast is true for Click, LongPress, Drag, or DragStart.");
}

if (!string.IsNullOrWhiteSpace(parameters.DropTargetPath) &&
parameters.Action != MouseAction.Drag &&
parameters.Action != MouseAction.DragEnd)
{
return CreateFailure(parameters, "DropTargetPath supports Drag and DragEnd only.");
}

return null;
}

private static bool RequiresIdlePointer(MouseAction action)
{
return action == MouseAction.Click || action == MouseAction.Drag || action == MouseAction.LongPress;
}

private static bool SupportsBypassRaycast(MouseAction action)
{
return action == MouseAction.Click
|| action == MouseAction.LongPress
|| IsDragAction(action);
}

private static bool RequiresBypassTargetPath(MouseAction action)
{
return action == MouseAction.Click
|| action == MouseAction.LongPress
|| action == MouseAction.Drag
|| action == MouseAction.DragStart;
}

private static bool IsDragAction(MouseAction action)
{
return action == MouseAction.Drag
|| action == MouseAction.DragStart
|| action == MouseAction.DragMove
|| action == MouseAction.DragEnd;
}
}
}

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

Loading
Loading