// Copyright (c) 2025 EFramework Innovation. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. using System; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; using EFramework.Unity.MVVM; using EFramework.Unity.Utility; using NUnit.Framework; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; /// /// TestXView 是 XView 的单元测试。 /// public class TestXView { #region 视图测试准备 private class MyHandler : XView.IHandler { public XView.IBase lastFocusedView; public List viewOrder = new(); public XView.IBase lastSetOrderView; public int lastSetOrderValue; public object[] bindingView = new object[3]; public void Load(XView.IMeta meta, Transform parent, out XView.IBase view, out GameObject panel) { panel = new GameObject(meta.Path); if (parent) panel.transform.SetParent(parent, false); var myView = panel.AddComponent(); myView.Meta = meta; myView.Panel = panel; view = myView; viewOrder.Add(view); } public void LoadAsync(XView.IMeta meta, Transform parent, Action callback) { var panel = new GameObject(meta.Path); if (parent) panel.transform.SetParent(parent, false); var mockView = panel.AddComponent(); mockView.Meta = meta; mockView.Panel = panel; viewOrder.Add(mockView); callback?.Invoke(mockView, panel); } public bool IsLoading(XView.IMeta meta) { return false; } public void SetBinding(GameObject go, object target, XView.Element[] elements) { bindingView[0] = go; bindingView[1] = target; bindingView[2] = elements; } public void SetOrder(XView.IBase view, int order) { if (!viewOrder.Contains(view)) viewOrder.Add(view); // 记录最后一次设置的顺序 lastSetOrderView = view; lastSetOrderValue = order; } public void SetFocus(XView.IBase view, bool focus) { if (focus) lastFocusedView = view; } } private class MyModule : XModule.Base { } private enum MyEvent { Event1, Event2, Event3, Event4, } private class MyView : XView.Base { public Action OnOpenCallback; public override void OnOpen(params object[] args) { base.OnOpen(args); OnOpenCallback?.Invoke(); } public Action OnFocusCallback; public override void OnFocus() { base.OnFocus(); OnFocusCallback?.Invoke(); } public Action OnBlurCallback; public override void OnBlur() { base.OnBlur(); OnBlurCallback?.Invoke(); } public Action OnCloseCallback; public override void OnClose(Action done) { OnCloseCallback?.Invoke(); done?.Invoke(); base.OnClose(done); } public bool OnEvent2Called = false; public int OnEvent2Param; [XModule.Event(MyEvent.Event2, typeof(MyModule), true)] public void OnEvent2(int param) { OnEvent2Called = true; OnEvent2Param = param; } } [XView.Element("MyModularView Class Attr")] [XView.Element("MyModularView Class Attr Extras", "Hello MyModularView")] private class MyModularView : XView.Base { public bool OnEvent3Called = false; public int OnEvent3Param; [XModule.Event(MyEvent.Event3)] [XView.Element("MyModularView Method Attr")] public void OnEvent3(int param) { OnEvent3Called = true; OnEvent3Param = param; } } [XView.Element("MySubView Class Attr")] [XView.Element("MySubView Class Attr Extras", "Hello MySubView")] private class MySubView : MyModularView { [XView.Element("MySubView Field Attr")] public bool OnEvent4Called = false; [XView.Element("MySubView Field Attr Extras", "Hello OnEvent4Param1")] public int OnEvent4Param1; public bool OnEvent4Param2; [XModule.Event(MyEvent.Event4)] [XView.Element("MySubView Method Attr Extras", "Hello OnEvent4")] public void OnEvent4(int param1, bool param2) { OnEvent4Called = true; OnEvent4Param1 = param1; OnEvent4Param2 = param2; } } private XView.Meta testMeta; private GameObject testPanel; private MyHandler myHandler; [SetUp] public void Setup() { testMeta = new XView.Meta("TestView"); testPanel = new GameObject("TestPanel"); myHandler = new MyHandler(); XView.Initialize(myHandler); } [TearDown] public void Reset() { if (testPanel != null) UnityEngine.Object.Destroy(testPanel); XView.DestroyAll(); } #endregion #region 基础视图测试 [Test] public void Meta() { // 验证Meta基本属性 var meta = new XView.Meta("TestView", 10, XView.EventType.Dynamic, XView.CacheType.None, true); Assert.That(meta.Path, Is.EqualTo("TestView"), "Meta的Path属性应当正确设置为指定值"); Assert.That(meta.FixedRQ, Is.EqualTo(10), "Meta的FixedRQ属性应当正确设置为指定值"); Assert.That(meta.Focus, Is.EqualTo(XView.EventType.Dynamic), "Meta的Focus属性应当正确设置为指定值"); Assert.That(meta.Cache, Is.EqualTo(XView.CacheType.None), "Meta的Cache属性应当正确设置为指定值"); Assert.That(meta.Multiple, Is.True, "Meta的Multiple属性应当正确设置为指定值"); } [Test] public void Event() { var contexts = new Dictionary(); // 基础视图 var myView = new GameObject("MyView").AddComponent(); contexts[myView.Event.context] = myView; // 有模块视图 var myModularView = new GameObject("MyModularView").AddComponent(); contexts[myModularView.Module.Event] = myModularView; // 继承视图 var mySubView = new GameObject("MySubView").AddComponent(); contexts[mySubView.Module.Event] = mySubView; #region 特性绑定模块 { myView.OnEvent2Called = false; myView.OnEvent2Param = 0; MyModule.Instance.Event.Notify(MyEvent.Event2, 1002); Assert.That(myView.OnEvent2Called, Is.True, "特性绑定模块注册事件后触发通知应当调用回调函数。"); Assert.That(myView.OnEvent2Param, Is.EqualTo(1002), "特性绑定模块注册事件后触发通知调用回调函数的透传参数1应当相等。"); myView.OnEvent2Called = false; MyModule.Instance.Event.Notify(MyEvent.Event2); Assert.That(myView.OnEvent2Called, Is.False, "特性绑定模块再次触发回调一次的事件应当不调用回调函数。"); } #endregion #region 特性默认模块 { myModularView.OnEvent3Called = false; myModularView.OnEvent3Param = 0; MyModule.Instance.Event.Notify(MyEvent.Event3, 1003); Assert.That(myModularView.OnEvent3Called, Is.True, "特性默认模块注册事件后触发通知应当调用回调函数。"); Assert.That(myModularView.OnEvent3Param, Is.EqualTo(1003), "特性默认模块注册事件后触发通知调用回调函数的透传参数1应当相等。"); myModularView.OnEvent3Called = false; MyModule.Instance.Event.Notify(MyEvent.Event3, 1003); Assert.That(myModularView.OnEvent3Called, Is.True, "特性默认模块注册事件后再次触发通知应当调用回调函数。"); } #endregion #region 特性继承模块 { mySubView.OnEvent3Called = false; mySubView.OnEvent3Param = 0; mySubView.OnEvent4Called = false; mySubView.OnEvent4Param1 = 0; mySubView.OnEvent4Param2 = false; MyModule.Instance.Event.Notify(MyEvent.Event3, 1003); MyModule.Instance.Event.Notify(MyEvent.Event4, 1004, true); Assert.That(mySubView.OnEvent3Called, Is.True, "特性继承模块注册事件后触发通知应当调用父类回调函数。"); Assert.That(mySubView.OnEvent3Param, Is.EqualTo(1003), "特性继承模块注册事件后触发通知调用父类回调函数的透传参数1应当相等。"); Assert.That(mySubView.OnEvent4Called, Is.True, "特性继承模块注册事件后触发通知应当调用子类回调函数。"); Assert.That(mySubView.OnEvent4Param1, Is.EqualTo(1004), "特性继承模块注册事件后触发通知调用子类回调函数的透传参数1应当相等。"); Assert.That(mySubView.OnEvent4Param2, Is.True, "特性继承模块注册事件后触发通知调用子类回调函数的透传参数2应当相等。"); mySubView.OnEvent3Called = false; mySubView.OnEvent4Called = false; MyModule.Instance.Event.Notify(MyEvent.Event3, 1003); MyModule.Instance.Event.Notify(MyEvent.Event4, 1004, false); Assert.That(mySubView.OnEvent3Called, Is.True, "特性继承模块注册事件后再次触发通知应当调用父类回调函数。"); Assert.That(mySubView.OnEvent4Called, Is.True, "特性继承模块注册事件后再次触发通知应当调用子类回调函数。"); } #endregion foreach (var kvp in contexts) { var context = kvp.Key; var view = kvp.Value; #region 非泛型 { var called = false; object[] param1 = null; void callback(params object[] args) { called = true; param1 = args; } Assert.That(view.Event.Register(MyEvent.Event1, callback, true), Is.True, "非泛型注册事件应当成功。"); context.Notify(MyEvent.Event1, 1001); Assert.That(called, Is.True, "非泛型注册事件后触发通知应当调用回调函数。"); Assert.That(param1[0], Is.EqualTo(1001), "非泛型注册事件后触发通知调用回调函数的透传参数1应当相等。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "非泛型再次触发回调一次的事件应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "非泛型注册事件应当成功。"); Assert.That(view.Event.Unregister(MyEvent.Event1, callback), Is.True, "非泛型注销事件应当成功。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "非泛型注销事件后触发通知应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "非泛型注册事件应当成功。"); view.Event.UnregisterAll(); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "非泛型清除事件后触发通知应当不调用回调函数。"); } #endregion #region 泛型 { var called = false; var param1 = 0; void callback(int p1) { called = true; param1 = p1; } Assert.That(view.Event.Register(MyEvent.Event1, callback, true), Is.True, "泛型 注册事件应当成功。"); context.Notify(MyEvent.Event1, 1001); Assert.That(called, Is.True, "泛型 注册事件后触发通知应当调用回调函数。"); Assert.That(param1, Is.EqualTo(1001), "泛型 注册事件后触发通知调用回调函数的透传参数1应当相等。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 再次触发回调一次的事件应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); Assert.That(view.Event.Unregister(MyEvent.Event1, callback), Is.True, "泛型 注销事件应当成功。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 注销事件后触发通知应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); view.Event.UnregisterAll(); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 清除事件后触发通知应当不调用回调函数。"); } #endregion #region 泛型 { var called = false; var param1 = 0; var param2 = 0; void callback(int p1, int p2) { called = true; param1 = p1; param2 = p2; } Assert.That(view.Event.Register(MyEvent.Event1, callback, true), Is.True, "泛型 注册事件应当成功。"); context.Notify(MyEvent.Event1, 1001, 1002); Assert.That(called, Is.True, "泛型 注册事件后触发通知应当调用回调函数。"); Assert.That(param1, Is.EqualTo(1001), "泛型 注册事件后触发通知调用回调函数的透传参数1应当相等。"); Assert.That(param2, Is.EqualTo(1002), "泛型 注册事件后触发通知调用回调函数的透传参数2应当相等。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 再次触发回调一次的事件应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); Assert.That(view.Event.Unregister(MyEvent.Event1, callback), Is.True, "泛型 注销事件应当成功。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 注销事件后触发通知应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); view.Event.UnregisterAll(); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 清除事件后触发通知应当不调用回调函数。"); } #endregion #region 泛型 { var called = false; var param1 = 0; var param2 = 0; var param3 = 0; void callback(int p1, int p2, int p3) { called = true; param1 = p1; param2 = p2; param3 = p3; } Assert.That(view.Event.Register(MyEvent.Event1, callback, true), Is.True, "泛型 注册事件应当成功。"); context.Notify(MyEvent.Event1, 1001, 1002, 1003); Assert.That(called, Is.True, "泛型 注册事件后触发通知应当调用回调函数。"); Assert.That(param1, Is.EqualTo(1001), "泛型 注册事件后触发通知调用回调函数的透传参数1应当相等。"); Assert.That(param2, Is.EqualTo(1002), "泛型 注册事件后触发通知调用回调函数的透传参数2应当相等。"); Assert.That(param3, Is.EqualTo(1003), "泛型 注册事件后触发通知调用回调函数的透传参数3应当相等。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 再次触发回调一次的事件应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); Assert.That(view.Event.Unregister(MyEvent.Event1, callback), Is.True, "泛型 注销事件应当成功。"); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 注销事件后触发通知应当不调用回调函数。"); Assert.That(view.Event.Register(MyEvent.Event1, callback, false), Is.True, "泛型 注册事件应当成功。"); view.Event.UnregisterAll(); called = false; context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "泛型 清除事件后触发通知应当不调用回调函数。"); } #endregion #region 删除对象 { var called = false; var calledT1 = false; var calledT2 = false; var calledT3 = false; view.Event.Register(MyEvent.Event1, (_) => called = true, false); view.Event.Register(MyEvent.Event1, (_) => calledT1 = true, false); view.Event.Register(MyEvent.Event1, (_, _) => calledT2 = true, false); view.Event.Register(MyEvent.Event1, (_, _, _) => calledT3 = true, false); UnityEngine.Object.DestroyImmediate(view); context.Notify(MyEvent.Event1); Assert.That(called, Is.False, "非泛型删除对象后触发通知应当不调用回调函数。"); Assert.That(calledT1, Is.False, "泛型 删除对象后触发通知应当不调用回调函数。"); Assert.That(calledT2, Is.False, "泛型 删除对象后触发通知应当不调用回调函数。"); Assert.That(calledT3, Is.False, "泛型 删除对象后触发通知应当不调用回调函数。"); } #endregion } } [Test] public void Element() { #region 未标记元素特性 { Assert.That(XView.Element.Get(null), Is.Null, "对空类型获取元素特性应当返回空。"); Assert.That(XView.Element.Get(typeof(MyView)).Length, Is.EqualTo(0), "对空类型获取元素特性应当返回空。"); } #endregion #region 父类标记元素特性 { var type = typeof(MyModularView); var elements = XView.Element.Get(type); Assert.That(elements, Is.Not.Null, "对标记的父类型获取元素特性不应当为空。"); Assert.That(elements.Length, Is.EqualTo(3), "对标记的父类型获取元素特性数量应当为 2。"); Assert.That(elements[0].Name, Is.EqualTo("MyModularView Class Attr"), "父类标记在类上的元素特性的名称应当和设置的相等。"); Assert.That(elements[0].Reflect, Is.EqualTo(type), "父类标记在类上的元素特性的反射信息应当和所属类相等。"); Assert.That(elements[1].Name, Is.EqualTo("MyModularView Class Attr Extras"), "父类标记在类上的元素特性的名称应当和设置的相等。"); Assert.That(elements[1].Reflect, Is.EqualTo(type), "父类标记在类上的元素特性的反射信息应当和所属类相等。"); Assert.That(elements[1].Extras[0], Is.EqualTo("Hello MyModularView"), "父类标记在类上的元素特性的参数应当和设置的相等。"); Assert.That(elements[2].Name, Is.EqualTo("MyModularView Method Attr"), "父类标记在方法上的元素特性的名称应当和设置的相等。"); Assert.That(elements[2].Reflect, Is.EqualTo(type.GetMember("OnEvent3")[0]), "父类标记在方法上的元素特性的反射信息应当和所属方法相等。"); } #endregion #region 子类标记元素特性 { var type = typeof(MySubView); var elements = XView.Element.Get(type); Assert.That(elements, Is.Not.Null, "对标记的子类型获取元素特性不应当为空。"); Assert.That(elements.Length, Is.EqualTo(8), "对标记的子类型获取元素特性数量应当为 5(父类 3 + 子类 5)。"); Assert.That(elements[0].Name, Is.EqualTo("MySubView Class Attr"), "子类标记在类上的元素特性的名称应当和设置的相等。"); Assert.That(elements[0].Reflect, Is.EqualTo(type), "子类标记在类上的元素特性的反射信息应当和所属类相等。"); Assert.That(elements[1].Name, Is.EqualTo("MySubView Class Attr Extras"), "子类标记在类上的元素特性的名称应当和设置的相等。"); Assert.That(elements[1].Reflect, Is.EqualTo(type), "子类标记在类上的元素特性的反射信息应当和所属类相等。"); Assert.That(elements[1].Extras[0], Is.EqualTo("Hello MySubView"), "子类标记在类上的元素特性的参数应当和设置的相等。"); Assert.That(elements[4].Name, Is.EqualTo("MySubView Method Attr Extras"), "子类标记在方法上的元素特性的名称应当和设置的相等。"); Assert.That(elements[4].Reflect, Is.EqualTo(type.GetMember("OnEvent4")[0]), "子类标记在方法上的元素特性的反射信息应当和所属方法相等。"); Assert.That(elements[4].Extras[0], Is.EqualTo("Hello OnEvent4"), "子类标记在方法上的元素特性的参数应当和设置的相等。"); Assert.That(elements[6].Name, Is.EqualTo("MySubView Field Attr"), "子类标记在字段上的元素特性的名称应当和设置的相等。"); Assert.That(elements[6].Reflect, Is.EqualTo(type.GetMember("OnEvent4Called")[0]), "子类标记在字段上的元素特性的反射信息应当和所属字段相等。"); Assert.That(elements[7].Name, Is.EqualTo("MySubView Field Attr Extras"), "子类标记在字段上的元素特性的名称应当和设置的相等。"); Assert.That(elements[7].Reflect, Is.EqualTo(type.GetMember("OnEvent4Param1")[0]), "子类标记在字段上的元素特性的反射信息应当和所属字段相等。"); Assert.That(elements[7].Extras[0], Is.EqualTo("Hello OnEvent4Param1"), "子类标记在字段上的元素特性的参数应当和设置的相等。"); } #endregion } [Test] public void Base() { // 测试Base类的基本方法 var myView = testPanel.AddComponent(); myView.Meta = testMeta; myView.Panel = testPanel; Assert.That(myView.Event, Is.Not.Null, "视图的Event属性不应为空"); Assert.That(myView.Tags, Is.Not.Null, "视图的Tags属性不应为空"); // 测试生命周期方法 var openCalled = false; var focusCalled = false; var blurCalled = false; var closeCalled = false; myView.OnOpenCallback = () => openCalled = true; myView.OnFocusCallback = () => focusCalled = true; myView.OnBlurCallback = () => blurCalled = true; myView.OnCloseCallback = () => closeCalled = true; myView.OnOpen(); Assert.That(openCalled, Is.True, "OnOpen方法应当调用OnOpenCallback"); myView.OnFocus(); Assert.That(focusCalled, Is.True, "OnFocus方法应当调用OnFocusCallback"); myView.OnBlur(); Assert.That(blurCalled, Is.True, "OnBlur方法应当调用OnBlurCallback"); var closeDoneCalled = false; myView.OnClose(() => closeDoneCalled = true); Assert.That(closeCalled, Is.True, "OnClose方法应当调用OnCloseCallback"); Assert.That(closeDoneCalled, Is.True, "OnClose方法应当执行传入的done回调"); } #endregion #region 视图管理测试 [UnityTest] public IEnumerator Init() { // 创建三种不同缓存类型的视图 var sceneCachedMeta = new XView.Meta("SceneCachedView", 0, XView.EventType.Dynamic, XView.CacheType.Scene); var sharedCachedMeta = new XView.Meta("SharedCachedView", 0, XView.EventType.Dynamic, XView.CacheType.Shared); var nonCachedMeta = new XView.Meta("NonCachedView", 0, XView.EventType.Dynamic, XView.CacheType.None); // 创建视图并添加到缓存列表 var sceneCachedView = XView.Open(sceneCachedMeta); var sharedCachedView = XView.Open(sharedCachedMeta); var nonCachedView = XView.Open(nonCachedMeta); // 确保视图的GameObject存在 Assert.That(sceneCachedView.Panel, Is.Not.Null, "SceneCached视图的GameObject应当存在"); Assert.That(sharedCachedView.Panel, Is.Not.Null, "SharedCached视图的GameObject应当存在"); Assert.That(nonCachedView.Panel, Is.Not.Null, "NonCached视图的GameObject应当存在"); // 添加到缓存视图列表 XView.cachedView.Add(sceneCachedView); XView.cachedView.Add(sharedCachedView); XView.cachedView.Add(nonCachedView); // 创建一个测试场景并卸载,触发sceneUnloaded事件 var scene = SceneManager.CreateScene("MSVTestScene"); SceneManager.SetActiveScene(scene); yield return SceneManager.UnloadSceneAsync(scene); // 验证Scene类型的缓存视图被移除,而Shared类型的视图保留 Assert.That(XView.cachedView.Count, Is.EqualTo(2), "缓存视图数量应当为2"); Assert.That(XView.cachedView.Contains(sceneCachedView), Is.False, "SceneCached视图应当被移除"); Assert.That(XView.cachedView.Contains(sharedCachedView), Is.True, "SharedCached视图应当保留"); Assert.That(XView.cachedView.Contains(nonCachedView), Is.True, "NonCached视图只在关闭时销毁"); Assert.That(sceneCachedView.Panel == null && !sceneCachedView.Panel, Is.True, "SceneCached视图的GameObject应当被销毁"); Assert.That(sharedCachedView.Panel != null && sharedCachedView.Panel, Is.True, "SharedCached视图的GameObject应当仍然存在"); Assert.That(nonCachedView.Panel != null && nonCachedView.Panel, Is.True, "NonCached视图只在关闭时销毁"); } [Test] public void Load() { var parentTransform = new GameObject("Parent").transform; // 1. 测试加载普通视图 var normalMeta = new XView.Meta("NormalView", 0, XView.EventType.Dynamic, XView.CacheType.None, false); var normalView = XView.Load(normalMeta, parentTransform, false); Assert.That(normalView, Is.Not.Null, "加载的视图不应为空"); Assert.That(normalView.Meta, Is.EqualTo(normalMeta), "加载的视图Meta属性应当与传入的Meta一致"); Assert.That(normalView.Panel, Is.Not.Null, "加载的视图Panel不应为空"); Assert.That(normalView.Panel.activeSelf, Is.True, "加载的视图Panel应当处于激活状态"); // 2. 测试加载多实例视图 var multipleMeta = new XView.Meta("MultipleView", 0, XView.EventType.Dynamic, XView.CacheType.None, true); var multipleView1 = XView.Load(multipleMeta, parentTransform, false); var multipleView2 = XView.Load(multipleMeta, parentTransform, false); Assert.That(multipleView1, Is.Not.Null, "第一个多实例视图不应为空"); Assert.That(multipleView2, Is.Not.Null, "第二个多实例视图不应为空"); Assert.That(multipleView1, Is.Not.SameAs(multipleView2), "多实例视图应当创建不同的实例"); Assert.That(multipleView1.Panel.activeSelf, Is.True, "多实例视图应当创建不同的实例"); // 3. 测试加载已存在的非多实例视图 (closeIfOpened = false) // 先打开视图使其进入openedView列表 var openedView = XView.Open(normalMeta); Assert.That(openedView, Is.Not.Null, "打开的视图不应为空"); Assert.That(openedView.Panel.activeSelf, Is.True, "打开的视图Panel应当处于激活状态"); // 尝试再次加载 var existingView = XView.Load(normalMeta, parentTransform, false); Assert.That(existingView, Is.Not.Null, "加载已存在视图不应为空"); Assert.That(existingView, Is.SameAs(openedView), "closeIfOpened为false时应当返回已存在的视图实例"); // 4. 测试加载已存在的非多实例视图 (closeIfOpened = true) XView.Close(normalMeta); // 先关闭之前的视图 var openedViewAgain = XView.Open(normalMeta); Assert.That(openedViewAgain, Is.Not.Null, "重新打开的视图不应为空"); // 尝试再次加载 var newView = XView.Load(normalMeta, parentTransform, true); Assert.That(newView, Is.Not.Null, "新加载的视图不应为空"); Assert.That(openedViewAgain, Is.Not.SameAs(newView), "closeIfOpened为true时应当创建新的视图实例"); Assert.That(openedViewAgain.Panel == null, Is.True, "原视图实例应当被关闭并销毁"); // 5. 测试从缓存加载视图 var cachedMeta = new XView.Meta("CachedView", 0, XView.EventType.Dynamic, XView.CacheType.Scene, false); var cachedView = XView.Open(cachedMeta); Assert.That(cachedView, Is.Not.Null, "缓存类型视图应当成功创建"); // 关闭视图使其进入缓存 XView.Close(cachedMeta); // 再次加载,应该从缓存获取 var reloadedView = XView.Load(cachedMeta, parentTransform, false); Assert.That(reloadedView, Is.Not.Null, "从缓存加载的视图不应为空"); Assert.That(reloadedView, Is.SameAs(cachedView), "应当从缓存中获取到相同的视图实例"); if (parentTransform != null) UnityEngine.Object.Destroy(parentTransform.gameObject); } [Test] public void Binding() { myHandler.bindingView = new object[3]; var myView = testPanel.AddComponent(); Assert.That(myHandler.bindingView[0], Is.SameAs(testPanel), "绑定回调的 go 对象应当和挂载的相等。"); Assert.That(myHandler.bindingView[1], Is.SameAs(myView), "绑定回调的 target 对象应当和挂载的相等。"); Assert.That(myHandler.bindingView[2], Is.SameAs(XView.Element.Get(typeof(MyView))), "绑定回调的 elements 列表应当和 XView.Element 获取的相等。"); } [UnityTest] public IEnumerator Open() { // 测试不同缓存类型 var meta1 = new XView.Meta("View1", 0, XView.EventType.Dynamic, XView.CacheType.None); var meta2 = new XView.Meta("View2", 0, XView.EventType.Dynamic, XView.CacheType.Scene); var meta3 = new XView.Meta("View3", 0, XView.EventType.Dynamic, XView.CacheType.Shared); // 测试Open方法 var view1 = XView.Open(meta1); var view2 = XView.Open(meta2); var view3 = XView.Open(meta3); Assert.That(view1, Is.Not.Null, "视图1应当成功创建且不为空"); Assert.That(view2, Is.Not.Null, "视图2应当成功创建且不为空"); Assert.That(view3, Is.Not.Null, "视图3应当成功创建且不为空"); Assert.That(view1.Panel.activeSelf, Is.True, "视图1的面板应当处于激活状态"); Assert.That(view2.Panel.activeSelf, Is.True, "视图2的面板应当处于激活状态"); Assert.That(view3.Panel.activeSelf, Is.True, "视图3的面板应当处于激活状态"); // 测试Close方法 XView.Close(view1); XView.Close(view2); XView.Close(view3); Assert.That(view1.Panel == null, Is.True, "CacheType.None类型的视图关闭后Panel应当被销毁"); Assert.That(view2.Panel.activeSelf, Is.False, "CacheType.Scene类型的视图关闭后Panel应当设为非活动状态"); Assert.That(view3.Panel.activeSelf, Is.False, "CacheType.Shared类型的视图关闭后Panel应当设为非活动状态"); // 测试异步操作 var asyncMeta = new XView.Meta("AsyncView"); var callbackCalled = false; yield return XView.OpenAsync(asyncMeta, (view) => { callbackCalled = true; Assert.That(view, Is.Not.Null, "异步加载的视图不应为空"); Assert.That(view.Meta.Path, Is.EqualTo(asyncMeta.Path), "异步加载的视图Meta路径应当与传入的Meta一致"); }); Assert.That(callbackCalled, Is.True, "异步加载完成后应当调用回调函数"); } [Test] public void Close() { // 测试CloseAll方法 var meta1 = new XView.Meta("View1"); var meta2 = new XView.Meta("View2"); var meta3 = new XView.Meta("View3"); var view1 = XView.Open(meta1); var view2 = XView.Open(meta2); var view3 = XView.Open(meta3); Assert.That(view1, Is.Not.Null, "视图1应当不为空"); Assert.That(view2, Is.Not.Null, "视图2应当不为空"); Assert.That(view3, Is.Not.Null, "视图3应当不为空"); XView.Close(meta1); Assert.That(view1.Panel.activeSelf, Is.False, "视图1应当被关闭且处于非激活状态"); XView.Open(meta1); XView.Close(view1); Assert.That(view1.Panel.activeSelf, Is.False, "视图1应当被关闭且处于非激活状态"); XView.Open(meta1); XView.CloseAll(meta1); // 关闭除meta1外的所有界面 Assert.That(view1.Panel.activeSelf, Is.True, "排除的视图1应当保持激活状态"); Assert.That(view2.Panel.activeSelf, Is.False, "视图2应当被关闭且处于非激活状态"); Assert.That(view3.Panel.activeSelf, Is.False, "视图3应当被关闭且处于非激活状态"); XView.DestroyAll(meta1); Assert.That(view1.Panel, Is.Not.Null, "排除的视图1应当不为空"); Assert.That(view2.Panel == null, Is.True, "销毁的视图2应当为空"); Assert.That(view3.Panel == null, Is.True, "销毁的视图3应当为空"); XView.DestroyAll(); Assert.That(view1.Panel == null, Is.True, "销毁的视图1应当为空"); } [Test] public void Sort() { // 创建测试视图 var meta1 = new XView.Meta("View1", 0, XView.EventType.Dynamic); var meta2 = new XView.Meta("View2", 0, XView.EventType.Dynamic); var meta3 = new XView.Meta("View3", 0, XView.EventType.Dynamic); var meta4 = new XView.Meta("View4", 0, XView.EventType.Dynamic); var view1 = XView.Open(meta1); var view2 = XView.Open(meta2); var view3 = XView.Open(meta3); XView.openedView.Clear(); // 测试view被添加到below视图之前 XView.Sort(view2, view1, null); XView.Sort(view1, null, null); Assert.That(XView.openedView.IndexOf(view1), Is.EqualTo(1), "view1应当位于索引1的位置"); Assert.That(XView.openedView.IndexOf(view2), Is.EqualTo(0), "view2应当位于索引0的位置"); // 测试view被添加到above视图之后 XView.openedView.Clear(); XView.Sort(view1, null, null); XView.Sort(view2, null, view1); Assert.That(XView.openedView.IndexOf(view1), Is.EqualTo(0), "view1应当位于索引0的位置"); Assert.That(XView.openedView.IndexOf(view2), Is.EqualTo(1), "view2应当位于索引1的位置"); // 测试below和above都为null时,view被添加到末尾 XView.openedView.Clear(); XView.Sort(view1, null, null); XView.Sort(view2, null, null); Assert.That(XView.openedView.IndexOf(view1), Is.EqualTo(0), "view1应当位于索引0的位置"); Assert.That(XView.openedView.IndexOf(view2), Is.EqualTo(1), "view2应当位于索引1的位置"); // 渲染顺序测试 // 测试FixedRQ的渲染顺序 XView.openedView.Clear(); myHandler.lastSetOrderView = null; myHandler.lastSetOrderValue = 0; XView.Sort(view1, null, null); Assert.That(myHandler.lastSetOrderView, Is.SameAs(view1), "SetOrder方法应当使用正确的视图参数"); Assert.That(myHandler.lastSetOrderValue, Is.EqualTo(500), "普通视图的渲染顺序应当按公式计算"); // 焦点状态测试 // 测试EventType.Slience类型视图 var silenceMeta = new XView.Meta("SilenceView", 0, XView.EventType.Slience); var silenceView = XView.Open(silenceMeta); var blurCalled = false; (silenceView as MyView).OnBlurCallback = () => blurCalled = true; // 确保视图先获得焦点 XView.focusedView[silenceView] = true; myHandler.lastFocusedView = null; // Sort应该使Slience类型视图失去焦点 XView.Sort(silenceView, null, null); Assert.That(myHandler.lastFocusedView, Is.Null, "Slience类型视图不应调用SetFocus方法"); Assert.That(blurCalled, Is.True, "Slience类型视图应当调用OnBlur方法"); Assert.That(XView.focusedView[silenceView], Is.False, "Slience类型视图在focusedView中的标记应为false"); // 测试EventType.Static和Dynamic类型视图 XView.openedView.Clear(); XView.focusedView.Clear(); var staticMeta = new XView.Meta("StaticView", 0, XView.EventType.Static); var dynamicMeta = new XView.Meta("DynamicView", 0, XView.EventType.Dynamic); var staticView = XView.Open(staticMeta); var dynamicView = XView.Open(dynamicMeta); var staticFocusCalled = false; var dynamicFocusCalled = false; (staticView as MyView).OnFocusCallback = () => staticFocusCalled = true; (dynamicView as MyView).OnFocusCallback = () => dynamicFocusCalled = true; // 先清空焦点状态 XView.openedView.Clear(); XView.focusedView.Clear(); // Sort后,Static类型视图应该获得焦点 XView.Sort(staticView, null, null); Assert.That(staticFocusCalled, Is.True, "Static类型视图应当调用OnFocus方法"); Assert.That(XView.focusedView[staticView], Is.True, "Static类型视图在focusedView中的标记应为true"); // 添加Dynamic类型视图后,由于lastFocused已为true,Dynamic视图不应获得焦点 XView.Sort(dynamicView, staticView, null); Assert.That(dynamicFocusCalled, Is.False, "当lastFocused为true时,Dynamic类型视图不应调用OnFocus方法"); Assert.That(XView.focusedView.ContainsKey(dynamicView) && XView.focusedView[dynamicView], Is.False, "Dynamic类型视图在focusedView中的标记应为false"); // 测试Panel为null的情况 var testView = XView.Open(new XView.Meta("TestView")); UnityEngine.Object.DestroyImmediate(testView.Panel); int initialCount = XView.openedView.Count; LogAssert.Expect(LogType.Error, new Regex("XView.Sort: view .* has already been destroyed.")); XView.Sort(null, null, null); // 调用Sort应该清理无效视图 Assert.That(XView.openedView.Count, Is.EqualTo(initialCount - 1), "Panel为null的视图应当从openedView列表中移除"); } [Test] public void Focus() { var view = XView.Open(testMeta); myHandler.lastFocusedView = null; XView.Focus(view); Assert.That(myHandler.lastFocusedView, Is.SameAs(view), "Focus方法应当正确设置视图的焦点状态"); } [Test] public void Find() { var parentTransform = new GameObject("Parent").transform; // 通过Load加载,不会自动加入openedView列表 XView.Load(testMeta, parentTransform, false); var foundView = XView.Find(testMeta); Assert.That(foundView, Is.Null); // 通过Open加载,会自动加入openedView列表 var openedView = XView.Open(testMeta); foundView = XView.Find(testMeta); Assert.That(foundView, Is.Not.Null); Assert.That(foundView, Is.SameAs(openedView)); if (parentTransform != null) UnityEngine.Object.Destroy(parentTransform.gameObject); } #endregion }