431 lines
13 KiB
C#
431 lines
13 KiB
C#
|
|
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;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Integration tests for the ElsaScript parser.
|
||
|
|
/// </summary>
|
||
|
|
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<IElsaScriptParser>();
|
||
|
|
}
|
||
|
|
|
||
|
|
[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<Ast.VariableDeclarationNode>(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<Ast.ActivityInvocationNode>(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<Ast.ListenNode>(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<Ast.VariableDeclarationNode>(workflow.Body[0]);
|
||
|
|
Assert.Equal("message", varDecl.Name);
|
||
|
|
Assert.Equal(Ast.VariableKind.Var, varDecl.Kind);
|
||
|
|
|
||
|
|
// Verify listen statement
|
||
|
|
var listenNode = Assert.IsType<Ast.ListenNode>(workflow.Body[1]);
|
||
|
|
Assert.Equal("HttpEndpoint", listenNode.Activity.ActivityName);
|
||
|
|
Assert.Single(listenNode.Activity.Arguments);
|
||
|
|
|
||
|
|
// Verify WriteLine with ElsaScript expression
|
||
|
|
var writeLineNode = Assert.IsType<Ast.ActivityInvocationNode>(workflow.Body[2]);
|
||
|
|
Assert.Equal("WriteLine", writeLineNode.ActivityName);
|
||
|
|
Assert.Single(writeLineNode.Arguments);
|
||
|
|
var writeLineExpr = Assert.IsType<Ast.ElsaExpressionNode>(writeLineNode.Arguments[0].Value);
|
||
|
|
Assert.Equal("js", writeLineExpr.Language);
|
||
|
|
|
||
|
|
// Verify WriteHttpResponse with variable reference
|
||
|
|
var writeHttpNode = Assert.IsType<Ast.ActivityInvocationNode>(workflow.Body[3]);
|
||
|
|
Assert.Equal("WriteHttpResponse", writeHttpNode.ActivityName);
|
||
|
|
Assert.Single(writeHttpNode.Arguments);
|
||
|
|
var writeHttpArg = Assert.IsType<Ast.IdentifierNode>(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<Ast.ForNode>(workflow.Body[0]);
|
||
|
|
Assert.True(forNode.DeclaresVariable); // var i
|
||
|
|
Assert.Equal("i", forNode.VariableName);
|
||
|
|
Assert.False(forNode.IsInclusive);
|
||
|
|
|
||
|
|
var startLiteral = Assert.IsType<Ast.LiteralNode>(forNode.Start);
|
||
|
|
// Numbers are parsed as decimals by the parser
|
||
|
|
Assert.Equal(0m, Convert.ToDecimal(startLiteral.Value!));
|
||
|
|
|
||
|
|
var endLiteral = Assert.IsType<Ast.LiteralNode>(forNode.End);
|
||
|
|
Assert.Equal(10m, Convert.ToDecimal(endLiteral.Value!));
|
||
|
|
|
||
|
|
var stepLiteral = Assert.IsType<Ast.LiteralNode>(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<Ast.ForNode>(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<Ast.FlowchartNode>(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<Ast.FlowchartNode>(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<Ast.FlowchartNode>(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<Ast.BlockNode>(flowchart.Activities[0].Activity);
|
||
|
|
Assert.Equal(2, blockNode.Statements.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|