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

188 lines
9.0 KiB
C#

using System;
using System.Linq;
using AibisDream.FrameAnimation.Editor;
using NUnit.Framework;
using UnityEngine;
namespace AibisDream.FrameAnimation.Tests.EditMode
{
public sealed class FrameAnimationGraphAuthoringTests
{
private FrameAnimationGraph graph;
private FrameClip clip;
[SetUp]
public void SetUp()
{
clip = ScriptableObject.CreateInstance<FrameClip>();
clip.Configure("Idle", "Idle", new[] { new FrameAnimationFrame(null, 100, string.Empty, -1) },
1f, FrameClipEndBehavior.HoldLastFrame);
graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
graph.Configure("Graph", "Graph", new[] { clip }, Array.Empty<AnimationNode>(),
Array.Empty<AnimationEdge>(), Array.Empty<AnimationFlow>(), 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("IdleFlow"));
Assert.That(FrameAnimationGraphMutationService.CreateFlow(
graph, entry, "IdleFlow", "Idle", out var flow, out var error), Is.True, error);
Assert.That(FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, other), Is.EqualTo("IdleFlow2"));
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<AnimationNode, Vector2>
{
[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<FrameAnimationEditorIssue>(), 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"));
}
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;
}
}
}