Files
aibis-dream/Assets/Tests/FrameAnimation/EditMode/FrameAnimationEditorWorkspaceTests.cs
T

228 lines
10 KiB
C#

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<AnimationNode>(),
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 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<FrameAnimationGraph>();
graph.Configure(PathId(fileName), PathId(fileName), clips ?? Array.Empty<FrameClip>(),
nodes ?? Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>(),
flows ?? Array.Empty<AnimationFlow>(), defaultPlayableId);
AssetDatabase.CreateAsset(graph, TestRoot + "/" + fileName);
foreach (var clip in (clips ?? Array.Empty<FrameClip>()).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<FrameClip>();
clip.name = id;
clip.Configure(id, displayName ?? id, Array.Empty<FrameAnimationFrame>(), 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;
}
}
}
}