2025-11-27 19:57:38 +00:00
|
|
|
using Elsa.Testing.Shared;
|
|
|
|
|
using Elsa.Workflows.Activities;
|
|
|
|
|
using Elsa.Workflows.Activities.Flowchart.Activities;
|
2025-12-15 09:15:39 +00:00
|
|
|
using Elsa.Workflows.Activities.Flowchart.Extensions;
|
|
|
|
|
using Elsa.Workflows.Activities.Flowchart.Models;
|
|
|
|
|
using Elsa.Workflows.Options;
|
2025-11-27 19:57:38 +00:00
|
|
|
using Xunit.Abstractions;
|
|
|
|
|
|
|
|
|
|
namespace Elsa.Activities.IntegrationTests.Branching;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Integration tests for FlowDecision activity in flowchart scenarios.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Collection("FlowchartTests")]
|
2025-12-15 09:15:39 +00:00
|
|
|
public class FlowDecisionTests(ITestOutputHelper testOutputHelper)
|
2025-11-27 19:57:38 +00:00
|
|
|
{
|
|
|
|
|
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
|
|
|
|
|
|
|
|
|
[Theory(DisplayName = "FlowDecision follows correct path based on condition")]
|
|
|
|
|
[MemberData(nameof(BasicPathTestCases))]
|
2025-12-15 09:15:39 +00:00
|
|
|
public async Task Should_Follow_Correct_Path_Based_On_Condition(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
|
2025-11-27 19:57:38 +00:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var start = new WriteLine("Start");
|
|
|
|
|
var decision = new FlowDecision(ctx => condition);
|
|
|
|
|
var truePath = new WriteLine("TruePath");
|
|
|
|
|
var falsePath = new WriteLine("FalsePath");
|
|
|
|
|
|
|
|
|
|
var flowchart = new Flowchart
|
|
|
|
|
{
|
|
|
|
|
Start = start,
|
|
|
|
|
Activities = { start, decision, truePath, falsePath },
|
|
|
|
|
Connections =
|
|
|
|
|
{
|
|
|
|
|
new() { Source = new(start, "Done"), Target = new(decision) },
|
|
|
|
|
new() { Source = new(decision, "True"), Target = new(truePath) },
|
|
|
|
|
new() { Source = new(decision, "False"), Target = new(falsePath) }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-15 09:15:39 +00:00
|
|
|
var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode);
|
|
|
|
|
|
2025-11-27 19:57:38 +00:00
|
|
|
// Act
|
2025-12-15 09:15:39 +00:00
|
|
|
await _fixture.RunActivityAsync(flowchart, options);
|
2025-11-27 19:57:38 +00:00
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
AssertOutputs(expectedOutputs, unexpectedOutputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> BasicPathTestCases()
|
|
|
|
|
{
|
|
|
|
|
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
|
2025-12-15 09:15:39 +00:00
|
|
|
yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }];
|
2025-11-27 19:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory(DisplayName = "FlowDecision handles nested decisions")]
|
|
|
|
|
[MemberData(nameof(NestedDecisionTestCases))]
|
2025-12-15 09:15:39 +00:00
|
|
|
public async Task Should_Handle_Nested_Decisions(FlowchartExecutionMode executionMode, bool outerCondition, bool innerCondition, string[] expectedOutputs, string[] unexpectedOutputs)
|
2025-11-27 19:57:38 +00:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var start = new WriteLine("Start");
|
|
|
|
|
var outerDecision = new FlowDecision(ctx => outerCondition);
|
|
|
|
|
var innerDecision = new FlowDecision(ctx => innerCondition);
|
|
|
|
|
var innerTrue = new WriteLine("InnerTrue");
|
|
|
|
|
var innerFalse = new WriteLine("InnerFalse");
|
|
|
|
|
var outerFalse = new WriteLine("OuterFalse");
|
|
|
|
|
|
|
|
|
|
var flowchart = new Flowchart
|
|
|
|
|
{
|
|
|
|
|
Start = start,
|
|
|
|
|
Activities = { start, outerDecision, innerDecision, innerTrue, innerFalse, outerFalse },
|
|
|
|
|
Connections =
|
|
|
|
|
{
|
|
|
|
|
new() { Source = new(start, "Done"), Target = new(outerDecision) },
|
|
|
|
|
new() { Source = new(outerDecision, "True"), Target = new(innerDecision) },
|
|
|
|
|
new() { Source = new(outerDecision, "False"), Target = new(outerFalse) },
|
|
|
|
|
new() { Source = new(innerDecision, "True"), Target = new(innerTrue) },
|
|
|
|
|
new() { Source = new(innerDecision, "False"), Target = new(innerFalse) }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-15 09:15:39 +00:00
|
|
|
var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode);
|
|
|
|
|
|
2025-11-27 19:57:38 +00:00
|
|
|
// Act
|
2025-12-15 09:15:39 +00:00
|
|
|
await _fixture.RunActivityAsync(flowchart, options);
|
2025-11-27 19:57:38 +00:00
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
AssertOutputs(expectedOutputs, unexpectedOutputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> NestedDecisionTestCases()
|
|
|
|
|
{
|
|
|
|
|
// useTokenFlow, outerCondition, innerCondition, expectedOutputs, unexpectedOutputs
|
2025-12-15 09:15:39 +00:00
|
|
|
yield return [FlowchartExecutionMode.TokenBased, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
|
2025-11-27 19:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory(DisplayName = "FlowDecision works with only one path connected")]
|
|
|
|
|
[MemberData(nameof(OnePathConnectedTestCases))]
|
2025-12-15 09:15:39 +00:00
|
|
|
public async Task Should_Work_With_Only_One_Path_Connected(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
|
2025-11-27 19:57:38 +00:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var start = new WriteLine("Start");
|
|
|
|
|
var decision = new FlowDecision(ctx => condition);
|
|
|
|
|
var truePath = new WriteLine("TruePath");
|
|
|
|
|
var end = new WriteLine("End");
|
|
|
|
|
|
|
|
|
|
var flowchart = new Flowchart
|
|
|
|
|
{
|
|
|
|
|
Start = start,
|
|
|
|
|
Activities = { start, decision, truePath, end },
|
|
|
|
|
Connections =
|
|
|
|
|
{
|
|
|
|
|
new() { Source = new(start, "Done"), Target = new(decision) },
|
|
|
|
|
new() { Source = new(decision, "True"), Target = new(truePath) },
|
|
|
|
|
// False path not connected
|
|
|
|
|
new() { Source = new(truePath, "Done"), Target = new(end) }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-15 09:15:39 +00:00
|
|
|
var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode);
|
|
|
|
|
|
2025-11-27 19:57:38 +00:00
|
|
|
// Act
|
2025-12-15 09:15:39 +00:00
|
|
|
await _fixture.RunActivityAsync(flowchart, options);
|
2025-11-27 19:57:38 +00:00
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
AssertOutputs(expectedOutputs, unexpectedOutputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> OnePathConnectedTestCases()
|
|
|
|
|
{
|
|
|
|
|
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
|
2025-12-15 09:15:39 +00:00
|
|
|
yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start" }, new[] { "TruePath", "End" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start" }, new[] { "TruePath", "End" }];
|
2025-11-27 19:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory(DisplayName = "FlowDecision converges paths correctly")]
|
|
|
|
|
[MemberData(nameof(ConvergePathsTestCases))]
|
2025-12-15 09:15:39 +00:00
|
|
|
public async Task Should_Converge_Paths_Correctly(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
|
2025-11-27 19:57:38 +00:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var start = new WriteLine("Start");
|
|
|
|
|
var decision = new FlowDecision(ctx => condition);
|
|
|
|
|
var truePath = new WriteLine("TruePath");
|
|
|
|
|
var falsePath = new WriteLine("FalsePath");
|
|
|
|
|
var converge = new WriteLine("Converge");
|
|
|
|
|
|
|
|
|
|
var flowchart = new Flowchart
|
|
|
|
|
{
|
|
|
|
|
Start = start,
|
|
|
|
|
Activities = { start, decision, truePath, falsePath, converge },
|
|
|
|
|
Connections =
|
|
|
|
|
{
|
|
|
|
|
new() { Source = new(start, "Done"), Target = new(decision) },
|
|
|
|
|
new() { Source = new(decision, "True"), Target = new(truePath) },
|
|
|
|
|
new() { Source = new(decision, "False"), Target = new(falsePath) },
|
|
|
|
|
new() { Source = new(truePath, "Done"), Target = new(converge) },
|
|
|
|
|
new() { Source = new(falsePath, "Done"), Target = new(converge) }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-15 09:15:39 +00:00
|
|
|
var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode);
|
|
|
|
|
|
2025-11-27 19:57:38 +00:00
|
|
|
// Act
|
2025-12-15 09:15:39 +00:00
|
|
|
await _fixture.RunActivityAsync(flowchart, options);
|
2025-11-27 19:57:38 +00:00
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
AssertOutputs(expectedOutputs, unexpectedOutputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> ConvergePathsTestCases()
|
|
|
|
|
{
|
|
|
|
|
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
|
2025-12-15 09:15:39 +00:00
|
|
|
yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }];
|
|
|
|
|
yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }];
|
2025-11-27 19:57:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AssertOutputs(string[] expectedOutputs, string[] unexpectedOutputs)
|
|
|
|
|
{
|
2025-12-15 09:15:39 +00:00
|
|
|
foreach (var expected in expectedOutputs) Assert.Contains(expected, _fixture.CapturingTextWriter.Lines);
|
|
|
|
|
foreach (var unexpected in unexpectedOutputs) Assert.DoesNotContain(unexpected, _fixture.CapturingTextWriter.Lines);
|
2025-11-27 19:57:38 +00:00
|
|
|
}
|
|
|
|
|
}
|