// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "EnhancedActionKeyMapping.h" #include "EnhancedPlayerInput.h" #include "InputModifiers.h" #include "InputTestFramework.h" #include "Misc/AutomationTest.h" // Tests focused on individual modifiers // THEN step wrappers to give human readable test failure output. bool TestActionIsActuated(UControllablePlayer& Data, FName ActionName) { return FInputTestHelper::GetActionData(Data, ActionName).GetValue().Get(); } bool TestActionIsNotActuated(UControllablePlayer& Data, FName ActionName) { return !FInputTestHelper::GetActionData(Data, ActionName).GetValue().Get(); } float GetActionValue(UControllablePlayer& Data, FName ActionName) { return FInputTestHelper::GetActionData(Data, ActionName).GetValue().Get(); } constexpr auto BasicModifierTestFlags = EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter; // TODO: Run as Smoke/Client? No world on RunSmokeTests startup... UControllablePlayer& ABasicModifierTest(FAutomationTestBase* Test, EInputActionValueType ForValueType) { UWorld* World = GIVEN(AnEmptyWorld()); UControllablePlayer& Data = AND(AControllablePlayer(World)); Test->TestTrue(TEXT("Controllable Player is valid"), Data.IsValid()); // TODO: Can we early out on a failed Test? AND(AnInputContextIsAppliedToAPlayer(Data, TestContext, 0)); UInputAction* Action = AND(AnInputAction(Data, TestAction, ForValueType)); return Data; } // ****************************** // Value modification tests // ****************************** IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInputModifieNegateTest, "Input.Modifiers.Negate", BasicModifierTestFlags) bool FInputModifieNegateTest::RunTest(const FString& Parameters) { UControllablePlayer& Data = GIVEN(ABasicModifierTest(this, EInputActionValueType::Axis1D)); AND(AnActionIsMappedToAKey(Data, TestContext, TestAction, TestKey)); AND(AModifierIsAppliedToAnActionMapping(Data, NewObject(), TestContext, TestAction, TestKey)); // Bool key tests. Negating a false/true bool should give -0/-1, not true/false, allowing driving a negative axis movement. // An unactuated bool input should always return false. // Test 1 - By default, output is false (0) (unactuated bools always return false) WHEN(InputIsTicked(Data)); THEN(TestActionIsNotActuated(Data, TestAction)); // Test 2 - Key press/hold/release // Actuated output is true (-1) when key is down WHEN(AKeyIsActuated(Data, TestKey)); AND(InputIsTicked(Data)); THEN(TestActionIsActuated(Data, TestAction)); AND(TestEqual(TEXT("Key actuated press value"), GetActionValue(Data, TestAction), -1.f)); // Actuated output remains true (-1) next tick WHEN(InputIsTicked(Data)); THEN(TestActionIsActuated(Data, TestAction)); AND(TestEqual(TEXT("Key actuated hold value"), GetActionValue(Data, TestAction), -1.f)); // Releasing the key reverts to false (0) WHEN(AKeyIsReleased(Data, TestKey)); AND(InputIsTicked(Data)); THEN(TestActionIsNotActuated(Data, TestAction)); InputIsTicked(Data);// Clear state // Test 3 - Axis press/hold/release WHEN(AnActionIsMappedToAKey(Data, TestContext, TestAction, TestAxis)); AND(AModifierIsAppliedToAnActionMapping(Data, NewObject(), TestContext, TestAction, TestAxis)); AND(AKeyIsActuated(Data, TestAxis, 0.5f)); AND(InputIsTicked(Data)); THEN(TestActionIsActuated(Data, TestAction)); AND(TestEqual(TEXT("Axis inverted press value"), GetActionValue(Data, TestAction), -0.5f)); // Actuated output remains constant next tick WHEN(InputIsTicked(Data)); AND(TestEqual(TEXT("Axis inverted hold value"), GetActionValue(Data, TestAction), -0.5f)); // Actuation stops when axis is released WHEN(AKeyIsReleased(Data, TestAxis)); AND(InputIsTicked(Data)); THEN(TestActionIsNotActuated(Data, TestAction)); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInputModifierScalarTest, "Input.Modifiers.Scalar", BasicModifierTestFlags) bool FInputModifierScalarTest::RunTest(const FString& Parameters) { GIVEN(UControllablePlayer& Data = ABasicModifierTest(this, EInputActionValueType::Axis1D)); AND(AnActionIsMappedToAKey(Data, TestContext, TestAction, TestAxis)); AND(UInputModifierScalar* Scalar = Cast(AModifierIsAppliedToAnActionMapping(Data, NewObject(), TestContext, TestAction, TestAxis))); check(Scalar); // Test 1 - By default (no input), output is 0 WHEN(InputIsTicked(Data)); TestEqual(TEXT("No input value"), GetActionValue(Data, TestAction), 0.f); // Test 2 - By default scale is 1. When actuated output == actuated value WHEN(AKeyIsActuated(Data, TestAxis, 0.5f)); AND(InputIsTicked(Data)); TestEqual(TEXT("Input value"), GetActionValue(Data, TestAction), 0.5f); // Multi value tests TArray TestValues = { 0.5f, -0.5f, 2.f, -1000.f, 0.f }; for(float TestValue : TestValues) { WHEN(AKeyIsActuated(Data, TestAxis, TestValue)); Scalar->Scalar = FVector::OneVector * 1.f; AND(InputIsTicked(Data)); TestEqual(TEXT("Input value (new)"), GetActionValue(Data, TestAction), TestValue * (float)Scalar->Scalar.X); // Test 3 - Modify scalar on the fly. Scalar->Scalar = FVector::OneVector * 0.5f; WHEN(InputIsTicked(Data)); TestEqual(TEXT("Input value (modify)"), GetActionValue(Data, TestAction), TestValue * (float)Scalar->Scalar.X); // Test 4 - negate Scalar->Scalar = FVector::OneVector * -2.f; WHEN(InputIsTicked(Data)); TestEqual(TEXT("Input value (negate)"), GetActionValue(Data, TestAction), TestValue * (float)Scalar->Scalar.X); } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInputModifierDeadzoneTest, "Input.Modifiers.DeadZone", BasicModifierTestFlags) bool FInputModifierDeadzoneTest::RunTest(const FString& Parameters) { UControllablePlayer& Data = GIVEN(ABasicModifierTest(this, EInputActionValueType::Axis1D)); AND(AnActionIsMappedToAKey(Data, TestContext, TestAction, TestAxis)); UInputModifierDeadZone* DeadZone = AND(Cast(AModifierIsAppliedToAnAction(Data, NewObject(), TestAction))); DeadZone->LowerThreshold = 0.1f; // Provide initial valid input WHEN(AKeyIsActuated(Data, TestAxis, 1.f)); AND(InputIsTicked(Data)); THEN(PressingKeyTriggersAction(Data, TestAction)); // Drop below deadzone WHEN(AKeyIsActuated(Data, TestAxis, DeadZone->LowerThreshold * 0.5f)); AND(InputIsTicked(Data)); THEN(HoldingKeyTriggersCompleted(Data, TestAction)); // Jitter samples below deadzone lower threshold TArray Sample = { 0.01f, 0.0f, 0.02f, 0.07f, 0.01f }; const int NumSamples = 50; for (int32 i = 0; i < NumSamples; ++i) { WHEN(AKeyIsActuated(Data, TestAxis, Sample[i % Sample.Num()])); AND(InputIsTicked(Data)); THEN(HoldingKeyDoesNotTrigger(Data, TestAction)); } // No noise on release WHEN(AKeyIsReleased(Data, TestAxis)); AND(InputIsTicked(Data)); THEN(ReleasingKeyDoesNotTrigger(Data, TestAction)); // Upper threshold testing DeadZone->UpperThreshold = 0.9f; WHEN(AKeyIsActuated(Data, TestAxis, 0.5f)); AND(InputIsTicked(Data)); THEN(PressingKeyTriggersAction(Data, TestAction)); // At threshold response is 1 WHEN(AKeyIsActuated(Data, TestAxis, 0.9f)); AND(InputIsTicked(Data)); THEN(HoldingKeyTriggersAction(Data, TestAction)); AND(TestEqual(TEXT("Upper threshold value at threshold"), GetActionValue(Data, TestAction), 1.f)); // Past threshold response is clamped to 1 WHEN(AKeyIsActuated(Data, TestAxis, 0.99f)); AND(InputIsTicked(Data)); THEN(HoldingKeyTriggersAction(Data, TestAction)); AND(TestEqual(TEXT("Upper threshold value beyond threshold"), GetActionValue(Data, TestAction), 1.f)); return true; }