307 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|