From ee08eab9b028770f847fd3a49330bef0af8f49be Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Sat, 1 Aug 2026 13:57:45 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=87=AA=E5=8A=A8=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=8F=8A=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 + .../AudioGlobalParameterSnapshotTests.cs | 260 --------- .../DeveloperMode/DeveloperModeCoreTests.cs | 110 ---- .../DeveloperModeCoreTests.cs.meta | 11 - .../DeveloperMode/TestSaveSystemTests.cs | 476 ---------------- .../DeveloperMode/TestSaveSystemTests.cs.meta | 11 - .../FrameAnimationActorIntegrationTests.cs | 74 --- .../Huoshan/ExpressionLocalizationTests.cs | 534 ----------------- .../ExpressionLocalizationTests.cs.meta | 11 - .../Huoshan/LogReleasePresentationTests.cs | 192 ------- .../LogReleasePresentationTests.cs.meta | 11 - Assets/Editor/LocalizationExpansionTests.cs | 152 ----- .../PresentationSnapshotContractTests.cs | 98 ---- Assets/Editor/Tests.meta | 8 + .../Editor/Tests/ActorPrefabContractTests.cs | 50 ++ .../ActorPrefabContractTests.cs.meta} | 6 +- .../Tests/AudioSnapshotContractTests.cs | 152 +++++ .../AudioSnapshotContractTests.cs.meta} | 0 .../Editor/Tests/LocalizationContractTests.cs | 131 +++++ .../LocalizationContractTests.cs.meta} | 6 +- .../Editor/Tests/SaveSnapshotContractTests.cs | 60 ++ .../SaveSnapshotContractTests.cs.meta} | 0 Assets/Editor/Tests/TextSpeedSettingsTests.cs | 94 +++ .../TextSpeedSettingsTests.cs.meta | 6 +- Assets/Editor/Tests/YarnLineMetadataTests.cs | 75 +++ .../{ => Tests}/YarnLineMetadataTests.cs.meta | 6 +- Assets/Editor/TextSpeedSettingsTests.cs | 75 --- Assets/Editor/YarnLineMetadataTests.cs | 158 ------ .../EditMode/FrameAnimationCoreTests.cs | 211 +++---- .../FrameAnimationEditorWorkspaceTests.cs | 243 -------- ...FrameAnimationEditorWorkspaceTests.cs.meta | 2 - .../FrameAnimationGraphAuthoringTests.cs | 303 ---------- .../FrameAnimationGraphAuthoringTests.cs.meta | 2 - .../EditMode/FrameAnimationImportTests.cs | 469 +++++---------- .../EditMode/FrameAnimationPreviewTests.cs | 311 ---------- .../FrameAnimationPreviewTests.cs.meta | 2 - .../EditMode/FrameAnimationTimelineTests.cs | 307 ---------- .../FrameAnimationTimelineTests.cs.meta | 11 - .../FrameClipStandaloneImportTests.cs | 196 ------- .../FrameClipStandaloneImportTests.cs.meta | 11 - .../FrameAnimation/GeneratedImportTests.meta | 8 - .../GeneratedImportTests/Atlas.png | 3 - .../GeneratedImportTests/Atlas.png.meta | 209 ------- .../GeneratedImportTests/ImportGraph.asset | 75 --- .../ImportGraph.asset.meta | 8 - .../PlayMode/FrameAnimationPlayerTests.cs | 535 +++--------------- .../FrameAnimationTimelinePlayModeTests.cs | 87 ++- Assets/Tests/Huoshan.meta | 8 - Assets/Tests/Huoshan/PlayMode.meta | 8 - .../AibisDream.Huoshan.Tests.PlayMode.asmdef | 19 - ...isDream.Huoshan.Tests.PlayMode.asmdef.meta | 7 - .../ExpressionParticlePlayModeTests.cs | 90 --- .../ExpressionParticlePlayModeTests.cs.meta | 11 - Assets/Tests/_Temporary.meta | 8 + Assets/Tests/_Temporary/README.md | 22 + Assets/Tests/_Temporary/README.md.meta | 7 + Docs/UnityTestingGuidelines.md | 170 ++++++ Docs/UnityTestingGuidelines.md.meta | 7 + 58 files changed, 1157 insertions(+), 4971 deletions(-) delete mode 100644 Assets/Editor/AudioGlobalParameterSnapshotTests.cs delete mode 100644 Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs delete mode 100644 Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta delete mode 100644 Assets/Editor/DeveloperMode/TestSaveSystemTests.cs delete mode 100644 Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta delete mode 100644 Assets/Editor/FrameAnimationActorIntegrationTests.cs delete mode 100644 Assets/Editor/Huoshan/ExpressionLocalizationTests.cs delete mode 100644 Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta delete mode 100644 Assets/Editor/Huoshan/LogReleasePresentationTests.cs delete mode 100644 Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta delete mode 100644 Assets/Editor/LocalizationExpansionTests.cs delete mode 100644 Assets/Editor/PresentationSnapshotContractTests.cs create mode 100644 Assets/Editor/Tests.meta create mode 100644 Assets/Editor/Tests/ActorPrefabContractTests.cs rename Assets/Editor/{FrameAnimationActorIntegrationTests.cs.meta => Tests/ActorPrefabContractTests.cs.meta} (76%) create mode 100644 Assets/Editor/Tests/AudioSnapshotContractTests.cs rename Assets/Editor/{AudioGlobalParameterSnapshotTests.cs.meta => Tests/AudioSnapshotContractTests.cs.meta} (100%) create mode 100644 Assets/Editor/Tests/LocalizationContractTests.cs rename Assets/Editor/{LocalizationExpansionTests.cs.meta => Tests/LocalizationContractTests.cs.meta} (76%) create mode 100644 Assets/Editor/Tests/SaveSnapshotContractTests.cs rename Assets/Editor/{PresentationSnapshotContractTests.cs.meta => Tests/SaveSnapshotContractTests.cs.meta} (100%) create mode 100644 Assets/Editor/Tests/TextSpeedSettingsTests.cs rename Assets/Editor/{ => Tests}/TextSpeedSettingsTests.cs.meta (76%) create mode 100644 Assets/Editor/Tests/YarnLineMetadataTests.cs rename Assets/Editor/{ => Tests}/YarnLineMetadataTests.cs.meta (76%) delete mode 100644 Assets/Editor/TextSpeedSettingsTests.cs delete mode 100644 Assets/Editor/YarnLineMetadataTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs.meta delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs.meta delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs.meta delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationTimelineTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameAnimationTimelineTests.cs.meta delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameClipStandaloneImportTests.cs delete mode 100644 Assets/Tests/FrameAnimation/EditMode/FrameClipStandaloneImportTests.cs.meta delete mode 100644 Assets/Tests/FrameAnimation/GeneratedImportTests.meta delete mode 100644 Assets/Tests/FrameAnimation/GeneratedImportTests/Atlas.png delete mode 100644 Assets/Tests/FrameAnimation/GeneratedImportTests/Atlas.png.meta delete mode 100644 Assets/Tests/FrameAnimation/GeneratedImportTests/ImportGraph.asset delete mode 100644 Assets/Tests/FrameAnimation/GeneratedImportTests/ImportGraph.asset.meta delete mode 100644 Assets/Tests/Huoshan.meta delete mode 100644 Assets/Tests/Huoshan/PlayMode.meta delete mode 100644 Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef delete mode 100644 Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef.meta delete mode 100644 Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs delete mode 100644 Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs.meta create mode 100644 Assets/Tests/_Temporary.meta create mode 100644 Assets/Tests/_Temporary/README.md create mode 100644 Assets/Tests/_Temporary/README.md.meta create mode 100644 Docs/UnityTestingGuidelines.md create mode 100644 Docs/UnityTestingGuidelines.md.meta diff --git a/AGENTS.md b/AGENTS.md index a478c5699..534f65de8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,17 @@ Modular framework with Kit-pattern utilities: 4. Favor composition over inheritance 5. All runtime asset loading via `ResourceSystem` — never `Resources.Load` +### Unity Tests + +- Unity Tests are encouraged when they provide useful feedback, but do not add tests mechanically at every layer merely because production code changed. +- Choose the lowest layer that fully protects each behavior. Add a second layer only when it covers a distinct integration risk. +- Stable tests protect accepted contracts and high-risk regressions. Prefer extending an existing domain suite; create a new permanent file or asmdef only for a genuinely new long-lived boundary. +- Tests for unfinished, exploratory, or intermediate behavior must live under `Assets/Tests/_Temporary//`; never mix them into permanent suites. +- Every temporary test folder must state its purpose and deletion or promotion condition. Delete it when the feature is accepted, replaced, abandoned, or adequately covered by permanent contract tests. +- Prefer EditMode when it is sufficient; use PlayMode for behavior that truly requires Unity lifecycle, frames, coroutines, physics, or Timeline runtime integration. +- Remove tests when their feature is removed, their contract is replaced, or they only mirror implementation details. +- Follow [Unity Test Guidelines](Docs/UnityTestingGuidelines.md) for placement, design, cleanup, and review criteria. + ### Scene Structure - Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader - Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`) diff --git a/Assets/Editor/AudioGlobalParameterSnapshotTests.cs b/Assets/Editor/AudioGlobalParameterSnapshotTests.cs deleted file mode 100644 index 82b6dfd04..000000000 --- a/Assets/Editor/AudioGlobalParameterSnapshotTests.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AibisDream.Kit; -using AibisDream.SaveSystem; -using FMOD; -using FMOD.Studio; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class AudioGlobalParameterSnapshotTests - { - [Test] - public void SetFromYarn_TracksOnlySuccessfulUniqueNames() - { - var api = new FakeFmodGlobalParameterApi(); - api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND; - var registry = new YarnGlobalParameterRegistry(api); - - Assert.That(registry.SetFromYarn("reverb", 0.4f), Is.EqualTo(RESULT.OK)); - Assert.That(registry.SetFromYarn("REVERB", 0.7f), Is.EqualTo(RESULT.OK)); - Assert.That( - registry.SetFromYarn("missing", 1f), - Is.EqualTo(RESULT.ERR_EVENT_NOTFOUND)); - - Assert.That(registry.TrackedNames, Has.Count.EqualTo(1)); - Assert.That(registry.TrackedNames.Single(), Is.EqualTo("reverb")); - Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.False); - Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.False); - } - - [Test] - public void CaptureFinalValues_ReadsFmodFinalValueAndRetainsFailuresForRetry() - { - var api = new FakeFmodGlobalParameterApi(); - api.Reads["stage"] = new ParameterRead(9f, 2.37f, RESULT.OK); - api.Reads["missing"] = new ParameterRead(0f, 0f, RESULT.ERR_EVENT_NOTFOUND); - var registry = new YarnGlobalParameterRegistry(api); - registry.SetFromYarn("stage", 9f); - registry.SetFromYarn("missing", 1f); - var warnings = new List(); - - var snapshot = registry.CaptureFinalValues(warnings.Add); - - Assert.That(snapshot, Has.Count.EqualTo(1)); - Assert.That(snapshot["stage"], Is.EqualTo(2.37f)); - Assert.That(snapshot["stage"], Is.Not.EqualTo(9f)); - Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage", "missing" })); - Assert.That(warnings, Has.Count.EqualTo(1)); - StringAssert.Contains("missing", warnings[0]); - } - - [Test] - public void ResetToDefaults_RemovesOnlyParametersThatFmodResetSuccessfully() - { - var goodId = new PARAMETER_ID { data1 = 1, data2 = 2 }; - var failedId = new PARAMETER_ID { data1 = 3, data2 = 4 }; - var api = new FakeFmodGlobalParameterApi(); - api.Descriptions["good"] = new ParameterDescriptionRead( - new PARAMETER_DESCRIPTION { id = goodId, defaultvalue = 0.25f }, - RESULT.OK); - api.Descriptions["failed"] = new ParameterDescriptionRead( - new PARAMETER_DESCRIPTION { id = failedId, defaultvalue = 0.75f }, - RESULT.OK); - api.SetByIdResults[failedId] = RESULT.ERR_INVALID_PARAM; - var registry = new YarnGlobalParameterRegistry(api); - registry.SetFromYarn("good", 1f); - registry.SetFromYarn("failed", 1f); - var warnings = new List(); - - registry.ResetToDefaults(warnings.Add); - - Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "failed" })); - Assert.That(api.SetByIdCalls, Has.Count.EqualTo(2)); - var goodReset = api.SetByIdCalls.Single(call => call.Id.Equals(goodId)); - Assert.That(goodReset.Value, Is.EqualTo(0.25f)); - Assert.That(goodReset.IgnoreSeekSpeed, Is.True); - Assert.That(warnings, Has.Count.EqualTo(1)); - StringAssert.Contains("failed", warnings[0]); - } - - [Test] - public void RestoreFinalValues_JumpsImmediatelyThenNextYarnTargetUsesSeekSpeed() - { - var api = new FakeFmodGlobalParameterApi(); - api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND; - var registry = new YarnGlobalParameterRegistry(api); - var warnings = new List(); - - registry.RestoreFinalValues( - new Dictionary - { - ["stage"] = 2.37f, - ["missing"] = 1f - }, - warnings.Add); - registry.SetFromYarn("stage", 3f); - - Assert.That(api.SetByNameCalls, Has.Count.EqualTo(3)); - Assert.That(api.SetByNameCalls[0].Value, Is.EqualTo(2.37f)); - Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.True); - Assert.That(api.SetByNameCalls[1].Name, Is.EqualTo("missing")); - Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.True); - Assert.That(api.SetByNameCalls[2].Value, Is.EqualTo(3f)); - Assert.That(api.SetByNameCalls[2].IgnoreSeekSpeed, Is.False); - Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage" })); - Assert.That(warnings, Has.Count.EqualTo(1)); - StringAssert.Contains("missing", warnings[0]); - } - - [Test] - public void AudioSnapshotDto_RoundTripsFinalValuesWithoutChangingSchema() - { - var source = new SaveSnapshot(); - source.sections[SnapshotProviderIds.Audio] = new AudioSnapshotDto - { - ambState = "Dream", - yarnGlobalParameters = new Dictionary - { - ["reverb"] = 0.42f, - ["hs1LogStage"] = 2.37f - } - }; - - var json = JsonConvert.SerializeObject(source); - var restored = JsonConvert.DeserializeObject(json); - var audio = ((JObject)restored.sections[SnapshotProviderIds.Audio]) - .ToObject(); - - Assert.That(restored.schemaVersion, Is.EqualTo(2)); - Assert.That(SaveSnapshotSchema.CurrentVersion, Is.EqualTo(2)); - Assert.That(audio.yarnGlobalParameters["reverb"], Is.EqualTo(0.42f)); - Assert.That(audio.yarnGlobalParameters["hs1LogStage"], Is.EqualTo(2.37f)); - } - - private readonly struct SetByNameCall - { - internal SetByNameCall(string name, float value, bool ignoreSeekSpeed) - { - Name = name; - Value = value; - IgnoreSeekSpeed = ignoreSeekSpeed; - } - - internal string Name { get; } - internal float Value { get; } - internal bool IgnoreSeekSpeed { get; } - } - - private readonly struct SetByIdCall - { - internal SetByIdCall(PARAMETER_ID id, float value, bool ignoreSeekSpeed) - { - Id = id; - Value = value; - IgnoreSeekSpeed = ignoreSeekSpeed; - } - - internal PARAMETER_ID Id { get; } - internal float Value { get; } - internal bool IgnoreSeekSpeed { get; } - } - - private readonly struct ParameterRead - { - internal ParameterRead(float value, float finalValue, RESULT result) - { - Value = value; - FinalValue = finalValue; - Result = result; - } - - internal float Value { get; } - internal float FinalValue { get; } - internal RESULT Result { get; } - } - - private readonly struct ParameterDescriptionRead - { - internal ParameterDescriptionRead( - PARAMETER_DESCRIPTION description, - RESULT result) - { - Description = description; - Result = result; - } - - internal PARAMETER_DESCRIPTION Description { get; } - internal RESULT Result { get; } - } - - private sealed class FakeFmodGlobalParameterApi : IFmodGlobalParameterApi - { - internal readonly Dictionary SetByNameResults = - new(System.StringComparer.OrdinalIgnoreCase); - internal readonly Dictionary Reads = - new(System.StringComparer.OrdinalIgnoreCase); - internal readonly Dictionary Descriptions = - new(System.StringComparer.OrdinalIgnoreCase); - internal readonly Dictionary SetByIdResults = new(); - internal readonly List SetByNameCalls = new(); - internal readonly List SetByIdCalls = new(); - - public RESULT SetParameterByName( - string name, - float value, - bool ignoreSeekSpeed) - { - SetByNameCalls.Add(new SetByNameCall(name, value, ignoreSeekSpeed)); - return SetByNameResults.TryGetValue(name, out var result) - ? result - : RESULT.OK; - } - - public RESULT GetParameterByName( - string name, - out float value, - out float finalValue) - { - if (Reads.TryGetValue(name, out var read)) - { - value = read.Value; - finalValue = read.FinalValue; - return read.Result; - } - - value = 0f; - finalValue = 0f; - return RESULT.OK; - } - - public RESULT GetParameterDescriptionByName( - string name, - out PARAMETER_DESCRIPTION description) - { - if (Descriptions.TryGetValue(name, out var read)) - { - description = read.Description; - return read.Result; - } - - description = default; - return RESULT.ERR_EVENT_NOTFOUND; - } - - public RESULT SetParameterById( - PARAMETER_ID id, - float value, - bool ignoreSeekSpeed) - { - SetByIdCalls.Add(new SetByIdCall(id, value, ignoreSeekSpeed)); - return SetByIdResults.TryGetValue(id, out var result) - ? result - : RESULT.OK; - } - } - } -} diff --git a/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs deleted file mode 100644 index b6822d200..000000000 --- a/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -#if UNITY_EDITOR -using System.Linq; -using AibisDream.Kit; -using AibisDream.UI; -using AibisDream.Utility; -using NUnit.Framework; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.DeveloperMode.Editor -{ - public sealed class DeveloperModeCoreTests - { - [Test] - public void RuntimeLogBufferEvictsOldestAndPreservesOrder() - { - var buffer = new RuntimeLogBuffer(3); - buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, "one")); - buffer.Add(LogEntry.System(LogLevel.Warning, LogCategory.Save, "two")); - buffer.Add(LogEntry.System(LogLevel.Error, LogCategory.Yarn, "three")); - buffer.Add(LogEntry.System(LogLevel.Fatal, LogCategory.Scene, "four")); - - var snapshot = buffer.Snapshot(); - Assert.That(snapshot.Select(item => item.Message), Is.EqualTo(new[] { "two", "three", "four" })); - Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending); - Assert.That(snapshot.Length, Is.EqualTo(buffer.Capacity)); - } - - [Test] - public void RuntimeLogBufferAcceptsConcurrentWritersAndClears() - { - var buffer = new RuntimeLogBuffer(128); - System.Threading.Tasks.Parallel.For(0, 1000, index => - buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, index.ToString()))); - - var snapshot = buffer.Snapshot(); - Assert.That(snapshot.Length, Is.EqualTo(128)); - Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending); - buffer.Clear(); - Assert.That(buffer.Snapshot(), Is.Empty); - } - - [Test] - public void YarnStorageTypedSettersAndDebugSnapshotAreReadOnlyAndConsistent() - { - var go = new GameObject("YarnVariableStorage Test"); - try - { - var storage = go.AddComponent(); - storage.SetValue("$score", 12.5f); - storage.SetValue("$global_flag", true); - storage.SetValue("$name", "AIBIS"); - - var snapshot = DeveloperVariableSnapshot.Capture(storage, null); - Assert.That(snapshot.Select(item => item.Name), - Is.EquivalentTo(new[] { "$score", "$global_flag", "$name" })); - Assert.That(snapshot.Single(item => item.Name == "$score").Value, Is.EqualTo("12.5")); - Assert.That(snapshot.Single(item => item.Name == "$global_flag").IsGlobal, Is.True); - Assert.That(snapshot.All(item => item.HasRuntimeOverride), Is.True); - - storage.ClearLocal(); - var remaining = DeveloperVariableSnapshot.Capture(storage, null); - Assert.That(remaining.Select(item => item.Name), Is.EqualTo(new[] { "$global_flag" })); - storage.Clear(); - Assert.That(DeveloperVariableSnapshot.Capture(storage, null), Is.Empty); - } - finally - { - Object.DestroyImmediate(go); - } - } - - [Test] - public void PrefabIsConfiguredAndPersistenceReferencesIt() - { - const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab"; - const string scenePath = "Assets/Scenes/Persistence.unity"; - var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); - Assert.That(prefab, Is.Not.Null); - Assert.That(prefab.activeSelf, Is.False); - var panel = prefab.GetComponent(); - Assert.That(panel, Is.Not.Null); - Assert.That(panel.IsConfigured, Is.True); - Assert.That(AssetDatabase.GetDependencies(scenePath), Does.Contain(prefabPath)); - } - - [Test] - public void PanelToggleIsNonModalAndEscapeClosesIt() - { - const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab"; - var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); - var instance = Object.Instantiate(prefab); - try - { - var panel = instance.GetComponent(); - var originalScale = Time.timeScale; - panel.TogglePanel(); - Assert.That(panel.IsOpen, Is.True); - Assert.That(Time.timeScale, Is.EqualTo(originalScale)); - Assert.That(panel.HandleEscape(), Is.True); - Assert.That(panel.IsOpen, Is.False); - } - finally - { - Object.DestroyImmediate(instance); - } - } - } -} -#endif diff --git a/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta deleted file mode 100644 index 71b3a549c..000000000 --- a/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b95ba9d1e80e3dc4e83bc408569c4955 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs deleted file mode 100644 index 57537d9c9..000000000 --- a/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs +++ /dev/null @@ -1,476 +0,0 @@ -#if UNITY_EDITOR -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AibisDream.Framework; -using AibisDream.SaveSystem; -using Newtonsoft.Json; -using NUnit.Framework; -using UnityEngine; - -namespace AibisDream.DeveloperMode.Editor.Tests -{ - public sealed class TestSaveSystemTests - { - private string _root; - private TestSaveRepository _repository; - - [SetUp] - public void SetUp() - { - _root = Path.Combine( - Path.GetTempPath(), - "AibisDream-TestSaveTests", - Guid.NewGuid().ToString("N")); - _repository = new TestSaveRepository(_root); - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_root)) - { - Directory.Delete(_root, true); - } - } - - [Test] - public void Record_SameAnchor_ReplacesSnapshotAndKeepsFirstSeenOrder() - { - var first = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneOne")); - var second = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneTwo")); - var scan = _repository.Scan("1.0"); - - Assert.That(scan.Entries.Count, Is.EqualTo(1)); - Assert.That(second.Meta.firstSeenOrder, Is.EqualTo(first.Meta.firstSeenOrder)); - Assert.That(scan.Entries[0].Meta.sceneName, Is.EqualTo("SceneTwo")); - Assert.That(_repository.TryLoad(scan.Entries[0], out var snapshot, out var error), Is.True, error); - Assert.That(snapshot.scene.sceneName, Is.EqualTo("SceneTwo")); - } - - [Test] - public void Record_ReplacingEarlierEntry_DoesNotMoveItsOrder() - { - _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); - _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); - _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA2")); - - var entries = _repository.Scan().Entries.ToArray(); - Assert.That(entries.Select(item => item.NodeName), Is.EqualTo(new[] { "NodeA", "NodeB" })); - Assert.That(entries[0].Meta.firstSeenOrder, Is.LessThan(entries[1].Meta.firstSeenOrder)); - } - - [Test] - public void Scan_RetainsMissingSnapshotAsInvalid() - { - var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); - File.Delete(entry.SnapshotPath); - - var scan = _repository.Scan(); - - Assert.That(scan.ValidCount, Is.Zero); - Assert.That(scan.InvalidCount, Is.EqualTo(1)); - Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.MissingSnapshot)); - } - - [Test] - public void Scan_RetainsOrphanSnapshotWithBrokenMeta() - { - var directory = Path.Combine(_root, "chapter", "broken"); - Directory.CreateDirectory(directory); - File.WriteAllText( - Path.Combine(directory, TestSaveRepository.SnapshotFileName), - "{}", - Encoding.UTF8); - - var scan = _repository.Scan(); - - Assert.That(scan.InvalidCount, Is.EqualTo(1)); - Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptMeta)); - } - - [Test] - public void Validate_CorruptSnapshot_IsRetainedAndDisabledOnNextScan() - { - var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); - File.WriteAllText(entry.SnapshotPath, "{broken", Encoding.UTF8); - - Assert.That(_repository.Validate(entry, out var error), Is.False); - var rescanned = _repository.Scan(); - - Assert.That(error, Does.Contain("无法解析")); - Assert.That(rescanned.InvalidCount, Is.EqualTo(1)); - Assert.That(rescanned.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptSnapshot)); - } - - [Test] - public void Scan_RecoversCompleteStagingDirectory() - { - var final = Path.Combine(_root, "chapter", "node"); - var staging = final + ".__staging"; - Directory.CreateDirectory(staging); - var request = CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"); - var meta = CreateMeta(request, 1); - File.WriteAllText( - Path.Combine(staging, TestSaveRepository.MetaFileName), - JsonConvert.SerializeObject(meta), - Encoding.UTF8); - File.WriteAllText( - Path.Combine(staging, TestSaveRepository.SnapshotFileName), - SnapshotPersistence.Serialize(request.Snapshot), - Encoding.UTF8); - - var scan = _repository.Scan(); - - Assert.That(Directory.Exists(final), Is.True); - Assert.That(Directory.Exists(staging), Is.False); - Assert.That(scan.ValidCount, Is.EqualTo(1)); - } - - [Test] - public void Record_ConcurrentRequests_ProducesCompleteUniqueEntries() - { - var tasks = Enumerable.Range(0, 12) - .Select(index => Task.Run(() => - _repository.Record(CreateRequest( - "ChapterA", - "ProjectA", - $"Node{index:00}", - $"Scene{index:00}")))) - .ToArray(); - - Task.WaitAll(tasks); - var scan = _repository.Scan(); - - Assert.That(scan.ValidCount, Is.EqualTo(12)); - Assert.That(scan.InvalidCount, Is.Zero); - Assert.That(scan.Entries.Select(item => item.Meta.firstSeenOrder).Distinct().Count(), Is.EqualTo(12)); - } - - [Test] - public void DeleteInvalid_RemovesOnlyInvalidEntries() - { - _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); - var invalid = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); - File.Delete(invalid.SnapshotPath); - - var removed = _repository.DeleteInvalid(out var error); - var scan = _repository.Scan(); - - Assert.That(error, Is.Null); - Assert.That(removed, Is.EqualTo(1)); - Assert.That(scan.ValidCount, Is.EqualTo(1)); - Assert.That(scan.InvalidCount, Is.Zero); - } - - [TestCase(new[] { "hub" }, true)] - [TestCase(new[] { "linear" }, true)] - [TestCase(new[] { "content" }, true)] - [TestCase(new[] { "event" }, false)] - [TestCase(new[] { "hub", "no_save" }, false)] - [TestCase(new string[0], true)] - public void SilentCoverageEvaluator_MatchesSavePointTagSemantics(string[] tags, bool expected) - { - var actual = SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently( - "Node", - tags, - out _); - Assert.That(actual, Is.EqualTo(expected)); - } - - [TestCase(new[] { "content" }, NodeSaveTiming.NodeEnter, SaveResumeMode.RestartNode)] - [TestCase(new[] { "interaction" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)] - [TestCase(new[] { "content", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)] - [TestCase(new[] { "event", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)] - [TestCase(new[] { "interaction", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)] - [TestCase(new[] { "interaction", "no_save" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)] - [TestCase(new[] { "content", "event" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)] - public void ResolveNodePolicy_MapsNodeTypeAndModifiers( - string[] tags, - NodeSaveTiming expectedTiming, - SaveResumeMode expectedResumeMode) - { - var policy = SavePointEvaluator.ResolveNodePolicy( - "Node", - tags, - out _, - logWarnings: false); - - Assert.That(policy.Timing, Is.EqualTo(expectedTiming)); - Assert.That(policy.ResumeMode, Is.EqualTo(expectedResumeMode)); - } - - [Test] - public void Repository_StateOnlySnapshot_PreservesAnchorNode() - { - var request = CreateRequest("ChapterA", "ProjectA", "InteractionNode", "SceneA"); - request.Snapshot.anchor.startDialogueOnRestore = false; - request.SaveTrigger = "DialogueExit"; - request.ResumeMode = nameof(SaveResumeMode.StateOnly); - request.StartDialogueOnRestore = false; - request.DedupeKey = TestSaveRecorder.BuildDedupeKey( - request.SceneSoName, - request.YarnProjectId, - request.NodeName, - request.ResumeMode); - request.EntryId = TestSaveRepository.StableHash(request.DedupeKey); - - var entry = _repository.Record(request); - var loaded = _repository.TryLoad(entry, out var snapshot, out var error); - - Assert.That(loaded, Is.True, error); - Assert.That(entry.NodeName, Is.EqualTo("InteractionNode")); - Assert.That(entry.ResumeMode, Is.EqualTo(nameof(SaveResumeMode.StateOnly))); - Assert.That(snapshot.anchor.nodeName, Is.EqualTo("InteractionNode")); - Assert.That(snapshot.anchor.startDialogueOnRestore, Is.False); - } - - [Test] - public void SnapshotPersistence_MissingRestoreFlag_IsRejected() - { - const string json = - "{\"schemaVersion\":2,\"anchor\":{\"sceneSoName\":\"ChapterA\"," + - "\"yarnProjectId\":\"ProjectA\",\"nodeName\":\"NodeA\"}}"; - - Assert.Throws( - () => SnapshotPersistence.Deserialize(json)); - } - - [Test] - public void SerialTaskQueue_DoesNotDropOrReorderRequests() - { - var queue = new SerialTaskQueue(); - var releaseFirst = new TaskCompletionSource(); - var firstStarted = new TaskCompletionSource(); - var order = new List(); - - var first = queue.Enqueue(async () => - { - order.Add(1); - firstStarted.SetResult(true); - await releaseFirst.Task; - order.Add(2); - }); - var second = queue.Enqueue(() => - { - order.Add(3); - return Task.CompletedTask; - }); - - Assert.That(firstStarted.Task.Wait(TimeSpan.FromSeconds(2)), Is.True); - Assert.That(order, Is.EqualTo(new[] { 1 })); - Assert.That(queue.PendingCount, Is.EqualTo(2)); - - releaseFirst.SetResult(true); - Assert.That( - Task.WaitAll( - new[] { first.Completion, second.Completion }, - TimeSpan.FromSeconds(2)), - Is.True); - - Assert.That(first.Sequence, Is.LessThan(second.Sequence)); - Assert.That(order, Is.EqualTo(new[] { 1, 2, 3 })); - } - - [Test] - public void SaveRequest_MapsResumeModeToExplicitAnchorFlag() - { - var enter = SaveRequest.NodeEnter("NodeA", "ProjectA", "ChapterA", 3, 7); - var exit = SaveRequest.DialogueExit("NodeB", "ProjectA", "ChapterA", 3, 9); - - Assert.That(enter.AnchorSpec.NodeName, Is.EqualTo("NodeA")); - Assert.That(enter.AnchorSpec.StartDialogueOnRestore, Is.True); - Assert.That(exit.AnchorSpec.NodeName, Is.EqualTo("NodeB")); - Assert.That(exit.AnchorSpec.StartDialogueOnRestore, Is.False); - } - - [TestCase(false, "DetourNode", true)] - [TestCase(false, "NoSaveNode", false)] - [TestCase(true, "AnotherNode", true)] - public void ValidateDialogueTiming_NonExitRequestIgnoresLaterDialogueState( - bool explicitCommand, - string currentNodeName, - bool isDialogueRunning) - { - var request = explicitCommand - ? SaveRequest.Explicit("SourceNode", "ProjectA", "ChapterA", 3, 7) - : SaveRequest.NodeEnter("SourceNode", "ProjectA", "ChapterA", 3, 7); - - var valid = SaveRestoreOrchestrator.ValidateDialogueTiming( - request, - currentDialogueRunId: 99, - currentFlowRevision: 101, - currentNodeName: currentNodeName, - isDialogueRunning: isDialogueRunning, - out var reason); - - Assert.That(valid, Is.True, reason); - Assert.That(request.AnchorSpec.NodeName, Is.EqualTo("SourceNode")); - } - - [Test] - public void ValidateDialogueTiming_ConsecutiveNodeEnterRequestsRemainEligible() - { - var first = SaveRequest.NodeEnter("FirstNode", "ProjectA", "ChapterA", 3, 7); - var second = SaveRequest.NodeEnter("SecondNode", "ProjectA", "ChapterA", 3, 8); - - var firstValid = SaveRestoreOrchestrator.ValidateDialogueTiming( - first, - currentDialogueRunId: 3, - currentFlowRevision: 8, - currentNodeName: "SecondNode", - isDialogueRunning: true, - out var firstReason); - var secondValid = SaveRestoreOrchestrator.ValidateDialogueTiming( - second, - currentDialogueRunId: 3, - currentFlowRevision: 8, - currentNodeName: "SecondNode", - isDialogueRunning: true, - out var secondReason); - - Assert.That(firstValid, Is.True, firstReason); - Assert.That(secondValid, Is.True, secondReason); - Assert.That(first.AnchorSpec.NodeName, Is.EqualTo("FirstNode")); - Assert.That(second.AnchorSpec.NodeName, Is.EqualTo("SecondNode")); - } - - [Test] - public void ValidateDialogueTiming_DialogueExitAcceptsStableCompletedFlow() - { - var request = SaveRequest.DialogueExit( - "InteractionNode", - "ProjectA", - "ChapterA", - 3, - 9); - - var valid = SaveRestoreOrchestrator.ValidateDialogueTiming( - request, - currentDialogueRunId: 3, - currentFlowRevision: 9, - currentNodeName: null, - isDialogueRunning: false, - out var reason); - - Assert.That(valid, Is.True, reason); - } - - [TestCase(4, 9, null, false, "dialogue run changed")] - [TestCase(3, 10, null, false, "flow revision changed")] - [TestCase(3, 9, "DetourNode", false, "current Yarn node")] - [TestCase(3, 9, "NoSaveNode", false, "current Yarn node")] - [TestCase(3, 9, "JumpTarget", true, "still running")] - public void ValidateDialogueTiming_DialogueExitRejectsChangedOrRunningFlow( - long currentDialogueRunId, - long currentFlowRevision, - string currentNodeName, - bool isDialogueRunning, - string expectedReason) - { - var request = SaveRequest.DialogueExit( - "InteractionNode", - "ProjectA", - "ChapterA", - 3, - 9); - - var valid = SaveRestoreOrchestrator.ValidateDialogueTiming( - request, - currentDialogueRunId, - currentFlowRevision, - currentNodeName, - isDialogueRunning, - out var reason); - - Assert.That(valid, Is.False); - Assert.That(reason, Does.Contain(expectedReason)); - } - - [Test] - public void InteractionLock_ComposesWithDialogueLock() - { - var gameObject = new GameObject("EventSystemEx-Test"); - try - { - var eventSystem = gameObject.AddComponent(); - EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart); - var checkpointLock = eventSystem.AcquireInteractionLock("test"); - - EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd); - Assert.That(eventSystem.isLocked, Is.True); - - checkpointLock.Dispose(); - Assert.That(eventSystem.isLocked, Is.False); - } - finally - { - UnityEngine.Object.DestroyImmediate(gameObject); - } - } - - private static TestSaveRecordRequest CreateRequest( - string sceneSoName, - string yarnProject, - string nodeName, - string sceneName) - { - var key = TestSaveRecorder.BuildDedupeKey(sceneSoName, yarnProject, nodeName); - return new TestSaveRecordRequest - { - Snapshot = new SaveSnapshot - { - gameVersion = "1.0", - savedAt = "2026-01-01 00:00:00", - scene = new SceneSnapshotDto { sceneName = sceneName }, - anchor = new AnchorSnapshot - { - sceneSoName = sceneSoName, - yarnProjectId = yarnProject, - nodeName = nodeName, - startDialogueOnRestore = true - } - }, - DedupeKey = key, - EntryId = TestSaveRepository.StableHash(key), - ChapterId = sceneSoName, - ChapterTitle = sceneSoName, - SceneSoName = sceneSoName, - YarnProjectId = yarnProject, - NodeName = nodeName, - SaveTrigger = "NodeEnter", - ResumeMode = nameof(SaveResumeMode.RestartNode), - StartDialogueOnRestore = true, - SceneName = sceneName, - GameVersion = "1.0" - }; - } - - private static TestSaveMeta CreateMeta(TestSaveRecordRequest request, long order) - { - return new TestSaveMeta - { - entryId = request.EntryId, - dedupeKey = request.DedupeKey, - chapterId = request.ChapterId, - chapterTitle = request.ChapterTitle, - sceneSoName = request.SceneSoName, - yarnProjectId = request.YarnProjectId, - nodeName = request.NodeName, - saveTrigger = request.SaveTrigger, - resumeMode = request.ResumeMode, - startDialogueOnRestore = request.StartDialogueOnRestore, - sceneName = request.SceneName, - firstSeenOrder = order, - firstRecordedAt = "2026-01-01 00:00:00", - lastRecordedAt = "2026-01-01 00:00:00", - snapshotSchemaVersion = SaveSnapshotSchema.CurrentVersion, - gameVersion = request.GameVersion - }; - } - } -} -#endif diff --git a/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta deleted file mode 100644 index 35ca4567c..000000000 --- a/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: cbbba0c3621cf204483f453bad2d75a4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/FrameAnimationActorIntegrationTests.cs b/Assets/Editor/FrameAnimationActorIntegrationTests.cs deleted file mode 100644 index af01998cf..000000000 --- a/Assets/Editor/FrameAnimationActorIntegrationTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using AibisDream.FrameAnimation; -using AibisDream.SaveSystem; -using AibisDream.Utility; -using NUnit.Framework; -using UnityEditor; -using UnityEditor.AddressableAssets; -using UnityEngine; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class FrameAnimationActorIntegrationTests - { - private const string PrefabPath = - "Assets/Prefabs/FixSystemPrefabs/FrameAnimationActor.prefab"; - - [Test] - public void ActorType_IncludesFrameAnimationMember() - { - Assert.That(System.Enum.IsDefined(typeof(ActorType), ActorType.FrameAnimation), Is.True); - Assert.That( - System.Enum.TryParse("FrameAnimation", out ActorType parsed), - Is.True); - Assert.That(parsed, Is.EqualTo(ActorType.FrameAnimation)); - } - - [Test] - public void Prefab_HasRequiredComponentsAndPersistentAddress() - { - var prefab = AssetDatabase.LoadAssetAtPath(PrefabPath); - Assert.That(prefab, Is.Not.Null); - Assert.That(prefab.GetComponent(), Is.Not.Null); - Assert.That(prefab.GetComponent(), Is.Not.Null); - Assert.That(prefab.GetComponent(), Is.Not.Null); - Assert.That(prefab.GetComponent().PlayOnEnable, Is.False); - Assert.That(prefab.GetComponent().Graph, Is.Null); - - var guid = AssetDatabase.AssetPathToGUID(PrefabPath); - var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid); - Assert.That(entry, Is.Not.Null); - Assert.That(entry.address, Is.EqualTo(ConstRef.FrameAnimationActorPrefabName)); - } - - [Test] - public void CaptureEntry_ReusesActorSnapshotFields() - { - var prefab = AssetDatabase.LoadAssetAtPath(PrefabPath); - var actorObject = Object.Instantiate(prefab); - var slotObject = new GameObject("Slot"); - try - { - var slot = slotObject.AddComponent(); - slot.slotName = "test-slot"; - slot.sortingLayer = 0; - slot.sortingOrder = 17; - var actor = actorObject.GetComponent(); - actor.Init("TestActor", slot, ActorType.FrameAnimation); - actor.Show(); - - ActorEntrySnapshotDto entry = actor.CaptureEntry(); - - Assert.That(entry.actorName, Is.EqualTo("TestActor")); - Assert.That(entry.slotName, Is.EqualTo("test-slot")); - Assert.That(entry.actorType, Is.EqualTo(nameof(ActorType.FrameAnimation))); - Assert.That(entry.alpha, Is.EqualTo(1f)); - Assert.That(entry.stateName, Is.Empty); - } - finally - { - Object.DestroyImmediate(actorObject); - Object.DestroyImmediate(slotObject); - } - } - } -} diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs deleted file mode 100644 index ee9f1087a..000000000 --- a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs +++ /dev/null @@ -1,534 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using AibisDream.Framework; -using AibisDream.MiniGame.Language; -using AibisDream.Utility; -using NUnit.Framework; -using UnityEditor; -using UnityEditor.Localization; -using UnityEngine; -using UnityEngine.Localization; -using UnityEngine.Localization.Settings; -using UnityEngine.Localization.Tables; -using UnityEngine.TestTools; - -namespace AibisDream.EditorTests.Huoshan -{ - public sealed class ExpressionLocalizationTests - { - private const string CatalogPath = - "Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset"; - private const string ParticleProfilePath = - "Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset"; - - [TestCase(null, false)] - [TestCase("", false)] - [TestCase("plain", false)] - [TestCase("L10N.key", false)] - [TestCase("l10n.key", true)] - public void LocalizedParamPrefix_IsStrict(string value, bool expected) - { - Assert.That(LocalizationKit.IsLocalizedParam(value), Is.EqualTo(expected)); - } - - [Test] - public void LocalizedParamKey_StripsOnlyKnownPrefix() - { - Assert.That(LocalizationKit.GetL10NParamKey("l10n.hs.exp.log1.target"), - Is.EqualTo("hs.exp.log1.target")); - Assert.That(LocalizationKit.GetL10NParamKey("raw"), Is.EqualTo("raw")); - } - - [Test] - public void MissingParamResult_RequiresMatchingLocalizedReference() - { - Assert.That( - LocalizationKit.IsMissingParamResult( - "l10n.hs.exp.pool", - "⟦hs.exp.pool⟧"), - Is.True); - Assert.That( - LocalizationKit.IsMissingParamResult( - "l10n.hs.exp.pool", - "⟦another.key⟧"), - Is.False); - Assert.That( - LocalizationKit.IsMissingParamResult( - "raw", - "⟦raw⟧"), - Is.False); - } - - [UnityTest] - public IEnumerator LocalizeParamAsync_UsesExplicitLocaleAndDoesNotFallback() - { - Locale chinese = LocalizationEditorSettings.GetLocale( - new LocaleIdentifier("zh-Hans")); - Locale english = LocalizationEditorSettings.GetLocale( - new LocaleIdentifier("en")); - Assert.That(chinese, Is.Not.Null); - Assert.That(english, Is.Not.Null); - - Locale previous = LocalizationSettings.SelectedLocale; - LocalizationSettings.SelectedLocale = english; - Task chineseTask = LocalizationKit.LocalizeParamAsync( - "l10n.hs.exp.log1.target", - chinese); - Task englishTask = LocalizationKit.LocalizeParamAsync( - "l10n.hs.exp.log1.target", - english); - - while (!chineseTask.IsCompleted || !englishTask.IsCompleted) - yield return null; - - LocalizationSettings.SelectedLocale = previous; - Assert.That(chineseTask.Result, Is.Not.Empty); - Assert.That(englishTask.Result, Is.Not.Empty); - Assert.That(chineseTask.Result, Is.Not.EqualTo(englishTask.Result)); - } - - [UnityTest] - public IEnumerator LocalizeParamAsync_ReturnsRawTextAndMissingKeyMarker() - { - Task rawTask = LocalizationKit.LocalizeParamAsync("raw text"); - while (!rawTask.IsCompleted) - yield return null; - Assert.That(rawTask.Result, Is.EqualTo("raw text")); - - Locale chinese = LocalizationEditorSettings.GetLocale( - new LocaleIdentifier("zh-Hans")); - LogAssert.Expect( - LogType.Error, - new Regex(@"Params 缺少条目.*Key=hs\.exp\.missing.*Locale=zh-Hans")); - Task missingTask = LocalizationKit.LocalizeParamAsync( - "l10n.hs.exp.missing", - chinese); - while (!missingTask.IsCompleted) - yield return null; - Assert.That(missingTask.Result, Is.EqualTo("⟦hs.exp.missing⟧")); - } - - [Test] - public void LegacyCommonParams_ArePresentInUnityParams() - { - StringTableCollection collection = - LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); - Assert.That(collection, Is.Not.Null); - - string[] legacyKeys = { "天空", "海报", "植物", "朋友", "情绪", "记忆", "逻辑" }; - foreach (string key in legacyKeys) - Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null, key); - - Assert.That(AssetDatabase.LoadAssetAtPath( - "Assets/StreamingAssets/Config/params.csv"), Is.Null); - } - - [Test] - public void ExpressionCatalog_IsValidAndCaseSensitive() - { - ExpressionContentCatalog catalog = - AssetDatabase.LoadAssetAtPath(CatalogPath); - Assert.That(catalog, Is.Not.Null); - Assert.That(catalog.GetValidationErrors(), Is.Empty); - Assert.That(catalog.Rounds, Is.Not.Empty); - - string firstRoundId = catalog.Rounds[0].Id; - Assert.That(catalog.TryGetRound(firstRoundId, out _), Is.True); - Assert.That( - catalog.TryGetRound(firstRoundId.ToUpperInvariant(), out _), - Is.False); - Assert.That(catalog.TryGetRound("missing", out _), Is.False); - } - - [Test] - public void ExpressionCatalog_RejectsDuplicateIdsAndNonLocalizedReferences() - { - ExpressionContentCatalog catalog = - ScriptableObject.CreateInstance(); - try - { - var first = new ExpressionRoundDefinition(); - var second = new ExpressionRoundDefinition(); - ConfigureRound(first, "log1", "l10n.target", "l10n.tokens", 1); - ConfigureRound(second, "log1", "raw", "l10n.tokens", 1); - SetPrivateField( - catalog, - "rounds", - new List { first, second }); - - string errors = string.Join("\n", catalog.GetValidationErrors()); - Assert.That(errors, Does.Contain("重复")); - Assert.That(errors, Does.Contain("l10n.*")); - } - finally - { - Object.DestroyImmediate(catalog); - } - } - - [Test] - public void TokenParser_TrimsDropsEmptyAndRejectsFullwidthSeparator() - { - Assert.That( - LanguageYarnCommand.TryParseExpressionTokens( - " 哈哈 | | 嘿嘿|呵呵 ", - out var tokens), - Is.True); - Assert.That(tokens, Is.EqualTo(new[] { "哈哈", "嘿嘿", "呵呵" })); - - LogAssert.Expect(LogType.Error, new Regex("全角分隔符")); - Assert.That( - LanguageYarnCommand.TryParseExpressionTokens("哈哈|嘿嘿", out _), - Is.False); - Assert.That( - LanguageYarnCommand.TryParseExpressionTokens(" | | ", out _), - Is.False); - } - - [Test] - public void CharacterPoolParser_PreservesUnicodeDuplicatesAndRejectsSeparators() - { - string combining = "e\u0301"; - Assert.That( - LanguageYarnCommand.TryParseExpressionPool( - $" 中 中 あ {combining} 😀。 ", - out List pool), - Is.True); - Assert.That( - pool, - Is.EqualTo(new[] { "中", "中", "あ", combining, "😀", "。" })); - Assert.That( - LanguageYarnCommand.TryParseExpressionPool("中|あ", out _), - Is.False); - Assert.That( - LanguageYarnCommand.TryParseExpressionPool("中|あ", out _), - Is.False); - Assert.That( - LanguageYarnCommand.TryParseExpressionPool(" \t ", out _), - Is.False); - } - - [Test] - public void ParticleLanguageProfile_ResolvesExactPrefixAndFallbackModes() - { - ExpressionParticleLanguageProfile profile = - AssetDatabase.LoadAssetAtPath( - ParticleProfilePath); - Assert.That(profile, Is.Not.Null); - Assert.That(profile.GetValidationErrors(), Is.Empty); - - Assert.That( - profile.Resolve(new LocaleIdentifier("zh-Hans")).UnitMode, - Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); - Assert.That( - profile.Resolve(new LocaleIdentifier("ja-JP")).UnitMode, - Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); - - foreach (string localeCode in new[] { "en", "es", "ru", "pt-BR" }) - { - Assert.That( - profile.Resolve(new LocaleIdentifier(localeCode)).UnitMode, - Is.EqualTo(ExpressionParticleUnitMode.Word), - localeCode); - } - - Assert.That( - profile.Resolve(new LocaleIdentifier("en-US")).LocaleCode, - Is.EqualTo("en")); - - LogAssert.Expect( - LogType.Warning, - new Regex(@"Locale 'zz-ZZ'.*Grapheme")); - Assert.That( - profile.Resolve(new LocaleIdentifier("zz-ZZ")).UnitMode, - Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); - } - - [Test] - public void WordTokenizer_UsesWhitespaceBoundariesAndKeepsPunctuation() - { - Assert.That( - ExpressionTextTokenizer.TokenizeText( - "I am not a joke", - ExpressionParticleUnitMode.Word), - Is.EqualTo(new[] { "I", "am", "not", "a", "joke" })); - Assert.That( - ExpressionTextTokenizer.TokenizeText( - "don't self-doubt ¿Por qué? веришь?", - ExpressionParticleUnitMode.Word), - Is.EqualTo(new[] - { - "don't", - "self-doubt", - "¿Por", - "qué?", - "веришь?" - })); - Assert.That( - ExpressionTextTokenizer.TokenizeText( - "one \t\r\n two", - ExpressionParticleUnitMode.Word), - Is.EqualTo(new[] { "one", "two" })); - } - - [Test] - public void GraphemeTokenizer_PreservesCjkKanaAndCombiningCharacters() - { - const string combining = "e\u0301"; - Assert.That( - ExpressionTextTokenizer.TokenizeText( - $"汉 あ {combining}", - ExpressionParticleUnitMode.Grapheme), - Is.EqualTo(new[] { "汉", "あ", combining })); - } - - [Test] - public void WordPool_PreservesDuplicateWeightsAndRejectsPipeSeparators() - { - Assert.That( - LanguageYarnCommand.TryParseExpressionPool( - "noise doubt noise", - ExpressionParticleUnitMode.Word, - out List pool), - Is.True); - Assert.That(pool, Is.EqualTo(new[] { "noise", "doubt", "noise" })); - Assert.That( - LanguageYarnCommand.TryParseExpressionPool( - "noise|doubt", - ExpressionParticleUnitMode.Word, - out _), - Is.False); - Assert.That( - LanguageYarnCommand.TryParseExpressionPool( - "noise|doubt", - ExpressionParticleUnitMode.Word, - out _), - Is.False); - } - - [Test] - public void RoundSnapshot_IsParsedOnceAndKeepsItsLocaleAndWordUnits() - { - ExpressionParticleLanguageProfile languageProfile = - AssetDatabase.LoadAssetAtPath( - ParticleProfilePath); - ExpressionParticleLocaleSettings english = - languageProfile.Resolve(new LocaleIdentifier("en")); - Assert.That( - ExpressionRoundTextSnapshot.TryCreate( - new LocaleIdentifier("en"), - english, - "I am not a joke", - new[] { "false alarm", "false alarm" }, - "noise doubt noise", - out ExpressionRoundTextSnapshot snapshot, - out string error), - Is.True, - error); - - Locale previous = LocalizationSettings.SelectedLocale; - try - { - LocalizationSettings.SelectedLocale = - LocalizationEditorSettings.GetLocale( - new LocaleIdentifier("zh-Hans")); - - Assert.That(snapshot.Locale.Code, Is.EqualTo("en")); - Assert.That( - snapshot.TargetUnits, - Is.EqualTo(new[] { "I", "am", "not", "a", "joke" })); - Assert.That( - snapshot.InterferencePoolUnits, - Is.EqualTo(new[] { "false", "alarm", "false", "alarm" })); - Assert.That( - snapshot.DefaultPoolUnits, - Is.EqualTo(new[] { "noise", "doubt", "noise" })); - } - finally - { - LocalizationSettings.SelectedLocale = previous; - } - } - - [Test] - public void WordProfile_ScalesNonTargetCountByConfiguredScale() - { - var settings = ExpressionParticleLocaleSettings.CreateFallback(); - SetPrivateField(settings, "nonTargetCountScale", 0.625f); - - Assert.That(settings.ScaleNonTargetCount(16), Is.EqualTo(10)); - Assert.That(settings.ScaleNonTargetCount(-1), Is.EqualTo(-1)); - Assert.That(settings.ScaleNonTargetCount(0), Is.EqualTo(0)); - } - - [Test] - public void ParticleGeometry_UsesVisualEdgeDistanceAndShortestSeparationAxis() - { - var left = new Bounds(Vector3.zero, new Vector3(4f, 1f, 0.01f)); - var right = new Bounds( - new Vector3(4.15f, 0f, 0f), - new Vector3(4f, 1f, 0.01f)); - - Assert.That( - ExpressionParticleGeometry.BoundsDistance(left, right), - Is.EqualTo(0.15f).Within(0.0001f)); - - right.center = new Vector3(3.8f, 0f, 0f); - Assert.That( - ExpressionParticleGeometry.TryGetSeparation( - left, - right, - 0.1f, - out Vector2 direction, - out float overlap), - Is.True); - Assert.That(direction, Is.EqualTo(Vector2.left)); - Assert.That(overlap, Is.EqualTo(0.3f).Within(0.0001f)); - } - - [Test] - public void TextParticle_VisualBoundsIncludesTheWholeWordForPointerDistance() - { - var gameObject = new GameObject("TextParticleBoundsTest"); - try - { - CandidateParticle particle = - gameObject.AddComponent(); - particle.ForceSetUnitText("self-doubt"); - Bounds bounds = particle.GetVisualWorldBounds(); - - Assert.That(bounds.size.x, Is.GreaterThan(0f)); - Assert.That( - particle.DistanceToVisualBounds( - new Vector2(bounds.min.x, bounds.center.y)), - Is.EqualTo(0f).Within(0.0001f)); - Assert.That( - particle.DistanceToVisualBounds( - new Vector2(bounds.max.x, bounds.center.y)), - Is.EqualTo(0f).Within(0.0001f)); - } - finally - { - Object.DestroyImmediate(gameObject); - } - } - - [Test] - public void TextParticle_UsesConfiguredRoleAndOverridePoolsWithoutFirstFrameFallback() - { - var gameObject = new GameObject("TextParticlePoolTest"); - try - { - CandidateParticle particle = gameObject.AddComponent(); - Assert.That(particle.CurrentUnitText, Is.Null.Or.Empty); - - string combining = "e\u0301"; - particle.SetUnitPools( - new[] { "😀" }, - new[] { combining }); - - particle.originalIsRed = true; - particle.InitializeRandomUnit(); - Assert.That(particle.CurrentUnitText, Is.EqualTo("😀")); - - particle.originalIsRed = false; - particle.InitializeRandomUnit(); - Assert.That(particle.CurrentUnitText, Is.EqualTo(combining)); - - particle.SetOverrideUnitPool(new[] { "。" }); - particle.InitializeRandomUnit(); - Assert.That(particle.CurrentUnitText, Is.EqualTo("。")); - - particle.SetOverrideUnitPool((IReadOnlyList)null); - particle.InitializeRandomUnit(); - Assert.That(particle.CurrentUnitText, Is.EqualTo(combining)); - } - finally - { - Object.DestroyImmediate(gameObject); - } - } - - [Test] - public void UnicodeTokenizer_PreservesTextElementsAndDropsWhitespace() - { - string combining = "e\u0301"; - var elements = - ExpressionTextTokenizer.GetVisibleElements($"中 A あ {combining} 😀。"); - - Assert.That( - elements, - Is.EqualTo(new[] { "中", "A", "あ", combining, "😀", "。" })); - } - - [Test] - public void UnicodeLayout_PreservesWordGapWithoutWhitespaceParticle() - { - ExpressionTextTokenizer.Layout layout = - ExpressionTextTokenizer.BuildLayout("A B", 1f, 0.6f); - - Assert.That(layout.VisibleElements, Is.EqualTo(new[] { "A", "B" })); - Assert.That(layout.Offsets[0], Is.EqualTo(-0.8f).Within(0.0001f)); - Assert.That(layout.Offsets[1], Is.EqualTo(0.8f).Within(0.0001f)); - } - - [Test] - public void UnicodeSequenceMatch_UsesWholeTextElements() - { - var source = ExpressionTextTokenizer.GetVisibleElements("A😀e\u0301。"); - var fragment = ExpressionTextTokenizer.GetVisibleElements("😀e\u0301"); - Assert.That(ExpressionTextTokenizer.FindVisibleSequence(source, fragment), - Is.EqualTo(1)); - Assert.That( - ExpressionTextTokenizer.FindVisibleSequence( - source, - ExpressionTextTokenizer.GetVisibleElements("😀x")), - Is.EqualTo(-1)); - } - - [Test] - public void ExpressContentValidation_HasNoBlockingErrors() - { - var issues = ExpressionLocalizationValidator.Validate(); - string errors = string.Join( - "\n", - issues - .Where(issue => - issue.Severity == ExpressionLocalizationIssueSeverity.Error) - .Select(issue => issue.Message)); - - Assert.That(errors, Is.Empty); - Assert.That( - issues.Any(issue => - issue.Severity == ExpressionLocalizationIssueSeverity.Warning), - Is.True, - "未翻译 Locale 的空条目应作为待翻译警告保留。"); - } - - private static void ConfigureRound( - ExpressionRoundDefinition round, - string id, - string target, - string tokens, - int nonTargetCount) - { - SetPrivateField(round, "id", id); - SetPrivateField(round, "targetReference", target); - SetPrivateField(round, "tokenReference", tokens); - SetPrivateField(round, "nonTargetParticleCount", nonTargetCount); - } - - private static void SetPrivateField(object target, string fieldName, object value) - { - FieldInfo field = target.GetType().GetField( - fieldName, - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.That(field, Is.Not.Null, fieldName); - field.SetValue(target, value); - } - } -} diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta deleted file mode 100644 index b5f63475d..000000000 --- a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d6b8797332ac4876b66e381ea3c98723 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs deleted file mode 100644 index bf8fea707..000000000 --- a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections; -using AibisDream.MiniGame.Language; -using NUnit.Framework; -using UnityEngine; - -namespace AibisDream.EditorTests.Huoshan -{ - public sealed class LogReleasePresentationTests - { - [Test] - public void LineClip_RejectsOutsideSegment() - { - Bounds bounds = new Bounds(Vector3.zero, new Vector3(2f, 2f, 0f)); - bool visible = ConnectionRenderer.TryClipLineToBounds( - new Vector3(-3f, 2f), - new Vector3(3f, 2f), - bounds, - out _, - out _); - - Assert.That(visible, Is.False); - } - - [TestCase(-0.5f, 0f, 0.5f, 0f, -0.5f, 0f, 0.5f, 0f)] - [TestCase(-2f, 0f, 2f, 0f, -1f, 0f, 1f, 0f)] - [TestCase(0f, -2f, 0f, 0.5f, 0f, -1f, 0f, 0.5f)] - [TestCase(-2f, -2f, 2f, 2f, -1f, -1f, 1f, 1f)] - public void LineClip_ReturnsExpectedInteriorSegment( - float ax, - float ay, - float bx, - float by, - float expectedAx, - float expectedAy, - float expectedBx, - float expectedBy) - { - Bounds bounds = new Bounds(Vector3.zero, new Vector3(2f, 2f, 0f)); - bool visible = ConnectionRenderer.TryClipLineToBounds( - new Vector3(ax, ay), - new Vector3(bx, by), - bounds, - out Vector3 clippedA, - out Vector3 clippedB); - - Assert.That(visible, Is.True); - Assert.That(clippedA.x, Is.EqualTo(expectedAx).Within(0.0001f)); - Assert.That(clippedA.y, Is.EqualTo(expectedAy).Within(0.0001f)); - Assert.That(clippedB.x, Is.EqualTo(expectedBx).Within(0.0001f)); - Assert.That(clippedB.y, Is.EqualTo(expectedBy).Within(0.0001f)); - } - - [TestCase(CompletionPhase.None, false)] - [TestCase(CompletionPhase.Completed, false)] - [TestCase(CompletionPhase.Focusing, true)] - [TestCase(CompletionPhase.FocusHolding, true)] - [TestCase(CompletionPhase.Finished, true)] - public void ConnectionClipping_OnlyStartsAfterOutput( - CompletionPhase phase, - bool expected) - { - Assert.That(LanguageParticleManager.ShouldClipConnections(phase), Is.EqualTo(expected)); - } - - [Test] - public void LieEdgePosition_LandsOnScreenPerimeterAndStaysInside() - { - Bounds bounds = new Bounds(Vector3.zero, new Vector3(6f, 4f, 0f)); - const float inset = 0.1f; - for (int i = 0; i < 8; i++) - { - Vector3 position = LanguageParticleManager.CalculateScreenEdgePosition( - bounds, - i, - 8, - inset); - - Assert.That(position.x, Is.InRange(bounds.min.x, bounds.max.x)); - Assert.That(position.y, Is.InRange(bounds.min.y, bounds.max.y)); - bool touchesVerticalEdge = - Mathf.Abs(Mathf.Abs(position.x) - (bounds.extents.x - inset)) < 0.0001f; - bool touchesHorizontalEdge = - Mathf.Abs(Mathf.Abs(position.y) - (bounds.extents.y - inset)) < 0.0001f; - Assert.That(touchesVerticalEdge || touchesHorizontalEdge, Is.True); - } - } - - [TestCase(6, 0.70f)] - [TestCase(6, 0.90f)] - [TestCase(6, 1.10f)] - public void ResolveTiming_LastCharacterCompletesWithinPreset(int characterCount, float totalDuration) - { - LanguageParticleManager.CalculateResolveTiming( - characterCount, - totalDuration, - 3.5f, - 0.4f, - out float characterDuration, - out float stagger); - - float lastCompletion = characterDuration + stagger * (characterCount - 1); - Assert.That(lastCompletion, Is.LessThanOrEqualTo(totalDuration + 0.0001f)); - Assert.That(lastCompletion, Is.EqualTo(totalDuration).Within(0.0001f)); - } - - [Test] - public void TmpClipRect_ConvertsWorldCornersIntoTextLocalSpace() - { - GameObject rectObject = new GameObject("ScreenRect", typeof(RectTransform)); - GameObject textObject = new GameObject("TextTransform"); - try - { - RectTransform rect = rectObject.GetComponent(); - rect.sizeDelta = new Vector2(4f, 2f); - rect.position = new Vector3(2f, -1f, 0f); - rect.rotation = Quaternion.Euler(0f, 0f, 17f); - rect.localScale = new Vector3(1.2f, 0.8f, 1f); - - textObject.transform.position = new Vector3(-0.5f, 0.75f, 0f); - textObject.transform.rotation = Quaternion.Euler(0f, 0f, -11f); - textObject.transform.localScale = new Vector3(1.4f, 0.65f, 1f); - - Vector4 actual = TMPRectClipper.CalculateLocalClipRect(textObject.transform, rect); - Vector3[] corners = new Vector3[4]; - rect.GetWorldCorners(corners); - Vector3 first = textObject.transform.InverseTransformPoint(corners[0]); - float minX = first.x; - float minY = first.y; - float maxX = first.x; - float maxY = first.y; - for (int i = 1; i < corners.Length; i++) - { - Vector3 local = textObject.transform.InverseTransformPoint(corners[i]); - minX = Mathf.Min(minX, local.x); - minY = Mathf.Min(minY, local.y); - maxX = Mathf.Max(maxX, local.x); - maxY = Mathf.Max(maxY, local.y); - } - - Assert.That(actual.x, Is.EqualTo(minX).Within(0.0001f)); - Assert.That(actual.y, Is.EqualTo(minY).Within(0.0001f)); - Assert.That(actual.z, Is.EqualTo(maxX).Within(0.0001f)); - Assert.That(actual.w, Is.EqualTo(maxY).Within(0.0001f)); - } - finally - { - UnityEngine.Object.DestroyImmediate(rectObject); - UnityEngine.Object.DestroyImmediate(textObject); - } - } - - [TestCase(null, null)] - [TestCase("", null)] - [TestCase(" | ", null)] - [TestCase("没问题|冇", "没问题冇")] - [TestCase("没 问 题", "没问题")] - public void SlotMachinePool_StripsSeparatorsAndWhitespace(string input, string expected) - { - Assert.That(LanguageParticleManager.SanitizeSlotMachinePool(input), Is.EqualTo(expected)); - } - - [Test] - public void ActorPhraseList_PreservesWordsAndDropsEmptyEntries() - { - string[] phrases = LogReleasePresentationController.ParseActorPhrases( - " 没问题 | | 冇问题|帽问题 "); - - Assert.That(phrases, Is.EqualTo(new[] { "没问题", "冇问题", "帽问题" })); - } - - [Test] - public void ActorFlash_OutsideMemoryState_CompletesImmediatelyWithoutStateChange() - { - GameObject host = new GameObject("PresentationHost"); - try - { - var controller = host.AddComponent(); - IEnumerator routine = controller.FlashActorLie("没问题", 0.6f); - - Assert.That(routine.MoveNext(), Is.False); - Assert.That( - controller.State, - Is.EqualTo(LogReleasePresentationController.PresentationState.Idle)); - } - finally - { - UnityEngine.Object.DestroyImmediate(host); - } - } - } -} diff --git a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta deleted file mode 100644 index fffd6d2d1..000000000 --- a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5f75b14634314395a3a67721065f86bd -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/LocalizationExpansionTests.cs b/Assets/Editor/LocalizationExpansionTests.cs deleted file mode 100644 index cb73417e9..000000000 --- a/Assets/Editor/LocalizationExpansionTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using AibisDream.Framework; -using AibisDream.Kit; -using AibisDream.Utility; -using NUnit.Framework; -using UnityEditor; -using UnityEngine; -using YarnProjectDefinition = Yarn.Compiler.Project; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class LocalizationExpansionTests - { - private const string FirstRuntimeChapterPath = - "Assets/ScriptableObjects/SceneSO/Demo/Day0_Prologue.asset"; - - private static readonly string[] AddedLocaleCodes = { "ru", "es", "pt-BR" }; - - private static readonly MethodInfo GetLocalizedNameMethod = - typeof(CharacterVo).GetMethod( - "GetLocalizedNameByKind", - BindingFlags.Instance | BindingFlags.NonPublic); - - private static readonly MethodInfo GetCharacterVoByKeyMethod = - typeof(YarnUtil).GetMethod( - "GetCharacterVoByKey", - BindingFlags.Static | BindingFlags.NonPublic); - - [TestCase("ru", LocaleKind.Ru)] - [TestCase("ru-RU", LocaleKind.Ru)] - [TestCase("es", LocaleKind.Es)] - [TestCase("es-ES", LocaleKind.Es)] - [TestCase("pt", LocaleKind.PtBr)] - [TestCase("pt-BR", LocaleKind.PtBr)] - public void GetLocaleKind_AddedLocaleCode_ReturnsExpectedKind( - string localeCode, - LocaleKind expected) - { - Assert.That(LocalizationKit.GetLocaleKind(localeCode), Is.EqualTo(expected)); - } - - [TestCase(SystemLanguage.Russian, "ru")] - [TestCase(SystemLanguage.Spanish, "es")] - [TestCase(SystemLanguage.Portuguese, "pt-BR")] - public void MapSystemLanguage_AddedSystemLanguage_ReturnsSupportedLocale( - SystemLanguage systemLanguage, - string expected) - { - Assert.That(LocalizationKit.MapSystemLanguage(systemLanguage), Is.EqualTo(expected)); - } - - [Test] - public void CharacterVo_LocalizedName_ReturnsOnlySelectedLocaleContent() - { - Assert.That(GetLocalizedNameMethod, Is.Not.Null); - - var translated = new CharacterVo - { - key = "actor_key", - ru = "Русское имя", - es = "Nombre español", - ptBr = "Nome português", - }; - - Assert.That(GetLocalizedName(translated, LocaleKind.Ru), Is.EqualTo(translated.ru)); - Assert.That(GetLocalizedName(translated, LocaleKind.Es), Is.EqualTo(translated.es)); - Assert.That(GetLocalizedName(translated, LocaleKind.PtBr), Is.EqualTo(translated.ptBr)); - - var untranslated = new CharacterVo { key = "actor_key" }; - Assert.That(GetLocalizedName(untranslated, LocaleKind.Cn), Is.Null); - Assert.That(GetLocalizedName(untranslated, LocaleKind.En), Is.Null); - Assert.That(GetLocalizedName(untranslated, LocaleKind.Ja), Is.Null); - Assert.That(GetLocalizedName(untranslated, LocaleKind.Ru), Is.Null); - Assert.That(GetLocalizedName(untranslated, LocaleKind.Es), Is.Null); - Assert.That(GetLocalizedName(untranslated, LocaleKind.PtBr), Is.Null); - } - - [Test] - public void CharacterCsv_AddedLocaleColumns_ParseSuccessfully() - { - string csvPath = Path.Combine( - Application.streamingAssetsPath, - "Config", - "character.csv"); - - List characters = CsvUtil.ReadAsBean(csvPath); - - Assert.That(characters, Is.Not.Empty); - Character peipei = characters.Single(character => character.Key == "peipei"); - Assert.That(peipei.Ru, Is.Empty); - Assert.That(peipei.Es, Is.Empty); - Assert.That(peipei.PtBr, Is.Empty); - } - - [Test] - public void RuntimeYarnProjects_DeclareAddedLocales() - { - TalkSceneSO firstChapter = - AssetDatabase.LoadAssetAtPath(FirstRuntimeChapterPath); - Assert.That(firstChapter, Is.Not.Null); - - var pending = new Queue(); - var visited = new HashSet(); - pending.Enqueue(firstChapter); - - while (pending.Count > 0) - { - TalkSceneSO chapter = pending.Dequeue(); - if (chapter == null || !visited.Add(chapter)) - continue; - - Assert.That(chapter.yarnProject, Is.Not.Null, chapter.name); - AssertYarnProjectDeclaresAddedLocales(chapter); - - if (chapter.exits == null) - continue; - - foreach (SceneExit sceneExit in chapter.exits) - { - if (sceneExit?.targetScene != null) - pending.Enqueue(sceneExit.targetScene); - } - } - - Assert.That(visited, Is.Not.Empty); - } - - private static string GetLocalizedName(CharacterVo character, LocaleKind localeKind) - { - return (string)GetLocalizedNameMethod.Invoke(character, new object[] { localeKind }); - } - - private static void AssertYarnProjectDeclaresAddedLocales(TalkSceneSO chapter) - { - string assetPath = AssetDatabase.GetAssetPath(chapter.yarnProject); - string projectRoot = Directory.GetParent(Application.dataPath).FullName; - string absolutePath = Path.GetFullPath(Path.Combine(projectRoot, assetPath)); - YarnProjectDefinition project = YarnProjectDefinition.LoadFromFile(absolutePath); - - foreach (string localeCode in AddedLocaleCodes) - { - Assert.That( - project.Localisation.ContainsKey(localeCode), - Is.True, - $"{chapter.name} ({assetPath}) does not declare Locale {localeCode}."); - } - } - } -} diff --git a/Assets/Editor/PresentationSnapshotContractTests.cs b/Assets/Editor/PresentationSnapshotContractTests.cs deleted file mode 100644 index 74f8ac368..000000000 --- a/Assets/Editor/PresentationSnapshotContractTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using AibisDream.SaveSystem; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class PresentationSnapshotContractTests - { - [Test] - public void PresentationProviders_UseStableIdsAndRestoreInDependencyOrder() - { - var showcase = new ShowcaseSnapshotProvider(); - var day2Sleep = new Day2SleepPresentationSnapshotProvider(); - var playTool = new PlayToolSnapshotProvider(); - var screen = new ScreenSnapshotProvider(); - - Assert.That(showcase.SaveId, Is.EqualTo("showcase")); - Assert.That(day2Sleep.SaveId, Is.EqualTo("day2SleepPresentation")); - Assert.That(playTool.SaveId, Is.EqualTo("playTool")); - Assert.That(showcase.RestoreOrder, Is.LessThan(day2Sleep.RestoreOrder)); - Assert.That(day2Sleep.RestoreOrder, Is.LessThan(playTool.RestoreOrder)); - Assert.That(playTool.RestoreOrder, Is.LessThan(screen.RestoreOrder)); - } - - [Test] - public void PlayToolSection_RoundTripsThroughSnapshotJson() - { - var source = new SaveSnapshot(); - source.sections[SnapshotProviderIds.PlayTool] = new PlayToolSnapshotDto - { - isObjVisible = true, - objPicName = "证物", - isFullScreenVisible = true, - fullScreenPicName = "教室" - }; - - var json = JsonConvert.SerializeObject(source); - var restored = JsonConvert.DeserializeObject(json); - var playTool = ((JObject)restored.sections[SnapshotProviderIds.PlayTool]) - .ToObject(); - - Assert.That(playTool.isObjVisible, Is.True); - Assert.That(playTool.objPicName, Is.EqualTo("证物")); - Assert.That(playTool.isFullScreenVisible, Is.True); - Assert.That(playTool.fullScreenPicName, Is.EqualTo("教室")); - Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion)); - } - - [Test] - public void ShowcaseAndDay2SleepSections_RoundTripSemanticState() - { - var source = new SaveSnapshot(); - source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto - { - displayMode = ShowcaseDisplayMode.Large.ToString(), - picName = "D2S星图", - isBackgroundVisible = true - }; - source.sections[SnapshotProviderIds.Day2SleepPresentation] = - new Day2SleepPresentationSnapshotDto - { - mode = Day2SleepPresentationMode.LargeEffects.ToString(), - largeBlurAmount = 0.72f, - largeBlurSize = 0.015f, - largeScaleMultiplier = 1.1f, - largeAlpha = 0.4f - }; - - var json = JsonConvert.SerializeObject(source); - var restored = JsonConvert.DeserializeObject(json); - var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase]) - .ToObject(); - var day2Sleep = ((JObject)restored.sections[SnapshotProviderIds.Day2SleepPresentation]) - .ToObject(); - - Assert.That(showcase.displayMode, Is.EqualTo("Large")); - Assert.That(showcase.picName, Is.EqualTo("D2S星图")); - Assert.That(showcase.isBackgroundVisible, Is.True); - Assert.That(day2Sleep.mode, Is.EqualTo("LargeEffects")); - Assert.That(day2Sleep.largeBlurAmount, Is.EqualTo(0.72f)); - Assert.That(day2Sleep.largeScaleMultiplier, Is.EqualTo(1.1f)); - Assert.That(day2Sleep.largeAlpha, Is.EqualTo(0.4f)); - } - - [Test] - public void OldSnapshotWithoutPresentationSections_DeserializesWithoutSynthesizingSections() - { - var restored = JsonConvert.DeserializeObject( - "{\"schemaVersion\":1,\"sections\":{}}"); - - Assert.That(restored.schemaVersion, Is.EqualTo(1)); - Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Showcase), Is.False); - Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Day2SleepPresentation), Is.False); - Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.PlayTool), Is.False); - } - } -} diff --git a/Assets/Editor/Tests.meta b/Assets/Editor/Tests.meta new file mode 100644 index 000000000..e4f2cc763 --- /dev/null +++ b/Assets/Editor/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d6e75cb49ad84a8ebe0520a529e7a67e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Tests/ActorPrefabContractTests.cs b/Assets/Editor/Tests/ActorPrefabContractTests.cs new file mode 100644 index 000000000..6fc6e1bed --- /dev/null +++ b/Assets/Editor/Tests/ActorPrefabContractTests.cs @@ -0,0 +1,50 @@ +using AibisDream.FrameAnimation; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.AddressableAssets; +using UnityEngine; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class ActorPrefabContractTests + { + private const string PrefabPath = + "Assets/Prefabs/FixSystemPrefabs/FrameAnimationActor.prefab"; + + [Test] + public void FrameAnimationActor_PrefabAndSnapshotContractAreConsistent() + { + var prefab = AssetDatabase.LoadAssetAtPath(PrefabPath); + Assert.That(prefab, Is.Not.Null); + Assert.That(prefab.GetComponent(), Is.Not.Null); + Assert.That(prefab.GetComponent(), Is.Not.Null); + Assert.That(prefab.GetComponent(), Is.Not.Null); + + var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry( + AssetDatabase.AssetPathToGUID(PrefabPath)); + Assert.That(entry, Is.Not.Null); + Assert.That(entry.address, Is.EqualTo(ConstRef.FrameAnimationActorPrefabName)); + + var actorObject = Object.Instantiate(prefab); + var slotObject = new GameObject("Actor Slot"); + try + { + var slot = slotObject.AddComponent(); + slot.slotName = "test-slot"; + var actor = actorObject.GetComponent(); + actor.Init("TestActor", slot, ActorType.FrameAnimation); + var snapshot = actor.CaptureEntry(); + + Assert.That(snapshot.actorName, Is.EqualTo("TestActor")); + Assert.That(snapshot.slotName, Is.EqualTo("test-slot")); + Assert.That(snapshot.actorType, Is.EqualTo(nameof(ActorType.FrameAnimation))); + } + finally + { + Object.DestroyImmediate(actorObject); + Object.DestroyImmediate(slotObject); + } + } + } +} diff --git a/Assets/Editor/FrameAnimationActorIntegrationTests.cs.meta b/Assets/Editor/Tests/ActorPrefabContractTests.cs.meta similarity index 76% rename from Assets/Editor/FrameAnimationActorIntegrationTests.cs.meta rename to Assets/Editor/Tests/ActorPrefabContractTests.cs.meta index 36c1bed20..4548f4db4 100644 --- a/Assets/Editor/FrameAnimationActorIntegrationTests.cs.meta +++ b/Assets/Editor/Tests/ActorPrefabContractTests.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Tests/AudioSnapshotContractTests.cs b/Assets/Editor/Tests/AudioSnapshotContractTests.cs new file mode 100644 index 000000000..9ae04e253 --- /dev/null +++ b/Assets/Editor/Tests/AudioSnapshotContractTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq; +using AibisDream.Kit; +using AibisDream.SaveSystem; +using FMOD; +using FMOD.Studio; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class AudioSnapshotContractTests + { + [Test] + public void Registry_CapturesFinalValuesOnlyForSuccessfulParameters() + { + var api = new FakeFmodGlobalParameterApi(); + api.Reads["stage"] = new ParameterRead(9f, 2.37f, RESULT.OK); + api.SetResults["missing"] = RESULT.ERR_EVENT_NOTFOUND; + var registry = new YarnGlobalParameterRegistry(api); + + registry.SetFromYarn("stage", 9f); + registry.SetFromYarn("STAGE", 10f); + registry.SetFromYarn("missing", 1f); + var snapshot = registry.CaptureFinalValues(_ => { }); + + Assert.That(registry.TrackedNames, Is.EqualTo(new[] { "stage" })); + Assert.That(snapshot["stage"], Is.EqualTo(2.37f)); + Assert.That(api.SetCalls.All(call => !call.IgnoreSeekSpeed), Is.True); + } + + [Test] + public void Registry_RestoreJumpsImmediatelyThenYarnUsesSeekSpeed() + { + var api = new FakeFmodGlobalParameterApi(); + var registry = new YarnGlobalParameterRegistry(api); + + registry.RestoreFinalValues( + new Dictionary { ["stage"] = 2.37f }, + _ => { }); + registry.SetFromYarn("stage", 3f); + + Assert.That(api.SetCalls, Has.Count.EqualTo(2)); + Assert.That(api.SetCalls[0].Value, Is.EqualTo(2.37f)); + Assert.That(api.SetCalls[0].IgnoreSeekSpeed, Is.True); + Assert.That(api.SetCalls[1].Value, Is.EqualTo(3f)); + Assert.That(api.SetCalls[1].IgnoreSeekSpeed, Is.False); + } + + [Test] + public void AudioSnapshot_RoundTripsWithoutChangingSchema() + { + var source = new SaveSnapshot(); + source.sections[SnapshotProviderIds.Audio] = new AudioSnapshotDto + { + ambState = "Dream", + yarnGlobalParameters = new Dictionary + { + ["reverb"] = 0.42f, + ["hs1LogStage"] = 2.37f + } + }; + + var restored = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(source)); + var audio = ((JObject)restored.sections[SnapshotProviderIds.Audio]) + .ToObject(); + + Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion)); + Assert.That(audio.yarnGlobalParameters["reverb"], Is.EqualTo(0.42f)); + Assert.That(audio.yarnGlobalParameters["hs1LogStage"], Is.EqualTo(2.37f)); + } + + private readonly struct SetCall + { + internal SetCall(float value, bool ignoreSeekSpeed) + { + Value = value; + IgnoreSeekSpeed = ignoreSeekSpeed; + } + + internal float Value { get; } + internal bool IgnoreSeekSpeed { get; } + } + + private readonly struct ParameterRead + { + internal ParameterRead(float value, float finalValue, RESULT result) + { + Value = value; + FinalValue = finalValue; + Result = result; + } + + internal float Value { get; } + internal float FinalValue { get; } + internal RESULT Result { get; } + } + + private sealed class FakeFmodGlobalParameterApi : IFmodGlobalParameterApi + { + internal readonly Dictionary SetResults = + new Dictionary(System.StringComparer.OrdinalIgnoreCase); + internal readonly Dictionary Reads = + new Dictionary(System.StringComparer.OrdinalIgnoreCase); + internal readonly List SetCalls = new List(); + + public RESULT SetParameterByName( + string name, + float value, + bool ignoreSeekSpeed) + { + SetCalls.Add(new SetCall(value, ignoreSeekSpeed)); + return SetResults.TryGetValue(name, out var result) ? result : RESULT.OK; + } + + public RESULT GetParameterByName( + string name, + out float value, + out float finalValue) + { + if (Reads.TryGetValue(name, out var read)) + { + value = read.Value; + finalValue = read.FinalValue; + return read.Result; + } + + value = 0f; + finalValue = 0f; + return RESULT.ERR_EVENT_NOTFOUND; + } + + public RESULT GetParameterDescriptionByName( + string name, + out PARAMETER_DESCRIPTION description) + { + description = default; + return RESULT.ERR_EVENT_NOTFOUND; + } + + public RESULT SetParameterById( + PARAMETER_ID id, + float value, + bool ignoreSeekSpeed) + { + return RESULT.OK; + } + } + } +} diff --git a/Assets/Editor/AudioGlobalParameterSnapshotTests.cs.meta b/Assets/Editor/Tests/AudioSnapshotContractTests.cs.meta similarity index 100% rename from Assets/Editor/AudioGlobalParameterSnapshotTests.cs.meta rename to Assets/Editor/Tests/AudioSnapshotContractTests.cs.meta diff --git a/Assets/Editor/Tests/LocalizationContractTests.cs b/Assets/Editor/Tests/LocalizationContractTests.cs new file mode 100644 index 000000000..b6e327e2f --- /dev/null +++ b/Assets/Editor/Tests/LocalizationContractTests.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using AibisDream.Framework; +using AibisDream.Kit; +using AibisDream.MiniGame.Language; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.Localization; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class LocalizationContractTests + { + private const string ParticleProfilePath = + "Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset"; + + [Test] + public void LocaleMappings_ResolveSupportedCodesAndSystemLanguages() + { + var localeCases = new Dictionary + { + ["ru"] = LocaleKind.Ru, + ["ru-RU"] = LocaleKind.Ru, + ["es"] = LocaleKind.Es, + ["es-ES"] = LocaleKind.Es, + ["pt"] = LocaleKind.PtBr, + ["pt-BR"] = LocaleKind.PtBr + }; + foreach (var pair in localeCases) + Assert.That(LocalizationKit.GetLocaleKind(pair.Key), Is.EqualTo(pair.Value), pair.Key); + + Assert.That(LocalizationKit.MapSystemLanguage(SystemLanguage.Russian), Is.EqualTo("ru")); + Assert.That(LocalizationKit.MapSystemLanguage(SystemLanguage.Spanish), Is.EqualTo("es")); + Assert.That(LocalizationKit.MapSystemLanguage(SystemLanguage.Portuguese), Is.EqualTo("pt-BR")); + } + + [Test] + public void UnknownCharacter_UsesItsKeyAsEveryLocalizedName() + { + var method = typeof(YarnUtil).GetMethod( + "GetCharacterVoByKey", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.That(method, Is.Not.Null); + + const string missingKey = "missing_character_contract_test"; + var character = (CharacterVo)method.Invoke(null, new object[] { missingKey }); + + Assert.That(character.key, Is.EqualTo(missingKey)); + Assert.That( + new[] + { + character.cn, + character.en, + character.ja, + character.ru, + character.es, + character.ptBr + }, + Has.All.EqualTo(missingKey)); + } + + [Test] + public void CharacterCsv_ParsesExpandedLocaleColumns() + { + var characters = CsvUtil.ReadAsBean(Path.Combine( + Application.streamingAssetsPath, + "Config", + "character.csv")); + + Assert.That(characters, Is.Not.Empty); + var peipei = characters.Single(character => character.Key == "peipei"); + Assert.That(peipei.Ru, Is.Not.Null); + Assert.That(peipei.Es, Is.Not.Null); + Assert.That(peipei.PtBr, Is.Not.Null); + } + + [Test] + public void ExpressionParsing_PreservesUnicodeAndWordBoundaries() + { + const string combining = "e\u0301"; + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + $"中 中 あ {combining} 😀。", + out List graphemePool), + Is.True); + Assert.That( + graphemePool, + Is.EqualTo(new[] { "中", "中", "あ", combining, "😀", "。" })); + + Assert.That( + ExpressionTextTokenizer.TokenizeText( + "don't self-doubt ¿Por qué? веришь?", + ExpressionParticleUnitMode.Word), + Is.EqualTo(new[] + { + "don't", + "self-doubt", + "¿Por", + "qué?", + "веришь?" + })); + } + + [Test] + public void ExpressionParticleProfile_MapsLanguageFamilies() + { + var profile = AssetDatabase.LoadAssetAtPath( + ParticleProfilePath); + Assert.That(profile, Is.Not.Null); + Assert.That(profile.GetValidationErrors(), Is.Empty); + + Assert.That( + profile.Resolve(new LocaleIdentifier("zh-Hans")).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); + Assert.That( + profile.Resolve(new LocaleIdentifier("ja-JP")).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); + foreach (var localeCode in new[] { "en", "es", "ru", "pt-BR" }) + { + Assert.That( + profile.Resolve(new LocaleIdentifier(localeCode)).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Word), + localeCode); + } + } + } +} diff --git a/Assets/Editor/LocalizationExpansionTests.cs.meta b/Assets/Editor/Tests/LocalizationContractTests.cs.meta similarity index 76% rename from Assets/Editor/LocalizationExpansionTests.cs.meta rename to Assets/Editor/Tests/LocalizationContractTests.cs.meta index b8c68824f..e30eb865f 100644 --- a/Assets/Editor/LocalizationExpansionTests.cs.meta +++ b/Assets/Editor/Tests/LocalizationContractTests.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Tests/SaveSnapshotContractTests.cs b/Assets/Editor/Tests/SaveSnapshotContractTests.cs new file mode 100644 index 000000000..e3b7b87a7 --- /dev/null +++ b/Assets/Editor/Tests/SaveSnapshotContractTests.cs @@ -0,0 +1,60 @@ +using AibisDream.SaveSystem; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class SaveSnapshotContractTests + { + [Test] + public void PresentationProviders_KeepStableIdsAndRestoreOrder() + { + var showcase = new ShowcaseSnapshotProvider(); + var day2Sleep = new Day2SleepPresentationSnapshotProvider(); + var playTool = new PlayToolSnapshotProvider(); + var screen = new ScreenSnapshotProvider(); + + Assert.That(showcase.SaveId, Is.EqualTo("showcase")); + Assert.That(day2Sleep.SaveId, Is.EqualTo("day2SleepPresentation")); + Assert.That(playTool.SaveId, Is.EqualTo("playTool")); + Assert.That(showcase.RestoreOrder, Is.LessThan(day2Sleep.RestoreOrder)); + Assert.That(day2Sleep.RestoreOrder, Is.LessThan(playTool.RestoreOrder)); + Assert.That(playTool.RestoreOrder, Is.LessThan(screen.RestoreOrder)); + } + + [Test] + public void PresentationSections_RoundTripAndLegacySnapshotsRemainReadable() + { + var source = new SaveSnapshot(); + source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto + { + displayMode = ShowcaseDisplayMode.Large.ToString(), + picName = "D2S星图", + isBackgroundVisible = true + }; + source.sections[SnapshotProviderIds.PlayTool] = new PlayToolSnapshotDto + { + isObjVisible = true, + objPicName = "证物" + }; + + var restored = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(source)); + var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase]) + .ToObject(); + var playTool = ((JObject)restored.sections[SnapshotProviderIds.PlayTool]) + .ToObject(); + + Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion)); + Assert.That(showcase.displayMode, Is.EqualTo("Large")); + Assert.That(showcase.picName, Is.EqualTo("D2S星图")); + Assert.That(playTool.objPicName, Is.EqualTo("证物")); + + var legacy = JsonConvert.DeserializeObject( + "{\"schemaVersion\":1,\"sections\":{}}"); + Assert.That(legacy.schemaVersion, Is.EqualTo(1)); + Assert.That(legacy.sections, Is.Empty); + } + } +} diff --git a/Assets/Editor/PresentationSnapshotContractTests.cs.meta b/Assets/Editor/Tests/SaveSnapshotContractTests.cs.meta similarity index 100% rename from Assets/Editor/PresentationSnapshotContractTests.cs.meta rename to Assets/Editor/Tests/SaveSnapshotContractTests.cs.meta diff --git a/Assets/Editor/Tests/TextSpeedSettingsTests.cs b/Assets/Editor/Tests/TextSpeedSettingsTests.cs new file mode 100644 index 000000000..abe2ecb81 --- /dev/null +++ b/Assets/Editor/Tests/TextSpeedSettingsTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class TextSpeedSettingsTests + { + [SetUp] + public void SetUp() + { + TextSpeedSettings.Reset(); + } + + [TearDown] + public void TearDown() + { + TextSpeedSettings.Reset(); + } + + [Test] + public void Normalize_MapsEverySupportedValueToCanonicalForm() + { + AssertNormalized( + "0.5", + TextSpeedSettings.SlowMultiplier, + TextSpeedSettings.SlowValue); + AssertNormalized( + "1.0", + TextSpeedSettings.DefaultMultiplier, + TextSpeedSettings.DefaultValue); + AssertNormalized( + "3", + TextSpeedSettings.FastMultiplier, + TextSpeedSettings.FastValue); + } + + [Test] + public void Normalize_RejectsInvalidAndUnsupportedValues() + { + foreach (var rawValue in new[] + { + null, + string.Empty, + "invalid", + "NaN", + "Infinity", + "0", + "2", + "4" + }) + { + Assert.That( + TextSpeedSettings.TryNormalize( + rawValue, + out var multiplier, + out var canonicalValue), + Is.False, + rawValue); + Assert.That(multiplier, Is.EqualTo(TextSpeedSettings.DefaultMultiplier)); + Assert.That(canonicalValue, Is.EqualTo(TextSpeedSettings.DefaultValue)); + } + } + + [Test] + public void Reset_RestoresDefaultAfterRuntimeChange() + { + Assert.That(TextSpeedSettings.ApplyStoredValue("3"), Is.True); + + TextSpeedSettings.Reset(); + + Assert.That( + TextSpeedSettings.CurrentMultiplier, + Is.EqualTo(TextSpeedSettings.DefaultMultiplier)); + Assert.That( + TextSpeedSettings.CurrentCanonicalValue, + Is.EqualTo(TextSpeedSettings.DefaultValue)); + } + + private static void AssertNormalized( + string rawValue, + float expectedMultiplier, + string expectedCanonicalValue) + { + Assert.That( + TextSpeedSettings.TryNormalize( + rawValue, + out var multiplier, + out var canonicalValue), + Is.True, + rawValue); + Assert.That(multiplier, Is.EqualTo(expectedMultiplier)); + Assert.That(canonicalValue, Is.EqualTo(expectedCanonicalValue)); + } + } +} diff --git a/Assets/Editor/TextSpeedSettingsTests.cs.meta b/Assets/Editor/Tests/TextSpeedSettingsTests.cs.meta similarity index 76% rename from Assets/Editor/TextSpeedSettingsTests.cs.meta rename to Assets/Editor/Tests/TextSpeedSettingsTests.cs.meta index 254f726e6..2e29a84ac 100644 --- a/Assets/Editor/TextSpeedSettingsTests.cs.meta +++ b/Assets/Editor/Tests/TextSpeedSettingsTests.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Tests/YarnLineMetadataTests.cs b/Assets/Editor/Tests/YarnLineMetadataTests.cs new file mode 100644 index 000000000..02dd6f80d --- /dev/null +++ b/Assets/Editor/Tests/YarnLineMetadataTests.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Yarn.Markup; +using Yarn.Unity; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class YarnLineMetadataTests + { + [Test] + public void AutoNext_UsesDefaultOrInvariantParameterizedDelay() + { + var plain = LineInfo.Generate(CreateLine("auto_next")); + Assert.That(plain.isAutoSkip, Is.True); + Assert.That(plain.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); + + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("de-DE"); + var parameterized = LineInfo.Generate(CreateLine("auto_next:4.5")); + Assert.That(parameterized.isAutoSkip, Is.True); + Assert.That(parameterized.autoNextDelaySeconds, Is.EqualTo(4.5f)); + Assert.That(parameterized.CalcAutoNextDelayTime(), Is.EqualTo(4500)); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Test] + public void AutoNext_InvalidValueFallsBackAndSimilarTagIsIgnored() + { + LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效")); + var invalid = LineInfo.Generate(CreateLine("auto_next:abc")); + + Assert.That(invalid.isAutoSkip, Is.True); + Assert.That(invalid.autoNextDelaySeconds, Is.Null); + Assert.That(invalid.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); + + var similar = CreateLine("AUTO_NEXT", "auto_next_extra:4.5"); + Assert.That(similar.IsAutoSkipLine(), Is.False); + Assert.That(similar.TryGetAutoNextDelaySeconds(out _), Is.False); + } + + [Test] + public void OptionPrompt_RequiresExactMetadataTag() + { + Assert.That(CreateLine(YarnUtil.OptionPrompt).IsOptionPromptLine(), Is.True); + Assert.That(CreateLine("OPTION_PROMPT").IsOptionPromptLine(), Is.False); + Assert.That(CreateLine("option_prompts").IsOptionPromptLine(), Is.False); + Assert.That( + new LocalizedLine { Metadata = null }.IsOptionPromptLine(), + Is.False); + } + + private static LocalizedLine CreateLine(params string[] metadata) + { + return new LocalizedLine + { + TextID = "line:yarn-metadata-contract", + Metadata = metadata, + Text = new MarkupParseResult( + "Test line", + new List()) + }; + } + } +} diff --git a/Assets/Editor/YarnLineMetadataTests.cs.meta b/Assets/Editor/Tests/YarnLineMetadataTests.cs.meta similarity index 76% rename from Assets/Editor/YarnLineMetadataTests.cs.meta rename to Assets/Editor/Tests/YarnLineMetadataTests.cs.meta index 82bd2f14c..ea1268f38 100644 --- a/Assets/Editor/YarnLineMetadataTests.cs.meta +++ b/Assets/Editor/Tests/YarnLineMetadataTests.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/TextSpeedSettingsTests.cs b/Assets/Editor/TextSpeedSettingsTests.cs deleted file mode 100644 index 06429d97c..000000000 --- a/Assets/Editor/TextSpeedSettingsTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using NUnit.Framework; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class TextSpeedSettingsTests - { - [SetUp] - public void SetUp() - { - TextSpeedSettings.Reset(); - } - - [TearDown] - public void TearDown() - { - TextSpeedSettings.Reset(); - } - - [TestCase("0.5", TextSpeedSettings.SlowMultiplier, TextSpeedSettings.SlowValue)] - [TestCase("0.50", TextSpeedSettings.SlowMultiplier, TextSpeedSettings.SlowValue)] - [TestCase("1", TextSpeedSettings.DefaultMultiplier, TextSpeedSettings.DefaultValue)] - [TestCase("1.0", TextSpeedSettings.DefaultMultiplier, TextSpeedSettings.DefaultValue)] - [TestCase("3", TextSpeedSettings.FastMultiplier, TextSpeedSettings.FastValue)] - [TestCase("3.0", TextSpeedSettings.FastMultiplier, TextSpeedSettings.FastValue)] - public void TryNormalize_SupportedNumericValue_ReturnsCanonicalValue( - string rawValue, - float expectedMultiplier, - string expectedCanonicalValue) - { - var result = TextSpeedSettings.TryNormalize( - rawValue, - out var multiplier, - out var canonicalValue); - - Assert.That(result, Is.True); - Assert.That(multiplier, Is.EqualTo(expectedMultiplier)); - Assert.That(canonicalValue, Is.EqualTo(expectedCanonicalValue)); - } - - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - [TestCase("invalid")] - [TestCase("NaN")] - [TestCase("Infinity")] - [TestCase("-1")] - [TestCase("0")] - [TestCase("2")] - [TestCase("4")] - public void TryNormalize_InvalidOrUnsupportedValue_ReturnsDefault( - string rawValue) - { - var result = TextSpeedSettings.TryNormalize( - rawValue, - out var multiplier, - out var canonicalValue); - - Assert.That(result, Is.False); - Assert.That(multiplier, Is.EqualTo(TextSpeedSettings.DefaultMultiplier)); - Assert.That(canonicalValue, Is.EqualTo(TextSpeedSettings.DefaultValue)); - } - - [Test] - public void Reset_AfterApplyingFastValue_RestoresDefault() - { - Assert.That(TextSpeedSettings.ApplyStoredValue("3"), Is.True); - Assert.That(TextSpeedSettings.CurrentMultiplier, Is.EqualTo(TextSpeedSettings.FastMultiplier)); - - TextSpeedSettings.Reset(); - - Assert.That(TextSpeedSettings.CurrentMultiplier, Is.EqualTo(TextSpeedSettings.DefaultMultiplier)); - Assert.That(TextSpeedSettings.CurrentCanonicalValue, Is.EqualTo(TextSpeedSettings.DefaultValue)); - } - } -} diff --git a/Assets/Editor/YarnLineMetadataTests.cs b/Assets/Editor/YarnLineMetadataTests.cs deleted file mode 100644 index 12a0a94e7..000000000 --- a/Assets/Editor/YarnLineMetadataTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Text.RegularExpressions; -using AibisDream.Utility; -using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; -using Yarn.Markup; -using Yarn.Unity; - -namespace AibisDream.SystemEditor.Tests -{ - public sealed class YarnLineMetadataTests - { - [Test] - public void AutoNextWithoutParameter_UsesExistingFixedDelay() - { - var lineInfo = LineInfo.Generate(CreateLine("auto_next")); - - Assert.That(lineInfo.isAutoSkip, Is.True); - Assert.That(lineInfo.autoNextDelaySeconds, Is.Null); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); - } - - [Test] - public void AutoNextWithParameter_UsesSpecifiedSeconds() - { - var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5")); - - Assert.That(lineInfo.isAutoSkip, Is.True); - Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f)); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500)); - } - - [Test] - public void AutoNextWithZeroDelay_IsValid() - { - var lineInfo = LineInfo.Generate(CreateLine("auto_next:0")); - - Assert.That(lineInfo.isAutoSkip, Is.True); - Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f)); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero); - } - - [Test] - public void AutoNextParameter_UsesInvariantCulture() - { - var originalCulture = CultureInfo.CurrentCulture; - try - { - CultureInfo.CurrentCulture = new CultureInfo("de-DE"); - var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5")); - - Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f)); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500)); - } - finally - { - CultureInfo.CurrentCulture = originalCulture; - } - } - - [TestCase("auto_next:")] - [TestCase("auto_next:abc")] - [TestCase("auto_next:-1")] - [TestCase("auto_next:NaN")] - [TestCase("auto_next:Infinity")] - [TestCase("auto_next:2147484")] - public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata) - { - LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效")); - - var lineInfo = LineInfo.Generate(CreateLine(metadata)); - - Assert.That(lineInfo.isAutoSkip, Is.True); - Assert.That(lineInfo.autoNextDelaySeconds, Is.Null); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); - } - - [TestCase("auto_next_extra")] - [TestCase("auto_next_extra:4.5")] - [TestCase("AUTO_NEXT")] - [TestCase("AUTO_NEXT:4.5")] - public void SimilarAutoNextMetadata_IsNotRecognized(string metadata) - { - var line = CreateLine(metadata); - - Assert.That(line.IsAutoSkipLine(), Is.False); - Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False); - } - - [Test] - public void ParameterizedAutoNextTag_TakesPrecedenceOverPlainTag() - { - var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5")); - - Assert.That(lineInfo.isAutoSkip, Is.True); - Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f)); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500)); - } - - [Test] - public void MultipleParameterizedAutoNextTags_UseFirstAndWarn() - { - LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签")); - - var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5")); - - Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f)); - Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500)); - } - - [Test] - public void IsOptionPromptLine_WithExactMetadata_ReturnsTrue() - { - var line = new LocalizedLine - { - Metadata = new[] { "line:0123456", YarnUtil.OptionPrompt } - }; - - Assert.That(line.IsOptionPromptLine(), Is.True); - } - - [TestCase("option_prompts")] - [TestCase("dream_option_prompt")] - [TestCase("OPTION_PROMPT")] - public void IsOptionPromptLine_WithSimilarMetadata_ReturnsFalse(string metadata) - { - var line = new LocalizedLine - { - Metadata = new[] { metadata } - }; - - Assert.That(line.IsOptionPromptLine(), Is.False); - } - - [Test] - public void IsOptionPromptLine_WithoutMetadata_ReturnsFalse() - { - var line = new LocalizedLine - { - Metadata = null - }; - - Assert.That(line.IsOptionPromptLine(), Is.False); - } - - private static LocalizedLine CreateLine(params string[] metadata) - { - return new LocalizedLine - { - TextID = "line:yarn-metadata-test", - Metadata = metadata, - Text = new MarkupParseResult("Test line", new List()) - }; - } - } -} diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationCoreTests.cs b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationCoreTests.cs index 13fa7760d..1ff55bb90 100644 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationCoreTests.cs +++ b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationCoreTests.cs @@ -7,87 +7,77 @@ using UnityEngine; namespace AibisDream.FrameAnimation.Tests.EditMode { + /// + /// 帧动画运行时模型的稳定契约。编辑器窗口与具体布局不在此处测试。 + /// public sealed class FrameAnimationCoreTests { - private readonly List createdObjects = new List(); + private readonly List createdObjects = + new List(); [TearDown] public void TearDown() { foreach (var createdObject in createdObjects.Where(item => item != null)) - { UnityEngine.Object.DestroyImmediate(createdObject); - } + createdObjects.Clear(); } [Test] - public void DataModel_SerializesOptionalOverridesAndKeepsInternalIds() + public void GraphSerialization_PreservesRuntimeIdentityAndOverrides() { - var clip = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100, 100); + var clip = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100); var node = new AnimationNode( clip.Id, endBehaviorOverride: FrameClipEndBehavior.HideTarget, speedOverride: 2f); - var flow = new AnimationFlow("Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear); - var graph = CreateGraph("Graph", new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, flow.Id); - var clone = Track(ScriptableObject.CreateInstance()); - - var json = EditorJsonUtility.ToJson(graph); - EditorJsonUtility.FromJsonOverwrite(json, clone); - - Assert.That(clone.Nodes[0].InternalId, Is.EqualTo(node.InternalId)); - Assert.That(clone.Nodes[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.HideTarget)); - Assert.That(clone.Nodes[0].SpeedOverride, Is.EqualTo(2f)); - Assert.That(clone.Flows[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.Clear)); - } - - [Test] - public void Validator_ReportsDuplicatePlayableAndInvalidGraphStructure() - { - var first = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f, 100); - var second = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f); - var node = new AnimationNode("MissingClip", internalId: "invalid-id"); - var flow = new AnimationFlow("Same", "Same Flow", node.InternalId); + var flow = new AnimationFlow( + "Flow", + "Flow", + node.InternalId, + FrameClipEndBehavior.Clear); var graph = CreateGraph( - "Invalid", - new[] { first, second }, + new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, - "Missing"); + flow.Id); + var clone = Track(ScriptableObject.CreateInstance()); - var report = FrameAnimationGraphValidator.Validate(graph); + EditorJsonUtility.FromJsonOverwrite(EditorJsonUtility.ToJson(graph), clone); - Assert.That(report.HasErrors, Is.True); - Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.PlayableIdDuplicate), Is.True); - Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.ClipFramesEmpty), Is.True); - Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.InternalIdInvalid), Is.True); - Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.NodeClipMissing), Is.True); - Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.DefaultPlayableInvalid), Is.True); + Assert.That(clone.Nodes[0].InternalId, Is.EqualTo(node.InternalId)); + Assert.That( + clone.Nodes[0].EndBehaviorOverride, + Is.EqualTo(FrameClipEndBehavior.HideTarget)); + Assert.That(clone.Nodes[0].SpeedOverride, Is.EqualTo(2f)); + Assert.That( + clone.Flows[0].EndBehaviorOverride, + Is.EqualTo(FrameClipEndBehavior.Clear)); } [Test] - public void Resolver_RejectsCycleAndMultipleSuccessors() + public void InvalidTopology_IsRejectedBeforePlayback() { var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100); - var a = new AnimationNode(clip.Id); - var b = new AnimationNode(clip.Id); - var c = new AnimationNode(clip.Id); - var flow = new AnimationFlow("Flow", "Flow", a.InternalId); + var first = new AnimationNode(clip.Id); + var second = new AnimationNode(clip.Id); + var third = new AnimationNode(clip.Id); + var flow = new AnimationFlow("Flow", "Flow", first.InternalId); var graph = CreateGraph( - "Graph", new[] { clip }, - new[] { a, b, c }, + new[] { first, second, third }, new[] { - new AnimationEdge(a.InternalId, b.InternalId), - new AnimationEdge(a.InternalId, c.InternalId), - new AnimationEdge(b.InternalId, a.InternalId) + new AnimationEdge(first.InternalId, second.InternalId), + new AnimationEdge(first.InternalId, third.InternalId), + new AnimationEdge(second.InternalId, first.InternalId) }, new[] { flow }, flow.Id); + var report = FrameAnimationGraphValidator.Validate(graph); var resolved = FrameAnimationResolver.TryResolve( graph, flow.Id, @@ -95,27 +85,36 @@ namespace AibisDream.FrameAnimation.Tests.EditMode out _, out var error); + Assert.That(report.HasErrors, Is.True); Assert.That(resolved, Is.False); - Assert.That(error.Code, Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData)); + Assert.That( + error.Code, + Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData)); } [Test] - public void Session_LargeDeltaCrossesFlowAndLoopsTerminalClip() + public void PlaybackSession_CrossesFlowAndLoopsTerminalClip() { var intro = CreateClip("Intro", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100); var idle = CreateClip("Idle", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100, 100); var introNode = new AnimationNode(intro.Id); - var idleNode = new AnimationNode(idle.Id, endBehaviorOverride: FrameClipEndBehavior.Loop); + var idleNode = new AnimationNode( + idle.Id, + endBehaviorOverride: FrameClipEndBehavior.Loop); var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId); var graph = CreateGraph( - "Graph", new[] { intro, idle }, new[] { introNode, idleNode }, new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) }, new[] { flow }, flow.Id); Assert.That( - FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out var error), + FrameAnimationResolver.TryResolve( + graph, + flow.Id, + default, + out var plan, + out var error), Is.True, error.ToString()); @@ -130,59 +129,23 @@ namespace AibisDream.FrameAnimation.Tests.EditMode } [Test] - public void Session_ExactBoundaryAndRemainingTimeAdvanceDeterministically() + public void EndBehavior_NodeOverrideHasHighestPrecedence() { - var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 200, 300); + var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100); + var node = new AnimationNode( + clip.Id, + endBehaviorOverride: FrameClipEndBehavior.HideTarget); + var flow = new AnimationFlow( + "Flow", + "Flow", + node.InternalId, + FrameClipEndBehavior.Clear); var graph = CreateGraph( - "Graph", - new[] { clip }, - Array.Empty(), - Array.Empty(), - Array.Empty(), - clip.Id); - Assert.That(FrameAnimationResolver.TryResolve(graph, clip.Id, default, out var plan, out _), Is.True); - var session = new FrameAnimationPlaybackSession(plan); - session.Start(_ => { }); - - session.Evaluate(0.1d, 1f, _ => { }); - Assert.That(session.CurrentFrameIndex, Is.EqualTo(1)); - session.Evaluate(0.25d, 1f, _ => { }); - Assert.That(session.CurrentFrameIndex, Is.EqualTo(2)); - var completion = session.Evaluate(0.25d, 1f, _ => { }); - - Assert.That(completion.IsCompleted, Is.True); - Assert.That(completion.EndBehavior, Is.EqualTo(FrameClipEndBehavior.HoldLastFrame)); - } - - [Test] - public void Session_UsesNodeSpeedInsteadOfClipSpeedAndKeepsPlayerMultiplier() - { - var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 0.5f, 100, 100); - var node = new AnimationNode(clip.Id, speedOverride: 2f); - var flow = new AnimationFlow("Flow", "Flow", node.InternalId); - var graph = CreateGraph( - "Graph", new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, flow.Id); - Assert.That(FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out _), Is.True); - var session = new FrameAnimationPlaybackSession(plan); - session.Start(_ => { }); - - session.Evaluate(0.025d, 2f, _ => { }); - - Assert.That(session.CurrentFrameIndex, Is.EqualTo(1)); - } - - [Test] - public void Resolver_EndBehaviorPrecedenceIsNodeThenRequestThenFlowThenClip() - { - var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100); - var node = new AnimationNode(clip.Id, endBehaviorOverride: FrameClipEndBehavior.HideTarget); - var flow = new AnimationFlow("Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear); - var graph = CreateGraph("Graph", new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, flow.Id); Assert.That( FrameAnimationResolver.TryResolve( @@ -192,22 +155,17 @@ namespace AibisDream.FrameAnimation.Tests.EditMode out var plan, out _), Is.True); - Assert.That(plan.Steps[0].TerminalEndBehavior, Is.EqualTo(FrameClipEndBehavior.HideTarget)); + Assert.That( + plan.Steps[0].TerminalEndBehavior, + Is.EqualTo(FrameClipEndBehavior.HideTarget)); } [Test] - public void Handle_CompletesOnceAndLateObserversReceiveSameResult() + public void PlaybackHandle_CompletesExactlyOnce() { var handle = new FrameAnimationPlaybackHandle(42); var callbackCount = 0; - FrameAnimationPlaybackResult observed = default; - handle.RegisterCompleted(result => - { - callbackCount++; - observed = result; - }); - Assert.Throws(() => _ = handle.Result); - + handle.RegisterCompleted(_ => callbackCount++); var completion = new FrameAnimationPlaybackResult( 42, "Clip", @@ -219,26 +177,7 @@ namespace AibisDream.FrameAnimation.Tests.EditMode handle.RegisterCompleted(_ => callbackCount++); Assert.That(callbackCount, Is.EqualTo(2)); - Assert.That(observed.RequestId, Is.EqualTo(42)); - Assert.That(handle.WaitAsync().Result.RequestId, Is.EqualTo(42)); - } - - [Test] - public void SessionSnapshotAndSeek_UseTheRuntimeEvaluator() - { - var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 2f, 100, 200, 300); - var graph = CreateGraph("Graph", new[] { clip }, Array.Empty(), - Array.Empty(), Array.Empty(), clip.Id); - Assert.That(FrameAnimationResolver.TryResolve(graph, clip.Id, default, out var plan, out _), Is.True); - var session = new FrameAnimationPlaybackSession(plan); - session.Start(_ => { }); - - session.Seek(0.075d, _ => { }); - - Assert.That(session.Snapshot.StepIndex, Is.EqualTo(0)); - Assert.That(session.Snapshot.FrameIndex, Is.EqualTo(1)); - Assert.That(session.Snapshot.FrameElapsedSeconds, Is.EqualTo(0.05d).Within(0.000001d)); - Assert.That(session.Snapshot.StepSpeed, Is.EqualTo(2f)); + Assert.That(handle.Result.RequestId, Is.EqualTo(42)); } private FrameClip CreateClip( @@ -248,14 +187,17 @@ namespace AibisDream.FrameAnimation.Tests.EditMode params int[] durations) { var clip = Track(ScriptableObject.CreateInstance()); - var frames = durations.Select((duration, index) => - new FrameAnimationFrame(null, duration, $"frame_{index}", index)); - clip.Configure(id, id, frames, speed, endBehavior); + clip.Configure( + id, + id, + durations.Select((duration, index) => + new FrameAnimationFrame(null, duration, $"frame_{index}", index)), + speed, + endBehavior); return clip; } private FrameAnimationGraph CreateGraph( - string id, IEnumerable clips, IEnumerable nodes, IEnumerable edges, @@ -263,7 +205,14 @@ namespace AibisDream.FrameAnimation.Tests.EditMode string defaultPlayableId) { var graph = Track(ScriptableObject.CreateInstance()); - graph.Configure(id, id, clips, nodes, edges, flows, defaultPlayableId); + graph.Configure( + "Graph", + "Graph", + clips, + nodes, + edges, + flows, + defaultPlayableId); return graph; } diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs deleted file mode 100644 index 8ad32bacf..000000000 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Linq; -using AibisDream.FrameAnimation.Editor; -using NUnit.Framework; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.FrameAnimation.Tests.EditMode -{ - public sealed class FrameAnimationEditorWorkspaceTests - { - private const string TestRoot = "Assets/Tests/FrameAnimation/GeneratedWorkspaceTests"; - - [SetUp] - public void SetUp() - { - Undo.ClearAll(); - AssetDatabase.DeleteAsset(TestRoot); - EnsureFolder(TestRoot); - } - - [TearDown] - public void TearDown() - { - Undo.ClearAll(); - AssetDatabase.DeleteAsset(TestRoot); - AssetDatabase.Refresh(); - } - - [Test] - public void ManualClip_CreateAndImportedCopy_UseExpectedOwnershipAndData() - { - var graph = CreateGraph("Graph.asset"); - Assert.That(FrameAnimationAssetOperations.CreateManualClip( - graph, "Manual", "Manual", null, out var manual, out var createError), Is.True, createError); - Assert.That(AssetDatabase.IsSubAsset(manual), Is.True); - Assert.That(manual.IsImported, Is.False); - - var imported = CreateClip("Imported", imported: true); - imported.SetFrames(new[] { new FrameAnimationFrame(null, 175, "来源帧", 3) }); - AssetDatabase.AddObjectToAsset(imported, graph); - graph.AddClip(imported); - AssetDatabase.SaveAssets(); - - Assert.That(FrameAnimationAssetOperations.CopyToManual( - graph, imported, "Copied", "Copied", null, out var copy, out var copyError), Is.True, copyError); - Assert.That(copy.IsImported, Is.False); - Assert.That(copy.Frames.Count, Is.EqualTo(1)); - Assert.That(copy.Frames[0], Is.Not.SameAs(imported.Frames[0])); - Assert.That(copy.Frames[0].DurationMs, Is.EqualTo(175)); - Assert.That(copy.Frames[0].FrameName, Is.EqualTo("来源帧")); - Assert.That(copy.Frames[0].SourceIndex, Is.EqualTo(3)); - } - - [Test] - public void RenameClip_UpdatesNodeAndDefault_AndSupportsUndoRedo() - { - var clip = CreateClip("OldClip"); - var node = new AnimationNode("OldClip", internalId: "node-id"); - var graph = CreateGraph("Rename.asset", new[] { clip }, new[] { node }, defaultPlayableId: "OldClip"); - - Assert.That(FrameAnimationAssetOperations.RenameClip(graph, clip, "NewClip", out var error), Is.True, error); - Assert.That(clip.Id, Is.EqualTo("NewClip")); - Assert.That(node.ClipId, Is.EqualTo("NewClip")); - Assert.That(graph.Settings.DefaultPlayableId, Is.EqualTo("NewClip")); - - Undo.FlushUndoRecordObjects(); - Undo.PerformUndo(); - Assert.That(clip.Id, Is.EqualTo("OldClip")); - Assert.That(graph.Nodes.Single().ClipId, Is.EqualTo("OldClip")); - Assert.That(graph.Settings.DefaultPlayableId, Is.EqualTo("OldClip")); - - Undo.PerformRedo(); - Assert.That(clip.Id, Is.EqualTo("NewClip")); - Assert.That(graph.Nodes.Single().ClipId, Is.EqualTo("NewClip")); - } - - [Test] - public void RenameSharedExternalManualClip_IsBlocked() - { - var clip = CreateClip("Shared"); - AssetDatabase.CreateAsset(clip, TestRoot + "/Shared.asset"); - var first = CreateGraph("First.asset", new[] { clip }); - var second = CreateGraph("Second.asset", new[] { clip }); - AssetDatabase.SaveAssets(); - - Assert.That(FrameAnimationAssetOperations.RenameClip(first, clip, "Renamed", out var error), Is.False); - StringAssert.Contains(first.name, error); - StringAssert.Contains(second.name, error); - Assert.That(clip.Id, Is.EqualTo("Shared")); - } - - [Test] - public void RemoveClip_ProtectsReferences_ThenDeletesOwnedSubAsset() - { - var clip = CreateClip("Protected"); - var node = new AnimationNode("Protected", internalId: "node-id"); - var graph = CreateGraph("Remove.asset", new[] { clip }, new[] { node }); - - Assert.That(FrameAnimationAssetOperations.RemoveClip(graph, clip, out var blocked), Is.False); - StringAssert.Contains("AnimationNode", blocked); - - graph.Configure(graph.Id, graph.DisplayName, graph.Clips, Array.Empty(), - graph.Edges, graph.Flows, string.Empty); - Assert.That(FrameAnimationAssetOperations.RemoveClip(graph, clip, out var error), Is.True, error); - Assert.That(graph.Clips, Is.Empty); - Assert.That(clip == null, Is.True); - } - - [Test] - public void RemoveImportSource_IsBlockedWhileImportedClipIsAssociated() - { - var graph = CreateGraph("Source.asset"); - var source = new FrameAnimationImportSource("Source", null, null, Vector2.one, false, - sourceInternalId: "source-id"); - graph.AddImportSource(source); - var imported = CreateClip("Tag", imported: true, sourceId: source.InternalId); - AssetDatabase.AddObjectToAsset(imported, graph); - graph.AddClip(imported); - AssetDatabase.SaveAssets(); - - Assert.That(FrameAnimationAssetOperations.RemoveSource(graph, source, out var error), Is.False); - StringAssert.Contains("Tag", error); - Assert.That(graph.ImportSources, Contains.Item(source)); - } - - [Test] - public void RenameFlow_UpdatesDefaultAndEditorReferenceContract() - { - var flow = new AnimationFlow("OldFlow", "Old Flow", "entry"); - var graph = CreateGraph("Flow.asset", flows: new[] { flow }, defaultPlayableId: "OldFlow"); - graph.EditorData.GetOrCreateFlowData("OldFlow", Color.red); - - Assert.That(FrameAnimationAssetOperations.RenameFlow(graph, flow, "NewFlow", out var error), Is.True, error); - Assert.That(flow.Id, Is.EqualTo("NewFlow")); - Assert.That(graph.Settings.DefaultPlayableId, Is.EqualTo("NewFlow")); - Assert.That(graph.EditorData.FlowEditorData.Single().FlowId, Is.EqualTo("NewFlow")); - } - - [Test] - public void SetFlowColor_IsStoredAndSupportsUndo() - { - var flow = new AnimationFlow("Flow", "Flow", "entry"); - var graph = CreateGraph("FlowColor.asset", flows: new[] { flow }); - graph.EditorData.GetOrCreateFlowData(flow.Id, Color.red); - - FrameAnimationGraphMutationService.SetFlowColor(graph, flow, new Color(0.1f, 0.4f, 0.9f, 0.2f)); - Assert.That(graph.EditorData.FlowEditorData.Single().Color.b, Is.EqualTo(0.9f).Within(0.001f)); - Assert.That(graph.EditorData.FlowEditorData.Single().Color.a, Is.EqualTo(0.2f).Within(0.001f)); - - Undo.FlushUndoRecordObjects(); - Undo.PerformUndo(); - Assert.That(graph.EditorData.FlowEditorData.Single().Color, Is.EqualTo(Color.red)); - } - - [Test] - public void ResourceQuery_SearchFilterAndSort_AreStable() - { - var a = CreateClip("A", displayName: "Alpha"); - a.SetFrames(new[] { new FrameAnimationFrame(null, 200, string.Empty, -1) }); - var b = CreateClip("B", displayName: "Beta"); - b.SetFrames(new[] - { - new FrameAnimationFrame(null, 100, string.Empty, -1), - new FrameAnimationFrame(null, 100, string.Empty, -1) - }); - var node = new AnimationNode("B", internalId: "node-id"); - var graph = CreateGraph("Query.asset", new[] { b, a }, new[] { node }); - - CollectionAssert.AreEqual(new[] { "A", "B" }, - FrameAnimationResourceQuery.QueryClips(graph, string.Empty, - FrameAnimationClipFilter.All, FrameAnimationClipSort.Name).Select(clip => clip.Id)); - CollectionAssert.AreEqual(new[] { "B" }, - FrameAnimationResourceQuery.QueryClips(graph, "Beta", - FrameAnimationClipFilter.Referenced, FrameAnimationClipSort.ReferenceCount).Select(clip => clip.Id)); - CollectionAssert.AreEqual(new[] { "A" }, - FrameAnimationResourceQuery.QueryClips(graph, string.Empty, - FrameAnimationClipFilter.Unused, FrameAnimationClipSort.Name).Select(clip => clip.Id)); - } - - [Test] - public void ComprehensiveValidation_FindsDuplicateClipObjectReference() - { - var clip = CreateClip("Duplicate"); - var graph = CreateGraph("Validation.asset", new[] { clip, clip }); - - var issues = FrameAnimationEditorValidationService.Validate(graph, false, out _); - Assert.That(issues.Any(issue => issue.Code == "DuplicateClipReference"), Is.True); - } - - private static FrameAnimationGraph CreateGraph( - string fileName, - FrameClip[] clips = null, - AnimationNode[] nodes = null, - AnimationFlow[] flows = null, - string defaultPlayableId = "") - { - var graph = ScriptableObject.CreateInstance(); - graph.Configure(PathId(fileName), PathId(fileName), clips ?? Array.Empty(), - nodes ?? Array.Empty(), Array.Empty(), - flows ?? Array.Empty(), defaultPlayableId); - AssetDatabase.CreateAsset(graph, TestRoot + "/" + fileName); - foreach (var clip in (clips ?? Array.Empty()).Where(clip => - clip != null && string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip))).Distinct()) - { - AssetDatabase.AddObjectToAsset(clip, graph); - } - AssetDatabase.SaveAssets(); - return graph; - } - - private static FrameClip CreateClip( - string id, - string displayName = null, - bool imported = false, - string sourceId = "source-id") - { - var clip = ScriptableObject.CreateInstance(); - clip.name = id; - clip.Configure(id, displayName ?? id, Array.Empty(), 1f, - FrameClipEndBehavior.HoldLastFrame, - imported ? new FrameClipImportInfo(sourceId, id, false) : null); - return clip; - } - - private static string PathId(string fileName) => System.IO.Path.GetFileNameWithoutExtension(fileName); - - private static void EnsureFolder(string path) - { - var segments = path.Split('/'); - var current = segments[0]; - for (var index = 1; index < segments.Length; index++) - { - var next = current + "/" + segments[index]; - if (!AssetDatabase.IsValidFolder(next)) - { - AssetDatabase.CreateFolder(current, segments[index]); - } - current = next; - } - } - } -} diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs.meta b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs.meta deleted file mode 100644 index c6ad05da3..000000000 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8b1f03a11f33454a9a861ff25290577a diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs deleted file mode 100644 index 737bdfc57..000000000 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Linq; -using AibisDream.FrameAnimation.Editor; -using NUnit.Framework; -using UnityEngine; -using UnityEngine.UIElements; - -namespace AibisDream.FrameAnimation.Tests.EditMode -{ - public sealed class FrameAnimationGraphAuthoringTests - { - private FrameAnimationGraph graph; - private FrameClip clip; - - [SetUp] - public void SetUp() - { - clip = ScriptableObject.CreateInstance(); - clip.Configure("Idle", "Idle", new[] { new FrameAnimationFrame(null, 100, string.Empty, -1) }, - 1f, FrameClipEndBehavior.HoldLastFrame); - graph = ScriptableObject.CreateInstance(); - graph.Configure("Graph", "Graph", new[] { clip }, Array.Empty(), - Array.Empty(), Array.Empty(), string.Empty); - } - - [TearDown] - public void TearDown() - { - if (graph != null) UnityEngine.Object.DestroyImmediate(graph); - if (clip != null) UnityEngine.Object.DestroyImmediate(clip); - } - - [Test] - public void CreateNode_AllowsMultipleReferencesToSameClip_AndStoresPosition() - { - Assert.That(FrameAnimationGraphMutationService.CreateNode( - graph, clip, new Vector2(10f, 20f), out var first, out var firstError), Is.True, firstError); - Assert.That(FrameAnimationGraphMutationService.CreateNode( - graph, clip, new Vector2(40f, 50f), out var second, out var secondError), Is.True, secondError); - - Assert.That(first, Is.Not.SameAs(second)); - Assert.That(first.ClipId, Is.EqualTo(clip.Id)); - Assert.That(second.ClipId, Is.EqualTo(clip.Id)); - Assert.That(FrameAnimationGraphMutationService.GetNodePosition(graph, first), - Is.EqualTo(new Vector2(10f, 20f))); - Assert.That(graph.EditorData.NodeEditorData.Count, Is.EqualTo(2)); - } - - [Test] - public void Connect_EnforcesSingleSuccessorSelfCycleAndTerminalRules() - { - var a = AddNode("a"); - var b = AddNode("b"); - var c = AddNode("c"); - - Assert.That(FrameAnimationGraphMutationService.TryConnect(graph, a, b, out var first, out var error), - Is.True, error); - Assert.That(first.Condition, Is.EqualTo(AnimationEdgeCondition.Always)); - Assert.That(FrameAnimationGraphMutationService.TryConnect(graph, a, c, out _, out error), Is.False); - StringAssert.Contains("一个后继", error); - Assert.That(FrameAnimationGraphMutationService.TryConnect(graph, b, a, out _, out error), Is.False); - StringAssert.Contains("环路", error); - Assert.That(FrameAnimationGraphMutationService.TryConnect(graph, c, c, out _, out error), Is.False); - StringAssert.Contains("自身", error); - - c.SetEndBehaviorOverride(FrameClipEndBehavior.Loop); - Assert.That(FrameAnimationGraphMutationService.TryConnect(graph, c, a, out _, out error), Is.False); - StringAssert.Contains("结束行为", error); - } - - [Test] - public void Topology_FindsSharedReachabilityAndFlowUsage() - { - var first = AddNode("first"); - var second = AddNode("second"); - var shared = AddNode("shared"); - graph.AddEdge(new AnimationEdge(first.InternalId, shared.InternalId, "edge-a")); - graph.AddEdge(new AnimationEdge(second.InternalId, shared.InternalId, "edge-b")); - var flowA = new AnimationFlow("FlowA", "Flow A", first.InternalId); - var flowB = new AnimationFlow("FlowB", "Flow B", second.InternalId); - graph.AddFlow(flowA); - graph.AddFlow(flowB); - - var topology = new FrameAnimationGraphTopology(graph); - CollectionAssert.AreEquivalent(new[] { first, shared }, topology.GetReachable(first.InternalId).Nodes); - CollectionAssert.AreEquivalent(new[] { flowA, flowB }, topology.FindFlowsUsingNode(shared.InternalId)); - CollectionAssert.AreEqual(new[] { flowA }, topology.FindFlowsUsingEdge("edge-a")); - } - - [Test] - public void FlowCreation_UsesUniqueNamesAndEnforcesUniqueEntry() - { - var entry = AddNode("entry", "Idle"); - var other = AddNode("other", "Idle"); - Assert.That(FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry), Is.EqualTo("Idle_Flow")); - Assert.That(FrameAnimationGraphMutationService.CreateFlow( - graph, entry, "Idle_Flow", "Idle", out var flow, out var error), Is.True, error); - Assert.That(FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, other), Is.EqualTo("Idle_Flow2")); - Assert.That(FrameAnimationGraphMutationService.CreateFlow( - graph, entry, "OtherFlow", "Other", out _, out error), Is.False); - StringAssert.Contains("入口", error); - - Assert.That(FrameAnimationGraphMutationService.SetFlowEntry(graph, flow, other, out error), Is.True, error); - Assert.That(flow.EntryNodeId, Is.EqualTo(other.InternalId)); - } - - [Test] - public void EndBehaviorAndNodeRemoval_AreAtomicForTopologyAndEditorData() - { - var first = AddNode("first"); - var terminal = AddNode("terminal"); - Assert.That(FrameAnimationGraphMutationService.TryConnect( - graph, first, terminal, out _, out var error), Is.True, error); - Assert.That(FrameAnimationGraphMutationService.SetNodeEndBehavior( - graph, first, FrameClipEndBehavior.HoldLastFrame, true, out error), Is.True, error); - Assert.That(graph.Edges, Is.Empty); - - Assert.That(FrameAnimationGraphMutationService.CreateFlow( - graph, first, "Flow", "Flow", out _, out error), Is.True, error); - Assert.That(FrameAnimationGraphMutationService.RemoveNodes( - graph, new[] { first }, false, out error), Is.False); - Assert.That(FrameAnimationGraphMutationService.RemoveNodes( - graph, new[] { first }, true, out error), Is.True, error); - Assert.That(graph.Flows, Is.Empty); - Assert.That(graph.EditorData.NodeEditorData.Any(data => data.NodeId == first.InternalId), Is.False); - } - - [Test] - public void Layout_IsDeterministicLeftToRight_AndLeavesOutsideScopeUntouched() - { - var first = AddNode("first"); - var second = AddNode("second"); - var outside = AddNode("outside"); - graph.AddEdge(new AnimationEdge(first.InternalId, second.InternalId)); - FrameAnimationGraphMutationService.SetNodePositions(graph, - new System.Collections.Generic.Dictionary - { - [first] = new Vector2(50f, 40f), - [second] = new Vector2(500f, 400f), - [outside] = new Vector2(900f, 900f) - }, "Arrange Test"); - - var positions = FrameAnimationGraphLayoutService.Calculate(graph, new[] { first, second }); - Assert.That(positions[first], Is.EqualTo(new Vector2(50f, 40f))); - Assert.That(positions[second], Is.EqualTo(new Vector2(350f, 40f))); - Assert.That(positions.ContainsKey(outside), Is.False); - Assert.That(FrameAnimationGraphMutationService.GetNodePosition(graph, outside), - Is.EqualTo(new Vector2(900f, 900f))); - } - - [Test] - public void ComprehensiveValidation_ReportsMissingAndOrphanEditorData() - { - var node = new AnimationNode(clip.Id, internalId: "node-without-position"); - graph.AddNode(node); - graph.EditorData.GetOrCreateNodeData("orphan-node", Vector2.zero); - - var issues = FrameAnimationEditorValidationService.Validate(graph, false, out _); - Assert.That(issues.Any(issue => issue.Code == "EditorDataMissing" && - ReferenceEquals(issue.Selection.Value, node)), Is.True); - Assert.That(issues.Any(issue => issue.Code == "EditorDataOrphan"), Is.True); - } - - [Test] - public void GraphViewBind_RebuildsVisualsWithoutMutatingGraphData() - { - var first = AddNode("first"); - var second = AddNode("second"); - graph.AddEdge(new AnimationEdge(first.InternalId, second.InternalId, "stable-edge")); - var before = JsonUtility.ToJson(graph); - - var view = new FrameAnimationGraphView(); - view.Bind(graph, Array.Empty(), string.Empty); - - Assert.That(JsonUtility.ToJson(graph), Is.EqualTo(before)); - Assert.That(graph.Nodes.Count, Is.EqualTo(2)); - Assert.That(graph.Edges.Single().InternalId, Is.EqualTo("stable-edge")); - } - - [Test] - public void FlowFocusPolicy_ExitsForOutOfContextSelectionAndExpandsWithTopology() - { - var entry = AddNode("entry"); - var reachable = AddNode("reachable"); - var inside = AddNode("inside"); - var outside = AddNode("outside"); - var edge = new AnimationEdge(entry.InternalId, reachable.InternalId, "focused-edge"); - graph.AddEdge(edge); - graph.AddEdge(new AnimationEdge(reachable.InternalId, inside.InternalId)); - var flow = new AnimationFlow("FocusFlow", "Focus Flow", entry.InternalId); - graph.AddFlow(flow); - - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelection( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Node, entry)), Is.False); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelection( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Node, reachable)), Is.False); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelection( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Edge, edge)), Is.False); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelection( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Node, outside)), Is.True); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelection( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Graph, graph)), Is.True); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelectionSet(graph, flow.Id, new[] - { - Select(FrameAnimationEditorSelectionKind.Node, entry), - Select(FrameAnimationEditorSelectionKind.Node, inside) - }), Is.False); - Assert.That(FrameAnimationFlowFocusPolicy.ShouldExitForSelectionSet(graph, flow.Id, new[] - { - Select(FrameAnimationEditorSelectionKind.Node, inside), - Select(FrameAnimationEditorSelectionKind.Node, outside) - }), Is.True); - - graph.AddEdge(new AnimationEdge(inside.InternalId, outside.InternalId)); - Assert.That(FrameAnimationFlowFocusPolicy.Contains( - graph, flow.Id, Select(FrameAnimationEditorSelectionKind.Node, outside)), Is.True); - } - - [Test] - public void GraphViewFocus_DimsOutsideElements_AndBindDoesNotEmitSelectionChanges() - { - var entry = AddNode("entry"); - var inside = AddNode("inside"); - var outside = AddNode("outside"); - var outsideTail = AddNode("outside-tail"); - var edge = new AnimationEdge(entry.InternalId, inside.InternalId, "inside-edge"); - var outsideEdge = new AnimationEdge(outside.InternalId, outsideTail.InternalId, "outside-edge"); - graph.AddEdge(edge); - graph.AddEdge(outsideEdge); - var flow = new AnimationFlow("FocusFlow", "Focus Flow", entry.InternalId); - graph.AddFlow(flow); - var view = new FrameAnimationGraphView(); - var selectionNotifications = 0; - view.SelectionSetChanged += _ => selectionNotifications++; - - view.Bind(graph, Array.Empty(), flow.Id); - var nodes = view.Query().ToList() - .ToDictionary(nodeView => nodeView.Data.InternalId); - var edges = view.Query().ToList() - .ToDictionary(edgeView => edgeView.Data.InternalId); - - Assert.That(nodes[entry.InternalId].style.opacity.value, Is.EqualTo(1f)); - Assert.That(nodes[inside.InternalId].style.opacity.value, Is.EqualTo(1f)); - Assert.That(nodes[outside.InternalId].style.opacity.value, Is.EqualTo(0.45f)); - Assert.That(edges[edge.InternalId].style.opacity.value, Is.EqualTo(1f)); - Assert.That(edges[outsideEdge.InternalId].style.opacity.value, Is.EqualTo(0.30f)); - Assert.That(selectionNotifications, Is.Zero); - - view.AddToSelection(nodes[entry.InternalId]); - selectionNotifications = 0; - view.Bind(graph, Array.Empty(), flow.Id); - Assert.That(selectionNotifications, Is.Zero); - - var exitRequests = 0; - view.ExitFocusRequested += () => exitRequests++; - Assert.That(view.TryHandleFocusExitShortcut(KeyCode.Escape, false), Is.True); - Assert.That(exitRequests, Is.EqualTo(1)); - Assert.That(view.TryHandleFocusExitShortcut(KeyCode.Escape, true), Is.False); - view.SetFocusedFlow(string.Empty); - Assert.That(view.TryHandleFocusExitShortcut(KeyCode.Escape, false), Is.False); - } - - [Test] - public void GraphView_NodeCreatedEventFiresOnlyAfterSuccessfulCreation() - { - var view = new FrameAnimationGraphView(); - view.Bind(graph, Array.Empty(), string.Empty); - var created = 0; - view.NodeCreated += _ => created++; - - view.CreateNode(clip, new Vector2(15f, 25f)); - Assert.That(created, Is.EqualTo(1)); - - var foreignClip = ScriptableObject.CreateInstance(); - try - { - foreignClip.Configure("Foreign", "Foreign", Array.Empty(), 1f, - FrameClipEndBehavior.HoldLastFrame); - view.CreateNode(foreignClip, Vector2.zero); - Assert.That(created, Is.EqualTo(1)); - } - finally - { - UnityEngine.Object.DestroyImmediate(foreignClip); - } - } - - private static FrameAnimationEditorSelection Select( - FrameAnimationEditorSelectionKind kind, - object value) - { - return new FrameAnimationEditorSelection(kind, value); - } - - private AnimationNode AddNode(string id, string displayName = null) - { - var node = new AnimationNode(clip.Id, displayName ?? id, internalId: id); - graph.AddNode(node); - graph.EditorData.GetOrCreateNodeData(id, Vector2.zero); - return node; - } - } -} diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs.meta b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs.meta deleted file mode 100644 index 9010a30b2..000000000 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationGraphAuthoringTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4a7f50b95d6f49e093f3842f9ef81bc8 diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationImportTests.cs b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationImportTests.cs index d0f62b4e9..ad897df02 100644 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationImportTests.cs +++ b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationImportTests.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -9,335 +10,173 @@ using UnityEngine; namespace AibisDream.FrameAnimation.Tests.EditMode { + /// + /// Aseprite 到 FrameAnimationGraph 的边界契约。 + /// 只覆盖解析语义与一次完整刷新,不枚举编辑器操作步骤。 + /// public sealed class FrameAnimationImportTests { - private string TestRoot { get; set; } - private string TexturePath => TestRoot + "/Atlas.png"; - private string JsonPath => TestRoot + "/Atlas.json"; - private string GraphPath => TestRoot + "/ImportGraph.asset"; + private string testRoot; + private string TexturePath => testRoot + "/Atlas.png"; + private string JsonPath => testRoot + "/Atlas.json"; + private string GraphPath => testRoot + "/ImportGraph.asset"; [SetUp] public void SetUp() { - TestRoot = + testRoot = $"Assets/Tests/FrameAnimation/GeneratedImportTests_{Guid.NewGuid():N}"; - EnsureFolder(TestRoot); + EnsureFolder(testRoot); } [TearDown] public void TearDown() { - if (!string.IsNullOrEmpty(TestRoot)) + if (string.IsNullOrEmpty(testRoot)) + return; + + if (AssetDatabase.IsValidFolder(testRoot)) + AssetDatabase.DeleteAsset(testRoot); + + var projectRoot = Directory.GetParent(Application.dataPath).FullName; + var absoluteRoot = Path.GetFullPath(Path.Combine(projectRoot, testRoot)); + if (Directory.Exists(absoluteRoot)) + FileUtil.DeleteFileOrDirectory(absoluteRoot); + if (File.Exists(absoluteRoot + ".meta")) + FileUtil.DeleteFileOrDirectory(absoluteRoot + ".meta"); + + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + Assert.That(Directory.Exists(absoluteRoot), Is.False, + $"Test asset directory was not removed: {testRoot}"); + Assert.That(File.Exists(absoluteRoot + ".meta"), Is.False, + $"Test asset meta was not removed: {testRoot}.meta"); + } + + [Test] + public void Parser_PreservesUnicodeOrderAndSharedSpriteSlots() + { + Assert.That( + AsepriteJsonParser.TryParse( + ObjectJson(), + "source", + out var ordered, + out var orderedIssue), + Is.True, + orderedIssue?.Message); + Assert.That( + ordered.Frames.Select(frame => frame.FrameName), + Is.EqualTo(new[] { "第二帧.png", "第一帧.png" })); + Assert.That( + ordered.Tags.Select(tag => tag.Name), + Is.EqualTo(new[] { "待机", "眨眼" })); + + Assert.That( + AsepriteJsonParser.TryParse( + SharedRectJson(), + "source", + out var shared, + out var sharedIssue), + Is.True, + sharedIssue?.Message); + Assert.That(shared.Frames.Count, Is.EqualTo(3)); + Assert.That(shared.SpriteSlots.Count, Is.EqualTo(2)); + Assert.That( + shared.SpriteSlotBySourceIndex[0], + Is.SameAs(shared.SpriteSlotBySourceIndex[1])); + } + + [Test] + public void TagExpansion_SupportsEveryAsepriteDirection() + { + var cases = new Dictionary { + ["forward"] = new[] { 0, 1, 2, 3 }, + ["reverse"] = new[] { 3, 2, 1, 0 }, + ["pingpong"] = new[] { 0, 1, 2, 3, 2, 1 }, + ["pingpong_reverse"] = new[] { 3, 2, 1, 0, 1, 2 } + }; + + foreach (var pair in cases) + { + var tag = new AsepriteSourceTag("测试", 0, 3, pair.Key); Assert.That( - AssetDatabase.DeleteAsset(TestRoot), + AsepriteJsonParser.TryExpandTag( + tag, + 4, + "source", + out var actual, + out var issue), Is.True, - $"Failed to delete test asset folder: {TestRoot}"); + issue?.Message); + Assert.That(actual, Is.EqualTo(pair.Value), pair.Key); } - - AssetDatabase.Refresh(); } [Test] - public void Parser_PreservesObjectAndArrayOrderAndUnicode() - { - Assert.That(AsepriteJsonParser.TryParse(ObjectJson(), "source", out var objectDocument, out var objectIssue), - Is.True, objectIssue?.Message); - Assert.That(AsepriteJsonParser.TryParse(ArrayJson(), "source", out var arrayDocument, out var arrayIssue), - Is.True, arrayIssue?.Message); - - CollectionAssert.AreEqual( - new[] { "第二帧.png", "第一帧.png" }, - objectDocument.Frames.Select(frame => frame.FrameName)); - CollectionAssert.AreEqual( - objectDocument.Frames.Select(frame => frame.FrameName), - arrayDocument.Frames.Select(frame => frame.FrameName)); - CollectionAssert.AreEqual(new[] { "待机", "眨眼" }, objectDocument.Tags.Select(tag => tag.Name)); - - var trimJson = "{" + - "\"frames\":{" + - "\"FrameA\":{\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":100,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}}," + - "\"meta\":{\"image\":\"Atlas.png\",\"size\":{\"w\":2,\"h\":2},\"frameTags\":[" + - "{\"name\":\" 待机 \",\"from\":0,\"to\":0,\"direction\":\"forward\"}]}}"; - Assert.That(AsepriteJsonParser.TryParse(trimJson, "source", out var trimDocument, out var trimIssue), - Is.True, trimIssue?.Message); - Assert.That(trimDocument.Tags.Single().Name, Is.EqualTo("待机")); - } - - [Test] - public void Parser_GroupsSharedRectsWithoutCollapsingLogicalFrames() - { - Assert.That(AsepriteJsonParser.TryParse( - SharedRectJson(), "source", out var document, out var issue), Is.True, issue?.Message); - - Assert.That(document.Frames.Count, Is.EqualTo(3)); - Assert.That(document.SpriteSlots.Count, Is.EqualTo(2)); - Assert.That(document.SpriteSlots[0].CanonicalFrameName, Is.EqualTo("FrameA")); - CollectionAssert.AreEqual(new[] { 0, 1 }, document.SpriteSlots[0].SourceIndices); - CollectionAssert.AreEqual(new[] { "FrameA", "FrameB" }, document.SpriteSlots[0].FrameNames); - Assert.That(document.SpriteSlotBySourceIndex[0], Is.SameAs(document.SpriteSlotBySourceIndex[1])); - Assert.That(document.SpriteSlotBySourceIndex[2], Is.Not.SameAs(document.SpriteSlotBySourceIndex[0])); - } - - [TestCase("forward", new[] { 0, 1, 2, 3 })] - [TestCase("reverse", new[] { 3, 2, 1, 0 })] - [TestCase("pingpong", new[] { 0, 1, 2, 3, 2, 1 })] - [TestCase("pingpong_reverse", new[] { 3, 2, 1, 0, 1, 2 })] - public void TagExpansion_IsDeterministic(string direction, int[] expected) - { - var tag = new AsepriteSourceTag("测试", 0, 3, direction); - Assert.That(AsepriteJsonParser.TryExpandTag(tag, 4, "source", out var actual, out var issue), - Is.True, issue?.Message); - CollectionAssert.AreEqual(expected, actual); - } - - [TestCase("pingpong")] - [TestCase("pingpong_reverse")] - public void TagExpansion_SingleFrameDoesNotDuplicateEndpoint(string direction) - { - var tag = new AsepriteSourceTag("单帧", 1, 1, direction); - Assert.That(AsepriteJsonParser.TryExpandTag(tag, 3, "source", out var actual, out _), Is.True); - CollectionAssert.AreEqual(new[] { 1 }, actual); - } - - [Test] - public void WritableRefresh_CreatesAndUpdatesStableSubAssetsAndMissingState() + public void WritableRefresh_UpdatesFramesWithoutReplacingOwnedClip() { var graph = CreateWritableGraph(ObjectJson()); - var source = graph.ImportSources.Single(); var firstPreview = FrameAnimationImportService.PreviewAll(graph); Assert.That(firstPreview.HasErrors, Is.False, JoinIssues(firstPreview)); - Assert.That(firstPreview.Sources.Single().SpriteDiffs.Count(diff => - diff.Kind == FrameAnimationSpriteChangeKind.Added), Is.EqualTo(2)); - Assert.That(FrameAnimationImportService.Apply(firstPreview, true, out var firstError), Is.True, firstError); + Assert.That( + FrameAnimationImportService.Apply( + firstPreview, + true, + out var firstError), + Is.True, + firstError); - Assert.That(graph.Clips.Count, Is.EqualTo(2)); - var idle = graph.Clips.Single(clip => clip.ImportInfo.SourceTagName == "待机"); + var idle = graph.Clips.Single( + clip => clip.ImportInfo.SourceTagName == "待机"); Assert.That(AssetDatabase.IsSubAsset(idle), Is.True); - Assert.That(AssetDatabase.GetAssetPath(idle), Is.EqualTo(GraphPath)); - Assert.That(idle.Frames[0].FrameName, Is.EqualTo("第二帧.png")); - Assert.That(source.LastSourceHash, Is.Not.Empty); - Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long firstLocalId), Is.True); + Assert.That( + AssetDatabase.TryGetGUIDAndLocalFileIdentifier( + idle, + out _, + out long firstLocalId), + Is.True); - var idleSerialized = new SerializedObject(idle); - idleSerialized.FindProperty("id").stringValue = "用户重命名待机"; - idleSerialized.FindProperty("displayName").stringValue = "自定义显示名"; - idleSerialized.FindProperty("speed").floatValue = 1.75f; - idleSerialized.ApplyModifiedPropertiesWithoutUndo(); WriteJson(ObjectJson(duration: 180)); + var refreshPreview = FrameAnimationImportService.PreviewAll(graph); + Assert.That( + FrameAnimationImportService.Apply( + refreshPreview, + true, + out var refreshError), + Is.True, + refreshError); - var updatePreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(updatePreview.HasErrors, Is.False, JoinIssues(updatePreview)); - Assert.That(FrameAnimationImportService.Apply(updatePreview, true, out var updateError), Is.True, updateError); - Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long updatedLocalId), Is.True); - Assert.That(updatedLocalId, Is.EqualTo(firstLocalId)); - Assert.That(idle.Id, Is.EqualTo("用户重命名待机")); - Assert.That(idle.DisplayName, Is.EqualTo("自定义显示名")); - Assert.That(idle.Speed, Is.EqualTo(1.75f)); Assert.That(idle.Frames[0].DurationMs, Is.EqualTo(180)); - - WriteJson(ObjectJson(includeIdle: false)); - var missingPreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(FrameAnimationImportService.Apply(missingPreview, true, out var missingError), Is.True, missingError); - Assert.That(idle.ImportInfo.IsMissingFromSource, Is.True); - - WriteJson(ObjectJson(duration: 220)); - var restorePreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(FrameAnimationImportService.Apply(restorePreview, true, out var restoreError), Is.True, restoreError); - Assert.That(idle.ImportInfo.IsMissingFromSource, Is.False); - Assert.That(idle.Frames[0].DurationMs, Is.EqualTo(220)); - Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(idle, out _, out long restoredLocalId), Is.True); - Assert.That(restoredLocalId, Is.EqualTo(firstLocalId)); + Assert.That( + AssetDatabase.TryGetGUIDAndLocalFileIdentifier( + idle, + out _, + out long refreshedLocalId), + Is.True); + Assert.That(refreshedLocalId, Is.EqualTo(firstLocalId)); } - [Test] - public void WritableRefresh_SharedRectsCreateOneSpriteAndPreserveLogicalFrames() - { - var graph = CreateWritableGraph(SharedRectJson()); - var firstPreview = FrameAnimationImportService.PreviewAll(graph); - - Assert.That(firstPreview.HasErrors, Is.False, JoinIssues(firstPreview)); - var sourcePreview = firstPreview.Sources.Single(); - Assert.That(sourcePreview.Document.Frames.Count, Is.EqualTo(3)); - Assert.That(sourcePreview.Document.SpriteSlots.Count, Is.EqualTo(2)); - Assert.That(sourcePreview.SpriteDiffs.Count(diff => - diff.Kind == FrameAnimationSpriteChangeKind.Added), Is.EqualTo(2)); - Assert.That(sourcePreview.SpriteDiffs.Single(diff => diff.SourceFrameCount == 2).FrameName, - Is.EqualTo("FrameA")); - Assert.That(FrameAnimationImportService.Apply(firstPreview, true, out var firstError), - Is.True, firstError); - - var sprites = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType().ToArray(); - Assert.That(sprites.Length, Is.EqualTo(2)); - var clip = graph.Clips.Single(item => item.ImportInfo.SourceTagName == "Shared"); - Assert.That(clip.FrameCount, Is.EqualTo(3)); - Assert.That(clip.Frames[0].Sprite, Is.SameAs(clip.Frames[1].Sprite)); - Assert.That(clip.Frames[2].Sprite, Is.Not.SameAs(clip.Frames[0].Sprite)); - CollectionAssert.AreEqual(new[] { "FrameA", "FrameB", "FrameC" }, - clip.Frames.Select(frame => frame.FrameName)); - CollectionAssert.AreEqual(new[] { 0, 1, 2 }, clip.Frames.Select(frame => frame.SourceIndex)); - CollectionAssert.AreEqual(new[] { 100, 300, 80 }, clip.Frames.Select(frame => frame.DurationMs)); - - var firstIds = sprites.ToDictionary(sprite => sprite.name, GetLocalFileId); - var secondPreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(secondPreview.HasErrors, Is.False, JoinIssues(secondPreview)); - Assert.That(secondPreview.Sources.Single().SpriteDiffs.All(diff => - diff.Kind == FrameAnimationSpriteChangeKind.Unchanged), Is.True); - Assert.That(secondPreview.Sources.Single().ClipDiffs.All(diff => - diff.Kind == FrameAnimationImportChangeKind.Unchanged), Is.True); - Assert.That(FrameAnimationImportService.Apply(secondPreview, true, out var secondError), - Is.True, secondError); - - var secondIds = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType() - .ToDictionary(sprite => sprite.name, GetLocalFileId); - CollectionAssert.AreEquivalent(firstIds, secondIds); - Assert.That(clip.Frames[0].Sprite, Is.SameAs(clip.Frames[1].Sprite)); - } - - [Test] - public void WritableRefresh_SharedSlotSplitAndMergeKeepDeterministicSpriteIdentity() - { - var graph = CreateWritableGraph(SharedPairJson(true)); - var mergedPreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(FrameAnimationImportService.Apply(mergedPreview, true, out var mergedError), - Is.True, mergedError); - var canonical = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType().Single(); - var canonicalId = GetLocalFileId(canonical); - - WriteJson(SharedPairJson(false)); - var splitPreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(splitPreview.HasErrors, Is.False, JoinIssues(splitPreview)); - Assert.That(splitPreview.Sources.Single().SpriteDiffs.Count(diff => - diff.Kind == FrameAnimationSpriteChangeKind.Added), Is.EqualTo(1)); - Assert.That(FrameAnimationImportService.Apply(splitPreview, true, out var splitError), - Is.True, splitError); - var splitSprites = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType() - .ToDictionary(sprite => sprite.name); - Assert.That(splitSprites.Keys, Is.EquivalentTo(new[] { "FrameA", "FrameB" })); - Assert.That(GetLocalFileId(splitSprites["FrameA"]), Is.EqualTo(canonicalId)); - var clip = graph.Clips.Single(item => item.ImportInfo.SourceTagName == "SharedPair"); - Assert.That(clip.Frames[0].Sprite, Is.Not.SameAs(clip.Frames[1].Sprite)); - - WriteJson(SharedPairJson(true)); - var mergedAgainPreview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(mergedAgainPreview.HasErrors, Is.False, JoinIssues(mergedAgainPreview)); - Assert.That(mergedAgainPreview.Sources.Single().SpriteDiffs.Any(diff => - diff.Kind == FrameAnimationSpriteChangeKind.Retained && diff.FrameName == "FrameB"), Is.True); - Assert.That(FrameAnimationImportService.Apply(mergedAgainPreview, true, out var mergedAgainError), - Is.True, mergedAgainError); - Assert.That(GetLocalFileId(AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType() - .Single(sprite => sprite.name == "FrameA")), Is.EqualTo(canonicalId)); - Assert.That(clip.Frames[0].Sprite, Is.SameAs(clip.Frames[1].Sprite)); - } - - [Test] - public void ReadOnlyPreview_PreservesImporterAndMapsSharedSprites() - { - var writableGraph = CreateWritableGraph(ObjectJson()); - var writablePreview = FrameAnimationImportService.PreviewAll(writableGraph); - Assert.That(FrameAnimationImportService.Apply(writablePreview, true, out var objectApplyError), - Is.True, objectApplyError); - var importer = (TextureImporter)AssetImporter.GetAtPath(TexturePath); - var before = EditorJsonUtility.ToJson(importer); - - var readOnlyGraph = ScriptableObject.CreateInstance(); - readOnlyGraph.Configure("ReadOnly", "Read Only", Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); - readOnlyGraph.AddImportSource(new FrameAnimationImportSource( - "Read Only", AssetDatabase.LoadAssetAtPath(TexturePath), - AssetDatabase.LoadAssetAtPath(JsonPath), new Vector2(0.5f, 0.5f), false)); - AssetDatabase.CreateAsset(readOnlyGraph, TestRoot + "/ReadOnlyGraph.asset"); - AssetDatabase.SaveAssets(); - - var objectPreview = FrameAnimationImportService.PreviewAll(readOnlyGraph); - Assert.That(objectPreview.HasErrors, Is.False, JoinIssues(objectPreview)); - Assert.That(objectPreview.HasSpriteChanges, Is.False); - Assert.That(EditorJsonUtility.ToJson(importer), Is.EqualTo(before)); - - var sharedWritableGraph = CreateWritableGraph(SharedRectJson(), TestRoot + "/SharedImportGraph.asset"); - var sharedPreview = FrameAnimationImportService.PreviewAll(sharedWritableGraph); - Assert.That(FrameAnimationImportService.Apply(sharedPreview, true, out var sharedApplyError), - Is.True, sharedApplyError); - before = EditorJsonUtility.ToJson(importer); - - var readOnlySharedGraph = ScriptableObject.CreateInstance(); - readOnlySharedGraph.Configure("ReadOnlyShared", "Read Only Shared", Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); - readOnlySharedGraph.AddImportSource(new FrameAnimationImportSource( - "Read Only Shared", AssetDatabase.LoadAssetAtPath(TexturePath), - AssetDatabase.LoadAssetAtPath(JsonPath), new Vector2(0.5f, 0.5f), false)); - AssetDatabase.CreateAsset(readOnlySharedGraph, TestRoot + "/ReadOnlySharedGraph.asset"); - AssetDatabase.SaveAssets(); - - var sharedReadOnlyPreview = FrameAnimationImportService.PreviewAll(readOnlySharedGraph); - Assert.That(sharedReadOnlyPreview.HasErrors, Is.False, JoinIssues(sharedReadOnlyPreview)); - Assert.That(sharedReadOnlyPreview.HasSpriteChanges, Is.False); - Assert.That(sharedReadOnlyPreview.Sources.Single().SpritePlan.ReadOnlySpritesBySourceIndex[0], - Is.SameAs(sharedReadOnlyPreview.Sources.Single().SpritePlan.ReadOnlySpritesBySourceIndex[1])); - Assert.That(FrameAnimationImportService.Apply(sharedReadOnlyPreview, false, out var error), Is.True, error); - var clip = readOnlySharedGraph.Clips.Single(item => item.ImportInfo.SourceTagName == "Shared"); - Assert.That(clip.Frames[0].Sprite, Is.SameAs(clip.Frames[1].Sprite)); - Assert.That(EditorJsonUtility.ToJson(importer), Is.EqualTo(before)); - } - - [Test] - public void ReadOnlyPreview_BlocksWhenSharedRectHasNoSprite() - { - CreateTexture(); - WriteJson(SharedPairJson(true)); - var graph = ScriptableObject.CreateInstance(); - graph.Configure("MissingSprite", "Missing Sprite", Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); - graph.AddImportSource(new FrameAnimationImportSource( - "Read Only", AssetDatabase.LoadAssetAtPath(TexturePath), - AssetDatabase.LoadAssetAtPath(JsonPath), new Vector2(0.5f, 0.5f), false)); - AssetDatabase.CreateAsset(graph, TestRoot + "/MissingSpriteGraph.asset"); - AssetDatabase.SaveAssets(); - - var preview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(preview.HasErrors, Is.True); - Assert.That(preview.Sources.Single().Issues.Any(issue => - issue.Code == FrameAnimationImportIssueCode.SpriteMatchFailed), Is.True); - Assert.That(FrameAnimationImportService.Apply(preview, false, out _), Is.False); - Assert.That(graph.Clips, Is.Empty); - } - - [Test] - public void Preview_BlocksMultipleWritableOwnersWithoutMutation() - { - var graph = CreateWritableGraph(ObjectJson()); - var source = graph.ImportSources.Single(); - graph.AddImportSource(new FrameAnimationImportSource( - "Second Owner", source.Texture, source.AsepriteJson, source.Pivot, true)); - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - - var importer = (TextureImporter)AssetImporter.GetAtPath(TexturePath); - var before = EditorJsonUtility.ToJson(importer); - var preview = FrameAnimationImportService.PreviewAll(graph); - Assert.That(preview.HasErrors, Is.True); - Assert.That(preview.Sources.SelectMany(item => item.Issues).Any(issue => - issue.Code == FrameAnimationImportIssueCode.TextureOwnershipConflict), Is.True); - Assert.That(FrameAnimationImportService.Apply(preview, true, out _), Is.False); - Assert.That(EditorJsonUtility.ToJson(importer), Is.EqualTo(before)); - Assert.That(graph.Clips, Is.Empty); - } - - private FrameAnimationGraph CreateWritableGraph(string json, string graphAssetPath = null) + private FrameAnimationGraph CreateWritableGraph(string json) { CreateTexture(); WriteJson(json); var graph = ScriptableObject.CreateInstance(); - graph.Configure("ImportGraph", "Import Graph", Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); + graph.Configure( + "ImportGraph", + "Import Graph", + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + string.Empty); graph.AddImportSource(new FrameAnimationImportSource( "中文来源", AssetDatabase.LoadAssetAtPath(TexturePath), AssetDatabase.LoadAssetAtPath(JsonPath), new Vector2(0.5f, 0.5f), true)); - AssetDatabase.CreateAsset(graph, graphAssetPath ?? GraphPath); + AssetDatabase.CreateAsset(graph, GraphPath); AssetDatabase.SaveAssets(); return graph; } @@ -345,42 +184,31 @@ namespace AibisDream.FrameAnimation.Tests.EditMode private void CreateTexture() { var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false); - var pixels = Enumerable.Range(0, 8) + texture.SetPixels(Enumerable.Range(0, 8) .Select(index => index < 4 ? Color.red : Color.green) - .ToArray(); - texture.SetPixels(pixels); + .ToArray()); texture.Apply(); File.WriteAllBytes(TexturePath, texture.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(texture); - AssetDatabase.ImportAsset(TexturePath, ImportAssetOptions.ForceSynchronousImport); + AssetDatabase.ImportAsset( + TexturePath, + ImportAssetOptions.ForceSynchronousImport); } private void WriteJson(string json) { File.WriteAllText(JsonPath, json, new UTF8Encoding(false)); - AssetDatabase.ImportAsset(JsonPath, ImportAssetOptions.ForceSynchronousImport); + AssetDatabase.ImportAsset( + JsonPath, + ImportAssetOptions.ForceSynchronousImport); } - private static string ObjectJson(int duration = 120, bool includeIdle = true) + private static string ObjectJson(int duration = 120) { - var tags = includeIdle - ? "{\"name\":\"待机\",\"from\":0,\"to\":0,\"direction\":\"forward\"}," + - "{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}" - : "{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}"; return "{" + "\"frames\":{" + $"\"第二帧.png\":{{\"frame\":{{\"x\":0,\"y\":0,\"w\":2,\"h\":2}},\"rotated\":false,\"trimmed\":false,\"duration\":{duration},\"spriteSourceSize\":{{\"x\":0,\"y\":0,\"w\":2,\"h\":2}},\"sourceSize\":{{\"w\":2,\"h\":2}}}}," + "\"第一帧.png\":{\"frame\":{\"x\":2,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":80,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}}," + - $"\"meta\":{{\"image\":\"Atlas.png\",\"size\":{{\"w\":4,\"h\":2}},\"frameTags\":[{tags}]}}" + - "}"; - } - - private static string ArrayJson() - { - return "{" + - "\"frames\":[" + - "{\"filename\":\"第二帧.png\",\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":120,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}," + - "{\"filename\":\"第一帧.png\",\"frame\":{\"x\":2,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":80,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}]," + "\"meta\":{\"image\":\"Atlas.png\",\"size\":{\"w\":4,\"h\":2},\"frameTags\":[" + "{\"name\":\"待机\",\"from\":0,\"to\":0,\"direction\":\"forward\"}," + "{\"name\":\"眨眼\",\"from\":0,\"to\":1,\"direction\":\"pingpong\"}]}}"; @@ -397,26 +225,13 @@ namespace AibisDream.FrameAnimation.Tests.EditMode "{\"name\":\"Shared\",\"from\":0,\"to\":2,\"direction\":\"forward\"}]}}"; } - private static string SharedPairJson(bool merged) - { - var secondX = merged ? 0 : 2; - return "{" + - "\"frames\":{" + - "\"FrameA\":{\"frame\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"rotated\":false,\"trimmed\":false,\"duration\":100,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":2,\"h\":2},\"sourceSize\":{\"w\":2,\"h\":2}}," + - $"\"FrameB\":{{\"frame\":{{\"x\":{secondX},\"y\":0,\"w\":2,\"h\":2}},\"rotated\":false,\"trimmed\":false,\"duration\":300,\"spriteSourceSize\":{{\"x\":0,\"y\":0,\"w\":2,\"h\":2}},\"sourceSize\":{{\"w\":2,\"h\":2}}}}}}," + - "\"meta\":{\"image\":\"Atlas.png\",\"size\":{\"w\":4,\"h\":2},\"frameTags\":[" + - "{\"name\":\"SharedPair\",\"from\":0,\"to\":1,\"direction\":\"forward\"}]}}"; - } - - private static long GetLocalFileId(Sprite sprite) - { - Assert.That(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sprite, out _, out long localId), Is.True); - return localId; - } - private static string JoinIssues(FrameAnimationImportPreview preview) { - return string.Join("\n", preview.Sources.SelectMany(source => source.Issues).Select(issue => issue.Message)); + return string.Join( + "\n", + preview.Sources + .SelectMany(source => source.Issues) + .Select(issue => issue.Message)); } private static void EnsureFolder(string path) @@ -427,9 +242,7 @@ namespace AibisDream.FrameAnimation.Tests.EditMode { var next = current + "/" + segments[index]; if (!AssetDatabase.IsValidFolder(next)) - { AssetDatabase.CreateFolder(current, segments[index]); - } current = next; } } diff --git a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs b/Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs deleted file mode 100644 index 30c96f874..000000000 --- a/Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using AibisDream.FrameAnimation.Editor; -using NUnit.Framework; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace AibisDream.FrameAnimation.Tests.EditMode -{ - public sealed class FrameAnimationPreviewTests - { - private readonly List objects = new List(); - - [TearDown] - public void TearDown() - { - foreach (var item in objects.Where(item => item != null)) - { - UnityEngine.Object.DestroyImmediate(item); - } - objects.Clear(); - } - - [Test] - public void NodeResolver_AppliesNodeOverridesWithoutChangingClip() - { - var clip = Clip("Clip", 0.5f, FrameClipEndBehavior.HoldLastFrame, 100); - var node = new AnimationNode(clip.Id, speedOverride: 3f, - endBehaviorOverride: FrameClipEndBehavior.HideTarget); - var graph = Graph(new[] { clip }, new[] { node }, Array.Empty(), - Array.Empty()); - - Assert.That(FrameAnimationResolver.TryResolveNode(graph, node, out var plan, out var error), - Is.True, error.ToString()); - Assert.That(plan.Steps.Single().PlaybackSpeed, Is.EqualTo(3f)); - Assert.That(plan.Steps.Single().TerminalEndBehavior, Is.EqualTo(FrameClipEndBehavior.HideTarget)); - Assert.That(plan.Steps.Single().NodeId, Is.EqualTo(node.InternalId)); - Assert.That(clip.Speed, Is.EqualTo(0.5f)); - } - - [Test] - public void Timeline_FlattensFlowUsingEffectiveStepDurations() - { - var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100, 100); - var second = Clip("Second", 1f, FrameClipEndBehavior.HoldLastFrame, 300); - var firstNode = new AnimationNode(first.Id, speedOverride: 2f); - var secondNode = new AnimationNode(second.Id, speedOverride: 0.5f); - var edge = new AnimationEdge(firstNode.InternalId, secondNode.InternalId); - var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId); - var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode }, new[] { edge }, new[] { flow }); - Assert.That(FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out _), Is.True); - - var timeline = new FrameAnimationPreviewTimeline(plan); - - Assert.That(timeline.Segments.Count, Is.EqualTo(3)); - Assert.That(timeline.Segments[2].NodeId, Is.EqualTo(secondNode.InternalId)); - Assert.That(timeline.Segments[2].StartSeconds, Is.EqualTo(0.1d).Within(0.000001d)); - Assert.That(timeline.DisplayDurationSeconds, Is.EqualTo(0.7d).Within(0.000001d)); - Assert.That(timeline.IsInfinite, Is.False); - } - - [Test] - public void Timeline_LoopUsesOneCycle_AndZeroSpeedStopsReachability() - { - var loopClip = Clip("Loop", 1f, FrameClipEndBehavior.Loop, 100, 200); - var loopNode = new AnimationNode(loopClip.Id, endBehaviorOverride: FrameClipEndBehavior.Loop); - var loopFlow = new AnimationFlow("LoopFlow", "Loop", loopNode.InternalId); - var loopGraph = Graph(new[] { loopClip }, new[] { loopNode }, Array.Empty(), new[] { loopFlow }); - FrameAnimationResolver.TryResolve(loopGraph, loopFlow.Id, default, out var loopPlan, out _); - var loopTimeline = new FrameAnimationPreviewTimeline(loopPlan); - - Assert.That(loopTimeline.IsInfinite, Is.True); - Assert.That(loopTimeline.Segments.Count, Is.EqualTo(2)); - Assert.That(loopTimeline.DisplayDurationSeconds, Is.EqualTo(0.3d).Within(0.000001d)); - - var blockedClip = Clip("Blocked", 1f, FrameClipEndBehavior.HoldLastFrame, 100, 100); - var blockedNode = new AnimationNode(blockedClip.Id, speedOverride: 0f); - var blockedGraph = Graph(new[] { blockedClip }, new[] { blockedNode }, Array.Empty(), - Array.Empty()); - FrameAnimationResolver.TryResolveNode(blockedGraph, blockedNode, out var blockedPlan, out _); - var blockedTimeline = new FrameAnimationPreviewTimeline(blockedPlan); - - Assert.That(blockedTimeline.IsBlocked, Is.True); - Assert.That(blockedTimeline.IsInfinite, Is.True); - Assert.That(blockedTimeline.Segments.Count, Is.EqualTo(1)); - Assert.That(blockedTimeline.Segments[0].IsBlocked, Is.True); - } - - [Test] - public void Coordinator_FlowPreviewCrossesNodesAndRestoresAutomaticPolicyOnStop() - { - var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100); - var second = Clip("Second", 1f, FrameClipEndBehavior.Loop, 100); - var firstNode = new AnimationNode(first.Id); - var secondNode = new AnimationNode(second.Id, endBehaviorOverride: FrameClipEndBehavior.Loop); - var edge = new AnimationEdge(firstNode.InternalId, secondNode.InternalId); - var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId); - var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode }, new[] { edge }, new[] { flow }); - var coordinator = new FrameAnimationPreviewCoordinator(); - coordinator.SetGraph(graph); - coordinator.SetTarget(FrameAnimationPreviewTargetKind.Flow, flow); - - coordinator.Play(); - coordinator.Tick(0.1d, graph.Nodes); - - Assert.That(coordinator.IsFlowPreview, Is.True); - Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId)); - Assert.That(coordinator.GetNodeDisplay(firstNode).FlowState, Is.EqualTo(FrameAnimationFlowElementState.Passed)); - Assert.That(coordinator.GetNodeDisplay(secondNode).FlowState, Is.EqualTo(FrameAnimationFlowElementState.Current)); - - coordinator.Stop(); - Assert.That(coordinator.IsFlowPreview, Is.False); - Assert.That(coordinator.NodePolicy, Is.EqualTo(NodePreviewPolicy.SelectedOnly)); - } - - [Test] - public void Coordinator_FlowSeekAndStepCrossNodeBoundaries() - { - var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100); - var second = Clip("Second", 1f, FrameClipEndBehavior.HoldLastFrame, 200, 200); - var firstNode = new AnimationNode(first.Id); - var secondNode = new AnimationNode(second.Id); - var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId); - var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode }, - new[] { new AnimationEdge(firstNode.InternalId, secondNode.InternalId) }, new[] { flow }); - var coordinator = new FrameAnimationPreviewCoordinator(); - coordinator.SetGraph(graph); - coordinator.SetTarget(FrameAnimationPreviewTargetKind.Flow, flow); - - coordinator.Seek(0.1d); - Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId)); - Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(0)); - coordinator.Step(1); - Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId)); - Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(1)); - coordinator.Step(-1); - Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(0)); - } - - [Test] - public void GraphViewPreview_RendersOnePreviewPerNodeWithoutMutatingGraph() - { - var clip = Clip("Clip", 1f, FrameClipEndBehavior.Loop, 100); - var first = new AnimationNode(clip.Id); - var second = new AnimationNode(clip.Id); - var graph = Graph(new[] { clip }, new[] { first, second }, Array.Empty(), - Array.Empty()); - var before = JsonUtility.ToJson(graph); - var view = new FrameAnimationGraphView(); - view.Bind(graph, Array.Empty(), string.Empty); - var coordinator = new FrameAnimationPreviewCoordinator(); - coordinator.SetGraph(graph); - coordinator.SetNodePolicy(NodePreviewPolicy.AllVisible); - - view.ApplyPreview(coordinator); - - Assert.That(view.Query().ToList().Count, Is.EqualTo(2)); - Assert.That(JsonUtility.ToJson(graph), Is.EqualTo(before)); - } - - [Test] - public void FlowColorPalette_IsOpaqueAndReadableForExtremeColors() - { - var dark = FrameAnimationFlowColorUtility.CreatePalette(new Color(0.005f, 0.01f, 0.02f, 0f)); - var light = FrameAnimationFlowColorUtility.CreatePalette(new Color(1f, 0.98f, 0.9f, 0.1f)); - - Assert.That(dark.Raw.a, Is.EqualTo(1f)); - Assert.That(dark.Stroke.maxColorComponent, Is.GreaterThan(dark.Raw.maxColorComponent)); - Assert.That(light.Raw.a, Is.EqualTo(1f)); - Assert.That(FrameAnimationFlowColorUtility.Luminance(light.Text), - Is.LessThan(FrameAnimationFlowColorUtility.Luminance(light.Header))); - } - - [Test] - public void GraphView_FocusFlowColorSwitchAndValidationOverride() - { - var clip = Clip("Clip", 1f, FrameClipEndBehavior.HoldLastFrame, 100); - var first = new AnimationNode(clip.Id, internalId: "first"); - var second = new AnimationNode(clip.Id, internalId: "second"); - var edge = new AnimationEdge(first.InternalId, second.InternalId, internalId: "edge"); - var redFlow = new AnimationFlow("Red", "Red", first.InternalId); - var blueFlow = new AnimationFlow("Blue", "Blue", first.InternalId); - var graph = Graph(new[] { clip }, new[] { first, second }, new[] { edge }, new[] { redFlow, blueFlow }); - graph.EditorData.GetOrCreateFlowData(redFlow.Id, Color.red); - graph.EditorData.GetOrCreateFlowData(blueFlow.Id, Color.blue); - var view = new FrameAnimationGraphView(); - view.Bind(graph, Array.Empty(), redFlow.Id); - var edgeView = view.Query().First(); - var red = edgeView.edgeControl.inputColor; - - view.SetFocusedFlow(blueFlow.Id); - var blue = edgeView.edgeControl.inputColor; - Assert.That(red.r, Is.GreaterThan(red.b)); - Assert.That(blue.b, Is.GreaterThan(blue.r)); - Assert.That(view.Query(className: "fa-node--focused").ToList().Count, - Is.EqualTo(2)); - - var issue = new FrameAnimationEditorIssue(FrameAnimationValidationSeverity.Error, "BrokenEdge", - new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, edge), "Broken", string.Empty); - view.Bind(graph, new[] { issue }, redFlow.Id); - edgeView = view.Query().First(); - edgeView.SetPreviewState(FrameAnimationFlowElementState.Current); - Assert.That(edgeView.edgeControl.inputColor.r, Is.EqualTo(223f / 255f).Within(0.003f)); - Assert.That(edgeView.edgeControl.inputColor.g, Is.EqualTo(106f / 255f).Within(0.003f)); - } - - [Test] - public void PreviewSample_IsIsolatedAndStructurallyPlayable() - { - var sample = AssetDatabase.LoadAssetAtPath( - "Assets/GameContent/Test/FrameAnimation/Preview/PreviewSampleGraph.asset"); - - Assert.That(sample, Is.Not.Null); - Assert.That(sample.Clips.Count, Is.EqualTo(3)); - Assert.That(sample.Flows.Count, Is.EqualTo(5)); - Assert.That(sample.Clips.Any(item => item.Frames.Any(frame => frame.Sprite == null)), Is.True); - foreach (var flow in sample.Flows) - { - Assert.That(FrameAnimationResolver.TryResolve(sample, flow.Id, default, out _, out var error), - Is.True, error.ToString()); - } - } - - [Test] - public void Workbench_CreateGUIBuildsTabsAndSelectionPreviewForClipNodeAndFlow() - { - var sample = AssetDatabase.LoadAssetAtPath( - "Assets/GameContent/Test/FrameAnimation/Preview/PreviewSampleGraph.asset"); - var window = ScriptableObject.CreateInstance(); - try - { - typeof(FrameAnimationGraphEditorWindow).GetField("graph", - BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(window, sample); - typeof(FrameAnimationGraphEditorWindow).GetMethod("CreateGUI", - BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, null); - - Assert.That(window.rootVisualElement.Q("fa-workbench"), Is.Not.Null); - Assert.That(window.rootVisualElement.Query().ToList().Count, - Is.GreaterThanOrEqualTo(1)); - - var setSelection = typeof(FrameAnimationGraphEditorWindow).GetMethod("SetSelection", - BindingFlags.Instance | BindingFlags.NonPublic); - - var clip = sample.Clips.First(item => item != null); - setSelection?.Invoke(window, new object[] - { - new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip), - false - }); - AssertSelectionPreview(window, "Clip Preview"); - - var node = sample.Nodes.First(item => item != null); - setSelection?.Invoke(window, new object[] - { - new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node), - false - }); - AssertSelectionPreview(window, "Node Preview"); - - var flow = sample.Flows.First(item => item != null); - setSelection?.Invoke(window, new object[] - { - new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow), - false - }); - AssertSelectionPreview(window, "Flow Preview"); - } - finally - { - UnityEngine.Object.DestroyImmediate(window); - } - } - - private static void AssertSelectionPreview(FrameAnimationGraphEditorWindow window, string expectedTitle) - { - var panel = window.rootVisualElement.Q("selection-preview-panel"); - Assert.That(panel, Is.Not.Null); - Assert.That(panel.style.display.value, Is.EqualTo(DisplayStyle.Flex)); - Assert.That(window.rootVisualElement.Q