using System; using System.Collections; using System.Reflection; using System.IO; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using UnityEngine.TestTools; using System.Runtime.CompilerServices; public class ScrollRectTests : IPrebuildSetup { const int ScrollSensitivity = 3; GameObject m_PrefabRoot; const string kPrefabPath = "Assets/Resources/ScrollRectPrefab.prefab"; public void Setup() { #if UNITY_EDITOR var rootGO = new GameObject("rootGo"); GameObject eventSystemGO = new GameObject("EventSystem", typeof(EventSystem)); eventSystemGO.transform.SetParent(rootGO.transform); var canvasGO = new GameObject("Canvas", typeof(Canvas)); canvasGO.transform.SetParent(rootGO.transform); var canvas = canvasGO.GetComponent(); canvas.referencePixelsPerUnit = 100; GameObject scrollRectGO = new GameObject("ScrollRect", typeof(RectTransform), typeof(ScrollRect)); scrollRectGO.transform.SetParent(canvasGO.transform); GameObject contentGO = new GameObject("Content", typeof(RectTransform)); contentGO.transform.SetParent(scrollRectGO.transform); GameObject horizontalScrollBarGO = new GameObject("HorizontalScrollBar", typeof(Scrollbar)); horizontalScrollBarGO.transform.SetParent(scrollRectGO.transform); GameObject verticalScrollBarGO = new GameObject("VerticalScrollBar", typeof(Scrollbar)); verticalScrollBarGO.transform.SetParent(scrollRectGO.transform); ScrollRect scrollRect = scrollRectGO.GetComponent(); scrollRect.transform.position = Vector3.zero; scrollRect.transform.rotation = Quaternion.identity; scrollRect.transform.localScale = Vector3.one; (scrollRect.transform as RectTransform).sizeDelta = new Vector3(0.5f, 0.5f); scrollRect.horizontalScrollbar = horizontalScrollBarGO.GetComponent(); scrollRect.verticalScrollbar = verticalScrollBarGO.GetComponent(); scrollRect.content = contentGO.GetComponent(); scrollRect.content.anchoredPosition = Vector2.zero; scrollRect.content.sizeDelta = new Vector2(3, 3); scrollRect.scrollSensitivity = ScrollSensitivity; scrollRect.GetComponent().sizeDelta = new Vector2(1, 1); if (!Directory.Exists("Assets/Resources/")) Directory.CreateDirectory("Assets/Resources/"); PrefabUtility.SaveAsPrefabAsset(rootGO, kPrefabPath); GameObject.DestroyImmediate(rootGO); #endif } [SetUp] public void TestSetup() { m_PrefabRoot = UnityEngine.Object.Instantiate(Resources.Load("ScrollRectPrefab")) as GameObject; } [TearDown] public void TearDown() { GameObject.DestroyImmediate(m_PrefabRoot); } [OneTimeTearDown] public void OneTimeTearDown() { #if UNITY_EDITOR AssetDatabase.DeleteAsset(kPrefabPath); #endif } #region Enable disable scrollbars [UnityTest] [TestCase(true, ExpectedResult = null)] [TestCase(false, ExpectedResult = null)] public IEnumerator OnEnableShouldAddListeners(bool isHorizontal) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); Scrollbar scrollbar = isHorizontal ? scrollRect.horizontalScrollbar : scrollRect.verticalScrollbar; scrollRect.enabled = false; yield return null; FieldInfo field = scrollbar.onValueChanged.GetType().BaseType.BaseType.GetField("m_Calls", BindingFlags.NonPublic | BindingFlags.Instance); object invokeableCallList = field.GetValue(scrollbar.onValueChanged); PropertyInfo property = invokeableCallList.GetType().GetProperty("Count", BindingFlags.Public | BindingFlags.Instance); int callCount = (int)property.GetValue(invokeableCallList, null); scrollRect.enabled = true; yield return null; Assert.AreEqual(callCount + 1, (int)property.GetValue(invokeableCallList, null)); } [UnityTest] [TestCase(true, ExpectedResult = null)] [TestCase(false, ExpectedResult = null)] public IEnumerator OnDisableShouldRemoveListeners(bool isHorizontal) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); Scrollbar scrollbar = isHorizontal ? scrollRect.horizontalScrollbar : scrollRect.verticalScrollbar; scrollRect.enabled = true; yield return null; FieldInfo field = scrollbar.onValueChanged.GetType().BaseType.BaseType.GetField("m_Calls", BindingFlags.NonPublic | BindingFlags.Instance); object invokeableCallList = field.GetValue(scrollbar.onValueChanged); PropertyInfo property = invokeableCallList.GetType().GetProperty("Count", BindingFlags.Public | BindingFlags.Instance); Assert.AreNotEqual(0, (int)property.GetValue(invokeableCallList, null)); scrollRect.enabled = false; yield return null; Assert.AreEqual(0, (int)property.GetValue(invokeableCallList, null)); } [Test] [TestCase(true)] [TestCase(false)] public void SettingScrollbarShouldRemoveThenAddListeners(bool testHorizontal) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); Scrollbar scrollbar = testHorizontal ? scrollRect.horizontalScrollbar : scrollRect.verticalScrollbar; GameObject scrollBarGO = new GameObject("scrollBar", typeof(RectTransform), typeof(Scrollbar)); scrollBarGO.transform.SetParent(scrollRect.transform); Scrollbar newScrollbar = scrollBarGO.GetComponent(); FieldInfo field = newScrollbar.onValueChanged.GetType().BaseType.BaseType.GetField("m_Calls", BindingFlags.NonPublic | BindingFlags.Instance); PropertyInfo property = field.GetValue(newScrollbar.onValueChanged).GetType().GetProperty("Count", BindingFlags.Public | BindingFlags.Instance); int newCallCount = (int)property.GetValue(field.GetValue(newScrollbar.onValueChanged), null); if (testHorizontal) scrollRect.horizontalScrollbar = newScrollbar; else scrollRect.verticalScrollbar = newScrollbar; Assert.AreEqual(0, (int)property.GetValue(field.GetValue(scrollbar.onValueChanged), null), "The previous scrollbar should not have listeners anymore"); Assert.AreEqual(newCallCount + 1, (int)property.GetValue(field.GetValue(newScrollbar.onValueChanged), null), "The new scrollbar should have listeners now"); } #endregion #region Drag [Test] [TestCase(PointerEventData.InputButton.Left, true)] [TestCase(PointerEventData.InputButton.Right, false)] [TestCase(PointerEventData.InputButton.Middle, false)] public void PotentialDragNeedsLeftClick(PointerEventData.InputButton button, bool expectedEqualsZero) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; Assert.AreNotEqual(Vector2.zero, scrollRect.velocity); scrollRect.OnInitializePotentialDrag(new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { button = button }); if (expectedEqualsZero) Assert.AreEqual(Vector2.zero, scrollRect.velocity); else Assert.AreNotEqual(Vector2.zero, scrollRect.velocity); } [Test] [TestCase(PointerEventData.InputButton.Left, true, true)] [TestCase(PointerEventData.InputButton.Left, false, false)] [TestCase(PointerEventData.InputButton.Right, true, false)] [TestCase(PointerEventData.InputButton.Middle, true, false)] public void LeftClickShouldStartDrag(PointerEventData.InputButton button, bool active, bool expectedIsDragging) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.enabled = active; scrollRect.velocity = Vector2.one; Assert.AreNotEqual(Vector2.zero, scrollRect.velocity); var pointerEventData = new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { button = button }; scrollRect.OnInitializePotentialDrag(pointerEventData); FieldInfo field = typeof(ScrollRect).GetField("m_Dragging", BindingFlags.NonPublic | BindingFlags.Instance); Assert.IsFalse((bool)field.GetValue(scrollRect)); scrollRect.OnBeginDrag(pointerEventData); Assert.AreEqual(expectedIsDragging, (bool)field.GetValue(scrollRect)); } [Test] [TestCase(PointerEventData.InputButton.Left, true, false)] [TestCase(PointerEventData.InputButton.Left, false, false)] [TestCase(PointerEventData.InputButton.Right, false, false)] [TestCase(PointerEventData.InputButton.Right, true, true)] [TestCase(PointerEventData.InputButton.Middle, true, true)] [TestCase(PointerEventData.InputButton.Middle, false, false)] public void LeftClickUpShouldEndDrag(PointerEventData.InputButton button, bool active, bool expectedIsDragging) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; Assert.AreNotEqual(Vector2.zero, scrollRect.velocity); var startDragEventData = new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { button = PointerEventData.InputButton.Left }; scrollRect.OnInitializePotentialDrag(startDragEventData); FieldInfo field = typeof(ScrollRect).GetField("m_Dragging", BindingFlags.NonPublic | BindingFlags.Instance); Assert.IsFalse((bool)field.GetValue(scrollRect)); scrollRect.OnBeginDrag(startDragEventData); Assert.IsTrue((bool)field.GetValue(scrollRect), "Prerequisite: dragging should be true to test if it is set to false later"); scrollRect.enabled = active; var endDragEventData = new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { button = button }; scrollRect.OnEndDrag(endDragEventData); Assert.AreEqual(expectedIsDragging, (bool)field.GetValue(scrollRect)); } #endregion #region LateUpdate [UnityTest] public IEnumerator LateUpdateWithoutInertiaOrElasticShouldZeroVelocity() { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; scrollRect.inertia = false; scrollRect.movementType = ScrollRect.MovementType.Clamped; yield return null; Assert.AreEqual(Vector2.zero, scrollRect.velocity); } [UnityTest] public IEnumerator LateUpdateWithInertiaShouldDecelerate() { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; scrollRect.inertia = true; scrollRect.movementType = ScrollRect.MovementType.Clamped; yield return null; Assert.Less(scrollRect.velocity.magnitude, 1); } [UnityTest][Ignore("Fails")] public IEnumerator LateUpdateWithElasticShouldDecelerate() { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; scrollRect.inertia = false; scrollRect.content.anchoredPosition = Vector2.one * 2; scrollRect.movementType = ScrollRect.MovementType.Elastic; yield return null; Assert.AreNotEqual(1, scrollRect.velocity.magnitude); var newMagnitude = scrollRect.velocity.magnitude; yield return null; Assert.AreNotEqual(newMagnitude, scrollRect.velocity.magnitude); } [UnityTest] public IEnumerator LateUpdateWithElasticNoOffsetShouldZeroVelocity() { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.velocity = Vector2.one; scrollRect.inertia = false; scrollRect.movementType = ScrollRect.MovementType.Elastic; yield return null; Assert.AreEqual(Vector2.zero, scrollRect.velocity); } #endregion [Test] public void SetNormalizedPositionShouldSetContentLocalPosition() { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); scrollRect.normalizedPosition = Vector2.one; Assert.AreEqual(new Vector3(-1f, -1f, 0), scrollRect.content.localPosition); } #region Scroll, offset, ... [Test] [TestCase(1, 1, true, true, 1, -1, TestName = "Horizontal and vertical scroll")] [TestCase(1, 1, false, true, 0, -1, TestName = "Vertical scroll")] [TestCase(1, 1, true, false, 1, 0, TestName = "Horizontal scroll")] public void OnScrollClampedShouldMoveContentAnchoredPosition(int scrollDeltaX, int scrollDeltaY, bool horizontal, bool vertical, int expectedPosX, int expectedPosY) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); Vector2 scrollDelta = new Vector2(scrollDeltaX, scrollDeltaY); var expected = new Vector2(expectedPosX, expectedPosY) * ScrollSensitivity; scrollRect.horizontal = horizontal; scrollRect.vertical = vertical; scrollRect.OnScroll(new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { scrollDelta = scrollDelta }); Assert.AreEqual(expected, scrollRect.content.anchoredPosition); } [Test][Ignore("Tests fail without mocking")] [TestCase(ScrollRect.MovementType.Clamped, 1f, 1f)] [TestCase(ScrollRect.MovementType.Unrestricted, 150, 150)] [TestCase(ScrollRect.MovementType.Elastic, 150, 150)] public void OnScrollClampedShouldClampContentAnchoredPosition(ScrollRect.MovementType movementType, float anchoredPosX, float anchoredPosY) { ScrollRect scrollRect = m_PrefabRoot.GetComponentInChildren(); Vector2 scrollDelta = new Vector2(50, -50); scrollRect.movementType = movementType; scrollRect.content.anchoredPosition = new Vector2(2.5f, 2.5f); scrollRect.OnScroll(new PointerEventData(m_PrefabRoot.GetComponentInChildren()) { scrollDelta = scrollDelta }); Assert.AreEqual(new Vector2(anchoredPosX, anchoredPosY), scrollRect.content.anchoredPosition); } [Test] public void GetBoundsShouldEncapsulateAllCorners() { Matrix4x4 matrix = Matrix4x4.identity; object[] arguments = new object[2] { new Vector3[] { Vector3.zero, Vector3.one, Vector3.one * 2, Vector3.one }, matrix }; MethodInfo method = typeof(ScrollRect).GetMethod("InternalGetBounds", BindingFlags.NonPublic | BindingFlags.Static); var bounds = (Bounds)method.Invoke(null, arguments); Assert.AreEqual(Vector3.zero, bounds.min); Assert.AreEqual(Vector3.one * 2, bounds.max); } [Test] public void UpdateBoundsShouldPad() { Bounds viewBounds = new Bounds(Vector3.zero, Vector3.one * 2); Vector3 contentSize = Vector3.one; Vector3 contentPos = Vector3.one; var contentPivot = new Vector2(0.5f, 0.5f); object[] arguments = new object[] { viewBounds, contentPivot, contentSize, contentPos }; MethodInfo method = typeof(ScrollRect).GetMethod("AdjustBounds", BindingFlags.NonPublic | BindingFlags.Static); method.Invoke(null, arguments); //ScrollRect.AdjustBounds(ref viewBounds, ref contentPivot, ref contentSize, ref contentPos); Assert.AreEqual(new Vector3(2, 2, 1), arguments[2]); } [Test] [TestCase(true, true, 2, 4, -2, -2, TestName = "Should clamp offset")] [TestCase(false, true, 2, 4, 0, -2, TestName = "Vertical should clamp offset on one axis")] [TestCase(true, false, 2, 4, -2, 0, TestName = "Horizontal should clamp offset on one axis")] [TestCase(false, false, 2, 4, 0, 0, TestName = "No axis should not clamp offset")] [TestCase(true, true, 8, 10, 2, 2, TestName = "Should clamp negative offset")] [TestCase(false, true, 8, 10, 0, 2, TestName = "Vertical should clamp negative offset on one axis")] [TestCase(true, false, 8, 10, 2, 0, TestName = "Horizontal should clamp negative offset on one axis")] [TestCase(false, false, 8, 10, 0, 0, TestName = "No axis should not clamp negative offset")] public void CalculateOffsetShouldClamp(bool horizontal, bool vertical, int viewX, float viewY, float resX, float resY) { TestCalculateOffset(ScrollRect.MovementType.Clamped, horizontal, vertical, viewX, viewY, resX, resY, new Bounds(new Vector3(5, 7), new Vector3(4, 4))); } [Test] [TestCase(true, true, 2, 4, -2, -2, TestName = "Should clamp offset")] [TestCase(false, true, 2, 4, 0, -2, TestName = "Vertical should clamp offset on one axis")] [TestCase(true, false, 2, 4, -2, 0, TestName = "Horizontal should clamp offset on one axis")] [TestCase(false, false, 2, 4, 0, 0, TestName = "No axis should not clamp offset")] [TestCase(true, true, 8, 10, 2, 2, TestName = "Should clamp negative offset")] [TestCase(false, true, 8, 10, 0, 2, TestName = "Vertical should clamp negative offset on one axis")] [TestCase(true, false, 8, 10, 2, 0, TestName = "Horizontal should clamp negative offset on one axis")] [TestCase(false, false, 8, 10, 0, 0, TestName = "No axis should not clamp negative offset")] public void CalculateOffsetUnrestrictedShouldNotClamp(bool horizontal, bool vertical, int viewX, float viewY, float resX, float resY) { TestCalculateOffset(ScrollRect.MovementType.Unrestricted, horizontal, vertical, viewX, viewY, 0, 0, new Bounds(new Vector3(5, 7), new Vector3(4, 4))); } private static void TestCalculateOffset(ScrollRect.MovementType movementType, bool horizontal, bool vertical, int viewX, float viewY, float resX, float resY, Bounds contentBounds) { Bounds viewBounds = new Bounds(new Vector3(viewX, viewY), new Vector3(2, 2)); // content is south east of view Vector2 delta = Vector2.zero; object[] arguments = new object[] { viewBounds, contentBounds, horizontal, vertical, movementType, delta }; MethodInfo method = typeof(ScrollRect).GetMethod("InternalCalculateOffset", BindingFlags.NonPublic | BindingFlags.Static); var result = (Vector2)method.Invoke(null, arguments); Console.WriteLine(result); Assert.AreEqual(new Vector2(resX, resY), result); } #endregion }