This commit is contained in:
ThomasCharester committed 2026-09-21 12:15:57 +03:00
1 parent 36840f3ad4
commit fc5fa4aa1d
28259 files changed
+5452504 -105

No files matched your search

@@ -0,0 +1,267 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UI;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
namespace UnityEditor.UI.Tests
{
internal class LayoutElementEditorTests
{
static readonly string[] k_FloatFieldLabels =
{
"Min Width", "Min Height",
"Max Width", "Max Height",
"Preferred Width", "Preferred Height",
"Flexible Width", "Flexible Height"
};
GameObject m_GameObject;
LayoutElement m_LayoutElement;
Editor m_Editor;
EditorWindow m_Window;
VisualElement m_Root;
[SetUp]
public void SetUp()
{
VisualTreeBindingsUpdater.disableBindingsThrottling = true;
m_GameObject = new GameObject("LayoutElement", typeof(RectTransform));
m_LayoutElement = m_GameObject.AddComponent<LayoutElement>();
m_Editor = Editor.CreateEditor(m_LayoutElement);
m_Root = m_Editor.CreateInspectorGUI();
m_Window = EditorWindow.CreateWindow<EditorWindow>();
m_Window.rootVisualElement.Add(m_Root);
m_Window.Show();
}
[TearDown]
public void TearDown()
{
VisualTreeBindingsUpdater.disableBindingsThrottling = false;
if (m_Window != null)
m_Window.Close();
if (m_Editor != null)
Object.DestroyImmediate(m_Editor);
if (m_GameObject != null)
Object.DestroyImmediate(m_GameObject);
}
(Toggle toggle, FloatField field, Label labelElement) RowFor(string label)
{
var labelElement = m_Root.Query<Label>().Where(l => l.text == label).First();
var row = labelElement.parent;
return (row.Q<Toggle>(), row.Q<FloatField>(), labelElement);
}
float PersistedValue(string label)
{
switch (label)
{
case "Min Width": return m_LayoutElement.minWidth;
case "Min Height": return m_LayoutElement.minHeight;
case "Max Width": return m_LayoutElement.maxWidth;
case "Max Height": return m_LayoutElement.maxHeight;
case "Preferred Width": return m_LayoutElement.preferredWidth;
case "Preferred Height": return m_LayoutElement.preferredHeight;
case "Flexible Width": return m_LayoutElement.flexibleWidth;
case "Flexible Height": return m_LayoutElement.flexibleHeight;
default: throw new System.ArgumentException($"Unknown label '{label}'", nameof(label));
}
}
[UnityTest]
public IEnumerator NegativeValue_IsClampedToZero([ValueSource(nameof(k_FloatFieldLabels))] string label)
{
var (toggle, field, _) = RowFor(label);
toggle.value = true;
yield return null;
field.Focus();
yield return null;
field.value = -1f;
yield return null;
Assert.That(PersistedValue(label), Is.EqualTo(0f),
$"Negative '{label}' should be clamped to 0 in the LayoutElement.");
Assert.That(field.value, Is.EqualTo(0f),
$"The '{label}' field should display the clamped value rather than the rejected one.");
}
// Disabling a field stores a sentinel (-1, or +Infinity for Max fields); it should stay
// disabled rather than be clamped into the enabled range.
[UnityTest]
public IEnumerator DisablingField_DoesNotClampToEnabledValue([ValueSource(nameof(k_FloatFieldLabels))] string label)
{
var (toggle, field, _) = RowFor(label);
toggle.value = true;
field.value = 5f;
yield return null;
Assume.That(PersistedValue(label), Is.EqualTo(5f));
toggle.value = false;
yield return null;
yield return null;
float value = PersistedValue(label);
bool isEnabledValue = value >= 0f && value < LayoutUtility.DefaultMaxSize;
Assert.That(isEnabledValue, Is.False,
$"Disabling '{label}' should leave it disabled, but it was clamped to an enabled value ({value}).");
}
[UnityTest]
public IEnumerator BindingDrivenDisable_SyncsToggleAndVisibility([ValueSource(nameof(k_FloatFieldLabels))] string label)
{
var (toggle, field, _) = RowFor(label);
toggle.value = true;
yield return null;
Assume.That(toggle.value, Is.True);
// Setting the value without focusing the field simulates a binding push (Undo/Reset/external).
field.value = -1f;
yield return null;
Assert.That(toggle.value, Is.False,
$"A binding-driven disable of '{label}' should uncheck the toggle.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.None),
$"A binding-driven disable of '{label}' should hide the field.");
}
[UnityTest]
public IEnumerator UndoAfterClampingNegative_DoesNotExposeRejectedValue([ValueSource(nameof(k_FloatFieldLabels))] string label)
{
var (toggle, field, _) = RowFor(label);
toggle.value = true;
yield return null;
field.Focus();
yield return null;
field.value = 5f;
yield return null;
field.Blur();
yield return null;
Assume.That(PersistedValue(label), Is.EqualTo(5f));
field.Focus();
yield return null;
field.value = -1f;
yield return null;
field.Blur();
yield return null;
Assume.That(PersistedValue(label), Is.EqualTo(0f));
Undo.PerformUndo();
yield return null;
yield return null;
Assert.That(PersistedValue(label), Is.GreaterThanOrEqualTo(0f),
$"Undo after clamping a negative '{label}' must not expose the rejected negative value.");
}
[UnityTest]
public IEnumerator NegativeValueWhileDragging_IsClampedToZero([ValueSource(nameof(k_FloatFieldLabels))] string label)
{
var (toggle, field, labelElement) = RowFor(label);
toggle.value = true;
yield return null;
// The dragger captures the pointer on the label for the duration of a drag.
labelElement.CaptureMouse();
yield return null;
field.value = -1f;
yield return null;
labelElement.ReleaseMouse();
yield return null;
Assert.That(PersistedValue(label), Is.EqualTo(0f),
$"A negative '{label}' produced while dragging should be clamped to 0.");
Assert.That(field.value, Is.EqualTo(0f));
}
// When multiple objects with differing field values are selected, the row's enable toggle and field
// show the mixed-value indicator rather than a single object's value.
[UnityTest]
public IEnumerator MultiObject_DivergentField_ShowsMixedValue()
{
var goA = new GameObject("LayoutElementA", typeof(RectTransform));
var goB = new GameObject("LayoutElementB", typeof(RectTransform));
var layoutA = goA.AddComponent<LayoutElement>();
var layoutB = goB.AddComponent<LayoutElement>();
layoutA.minWidth = 100f; // enabled
layoutB.minWidth = -1f; // disabled sentinel -> the two selections diverge
var editor = Editor.CreateEditor(new Object[] { layoutA, layoutB });
var root = editor.CreateInspectorGUI();
m_Window.rootVisualElement.Add(root);
yield return null;
yield return null;
var labelElement = root.Query<Label>().Where(l => l.text == "Min Width").First();
var row = labelElement.parent;
var toggle = row.Q<Toggle>();
var field = row.Q<FloatField>();
Assert.That(toggle.showMixedValue, Is.True,
"Multi-selecting LayoutElements with a divergent 'Min Width' should show the mixed-value indicator on the toggle.");
Assert.That(field.showMixedValue, Is.True,
"A divergent 'Min Width' should also display the mixed-value indicator on the field.");
root.RemoveFromHierarchy();
Object.DestroyImmediate(editor);
Object.DestroyImmediate(goA);
Object.DestroyImmediate(goB);
}
// When selected objects share the enabled/disabled state but hold different numbers (both enabled,
// 100 vs 200), the enable toggle controls something they agree on, so it must NOT be mixed - only the
// field, which shows the raw number, is mixed.
[UnityTest]
public IEnumerator MultiObject_SameEnabledStateDifferentValues_MixedOnFieldOnly()
{
var goA = new GameObject("LayoutElementA", typeof(RectTransform));
var goB = new GameObject("LayoutElementB", typeof(RectTransform));
var layoutA = goA.AddComponent<LayoutElement>();
var layoutB = goB.AddComponent<LayoutElement>();
layoutA.minWidth = 100f; // enabled
layoutB.minWidth = 200f; // enabled, different value -> only the number diverges
var editor = Editor.CreateEditor(new Object[] { layoutA, layoutB });
var root = editor.CreateInspectorGUI();
m_Window.rootVisualElement.Add(root);
yield return null;
yield return null;
var labelElement = root.Query<Label>().Where(l => l.text == "Min Width").First();
var row = labelElement.parent;
var toggle = row.Q<Toggle>();
var field = row.Q<FloatField>();
Assert.That(toggle.showMixedValue, Is.False,
"Objects that are all enabled should not show the toggle as mixed, even if their values differ.");
Assert.That(toggle.value, Is.True,
"The toggle should read enabled when every selected object is enabled.");
Assert.That(field.showMixedValue, Is.True,
"Differing 'Min Width' values should still show the mixed-value indicator on the field.");
root.RemoveFromHierarchy();
Object.DestroyImmediate(editor);
Object.DestroyImmediate(goA);
Object.DestroyImmediate(goB);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4130b277c1a94612a9dc40631c012775
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,187 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
namespace UnityEditor.UI.Tests
{
// These assert the state the row is built with, which the inspector applies synchronously - hence no frames and
// no EditorWindow. LayoutElementEditorTests covers the binding-driven re-sync path.
internal class LayoutElementInspectorMixedValueTests
{
const string k_MinWidth = "Min Width";
const string k_MaxWidth = "Max Width";
readonly List<GameObject> m_GameObjects = new List<GameObject>();
Editor m_Editor;
VisualElement m_Root;
[TearDown]
public void TearDown()
{
if (m_Editor != null)
Object.DestroyImmediate(m_Editor);
m_Editor = null;
m_Root = null;
foreach (var gameObject in m_GameObjects)
{
if (gameObject != null)
Object.DestroyImmediate(gameObject);
}
m_GameObjects.Clear();
}
LayoutElement CreateLayoutElement()
{
var gameObject = new GameObject("LayoutElement", typeof(RectTransform));
m_GameObjects.Add(gameObject);
return gameObject.AddComponent<LayoutElement>();
}
void CreateInspectorFor(params LayoutElement[] elements)
{
var targets = new Object[elements.Length];
for (int i = 0; i < elements.Length; i++)
targets[i] = elements[i];
m_Editor = Editor.CreateEditor(targets);
Assume.That(m_Editor, Is.Not.Null, "Failed to create an editor for the LayoutElement selection.");
m_Root = m_Editor.CreateInspectorGUI();
Assume.That(m_Root, Is.Not.Null, "The LayoutElement inspector should be built with UI Toolkit.");
}
(Toggle toggle, FloatField field) RowFor(string label)
{
var labelElement = m_Root.Query<Label>().Where(l => l.text == label).First();
Assume.That(labelElement, Is.Not.Null, $"No '{label}' row found in the LayoutElement inspector.");
var row = labelElement.parent;
return (row.Q<Toggle>(), row.Q<FloatField>());
}
[Test]
public void SingleObject_EnabledField_ShowsFieldWithoutMixedIndicator()
{
var element = CreateLayoutElement();
element.minWidth = 100f;
CreateInspectorFor(element);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.value, Is.True, "A LayoutElement with a usable 'Min Width' should show the row as enabled.");
Assert.That(toggle.showMixedValue, Is.False, "A single selected object can never be mixed.");
Assert.That(field.showMixedValue, Is.False, "A single selected object can never be mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.Flex), "An enabled 'Min Width' should show its float field.");
}
[Test]
public void SingleObject_DisabledField_HidesFieldWithoutMixedIndicator()
{
var element = CreateLayoutElement();
element.minWidth = -1f;
CreateInspectorFor(element);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.value, Is.False, "The disabled sentinel should show the row as disabled.");
Assert.That(toggle.showMixedValue, Is.False, "A single selected object can never be mixed.");
Assert.That(field.showMixedValue, Is.False, "A single selected object can never be mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.None), "A disabled 'Min Width' should hide its float field.");
}
[Test]
public void MultiObject_DivergentEnabledState_ShowsMixedToggleAndKeepsFieldVisible()
{
var disabled = CreateLayoutElement();
var enabled = CreateLayoutElement();
disabled.minWidth = -1f;
enabled.minWidth = 100f;
CreateInspectorFor(disabled, enabled);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.showMixedValue, Is.True,
"Selecting an enabled and a disabled 'Min Width' should mark the toggle as mixed.");
Assert.That(field.showMixedValue, Is.True,
"Divergent 'Min Width' values should mark the field as mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.Flex),
"The field must stay visible when the enabled state is mixed, otherwise the mixed indicator is hidden from the user.");
}
[Test]
public void MultiObject_AllDisabled_HidesFieldWithoutMixedIndicator()
{
var first = CreateLayoutElement();
var second = CreateLayoutElement();
first.minWidth = -1f;
second.minWidth = -1f;
CreateInspectorFor(first, second);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.value, Is.False, "Objects that are all disabled should read as disabled.");
Assert.That(toggle.showMixedValue, Is.False, "Objects that agree on being disabled are not mixed.");
Assert.That(field.showMixedValue, Is.False, "Objects holding the same value are not mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.None),
"A field that is disabled across the whole selection should stay hidden.");
}
[Test]
public void MultiObject_DivergentEnabledStateOnMaxField_ShowsMixedToggle()
{
var disabled = CreateLayoutElement();
var enabled = CreateLayoutElement();
disabled.maxWidth = LayoutUtility.DefaultMaxSize;
enabled.maxWidth = 250f;
CreateInspectorFor(disabled, enabled);
var (toggle, field) = RowFor(k_MaxWidth);
Assert.That(toggle.showMixedValue, Is.True,
"An unbounded and a bounded 'Max Width' disagree about the enabled state and should mark the toggle as mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.Flex),
"The field must stay visible when the enabled state is mixed.");
}
[Test]
public void MultiObject_DivergenceInThirdObject_ShowsMixedToggle()
{
var first = CreateLayoutElement();
var second = CreateLayoutElement();
var third = CreateLayoutElement();
first.minWidth = 100f;
second.minWidth = 200f;
third.minWidth = -1f;
CreateInspectorFor(first, second, third);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.showMixedValue, Is.True,
"A divergent enabled state anywhere in the selection should mark the toggle as mixed.");
Assert.That(field.showMixedValue, Is.True,
"Divergent 'Min Width' values should mark the field as mixed.");
}
[Test]
public void MultiObject_SameEnabledStateDifferentValues_MixesFieldOnly()
{
var first = CreateLayoutElement();
var second = CreateLayoutElement();
first.minWidth = 100f;
second.minWidth = 200f;
CreateInspectorFor(first, second);
var (toggle, field) = RowFor(k_MinWidth);
Assert.That(toggle.showMixedValue, Is.False,
"Objects that are all enabled agree about the toggle, even when their values differ.");
Assert.That(toggle.value, Is.True, "The toggle should read enabled when every selected object is enabled.");
Assert.That(field.showMixedValue, Is.True, "Differing 'Min Width' values should mark the field as mixed.");
Assert.That(field.style.display.value, Is.EqualTo(DisplayStyle.Flex), "An enabled field stays visible.");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c25ee90b3284383bfbd87d30f331df5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,215 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools.Utils;
using UnityEngine.UI;
namespace UnityEditor.UI.Tests
{
// Covers the reach of the Undo registration in LayoutGroup.Reset: the whole subtree, including the group's own
// RectTransform. ScrollRectLayoutUndoTests covers the per-layout-group-type cases.
internal class LayoutGroupAddUndoTests
{
GameObject m_Canvas;
GameObject m_Root;
static readonly Vector2EqualityComparer k_Vector2Comparer = new Vector2EqualityComparer(0.01f);
[SetUp]
public void SetUp()
{
m_Canvas = new GameObject("Canvas", typeof(Canvas));
Undo.ClearAll();
}
[TearDown]
public void TearDown()
{
Undo.ClearAll();
if (m_Root != null)
Object.DestroyImmediate(m_Root);
if (m_Canvas != null)
Object.DestroyImmediate(m_Canvas);
}
struct RectState
{
public Vector2 anchorMin, anchorMax, anchoredPosition, sizeDelta;
}
static Dictionary<RectTransform, RectState> Snapshot(Transform root, bool includeRoot)
{
var map = new Dictionary<RectTransform, RectState>();
foreach (var rect in root.GetComponentsInChildren<RectTransform>(true))
{
if (!includeRoot && rect.transform == root)
continue;
map[rect] = new RectState
{
anchorMin = rect.anchorMin,
anchorMax = rect.anchorMax,
anchoredPosition = rect.anchoredPosition,
sizeDelta = rect.sizeDelta
};
}
return map;
}
static bool Matches(RectTransform rect, RectState state)
{
return k_Vector2Comparer.Equals(rect.anchorMin, state.anchorMin)
&& k_Vector2Comparer.Equals(rect.anchorMax, state.anchorMax)
&& k_Vector2Comparer.Equals(rect.anchoredPosition, state.anchoredPosition)
&& k_Vector2Comparer.Equals(rect.sizeDelta, state.sizeDelta);
}
static void AssertRestored(Dictionary<RectTransform, RectState> before, Dictionary<RectTransform, RectState> afterAdd)
{
foreach (var kvp in before)
{
var rect = kvp.Key;
Assert.That(rect != null, "A RectTransform was unexpectedly destroyed by the undo.");
var state = kvp.Value;
string name = rect.name;
string context = afterAdd.TryGetValue(rect, out var added)
? $" (post-add was anchoredPosition {added.anchoredPosition}, sizeDelta {added.sizeDelta})"
: string.Empty;
Assert.That(rect.anchorMin, Is.EqualTo(state.anchorMin).Using(k_Vector2Comparer), $"{name}.anchorMin not restored{context}");
Assert.That(rect.anchorMax, Is.EqualTo(state.anchorMax).Using(k_Vector2Comparer), $"{name}.anchorMax not restored{context}");
Assert.That(rect.anchoredPosition, Is.EqualTo(state.anchoredPosition).Using(k_Vector2Comparer), $"{name}.anchoredPosition not restored{context}");
Assert.That(rect.sizeDelta, Is.EqualTo(state.sizeDelta).Using(k_Vector2Comparer), $"{name}.sizeDelta not restored{context}");
}
}
static void AssertAnyChanged(Dictionary<RectTransform, RectState> before, string what)
{
foreach (var kvp in before)
{
if (!Matches(kvp.Key, kvp.Value))
return;
}
Assert.Fail($"Adding {what} left every tracked RectTransform unchanged, so this test would pass without exercising the undo registration.");
}
static void Rebuild(RectTransform rect)
{
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(rect);
}
static RectTransform AddChild(Transform parent, string name, Vector2 anchoredPosition, Vector2 size)
{
var child = new GameObject(name, typeof(RectTransform), typeof(LayoutElement));
child.transform.SetParent(parent, false);
var rect = (RectTransform)child.transform;
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
var element = child.GetComponent<LayoutElement>();
element.preferredWidth = size.x;
element.preferredHeight = size.y;
return rect;
}
RectTransform CreateRoot(string name, Transform parent)
{
var gameObject = new GameObject(name, typeof(RectTransform));
var rect = (RectTransform)gameObject.transform;
rect.SetParent(parent, false);
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(120f, 80f);
rect.sizeDelta = new Vector2(400f, 300f);
if (m_Root == null)
m_Root = gameObject;
return rect;
}
static Dictionary<RectTransform, RectState> AddLayoutGroupThenUndo<T>(GameObject target, RectTransform rebuildRoot,
Dictionary<RectTransform, RectState> before, Transform snapshotRoot, bool includeRoot) where T : Component
{
Undo.IncrementCurrentGroup();
int undoGroup = Undo.GetCurrentGroup();
// This is the path that makes the Editor invoke LayoutGroup.Reset.
ObjectFactory.AddComponent<T>(target);
Rebuild(rebuildRoot);
AssertAnyChanged(before, typeof(T).Name);
var afterAdd = Snapshot(snapshotRoot, includeRoot);
Undo.CollapseUndoOperations(undoGroup);
Undo.PerformUndo();
Rebuild(rebuildRoot);
return afterAdd;
}
[Test]
public void UndoAddLayoutGroup_RestoresNestedDescendantRectTransforms()
{
var outerRect = CreateRoot("Outer", m_Canvas.transform);
AddChild(outerRect, "Child0", new Vector2(-60f, 40f), new Vector2(80f, 40f));
AddChild(outerRect, "Child1", new Vector2(70f, -30f), new Vector2(90f, 50f));
var nestedRect = CreateRoot("Nested", outerRect);
nestedRect.anchoredPosition = new Vector2(-20f, -90f);
nestedRect.sizeDelta = new Vector2(180f, 70f);
nestedRect.gameObject.AddComponent<LayoutElement>();
var nestedGroup = nestedRect.gameObject.AddComponent<HorizontalLayoutGroup>();
nestedGroup.padding = new RectOffset(4, 4, 4, 4);
AddChild(nestedRect, "GrandChild0", new Vector2(-40f, 0f), new Vector2(50f, 30f));
AddChild(nestedRect, "GrandChild1", new Vector2(45f, 0f), new Vector2(60f, 30f));
Rebuild(outerRect);
var before = Snapshot(outerRect, includeRoot: false);
Assume.That(before.Count, Is.GreaterThanOrEqualTo(5),
"Expected the nested hierarchy to contribute grandchildren to the snapshot.");
var afterAdd = AddLayoutGroupThenUndo<VerticalLayoutGroup>(outerRect.gameObject, outerRect, before, outerRect, includeRoot: false);
Assert.That(outerRect.GetComponent<VerticalLayoutGroup>(), Is.Null,
"A single undo should remove the added VerticalLayoutGroup.");
AssertRestored(before, afterAdd);
}
[Test]
public void UndoAddNestedLayoutGroup_RestoresItsOwnRectTransform()
{
var outerRect = CreateRoot("Outer", m_Canvas.transform);
var outerGroup = outerRect.gameObject.AddComponent<VerticalLayoutGroup>();
outerGroup.childControlWidth = true;
outerGroup.childControlHeight = true;
outerGroup.childForceExpandWidth = false;
outerGroup.childForceExpandHeight = false;
var innerRect = CreateRoot("Inner", outerRect);
innerRect.gameObject.AddComponent<LayoutElement>();
AddChild(innerRect, "GrandChild0", new Vector2(-30f, 0f), new Vector2(50f, 30f));
AddChild(innerRect, "GrandChild1", new Vector2(35f, 0f), new Vector2(60f, 30f));
Rebuild(outerRect);
var before = Snapshot(innerRect, includeRoot: true);
Assume.That(before.ContainsKey(innerRect), "The snapshot should include the inner group's own RectTransform.");
var afterAdd = AddLayoutGroupThenUndo<HorizontalLayoutGroup>(innerRect.gameObject, outerRect, before, innerRect, includeRoot: true);
Assert.That(innerRect.GetComponent<HorizontalLayoutGroup>(), Is.Null,
"A single undo should remove the added HorizontalLayoutGroup.");
AssertRestored(before, afterAdd);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d96cc7adb6c4531998652ab1f09a983
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 24333a116497b144f9782b1160984b57
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using System;
using NUnit.Framework;
using System.Collections;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UI;
using UnityEngine.UIElements;
using UnityEngine.Search;
[Timeout(360000)]
internal class PropertyDrawerTests
{
class PropertyDrawerTestsWindow : EditorWindow
{
public Navigation navigation;
public SpriteState spriteState;
public Dropdown.OptionDataList optionDataList;
[SearchContext("t:gameObject is:gameObject", "gameObject")]
public GameObject searchContext;
SerializedObject serializedObject;
void CreateGUI()
{
serializedObject = new SerializedObject(this);
Add(nameof(navigation));
Add(nameof(spriteState));
Add(nameof(optionDataList));
Add(nameof(searchContext));
rootVisualElement.Bind(serializedObject);
// Forces visual tree update
Rebuild();
}
void Add(string propertyName)
{
rootVisualElement.Add(new PropertyField() { bindingPath = propertyName });
}
public SerializedProperty Property(string propertyName) => serializedObject.FindProperty(propertyName);
public void Rebuild() => rootVisualElement.Bind(serializedObject);
}
static PropertyDrawerTestsWindow window;
[UnitySetUp]
[MenuItem("Tests/Open Property Drawer Test Window")]
public static IEnumerator SetUp()
{
VisualTreeBindingsUpdater.disableBindingsThrottling = true;
window = EditorWindow.GetWindow<PropertyDrawerTestsWindow>();
window.Repaint();
window.Show();
yield return null;
}
[TearDown]
public void TearDown()
{
VisualTreeBindingsUpdater.disableBindingsThrottling = false;
window.Close();
}
[UnityTest]
public IEnumerator NavigationDrawer_IsVisible()
{
yield return null;
Assert.IsNotNull(window.rootVisualElement.Query<VisualElement>("Navigation").Build().First());
}
[UnityTest]
public IEnumerator SpriteStateDrawer_IsVisible()
{
yield return null;
Assert.IsNotNull(window.rootVisualElement.Q("SpriteState"));
}
[UnityTest]
[Ignore("UUM-18482")]
public IEnumerator DropdownOptionDataListDrawer_IsVisible()
{
yield return null;
Assert.IsNotNull(window.rootVisualElement.Q("DropdownOptionDataList"), $"Item is null. Root object count: {window.rootVisualElement.childCount}");
}
[UnityTest]
public IEnumerator SearchContextDrawer_IsVisible()
{
yield return null;
Assert.IsNotNull(window.rootVisualElement.Q("SearchContext"));
}
// Fake expected result in order to make TestCaseAttribute to work with UnityTest
[UnityTest]
[TestCase(new object[] { Navigation.Mode.None, 0}, ExpectedResult = null)]
[TestCase(new object[] { Navigation.Mode.Horizontal, 1 }, ExpectedResult = null)]
[TestCase(new object[] { Navigation.Mode.Vertical, 1 }, ExpectedResult = null)]
[TestCase(new object[] { Navigation.Mode.Automatic, 0 }, ExpectedResult = null)]
[TestCase(new object[] { Navigation.Mode.Explicit, 4 }, ExpectedResult = null)]
[TestCase(new object[] { (Navigation.Mode.Explicit | Navigation.Mode.Horizontal), 0 }, ExpectedResult = null)]
[TestCase(new object[] { (Navigation.Mode.Automatic | Navigation.Mode.Explicit), 0 }, ExpectedResult = null)]
public static IEnumerator NavigationDrawer_ShowsCorrectAdditionalControlCount(Enum mode, int expectedCount)
{
var field = window.rootVisualElement.Q<EnumFlagsField>("unity-input-navigation.m_Mode");
field.value = mode;
yield return null;
var indent = window.rootVisualElement.Q<VisualElement>("Navigation").Q<VisualElement>("Indent");
var visibleChildren = indent.Children().Count(child => child.resolvedStyle.display != DisplayStyle.None);
Assert.AreEqual(expectedCount, visibleChildren, $"{expectedCount} additional Navigation object properties should be " +
$"visible when 'Mode' is set to '{(Navigation.Mode)window.Property("navigation.m_Mode").enumValueFlag}'");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 433d3b96b2a40a5479d3d7d8495882ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,155 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools.Utils;
using UnityEngine.UI;
namespace UnityEditor.UI.Tests
{
// Adding a layout group to a Scroll View repositions its children (and descendants such as the scrollbar
// handles); undoing the "Add Component" must restore every descendant RectTransform to its pre-add state.
// (UUM-144838)
internal class ScrollRectLayoutUndoTests
{
GameObject m_Canvas;
GameObject m_ScrollView;
static readonly Vector2EqualityComparer k_Vector2Comparer = new Vector2EqualityComparer(0.01f);
[SetUp]
public void SetUp()
{
m_Canvas = new GameObject("Canvas", typeof(Canvas));
m_ScrollView = DefaultControls.CreateScrollView(new DefaultControls.Resources());
var scrollRT = (RectTransform)m_ScrollView.transform;
scrollRT.SetParent(m_Canvas.transform, false);
// Off-origin, where any leftover offset is most visible.
scrollRT.anchorMin = scrollRT.anchorMax = new Vector2(0.5f, 0.5f);
scrollRT.anchoredPosition = new Vector2(300, 200);
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(scrollRT);
}
[TearDown]
public void TearDown()
{
if (m_ScrollView != null)
Object.DestroyImmediate(m_ScrollView);
if (m_Canvas != null)
Object.DestroyImmediate(m_Canvas);
}
struct RectState
{
public Vector2 anchorMin, anchorMax, anchoredPosition, sizeDelta;
}
static Dictionary<RectTransform, RectState> SnapshotDescendants(Transform root)
{
var map = new Dictionary<RectTransform, RectState>();
foreach (var rt in root.GetComponentsInChildren<RectTransform>(true))
{
if (rt == root) // only descendants are recorded
continue;
map[rt] = new RectState
{
anchorMin = rt.anchorMin,
anchorMax = rt.anchorMax,
anchoredPosition = rt.anchoredPosition,
sizeDelta = rt.sizeDelta
};
}
return map;
}
static void AssertRestored(Dictionary<RectTransform, RectState> before)
{
foreach (var kvp in before)
{
var rt = kvp.Key;
Assert.That(rt != null, $"'{kvp.Key}' was unexpectedly destroyed by undo.");
var state = kvp.Value;
string name = rt.name;
Assert.That(rt.anchorMin, Is.EqualTo(state.anchorMin).Using(k_Vector2Comparer), $"{name}.anchorMin not restored");
Assert.That(rt.anchorMax, Is.EqualTo(state.anchorMax).Using(k_Vector2Comparer), $"{name}.anchorMax not restored");
Assert.That(rt.anchoredPosition, Is.EqualTo(state.anchoredPosition).Using(k_Vector2Comparer), $"{name}.anchoredPosition not restored");
Assert.That(rt.sizeDelta, Is.EqualTo(state.sizeDelta).Using(k_Vector2Comparer), $"{name}.sizeDelta not restored");
}
}
static void AssertAnyChanged(Dictionary<RectTransform, RectState> before, string layoutGroupName)
{
foreach (var kvp in before)
{
var rt = kvp.Key;
var state = kvp.Value;
if (!k_Vector2Comparer.Equals(rt.anchorMin, state.anchorMin)
|| !k_Vector2Comparer.Equals(rt.anchorMax, state.anchorMax)
|| !k_Vector2Comparer.Equals(rt.anchoredPosition, state.anchoredPosition)
|| !k_Vector2Comparer.Equals(rt.sizeDelta, state.sizeDelta))
return;
}
Assert.Fail($"Adding {layoutGroupName} left every descendant RectTransform unchanged, so this test would pass without exercising the undo.");
}
static void AddLayoutGroupThenUndo<T>(GameObject target, RectTransform scrollRT, Dictionary<RectTransform, RectState> before) where T : Component
{
Undo.IncrementCurrentGroup();
int group = Undo.GetCurrentGroup();
// ObjectFactory.AddComponent matches the Inspector's "Add Component" button.
ObjectFactory.AddComponent<T>(target);
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(scrollRT);
AssertAnyChanged(before, typeof(T).Name);
Undo.CollapseUndoOperations(group);
Undo.PerformUndo();
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(scrollRT);
}
[Test]
public void UndoAddVerticalLayoutGroup_RestoresScrollViewDescendants()
{
var scrollRT = (RectTransform)m_ScrollView.transform;
var before = SnapshotDescendants(scrollRT);
AddLayoutGroupThenUndo<VerticalLayoutGroup>(m_ScrollView, scrollRT, before);
Assert.That(m_ScrollView.GetComponent<VerticalLayoutGroup>(), Is.Null,
"A single Undo should remove the added VerticalLayoutGroup.");
AssertRestored(before);
}
[Test]
public void UndoAddHorizontalLayoutGroup_RestoresScrollViewDescendants()
{
var scrollRT = (RectTransform)m_ScrollView.transform;
var before = SnapshotDescendants(scrollRT);
AddLayoutGroupThenUndo<HorizontalLayoutGroup>(m_ScrollView, scrollRT, before);
Assert.That(m_ScrollView.GetComponent<HorizontalLayoutGroup>(), Is.Null);
AssertRestored(before);
}
[Test]
public void UndoAddGridLayoutGroup_RestoresScrollViewDescendants()
{
var scrollRT = (RectTransform)m_ScrollView.transform;
var before = SnapshotDescendants(scrollRT);
AddLayoutGroupThenUndo<GridLayoutGroup>(m_ScrollView, scrollRT, before);
Assert.That(m_ScrollView.GetComponent<GridLayoutGroup>(), Is.Null);
AssertRestored(before);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e76469fb3bb994083898a5bc426ca60d