// JayAnimator_EaseAndMathTests.cpp
// Tests for FjTweenUtils utilities and easings.
// Module: JayAnimatorTests | Namespace path: JayAnimatorTests.*
#include "Misc/AutomationTest.h"
#include "Math/Quat.h"
#include "Curves/CurveFloat.h"
#include "Curves/RichCurve.h"
#include "JayAnimator/Public/jTweenUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
// <Running unattended>
// <UnrealEditor-Cmd.exe> <Project.uproject> -unattended -AutomationRunTests="JayAnimatorTests.*"
namespace JTTest
{
static constexpr float Eps = 1e-4f;
static constexpr float TightEps= 1e-6f;
FORCEINLINE bool Near(float A, float B, float Tol = Eps)
{
return FMath::IsNearlyEqual(A, B, Tol);
}
FORCEINLINE bool NearVec(const FVector& A, const FVector& B, float Tol = Eps)
{
return A.Equals(B, Tol);
}
FORCEINLINE bool NearRot(const FRotator& A, const FRotator& B, float Tol = 1e-2f)
{
// Compare via quaternions to avoid wrap issues
const FQuat QA = A.Quaternion().GetNormalized();
const FQuat QB = B.Quaternion().GetNormalized();
return QA.Equals(QB, Tol);
}
}
// ---------- LERP (float / vector / rotator) ----------
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Lerp_Float_Basic,
"JayAnimatorTests.Math.Lerp.Float.Basic",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Lerp_Float_Basic::RunTest(const FString&)
{
using namespace JTTest;
TestTrue(TEXT("t=0 -> A"), Near(FjTweenUtils::Lerp(2.f, 10.f, 0.f), 2.f));
TestTrue(TEXT("t=1 -> B"), Near(FjTweenUtils::Lerp(2.f, 10.f, 1.f), 10.f));
TestTrue(TEXT("t=0.3"), Near(FjTweenUtils::Lerp(0.f, 100.f, 0.3f), 30.f));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Lerp_Vector_Basic,
"JayAnimatorTests.Math.Lerp.Vector.Basic",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Lerp_Vector_Basic::RunTest(const FString&)
{
using namespace JTTest;
const FVector A(0,0,0), B(10,5,-2);
TestTrue(TEXT("t=0.3 vec"),
NearVec(FjTweenUtils::Lerp(A, B, 0.3f), FVector(3.f, 1.5f, -0.6f)));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Lerp_Rotator_UsesSlerp,
"JayAnimatorTests.Math.Lerp.Rotator.SlerpMid",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Lerp_Rotator_UsesSlerp::RunTest(const FString&)
{
using namespace JTTest;
const FRotator R0(0, 0, 0);
const FRotator R1(0, 180, 0); // halfway should be around yaw 90 (short path across 180 is still 90)
const FRotator Mid = FjTweenUtils::Lerp(R0, R1, 0.5f);
TestTrue(TEXT("mid ~ 90 yaw"), FMath::IsNearlyEqual(Mid.Yaw, 90.f, 1.0f) || NearRot(Mid, FRotator(0,90,0)));
return true;
}
// ---------- MAP / UNLERP / CLAMP01 ----------
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Map_RoundTrip,
"JayAnimatorTests.Math.Map.RoundTrip",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Map_RoundTrip::RunTest(const FString&)
{
using namespace JTTest;
const float v = 25.f;
const float t = FjTweenUtils::Unlerp(10.f, 30.f, v); // expect 0.75
TestTrue(TEXT("Unlerp ok"), Near(t, 0.75f));
const float v2 = FjTweenUtils::Map(v, 10.f, 30.f, 100.f, 200.f); // expect 175
TestTrue(TEXT("Map ok"), Near(v2, 175.f));
// Round trip: Map back to original range
const float v3 = FjTweenUtils::Map(v2, 100.f, 200.f, 10.f, 30.f);
TestTrue(TEXT("Map invertible (linear)"), Near(v3, v));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Clamp01_Bounds,
"JayAnimatorTests.Math.Clamp01.Bounds",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Clamp01_Bounds::RunTest(const FString&)
{
using namespace JTTest;
TestTrue(TEXT("below 0"), Near(FjTweenUtils::Clamp01(-0.2f), 0.f));
TestTrue(TEXT("in range"), Near(FjTweenUtils::Clamp01(0.2f), 0.2f));
TestTrue(TEXT("above 1"), Near(FjTweenUtils::Clamp01(1.5f), 1.f));
return true;
}
// ---------- INDIVIDUAL EASINGS: spot checks at 0, 0.5, 1 ----------
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Ease_Quadratic_Spots,
"JayAnimatorTests.Ease.Quadratic.Spots",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Ease_Quadratic_Spots::RunTest(const FString&)
{
using namespace JTTest;
TestTrue(TEXT("QuadIn(0)=0"), Near(FjTweenUtils::QuadraticIn(0.f), 0.f, TightEps));
TestTrue(TEXT("QuadIn(1)=1"), Near(FjTweenUtils::QuadraticIn(1.f), 1.f, TightEps));
TestTrue(TEXT("QuadIn(0.5)=0.25"), Near(FjTweenUtils::QuadraticIn(0.5f), 0.25f, 1e-5f));
TestTrue(TEXT("QuadOut(0)=0"), Near(FjTweenUtils::QuadraticOut(0.f), 0.f, TightEps));
TestTrue(TEXT("QuadOut(1)=1"), Near(FjTweenUtils::QuadraticOut(1.f), 1.f, TightEps));
TestTrue(TEXT("QuadOut(0.5)=0.75"), Near(FjTweenUtils::QuadraticOut(0.5f), 0.75f, 1e-5f));
// SinusoidalInOut mid should be 0.5 exactly
TestTrue(TEXT("SineIO(0.5)=0.5"), Near(FjTweenUtils::SinusoidalInOut(0.5f), 0.5f, 1e-6f));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Ease_Expo_Bounds,
"JayAnimatorTests.Ease.Exponential.BoundsAndEdges",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Ease_Expo_Bounds::RunTest(const FString&)
{
using namespace JTTest;
// As implemented: hard-clamped edges for numerical stability
TestTrue(TEXT("ExpIn(0)=0"), Near(FjTweenUtils::ExponentialIn(0.f), 0.f, TightEps));
TestTrue(TEXT("ExpIn(1)=1? (not guaranteed)"), FjTweenUtils::ExponentialIn(1.f) > 0.99f);
TestTrue(TEXT("ExpOut(0)=0? (near)"), FjTweenUtils::ExponentialOut(0.f) < 0.01f);
TestTrue(TEXT("ExpOut(1)=1"), Near(FjTweenUtils::ExponentialOut(1.f), 1.f, TightEps));
// InOut clamps 0/1 and is continuous
TestTrue(TEXT("ExpIO(0)=0"), Near(FjTweenUtils::ExponentialInOut(0.f), 0.f, TightEps));
TestTrue(TEXT("ExpIO(1)=1"), Near(FjTweenUtils::ExponentialInOut(1.f), 1.f, TightEps));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Ease_Range_And_Monotonicity,
"JayAnimatorTests.Ease.General.RangeAndMonotonicity",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Ease_Range_And_Monotonicity::RunTest(const FString&)
{
// Sample a few easing functions to ensure outputs remain within [0,1]
// and (weakly) increase over t in [0,1].
auto Check = [this](auto Func, const TCHAR* Name)
{
float Prev = -1.f;
for (int i=0;i<=100;++i)
{
const float t = i / 100.f;
const float y = Func(t);
if (!(y >= -0.001f && y <= 1.001f))
{
AddError(FString::Printf(TEXT("%s out of [0,1]: t=%g y=%g"), Name, t, y));
return false;
}
if (i>0 && y + 1e-3f < Prev)
{
AddError(FString::Printf(TEXT("%s not monotonic enough: t=%g y=%g prev=%g"), Name, t, y, Prev));
return false;
}
Prev = y;
}
return true;
};
bool Ok = true;
Ok &= Check(&FjTweenUtils::Linear, TEXT("Linear"));
Ok &= Check(&FjTweenUtils::QuadraticIn, TEXT("QuadraticIn"));
Ok &= Check(&FjTweenUtils::QuadraticOut, TEXT("QuadraticOut"));
Ok &= Check(&FjTweenUtils::CubicInOut, TEXT("CubicInOut"));
Ok &= Check(&FjTweenUtils::SinusoidalInOut, TEXT("SinusoidalInOut"));
Ok &= Check(&FjTweenUtils::CircularInOut, TEXT("CircularInOut"));
Ok &= Check(&FjTweenUtils::BackInOut, TEXT("BackInOut"));
Ok &= Check(&FjTweenUtils::BounceInOut, TEXT("BounceInOut"));
return Ok;
}
// ---------- Ease dispatcher & curve ----------
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Ease_Dispatcher_SelectsCorrectFunc,
"JayAnimatorTests.Ease.Dispatcher.SelectsCorrect",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Ease_Dispatcher_SelectsCorrectFunc::RunTest(const FString&)
{
using namespace JTTest;
// NOTE: Requires EJTweenEasing values in your jTween.h to map to these functions in FjTweenUtils::Ease.
// Replace enum names if your values differ.
const float t = 0.5f;
#if 1
// Common spot checks (adjust enum names to your actual ones)
TestTrue(TEXT("Linear"),
Near(FjTweenUtils::Ease(EJTweenEasing::Linear, t), FjTweenUtils::Linear(t)));
TestTrue(TEXT("QuadIn"),
Near(FjTweenUtils::Ease(EJTweenEasing::QuadraticIn, t), FjTweenUtils::QuadraticIn(t)));
TestTrue(TEXT("QuadOut"),
Near(FjTweenUtils::Ease(EJTweenEasing::QuadraticOut, t), FjTweenUtils::QuadraticOut(t)));
TestTrue(TEXT("CubicInOut"),
Near(FjTweenUtils::Ease(EJTweenEasing::CubicInOut, t), FjTweenUtils::CubicInOut(t)));
TestTrue(TEXT("SineInOut"),
Near(FjTweenUtils::Ease(EJTweenEasing::SinusoidalInOut, t), FjTweenUtils::SinusoidalInOut(t)));
#endif
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FJayAnimator_Ease_ByCurve_LinearMatchesAlpha,
"JayAnimatorTests.Ease.ByCurve.LinearMatchesAlpha",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FJayAnimator_Ease_ByCurve_LinearMatchesAlpha::RunTest(const FString&)
{
// Create a transient linear curve: (0,0) -> (1,1)
UCurveFloat* Curve = NewObject<UCurveFloat>(GetTransientPackage(), NAME_None, RF_Transient);
if (!Curve)
{
AddError(TEXT("Failed to create UCurveFloat"));
return false;
}
// In UE5, UCurveFloat exposes FRichCurve FloatCurve directly
FRichCurve& Rich = Curve->FloatCurve;
Rich.Reset();
Rich.AddKey(0.f, 0.f);
Rich.AddKey(1.f, 1.f);
for (int i = 0; i <= 10; ++i)
{
const float a = i / 10.f;
const float y = FjTweenUtils::EaseByCurve(Curve, a);
if (!FMath::IsNearlyEqual(y, a, 1e-4f))
{
AddError(FString::Printf(TEXT("Curve mismatch: a=%g y=%g"), a, y));
return false;
}
}
return true;
}
#endif // WITH_DEV_AUTOMATION_TESTS