using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Tests.EditMode
{
///
/// 帧动画运行时模型的稳定契约。编辑器窗口与具体布局不在此处测试。
///
public sealed class FrameAnimationCoreTests
{
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 GraphSerialization_PreservesRuntimeIdentityAndOverrides()
{
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(
new[] { clip },
new[] { node },
Array.Empty(),
new[] { flow },
flow.Id);
var clone = Track(ScriptableObject.CreateInstance());
EditorJsonUtility.FromJsonOverwrite(EditorJsonUtility.ToJson(graph), 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 InvalidTopology_IsRejectedBeforePlayback()
{
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
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(
new[] { clip },
new[] { first, second, third },
new[]
{
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,
default,
out _,
out var error);
Assert.That(report.HasErrors, Is.True);
Assert.That(resolved, Is.False);
Assert.That(
error.Code,
Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData));
}
[Test]
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 flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId);
var graph = CreateGraph(
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),
Is.True,
error.ToString());
var session = new FrameAnimationPlaybackSession(plan);
session.Start(_ => { });
var evaluation = session.Evaluate(1.05d, 1f, _ => { });
Assert.That(evaluation.IsCompleted, Is.False);
Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id));
Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId));
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
}
[Test]
public void EndBehavior_NodeOverrideHasHighestPrecedence()
{
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(
new[] { clip },
new[] { node },
Array.Empty(),
new[] { flow },
flow.Id);
Assert.That(
FrameAnimationResolver.TryResolve(
graph,
flow.Id,
new FrameAnimationPlayOptions(FrameClipEndBehavior.Loop),
out var plan,
out _),
Is.True);
Assert.That(
plan.Steps[0].TerminalEndBehavior,
Is.EqualTo(FrameClipEndBehavior.HideTarget));
}
[Test]
public void PlaybackHandle_CompletesExactlyOnce()
{
var handle = new FrameAnimationPlaybackHandle(42);
var callbackCount = 0;
handle.RegisterCompleted(_ => callbackCount++);
var completion = new FrameAnimationPlaybackResult(
42,
"Clip",
FrameAnimationCompletionReason.Completed,
FrameAnimationPlaybackError.None);
Assert.That(handle.TryComplete(completion), Is.True);
Assert.That(handle.TryComplete(completion), Is.False);
handle.RegisterCompleted(_ => callbackCount++);
Assert.That(callbackCount, Is.EqualTo(2));
Assert.That(handle.Result.RequestId, Is.EqualTo(42));
}
private FrameClip CreateClip(
string id,
FrameClipEndBehavior endBehavior,
float speed,
params int[] durations)
{
var clip = Track(ScriptableObject.CreateInstance());
clip.Configure(
id,
id,
durations.Select((duration, index) =>
new FrameAnimationFrame(null, duration, $"frame_{index}", index)),
speed,
endBehavior);
return clip;
}
private FrameAnimationGraph CreateGraph(
IEnumerable clips,
IEnumerable nodes,
IEnumerable edges,
IEnumerable flows,
string defaultPlayableId)
{
var graph = Track(ScriptableObject.CreateInstance());
graph.Configure(
"Graph",
"Graph",
clips,
nodes,
edges,
flows,
defaultPlayableId);
return graph;
}
private T Track(T value) where T : UnityEngine.Object
{
createdObjects.Add(value);
return value;
}
}
}