using Elsa.Testing.Shared; using Elsa.Workflows; using Elsa.Workflows.Behaviors; using Elsa.Workflows.Exceptions; using NSubstitute; namespace Elsa.Activities.UnitTests.Looping; public class WhileTests { [Theory] [InlineData(false, 0)] [InlineData(true, 1)] public async Task Should_Schedule_Body_Based_On_Condition(bool conditionValue, int expectedScheduledCount) { // Arrange var bodyActivity = Substitute.For(); var whileActivity = new While(new Input(conditionValue), bodyActivity); // Act var context = await ExecuteAsync(whileActivity); // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); Assert.Equal(expectedScheduledCount, scheduledActivities.Count); if (expectedScheduledCount > 0) { Assert.Equal(bodyActivity, scheduledActivities.First().Activity); } } [Fact] public async Task Should_Use_True_Factory_Method() { // Arrange var bodyActivity = Substitute.For(); var whileActivity = While.True(bodyActivity); // Act var context = await ExecuteAsync(whileActivity); // Assert var hasBodyScheduledActivity = context.HasScheduledActivity(bodyActivity); Assert.True(hasBodyScheduledActivity); } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public async Task Should_Evaluate_Condition_Before_Each_Iteration(int maxIterations) { // Arrange - Create separate condition functions for each test case to verify // that each execution of the While activity evaluates the condition exactly once var conditionEvaluationCount = 0; var bodyActivity = Substitute.For(); // Create condition that tracks evaluation count across multiple While activity executions var condition = new Input(_ => { conditionEvaluationCount++; // Return true for the first few evaluations, then false to exit return conditionEvaluationCount <= maxIterations; }); var whileActivity = new While(condition, bodyActivity); // Act - Execute the While activity multiple times to simulate loop iterations // Each execution represents one iteration of the loop in real workflow execution for (var iteration = 1; iteration <= maxIterations; iteration++) { // Execute the While activity (this represents one iteration of the loop) var context = await ExecuteAsync(whileActivity); // Assert this iteration Assert.Equal(iteration, conditionEvaluationCount); // Condition evaluated exactly once per execution Assert.True(context.HasScheduledActivity(bodyActivity)); // Body should be scheduled because condition is true } // Execute one final time - this should make the condition false and not schedule the body var finalContext = await ExecuteAsync(whileActivity); // Assert final execution Assert.Equal(maxIterations + 1, conditionEvaluationCount); // One final evaluation that returns false Assert.False(finalContext.HasScheduledActivity(bodyActivity)); // Body NOT scheduled because condition is false } [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); // 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(); } }