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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void InterruptedButtonResult_WithMultiplePausePointHits_MapsPauseEvidence
Assert.That(
response.Message,
Is.EqualTo(
"Mouse input stopped because Unity paused during Pause Point inspection. Button 'Left' was released from Unity CLI Loop bookkeeping; the queued input edge was discarded."));
"Mouse input stopped because Unity paused during Pause Point inspection. Button 'Left' was released from Unity CLI Loop bookkeeping; the queued input edge was discarded before any gameplay update processed it, so the game never observed a press and it is safe to retry after resume."));
Assert.That(response.Action, Is.EqualTo(UnityCliLoopMouseInputAction.Click.ToString()));
Assert.That(response.Button, Is.EqualTo("Left"));
Assert.That(response.PositionX, Is.EqualTo(inputPosition.x));
Expand Down Expand Up @@ -83,7 +83,7 @@ public void InterruptedButtonResult_WhenPressWasApplied_ReportsDeliveredBeforePa
Assert.That(
response.Message,
Is.EqualTo(
"Mouse input stopped because Unity paused during Pause Point inspection. Button 'Right' press was already delivered to the game before the pause; Unity CLI Loop released it from bookkeeping, so the game may have registered the press."));
"Mouse input stopped because Unity paused during Pause Point inspection. Button 'Right' press was delivered to the game before the pause: the Input System processed the press edge in a gameplay update, so game code polling that frame observed it and the world state may already have changed. Do not retry the press; re-check the affected state (and pause-point-status) before deciding the next step."));
Assert.That(response.Action, Is.EqualTo(UnityCliLoopMouseInputAction.LongPress.ToString()));
Assert.That(response.Button, Is.EqualTo("Right"));
Assert.That(response.PositionX, Is.EqualTo(inputPosition.x));
Expand Down
32 changes: 32 additions & 0 deletions Assets/Tests/PlayMode/SimulateMouseInputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,38 @@ public IEnumerator LongPress_Should_HoldButtonForDuration()
AssertCoordinateMetadata(400f, 300f);
}

[UnityTest]
public IEnumerator LongPress_WhenUnityPausesDuringObservation_Should_ReleaseButton()
{
// Verifies a pause-point pause during a long press releases the injected mouse button state.
yield return null;

Task<UnityCliLoopToolResponse> task = tool.ExecuteAsync(new JObject
{
["action"] = MouseInputAction.LongPress.ToString(),
["x"] = 400,
["y"] = 300,
["duration"] = 1f
}, System.Threading.CancellationToken.None);

float pressDeadline = Time.realtimeSinceStartup + 5f;
yield return new WaitUntil(() =>
mouse.leftButton.isPressed || task.IsCompleted || Time.realtimeSinceStartup >= pressDeadline);
Assert.IsTrue(mouse.leftButton.isPressed, "The press must be applied before the pause is simulated.");
Assert.IsFalse(task.IsCompleted, "The test must pause during the long-press observation window.");

InputSystemUpdateHelper.ConfigurePauseProviderForTests(() => true);
yield return WaitForTask(task);
InputSystemUpdateHelper.ResetPauseProviderForTests();

lastResponse = (SimulateMouseInputResponse)task.Result;
Assert.IsTrue(lastResponse.Success);
Assert.IsTrue(lastResponse.InterruptedByPausePoint);
Assert.AreEqual("LongPress", lastResponse.Action);
Assert.IsFalse(mouse.leftButton.isPressed, "Pause-point interruption should release the injected mouse button state.");
Assert.IsFalse(SimulateMouseInputOverlayState.HasAnyActivity, "Pause-point interruption should clear mouse overlay state.");
}

[UnityTest]
public IEnumerator LongPress_WithZeroDuration_Should_ReturnError()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ private static async Task ReleaseButtonImmediatelyOnMainThreadAsync(
ReleaseButtonImmediately(mouse, button);
}

/// <summary>
/// Releases the button right away after a pause interrupted the press, once the pending apply
/// subscription has been disposed.
/// </summary>
internal static void ReleaseButtonImmediatelyAfterPauseInterruption(Mouse mouse, RuntimeMouseButton button)
{
ReleaseButtonImmediately(mouse, button);
}

private static void ReleaseButtonImmediately(Mouse mouse, RuntimeMouseButton button)
{
Debug.Assert(CanInjectMouseState(mouse), "mouse button can only be released while PlayMode has a mouse");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,45 +85,8 @@ internal static async Task<SimulateMouseInputResponse> ExecuteClick(
}
finally
{
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
}
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
mouse,
button,
CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false);
}
else
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputState.SetButtonUp(button);
}
}
else
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputState.SetButtonUp(button);
}

if (waitOutcome != InputSimulationWaitOutcome.TimedOut)
{
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
SimulateMouseInputOverlayState.Clear();
}
else
{
SimulateMouseInputOverlayState.SetButtonHeld(button, false);
}
}
waitOutcome = await FinishHeldButton(mouse, button, waitOutcome, pressWasApplied)
.ConfigureAwait(false);
}

if (waitOutcome == InputSimulationWaitOutcome.Paused)
Expand Down Expand Up @@ -216,45 +179,8 @@ internal static async Task<SimulateMouseInputResponse> ExecuteLongPress(
}
finally
{
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
}
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
mouse,
button,
CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false);
}
else
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputState.SetButtonUp(button);
}
}
else
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputState.SetButtonUp(button);
}

if (waitOutcome != InputSimulationWaitOutcome.TimedOut)
{
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
SimulateMouseInputOverlayState.Clear();
}
else
{
SimulateMouseInputOverlayState.SetButtonHeld(button, false);
}
}
waitOutcome = await FinishHeldButton(mouse, button, waitOutcome, pressWasApplied)
.ConfigureAwait(false);
}

if (waitOutcome == InputSimulationWaitOutcome.Paused)
Expand Down Expand Up @@ -282,6 +208,80 @@ await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
conversion);
}

/// <summary>
/// Releases a button that a finished press left held and returns the outcome the caller must
/// report; a release that timed out downgrades the outcome to TimedOut.
/// </summary>
private static async Task<InputSimulationWaitOutcome> FinishHeldButton(
Mouse mouse,
RuntimeMouseButton button,
InputSimulationWaitOutcome waitOutcome,
bool pressWasApplied)
{
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
return waitOutcome;
}

if (waitOutcome == InputSimulationWaitOutcome.Paused && pressWasApplied)
{
return await ReleaseHeldButtonAfterPause(mouse, button).ConfigureAwait(false);
}

if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
mouse,
button,
CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false);
return InputSimulationWaitOutcome.TimedOut;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// The pause can also arrive while this release is queued, which discards it the same way.
if (releaseOutcome == InputSimulationWaitOutcome.Paused)
{
return await ReleaseHeldButtonAfterPause(mouse, button).ConfigureAwait(false);
}
}

await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputState.SetButtonUp(button);
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
SimulateMouseInputOverlayState.Clear();
}
else
{
SimulateMouseInputOverlayState.SetButtonHeld(button, false);
}

return waitOutcome;
}

/// <summary>
/// Writes the release straight into the device after a pause discarded the queued one, and
/// reports the paused outcome.
/// </summary>
private static async Task<InputSimulationWaitOutcome> ReleaseHeldButtonAfterPause(
Mouse mouse,
RuntimeMouseButton button)
{
// Why: ApplyOnNextConfiguredUpdate already disposed the pending apply subscription when it
// returned Paused, so a release routed through it again is discarded and the button stays
// held until the next command. Writing after that dispose also keeps a queued edge from
// re-applying the press once the Editor resumes.
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None);
MouseInputMainThreadCleanup.ReleaseButtonImmediatelyAfterPauseInterruption(mouse, button);
MouseInputState.SetButtonUp(button);
SimulateMouseInputOverlayState.Clear();
return InputSimulationWaitOutcome.Paused;
}

private static RuntimeMouseButton ToRuntimeMouseButton(UnityCliLoopMouseButton button)
{
switch (button)
Expand Down
Loading