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