228 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|