Files
FlowScope/My project/Assets/FlowScope/Tests/PlayMode/UI/UIManagerTests.cs
2026-05-19 10:41:31 +08:00

307 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Resources;
using FlowScope.UI;
using NUnit.Framework;
using UnityEngine;
namespace FlowScope.Tests.PlayMode.UI
{
public sealed class UIManagerTests
{
private readonly List<GameObject> _objects = new();
[TearDown]
public void TearDown()
{
foreach (var obj in _objects)
{
if (obj != null)
{
UnityEngine.Object.DestroyImmediate(obj);
}
}
_objects.Clear();
}
[Test]
public void OpenAsync_UsesAttributePathAndMountsPanelToRegisteredLayer()
{
var canvas = CreateCanvas("popup");
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
var resources = new FakeResourceService();
resources.Register("Custom/Explicit", prefab);
var manager = new UIManager(resources);
manager.RegisterLayer("popup", canvas, 120);
var viewModel = new TestViewModel("first");
var panel = manager.OpenAsync<ExplicitPathPanel, TestViewModel>(viewModel, CancellationToken.None).GetAwaiter().GetResult();
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" }));
Assert.That(panel.transform.parent, Is.EqualTo(canvas.transform));
Assert.That(canvas.sortingOrder, Is.EqualTo(120));
Assert.That(panel.BoundModels, Is.EqualTo(new[] { viewModel }));
Assert.That(panel.gameObject.activeSelf, Is.True);
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
}
[Test]
public void OpenAsync_WhenPathIsMissing_UsesPanelNameConvention()
{
var canvas = CreateCanvas("hud");
var prefab = CreatePrefab<MainHudPanel>("MainHudPrefab");
var resources = new FakeResourceService();
resources.Register("UI/MainHud/Prefab", prefab);
var manager = new UIManager(resources);
manager.RegisterLayer("hud", canvas, 10);
manager.OpenAsync<MainHudPanel, TestViewModel>(new TestViewModel("hud"), CancellationToken.None).GetAwaiter().GetResult();
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "UI/MainHud/Prefab" }));
}
[Test]
public void Close_WhenPanelIsNotLayerTop_ThrowsInvalidOperationException()
{
var canvas = CreateCanvas("popup");
var resources = new FakeResourceService();
resources.Register("Custom/Explicit", CreatePrefab<ExplicitPathPanel>("FirstPrefab"));
resources.Register("UI/Second/Prefab", CreatePrefab<SecondPanel>("SecondPrefab"));
var manager = new UIManager(resources);
manager.RegisterLayer("popup", canvas, 0);
manager.OpenAsync<ExplicitPathPanel, TestViewModel>(new TestViewModel("first"), CancellationToken.None).GetAwaiter().GetResult();
manager.OpenAsync<SecondPanel, TestViewModel>(new TestViewModel("second"), CancellationToken.None).GetAwaiter().GetResult();
Assert.Throws<InvalidOperationException>(() => manager.Close<ExplicitPathPanel>());
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
Assert.That(manager.IsOpen<SecondPanel>(), Is.True);
}
[Test]
public void Close_WithDestroyStrategy_UnbindsDestroysInstanceAndDisposesHandle()
{
var canvas = CreateCanvas("popup");
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
var resources = new FakeResourceService();
resources.Register("Custom/Explicit", prefab);
var manager = new UIManager(resources);
manager.RegisterLayer("popup", canvas, 0, PanelStrategy.Destroy);
var panel = manager.OpenAsync<ExplicitPathPanel, TestViewModel>(new TestViewModel("first"), CancellationToken.None).GetAwaiter().GetResult();
var handle = resources.Handles[0];
manager.Close<ExplicitPathPanel>();
Assert.That(panel.UnbindCount, Is.EqualTo(1));
Assert.That(handle.IsDisposed, Is.True);
Assert.That(panel == null, Is.True);
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.False);
}
[Test]
public void OpenAsync_WithCacheStrategy_ReusesHiddenInstanceAndBindsAgain()
{
var canvas = CreateCanvas("dialog");
var prefab = CreatePrefab<CachedPanel>("CachedPrefab");
var resources = new FakeResourceService();
resources.Register("UI/Cached/Prefab", prefab);
var manager = new UIManager(resources);
manager.RegisterLayer("dialog", canvas, 0, PanelStrategy.Destroy);
var firstViewModel = new TestViewModel("first");
var secondViewModel = new TestViewModel("second");
var first = manager.OpenAsync<CachedPanel, TestViewModel>(firstViewModel, CancellationToken.None).GetAwaiter().GetResult();
manager.Close<CachedPanel>();
var second = manager.OpenAsync<CachedPanel, TestViewModel>(secondViewModel, CancellationToken.None).GetAwaiter().GetResult();
Assert.That(second, Is.SameAs(first));
Assert.That(second.gameObject.activeSelf, Is.True);
Assert.That(second.BoundModels, Is.EqualTo(new[] { firstViewModel, secondViewModel }));
Assert.That(second.UnbindCount, Is.EqualTo(1));
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "UI/Cached/Prefab" }));
Assert.That(resources.Handles[0].IsDisposed, Is.False);
}
[Test]
public void CloseLayer_OnlyClosesRequestedLayer()
{
var popupCanvas = CreateCanvas("popup");
var hudCanvas = CreateCanvas("hud");
var resources = new FakeResourceService();
resources.Register("Custom/Explicit", CreatePrefab<ExplicitPathPanel>("PopupPrefab"));
resources.Register("UI/MainHud/Prefab", CreatePrefab<MainHudPanel>("HudPrefab"));
var manager = new UIManager(resources);
manager.RegisterLayer("popup", popupCanvas, 0);
manager.RegisterLayer("hud", hudCanvas, 0);
manager.OpenAsync<ExplicitPathPanel, TestViewModel>(new TestViewModel("popup"), CancellationToken.None).GetAwaiter().GetResult();
manager.OpenAsync<MainHudPanel, TestViewModel>(new TestViewModel("hud"), CancellationToken.None).GetAwaiter().GetResult();
manager.CloseLayer("popup");
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.False);
Assert.That(manager.IsOpen<MainHudPanel>(), Is.True);
}
[Test]
public void OpenAsync_WhenBindThrows_DisposesHandleAndDestroysCreatedInstance()
{
var canvas = CreateCanvas("popup");
var prefab = CreatePrefab<ThrowingBindPanel>("ThrowingPrefab");
var resources = new FakeResourceService();
resources.Register("UI/ThrowingBind/Prefab", prefab);
var manager = new UIManager(resources);
manager.RegisterLayer("popup", canvas, 0);
ThrowsAsync<InvalidOperationException>(
() => manager.OpenAsync<ThrowingBindPanel, TestViewModel>(new TestViewModel("boom"), CancellationToken.None));
Assert.That(resources.Handles[0].IsDisposed, Is.True);
Assert.That(canvas.transform.childCount, Is.EqualTo(0));
Assert.That(manager.IsOpen<ThrowingBindPanel>(), Is.False);
}
private Canvas CreateCanvas(string name)
{
var gameObject = new GameObject(name);
_objects.Add(gameObject);
return gameObject.AddComponent<Canvas>();
}
private static TException ThrowsAsync<TException>(Func<Task> action)
where TException : Exception
{
try
{
action().GetAwaiter().GetResult();
}
catch (TException exception)
{
return exception;
}
Assert.Fail($"Expected {typeof(TException).Name}.");
return null;
}
private GameObject CreatePrefab<TPanel>(string name)
where TPanel : Component
{
var gameObject = new GameObject(name);
_objects.Add(gameObject);
gameObject.AddComponent<TPanel>();
return gameObject;
}
private sealed class FakeResourceService : IResourceService
{
private readonly Dictionary<string, GameObject> _assets = new();
public List<string> LoadedKeys { get; } = new();
public List<FakeResourceHandle<GameObject>> Handles { get; } = new();
public void Register(string key, GameObject asset)
{
_assets.Add(key, asset);
}
public Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
{
cancellationToken.ThrowIfCancellationRequested();
LoadedKeys.Add(key);
if (!_assets.TryGetValue(key, out var asset))
{
throw new InvalidOperationException($"Missing resource: {key}");
}
var handle = new FakeResourceHandle<GameObject>(key, asset);
Handles.Add(handle);
return Task.FromResult<IResourceHandle<T>>((IResourceHandle<T>)(object)handle);
}
public IResourceGroup CreateGroup()
{
throw new NotSupportedException();
}
}
private sealed class FakeResourceHandle<T> : IResourceHandle<T>
where T : class
{
public FakeResourceHandle(string key, T asset)
{
Key = key;
Asset = asset;
}
public string Key { get; }
public T Asset { get; private set; }
public bool IsDisposed { get; private set; }
public void Dispose()
{
IsDisposed = true;
}
}
}
public sealed class TestViewModel
{
public TestViewModel(string name)
{
Name = name;
}
public string Name { get; }
}
[UIPanel("popup", "Custom/Explicit")]
public sealed class ExplicitPathPanel : RecordingPanel
{
}
[UIPanel("hud")]
public sealed class MainHudPanel : RecordingPanel
{
}
[UIPanel("popup")]
public sealed class SecondPanel : RecordingPanel
{
}
[UIPanel("dialog", overrideStrategy: PanelStrategy.Cache)]
public sealed class CachedPanel : RecordingPanel
{
}
[UIPanel("popup")]
public sealed class ThrowingBindPanel : UIPanelBase<TestViewModel>
{
protected override void OnBind(TestViewModel viewModel)
{
throw new InvalidOperationException("bind failed");
}
}
public abstract class RecordingPanel : UIPanelBase<TestViewModel>
{
public List<TestViewModel> BoundModels { get; } = new();
public int UnbindCount { get; private set; }
public override void Unbind()
{
UnbindCount++;
base.Unbind();
}
protected override void OnBind(TestViewModel viewModel)
{
BoundModels.Add(viewModel);
}
}
}