Files
aibis-dream/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs

329 lines
14 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using AibisDream.Framework;
using NUnit.Framework;
using TMPro;
using UnityEngine;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace AibisDream.Typography.Tests.PlayMode
{
public sealed class TypographyServicePlayModeTests
{
private readonly List<Object> createdObjects = new();
[UnitySetUp]
public IEnumerator SetUp()
{
TypographyService.ResetForTests();
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
TypographyService.ResetForTests();
foreach (Object created in createdObjects)
{
if (created != null)
Object.Destroy(created);
}
createdObjects.Clear();
yield return null;
}
[UnityTest]
public IEnumerator RuntimeCreatedText_AppliesCurrentProfile()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1.25f);
var loader = new ImmediateLoader(new Dictionary<string, TypographyProfile>
{
[TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin
});
TypographyService.ConfigureLoader(loader);
Task<bool> loadTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
yield return new WaitUntil(() => loadTask.IsCompleted);
Assert.That(loadTask.Result, Is.True);
var go = new GameObject("RuntimeTypographyText", typeof(RectTransform));
createdObjects.Add(go);
TextMeshProUGUI text = go.AddComponent<TextMeshProUGUI>();
text.fontSize = 20f;
Assert.That(TypographyService.Apply(text, TypographyRole.Body), Is.True);
Assert.That(text.fontSize, Is.EqualTo(25f));
Assert.That(text.font, Is.EqualTo(TMP_Settings.defaultFontAsset));
}
[UnityTest]
public IEnumerator RegisteredBinding_SwitchesProfilesWithoutMetricAccumulationAndReleasesOldKey()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 0.5f);
TypographyProfile russian = CreateProfile(TypographyProfileKind.Russian, 0.75f);
var loader = new ImmediateLoader(new Dictionary<string, TypographyProfile>
{
[TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin,
[TypographyLocaleMapping.GetProfileKey(LocaleKind.Ru)] = russian
});
TypographyService.ConfigureLoader(loader);
var go = new GameObject("BoundTypographyText", typeof(RectTransform));
createdObjects.Add(go);
TextMeshProUGUI text = go.AddComponent<TextMeshProUGUI>();
text.fontSize = 40f;
go.AddComponent<LocalizedTypography>();
Task<bool> latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
yield return new WaitUntil(() => latinTask.IsCompleted);
Assert.That(text.fontSize, Is.EqualTo(20f));
Task<bool> russianTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ru);
yield return new WaitUntil(() => russianTask.IsCompleted);
Assert.That(text.fontSize, Is.EqualTo(30f));
Assert.That(
loader.ReleasedKeys,
Does.Contain(TypographyLocaleMapping.GetProfileKey(LocaleKind.En)));
Task<bool> backTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
yield return new WaitUntil(() => backTask.IsCompleted);
Assert.That(text.fontSize, Is.EqualTo(20f));
}
[UnityTest]
public IEnumerator PrefabOverride_UsesResolvedInstanceMetricsAsBaseline()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f);
var loader = new ImmediateLoader(new Dictionary<string, TypographyProfile>
{
[TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin
});
TypographyService.ConfigureLoader(loader);
Task<bool> loadTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
yield return new WaitUntil(() => loadTask.IsCompleted);
Assert.That(loadTask.Result, Is.True);
var go = new GameObject("PrefabOverrideTypographyText", typeof(RectTransform));
createdObjects.Add(go);
go.SetActive(false);
TextMeshProUGUI text = go.AddComponent<TextMeshProUGUI>();
LocalizedTypography binding = go.AddComponent<LocalizedTypography>();
const float sourceBaseline = 40f;
const float resolvedOverride = 36f;
SetPrivateField(binding, "baselineCaptured", true);
SetPrivateField(binding, "baseFontSize", sourceBaseline);
text.fontSize = resolvedOverride;
go.SetActive(true);
yield return null;
Assert.That(text.fontSize, Is.EqualTo(resolvedOverride));
}
[UnityTest]
public IEnumerator OverlappingLoads_LastRequestWins()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f);
TypographyProfile japanese = CreateProfile(TypographyProfileKind.Japanese, 1f);
var loader = new DeferredLoader();
TypographyService.ConfigureLoader(loader);
Task<bool> latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
Task<bool> japaneseTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ja);
loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.Ja), japanese);
yield return new WaitUntil(() => japaneseTask.IsCompleted);
loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.En), latin);
yield return new WaitUntil(() => latinTask.IsCompleted);
Assert.That(TypographyService.CurrentProfile, Is.SameAs(japanese));
Assert.That(
TypographyService.CurrentProfileKey,
Is.EqualTo(TypographyLocaleMapping.GetProfileKey(LocaleKind.Ja)));
Assert.That(latinTask.Result, Is.False);
}
[UnityTest]
public IEnumerator OverlappingLoads_ForSameProfileDoNotReleaseTheWinningHandle()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f);
var loader = new DeferredLoader();
TypographyService.ConfigureLoader(loader);
Task<bool> firstTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
Task<bool> secondTask = TypographyService.LoadAndActivateAsync(LocaleKind.Es);
loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.En), latin);
yield return new WaitUntil(() => firstTask.IsCompleted && secondTask.IsCompleted);
Assert.That(firstTask.Result, Is.False);
Assert.That(secondTask.Result, Is.True);
Assert.That(TypographyService.CurrentProfile, Is.SameAs(latin));
Assert.That(loader.ReleasedKeys, Is.Empty);
}
[UnityTest]
public IEnumerator MissingProfile_KeepsTheLastValidProfile()
{
TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f);
var loader = new ImmediateLoader(new Dictionary<string, TypographyProfile>
{
[TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin
});
TypographyService.ConfigureLoader(loader);
Task<bool> validTask = TypographyService.LoadAndActivateAsync(LocaleKind.En);
yield return new WaitUntil(() => validTask.IsCompleted);
LogAssert.Expect(
LogType.Error,
"[TypographyService] 字体 Profile 不存在或加载失败:Typography/Profile/Japanese");
Task<bool> missingTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ja);
yield return new WaitUntil(() => missingTask.IsCompleted);
Assert.That(validTask.Result, Is.True);
Assert.That(missingTask.Result, Is.False);
Assert.That(TypographyService.CurrentProfile, Is.SameAs(latin));
Assert.That(
TypographyService.CurrentProfileKey,
Is.EqualTo(TypographyLocaleMapping.GetProfileKey(LocaleKind.En)));
}
[UnityTest]
public IEnumerator LocalizationKit_SupportedLocalesLoadRealAddressableProfilesAndReleasePreviousHandles()
{
Type localizationKit = Type.GetType(
"AibisDream.Framework.LocalizationKit, Assembly-CSharp",
throwOnError: true);
Type resourceSystem = Type.GetType(
"AibisDream.Framework.ResourceSystem, Assembly-CSharp",
throwOnError: true);
MethodInfo switchLanguage = localizationKit.GetMethod(
"SwitchLanguageAsync",
BindingFlags.Public | BindingFlags.Static);
PropertyInfo currentLanguageCode = localizationKit.GetProperty(
"CurrentLanguageCode",
BindingFlags.Public | BindingFlags.Static);
PropertyInfo persistentLoader = resourceSystem.GetProperty(
"PersistentLoader",
BindingFlags.Public | BindingFlags.Static);
Assert.That(switchLanguage, Is.Not.Null);
Assert.That(currentLanguageCode, Is.Not.Null);
Assert.That(persistentLoader, Is.Not.Null);
string originalLocale = (string)currentLanguageCode.GetValue(null);
var localeCases = new[]
{
(Code: "zh-Hans", Kind: LocaleKind.Cn),
(Code: "en", Kind: LocaleKind.En),
(Code: "ja-JP", Kind: LocaleKind.Ja),
(Code: "ru", Kind: LocaleKind.Ru),
(Code: "es", Kind: LocaleKind.Es),
(Code: "pt-BR", Kind: LocaleKind.PtBr)
};
foreach (var localeCase in localeCases)
{
var switchTask = (Task<bool>)switchLanguage.Invoke(null, new object[] { localeCase.Code });
yield return new WaitUntil(() => switchTask.IsCompleted);
Assert.That(switchTask.IsFaulted, Is.False, localeCase.Code);
Assert.That(switchTask.Result, Is.True, localeCase.Code);
Assert.That(
TypographyService.CurrentProfileKey,
Is.EqualTo(TypographyLocaleMapping.GetProfileKey(localeCase.Kind)),
localeCase.Code);
object loaderInstance = persistentLoader.GetValue(null);
Assert.That(loaderInstance, Is.Not.Null, localeCase.Code);
int handleCount = (int)loaderInstance.GetType()
.GetProperty("TotalHandleCount", BindingFlags.Public | BindingFlags.Instance)
.GetValue(loaderInstance);
Assert.That(handleCount, Is.EqualTo(1), localeCase.Code);
}
var restoreTask = (Task<bool>)switchLanguage.Invoke(null, new object[] { originalLocale });
yield return new WaitUntil(() => restoreTask.IsCompleted);
Assert.That(restoreTask.Result, Is.True);
}
private TypographyProfile CreateProfile(TypographyProfileKind kind, float multiplier)
{
var profile = ScriptableObject.CreateInstance<TypographyProfile>();
createdObjects.Add(profile);
profile.Configure(
kind,
new[]
{
new TypographyStyle(
TypographyRole.Body,
TMP_Settings.defaultFontAsset,
fontSizeMultiplier: multiplier)
});
return profile;
}
private static void SetPrivateField<T>(LocalizedTypography binding, string fieldName, T value)
{
FieldInfo field = typeof(LocalizedTypography).GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, fieldName);
field.SetValue(binding, value);
}
private sealed class ImmediateLoader : ITypographyProfileLoader
{
private readonly IReadOnlyDictionary<string, TypographyProfile> profiles;
public ImmediateLoader(IReadOnlyDictionary<string, TypographyProfile> profiles)
{
this.profiles = profiles;
}
public List<string> ReleasedKeys { get; } = new();
public Task<TypographyProfile> LoadAsync(string key)
{
profiles.TryGetValue(key, out TypographyProfile profile);
return Task.FromResult(profile);
}
public void Release(string key)
{
ReleasedKeys.Add(key);
}
}
private sealed class DeferredLoader : ITypographyProfileLoader
{
private readonly Dictionary<string, TaskCompletionSource<TypographyProfile>> pending = new();
public List<string> ReleasedKeys { get; } = new();
public Task<TypographyProfile> LoadAsync(string key)
{
if (!pending.TryGetValue(key, out TaskCompletionSource<TypographyProfile> source))
{
source = new TaskCompletionSource<TypographyProfile>();
pending[key] = source;
}
return source.Task;
}
public void Complete(string key, TypographyProfile profile)
{
pending[key].SetResult(profile);
}
public void Release(string key)
{
ReleasedKeys.Add(key);
}
}
}
}