using Elsa.Dsl.ElsaScript.Contracts;
using Elsa.Extensions;
using Elsa.Http;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Dsl.ElsaScript.IntegrationTests;
///
/// Integration tests for the ElsaScript compiler.
///
public class CompilerTests
{
private readonly IElsaScriptCompiler _compiler;
public CompilerTests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper)
.ConfigureElsa(elsa =>
{
elsa.UseElsaScript();
elsa.AddActivitiesFrom();
})
.Build();
_compiler = services.GetRequiredService();
}
[Fact(DisplayName = "Compiler can compile a simple workflow from source")]
public async Task CompileAsync_WithSimpleWorkflowSource_ShouldCreateWorkflowWithCorrectName()
{
// Arrange
var source = @"
use Elsa.Activities.Console;
use expressions js;
workflow HelloWorld {
WriteLine(""Hello World"");
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert - verify the workflow is correctly compiled
Assert.NotNull(workflow);
Assert.Equal("HelloWorld", workflow.Name);
Assert.NotNull(workflow.Root);
}
[Fact(DisplayName = "Compiler can compile workflow with variable declarations")]
public async Task CompileAsync_WithVariableDeclarations_ShouldCreateWorkflowWithAllVariables()
{
// Arrange
var source = @"
use expressions js;
workflow VariableTest {
var message = ""Hello from variable"";
var count = 42;
const pi = 3.14;
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert - verify variables are compiled correctly
Assert.NotNull(workflow);
Assert.Equal("VariableTest", workflow.Name);
Assert.Equal(3, workflow.Variables.Count);
Assert.Contains(workflow.Variables, v => v.Name == "message");
Assert.Contains(workflow.Variables, v => v.Name == "count");
Assert.Contains(workflow.Variables, v => v.Name == "pi");
}
[Fact(DisplayName = "Compiler can compile workflow without workflow keyword")]
public async Task CompileAsync_WithoutWorkflowKeyword_ShouldCreateWorkflow()
{
// Arrange
var source = @"WriteLine(""Hello World"");";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.NotNull(workflow.Root);
}
[Fact(DisplayName = "Compiler can compile complex workflow with variables, listen statements, and expressions")]
public async Task CompileAsync_WithComplexWorkflow_ShouldCreateWorkflowWithCorrectStructure()
{
// Arrange
var source = @"
use expressions js;
workflow HelloWorldHttpDsl {
var message = ""Hello World from DSL via Expressions!"";
listen HttpEndpoint(""/hello-world-dsl"");
WriteLine(js => `Message: ${message}`);
WriteHttpResponse(message);
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.Equal("HelloWorldHttpDsl", workflow.Name);
// Verify workflow has the message variable
Assert.Single(workflow.Variables);
var messageVar = workflow.Variables.First();
Assert.Equal("message", messageVar.Name);
Assert.Equal("Hello World from DSL via Expressions!", messageVar.Value);
// Verify root is a Sequence with 3 activities (HttpEndpoint, WriteLine, WriteHttpResponse)
var sequence = Assert.IsType(workflow.Root);
Assert.Equal(3, sequence.Activities.Count);
// Verify HttpEndpoint activity (from listen statement)
var httpEndpoint = sequence.Activities.ElementAt(0);
Assert.Equal("Elsa.HttpEndpoint", httpEndpoint.Type);
// Verify HttpEndpoint can start workflow (CanStartWorkflow property should be true)
var canStartWorkflowProp = httpEndpoint.GetType().GetProperty("CanStartWorkflow");
Assert.NotNull(canStartWorkflowProp);
var canStartWorkflow = (bool)canStartWorkflowProp.GetValue(httpEndpoint)!;
Assert.True(canStartWorkflow);
// Verify WriteLine activity with JavaScript expression
var writeLine = sequence.Activities.ElementAt(1);
Assert.Equal("Elsa.WriteLine", writeLine.Type);
var textProp = writeLine.GetType().GetProperty("Text");
Assert.NotNull(textProp);
var textInput = Assert.IsType>(textProp.GetValue(writeLine));
// Verify it's a JavaScript expression
var expression = textInput.Expression;
Assert.NotNull(expression);
Assert.Equal("JavaScript", expression.Type);
Assert.Contains("Message:", expression.Value?.ToString());
Assert.Contains("message", expression.Value?.ToString());
// Verify WriteHttpResponse activity with variable reference
var writeHttpResponse = sequence.Activities.ElementAt(2);
Assert.Equal("Elsa.WriteHttpResponse", writeHttpResponse.Type);
var contentProp = writeHttpResponse.GetType().GetProperty("Content");
Assert.NotNull(contentProp);
var contentInput = Assert.IsType>(contentProp.GetValue(writeHttpResponse));
// Verify it references the message variable
var memoryBlockReference = contentInput.MemoryBlockReference();
Assert.NotNull(memoryBlockReference);
Assert.Equal(messageVar.Id, memoryBlockReference.Id);
}
[Fact(DisplayName = "Compiler can compile for loop with 'to' keyword (exclusive)")]
public async Task CompileAsync_WithForLoopExclusive_ShouldCreateForActivity()
{
// Arrange
var source = @"
use expressions js;
workflow ForLoopTest {
for (var i = 0 to 10 step 1)
{
WriteLine(js => `Step: ${i}`)
}
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.Equal("ForLoopTest", workflow.Name);
// Verify the For activity is created
var forActivity = Assert.IsType(workflow.Root);
Assert.NotNull(forActivity);
// Verify Start, End, Step values
Assert.NotNull(forActivity.Start);
Assert.NotNull(forActivity.End);
Assert.NotNull(forActivity.Step);
// Verify OuterBoundInclusive is false (exclusive 'to')
Assert.NotNull(forActivity.OuterBoundInclusive);
var outerBoundInput = forActivity.OuterBoundInclusive;
var outerBoundExpr = outerBoundInput.Expression;
Assert.NotNull(outerBoundExpr);
Assert.False((bool?)outerBoundExpr.Value);
// Verify loop variable exists
Assert.Single(workflow.Variables);
var loopVar = workflow.Variables.First();
Assert.Equal("i", loopVar.Name);
// Verify body exists
Assert.NotNull(forActivity.Body);
}
[Fact(DisplayName = "Compiler can compile for loop with 'through' keyword (inclusive)")]
public async Task CompileAsync_WithForLoopInclusive_ShouldCreateForActivity()
{
// Arrange
var source = @"
use expressions js;
workflow ForLoopInclusiveTest {
for (var i = 0 through 10 step 1)
{
WriteLine(js => `Step: ${i}`)
}
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.Equal("ForLoopInclusiveTest", workflow.Name);
// Verify the For activity is created
var forActivity = Assert.IsType(workflow.Root);
Assert.NotNull(forActivity);
// Verify OuterBoundInclusive is true (inclusive 'through')
Assert.NotNull(forActivity.OuterBoundInclusive);
var outerBoundInput = forActivity.OuterBoundInclusive;
var outerBoundExpr = outerBoundInput.Expression;
Assert.NotNull(outerBoundExpr);
Assert.True((bool?)outerBoundExpr.Value);
// Verify loop variable exists
Assert.Single(workflow.Variables);
var loopVar = workflow.Variables.First();
Assert.Equal("i", loopVar.Name);
}
[Fact(DisplayName = "Compiler can compile workflow with metadata")]
public async Task CompileAsync_WithWorkflowMetadata_ShouldCreateWorkflowWithCorrectMetadata()
{
// Arrange
var source = @"
use Elsa.Activities.Console;
workflow HelloWorldDsl(
DisplayName: ""Hello World DSL"",
Description: ""Demonstrates ElsaScript with metadata"",
DefinitionId: ""hello-world-dsl"",
DefinitionVersionId: ""hello-world-dsl-v1"",
Version: 2,
UsableAsActivity: true
) {
use expressions js;
WriteLine(""Hello World from Elsa DSL!"");
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
// Check identity
Assert.Equal("hello-world-dsl", workflow.Identity.DefinitionId);
Assert.Equal(2, workflow.Identity.Version);
Assert.Equal("hello-world-dsl-v1", workflow.Identity.Id);
// Check metadata
Assert.Equal("Hello World DSL", workflow.WorkflowMetadata.Name);
Assert.Equal("Demonstrates ElsaScript with metadata", workflow.WorkflowMetadata.Description);
// Check options
Assert.True(workflow.Options.UsableAsActivity);
// Check root activity
Assert.NotNull(workflow.Root);
}
[Fact(DisplayName = "Compiler can compile empty flowchart")]
public async Task CompileAsync_WithEmptyFlowchart_ShouldCreateFlowchartActivity()
{
// Arrange
var source = @"
workflow FlowchartTest {
flowchart {
}
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.NotNull(workflow.Root);
Assert.IsType(workflow.Root);
}
[Fact(DisplayName = "Compiler can compile flowchart with node and connection")]
public async Task CompileAsync_WithFlowchartNodeAndConnection_ShouldCreateFlowchartWithCorrectStructure()
{
// Arrange
var source = @"
workflow FlowchartTest {
flowchart {
entry Start;
Start: WriteLine();
End: WriteLine();
Start -> End;
}
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.NotNull(workflow.Root);
var flowchart = Assert.IsType(workflow.Root);
// Check that flowchart has 2 activities
Assert.Equal(2, flowchart.Activities.Count);
// Check connections
Assert.Single(flowchart.Connections);
// Check entry point is set
Assert.NotNull(flowchart.Start);
}
[Fact(DisplayName = "Compiler can compile flowchart with block node")]
public async Task CompileAsync_WithFlowchartBlockNode_ShouldCreateSequenceActivity()
{
// Arrange
var source = @"
workflow FlowchartWithBlock {
flowchart {
entry Start;
Start: {
WriteLine(""First"");
WriteLine(""Second"");
}
}
}";
// Act
var workflow = await _compiler.CompileAsync(source);
// Assert
Assert.NotNull(workflow);
Assert.NotNull(workflow.Root);
var flowchart = Assert.IsType(workflow.Root);
// Should have one activity
Assert.Single(flowchart.Activities);
// The activity should be a Sequence (from the block)
var sequenceActivity = Assert.IsType(flowchart.Activities.First());
// Sequence should have 2 activities
Assert.Equal(2, sequenceActivity.Activities.Count);
}
[Fact(DisplayName = "Compiler resets default expression language between compilations")]
public async Task CompileAsync_WithSecondWorkflowAfterLiquid_ShouldResetToJavaScript()
{
// Arrange - First workflow sets Liquid as default
var firstSource = @"
use expressions liquid;
workflow FirstWorkflow {
WriteLine(liquid => ""{{ 'test' }}"");
}";
// Second workflow with no 'use expressions' directive
var secondSource = @"
workflow SecondWorkflow {
WriteLine(js => ""test"");
}";
// Act - Compile first workflow (sets default to Liquid)
var firstWorkflow = await _compiler.CompileAsync(firstSource);
// Act - Compile second workflow (should reset to JavaScript)
var secondWorkflow = await _compiler.CompileAsync(secondSource);
// Assert - Verify first workflow used Liquid
Assert.NotNull(firstWorkflow);
var firstWriteLine = firstWorkflow.Root;
var firstTextProp = firstWriteLine.GetType().GetProperty("Text");
var firstTextInput = Assert.IsType>(firstTextProp!.GetValue(firstWriteLine));
Assert.Equal("Liquid", firstTextInput.Expression?.Type);
// Assert - Verify second workflow uses JavaScript (not leaked Liquid)
Assert.NotNull(secondWorkflow);
var secondWriteLine = secondWorkflow.Root;
var secondTextProp = secondWriteLine.GetType().GetProperty("Text");
var secondTextInput = Assert.IsType>(secondTextProp!.GetValue(secondWriteLine));
Assert.Equal("JavaScript", secondTextInput.Expression?.Type);
}
}