using Elsa.Dsl.ElsaScript.Contracts;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Dsl.ElsaScript.IntegrationTests;
///
/// Integration tests for the ElsaScript parser.
///
public class ParserTests
{
private readonly IServiceProvider _services;
private readonly IElsaScriptParser _parser;
public ParserTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.ConfigureElsa(elsa => elsa.UseElsaScript())
.Build();
_parser = _services.GetRequiredService();
}
[Fact(DisplayName = "Parser can parse a simple workflow definition")]
public void Parse_WithSimpleWorkflowDefinition_ShouldReturnWorkflowWithCorrectStructure()
{
// Arrange
var source = @"
use Elsa.Activities.Console;
use expressions js;
workflow HelloWorld {
WriteLine(""Hello World"");
WriteLine(""Great to meet you!"");
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("HelloWorld", workflow.Id);
// Global use statements
Assert.Equal(2, program.GlobalUseStatements.Count);
// No workflow-level use statements (all are global)
Assert.Empty(workflow.UseStatements);
Assert.Equal(2, workflow.Body.Count);
}
[Fact(DisplayName = "Parser can parse variable declarations")]
public void Parse_WithVariableDeclarations_ShouldReturnWorkflowWithAllVariableNodes()
{
// Arrange
var source = @"
use expressions js;
workflow VariableTest {
var greeting = ""Hello"";
var count = 42;
const pi = 3.14;
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("VariableTest", workflow.Id);
Assert.Equal(3, workflow.Body.Count);
var varDecl = Assert.IsType(workflow.Body[0]);
Assert.Equal(Ast.VariableKind.Var, varDecl.Kind);
Assert.Equal("greeting", varDecl.Name);
}
[Fact(DisplayName = "Parser can parse activity invocations with named arguments")]
public void Parse_WithActivityInvocationWithNamedArguments_ShouldReturnActivityNodeWithCorrectArguments()
{
// Arrange
var source = @"
use expressions js;
workflow ActivityTest {
WriteLine(Text: ""Hello World"");
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Single(workflow.Body);
var activity = Assert.IsType(workflow.Body[0]);
Assert.Equal("WriteLine", activity.ActivityName);
Assert.Single(activity.Arguments);
Assert.Equal("Text", activity.Arguments[0].Name);
}
[Fact(DisplayName = "Parser can parse listen statements")]
public void Parse_WithListenStatement_ShouldReturnWorkflowWithListenNode()
{
// Arrange
var source = @"
use expressions js;
workflow ListenTest {
listen HttpEndpoint(""/test"");
WriteLine(""Triggered!"");
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal(2, workflow.Body.Count);
var listen = Assert.IsType(workflow.Body[0]);
Assert.Equal("HttpEndpoint", listen.Activity.ActivityName);
}
[Fact(DisplayName = "Parser can parse workflow without workflow keyword")]
public void Parse_WithoutWorkflowKeyword_ShouldReturnWorkflowWithCorrectStructure()
{
// Arrange
var source = @"WriteLine(""Hello World""); WriteLine(""Great to meet you!"");";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal(2, workflow.Body.Count);
}
[Fact(DisplayName = "Parser can parse complex workflow with variables, listen statements, and ElsaScript expressions")]
public void Parse_WithComplexWorkflow_ShouldReturnWorkflowWithCorrectStructure()
{
// 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 program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("HelloWorldHttpDsl", workflow.Id);
// Verify global use statement
Assert.Single(program.GlobalUseStatements);
var useStatement = program.GlobalUseStatements[0];
Assert.Equal(Ast.UseType.Expressions, useStatement.Type);
Assert.Equal("js", useStatement.Value);
// No workflow-level use statements
Assert.Empty(workflow.UseStatements);
// Verify body contains: var declaration, listen statement, WriteLine, WriteHttpResponse
Assert.Equal(4, workflow.Body.Count);
// Verify variable declaration
var varDecl = Assert.IsType(workflow.Body[0]);
Assert.Equal("message", varDecl.Name);
Assert.Equal(Ast.VariableKind.Var, varDecl.Kind);
// Verify listen statement
var listenNode = Assert.IsType(workflow.Body[1]);
Assert.Equal("HttpEndpoint", listenNode.Activity.ActivityName);
Assert.Single(listenNode.Activity.Arguments);
// Verify WriteLine with ElsaScript expression
var writeLineNode = Assert.IsType(workflow.Body[2]);
Assert.Equal("WriteLine", writeLineNode.ActivityName);
Assert.Single(writeLineNode.Arguments);
var writeLineExpr = Assert.IsType(writeLineNode.Arguments[0].Value);
Assert.Equal("js", writeLineExpr.Language);
// Verify WriteHttpResponse with variable reference
var writeHttpNode = Assert.IsType(workflow.Body[3]);
Assert.Equal("WriteHttpResponse", writeHttpNode.ActivityName);
Assert.Single(writeHttpNode.Arguments);
var writeHttpArg = Assert.IsType(writeHttpNode.Arguments[0].Value);
Assert.Equal("message", writeHttpArg.Name);
}
[Fact(DisplayName = "Parser can parse for loop with 'to' keyword (exclusive)")]
public void Parse_WithForLoopExclusive_ShouldReturnWorkflowWithForNode()
{
// Arrange
var source = @"
use expressions js;
workflow ForLoopTest {
for (var i = 0 to 10 step 1)
{
WriteLine(js => `Step: ${i}`)
}
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("ForLoopTest", workflow.Id);
Assert.Single(workflow.Body);
var forNode = Assert.IsType(workflow.Body[0]);
Assert.True(forNode.DeclaresVariable); // var i
Assert.Equal("i", forNode.VariableName);
Assert.False(forNode.IsInclusive);
var startLiteral = Assert.IsType(forNode.Start);
// Numbers are parsed as decimals by the parser
Assert.Equal(0m, Convert.ToDecimal(startLiteral.Value!));
var endLiteral = Assert.IsType(forNode.End);
Assert.Equal(10m, Convert.ToDecimal(endLiteral.Value!));
var stepLiteral = Assert.IsType(forNode.Step);
Assert.Equal(1m, Convert.ToDecimal(stepLiteral.Value!));
}
[Fact(DisplayName = "Parser can parse for loop with 'through' keyword (inclusive)")]
public void Parse_WithForLoopInclusive_ShouldReturnWorkflowWithForNode()
{
// Arrange
var source = @"
use expressions js;
workflow ForLoopInclusiveTest {
for (var i = 0 through 10 step 1)
{
WriteLine(js => `Step: ${i}`)
}
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("ForLoopInclusiveTest", workflow.Id);
Assert.Single(workflow.Body);
var forNode = Assert.IsType(workflow.Body[0]);
Assert.True(forNode.DeclaresVariable); // var i
Assert.Equal("i", forNode.VariableName);
Assert.True(forNode.IsInclusive);
}
[Fact(DisplayName = "Parser can parse workflow with metadata")]
public void Parse_WithWorkflowMetadata_ShouldReturnWorkflowWithCorrectMetadata()
{
// 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: 1,
UsableAsActivity: true
) {
use expressions js;
WriteLine(""Hello World from Elsa DSL!"");
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("HelloWorldDsl", workflow.Id);
// Check metadata
Assert.Equal("Hello World DSL", (string)workflow.Metadata["DisplayName"]);
Assert.Equal("Demonstrates ElsaScript with metadata", (string)workflow.Metadata["Description"]);
Assert.Equal("hello-world-dsl", (string)workflow.Metadata["DefinitionId"]);
Assert.Equal("hello-world-dsl-v1", (string)workflow.Metadata["DefinitionVersionId"]);
Assert.Equal(1L, Convert.ToInt64(workflow.Metadata["Version"]));
Assert.True(Convert.ToBoolean(workflow.Metadata["UsableAsActivity"]));
// Check use statements (workflow-level only, global handled separately)
Assert.Single(workflow.UseStatements);
Assert.Equal(Ast.UseType.Expressions, workflow.UseStatements[0].Type);
Assert.Equal("js", workflow.UseStatements[0].Value);
// Check body
Assert.Single(workflow.Body);
}
[Fact(DisplayName = "Parser can parse empty flowchart")]
public void Parse_WithEmptyFlowchart_ShouldReturnFlowchartNode()
{
// Arrange
var source = @"
workflow FlowchartTest {
flowchart {
}
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
Assert.Equal("FlowchartTest", workflow.Id);
Assert.Single(workflow.Body);
Assert.IsType(workflow.Body[0]);
}
[Fact(DisplayName = "Parser can parse flowchart with node and connection")]
public void Parse_WithFlowchartNodeAndConnection_ShouldReturnCorrectStructure()
{
// Arrange
var source = @"
workflow FlowchartTest {
flowchart {
entry Start;
Start: WriteLine();
End: WriteLine();
Start -> End;
}
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
Assert.Single(program.Workflows);
var workflow = program.Workflows[0];
var flowchart = Assert.IsType(workflow.Body[0]);
// Check entry point
Assert.Equal("Start", flowchart.EntryPoint);
// Check nodes
Assert.Equal(2, flowchart.Activities.Count);
// Check connections
Assert.Single(flowchart.Connections);
Assert.Equal("Start", flowchart.Connections[0].Source);
Assert.Equal("End", flowchart.Connections[0].Target);
}
[Fact(DisplayName = "Parser can parse flowchart with block node")]
public void Parse_WithFlowchartBlockNode_ShouldReturnBlockStatement()
{
// Arrange
var source = @"
workflow FlowchartWithBlock {
flowchart {
entry Start;
Start: {
WriteLine(""First"");
WriteLine(""Second"");
}
}
}";
// Act
var program = _parser.Parse(source);
// Assert
Assert.NotNull(program);
var workflow = program.Workflows[0];
var flowchart = Assert.IsType(workflow.Body[0]);
Assert.Single(flowchart.Activities);
Assert.Equal("Start", flowchart.Activities[0].Label);
// The block should be a BlockNode with 2 statements
var blockNode = Assert.IsType(flowchart.Activities[0].Activity);
Assert.Equal(2, blockNode.Statements.Count);
}
}