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
2 changes: 1 addition & 1 deletion .github/workflows/code-complexity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
(cd cli && golangci-lint run --config .golangci-complexity.yml --issues-exit-code=0 --show-stats=false --output.json.path ../artifacts/go-code-complexity.json ./...)
dotnet run --project tools/UnityCliLoop.CodeComplexity/UnityCliLoop.CodeComplexity.csproj -- \
--root . \
--max-complexity 25 \
--max-complexity 15 \
--format json \
--fail-on-exceeded false > artifacts/csharp-code-complexity.json

Expand Down
12 changes: 12 additions & 0 deletions Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,17 @@ public void HasTopLevelReturn_WhenInterpolationHoleContainsNestedStringLiteral_S

Assert.IsTrue(hasReturn);
}

[Test]
public void Analyze_WhenAttributedTypeHasAccessModifier_ShouldDetectTypeDeclaration()
{
// Verifies attributed public types are not mistaken for top-level statements.
string source = "[System.Serializable] public sealed class Example {}";

SourceShapeResult result = SourceShaper.Analyze(source);

Assert.IsTrue(result.HasTypeDeclaration);
Assert.IsFalse(result.HasTopLevelStatements);
}
}
}
45 changes: 45 additions & 0 deletions Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3855,6 +3855,51 @@ public async Task HasMigrationTargetsAsync_WhenCurrentFirstPartyToolsAliasOnlyNe
}
}

[Test]
public async Task HasMigrationTargetsAsync_WhenAssemblyUsesLegacyGlobalAliasForScreenshotCapture_ReturnsTrue()
{
// Verifies that startup detection carries legacy global aliases into screenshot fast scans.
string projectRoot = CreateProjectRoot();
try
{
string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools");
Directory.CreateDirectory(toolDirectory);
File.WriteAllText(
Path.Combine(toolDirectory, "GlobalUsings.cs"),
"global using Old = io.github.hatayama.uLoopMCP;");
File.WriteAllText(
Path.Combine(toolDirectory, "ScreenshotTool.cs"),
@"using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

public sealed class ScreenshotTool
{
public async Task<Texture2D> CaptureAsync(EditorWindow window, CancellationToken ct)
{
return await Old.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);
}
}");
File.WriteAllText(
Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"),
@"{
""name"": ""VendorTools.Editor"",
""references"": []
}");

ThirdPartyToolMigrationFileService service = new();

bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None);

Assert.That(hasTargets, Is.True);
}
finally
{
Directory.Delete(projectRoot, recursive: true);
}
}

[Test]
public void ApplyMigration_WhenCurrentDomainLocalAliasUsesMovedContractTypes_AddsToolContractsReference()
{
Expand Down
17 changes: 17 additions & 0 deletions Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,23 @@ public void MigrateCSharpSource_WhenLegacyToolDescriptionInterpolatesCommaExpres
Assert.That(result.Content, Does.Not.Contain("string.Join"));
}

[Test]
public void MigrateCSharpSource_WhenLegacyToolAttributeCommentsContainCommas_PreservesSupportedArguments()
{
// Verifies that commas inside comments do not split legacy attribute arguments.
string source = "using io.github.hatayama.uLoopMCP;\n" +
"[McpTool(Description = \"hello\" /*, block */\n" +
" //, line\n" +
" , DisplayDevelopmentOnly = true)] public sealed class HelloTool {}";

ThirdPartyToolMigrationContentResult result =
ThirdPartyToolMigrationRules.MigrateCSharpSource(source);

Assert.That(result.Changed, Is.True);
Assert.That(result.Content, Does.Contain("[UnityCliLoopTool(DisplayDevelopmentOnly = true)]"));
Assert.That(result.Content, Does.Not.Contain("Description"));
}

[Test]
public void FindRegularInterpolatedStringEndIndex_WhenHoleContainsInterpolatedRawStringWithRawStringHole_FindsOuterStringEnd()
{
Expand Down
43 changes: 43 additions & 0 deletions Assets/Tests/PlayMode/SimulateMouseUiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ public IEnumerator Click_WithBypassRaycast_Should_ClickTargetBehindBlocker()
Assert.AreEqual("ClickTarget", lastResponse.HitGameObjectName);
}

[UnityTest]
public IEnumerator Click_WithBypassRaycastAndClickOnlyTarget_Should_FirePointerUpAndClick()
{
// Verifies click-only targets still receive pointer up before pointer click.
ClickOnlyTracker tracker = CreateClickOnlyElement("ClickOnlyTarget", Vector2.zero, new Vector2(200, 100));
yield return null;

Vector2 screenPos = GetScreenPosition(tracker.gameObject);

yield return RunTool(new JObject
{
["action"] = MouseAction.Click.ToString(),
["x"] = screenPos.x,
["y"] = screenPos.y,
["bypassRaycast"] = true,
["targetPath"] = "TestCanvas/ClickOnlyTarget"
});

Assert.IsTrue(lastResponse.Success);
Assert.IsTrue(tracker.PointerUpCalled, "PointerUp should be fired");
Assert.IsTrue(tracker.PointerClickCalled, "PointerClick should be fired");
Assert.AreEqual("ClickOnlyTarget", lastResponse.HitGameObjectName);
}

// Verifies clipped overlay UI wins over a non-GraphicRaycaster hit behind it.
[UnityTest]
public IEnumerator Click_Should_PreferClippedOverlayUiOverNonUiRaycastHit()
Expand Down Expand Up @@ -937,6 +961,13 @@ private ClickTracker CreateClickableElement(string name, Vector2 anchoredPositio
return go.AddComponent<ClickTracker>();
}

private ClickOnlyTracker CreateClickOnlyElement(string name, Vector2 anchoredPosition, Vector2 sizeDelta)
{
GameObject go = CreateUIElement(name, anchoredPosition, sizeDelta);
go.AddComponent<Image>();
return go.AddComponent<ClickOnlyTracker>();
}

private ClickTracker CreateChildClickableElement(string name, Transform parent, Vector2 anchoredPosition, Vector2 sizeDelta)
{
GameObject go = CreateChildUIElement(name, parent, anchoredPosition, sizeDelta);
Expand Down Expand Up @@ -1106,6 +1137,18 @@ public class ClickTracker : MonoBehaviour, IPointerDownHandler, IPointerUpHandle
public void OnPointerClick(PointerEventData eventData) { PointerClickCalled = true; }
}

/// <summary>
/// Test support type that exposes click targets without pointer-down handlers.
/// </summary>
public class ClickOnlyTracker : MonoBehaviour, IPointerUpHandler, IPointerClickHandler
{
public bool PointerUpCalled { get; private set; }
public bool PointerClickCalled { get; private set; }

public void OnPointerUp(PointerEventData eventData) { PointerUpCalled = true; }
public void OnPointerClick(PointerEventData eventData) { PointerClickCalled = true; }
}

// Tracks drag events and moves the element for testing
/// <summary>
/// Test support type used by editor and play mode fixtures.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,17 +456,28 @@ private static Dictionary<string, Key> BuildKeyLookup()

private void ApplyUiEvents()
{
if (!_replayMousePosition.HasValue)
UiReplayFrame? replayFrame = CreateUiReplayFrame();
if (!replayFrame.HasValue)
{
RestoreUiInputModules();
return;
}

ApplyUiPointerActivity(replayFrame.Value);
ApplyUiPointerRelease(replayFrame.Value);
}

private UiReplayFrame? CreateUiReplayFrame()
{
if (!_replayMousePosition.HasValue)
{
return null;
}

EventSystem? eventSystem = EventSystem.current;
if (eventSystem == null)
{
RestoreUiInputModules();
return;
return null;
}

Vector2 screenPos = _replayMousePosition.Value;
Expand All @@ -483,52 +494,98 @@ private void ApplyUiEvents()
Vector2 gameViewSize = Handles.GetMainGameViewSize();
Vector2 inputPos = new(screenPos.x, gameViewSize.y - screenPos.y);

if (justPressed)
return new UiReplayFrame(
eventSystem,
screenPos,
inputPos,
gameViewSize,
leftHeld,
justPressed,
justReleased,
mouseMoved);
}

private void ApplyUiPointerActivity(UiReplayFrame replayFrame)
{
if (replayFrame.JustPressed)
{
_suppressIdleUiOverlay = false;
_pressTime = Time.realtimeSinceStartup;
OnUiPointerDown(screenPos, eventSystem);
OnUiPointerDown(replayFrame.ScreenPosition, replayFrame.EventSystem);
SimulateMouseUiOverlayState.Update(
MouseAction.Click, inputPos, null, _currentPressTarget?.name, gameViewSize);
MouseAction.Click,
replayFrame.InputPosition,
null,
_currentPressTarget?.name,
replayFrame.GameViewSize);
SimulateMouseUiOverlayState.RequestExpandAnimation();
return;
}
else if (leftHeld && (_currentPressTarget != null || _currentDragTarget != null))
{
OnUiDrag(screenPos);

if (_isDragging)
{
Vector2 pressInputPos = new(_pressScreenPosition.x, gameViewSize.y - _pressScreenPosition.y);
SimulateMouseUiOverlayState.Update(
MouseAction.Drag, inputPos, pressInputPos, null, gameViewSize);
}
else
{
float elapsed = Time.realtimeSinceStartup - _pressTime;
if (elapsed >= 0.5f)
{
SimulateMouseUiOverlayState.Update(
MouseAction.LongPress, inputPos, null, _currentPressTarget?.name, gameViewSize);
SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed);
}
}
if (replayFrame.LeftHeld && (_currentPressTarget != null || _currentDragTarget != null))
{
ApplyUiPointerHold(replayFrame);
return;
}
else if (!_suppressIdleUiOverlay || mouseMoved)

if (!_suppressIdleUiOverlay || replayFrame.MouseMoved)
{
// Keeping the overlay hidden until the pointer actually moves prevents release fade-out
// from being cancelled by the next idle frame at the same position.
_suppressIdleUiOverlay = false;
SimulateMouseUiOverlayState.Update(
MouseAction.Click, inputPos, null, null, gameViewSize);
MouseAction.Click,
replayFrame.InputPosition,
null,
null,
replayFrame.GameViewSize);
}
}

if (justReleased)
private void ApplyUiPointerHold(UiReplayFrame replayFrame)
{
OnUiDrag(replayFrame.ScreenPosition);

if (_isDragging)
{
OnUiPointerUp(screenPos, eventSystem);
_suppressIdleUiOverlay = true;
SimulateMouseUiOverlayState.RequestDissipateAnimation();
SimulateMouseUiOverlayState.Clear();
Vector2 pressInputPos = new(
_pressScreenPosition.x,
replayFrame.GameViewSize.y - _pressScreenPosition.y);
SimulateMouseUiOverlayState.Update(
MouseAction.Drag,
replayFrame.InputPosition,
pressInputPos,
null,
replayFrame.GameViewSize);
return;
}

float elapsed = Time.realtimeSinceStartup - _pressTime;
if (elapsed < 0.5f)
{
return;
}

SimulateMouseUiOverlayState.Update(
MouseAction.LongPress,
replayFrame.InputPosition,
null,
_currentPressTarget?.name,
replayFrame.GameViewSize);
SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed);
}

private void ApplyUiPointerRelease(UiReplayFrame replayFrame)
{
if (!replayFrame.JustReleased)
{
return;
}

OnUiPointerUp(replayFrame.ScreenPosition, replayFrame.EventSystem);
_suppressIdleUiOverlay = true;
SimulateMouseUiOverlayState.RequestDissipateAnimation();
SimulateMouseUiOverlayState.Clear();
}

private void OnUiPointerDown(Vector2 screenPos, EventSystem eventSystem)
Expand Down Expand Up @@ -733,6 +790,38 @@ private void OnPlayModeStateChanged(PlayModeStateChange state)
StopReplay();
}
}

private readonly struct UiReplayFrame
{
public UiReplayFrame(
EventSystem eventSystem,
Vector2 screenPosition,
Vector2 inputPosition,
Vector2 gameViewSize,
bool leftHeld,
bool justPressed,
bool justReleased,
bool mouseMoved)
{
EventSystem = eventSystem;
ScreenPosition = screenPosition;
InputPosition = inputPosition;
GameViewSize = gameViewSize;
LeftHeld = leftHeld;
JustPressed = justPressed;
JustReleased = justReleased;
MouseMoved = mouseMoved;
}

public EventSystem EventSystem { get; }
public Vector2 ScreenPosition { get; }
public Vector2 InputPosition { get; }
public Vector2 GameViewSize { get; }
public bool LeftHeld { get; }
public bool JustPressed { get; }
public bool JustReleased { get; }
public bool MouseMoved { get; }
}
}

/// <summary>
Expand Down
Loading
Loading