diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/WhileTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/WhileTests.cs index 61e39d137..93097d733 100644 --- a/test/unit/Elsa.Activities.UnitTests/Looping/WhileTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Looping/WhileTests.cs @@ -1,5 +1,7 @@ using Elsa.Testing.Shared; using Elsa.Workflows; +using Elsa.Workflows.Behaviors; +using Elsa.Workflows.Exceptions; using NSubstitute; namespace Elsa.Activities.UnitTests.Looping; @@ -62,8 +64,160 @@ public class WhileTests var hasBodyScheduledActivity = context.HasScheduledActivity(bodyActivity); Assert.True(hasBodyScheduledActivity); } + + [Fact] + public async Task Should_Throw_When_Condition_Is_Not_Set() + { + // Arrange + var body = new MockBodyActivity(); + var whileActivity = new While(body) + { + // Reset condition to default/unset state + Condition = new Input(false) // This will be the default, but let's test the actual execution behavior + }; + + // Act + var context = await ExecuteAsync(whileActivity); + + // Assert - with condition false, body should not be scheduled + Assert.False(context.HasScheduledActivity(body)); + } + + [Fact] + public async Task Should_Throw_InputEvaluationException_WhenExceptionFromCondition_And_Not_Schedule_Body() + { + // Arrange + var body = new MockBodyActivity(); + var whileActivity = new While( + condition: new Input((Func)(() => throw new ApplicationException("boom"))), + body: body + ); + + // Act + // Any exception from condition evaluation throws InputEvaluationException + var ex = await Assert.ThrowsAsync(() => ExecuteAsync(whileActivity)); + + // Assert + Assert.Contains("Failed to evaluate activity input", ex.Message); + } + + [Fact] + public async Task BodyIsNull_DoesNotSchedule() + { + // Arrange + var whileActivity = new While( + condition: new Input(true), + body: null); - // Helper methods + // Act + var context = await ExecuteAsync(whileActivity); + + // Assert + var scheduled = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Empty(scheduled); + } + + [Fact] + public async Task BodyThrows_ActivitySchedules_WithoutBreaking() + { + // Arrange + var body = new ThrowingBody(new InvalidOperationException("boom")); + var whileActivity = new While( + condition: new Input(true), + body: body + ); + + // Act + Assert + var context = await ExecuteAsync(whileActivity); + Assert.NotNull(context); + Assert.True(context.HasScheduledActivity(body)); + } + + [Fact] + public async Task Should_Use_Latest_Captured_State_When_Evaluating_Condition() + { + // Arrange + var body = new MockBodyActivity(); + + // Use a class to hold mutable state that can be changed after While construction + var stateHolder = new StateHolder { ShouldContinue = false }; + var whileActivity = new While( + condition: new Input((Func)(() => stateHolder.ShouldContinue)), + body: body + ); + + // Mutate after construction, before execution. + stateHolder.ShouldContinue = true; + + // Act + var context = await ExecuteAsync(whileActivity); + + // Assert + Assert.True(context.HasScheduledActivity(body)); + } + + [Fact] + public async Task ConditionFalseInitially_EvaluatesOnce_AndDoesNotSchedule() + { + // Arrange + var evals = 0; + var body = new MockBodyActivity(); + var whileActivity = new While( + condition: new Input((Func)(() => { evals++; return false; })), + body: body + ); + + // Act + var context = await ExecuteAsync(whileActivity); + + // Assert + Assert.Equal(1, evals); + Assert.False(context.HasScheduledActivity(body)); + } + + [Fact] + public async Task DoesNotScheduleBodyTwice_InSingleExecution() + { + // Arrange + var body = new MockBodyActivity(); + var whileActivity = new While(new Input(true), body); + var context = await ExecuteAsync(whileActivity); + + // Assert + var all = context.WorkflowExecutionContext.Scheduler.List() + .Where(x => x.Activity == body) + .ToList(); + Assert.Single(all); + } + + // Break behavior registered (parity with For). + [Fact] + public void VerifyBreakBehaviorIsRegistered() + { + // Arrange + var whileActivity = new While(body: null); + + // Act + var breakBehavior = whileActivity.Behaviors.OfType().FirstOrDefault(); + + // Assert + Assert.NotNull(breakBehavior); + } + + private class MockBodyActivity : Activity { + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ValueTask.CompletedTask; + } + + private class ThrowingBody(Exception exception) : Activity + { + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => throw exception; + } + + private class StateHolder + { + public bool ShouldContinue; + } + private static Task ExecuteAsync(IActivity activity) { return new ActivityTestFixture(activity).ExecuteAsync();