扩展 P1 UI 导航和预加载
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using FlowScope.Resources;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace FlowScope.UI
|
namespace FlowScope.UI
|
||||||
{
|
{
|
||||||
@@ -7,6 +9,7 @@ namespace FlowScope.UI
|
|||||||
{
|
{
|
||||||
Task PreloadAsync<TPanel>(CancellationToken cancellationToken);
|
Task PreloadAsync<TPanel>(CancellationToken cancellationToken);
|
||||||
bool IsPreloaded<TPanel>();
|
bool IsPreloaded<TPanel>();
|
||||||
|
bool TryGetPreloadedHandle<TPanel>(out IResourceHandle<GameObject> handle);
|
||||||
void Release<TPanel>();
|
void Release<TPanel>();
|
||||||
void ReleaseAll();
|
void ReleaseAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ namespace FlowScope.UI
|
|||||||
public sealed class UIManager
|
public sealed class UIManager
|
||||||
{
|
{
|
||||||
private readonly IResourceService _resources;
|
private readonly IResourceService _resources;
|
||||||
|
private readonly IUIPreloadService _preloadService;
|
||||||
private readonly Dictionary<string, UILayer> _layers = new();
|
private readonly Dictionary<string, UILayer> _layers = new();
|
||||||
|
|
||||||
public UIManager(IResourceService resources)
|
public UIManager(IResourceService resources, IUIPreloadService preloadService = null)
|
||||||
{
|
{
|
||||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||||
|
_preloadService = preloadService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterLayer(
|
public void RegisterLayer(
|
||||||
@@ -58,7 +60,7 @@ namespace FlowScope.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
var path = ResolvePath(typeof(TPanel), attribute);
|
var path = ResolvePath(typeof(TPanel), attribute);
|
||||||
var handle = await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
var handle = await LoadPanelHandleAsync<TPanel>(path, cancellationToken);
|
||||||
if (handle == null || handle.Asset == null)
|
if (handle == null || handle.Asset == null)
|
||||||
{
|
{
|
||||||
handle?.Dispose();
|
handle?.Dispose();
|
||||||
@@ -66,6 +68,7 @@ namespace FlowScope.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
GameObject instance = null;
|
GameObject instance = null;
|
||||||
|
var ownsHandle = !IsPreloadedHandle<TPanel>(handle);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false);
|
instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false);
|
||||||
@@ -79,7 +82,8 @@ namespace FlowScope.UI
|
|||||||
panel.Bind(viewModel);
|
panel.Bind(viewModel);
|
||||||
instance.SetActive(true);
|
instance.SetActive(true);
|
||||||
|
|
||||||
var record = new UIPanelRecord(typeof(TPanel), layer.Name, panel, handle, strategy);
|
var recordHandle = ownsHandle ? (IDisposable)handle : RetainedPanelHandle.Instance;
|
||||||
|
var record = new UIPanelRecord(typeof(TPanel), layer.Name, panel, recordHandle, strategy);
|
||||||
if (strategy == PanelStrategy.Cache)
|
if (strategy == PanelStrategy.Cache)
|
||||||
{
|
{
|
||||||
layer.Cache(record);
|
layer.Cache(record);
|
||||||
@@ -95,7 +99,11 @@ namespace FlowScope.UI
|
|||||||
UnityEngine.Object.Destroy(instance);
|
UnityEngine.Object.Destroy(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ownsHandle)
|
||||||
|
{
|
||||||
handle.Dispose();
|
handle.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +196,27 @@ namespace FlowScope.UI
|
|||||||
record.Handle.Dispose();
|
record.Handle.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UIPanelAttribute GetPanelAttribute(Type panelType)
|
private async Task<IResourceHandle<GameObject>> LoadPanelHandleAsync<TPanel>(
|
||||||
|
string path,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_preloadService != null &&
|
||||||
|
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle))
|
||||||
|
{
|
||||||
|
return preloadedHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPreloadedHandle<TPanel>(IResourceHandle<GameObject> handle)
|
||||||
|
{
|
||||||
|
return _preloadService != null &&
|
||||||
|
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle) &&
|
||||||
|
ReferenceEquals(preloadedHandle, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static UIPanelAttribute GetPanelAttribute(Type panelType)
|
||||||
{
|
{
|
||||||
var attribute = (UIPanelAttribute)Attribute.GetCustomAttribute(
|
var attribute = (UIPanelAttribute)Attribute.GetCustomAttribute(
|
||||||
panelType,
|
panelType,
|
||||||
@@ -209,7 +237,7 @@ namespace FlowScope.UI
|
|||||||
return attribute;
|
return attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolvePath(Type panelType, UIPanelAttribute attribute)
|
internal static string ResolvePath(Type panelType, UIPanelAttribute attribute)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(attribute.Path))
|
if (!string.IsNullOrWhiteSpace(attribute.Path))
|
||||||
{
|
{
|
||||||
@@ -225,5 +253,14 @@ namespace FlowScope.UI
|
|||||||
|
|
||||||
return $"UI/{name}/Prefab";
|
return $"UI/{name}/Prefab";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RetainedPanelHandle : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly RetainedPanelHandle Instance = new();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
89
My project/Assets/FlowScope/Runtime/UI/UIPreloadService.cs
Normal file
89
My project/Assets/FlowScope/Runtime/UI/UIPreloadService.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FlowScope.Resources;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace FlowScope.UI
|
||||||
|
{
|
||||||
|
public sealed class UIPreloadService : IUIPreloadService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IResourceService _resources;
|
||||||
|
private readonly Dictionary<Type, IResourceHandle<GameObject>> _handles = new();
|
||||||
|
|
||||||
|
public UIPreloadService(IResourceService resources)
|
||||||
|
{
|
||||||
|
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PreloadAsync<TPanel>(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var panelType = typeof(TPanel);
|
||||||
|
if (IsPreloaded<TPanel>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = UIManager.GetPanelAttribute(panelType);
|
||||||
|
var path = UIManager.ResolvePath(panelType, attribute);
|
||||||
|
var handle = await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
||||||
|
if (handle == null || handle.Asset == null)
|
||||||
|
{
|
||||||
|
handle?.Dispose();
|
||||||
|
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_handles[panelType] = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPreloaded<TPanel>()
|
||||||
|
{
|
||||||
|
return _handles.TryGetValue(typeof(TPanel), out var handle) &&
|
||||||
|
handle != null &&
|
||||||
|
!handle.IsDisposed &&
|
||||||
|
handle.Asset != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetPreloadedHandle<TPanel>(out IResourceHandle<GameObject> handle)
|
||||||
|
{
|
||||||
|
if (IsPreloaded<TPanel>())
|
||||||
|
{
|
||||||
|
handle = _handles[typeof(TPanel)];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release<TPanel>()
|
||||||
|
{
|
||||||
|
var panelType = typeof(TPanel);
|
||||||
|
if (!_handles.TryGetValue(panelType, out var handle))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handles.Remove(panelType);
|
||||||
|
handle.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseAll()
|
||||||
|
{
|
||||||
|
foreach (var handle in _handles.Values)
|
||||||
|
{
|
||||||
|
handle.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handles.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ReleaseAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6b2cbe6065884f82838b7c882ad5085a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
50
My project/Assets/FlowScope/Runtime/UI/UIScreenNavigator.cs
Normal file
50
My project/Assets/FlowScope/Runtime/UI/UIScreenNavigator.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FlowScope.UI
|
||||||
|
{
|
||||||
|
public sealed class UIScreenNavigator : IUIScreenNavigator
|
||||||
|
{
|
||||||
|
private readonly UIManager _uiManager;
|
||||||
|
private readonly Stack<Action> _closeStack = new();
|
||||||
|
|
||||||
|
public UIScreenNavigator(UIManager uiManager)
|
||||||
|
{
|
||||||
|
_uiManager = uiManager ?? throw new ArgumentNullException(nameof(uiManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanGoBack => _closeStack.Count > 1;
|
||||||
|
|
||||||
|
public async Task<TPanel> PushAsync<TPanel, TViewModel>(
|
||||||
|
TViewModel viewModel,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
where TPanel : UIPanelBase<TViewModel>
|
||||||
|
{
|
||||||
|
var panel = await _uiManager.OpenAsync<TPanel, TViewModel>(viewModel, cancellationToken);
|
||||||
|
_closeStack.Push(() => _uiManager.Close<TPanel>());
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GoBack()
|
||||||
|
{
|
||||||
|
if (!CanGoBack)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var close = _closeStack.Pop();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
while (_closeStack.Count > 0)
|
||||||
|
{
|
||||||
|
var close = _closeStack.Pop();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f991669426b4444098f1973daeb8fb58
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using FlowScope.UI;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
|
||||||
|
namespace FlowScope.Tests.PlayMode.UI
|
||||||
|
{
|
||||||
|
public sealed class UIPreloadServiceTests
|
||||||
|
{
|
||||||
|
private readonly List<GameObject> _objects = new();
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
foreach (var obj in _objects)
|
||||||
|
{
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_objects.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator PreloadAsync_LoadsPanelResourceAndKeepsHandle()
|
||||||
|
{
|
||||||
|
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||||
|
var resources = new UITestResources();
|
||||||
|
resources.Register("Custom/Explicit", prefab);
|
||||||
|
var preload = new UIPreloadService(resources);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" }));
|
||||||
|
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||||
|
Assert.That(preload.TryGetPreloadedHandle<ExplicitPathPanel>(out var handle), Is.True);
|
||||||
|
Assert.That(handle.Asset, Is.SameAs(prefab));
|
||||||
|
Assert.That(resources.Handles[0].IsDisposed, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator PreloadAsync_WhenRepeated_DoesNotLoadAgain()
|
||||||
|
{
|
||||||
|
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||||
|
var resources = new UITestResources();
|
||||||
|
resources.Register("Custom/Explicit", prefab);
|
||||||
|
var preload = new UIPreloadService(resources);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||||
|
yield return UITestAsync.Await(preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" }));
|
||||||
|
Assert.That(resources.Handles, Has.Count.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator OpenAsync_WhenPanelIsPreloaded_ReusesPreloadedHandle()
|
||||||
|
{
|
||||||
|
var canvas = CreateCanvas("popup");
|
||||||
|
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||||
|
var resources = new UITestResources();
|
||||||
|
resources.Register("Custom/Explicit", prefab);
|
||||||
|
var preload = new UIPreloadService(resources);
|
||||||
|
var manager = new UIManager(resources, preload);
|
||||||
|
manager.RegisterLayer("popup", canvas, 0);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
manager.OpenAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("preloaded"),
|
||||||
|
CancellationToken.None));
|
||||||
|
manager.Close<ExplicitPathPanel>();
|
||||||
|
|
||||||
|
Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" }));
|
||||||
|
Assert.That(resources.Handles[0].IsDisposed, Is.False);
|
||||||
|
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator Release_DisposesPreloadedHandle()
|
||||||
|
{
|
||||||
|
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||||
|
var resources = new UITestResources();
|
||||||
|
resources.Register("Custom/Explicit", prefab);
|
||||||
|
var preload = new UIPreloadService(resources);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||||
|
preload.Release<ExplicitPathPanel>();
|
||||||
|
|
||||||
|
Assert.That(resources.Handles[0].IsDisposed, Is.True);
|
||||||
|
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Canvas CreateCanvas(string name)
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject(name);
|
||||||
|
_objects.Add(gameObject);
|
||||||
|
return gameObject.AddComponent<Canvas>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject CreatePrefab<TPanel>(string name)
|
||||||
|
where TPanel : Component
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject(name);
|
||||||
|
_objects.Add(gameObject);
|
||||||
|
gameObject.AddComponent<TPanel>();
|
||||||
|
return gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 737c460841f54ab5a073695d039b9524
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using FlowScope.UI;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
|
||||||
|
namespace FlowScope.Tests.PlayMode.UI
|
||||||
|
{
|
||||||
|
public sealed class UIScreenNavigatorTests
|
||||||
|
{
|
||||||
|
private readonly List<GameObject> _objects = new();
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
foreach (var obj in _objects)
|
||||||
|
{
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_objects.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator PushAsync_OpensPanelAndRecordsHistory()
|
||||||
|
{
|
||||||
|
var manager = CreateManager(out _);
|
||||||
|
var navigator = new UIScreenNavigator(manager);
|
||||||
|
|
||||||
|
ExplicitPathPanel panel = null;
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("first"),
|
||||||
|
CancellationToken.None),
|
||||||
|
result => panel = result);
|
||||||
|
|
||||||
|
Assert.That(panel, Is.Not.Null);
|
||||||
|
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
|
||||||
|
Assert.That(navigator.CanGoBack, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator GoBack_ClosesTopPanel()
|
||||||
|
{
|
||||||
|
var manager = CreateManager(out _);
|
||||||
|
var navigator = new UIScreenNavigator(manager);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("first"),
|
||||||
|
CancellationToken.None));
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<SecondPanel, TestViewModel>(
|
||||||
|
new TestViewModel("second"),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.That(navigator.CanGoBack, Is.True);
|
||||||
|
navigator.GoBack();
|
||||||
|
|
||||||
|
Assert.That(manager.IsOpen<SecondPanel>(), Is.False);
|
||||||
|
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
|
||||||
|
Assert.That(navigator.CanGoBack, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator Clear_ClosesAllNavigatorPanels()
|
||||||
|
{
|
||||||
|
var manager = CreateManager(out _);
|
||||||
|
var navigator = new UIScreenNavigator(manager);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("first"),
|
||||||
|
CancellationToken.None));
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<SecondPanel, TestViewModel>(
|
||||||
|
new TestViewModel("second"),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
navigator.Clear();
|
||||||
|
|
||||||
|
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.False);
|
||||||
|
Assert.That(manager.IsOpen<SecondPanel>(), Is.False);
|
||||||
|
Assert.That(navigator.CanGoBack, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator GoBack_WhenOnlyOnePanelOpen_DoesNothing()
|
||||||
|
{
|
||||||
|
var manager = CreateManager(out _);
|
||||||
|
var navigator = new UIScreenNavigator(manager);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("first"),
|
||||||
|
CancellationToken.None));
|
||||||
|
navigator.GoBack();
|
||||||
|
|
||||||
|
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
|
||||||
|
Assert.That(navigator.CanGoBack, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator Clear_DoesNotClosePanelsOpenedOutsideNavigator()
|
||||||
|
{
|
||||||
|
var manager = CreateManager(out _);
|
||||||
|
var navigator = new UIScreenNavigator(manager);
|
||||||
|
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
manager.OpenAsync<ExplicitPathPanel, TestViewModel>(
|
||||||
|
new TestViewModel("outside"),
|
||||||
|
CancellationToken.None));
|
||||||
|
yield return UITestAsync.Await(
|
||||||
|
navigator.PushAsync<SecondPanel, TestViewModel>(
|
||||||
|
new TestViewModel("inside"),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
navigator.Clear();
|
||||||
|
|
||||||
|
Assert.That(manager.IsOpen<SecondPanel>(), Is.False);
|
||||||
|
Assert.That(manager.IsOpen<ExplicitPathPanel>(), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIManager CreateManager(out UITestResources resources)
|
||||||
|
{
|
||||||
|
var canvas = CreateCanvas("popup");
|
||||||
|
resources = new UITestResources();
|
||||||
|
resources.Register("Custom/Explicit", CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab"));
|
||||||
|
resources.Register("UI/Second/Prefab", CreatePrefab<SecondPanel>("SecondPrefab"));
|
||||||
|
var manager = new UIManager(resources);
|
||||||
|
manager.RegisterLayer("popup", canvas, 0);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Canvas CreateCanvas(string name)
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject(name);
|
||||||
|
_objects.Add(gameObject);
|
||||||
|
return gameObject.AddComponent<Canvas>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject CreatePrefab<TPanel>(string name)
|
||||||
|
where TPanel : Component
|
||||||
|
{
|
||||||
|
var gameObject = new GameObject(name);
|
||||||
|
_objects.Add(gameObject);
|
||||||
|
gameObject.AddComponent<TPanel>();
|
||||||
|
return gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 09c0ef54950240efb3894c9ff75f4eea
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
104
My project/Assets/FlowScope/Tests/PlayMode/UI/UITestHelpers.cs
Normal file
104
My project/Assets/FlowScope/Tests/PlayMode/UI/UITestHelpers.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FlowScope.Resources;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace FlowScope.Tests.PlayMode.UI
|
||||||
|
{
|
||||||
|
internal sealed class UITestResources : IResourceService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, GameObject> _assets = new();
|
||||||
|
|
||||||
|
public List<string> LoadedKeys { get; } = new();
|
||||||
|
public List<UITestResourceHandle<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 UITestResourceHandle<GameObject>(key, asset);
|
||||||
|
Handles.Add(handle);
|
||||||
|
return Task.FromResult<IResourceHandle<T>>((IResourceHandle<T>)(object)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IResourceGroup CreateGroup()
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class UITestResourceHandle<T> : IResourceHandle<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
public UITestResourceHandle(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class UITestAsync
|
||||||
|
{
|
||||||
|
public static IEnumerator Await(Task task)
|
||||||
|
{
|
||||||
|
while (!task.IsCompleted)
|
||||||
|
{
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.IsFaulted)
|
||||||
|
{
|
||||||
|
throw task.Exception.GetBaseException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.IsCanceled)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerator Await<T>(Task<T> task, Action<T> onCompleted)
|
||||||
|
{
|
||||||
|
while (!task.IsCompleted)
|
||||||
|
{
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.IsFaulted)
|
||||||
|
{
|
||||||
|
throw task.Exception.GetBaseException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.IsCanceled)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted(task.Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 69f35d03665e47d3bf7ca6df393f07c2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user