Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions S1API.Tests/Interaction/InteractionPromptApiCompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.Reflection;
using S1API.Interaction;
using UnityEngine;

namespace S1API.Tests.Interaction;

public sealed class InteractionPromptApiCompatibilityTests
{
[Fact]
public void BuilderFactoriesPreserveSourceAndBinaryShape()
{
AssertStaticFactory(
typeof(InteractionPromptBuilder),
nameof(InteractionPromptBuilder.Create));
AssertStaticFactory(
typeof(InteractionPrompt),
nameof(InteractionPrompt.CreateBuilder));
}

[Fact]
public void BuilderExposesExpectedFluentShape()
{
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithMessage), typeof(string));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithInput), typeof(InteractionPromptInput));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithState), typeof(InteractionPromptState));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithRange), typeof(float));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithPriority), typeof(int));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithAngleLimit), typeof(float));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithoutAngleLimit));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithDisplayLocation), typeof(Transform));
AssertBuilderMethod(nameof(InteractionPromptBuilder.WithDisplayLocation), typeof(Collider));
AssertBuilderMethod(nameof(InteractionPromptBuilder.OnHovered), typeof(Action));
AssertBuilderMethod(nameof(InteractionPromptBuilder.OnInteractionStarted), typeof(Action));
AssertBuilderMethod(nameof(InteractionPromptBuilder.OnInteractionEnded), typeof(Action));

MethodInfo build = typeof(InteractionPromptBuilder).GetMethod(
nameof(InteractionPromptBuilder.Build),
Type.EmptyTypes)!;
Assert.Equal(typeof(InteractionPrompt), build.ReturnType);
}

[Fact]
public void HandleExposesExpectedRuntimeShape()
{
AssertHandleMethod(nameof(InteractionPrompt.SetMessage), typeof(string));
AssertHandleMethod(nameof(InteractionPrompt.SetInput), typeof(InteractionPromptInput));
AssertHandleMethod(nameof(InteractionPrompt.SetState), typeof(InteractionPromptState));
AssertHandleMethod(nameof(InteractionPrompt.SetRange), typeof(float));
AssertHandleMethod(nameof(InteractionPrompt.SetPriority), typeof(int));
AssertHandleMethod(nameof(InteractionPrompt.SetAngleLimit), typeof(float));
AssertHandleMethod(nameof(InteractionPrompt.WithoutAngleLimit));
AssertHandleMethod(nameof(InteractionPrompt.SetDisplayLocation), typeof(Transform));
AssertHandleMethod(nameof(InteractionPrompt.SetDisplayLocation), typeof(Collider));
AssertHandleMethod(nameof(InteractionPrompt.ClearDisplayLocation));

MethodInfo remove = typeof(InteractionPrompt).GetMethod(
nameof(InteractionPrompt.Remove),
Type.EmptyTypes)!;
Assert.Equal(typeof(bool), remove.ReturnType);
Assert.Contains(typeof(IDisposable), typeof(InteractionPrompt).GetInterfaces());

AssertProperty(nameof(InteractionPrompt.Target), typeof(GameObject));
AssertProperty(nameof(InteractionPrompt.Message), typeof(string));
AssertProperty(nameof(InteractionPrompt.Input), typeof(InteractionPromptInput));
AssertProperty(nameof(InteractionPrompt.State), typeof(InteractionPromptState));
AssertProperty(nameof(InteractionPrompt.Range), typeof(float));
AssertProperty(nameof(InteractionPrompt.Priority), typeof(int));
AssertProperty(nameof(InteractionPrompt.IsAngleLimited), typeof(bool));
AssertProperty(nameof(InteractionPrompt.AngleLimit), typeof(float));
AssertProperty(nameof(InteractionPrompt.IsRemoved), typeof(bool));

AssertEvent(nameof(InteractionPrompt.Hovered));
AssertEvent(nameof(InteractionPrompt.InteractionStarted));
AssertEvent(nameof(InteractionPrompt.InteractionEnded));
}

private static void AssertStaticFactory(Type declaringType, string methodName)
{
MethodInfo method = declaringType.GetMethod(methodName, new[] { typeof(GameObject) })!;
Assert.True(method.IsStatic);
Assert.Equal(typeof(InteractionPromptBuilder), method.ReturnType);
Assert.Equal("target", Assert.Single(method.GetParameters()).Name);
}

private static void AssertBuilderMethod(string methodName, params Type[] parameterTypes)
{
MethodInfo method = typeof(InteractionPromptBuilder).GetMethod(methodName, parameterTypes)!;
Assert.Equal(typeof(InteractionPromptBuilder), method.ReturnType);
}

private static void AssertHandleMethod(string methodName, params Type[] parameterTypes)
{
MethodInfo method = typeof(InteractionPrompt).GetMethod(methodName, parameterTypes)!;
Assert.Equal(typeof(InteractionPrompt), method.ReturnType);
}

private static void AssertProperty(string propertyName, Type propertyType)
{
PropertyInfo property = typeof(InteractionPrompt).GetProperty(propertyName)!;
Assert.Equal(propertyType, property.PropertyType);
Assert.NotNull(property.GetMethod);
}

private static void AssertEvent(string eventName)
{
EventInfo eventInfo = typeof(InteractionPrompt).GetEvent(eventName)!;
Assert.Equal(typeof(Action), eventInfo.EventHandlerType);
}
}
60 changes: 60 additions & 0 deletions S1API.Tests/Interaction/InteractionPromptApiCompileFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using S1API.Interaction;
using UnityEngine;

namespace S1API.Tests.Interaction;

internal static class InteractionPromptApiCompileFixture
{
internal static InteractionPrompt Configure(
GameObject target,
Collider displayCollider,
Transform displayPoint,
Action onHovered,
Action onStarted,
Action onEnded)
{
return InteractionPrompt
.CreateBuilder(target)
.WithMessage("Use machine")
.WithInput(InteractionPromptInput.Interact)
.WithState(InteractionPromptState.Default)
.WithRange(3f)
.WithPriority(5)
.WithAngleLimit(75f)
.WithoutAngleLimit()
.WithDisplayLocation(displayPoint)
.WithDisplayLocation(displayCollider)
.OnHovered(onHovered)
.OnInteractionStarted(onStarted)
.OnInteractionEnded(onEnded)
.Build();
}

internal static void ConfigureRuntime(InteractionPrompt prompt, Collider displayCollider, Transform displayPoint)
{
prompt
.SetMessage("Stop machine")
.SetInput(InteractionPromptInput.PrimaryClick)
.SetState(InteractionPromptState.Invalid)
.SetRange(2f)
.SetPriority(10)
.SetAngleLimit(45f)
.WithoutAngleLimit()
.SetDisplayLocation(displayPoint)
.SetDisplayLocation(displayCollider)
.ClearDisplayLocation();

_ = prompt.Target;
_ = prompt.Message;
_ = prompt.Input;
_ = prompt.State;
_ = prompt.Range;
_ = prompt.Priority;
_ = prompt.IsAngleLimited;
_ = prompt.AngleLimit;
_ = prompt.IsRemoved;
prompt.Remove();
prompt.Dispose();
}
}
185 changes: 185 additions & 0 deletions S1API.Tests/Interaction/InteractionPromptContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System;
using System.Reflection;
using S1API.Interaction;
using UnityEngine;

namespace S1API.Tests.Interaction;

public sealed class InteractionPromptContractTests
{
[Fact]
public void ExplicitBuilderConfigurationReplacesDefaults()
{
InteractionPromptBuilder builder = CreateManagedBuilderFixture();
Action hovered = () => { };
Action started = () => { };
Action ended = () => { };

InteractionPromptBuilder result = builder
.WithMessage("Use")
.WithInput(InteractionPromptInput.PrimaryClick)
.WithState(InteractionPromptState.Label)
.WithRange(2.5f)
.WithPriority(8)
.WithAngleLimit(60f)
.OnHovered(hovered)
.OnHovered(hovered)
.OnInteractionStarted(started)
.OnInteractionStarted(started)
.OnInteractionEnded(ended)
.OnInteractionEnded(ended);

Assert.Same(builder, result);
Assert.Equal("Use", builder.Message);
Assert.Equal(InteractionPromptInput.PrimaryClick, builder.Input);
Assert.Equal(InteractionPromptState.Label, builder.State);
Assert.Equal(2.5f, builder.Range);
Assert.Equal(8, builder.Priority);
Assert.True(builder.LimitAngle);
Assert.Equal(60f, builder.AngleLimit);
Assert.Same(hovered, Assert.Single(builder.HoveredCallbacks));
Assert.Same(started, Assert.Single(builder.InteractionStartedCallbacks));
Assert.Same(ended, Assert.Single(builder.InteractionEndedCallbacks));
}

[Fact]
public void FailedMessageValidationLeavesBuilderMutableForImmediateRetry()
{
InteractionPromptBuilder builder = CreateManagedBuilderFixture();

Assert.Throws<InvalidOperationException>(() => builder.Build());

Assert.Same(builder, builder.WithMessage("Retry"));
Assert.Equal("Retry", builder.Message);
}

[Fact]
public void BuiltBuilderReturnsCachedHandleAndRejectsFurtherMutation()
{
InteractionPromptBuilder builder = CreateManagedBuilderFixture();
InteractionPrompt prompt = TestObjectFactory.CreateUninitialized<InteractionPrompt>();
SetBuiltPrompt(builder, prompt);

Assert.Same(prompt, builder.Build());
Assert.Throws<InvalidOperationException>(() => builder.WithMessage("Changed"));
Assert.Throws<InvalidOperationException>(() => builder.WithPriority(1));
Assert.Throws<InvalidOperationException>(() => builder.OnHovered(() => { }));
}

[Fact]
public void BuilderRejectsNullCallbacksAndUndefinedEnums()
{
InteractionPromptBuilder builder = CreateManagedBuilderFixture();

Assert.Throws<ArgumentNullException>(() => builder.OnHovered(null!));
Assert.Throws<ArgumentNullException>(() => builder.OnInteractionStarted(null!));
Assert.Throws<ArgumentNullException>(() => builder.OnInteractionEnded(null!));
Assert.Throws<ArgumentNullException>(
() => builder.WithDisplayLocation((Transform)null!));
Assert.Throws<ArgumentNullException>(
() => builder.WithDisplayLocation((Collider)null!));
Assert.Throws<ArgumentOutOfRangeException>(
() => builder.WithInput((InteractionPromptInput)99));
Assert.Throws<ArgumentOutOfRangeException>(
() => builder.WithState((InteractionPromptState)99));
}

[Fact]
public void PublicFactoriesRejectNullTargets()
{
Assert.Throws<ArgumentNullException>(() => InteractionPromptBuilder.Create(null!));
Assert.Throws<ArgumentNullException>(() => InteractionPrompt.CreateBuilder(null!));
}

[Fact]
public void NativeRangeIsBoundedByInteractionManagerCast()
{
Assert.Equal(4f, InteractionPromptContract.NativeMaxInteractionRange);
Assert.Equal(90f, InteractionPromptContract.DefaultAngleLimit);
Assert.Equal(0.1f, InteractionPromptContract.NormalizeRange(0.1f));
Assert.Equal(4f, InteractionPromptContract.NormalizeRange(4f));
}

[Theory]
[InlineData(0f)]
[InlineData(-1f)]
[InlineData(4.01f)]
[InlineData(float.NaN)]
[InlineData(float.PositiveInfinity)]
[InlineData(float.NegativeInfinity)]
public void RangeRejectsUnsupportedValues(float range)
{
Assert.Throws<ArgumentOutOfRangeException>(
() => InteractionPromptContract.NormalizeRange(range));
}

[Theory]
[InlineData(0f)]
[InlineData(-1f)]
[InlineData(180.01f)]
[InlineData(float.NaN)]
[InlineData(float.PositiveInfinity)]
[InlineData(float.NegativeInfinity)]
public void AngleLimitRejectsUnsupportedValues(float angleLimit)
{
Assert.Throws<ArgumentOutOfRangeException>(
() => InteractionPromptContract.NormalizeAngleLimit(angleLimit));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void MessageRejectsMissingText(string? message)
{
Assert.ThrowsAny<ArgumentException>(
() => InteractionPromptContract.NormalizeMessage(message!));
}

[Fact]
public void PublicEnumsRemainStableAndNativeOrdered()
{
Assert.Equal(0, (int)InteractionPromptInput.Interact);
Assert.Equal(1, (int)InteractionPromptInput.PrimaryClick);
Assert.Equal(0, (int)InteractionPromptState.Default);
Assert.Equal(1, (int)InteractionPromptState.Invalid);
Assert.Equal(2, (int)InteractionPromptState.Disabled);
Assert.Equal(3, (int)InteractionPromptState.Label);
}

[Fact]
public void UndefinedEnumValuesAreRejected()
{
Assert.Throws<ArgumentOutOfRangeException>(
() => InteractionPromptContract.ValidateEnum(
(InteractionPromptInput)99,
"input"));
Assert.Throws<ArgumentOutOfRangeException>(
() => InteractionPromptContract.ValidateEnum(
(InteractionPromptState)99,
"state"));
}

private static InteractionPromptBuilder CreateManagedBuilderFixture()
{
InteractionPromptBuilder builder =
TestObjectFactory.CreateUninitialized<InteractionPromptBuilder>();
SetPrivateField(builder, "_hoveredCallbacks", new List<Action>());
SetPrivateField(builder, "_interactionStartedCallbacks", new List<Action>());
SetPrivateField(builder, "_interactionEndedCallbacks", new List<Action>());
return builder;
}

private static void SetBuiltPrompt(InteractionPromptBuilder builder, InteractionPrompt prompt)
{
SetPrivateField(builder, "_builtPrompt", prompt);
}

private static void SetPrivateField<TValue>(InteractionPromptBuilder builder, string name, TValue value)
{
FieldInfo field = typeof(InteractionPromptBuilder).GetField(
name,
BindingFlags.Instance | BindingFlags.NonPublic)!;
field.SetValue(builder, value);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4 changes: 4 additions & 0 deletions S1API.Tests/S1API.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(MonoAssembliesPath)/UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>$(MonoAssembliesPath)/UnityEngine.PhysicsModule.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' == 'Il2CppMelon'">
Expand All @@ -55,5 +58,6 @@
<Reference Include="$(Il2CppAssembliesPath)\Il2Cppmscorlib.dll" />
<Reference Include="$(Il2CppAssembliesPath)\UnityEngine.dll" />
<Reference Include="$(Il2CppAssembliesPath)\UnityEngine.CoreModule.dll" />
<Reference Include="$(Il2CppAssembliesPath)\UnityEngine.PhysicsModule.dll" />
</ItemGroup>
</Project>
Loading
Loading