From 87dc976b5f2b25ad07ab4e18262739fcc73486e4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 26 Nov 2025 12:26:04 +0100 Subject: [PATCH] Add integration and unit tests for `End` activity and update flowchart termination logic (#7109) - Introduced `EndInFlowchartWorkflow` and `EndInSequenceWorkflow` to test `End` activity behavior in flowcharts and sequences. - Added `EndTests` integration and unit tests to verify correct termination behavior, including `ITerminalNode` implementation. - Updated flowchart logic to handle terminal nodes, ensuring immediate flowchart completion upon encountering `End`. --- .../Flowchart/Activities/Flowchart.Tokens.cs | 10 +++- .../Flow/EndTests.cs | 48 ++++++++++++++++++ .../Flow/Workflows/EndInFlowchartWorkflow.cs | 50 +++++++++++++++++++ .../Flow/Workflows/EndInSequenceWorkflow.cs | 26 ++++++++++ .../Flow/EndTests.cs | 35 +++++++++++++ 5 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Flow/EndTests.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInFlowchartWorkflow.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInSequenceWorkflow.cs create mode 100644 test/unit/Elsa.Activities.UnitTests/Flow/EndTests.cs diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs index 7d379389e..127fe1e0e 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs @@ -14,12 +14,20 @@ public partial class Flowchart var flowContext = ctx.TargetContext; var completedActivity = ctx.ChildContext.Activity; var flowGraph = flowContext.GetFlowGraph(); + var tokens = GetTokenList(flowContext); + + // If the completed activity is a terminal node, complete the flowchart immediately. + if (completedActivity is ITerminalNode) + { + tokens.Clear(); + await flowContext.CompleteActivityAsync(); + return; + } // Emit tokens for active outcomes. var outcomes = (ctx.Result as Outcomes ?? Outcomes.Default).Names; var outboundConnections = flowGraph.GetOutboundConnections(completedActivity); var activeOutboundConnections = outboundConnections.Where(x => outcomes.Contains(x.Source.Port)).Distinct().ToList(); - var tokens = GetTokenList(flowContext); foreach (var connection in activeOutboundConnections) tokens.Add(Token.Create(connection.Source.Activity, connection.Target.Activity, connection.Source.Port)); diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/EndTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/EndTests.cs new file mode 100644 index 000000000..933a4b64e --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/EndTests.cs @@ -0,0 +1,48 @@ +using Elsa.Activities.IntegrationTests.Flow.Workflows; +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Xunit.Abstractions; + +namespace Elsa.Activities.IntegrationTests.Flow; + +/// +/// Integration tests for the activity. +/// +public class EndTests(ITestOutputHelper testOutputHelper) +{ + private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper) + .AddWorkflow() + .AddWorkflow(); + + [Fact(DisplayName = "End terminates sequence execution")] + public async Task End_TerminatesSequenceExecution() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(EndInSequenceWorkflow.DefinitionId); + + // Assert + Assert.Equal(WorkflowStatus.Finished, workflowState.Status); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); + Assert.Contains("Before End", lines); + Assert.DoesNotContain("After End", lines); + } + + [Fact(DisplayName = "End in flowchart terminates flowchart immediately")] + public async Task End_InFlowchart_TerminatesFlowchartImmediately() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(EndInFlowchartWorkflow.DefinitionId); + + // Assert + Assert.Equal(WorkflowStatus.Finished, workflowState.Status); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); + Assert.Contains("Start", lines); + Assert.Contains("Path A executed", lines); + // End is a terminal node - it terminates the flowchart immediately + // Path B should not execute because End completes the flowchart + Assert.DoesNotContain("Path B executed", lines); + // The outer sequence continues after the flowchart completes + Assert.Contains("After flowchart", lines); + } +} diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInFlowchartWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInFlowchartWorkflow.cs new file mode 100644 index 000000000..ca657cdd3 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInFlowchartWorkflow.cs @@ -0,0 +1,50 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; + +namespace Elsa.Activities.IntegrationTests.Flow.Workflows; + +/// +/// Workflow demonstrating that End terminates a flowchart. +/// +public class EndInFlowchartWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.WithDefinitionId(DefinitionId); + + var start = new WriteLine("Start"); + var pathA = new WriteLine("Path A executed"); + var end = new End(); + var pathB = new WriteLine("Path B executed"); + var afterFlowchart = new WriteLine("After flowchart"); + + workflow.Root = new Sequence + { + Activities = + { + new Flowchart + { + Start = start, + Activities = + { + start, + pathA, + end, + pathB + }, + Connections = + { + new Connection(start, pathA), + new Connection(pathA, end), + new Connection(end, pathB) + } + }, + afterFlowchart + } + }; + } +} diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInSequenceWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInSequenceWorkflow.cs new file mode 100644 index 000000000..8143bd484 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/Workflows/EndInSequenceWorkflow.cs @@ -0,0 +1,26 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.IntegrationTests.Flow.Workflows; + +/// +/// Workflow demonstrating that End activity completes successfully in a sequence. +/// +public class EndInSequenceWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.WithDefinitionId(DefinitionId); + workflow.Root = new Sequence + { + Activities = + { + new WriteLine("Before End"), + new End(), + new WriteLine("After End") + } + }; + } +} diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/EndTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/EndTests.cs new file mode 100644 index 000000000..bd2837b00 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Flow/EndTests.cs @@ -0,0 +1,35 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.UnitTests.Flow; + +/// +/// Unit tests for the activity. +/// +public class EndTests +{ + [Fact(DisplayName = "End implements ITerminalNode interface")] + public void End_ImplementsITerminalNode() + { + // Arrange + var endActivity = new End(); + + // Assert + Assert.IsAssignableFrom(endActivity); + } + + [Fact(DisplayName = "End completes execution")] + public async Task End_CompletesExecution() + { + // Arrange + var endActivity = new End(); + var fixture = new ActivityTestFixture(endActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.Equal(ActivityStatus.Completed, context.Status); + } +}