Files
FlowScope/My project/Assets/FlowScope/Tests/PlayMode/Samples/MainMenuP0Tests.cs
2026-05-21 15:20:32 +08:00

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;
}
}
}