Skip to content

Commit

Permalink
Merge pull request #103 from nowsprinting/chore/refactor_interactivec…
Browse files Browse the repository at this point in the history
…omponent

Refactor InteractiveComponent
  • Loading branch information
nowsprinting authored Mar 26, 2024
2 parents c33e384 + 6214d44 commit e421cbc
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 121 deletions.
2 changes: 1 addition & 1 deletion Runtime/GameObjectFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private enum Reason

if (reachable)
{
_eventData.position = _getScreenPoint(foundObject);
_eventData.position = _getScreenPoint.Invoke(foundObject);
if (!_isReachable.Invoke(foundObject, _eventData, _results))
{
return (null, Reason.NotReachable);
Expand Down
7 changes: 4 additions & 3 deletions Runtime/Hints/InteractiveComponentHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace TestHelper.Monkey.Hints
public class InteractiveComponentHint : MonoBehaviour
{
private static readonly Color s_orange = new Color(0xef, 0x81, 0x0f);

/// <summary>
/// Color for interactive components that users can operate
/// </summary>
Expand Down Expand Up @@ -98,9 +98,10 @@ private void Refresh()
{
Clear();

foreach (var component in InteractiveComponentCollector.FindInteractableComponents())
var interactiveComponentCollector = new InteractiveComponentCollector(getScreenPoint: GetScreenPoint);
foreach (var component in interactiveComponentCollector.FindInteractableComponents())
{
var dst = component.IsReallyInteractiveFromUser(GetScreenPoint)
var dst = component.IsReachable()
? _tmpReallyInteractives
: _tmpNotReallyInteractives;

Expand Down
63 changes: 46 additions & 17 deletions Runtime/InteractiveComponent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 Koji Hasegawa.
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System;
Expand All @@ -10,6 +10,7 @@
using TestHelper.Monkey.Extensions;
using TestHelper.Monkey.Operators;
using TestHelper.Monkey.Random;
using TestHelper.Monkey.ScreenPointStrategies;
using UnityEngine;
using UnityEngine.EventSystems;

Expand Down Expand Up @@ -39,32 +40,55 @@ public class InteractiveComponent
[SuppressMessage("ReSharper", "InconsistentNaming")]
public GameObject gameObject => component.gameObject;

private readonly Func<GameObject, Vector2> _getScreenPoint;
private readonly Func<GameObject, PointerEventData, List<RaycastResult>, bool> _isReachable;
private readonly PointerEventData _eventData = new PointerEventData(EventSystem.current);
private readonly List<RaycastResult> _results = new List<RaycastResult>();

/// <summary>
/// Constructor
/// </summary>
/// <param name="component"></param>
public InteractiveComponent(MonoBehaviour component)
/// <param name="getScreenPoint">The function returns the screen position where raycast for the found <c>GameObject</c>.
/// Default is <c>DefaultScreenPointStrategy.GetScreenPoint</c>.</param>
/// <param name="isReachable">The function returns the <c>GameObject</c> is reachable from user or not.
/// Default is <c>DefaultReachableStrategy.IsReachable</c>.</param>
public InteractiveComponent(MonoBehaviour component,
Func<GameObject, Vector2> getScreenPoint = null,
Func<GameObject, PointerEventData, List<RaycastResult>, bool> isReachable = null)
{
this.component = component;
_getScreenPoint = getScreenPoint ?? DefaultScreenPointStrategy.GetScreenPoint;
_isReachable = isReachable ?? DefaultReachableStrategy.IsReachable;
}

/// <summary>
/// Create <c>InteractableComponent</c> instance from GameObject.
/// </summary>
/// <param name="gameObject"></param>
/// <param name="isInteractable">The function returns the <c>Component</c> is interactable or not.
/// <param name="getScreenPoint">The function returns the screen position where raycast for the found <c>GameObject</c>.
/// Default is <c>DefaultScreenPointStrategy.GetScreenPoint</c>.</param>
/// <param name="isComponentInteractable">The function returns the <c>Component</c> is interactable or not.
/// Default is <c>DefaultComponentInteractableStrategy.IsInteractable</c>.</param>
/// <param name="isReachable">The function returns the <c>GameObject</c> is reachable from user or not.
/// Default is <c>DefaultReachableStrategy.IsReachable</c>.</param>
/// <returns>Returns new InteractableComponent instance from GameObject. If GameObject is not interactable so, return null.</returns>
public static InteractiveComponent CreateInteractableComponent(GameObject gameObject,
Func<Component, bool> isInteractable = null)
Func<GameObject, Vector2> getScreenPoint = null,
Func<Component, bool> isComponentInteractable = null,
Func<GameObject, PointerEventData, List<RaycastResult>, bool> isReachable = null)
{
isInteractable = isInteractable ?? DefaultComponentInteractableStrategy.IsInteractable;
getScreenPoint = getScreenPoint ?? DefaultScreenPointStrategy.GetScreenPoint;
isComponentInteractable = isComponentInteractable ?? DefaultComponentInteractableStrategy.IsInteractable;
isReachable = isReachable ?? DefaultReachableStrategy.IsReachable;

foreach (var component in gameObject.GetComponents<MonoBehaviour>())
{
if (isInteractable(component))
if (isComponentInteractable.Invoke(component))
{
return new InteractiveComponent(component);
return new InteractiveComponent(component,
getScreenPoint,
isReachable);
}
}

Expand All @@ -78,13 +102,23 @@ public static InteractiveComponent CreateInteractableComponent(GameObject gameOb
/// <param name="eventData">Specify if avoid GC memory allocation</param>
/// <param name="results">Specify if avoid GC memory allocation</param>
/// <returns>true: this object can control by user</returns>
[Obsolete("Use GameObjectExtensions.IsReachable() instead")]
[Obsolete("Use IsReachable() instead")]
public bool IsReallyInteractiveFromUser(Func<GameObject, Vector2> screenPointStrategy,
PointerEventData eventData = null, List<RaycastResult> results = null)
{
return gameObject.IsReachable(screenPointStrategy, eventData, results);
}

/// <summary>
/// Hit test using raycaster
/// </summary>
/// <returns>true: this object can control by user</returns>
public bool IsReachable()
{
_eventData.position = _getScreenPoint.Invoke(gameObject);
return _isReachable.Invoke(gameObject, _eventData, _results);
}

/// <summary>
/// Check inner component can receive click event
/// </summary>
Expand All @@ -94,9 +128,7 @@ public bool IsReallyInteractiveFromUser(Func<GameObject, Vector2> screenPointStr
/// <summary>
/// Click inner component
/// </summary>
/// <param name="screenPointStrategy">Function returns the screen position where monkey operators operate on for the specified gameObject</param>
public void Click(Func<GameObject, Vector2> screenPointStrategy = null) =>
ClickOperator.Click(component, screenPointStrategy);
public void Click() => ClickOperator.Click(component, _getScreenPoint);

/// <summary>
/// Check inner component can receive tap (click) event
Expand All @@ -107,9 +139,7 @@ public void Click(Func<GameObject, Vector2> screenPointStrategy = null) =>
/// <summary>
/// Tap (click) inner component
/// </summary>
/// <param name="screenPointStrategy">Function returns the screen position where monkey operators operate on for the specified gameObject</param>
public void Tap(Func<GameObject, Vector2> screenPointStrategy = null) =>
ClickOperator.Click(component, screenPointStrategy);
public void Tap() => ClickOperator.Click(component, _getScreenPoint);

/// <summary>
/// Check inner component can receive touch-and-hold event
Expand All @@ -123,9 +153,8 @@ public void Tap(Func<GameObject, Vector2> screenPointStrategy = null) =>
/// <param name="screenPointStrategy">Function returns the screen position where monkey operators operate on for the specified gameObject</param>
/// <param name="delayMillis">Delay time between down to up</param>
/// <param name="cancellationToken">Task cancellation token</param>
public async UniTask TouchAndHold(Func<GameObject, Vector2> screenPointStrategy = null, int delayMillis = 1000,
CancellationToken cancellationToken = default)
=> await TouchAndHoldOperator.TouchAndHold(component, screenPointStrategy, delayMillis, cancellationToken);
public async UniTask TouchAndHold(int delayMillis = 1000, CancellationToken cancellationToken = default)
=> await TouchAndHoldOperator.TouchAndHold(component, _getScreenPoint, delayMillis, cancellationToken);

/// <summary>
/// Check inner component can input text
Expand Down
66 changes: 40 additions & 26 deletions Runtime/InteractiveComponentCollector.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright (c) 2023 Koji Hasegawa.
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System;
using System.Collections.Generic;
using TestHelper.Monkey.DefaultStrategies;
using TestHelper.Monkey.ScreenPointStrategies;
using UnityEngine;
using UnityEngine.EventSystems;
using Object = UnityEngine.Object;
Expand All @@ -14,36 +15,58 @@ namespace TestHelper.Monkey
/// Find <c>InteractableComponent</c>s in the scene.
/// </summary>
// TODO: Rename to InteractableComponentsFinder
public static class InteractiveComponentCollector
public class InteractiveComponentCollector
{
private readonly Func<GameObject, Vector2> _getScreenPoint;
private readonly Func<Component, bool> _isComponentInteractable;
private readonly Func<GameObject, PointerEventData, List<RaycastResult>, bool> _isReachable;
private readonly PointerEventData _eventData = new PointerEventData(EventSystem.current);
private readonly List<RaycastResult> _results = new List<RaycastResult>();

/// <summary>
/// Constructor.
/// </summary>
/// <param name="getScreenPoint">The function returns the screen position where raycast for the found <c>GameObject</c>.
/// Default is <c>DefaultScreenPointStrategy.GetScreenPoint</c>.</param>
/// <param name="isComponentInteractable">The function returns the <c>Component</c> is interactable or not.
/// Default is <c>DefaultComponentInteractableStrategy.IsInteractable</c>.</param>
/// <param name="isReachable">The function returns the <c>GameObject</c> is reachable from user or not.
/// Default is <c>DefaultReachableStrategy.IsReachable</c>.</param>
public InteractiveComponentCollector(
Func<GameObject, Vector2> getScreenPoint = null,
Func<Component, bool> isComponentInteractable = null,
Func<GameObject, PointerEventData, List<RaycastResult>, bool> isReachable = null)
{
_getScreenPoint = getScreenPoint ?? DefaultScreenPointStrategy.GetScreenPoint;
_isComponentInteractable = isComponentInteractable ?? DefaultComponentInteractableStrategy.IsInteractable;
_isReachable = isReachable ?? DefaultReachableStrategy.IsReachable;
}

/// <summary>
/// Find components attached EventTrigger or implements IEventSystemHandler in scene.
/// Includes UI elements that inherit from the Selectable class, such as Button.
///
/// Note: If you only need UI elements, using UnityEngine.UI.Selectable.allSelectablesArray is faster.
/// </summary>
/// <param name="isInteractable">The function returns the <c>Component</c> is interactable or not.
/// Default is <c>DefaultComponentInteractableStrategy.IsInteractable</c>.</param>
/// <returns>Interactive components</returns>
// TODO: Change to instance method
public static IEnumerable<InteractiveComponent> FindInteractableComponents(
Func<Component, bool> isInteractable = null)
public IEnumerable<InteractiveComponent> FindInteractableComponents()
{
isInteractable = isInteractable ?? DefaultComponentInteractableStrategy.IsInteractable;

foreach (var component in FindMonoBehaviours())
{
if (isInteractable(component))
if (_isComponentInteractable.Invoke(component))
{
yield return new InteractiveComponent(component);
yield return new InteractiveComponent(component,
_getScreenPoint,
_isReachable);
}
}
}

[Obsolete("Use FindInteractableComponents() instead")]
public static IEnumerable<InteractiveComponent> FindInteractiveComponents()
{
return FindInteractableComponents();
var instance = new InteractiveComponentCollector();
return instance.FindInteractableComponents();
}

/// <summary>
Expand All @@ -52,23 +75,13 @@ public static IEnumerable<InteractiveComponent> FindInteractiveComponents()
///
/// Note: If you only need UI elements, using UnityEngine.UI.Selectable.allSelectablesArray is faster.
/// </summary>
/// <param name="screenPointStrategy">Function returns the screen position where monkey operators operate on for the specified gameObject</param>
/// <returns>Really interactive components</returns>
/// <param name="isReachable">The function returns the <c>GameObject</c> is reachable from user or not.
/// Default is <c>DefaultReachableStrategy.IsReachable</c>.</param>
// TODO: Change to instance method
public static IEnumerable<InteractiveComponent> FindReachableInteractableComponents(
Func<GameObject, Vector2> screenPointStrategy,
Func<GameObject, PointerEventData, List<RaycastResult>, bool> isReachable = null)
public IEnumerable<InteractiveComponent> FindReachableInteractableComponents()
{
isReachable = isReachable ?? DefaultReachableStrategy.IsReachable;
var data = new PointerEventData(EventSystem.current);
var results = new List<RaycastResult>();

foreach (var interactiveComponent in FindInteractableComponents())
{
data.position = screenPointStrategy(interactiveComponent.gameObject);
if (isReachable(interactiveComponent.gameObject, data, results))
_eventData.position = _getScreenPoint.Invoke(interactiveComponent.gameObject);
if (_isReachable.Invoke(interactiveComponent.gameObject, _eventData, _results))
{
yield return interactiveComponent;
}
Expand All @@ -79,7 +92,8 @@ public static IEnumerable<InteractiveComponent> FindReachableInteractableCompone
public static IEnumerable<InteractiveComponent> FindReallyInteractiveComponents(
Func<GameObject, Vector2> screenPointStrategy)
{
return FindReachableInteractableComponents(screenPointStrategy);
var instance = new InteractiveComponentCollector(getScreenPoint: screenPointStrategy);
return instance.FindReachableInteractableComponents();
}

private static IEnumerable<MonoBehaviour> FindMonoBehaviours()
Expand Down
32 changes: 14 additions & 18 deletions Runtime/Monkey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using TestHelper.Monkey.DefaultStrategies;
using TestHelper.Random;
using TestHelper.RuntimeInternals;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.EventSystems;

namespace TestHelper.Monkey
{
Expand Down Expand Up @@ -46,13 +44,18 @@ public static async UniTask Run(MonkeyConfig config, CancellationToken cancellat
GameViewControlHelper.SetGizmos(true);
}

var interactiveComponentCollector = new InteractiveComponentCollector(
getScreenPoint: config.ScreenPointStrategy
);
// TODO: Set other strategies

config.Logger.Log($"Using {config.Random}");

try
{
while (Time.time < endTime)
{
var didAct = await RunStep(config, cancellationToken);
var didAct = await RunStep(config, interactiveComponentCollector, cancellationToken);
if (didAct)
{
lastOperationTime = Time.time;
Expand Down Expand Up @@ -85,14 +88,16 @@ private class CoroutineRunner : MonoBehaviour
/// Run a step of monkey testing.
/// </summary>
/// <param name="config">Run configuration for monkey testing</param>
/// <param name="interactiveComponentCollector"></param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
public static async UniTask<bool> RunStep(MonkeyConfig config, CancellationToken cancellationToken = default)
public static async UniTask<bool> RunStep(MonkeyConfig config,
InteractiveComponentCollector interactiveComponentCollector, CancellationToken cancellationToken = default)
{
var components = InteractiveComponentCollector
var components = interactiveComponentCollector
.FindInteractableComponents()
.ToList();
var component = Lottery(ref components, config.Random, config.ScreenPointStrategy);
var component = Lottery(ref components, config.Random);
if (component == null)
{
return false;
Expand Down Expand Up @@ -120,20 +125,13 @@ await ScreenshotHelper.TakeScreenshot(

internal static InteractiveComponent Lottery(
ref List<InteractiveComponent> components,
IRandom random,
Func<GameObject, Vector2> screenPointStrategy
)
IRandom random)
{
if (components == null || components.Count == 0)
{
return null;
}

Func<GameObject, PointerEventData, List<RaycastResult>, bool> isReachable =
DefaultReachableStrategy.IsReachable;
var eventData = new PointerEventData(EventSystem.current);
var results = new List<RaycastResult>();

while (true)
{
if (components.Count == 0)
Expand All @@ -142,8 +140,7 @@ Func<GameObject, Vector2> screenPointStrategy
}

var next = components[random.Next(components.Count)];
eventData.position = screenPointStrategy(next.gameObject);
if (isReachable(next.gameObject, eventData, results) && GetCanOperations(next).Any())
if (next.IsReachable() && GetCanOperations(next).Any())
{
return next;
}
Expand Down Expand Up @@ -178,11 +175,10 @@ internal static async UniTask DoOperation(
switch (operation)
{
case SupportOperation.Click:
component.Click(config.ScreenPointStrategy);
component.Click();
break;
case SupportOperation.TouchAndHold:
await component.TouchAndHold(
config.ScreenPointStrategy,
config.TouchAndHoldDelayMillis,
cancellationToken
);
Expand Down
Loading

0 comments on commit e421cbc

Please sign in to comment.