360 lines
13 KiB
C#
360 lines
13 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FlowScope.Save;
|
|
using FlowScope.Samples.MainMenuP0;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
using UnityEngine.UI;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace FlowScope.Tests.PlayMode.Samples
|
|
{
|
|
public sealed class MainMenuP0Tests
|
|
{
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
foreach (var bootstrap in Object.FindObjectsOfType<GameBootstrap>(true))
|
|
{
|
|
Object.DestroyImmediate(bootstrap.gameObject);
|
|
}
|
|
|
|
DeleteSampleSave();
|
|
}
|
|
|
|
[Test]
|
|
public void ViewModel_IncrementGold_UpdatesPlayerDataImmediately()
|
|
{
|
|
var data = new PlayerData();
|
|
var viewModel = new MainMenuViewModel(data);
|
|
|
|
viewModel.IncrementGold();
|
|
|
|
Assert.AreEqual(1, data.Gold.Value);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Bootstrap_StartsAndOpensVisibleMainMenuPanel()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-visible-test", out var bootstrap);
|
|
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
Assert.That(panel, Is.Not.Null);
|
|
var label = panel.GetComponentInChildren<Text>(true);
|
|
var button = panel.GetComponentInChildren<Button>(true);
|
|
Assert.That(label, Is.Not.Null);
|
|
Assert.That(button, Is.Not.Null);
|
|
Assert.That(panel.gameObject.activeSelf, Is.True);
|
|
Assert.That(label.text, Is.EqualTo("0"));
|
|
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Awake_StartsThroughObservableStartupTask()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = new GameObject("bootstrap-awake-test");
|
|
var bootstrap = root.AddComponent<GameBootstrap>();
|
|
|
|
Assert.That(bootstrap.StartupTask, Is.Not.Null);
|
|
yield return AwaitTask(bootstrap.StartupTask);
|
|
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
Assert.That(panel, Is.Not.Null);
|
|
Assert.That(panel.gameObject.activeSelf, Is.True);
|
|
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ClickIncrement_UpdatesGoldTextImmediately()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-click-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
var label = panel.GetComponentInChildren<Text>(true);
|
|
var button = panel.GetComponentInChildren<Button>(true);
|
|
|
|
button.onClick.Invoke();
|
|
|
|
Assert.That(label.text, Is.EqualTo("1"));
|
|
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ShutdownAndRestart_RestoresSavedGold()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-save-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
var button = panel.GetComponentInChildren<Button>(true);
|
|
|
|
button.onClick.Invoke();
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
Object.DestroyImmediate(root);
|
|
|
|
var restartRoot = CreateInactiveBootstrap("bootstrap-restart-test", out var restartedBootstrap);
|
|
yield return AwaitTask(restartedBootstrap.StartupAsync(CancellationToken.None));
|
|
|
|
var restoredPanel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
var restoredLabel = restoredPanel.GetComponentInChildren<Text>(true);
|
|
Assert.That(restoredLabel.text, Is.EqualTo("1"));
|
|
|
|
yield return AwaitTask(restartedBootstrap.ShutdownAsync(CancellationToken.None));
|
|
Object.DestroyImmediate(restartRoot);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Shutdown_ReleasesPreloadedResourcesAndSubscriptions()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-release-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
yield return null;
|
|
|
|
Assert.That(bootstrap.IsShutdown, Is.True);
|
|
Assert.That(bootstrap.ReleasedResourceCount, Is.EqualTo(1));
|
|
Assert.That(panel == null, Is.True);
|
|
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Shutdown_WhenSaveFails_StillShutsDownFlowAndReleasesPreloads()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-save-fails-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
SetPrivateField(bootstrap, "_saveService", new ThrowingSaveService(new InvalidOperationException("save failed")));
|
|
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
|
|
|
yield return AwaitExpected<InvalidOperationException>(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
yield return null;
|
|
|
|
Assert.That(bootstrap.IsShutdown, Is.True);
|
|
Assert.That(bootstrap.ReleasedResourceCount, Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(panel == null, Is.True);
|
|
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Shutdown_WhenGameFlowShutdownFails_StillReleasesPreloads()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-flow-fails-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
SetPrivateField(bootstrap, "_saveService", new SuccessfulSaveService());
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
|
|
yield return AwaitExpected<OperationCanceledException>(bootstrap.ShutdownAsync(cancellation.Token));
|
|
|
|
Assert.That(bootstrap.IsShutdown, Is.True);
|
|
Assert.That(bootstrap.ReleasedResourceCount, Is.GreaterThanOrEqualTo(1));
|
|
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Shutdown_AfterFailure_DoesNotSkipCleanupOnSecondCall()
|
|
{
|
|
DeleteSampleSave();
|
|
var root = CreateInactiveBootstrap("bootstrap-shutdown-retry-test", out var bootstrap);
|
|
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
|
SetPrivateField(bootstrap, "_saveService", new ThrowOnceSaveService(new InvalidOperationException("save failed once")));
|
|
|
|
yield return AwaitExpected<InvalidOperationException>(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
var releaseCountAfterFailure = bootstrap.ReleasedResourceCount;
|
|
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
|
|
|
Assert.That(bootstrap.IsShutdown, Is.True);
|
|
Assert.That(releaseCountAfterFailure, Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(bootstrap.ReleasedResourceCount, Is.EqualTo(releaseCountAfterFailure));
|
|
|
|
Object.DestroyImmediate(root);
|
|
DeleteSampleSave();
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
private static IEnumerator AwaitTask(Task task)
|
|
{
|
|
while (!task.IsCompleted)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
if (task.IsFaulted)
|
|
{
|
|
throw task.Exception.GetBaseException();
|
|
}
|
|
|
|
if (task.IsCanceled)
|
|
{
|
|
throw new OperationCanceledException();
|
|
}
|
|
}
|
|
|
|
private static IEnumerator AwaitExpected<TException>(Task task)
|
|
where TException : Exception
|
|
{
|
|
while (!task.IsCompleted)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(task.IsFaulted || task.IsCanceled, Is.True);
|
|
if (task.IsCanceled)
|
|
{
|
|
Assert.That(typeof(TException), Is.EqualTo(typeof(OperationCanceledException)));
|
|
yield break;
|
|
}
|
|
|
|
Assert.That(task.Exception.GetBaseException(), Is.TypeOf<TException>());
|
|
}
|
|
|
|
private static GameObject CreateInactiveBootstrap(string name, out GameBootstrap bootstrap)
|
|
{
|
|
var root = new GameObject(name);
|
|
root.SetActive(false);
|
|
bootstrap = root.AddComponent<GameBootstrap>();
|
|
return root;
|
|
}
|
|
|
|
private static void SetPrivateField<TValue>(
|
|
GameBootstrap bootstrap,
|
|
string fieldName,
|
|
TValue value)
|
|
{
|
|
var field = typeof(GameBootstrap).GetField(
|
|
fieldName,
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.That(field, Is.Not.Null);
|
|
field.SetValue(bootstrap, value);
|
|
}
|
|
|
|
private static void DeleteSampleSave()
|
|
{
|
|
var path = Path.Combine(
|
|
Application.persistentDataPath,
|
|
"FlowScopeSaves",
|
|
Convert.ToBase64String(Encoding.UTF8.GetBytes("main-menu-player")) + ".json");
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
|
|
private sealed class SuccessfulSaveService : ISaveService
|
|
{
|
|
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(defaultValue);
|
|
}
|
|
|
|
public void Delete(string key)
|
|
{
|
|
}
|
|
|
|
public bool Exists(string key) => false;
|
|
}
|
|
|
|
private sealed class ThrowingSaveService : ISaveService
|
|
{
|
|
private readonly Exception _exception;
|
|
|
|
public ThrowingSaveService(Exception exception)
|
|
{
|
|
_exception = exception;
|
|
}
|
|
|
|
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
|
{
|
|
throw _exception;
|
|
}
|
|
|
|
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(defaultValue);
|
|
}
|
|
|
|
public void Delete(string key)
|
|
{
|
|
}
|
|
|
|
public bool Exists(string key) => false;
|
|
}
|
|
|
|
private sealed class ThrowOnceSaveService : ISaveService
|
|
{
|
|
private readonly Exception _exception;
|
|
private bool _thrown;
|
|
|
|
public ThrowOnceSaveService(Exception exception)
|
|
{
|
|
_exception = exception;
|
|
}
|
|
|
|
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
|
{
|
|
if (_thrown)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
_thrown = true;
|
|
throw _exception;
|
|
}
|
|
|
|
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(defaultValue);
|
|
}
|
|
|
|
public void Delete(string key)
|
|
{
|
|
}
|
|
|
|
public bool Exists(string key) => false;
|
|
}
|
|
}
|
|
}
|