Add DispatchWorkflow tests with new workflow definitions (#7035)

* Add DispatchWorkflow tests with new workflow definitions

- Introduced multiple workflow definitions with varied scenarios including input handling, correlation IDs, and fault handling.
- Enhanced `DispatchWorkflowsTests` with comprehensive test cases to validate `DispatchWorkflow` behavior under different configurations.
- Updated existing workflows and tests for improved structure, readability, and accuracy.
- Refactored and renamed related workflows for consistency across test suites.

* Update test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/DispatchWorkflows/DispatchWorkflowsTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor DispatchWorkflowsTests for readability and maintainability

- Replaced hardcoded constants with named variables for improved clarity.
- Enhanced assertions using utility methods like `Assert.Single` for cleaner code.
- Updated WriteLine activity tests to handle null values reliably.
- Introduced timeout handling for child workflow execution.

* Update GUID length validation in JintJavaScriptFunctionBehaviorTests

- Adjusted `shortGuid` length assertion to accommodate a range of 19-22 characters instead of 20-22.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2025-11-06 20:47:20 +01:00 committed by GitHub
parent 52e287f7ad
commit b32a067d03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 300 additions and 35 deletions

View file

@ -78,7 +78,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
[Fact(DisplayName = "BulkDispatchWorkflows should execute ChildFaulted ports")]
public async Task BulkDispatchWithChildPorts_ShouldExecuteChildFaultedPortForFaultedWorkflows()
{
var result = await RunWorkflowAsync(BulkDispatchWithChildPortsWorkflow.DefinitionId);
var result = await RunWorkflowAsync(BulkDispatchWithBulkChildPortsWorkflow.DefinitionId);
AssertWorkflowFinished(result);
var faultedCount = await GetWorkflowVariableAsync<int>(result, "FaultedCount");

View file

@ -3,7 +3,7 @@ using Elsa.Workflows.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.BulkDispatchWorkflows.Workflows;
public class BulkDispatchWithChildPortsWorkflow : WorkflowBase
public class BulkDispatchWithBulkChildPortsWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
@ -16,7 +16,7 @@ public class BulkDispatchWithChildPortsWorkflow : WorkflowBase
builder.Root = new Runtime.Activities.BulkDispatchWorkflows
{
WorkflowDefinitionId = new(FaultingChildWorkflow.DefinitionId),
WorkflowDefinitionId = new(FaultingBulkChildWorkflow.DefinitionId),
Items = new(new object[] { 1, 2, 3 }),
WaitForCompletion = new(true),
ChildCompleted = new Sequence

View file

@ -5,7 +5,7 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.BulkDispatchWorkflows.Workflows;
[UsedImplicitly]
public class FaultingChildWorkflow : WorkflowBase
public class FaultingBulkChildWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();

View file

@ -1,36 +1,140 @@
using Elsa.Common.Models;
using Elsa.Common.Models;
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Models;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.DispatchWorkflows;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows;
public class DispatchWorkflowsTests : AppComponentTest
{
private readonly SignalManager _signalManager;
private readonly IWorkflowRuntime _workflowRuntime;
private readonly AsyncWorkflowRunner _workflowRunner;
private const int ExpectedWriteLineCount = 2; // One from parent, one from child
private const int ChildWorkflowTimeoutSeconds = 30;
public DispatchWorkflowsTests(App app) : base(app)
{
_workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
_signalManager = Scope.ServiceProvider.GetRequiredService<SignalManager>();
_workflowRunner = Scope.ServiceProvider.GetRequiredService<AsyncWorkflowRunner>();
}
[Fact (Skip = "This test is flaky and needs to be fixed.")]
[Fact(DisplayName = "DispatchWorkflow should wait for child workflow to complete")]
public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
{
var workflowClient = await _workflowRuntime.CreateClientAsync();
await workflowClient.CreateInstanceAsync(new()
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(DispatchAndWaitWorkflow.DefinitionId, VersionOptions.Published)
});
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
await _signalManager.WaitAsync("Completed");
var result = await RunWorkflowAsync(DispatchAndWaitWorkflow.DefinitionId);
AssertWorkflowFinished(result);
var writeLineExecutionRecords = result.ActivityExecutionRecords.Where(x => x.ActivityType == "Elsa.WriteLine").ToList();
Assert.Equal(ExpectedWriteLineCount, writeLineExecutionRecords.Count);
}
}
[Fact(DisplayName = "DispatchWorkflow should dispatch and not wait when WaitForCompletion is false")]
public async Task DispatchFireAndForget_ShouldNotWaitForChildWorkflow()
{
// Run the main workflow and wait for child workflow to complete
var (result, completedChildWorkflows) = await RunWorkflowAndWaitForChildWorkflowAsync(
DispatchFireAndForgetWorkflow.DefinitionId,
SlowChildWorkflow.DefinitionId);
AssertWorkflowFinished(result);
var mainWorkflowCompletedAt = result.WorkflowExecutionContext.UpdatedAt;
// Assert that the child workflow completed after the main workflow
var childContext = Assert.Single(completedChildWorkflows);
Assert.True(childContext.UpdatedAt > mainWorkflowCompletedAt,
$"Child workflow should complete after main workflow. Main: {mainWorkflowCompletedAt}, Child: {childContext.UpdatedAt}");
}
[Fact(DisplayName = "DispatchWorkflow should send input to child workflow")]
public async Task DispatchWithInput_ShouldSendInputToChildWorkflow()
{
var result = await RunWorkflowAsync(DispatchWithInputWorkflow.DefinitionId);
AssertWorkflowFinished(result);
var writeLineExecutionRecords = result.ActivityExecutionRecords.Where(x => x.ActivityType == "Elsa.WriteLine").ToList();
Assert.Equal(ExpectedWriteLineCount, writeLineExecutionRecords.Count);
var writtenTexts = writeLineExecutionRecords
.Select(x => x.ActivityState?[nameof(WriteLine.Text)] as string ?? string.Empty)
.ToList();
Assert.Contains("Received: Hello from parent!", writtenTexts);
Assert.Contains("Parent completed", writtenTexts);
}
[Fact(DisplayName = "DispatchWorkflow should use CorrelationId")]
public async Task DispatchWithCorrelationId_ShouldUseCorrelationId()
{
// Run the main workflow and wait for child workflow to complete
var (result, completedChildWorkflows) = await RunWorkflowAndWaitForChildWorkflowAsync(
DispatchWithCorrelationIdWorkflow.DefinitionId,
ChildWorkflow.DefinitionId);
AssertWorkflowFinished(result);
// Assert that the child workflow has the expected correlation ID
var childContext = Assert.Single(completedChildWorkflows);
Assert.Equal("test-correlation-id-123", childContext.CorrelationId);
}
[Fact(DisplayName = "DispatchWorkflow should throw when workflow definition not found")]
public async Task DispatchWithInvalidWorkflowDefinitionId_ShouldThrow()
{
var result = await RunWorkflowAsync(DispatchInvalidDefinitionWorkflow.DefinitionId);
Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowExecutionContext.SubStatus);
}
private Task<TestWorkflowExecutionResult> RunWorkflowAsync(string workflowDefinitionId)
{
return _workflowRunner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(workflowDefinitionId, VersionOptions.Published));
}
private static void AssertWorkflowFinished(TestWorkflowExecutionResult result)
{
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowExecutionContext.SubStatus);
}
private async Task<(TestWorkflowExecutionResult Result, List<WorkflowExecutionContext> CompletedChildWorkflows)> RunWorkflowAndWaitForChildWorkflowAsync(
string parentWorkflowDefinitionId,
string childWorkflowDefinitionId)
{
var workflowEvents = Scope.ServiceProvider.GetRequiredService<WorkflowEvents>();
var completedChildWorkflows = new List<WorkflowExecutionContext>();
var childWorkflowCompletionTcs = new TaskCompletionSource();
// Subscribe to child workflow completion events
void OnWorkflowStateCommitted(object? sender, WorkflowStateCommittedEventArgs e)
{
if (e.WorkflowExecutionContext.Workflow.Identity.DefinitionId != childWorkflowDefinitionId ||
e.WorkflowExecutionContext.Status != WorkflowStatus.Finished)
{
return;
}
completedChildWorkflows.Add(e.WorkflowExecutionContext);
childWorkflowCompletionTcs.TrySetResult();
}
workflowEvents.WorkflowStateCommitted += OnWorkflowStateCommitted;
try
{
// Run the main workflow
var result = await RunWorkflowAsync(parentWorkflowDefinitionId);
// Wait for the child workflow to complete
await childWorkflowCompletionTcs.Task.WaitAsync(TimeSpan.FromSeconds(ChildWorkflowTimeoutSeconds));
return (result, completedChildWorkflows);
}
finally
{
workflowEvents.WorkflowStateCommitted -= OnWorkflowStateCommitted;
}
}
}

View file

@ -1,4 +1,3 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Activities;
using JetBrains.Annotations;
@ -7,17 +6,11 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.W
[UsedImplicitly]
public class ChildWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new Delay(TimeSpan.FromMilliseconds(250)),
new WriteLine("Hello from Child!")
}
};
builder.Root = new WriteLine("Child workflow executed");
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using JetBrains.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
[UsedImplicitly]
public class ChildWorkflowWithInput : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var message = builder.WithInput<string>("Message");
builder.Root = new WriteLine(context => $"Received: {context.GetInput<string>(message)}");
}
}

View file

@ -1,4 +1,3 @@
using Elsa.Testing.Shared.Activities;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;
@ -7,6 +6,7 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.W
public class DispatchAndWaitWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
@ -17,9 +17,9 @@ public class DispatchAndWaitWorkflow : WorkflowBase
new DispatchWorkflow
{
WorkflowDefinitionId = new(ChildWorkflow.DefinitionId),
WaitForCompletion = new (true)
WaitForCompletion = new(true)
},
new TriggerSignal("Completed")
new WriteLine("Parent completed")
}
};
}

View file

@ -0,0 +1,18 @@
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
public class DispatchFireAndForgetWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new DispatchWorkflow
{
WorkflowDefinitionId = new(SlowChildWorkflow.DefinitionId),
WaitForCompletion = new(false)
};
}
}

View file

@ -0,0 +1,31 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.IncidentStrategies;
using Elsa.Workflows.Runtime.Activities;
using JetBrains.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
[UsedImplicitly]
public class DispatchInvalidDefinitionWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
public static readonly string InvalidChildWorkflowId = "NonExistentWorkflow";
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WorkflowOptions.IncidentStrategyType = typeof(FaultStrategy);
builder.Root = new Sequence
{
Activities =
{
new DispatchWorkflow
{
WorkflowDefinitionId = new(InvalidChildWorkflowId),
WaitForCompletion = new(true)
}
}
};
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
public class DispatchWithCorrelationIdWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new DispatchWorkflow
{
WorkflowDefinitionId = new(ChildWorkflow.DefinitionId),
CorrelationId = new("test-correlation-id-123"),
WaitForCompletion = new(true)
},
new WriteLine("Parent completed")
}
};
}
}

View file

@ -0,0 +1,30 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
public class DispatchWithInputWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new DispatchWorkflow
{
WorkflowDefinitionId = new(ChildWorkflowWithInput.DefinitionId),
Input = new(new Dictionary<string, object>
{
["Message"] = "Hello from parent!"
}),
WaitForCompletion = new(true)
},
new WriteLine("Parent completed")
}
};
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Workflows.Activities;
using JetBrains.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
[UsedImplicitly]
public class FaultingChildWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Fault
{
Message = new("Child workflow failed intentionally")
};
}
}

View file

@ -0,0 +1,24 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Activities;
using JetBrains.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.DispatchWorkflows.Workflows;
[UsedImplicitly]
public class SlowChildWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
Delay.FromMilliseconds(10),
new WriteLine("Slow child workflow executed")
}
};
}
}

View file

@ -88,7 +88,7 @@ public class JintJavaScriptFunctionBehaviorTests(ITestOutputHelper testOutputHel
var shortGuid = dict["shortGuid"]?.ToString();
Assert.NotNull(shortGuid);
Assert.InRange(shortGuid.Length, 20, 22); // Base64 GUID without padding (can be 20-22 chars)
Assert.InRange(shortGuid.Length, 19, 22); // Base64 GUID without padding (can be 19-22 chars)
}
[Theory(DisplayName = "Encoding functions should round-trip correctly")]