using System; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FlowScope.UI; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; namespace FlowScope.Tests.PlayMode.UI { public sealed class UIPreloadServiceTests { private readonly List _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("ExplicitPathPrefab"); var resources = new UITestResources(); resources.Register("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); yield return UITestAsync.Await(preload.PreloadAsync(CancellationToken.None)); Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" })); Assert.That(preload.IsPreloaded(), Is.True); Assert.That(preload.TryGetPreloadedHandle(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("ExplicitPathPrefab"); var resources = new UITestResources(); resources.Register("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); yield return UITestAsync.Await(preload.PreloadAsync(CancellationToken.None)); yield return UITestAsync.Await(preload.PreloadAsync(CancellationToken.None)); Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" })); Assert.That(resources.Handles, Has.Count.EqualTo(1)); } [UnityTest] public IEnumerator PreloadAsync_WhenSamePanelLoadsConcurrently_UsesOneResourceLoad() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); var first = preload.PreloadAsync(CancellationToken.None); var second = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); resources.Complete(); yield return UITestAsync.Await(first); yield return UITestAsync.Await(second); Assert.That(resources.LoadCalls, Is.EqualTo(1)); Assert.That(resources.Handle.IsDisposed, Is.False); Assert.That(preload.IsPreloaded(), Is.True); } [UnityTest] public IEnumerator PreloadAsync_WhenFirstWaiterCancels_SecondWaiterStillSucceeds() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); using var firstCancellation = new CancellationTokenSource(); var first = preload.PreloadAsync(firstCancellation.Token); var second = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); Assert.That(resources.LoadCancellationTokenCanBeCanceled, Is.False); firstCancellation.Cancel(); yield return UITestAsync.AwaitExpected(first); resources.Complete(); yield return UITestAsync.Await(second); Assert.That(resources.LoadCalls, Is.EqualTo(1)); Assert.That(resources.Handle.IsDisposed, Is.False); Assert.That(preload.IsPreloaded(), Is.True); } [UnityTest] public IEnumerator PreloadAsync_WhenSecondWaiterCancels_FirstWaiterStillSucceeds() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); using var secondCancellation = new CancellationTokenSource(); var first = preload.PreloadAsync(CancellationToken.None); var second = preload.PreloadAsync(secondCancellation.Token); Assert.That(resources.LoadCalls, Is.EqualTo(1)); secondCancellation.Cancel(); yield return UITestAsync.AwaitExpected(second); resources.Complete(); yield return UITestAsync.Await(first); Assert.That(resources.LoadCalls, Is.EqualTo(1)); Assert.That(resources.Handle.IsDisposed, Is.False); Assert.That(preload.IsPreloaded(), Is.True); } [UnityTest] public IEnumerator PreloadAsync_WhenAllWaitersCancel_ReleasesLoadedHandleWithoutCaching() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); using var firstCancellation = new CancellationTokenSource(); using var secondCancellation = new CancellationTokenSource(); var first = preload.PreloadAsync(firstCancellation.Token); var second = preload.PreloadAsync(secondCancellation.Token); firstCancellation.Cancel(); secondCancellation.Cancel(); yield return UITestAsync.AwaitExpected(first); yield return UITestAsync.AwaitExpected(second); resources.Complete(); yield return null; Assert.That(resources.LoadCalls, Is.EqualTo(1)); Assert.That(resources.Handle.IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); } [UnityTest] public IEnumerator ReleaseAll_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); var task = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); preload.ReleaseAll(); resources.Complete(); yield return UITestAsync.Await(task); Assert.That(resources.Handle.IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); } [UnityTest] public IEnumerator PreloadAsync_AfterReleaseAllWhileOldLoadInFlight_StartsNewLoadAndCachesNewHandle() { var oldPrefab = CreatePrefab("OldExplicitPathPrefab"); var newPrefab = CreatePrefab("NewExplicitPathPrefab"); var resources = new SequencedGatedUITestResources("Custom/Explicit", oldPrefab, newPrefab); var preload = new UIPreloadService(resources); var oldTask = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); preload.ReleaseAll(); var newTask = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(2)); resources.Complete(0); yield return UITestAsync.Await(oldTask); Assert.That(resources.HandleAt(0).IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); resources.Complete(1); yield return UITestAsync.Await(newTask); Assert.That(resources.HandleAt(1).IsDisposed, Is.False); Assert.That(preload.TryGetPreloadedHandle(out var handle), Is.True); Assert.That(handle.Asset, Is.SameAs(newPrefab)); } [UnityTest] public IEnumerator OldInFlightCompletion_AfterReleaseAll_DoesNotRemoveNewInFlight() { var oldPrefab = CreatePrefab("OldExplicitPathPrefab"); var newPrefab = CreatePrefab("NewExplicitPathPrefab"); var resources = new SequencedGatedUITestResources("Custom/Explicit", oldPrefab, newPrefab); var preload = new UIPreloadService(resources); var oldTask = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); preload.ReleaseAll(); var newTask = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(2)); resources.Complete(0); yield return UITestAsync.Await(oldTask); Assert.That(preload.IsPreloaded(), Is.False); var thirdTask = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(2)); resources.Complete(1); yield return UITestAsync.Await(newTask); yield return UITestAsync.Await(thirdTask); Assert.That(resources.HandleAt(1).IsDisposed, Is.False); Assert.That(preload.IsPreloaded(), Is.True); } [UnityTest] public IEnumerator Dispose_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); var task = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); preload.Dispose(); resources.Complete(); yield return UITestAsync.Await(task); Assert.That(resources.Handle.IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); } [UnityTest] public IEnumerator PreloadAsync_AfterDispose_ThrowsObjectDisposedException() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new UITestResources(); resources.Register("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); preload.Dispose(); yield return UITestAsync.AwaitExpected( preload.PreloadAsync(CancellationToken.None)); Assert.That(resources.LoadedKeys, Is.Empty); } [UnityTest] public IEnumerator OpenAsync_WhenPanelIsPreloaded_ReusesPreloadedHandle() { var canvas = CreateCanvas("popup"); var prefab = CreatePrefab("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(CancellationToken.None)); yield return UITestAsync.Await( manager.OpenAsync( new TestViewModel("preloaded"), CancellationToken.None)); manager.Close(); Assert.That(resources.LoadedKeys, Is.EqualTo(new[] { "Custom/Explicit" })); Assert.That(resources.Handles[0].IsDisposed, Is.False); Assert.That(preload.IsPreloaded(), Is.True); } [UnityTest] public IEnumerator Release_DisposesPreloadedHandle() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new UITestResources(); resources.Register("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); yield return UITestAsync.Await(preload.PreloadAsync(CancellationToken.None)); preload.Release(); Assert.That(resources.Handles[0].IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); } [UnityTest] public IEnumerator Release_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle() { var prefab = CreatePrefab("ExplicitPathPrefab"); var resources = new GatedUITestResources("Custom/Explicit", prefab); var preload = new UIPreloadService(resources); var task = preload.PreloadAsync(CancellationToken.None); Assert.That(resources.LoadCalls, Is.EqualTo(1)); preload.Release(); resources.Complete(); yield return UITestAsync.Await(task); Assert.That(resources.Handle.IsDisposed, Is.True); Assert.That(preload.IsPreloaded(), Is.False); } private Canvas CreateCanvas(string name) { var gameObject = new GameObject(name); _objects.Add(gameObject); return gameObject.AddComponent(); } private GameObject CreatePrefab(string name) where TPanel : Component { var gameObject = new GameObject(name); _objects.Add(gameObject); gameObject.AddComponent(); return gameObject; } private sealed class GatedUITestResources : FlowScope.Resources.IResourceService { private readonly string _key; private readonly GameObject _asset; private readonly TaskCompletionSource> _gate = new(TaskCreationOptions.RunContinuationsAsynchronously); public GatedUITestResources(string key, GameObject asset) { _key = key; _asset = asset; Handle = new UITestResourceHandle(key, asset); } public int LoadCalls { get; private set; } public UITestResourceHandle Handle { get; } public bool LoadCancellationTokenCanBeCanceled { get; private set; } public Task> LoadAsync( string key, CancellationToken cancellationToken) where T : class { cancellationToken.ThrowIfCancellationRequested(); LoadCancellationTokenCanBeCanceled = cancellationToken.CanBeCanceled; if (cancellationToken.CanBeCanceled) { cancellationToken.Register(() => _gate.TrySetCanceled(cancellationToken)); } LoadCalls++; Assert.That(key, Is.EqualTo(_key)); return AwaitHandle(); } public FlowScope.Resources.IResourceGroup CreateGroup() { throw new System.NotSupportedException(); } public void Complete() { _gate.TrySetResult(Handle); } private async Task> AwaitHandle() where T : class { var handle = await _gate.Task; Assert.That(handle.Asset, Is.SameAs(_asset)); return (FlowScope.Resources.IResourceHandle)(object)handle; } } private sealed class SequencedGatedUITestResources : FlowScope.Resources.IResourceService { private readonly string _key; private readonly List _assets = new(); private readonly List> _handles = new(); private readonly List>> _gates = new(); public SequencedGatedUITestResources(string key, params GameObject[] assets) { _key = key; foreach (var asset in assets) { _assets.Add(asset); _handles.Add(new UITestResourceHandle(key, asset)); _gates.Add(new TaskCompletionSource>( TaskCreationOptions.RunContinuationsAsynchronously)); } } public int LoadCalls { get; private set; } public UITestResourceHandle HandleAt(int index) { return _handles[index]; } public Task> LoadAsync( string key, CancellationToken cancellationToken) where T : class { cancellationToken.ThrowIfCancellationRequested(); Assert.That(key, Is.EqualTo(_key)); var loadIndex = LoadCalls; LoadCalls++; Assert.That(loadIndex, Is.LessThan(_gates.Count), "Unexpected extra resource load."); return AwaitHandle(loadIndex); } public FlowScope.Resources.IResourceGroup CreateGroup() { throw new System.NotSupportedException(); } public void Complete(int index) { _gates[index].TrySetResult(_handles[index]); } private async Task> AwaitHandle(int index) where T : class { var handle = await _gates[index].Task; Assert.That(handle.Asset, Is.SameAs(_assets[index])); return (FlowScope.Resources.IResourceHandle)(object)handle; } } } }