diff --git a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs
new file mode 100644
index 000000000..2bc3d6b81
--- /dev/null
+++ b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs
@@ -0,0 +1,228 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Models;
+using Xunit.Abstractions;
+using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers;
+
+namespace Elsa.Activities.IntegrationTests.Branching;
+
+///
+/// Integration tests for FlowJoin activity in complex flowchart scenarios.
+///
+public class FlowJoinTests : IDisposable
+{
+ private readonly IServiceProvider _services;
+ private readonly CapturingTextWriter _output;
+ private readonly bool _originalFlowMode;
+
+ public FlowJoinTests(ITestOutputHelper testOutputHelper)
+ {
+ _output = new();
+ _services = CreateServiceProvider(testOutputHelper, _output);
+ _originalFlowMode = Flowchart.UseTokenFlow;
+ }
+
+ public void Dispose()
+ {
+ Flowchart.UseTokenFlow = _originalFlowMode;
+ }
+
+ [Theory]
+ [InlineData(true, FlowJoinMode.WaitAny)]
+ [InlineData(true, FlowJoinMode.WaitAll)]
+ [InlineData(false, FlowJoinMode.WaitAny)]
+ [InlineData(false, FlowJoinMode.WaitAll)]
+ public async Task Should_Handle_Complex_Flowchart_Scenarios(bool useTokenFlow, FlowJoinMode joinMode)
+ {
+ // Test with a more complex flowchart that has multiple activities
+ // Arrange
+ Flowchart.UseTokenFlow = useTokenFlow;
+
+ var startActivity = new WriteLine("Start");
+ var flowJoin = new FlowJoin { Mode = new(joinMode) };
+ var afterJoin = new WriteLine("AfterJoin");
+
+ var flowchart = new Flowchart
+ {
+ Start = startActivity,
+ Activities = { startActivity, flowJoin, afterJoin },
+ Connections =
+ {
+ new() { Source = new(startActivity, "Done"), Target = new(flowJoin) },
+ new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) }
+ }
+ };
+
+ // Act
+ await RunFlowchartAsync(_services, flowchart);
+
+ // Assert
+ Assert.Contains("Start", _output.Lines);
+
+ // Document expected behavior for different combinations
+ if (useTokenFlow)
+ {
+ // In token flow mode, both WaitAny and WaitAll should act as no-ops
+ // The flowchart should execute normally: Start -> FlowJoin -> AfterJoin
+ Assert.Contains("AfterJoin", _output.Lines);
+ }
+ else
+ {
+ // In counter flow mode:
+ // - WaitAny should allow continuation after first inbound connection
+ // - WaitAll should wait for all inbound connections (in this case just one)
+ // Since there's only one inbound connection, both should behave similarly
+ Assert.Contains("AfterJoin", _output.Lines);
+ }
+ }
+
+ [Theory]
+ [InlineData(true, FlowJoinMode.WaitAny)]
+ [InlineData(true, FlowJoinMode.WaitAll)]
+ [InlineData(false, FlowJoinMode.WaitAny)]
+ [InlineData(false, FlowJoinMode.WaitAll)]
+ public async Task Should_Handle_Fork_Join_Scenarios(bool useTokenFlow, FlowJoinMode joinMode)
+ {
+ // Test FlowJoin with a Fork-Join pattern
+ // Arrange
+ Flowchart.UseTokenFlow = useTokenFlow;
+
+ var start = new WriteLine("Start");
+ var fork = new FlowFork { Branches = new(["Branch1", "Branch2"]) };
+ var branch1 = new WriteLine("Branch1");
+ var branch2 = new WriteLine("Branch2");
+ var join = new FlowJoin { Mode = new(joinMode) };
+ var afterJoin = new WriteLine("AfterJoin");
+
+ var flowchart = new Flowchart
+ {
+ Start = start,
+ Activities = { start, fork, branch1, branch2, join, afterJoin },
+ Connections =
+ {
+ new() { Source = new(start, "Done"), Target = new(fork) },
+ new() { Source = new(fork, "Branch1"), Target = new(branch1) },
+ new() { Source = new(fork, "Branch2"), Target = new(branch2) },
+ new() { Source = new(branch1, "Done"), Target = new(join) },
+ new() { Source = new(branch2, "Done"), Target = new(join) },
+ new() { Source = new(join, "Done"), Target = new(afterJoin) }
+ }
+ };
+
+ // Act
+ await RunFlowchartAsync(_services, flowchart);
+
+ // Assert
+ Assert.Contains("Start", _output.Lines);
+
+ if (useTokenFlow)
+ {
+ // In token flow mode, FlowJoin acts as a no-op
+ // Both branches should execute and continue to AfterJoin
+ Assert.Contains("Branch1", _output.Lines);
+ Assert.Contains("Branch2", _output.Lines);
+ Assert.Contains("AfterJoin", _output.Lines);
+ }
+ else
+ {
+ // In counter flow mode, behavior depends on join mode
+ if (joinMode == FlowJoinMode.WaitAny)
+ {
+ // WaitAny allows continuation after first branch completes
+ Assert.Contains("AfterJoin", _output.Lines);
+ // At least one branch should execute
+ Assert.True(_output.Lines.Contains("Branch1") || _output.Lines.Contains("Branch2"));
+ }
+ else // WaitAll
+ {
+ // WaitAll waits for both branches to complete
+ Assert.Contains("Branch1", _output.Lines);
+ Assert.Contains("Branch2", _output.Lines);
+ Assert.Contains("AfterJoin", _output.Lines);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData(true, FlowJoinMode.WaitAny)]
+ [InlineData(true, FlowJoinMode.WaitAll)]
+ [InlineData(false, FlowJoinMode.WaitAny)]
+ [InlineData(false, FlowJoinMode.WaitAll)]
+ public async Task Should_Handle_Multiple_Join_Scenarios(bool useTokenFlow, FlowJoinMode joinMode)
+ {
+ // Test multiple FlowJoin activities in a complex flowchart
+ // Arrange
+ Flowchart.UseTokenFlow = useTokenFlow;
+
+ var start = new WriteLine("Start");
+ var fork1 = new FlowFork { Branches = new(["A", "B"]) };
+ var activityA = new WriteLine("A");
+ var activityB = new WriteLine("B");
+ var join1 = new FlowJoin { Mode = new(joinMode) };
+ var middle = new WriteLine("Middle");
+ var fork2 = new FlowFork { Branches = new(["C", "D"]) };
+ var activityC = new WriteLine("C");
+ var activityD = new WriteLine("D");
+ var join2 = new FlowJoin { Mode = new(joinMode) };
+ var end = new WriteLine("End");
+
+ var flowchart = new Flowchart
+ {
+ Start = start,
+ Activities = { start, fork1, activityA, activityB, join1, middle, fork2, activityC, activityD, join2, end },
+ Connections =
+ {
+ new() { Source = new(start, "Done"), Target = new(fork1) },
+ new() { Source = new(fork1, "A"), Target = new(activityA) },
+ new() { Source = new(fork1, "B"), Target = new(activityB) },
+ new() { Source = new(activityA, "Done"), Target = new(join1) },
+ new() { Source = new(activityB, "Done"), Target = new(join1) },
+ new() { Source = new(join1, "Done"), Target = new(middle) },
+ new() { Source = new(middle, "Done"), Target = new(fork2) },
+ new() { Source = new(fork2, "C"), Target = new(activityC) },
+ new() { Source = new(fork2, "D"), Target = new(activityD) },
+ new() { Source = new(activityC, "Done"), Target = new(join2) },
+ new() { Source = new(activityD, "Done"), Target = new(join2) },
+ new() { Source = new(join2, "Done"), Target = new(end) }
+ }
+ };
+
+ // Act
+ await RunFlowchartAsync(_services, flowchart);
+
+ // Assert
+ Assert.Contains("Start", _output.Lines);
+ Assert.Contains("Middle", _output.Lines);
+ Assert.Contains("End", _output.Lines);
+
+ // Verify expected execution patterns based on flow mode and join mode
+ if (useTokenFlow)
+ {
+ // In token flow mode, all activities should execute
+ Assert.Contains("A", _output.Lines);
+ Assert.Contains("B", _output.Lines);
+ Assert.Contains("C", _output.Lines);
+ Assert.Contains("D", _output.Lines);
+ }
+ else
+ {
+ // In counter flow mode, execution depends on join mode
+ if (joinMode == FlowJoinMode.WaitAll)
+ {
+ // WaitAll ensures all parallel branches complete
+ Assert.Contains("A", _output.Lines);
+ Assert.Contains("B", _output.Lines);
+ Assert.Contains("C", _output.Lines);
+ Assert.Contains("D", _output.Lines);
+ }
+ else // WaitAny
+ {
+ // WaitAny allows continuation after first branch in each fork
+ // At least one from each pair should execute
+ Assert.True(_output.Lines.Contains("A") || _output.Lines.Contains("B"));
+ Assert.True(_output.Lines.Contains("C") || _output.Lines.Contains("D"));
+ }
+ }
+ }
+}
diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
index 2e8684d2c..0ab8e6d86 100644
--- a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
+++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
@@ -10,86 +10,6 @@ namespace Elsa.Activities.UnitTests.Branching;
///
public class FlowJoinTests
{
- [Theory]
- [InlineData(FlowJoinMode.WaitAny)]
- [InlineData(FlowJoinMode.WaitAll)]
- public async Task Should_Complete_Activity_In_Token_Flow_Mode(FlowJoinMode joinMode)
- {
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = true;
-
- try
- {
- var flowJoin = new FlowJoin { Mode = new(joinMode) };
-
- // Act
- var context = await ExecuteAsync(flowJoin);
-
- // Assert
- Assert.True(context.IsCompleted);
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
- }
-
- [Theory]
- [InlineData(FlowJoinMode.WaitAny)]
- [InlineData(FlowJoinMode.WaitAll)]
- public async Task Should_Complete_Activity_In_Counter_Flow_Mode(FlowJoinMode joinMode)
- {
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = false;
-
- try
- {
- var flowJoin = new FlowJoin { Mode = new(joinMode) };
-
- // Act - Execute without parent context (simple case)
- var context = await ExecuteAsync(flowJoin);
-
- // Assert
- Assert.True(context.IsCompleted);
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
- }
-
- [Fact]
- public async Task Should_Execute_In_Flowchart_Context()
- {
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = false;
-
- try
- {
- var flowJoin = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
- var flowchart = new Flowchart
- {
- Start = flowJoin,
- Activities = { flowJoin }
- };
-
- // Act
- var context = await ExecuteFlowchartAsync(flowchart);
-
- // Assert
- Assert.NotNull(context);
- Assert.True(context.HasScheduledActivity(flowJoin));
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
- }
-
- // Comprehensive tests covering all flow mode combinations
[Theory]
[InlineData(true, FlowJoinMode.WaitAny)]
[InlineData(true, FlowJoinMode.WaitAll)]
@@ -97,38 +17,11 @@ public class FlowJoinTests
[InlineData(false, FlowJoinMode.WaitAll)]
public async Task Should_Complete_In_All_Flow_Mode_Combinations(bool useTokenFlow, FlowJoinMode joinMode)
{
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = useTokenFlow;
+ // Arrange & Act
+ var context = await ExecuteWithFlowModeAsync(useTokenFlow, joinMode);
- try
- {
- var flowJoin = new FlowJoin { Mode = new(joinMode) };
-
- // Act
- var context = await ExecuteAsync(flowJoin);
-
- // Assert
- Assert.True(context.IsCompleted);
-
- // Document expected behavior based on flow mode
- if (useTokenFlow)
- {
- // In token flow mode, FlowJoin acts as a no-op and always completes
- // regardless of join mode (WaitAny or WaitAll)
- Assert.True(context.IsCompleted, "Token flow mode should always complete");
- }
- else
- {
- // In counter flow mode, behavior depends on join mode and flowchart state
- // Without parent context, both WaitAny and WaitAll should complete
- Assert.True(context.IsCompleted, "Counter flow mode should complete when no parent context");
- }
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
+ // Assert
+ Assert.True(context.IsCompleted);
}
[Theory]
@@ -138,139 +31,85 @@ public class FlowJoinTests
[InlineData(false, FlowJoinMode.WaitAll)]
public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(bool useTokenFlow, FlowJoinMode joinMode)
{
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = useTokenFlow;
+ // Arrange & Act
+ var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(useTokenFlow, joinMode);
- try
- {
- var flowJoin = new FlowJoin { Mode = new(joinMode) };
- var flowchart = new Flowchart
- {
- Start = flowJoin,
- Activities = { flowJoin }
- };
+ // Assert
+ Assert.NotNull(context);
+
+ var expectedMessage = useTokenFlow
+ ? $"Token flow mode with {joinMode} should schedule the join activity"
+ : $"Counter flow mode with {joinMode} should schedule the join activity as start";
- // Act
- var context = await ExecuteFlowchartAsync(flowchart);
-
- // Assert
- Assert.NotNull(context);
-
- if (useTokenFlow)
- {
- // In token flow mode, the join should be scheduled regardless of join mode
- Assert.True(context.HasScheduledActivity(flowJoin),
- $"Token flow mode with {joinMode} should schedule the join activity");
- }
- else
- {
- // In counter flow mode, scheduling depends on join mode and flowchart state
- // Since this is a simple flowchart with just the join as start activity,
- // it should be scheduled
- Assert.True(context.HasScheduledActivity(flowJoin),
- $"Counter flow mode with {joinMode} should schedule the join activity as start");
- }
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
- }
-
- [Theory]
- [InlineData(true, FlowJoinMode.WaitAny)]
- [InlineData(true, FlowJoinMode.WaitAll)]
- [InlineData(false, FlowJoinMode.WaitAny)]
- [InlineData(false, FlowJoinMode.WaitAll)]
- public async Task Should_Handle_Complex_Flowchart_Scenarios(bool useTokenFlow, FlowJoinMode joinMode)
- {
- // Test with a more complex flowchart that has multiple activities
- // Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = useTokenFlow;
-
- try
- {
- var startActivity = new WriteLine("Start");
- var flowJoin = new FlowJoin { Mode = new(joinMode) };
- var afterJoin = new WriteLine("AfterJoin");
-
- var flowchart = new Flowchart
- {
- Start = startActivity,
- Activities = { startActivity, flowJoin, afterJoin },
- Connections =
- {
- new() { Source = new(startActivity, "Done"), Target = new(flowJoin) },
- new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) }
- }
- };
-
- // Act
- var context = await ExecuteFlowchartAsync(flowchart);
-
- // Assert
- Assert.NotNull(context);
-
- // The start activity should always be scheduled first
- Assert.True(context.HasScheduledActivity(startActivity),
- "Start activity should be scheduled");
-
- // Document expected behavior for different combinations
- if (useTokenFlow)
- {
- // In token flow mode, both WaitAny and WaitAll act as no-ops
- // The exact scheduling depends on the flowchart execution engine
- }
- else
- {
- // In counter flow mode:
- // - WaitAny should allow continuation after first inbound connection
- // - WaitAll should wait for all inbound connections (in this case just one)
- // Since there's only one inbound connection, both should behave similarly
- }
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
+ Assert.True(context.HasScheduledActivity(flowJoin), expectedMessage);
}
[Fact]
public async Task Should_Demonstrate_Token_vs_Counter_Flow_Differences()
{
- // This test demonstrates the key differences between token and counter flow modes
+ // Arrange
var joinMode = FlowJoinMode.WaitAll; // Use WaitAll to highlight differences
- // Test Token Flow Mode
+ // Act
+ var tokenContext = await ExecuteWithFlowModeAsync(true, joinMode);
+ var counterContext = await ExecuteWithFlowModeAsync(false, joinMode);
+
+ // Assert
+ Assert.True(tokenContext.IsCompleted, "Token flow should always complete");
+ Assert.True(counterContext.IsCompleted, "Counter flow should complete without parent context");
+ }
+
+ ///
+ /// Creates a FlowJoin activity with the specified mode.
+ ///
+ private static FlowJoin CreateFlowJoin(FlowJoinMode joinMode) => new() { Mode = new(joinMode) };
+
+ ///
+ /// Creates a simple flowchart with the FlowJoin as the start activity.
+ ///
+ private static Flowchart CreateSimpleFlowchart(FlowJoin flowJoin) => new()
+ {
+ Start = flowJoin,
+ Activities = { flowJoin }
+ };
+
+ ///
+ /// Executes a FlowJoin activity with the specified flow mode, handling the UseTokenFlow setup and teardown.
+ ///
+ private static async Task ExecuteWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode)
+ {
+ return await WithFlowModeAsync(useTokenFlow, async () =>
+ {
+ var flowJoin = CreateFlowJoin(joinMode);
+ return await ExecuteAsync(flowJoin);
+ });
+ }
+
+ ///
+ /// Executes a FlowJoin activity within a flowchart context with the specified flow mode.
+ ///
+ private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode)
+ {
+ return await WithFlowModeAsync(useTokenFlow, async () =>
+ {
+ var flowJoin = CreateFlowJoin(joinMode);
+ var flowchart = CreateSimpleFlowchart(flowJoin);
+ var context = await ExecuteFlowchartAsync(flowchart);
+ return (context, flowJoin);
+ });
+ }
+
+ ///
+ /// Executes an action with the specified flow mode, ensuring proper setup and teardown of UseTokenFlow.
+ ///
+ private static async Task WithFlowModeAsync(bool useTokenFlow, Func> action)
+ {
var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = true;
-
+ Flowchart.UseTokenFlow = useTokenFlow;
+
try
{
- var tokenFlowJoin = new FlowJoin { Mode = new(joinMode) };
- var tokenContext = await ExecuteAsync(tokenFlowJoin);
-
- // Token flow: FlowJoin acts as no-op, always completes
- Assert.True(tokenContext.IsCompleted, "Token flow should always complete");
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
-
- // Test Counter Flow Mode
- Flowchart.UseTokenFlow = false;
-
- try
- {
- var counterFlowJoin = new FlowJoin { Mode = new(joinMode) };
- var counterContext = await ExecuteAsync(counterFlowJoin);
-
- // Counter flow: Behavior depends on CanWaitAllProceed for WaitAll mode
- // Without parent context, it should complete
- Assert.True(counterContext.IsCompleted, "Counter flow should complete without parent context");
+ return await action();
}
finally
{
@@ -278,11 +117,17 @@ public class FlowJoinTests
}
}
+ ///
+ /// Executes an activity using the ActivityTestFixture.
+ ///
private static async Task ExecuteAsync(IActivity activity)
{
return await new ActivityTestFixture(activity).ExecuteAsync();
}
+ ///
+ /// Executes a flowchart using the ActivityTestFixture.
+ ///
private static async Task ExecuteFlowchartAsync(Flowchart flowchart)
{
return await new ActivityTestFixture(flowchart).ExecuteAsync();