UI相关优化补充

This commit is contained in:
2026-05-25 13:21:41 +08:00
parent 3c812cfb41
commit a1f9122153
54 changed files with 2008 additions and 112 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 71c6d91864f353542af76dc4bdd78175
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
{
"name": "BaseGames.Tests.PlayMode",
"rootNamespace": "BaseGames.Tests.PlayMode",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.UI",
"UnityEngine.TestRunner",
"Unity.TextMeshPro"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ba2164f377b449148960bb029fea13e7
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using BaseGames.Core;
using BaseGames.UI.Utility;
namespace BaseGames.Tests.PlayMode
{
/// <summary>
/// ColorblindApplier 测试:写入 Shader 全局变量。
/// </summary>
public class ColorblindApplierTests
{
private GameObject _host;
[SetUp] public void SetUp() { _host = new GameObject("ColorblindHost"); }
[TearDown] public void TearDown() { Object.DestroyImmediate(_host); }
[UnityTest]
public IEnumerator ApplyMode_WritesShaderGlobals()
{
var applier = _host.AddComponent<ColorblindApplier>();
applier.ApplyMode(ColorblindMode.Deuteranopia);
yield return null;
Assert.AreEqual((int)ColorblindMode.Deuteranopia,
Shader.GetGlobalInt(Shader.PropertyToID("_GlobalColorblindMode")));
Assert.AreEqual(1f,
Shader.GetGlobalFloat(Shader.PropertyToID("_GlobalColorblindStrength")), 0.001f);
applier.ApplyMode(ColorblindMode.None);
yield return null;
Assert.AreEqual(0,
Shader.GetGlobalInt(Shader.PropertyToID("_GlobalColorblindMode")));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 07ca55485ed3db14e822d55d1116e5d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
using NUnit.Framework;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.Tests.PlayMode
{
/// <summary>
/// RequiredFieldValidator 反射扫描测试。
/// </summary>
public class RequiredFieldValidatorTests
{
private class Sample : MonoBehaviour
{
[RequiredField] public GameObject Required;
[RequiredField("提示文本")] public string RequiredString;
public GameObject Optional;
}
[Test]
public void MissingField_LogsWarning()
{
var go = new GameObject("ReqHost");
var s = go.AddComponent<Sample>();
s.Required = null;
s.RequiredString = "";
// 期待两条警告Required + RequiredString
UnityEngine.TestTools.LogAssert.Expect(LogType.Warning,
new System.Text.RegularExpressions.Regex(@"\[RequiredField\] Sample\.Required.*"));
UnityEngine.TestTools.LogAssert.Expect(LogType.Warning,
new System.Text.RegularExpressions.Regex(@"\[RequiredField\] Sample\.RequiredString.*"));
RequiredFieldValidator.ValidateAll(s);
Object.DestroyImmediate(go);
}
[Test]
public void FilledField_NoWarning()
{
var go = new GameObject("ReqHost2");
var s = go.AddComponent<Sample>();
s.Required = go;
s.RequiredString = "ok";
RequiredFieldValidator.ValidateAll(s); // 不应有警告
Object.DestroyImmediate(go);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5afa9356c16c2954f899e0d092be37b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.TestTools;
using TMPro;
using BaseGames.UI;
namespace BaseGames.Tests.PlayMode
{
/// <summary>
/// ToastManager 队列行为测试:
/// · Enqueue 一条后 Toast 激活并最终隐藏
/// · 连续 Enqueue 3 条按序串行播放
///
/// 不依赖事件频道(直接调用 Enqueue不依赖本地化标题/正文为常量字符串)。
/// </summary>
public class ToastManagerTests
{
private GameObject _host;
private ToastManager _mgr;
private ToastNotification _toast;
[SetUp]
public void SetUp()
{
_host = new GameObject("ToastHost");
// Toast 预制CanvasGroup + 子文本
var toastGO = new GameObject("Toast", typeof(CanvasGroup));
toastGO.transform.SetParent(_host.transform);
toastGO.SetActive(false);
_toast = toastGO.AddComponent<ToastNotification>();
// 反射注入 _displayDuration / _fadeDuration 减为短值,缩短测试时长
SetPrivate(_toast, "_displayDuration", 0.05f);
SetPrivate(_toast, "_fadeDuration", 0.02f);
_mgr = _host.AddComponent<ToastManager>();
SetPrivate(_mgr, "_toast", _toast);
_host.SetActive(false);
_host.SetActive(true); // 触发 Awake/OnEnable
}
[TearDown]
public void TearDown() { Object.DestroyImmediate(_host); }
[UnityTest]
public IEnumerator Enqueue_ShowsThenHides()
{
_mgr.Enqueue("T", "B", null);
yield return null;
Assert.IsTrue(_toast.gameObject.activeSelf, "入队后 Toast 应当激活");
// 总时长 ~= 0.02+0.05+0.02 = 0.09s,再 + 队列等待 0.1s
yield return new WaitForSecondsRealtime(0.5f);
Assert.IsFalse(_toast.gameObject.activeSelf, "Toast 自动隐藏未生效");
}
[UnityTest]
public IEnumerator MultipleEnqueue_PlaysSerially()
{
_mgr.Enqueue("A", "1", null);
_mgr.Enqueue("B", "2", null);
yield return null;
Assert.IsTrue(_toast.gameObject.activeSelf, "第一条应当立即播放");
// 不严格验证内容(涉及私有字段),只验证活动状态推进。
yield return new WaitForSecondsRealtime(1.0f);
Assert.IsFalse(_toast.gameObject.activeSelf, "两条串行播放后应当全部结束");
}
// ── 反射工具 ──────────────────────────────────────────────────────
private static void SetPrivate(object target, string fieldName, object value)
{
var f = target.GetType().GetField(fieldName,
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
f?.SetValue(target, value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee01b829ae0f99445b90f0561e2ccae1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,94 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using BaseGames.UI;
namespace BaseGames.Tests.PlayMode
{
/// <summary>
/// UIManager 烟雾测试:验证面板栈基本不变式。
///
/// 覆盖:
/// · OpenPanel → CloseTopPanel 后栈为空;
/// · 嵌套打开A → B → CloseTop后 A 仍处于激活态;
/// · 重复 OpenPanel 同一面板不会双压栈HashSet 去重)。
///
/// 注测试只覆盖框架行为不验证业务面板CharmPanel 等)的内部逻辑。
/// </summary>
public class UIManagerSmokeTest
{
private GameObject _hostGO;
private UIManager _manager;
private GameObject _panelA;
private GameObject _panelB;
[SetUp]
public void SetUp()
{
_hostGO = new GameObject("UIManager_Host");
_manager = _hostGO.AddComponent<UIManager>();
_panelA = new GameObject("PanelA");
_panelB = new GameObject("PanelB");
_panelA.SetActive(false);
_panelB.SetActive(false);
// 强制 OnEnable 触发AddComponent 同帧已触发,这里二次启用保证 ServiceLocator 状态)
_hostGO.SetActive(false);
_hostGO.SetActive(true);
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(_panelA);
Object.DestroyImmediate(_panelB);
Object.DestroyImmediate(_hostGO);
}
[UnityTest]
public IEnumerator OpenThenCloseTop_PanelDeactivated()
{
_manager.OpenPanel(_panelA);
yield return null;
Assert.IsTrue(_panelA.activeSelf, "OpenPanel 应当激活面板");
_manager.CloseTopPanel();
yield return null;
Assert.IsFalse(_panelA.activeSelf, "CloseTopPanel 应当反激活栈顶面板");
}
[UnityTest]
public IEnumerator NestedOpen_PreviousPanelHiddenAndRestored()
{
_manager.OpenPanel(_panelA);
_manager.OpenPanel(_panelB);
yield return null;
Assert.IsFalse(_panelA.activeSelf, "嵌套打开时上一层应被隐藏");
Assert.IsTrue(_panelB.activeSelf, "新打开的面板应当激活");
_manager.CloseTopPanel();
yield return null;
Assert.IsTrue(_panelA.activeSelf, "关闭栈顶后上一层应当恢复");
Assert.IsFalse(_panelB.activeSelf, "关闭后栈顶面板应当反激活");
}
[UnityTest]
public IEnumerator DoubleOpenSamePanel_NoStackDuplication()
{
_manager.OpenPanel(_panelA);
_manager.OpenPanel(_panelA);
yield return null;
// 关闭一次后栈应为空
_manager.CloseTopPanel();
yield return null;
Assert.IsFalse(_panelA.activeSelf, "重复打开应被去重;关闭一次后即应隐藏");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 953f5cb03f10b1d4db3d8da7121ba910
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.TestTools;
using BaseGames.UI;
namespace BaseGames.Tests.PlayMode
{
/// <summary>
/// UITween 静态库测试验证补间在终态吸附、零时长立即返回、null 安全。
/// </summary>
public class UITweenTests
{
private GameObject _go;
[SetUp] public void SetUp() { _go = new GameObject("TweenHost", typeof(RectTransform), typeof(CanvasGroup)); }
[TearDown] public void TearDown() { Object.DestroyImmediate(_go); }
[UnityTest]
public IEnumerator MoveAnchored_ReachesTarget()
{
var rect = _go.GetComponent<RectTransform>();
rect.anchoredPosition = Vector2.zero;
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(rect, new Vector2(100, 50), 0.05f));
Assert.AreEqual(new Vector2(100, 50), rect.anchoredPosition);
}
[UnityTest]
public IEnumerator FadeCanvasGroup_ReachesTarget()
{
var cg = _go.GetComponent<CanvasGroup>();
cg.alpha = 0f;
yield return _go.AddComponent<TestRunner>().Run(UITween.FadeCanvasGroup(cg, 1f, 0.05f));
Assert.AreEqual(1f, cg.alpha, 0.001f);
}
[UnityTest]
public IEnumerator ZeroDuration_SnapsImmediately()
{
var rect = _go.GetComponent<RectTransform>();
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(rect, new Vector2(7, 7), 0f));
Assert.AreEqual(new Vector2(7, 7), rect.anchoredPosition);
}
[UnityTest]
public IEnumerator NullTarget_NoException()
{
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(null, Vector2.one, 0.05f));
yield return _go.AddComponent<TestRunner>().Run(UITween.FadeCanvasGroup(null, 1f, 0.05f));
Assert.Pass();
}
/// <summary>挂宿主跑协程的辅助 MonoBehaviour测试场景无 EventSystem。</summary>
private class TestRunner : MonoBehaviour
{
public IEnumerator Run(IEnumerator inner) { yield return StartCoroutine(inner); }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65492216fce745f41b6a7352763938e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: