namespace WallstopStudios.DataVisualizer.Tests.Editor
{
using System.Collections;
using System.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UIElements;
using WallstopStudios.DataVisualizer.Editor.Styles;
public sealed class TypePopoverLayoutTests
{
private const string DataVisualizerStyleSheetPath =
"Packages/com.wallstop-studios.data-visualizer/Editor/DataVisualizer/Styles/DataVisualizerStyles.uss";
private const string PlaceholderClass = "unity-text-field__placeholder";
private const float LayoutTolerance = 0.01f;
private static readonly string[] SearchFieldClassNames =
{
"type-add-search-field",
"type-search-field",
};
[UnityTest]
public IEnumerator Should_KeepSearchFieldHeight_When_PlaceholderChangesToTypedText()
{
StyleSheet styleSheet = LoadStyleSheet();
Assert.NotNull(styleSheet);
LayoutTestWindow window = CreateWindow(styleSheet, 360, 160);
try
{
TextField[] fields = SearchFieldClassNames
.Select(className =>
{
TextField field = new() { name = className };
field.AddToClassList(className);
window.rootVisualElement.Add(field);
return field;
})
.ToArray();
yield return WaitForResolvedHeights(fields);
foreach (TextField field in fields)
{
field.SetValueWithoutNotify("Search...");
field.AddToClassList(PlaceholderClass);
}
yield return WaitForResolvedHeights(fields);
foreach (TextField field in fields)
{
float placeholderHeight = field.resolvedStyle.height;
field.RemoveFromClassList(PlaceholderClass);
field.SetValueWithoutNotify("base");
yield return WaitForResolvedHeights(field);
Assert.That(
field.resolvedStyle.height,
Is.EqualTo(placeholderHeight).Within(LayoutTolerance),
$"{field.name} height changed when placeholder text was replaced."
);
}
}
finally
{
window.Close();
}
}
[UnityTest]
public IEnumerator Should_KeepTypeRowHeight_When_FilterChangesVisibleRows()
{
StyleSheet styleSheet = LoadStyleSheet();
Assert.NotNull(styleSheet);
LayoutTestWindow window = CreateWindow(styleSheet, 360, 180);
try
{
VisualElement container = new()
{
name = "type-add-list-content",
style = { height = 120 },
};
container.AddToClassList("type-add-list-container");
window.rootVisualElement.Add(container);
VisualElement header = new();
header.AddToClassList("popover-namespace-header");
header.style.width = 180;
Label indicator = new(StyleConstants.ArrowExpanded);
indicator.AddToClassList("popover-namespace-indicator");
Label namespaceLabel = new(
"WallstopStudios.DataVisualizer.Tests.Editor.Generated.Namespace"
);
namespaceLabel.AddToClassList("type-selection-list-namespace");
namespaceLabel.AddToClassList("type-selection-list-namespace--not-empty");
header.Add(indicator);
header.Add(namespaceLabel);
Label firstRow = CreateTypeRow("BaseData");
Label secondRow = CreateTypeRow("AdvancedData");
container.Add(header);
container.Add(firstRow);
container.Add(secondRow);
yield return WaitForResolvedHeights(header, namespaceLabel, firstRow, secondRow);
float unfilteredHeaderHeight = header.resolvedStyle.height;
float unfilteredNamespaceHeight = namespaceLabel.resolvedStyle.height;
float unfilteredHeight = firstRow.resolvedStyle.height;
Assert.That(
namespaceLabel.resolvedStyle.width,
Is.LessThanOrEqualTo(header.resolvedStyle.width)
);
container.Remove(secondRow);
namespaceLabel.enableRichText = true;
namespaceLabel.text =
"WallstopStudios.DataVisualizer.Tests.Editor.Generated.Namespace";
firstRow.enableRichText = true;
firstRow.text = "BaseData";
yield return WaitForResolvedHeights(header, namespaceLabel, firstRow);
Assert.That(
header.resolvedStyle.height,
Is.EqualTo(unfilteredHeaderHeight).Within(LayoutTolerance)
);
Assert.That(
namespaceLabel.resolvedStyle.height,
Is.EqualTo(unfilteredNamespaceHeight).Within(LayoutTolerance)
);
Assert.That(
firstRow.resolvedStyle.height,
Is.EqualTo(unfilteredHeight).Within(LayoutTolerance)
);
}
finally
{
window.Close();
}
}
private static Label CreateTypeRow(string text)
{
Label row = new(text);
row.AddToClassList("type-selection-list-item");
return row;
}
private static LayoutTestWindow CreateWindow(
StyleSheet styleSheet,
float width,
float height
)
{
LayoutTestWindow window = EditorWindow.CreateWindow();
window.titleContent = new GUIContent(nameof(TypePopoverLayoutTests));
window.position = new Rect(100, 100, width, height);
window.rootVisualElement.styleSheets.Add(styleSheet);
window.rootVisualElement.style.width = width;
window.rootVisualElement.style.height = height;
window.ShowUtility();
return window;
}
private static StyleSheet LoadStyleSheet()
{
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath(
DataVisualizerStyleSheetPath
);
if (styleSheet != null)
{
return styleSheet;
}
return AssetDatabase
.FindAssets("DataVisualizerStyles t:StyleSheet")
.Select(AssetDatabase.GUIDToAssetPath)
.Where(path =>
path.EndsWith(
"/Editor/DataVisualizer/Styles/DataVisualizerStyles.uss",
System.StringComparison.Ordinal
)
)
.Select(AssetDatabase.LoadAssetAtPath)
.FirstOrDefault(sheet => sheet != null);
}
private static IEnumerator WaitForResolvedHeights(params VisualElement[] elements)
{
float[] previousHeights = null;
for (int frame = 0; frame < 30; frame++)
{
yield return null;
bool allReady = elements.All(element =>
element?.panel != null && IsPositiveFinite(element.resolvedStyle.height)
);
if (!allReady)
{
previousHeights = null;
continue;
}
float[] currentHeights = elements
.Select(element => element.resolvedStyle.height)
.ToArray();
if (
previousHeights != null
&& currentHeights
.Zip(
previousHeights,
(current, previous) => Mathf.Abs(current - previous) <= LayoutTolerance
)
.All(stable => stable)
)
{
yield break;
}
previousHeights = currentHeights;
}
string heightSummary = string.Join(
", ",
elements.Select(element =>
element == null
? ""
: $"{element.name}:{element.resolvedStyle.height} panel={element.panel != null}"
)
);
Assert.Fail($"Timed out waiting for stable positive layout heights: {heightSummary}");
}
private static bool IsPositiveFinite(float value)
{
return value > 0 && !float.IsNaN(value) && !float.IsInfinity(value);
}
private sealed class LayoutTestWindow : EditorWindow { }
}
}