feat: 本地化字体兼容
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user