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 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 { [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin }); TypographyService.ConfigureLoader(loader); Task 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(); 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 { [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(); text.fontSize = 40f; go.AddComponent(); Task latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); yield return new WaitUntil(() => latinTask.IsCompleted); Assert.That(text.fontSize, Is.EqualTo(20f)); Task 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 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 { [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin }); TypographyService.ConfigureLoader(loader); Task 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(); LocalizedTypography binding = go.AddComponent(); 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 latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); Task 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 firstTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); Task 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 { [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin }); TypographyService.ConfigureLoader(loader); Task validTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); yield return new WaitUntil(() => validTask.IsCompleted); LogAssert.Expect( LogType.Error, "[TypographyService] 字体 Profile 不存在或加载失败:Typography/Profile/Japanese"); Task 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)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)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(); createdObjects.Add(profile); profile.Configure( kind, new[] { new TypographyStyle( TypographyRole.Body, TMP_Settings.defaultFontAsset, fontSizeMultiplier: multiplier) }); return profile; } private static void SetPrivateField(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 profiles; public ImmediateLoader(IReadOnlyDictionary profiles) { this.profiles = profiles; } public List ReleasedKeys { get; } = new(); public Task 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> pending = new(); public List ReleasedKeys { get; } = new(); public Task LoadAsync(string key) { if (!pending.TryGetValue(key, out TaskCompletionSource source)) { source = new TaskCompletionSource(); 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); } } } }