diff --git a/Assets/Tests/PlayMode/SimulateMouseUiTests.cs b/Assets/Tests/PlayMode/SimulateMouseUiTests.cs
index dc41d1cc11..2e4cb79f46 100644
--- a/Assets/Tests/PlayMode/SimulateMouseUiTests.cs
+++ b/Assets/Tests/PlayMode/SimulateMouseUiTests.cs
@@ -56,6 +56,82 @@ public IEnumerator TearDown()
eventSystemDisableScope.Restore();
}
+ #region Request Validation Tests
+
+ ///
+ /// Verifies one-shot drag rejects a negative speed before pointer execution starts.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verifies drag actions reject non-left buttons before pointer execution starts.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verifies bypass click requires an explicit target path before pointer execution starts.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verifies click rejects a drop target path that only drag completion can consume.
+ ///
+ [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]
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs
new file mode 100644
index 0000000000..da174482db
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs
@@ -0,0 +1,91 @@
+#nullable enable
+using System;
+
+using io.github.hatayama.UnityCliLoop.Runtime;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Provides Mouse UI Simulation Command behavior for Unity CLI Loop.
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs.meta
new file mode 100644
index 0000000000..145d4976c3
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d72a04fed9944f7b9ad6228ef9b6b120
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs
new file mode 100644
index 0000000000..0c1d16be2d
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs
@@ -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
+{
+ ///
+ /// Validates mouse UI simulation preconditions and request options.
+ ///
+ 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))
+ {
+ 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;
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs.meta
new file mode 100644
index 0000000000..41d6ec77de
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 10c5768ed7de48a1af6d0d6d3fb8132c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
index 0b75b63733..d1d1ac1721 100644
--- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
@@ -53,7 +53,10 @@ public async Task ExecuteAsync(
string correlationId = UnityCliLoopConstants.GenerateCorrelationId();
EventSystem? eventSystem = EventSystem.current;
- SimulateMouseUiResponse? validationFailure = ValidateSimulationStart(parameters, eventSystem);
+ SimulateMouseUiResponse? validationFailure = MouseUiSimulationValidator.ValidateSimulationStart(
+ parameters,
+ eventSystem,
+ PausedActionDescription);
if (validationFailure != null)
{
return validationFailure;
@@ -64,7 +67,8 @@ public async Task ExecuteAsync(
LogSimulationStart(parameters, correlationId);
EnsureOverlayExists();
- SimulateMouseUiResponse? dragStateFailure = ValidateActiveDragState(parameters);
+ SimulateMouseUiResponse? dragStateFailure =
+ MouseUiSimulationValidator.ValidateActiveDragState(parameters);
if (dragStateFailure != null)
{
return dragStateFailure;
@@ -77,94 +81,6 @@ public async Task ExecuteAsync(
return response;
}
- private static SimulateMouseUiResponse? ValidateSimulationStart(
- MouseUiSimulationCommand parameters,
- EventSystem? eventSystem)
- {
- 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);
- }
-
- 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))
- {
- 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 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.");
- }
-
- private static bool RequiresIdlePointer(MouseAction action)
- {
- return action == MouseAction.Click || action == MouseAction.Drag || action == MouseAction.LongPress;
- }
-
- private static SimulateMouseUiResponse CreateFailure(
- MouseUiSimulationCommand parameters,
- string message)
- {
- return new SimulateMouseUiResponse
- {
- Success = false,
- Message = message,
- Action = parameters.Action.ToString()
- };
- }
-
private static void LogSimulationStart(MouseUiSimulationCommand parameters, string correlationId)
{
VibeLogger.LogInfo(
@@ -224,7 +140,9 @@ private async Task ExecuteMouseAction(
default:
// Unreachable when TryFromSchema succeeds; kept as a defensive Success=false response
// instead of a throw so any future MouseAction addition surfaces as a validation failure.
- return CreateFailure(parameters, $"Unknown mouse action: {parameters.Action}");
+ return MouseUiSimulationValidator.CreateFailure(
+ parameters,
+ $"Unknown mouse action: {parameters.Action}");
}
}
@@ -1313,21 +1231,6 @@ private static SimulateMouseUiResponse CreateDragEndResult(
return UiRaycastHelper.RaycastUI(screenPosition, eventSystem);
}
- 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 TryResolveGameObjectPath(
string targetPath,
string parameterName,
@@ -1442,90 +1345,6 @@ private static RaycastResult CreateDirectRaycastResult(GameObject target)
};
}
- ///
- /// Provides Mouse UI Simulation Command behavior for Unity CLI Loop.
- ///
- private 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;
- }
- }
- }
-
private readonly struct TargetPathLookupResult
{
public TargetPathLookupResult(GameObject? target, int matchCount)
@@ -1580,12 +1399,5 @@ public static ResolvedPointerTargets Failure(
}
}
- private static bool IsDragAction(MouseAction action)
- {
- return action == MouseAction.Drag
- || action == MouseAction.DragStart
- || action == MouseAction.DragMove
- || action == MouseAction.DragEnd;
- }
}
}