Add integration tests for enumerables with projection to arrays (#7290)

* Add integration tests for enumerables with projection to arrays

Introduce integration tests to verify the conversion of a Select-projected IEnumerable to an array using JavaScript execution. This includes creating new test cases, activities, and workflows to ensure the end-to-end process behaves as expected. Also refined type inference in `ExpressionExecutionContextExtensions` for projection operators.

* Remove unused newline in EnumerableProjectionTests
This commit is contained in:
Sipke Schoorstra 2026-02-16 14:09:51 +01:00 committed by GitHub
parent f5505d66c9
commit af1f1b9ec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 4 deletions

View file

@ -556,7 +556,9 @@ public static class ExpressionExecutionContextExtensions
return obj;
// Use LINQ to convert the IEnumerable to an array.
var elementType = obj.GetType().GetGenericArguments().FirstOrDefault();
// For projection operators like Select, the element type is the LAST generic argument
// (e.g., ListSelectIterator<TSource, TResult> where TResult is the element type)
var elementType = obj.GetType().GetGenericArguments().LastOrDefault();
if (elementType == null)
return obj;

View file

@ -0,0 +1,38 @@
using Elsa.Expressions.JavaScript.Activities;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray;
/// <summary>
/// Code-first workflow that reproduces the "projected IEnumerable to array" scenario end-to-end.
/// </summary>
public class EnumerableProjectionTestWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
// Store in workflow instance state to mimic real execution behavior.
var messagesVar = new Variable<object>("Messages", Array.Empty<string>()).WithWorkflowStorage();
workflow.WithVariables(messagesVar);
workflow.Root = new Sequence
{
Activities =
{
new TestEnumerableActivity
{
EnumerableResult = new(messagesVar)
},
new RunJavaScript
{
// Accessing Messages through JS triggers the conversion logic.
Script = new("return getMessages();"),
Result = new(messagesVar)
}
}
};
}
}

View file

@ -0,0 +1,37 @@
using Elsa.Expressions.Helpers;
using Elsa.Testing.Shared;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray;
public class EnumerableProjectionTests(ITestOutputHelper testOutputHelper)
{
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "JavaScript should convert a Select-projected IEnumerable variable to an array")]
public async Task Should_Convert_Select_Projected_Enumerable_To_Array()
{
// Arrange
var workflow = new EnumerableProjectionTestWorkflow();
// Act
var result = await _fixture.RunWorkflowAsync(workflow);
// Assert
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); // If conversion failed, workflow will have faulted.
var variableManager = _fixture.Services.GetRequiredService<IWorkflowInstanceVariableManager>();
var messagesVariable = (await variableManager.GetVariablesAsync(result.WorkflowExecutionContext)).FirstOrDefault(x => x.Variable.Name == "Messages");
var messages = messagesVariable?.Value.ConvertTo<string[]>();
Assert.NotNull(messages);
Assert.Equal(5, messages.Length);
Assert.All(messages, message =>
{
Assert.Contains("Name:", message);
Assert.Contains("ID:", message);
});
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Workflows.Models;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray;
/// <summary>
/// A test activity that produces an IEnumerable&lt;string&gt; via Select projection, which has two generic arguments and was triggering the bug in ConvertIEnumerableToArray.
/// </summary>
public class TestEnumerableActivity : Activity
{
public Output<object?> EnumerableResult { get; set; } = new();
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Create a list of items
var items = Enumerable
.Range(1, 5)
.Select(x => new TestOutputItem())
.ToList();
// Use Select to project to strings - this creates a ListSelectIterator<TestOutputItem, string>
// which has TWO generic arguments, triggering the bug
var messages = items.Select(e => $"Name: {e.Name} ID: {e.Id}");
context.Set(EnumerableResult, messages);
return context.CompleteActivityWithOutcomesAsync("Done");
}
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray;
public class TestOutputItem
{
public string Name { get; } = Guid.NewGuid().ToString();
public string Id { get; } = Guid.NewGuid().ToString();
}

View file

@ -1,5 +1,8 @@
using Elsa.Expressions.JavaScript.Activities;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
namespace Elsa.Workflows.Core.UnitTests;
@ -13,7 +16,7 @@ public class ExpressionExecutionContextExtensionsTests
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
{ variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);
@ -46,7 +49,7 @@ public class ExpressionExecutionContextExtensionsTests
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
{ variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);
@ -92,7 +95,7 @@ public class ExpressionExecutionContextExtensionsTests
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
{ variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);