Add integration tests for JavaScript function availability and behavior validation
- Introduce `JintJavaScriptEvaluatorTests` to ensure all JavaScript custom functions are available and callable. - Add `JintJavaScriptFunctionBehaviorTests` to validate execution and behavior of JavaScript functions. - Extend `WorkflowTestFixture` with `CreateExpressionExecutionContextAsync` for testing JavaScript expressions. - Update test guidelines with examples for testing JavaScript functions and evaluating expressions.
This commit is contained in:
parent
ad0dfad77e
commit
d1f3ca7e41
|
|
@ -336,6 +336,9 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status);
|
|||
| `WorkflowTestFixture.GetOutcomes` | Get all outcomes from specific activity | Asserting activity outcomes in integration tests |
|
||||
| `WorkflowTestFixture.HasOutcome` | Check if activity produced specific outcome | Asserting single outcome in integration tests |
|
||||
| `WorkflowTestFixture.GetActivityStatus` | Get execution status of specific activity | Asserting activity status (Faulted, Completed, etc.) |
|
||||
| `WorkflowTestFixture.CreateWorkflowExecutionContextAsync` | Create workflow execution context without running workflow | Testing workflow-level concerns or base context setup |
|
||||
| `WorkflowTestFixture.CreateActivityExecutionContextAsync` | Create activity execution context (2 overloads) | Testing activity scheduling/execution or when needing activity context |
|
||||
| `WorkflowTestFixture.CreateExpressionExecutionContextAsync` | Create expression execution context (2 overloads) | Testing expression evaluators (JavaScript, Liquid, C#) with variables |
|
||||
| `context.GetActivityOutput` | Get output from activity using expression selector | Asserting activity outputs in unit tests |
|
||||
| `context.HasScheduledActivity` | Check if activity is scheduled | Verifying scheduling behavior in unit tests |
|
||||
| `context.GetOutcomes` | Get all outcomes from activity execution | Asserting multiple outcomes in unit tests |
|
||||
|
|
@ -439,6 +442,95 @@ Assert.Equal(ActivityStatus.Faulted, status);
|
|||
- `ActivityStatus.Canceled` - Activity was canceled
|
||||
- `ActivityStatus.Faulted` - Activity encountered an error
|
||||
|
||||
### Creating Execution Contexts for Testing
|
||||
|
||||
`WorkflowTestFixture` provides layered methods for creating execution contexts at different levels, giving you fine-grained control over test setup:
|
||||
|
||||
#### Creating a Workflow Execution Context
|
||||
|
||||
Use `CreateWorkflowExecutionContextAsync` to create a minimal workflow execution context without running the workflow:
|
||||
|
||||
```csharp
|
||||
var context = await _fixture.CreateWorkflowExecutionContextAsync(variables: new[]
|
||||
{
|
||||
new Variable<int>("Counter", 0)
|
||||
});
|
||||
```
|
||||
|
||||
#### Creating an Activity Execution Context
|
||||
|
||||
Use `CreateActivityExecutionContextAsync` to create an activity execution context. Two overloads available:
|
||||
|
||||
**Without existing workflow context (creates one automatically):**
|
||||
```csharp
|
||||
var activityContext = await _fixture.CreateActivityExecutionContextAsync(
|
||||
activity: myActivity,
|
||||
variables: new[] { new Variable<string>("MyVar", "value") }
|
||||
);
|
||||
```
|
||||
|
||||
**With existing workflow context:**
|
||||
```csharp
|
||||
var workflowContext = await _fixture.CreateWorkflowExecutionContextAsync();
|
||||
var activityContext = await _fixture.CreateActivityExecutionContextAsync(
|
||||
workflowContext,
|
||||
activity: myActivity
|
||||
);
|
||||
```
|
||||
|
||||
#### Creating an Expression Execution Context
|
||||
|
||||
Use `CreateExpressionExecutionContextAsync` to create a context for testing expression evaluation (e.g., JavaScript, Liquid). Variables are properly registered and accessible via dynamic accessors. Two overloads available:
|
||||
|
||||
**Without existing activity context (creates one automatically):**
|
||||
```csharp
|
||||
var expressionContext = await _fixture.CreateExpressionExecutionContextAsync(new[]
|
||||
{
|
||||
new Variable<string>("MyVariable", "test value")
|
||||
});
|
||||
|
||||
// Variables are accessible via dynamic accessors in expressions
|
||||
// e.g., getMyVariable() and setMyVariable(value) in JavaScript
|
||||
```
|
||||
|
||||
**With existing activity context:**
|
||||
```csharp
|
||||
var activityContext = await _fixture.CreateActivityExecutionContextAsync();
|
||||
var expressionContext = await _fixture.CreateExpressionExecutionContextAsync(
|
||||
activityContext,
|
||||
variables: new[] { new Variable<int>("Count", 42) }
|
||||
);
|
||||
```
|
||||
|
||||
**Example: Testing JavaScript Expression Evaluation**
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Dynamic_Variable_Accessors_Should_Work()
|
||||
{
|
||||
// Arrange
|
||||
var script = @"
|
||||
setMyVariable('updated value');
|
||||
return getMyVariable();
|
||||
";
|
||||
var context = await _fixture.CreateExpressionExecutionContextAsync(new[]
|
||||
{
|
||||
new Variable<string>("MyVariable", "initial value")
|
||||
});
|
||||
|
||||
// Act
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("updated value", result);
|
||||
}
|
||||
```
|
||||
|
||||
**When to use each method:**
|
||||
- Use `CreateWorkflowExecutionContextAsync` when testing workflow-level concerns or when you need a base context for further customization
|
||||
- Use `CreateActivityExecutionContextAsync` when testing activity scheduling, execution, or when you need access to activity-specific context
|
||||
- Use `CreateExpressionExecutionContextAsync` when testing expression evaluators (JavaScript, Liquid, C#) or when you need variables to be accessible in expressions
|
||||
|
||||
---
|
||||
|
||||
## Failure testing (faults & incidents)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.State;
|
||||
using JetBrains.Annotations;
|
||||
|
|
@ -25,8 +26,8 @@ public class WorkflowTestFixture
|
|||
/// <param name="testOutputHelper">The test output helper</param>
|
||||
public WorkflowTestFixture(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testApplicationBuilder = new TestApplicationBuilder(testOutputHelper);
|
||||
CapturingTextWriter = new CapturingTextWriter();
|
||||
_testApplicationBuilder = new(testOutputHelper);
|
||||
CapturingTextWriter = new();
|
||||
_testApplicationBuilder.WithCapturingTextWriter(CapturingTextWriter);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ public class WorkflowTestFixture
|
|||
/// <summary>
|
||||
/// Gets the service provider. Throws if Build() hasn't been called yet.
|
||||
/// </summary>
|
||||
private IServiceProvider Services => _services ?? throw new InvalidOperationException("Build() must be called before accessing services");
|
||||
public IServiceProvider Services => _services ?? throw new InvalidOperationException("Build() must be called before accessing services");
|
||||
|
||||
/// <summary>
|
||||
/// Configures Elsa features.
|
||||
|
|
@ -192,4 +193,96 @@ public class WorkflowTestFixture
|
|||
|
||||
return activityContext?.Status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a WorkflowExecutionContext for testing.
|
||||
/// This creates a minimal workflow execution context without executing the workflow.
|
||||
/// </summary>
|
||||
/// <param name="variables">Optional workflow variables to include in the workflow</param>
|
||||
/// <returns>A WorkflowExecutionContext that can be used for testing</returns>
|
||||
public async Task<WorkflowExecutionContext> CreateWorkflowExecutionContextAsync(Variable[]? variables = null)
|
||||
{
|
||||
if (_services == null)
|
||||
await BuildAsync();
|
||||
|
||||
// Create a minimal workflow with variables
|
||||
var workflow = new Workflow
|
||||
{
|
||||
Root = new Sequence()
|
||||
};
|
||||
|
||||
if (variables != null)
|
||||
foreach (var variable in variables)
|
||||
workflow.Variables.Add(variable);
|
||||
|
||||
// Build the workflow graph
|
||||
var workflowGraphBuilder = Services.GetRequiredService<IWorkflowGraphBuilder>();
|
||||
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
|
||||
|
||||
// Create workflow execution context
|
||||
return await WorkflowExecutionContext.CreateAsync(
|
||||
Services,
|
||||
workflowGraph,
|
||||
$"test-instance-{Guid.NewGuid()}",
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ActivityExecutionContext for testing.
|
||||
/// Creates a workflow execution context first, then creates an activity execution context for the specified activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to create a context for. If null, uses the workflow itself.</param>
|
||||
/// <param name="variables">Optional workflow variables to include</param>
|
||||
/// <returns>An ActivityExecutionContext that can be used for testing</returns>
|
||||
public async Task<ActivityExecutionContext> CreateActivityExecutionContextAsync(IActivity? activity = null, Variable[]? variables = null)
|
||||
{
|
||||
var workflowExecutionContext = await CreateWorkflowExecutionContextAsync(variables);
|
||||
return await CreateActivityExecutionContextAsync(workflowExecutionContext, activity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ActivityExecutionContext for testing using an existing WorkflowExecutionContext.
|
||||
/// </summary>
|
||||
/// <param name="workflowExecutionContext">The workflow execution context to use</param>
|
||||
/// <param name="activity">The activity to create a context for. If null, uses the workflow itself.</param>
|
||||
/// <returns>An ActivityExecutionContext that can be used for testing</returns>
|
||||
public async Task<ActivityExecutionContext> CreateActivityExecutionContextAsync(WorkflowExecutionContext workflowExecutionContext, IActivity? activity = null)
|
||||
{
|
||||
// Use the workflow itself if no activity specified, as Workflow implements IVariableContainer.
|
||||
// This ensures variables are accessible.
|
||||
var targetActivity = activity ?? workflowExecutionContext.Workflow;
|
||||
return await workflowExecutionContext.CreateActivityExecutionContextAsync(targetActivity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExpressionExecutionContext for testing expression evaluation.
|
||||
/// This creates a minimal workflow and activity execution context, then initializes variables.
|
||||
/// Variables are properly registered and accessible via dynamic accessors (e.g., getMyVariable, setMyVariable).
|
||||
/// </summary>
|
||||
/// <param name="variables">Optional workflow variables to include in the execution context</param>
|
||||
/// <returns>An ExpressionExecutionContext that can be used for expression evaluation</returns>
|
||||
public async Task<ExpressionExecutionContext> CreateExpressionExecutionContextAsync(Variable[]? variables = null)
|
||||
{
|
||||
var activityContext = await CreateActivityExecutionContextAsync(activity: null, variables: variables);
|
||||
return await CreateExpressionExecutionContextAsync(activityContext, variables);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExpressionExecutionContext using an existing ActivityExecutionContext.
|
||||
/// Initializes variables if provided.
|
||||
/// </summary>
|
||||
/// <param name="activityContext">The activity execution context to use</param>
|
||||
/// <param name="variables">Optional workflow variables to initialize</param>
|
||||
/// <returns>An ExpressionExecutionContext that can be used for expression evaluation</returns>
|
||||
public Task<ExpressionExecutionContext> CreateExpressionExecutionContextAsync(ActivityExecutionContext activityContext, Variable[]? variables = null)
|
||||
{
|
||||
// Initialize variables in the execution context if provided
|
||||
// Use Variable.Set() to properly register variables (same approach as ActivityTestFixture)
|
||||
if (variables != null)
|
||||
foreach (var variable in variables)
|
||||
variable.Set(activityContext.ExpressionExecutionContext, variable.Value);
|
||||
|
||||
return Task.FromResult(activityContext.ExpressionExecutionContext);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.JavaScript.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.JavaScript.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for JintJavaScriptEvaluator to ensure all custom functions remain available.
|
||||
/// These tests protect against accidental renaming or removal of JavaScript functions.
|
||||
/// </summary>
|
||||
public class JintJavaScriptEvaluatorTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
[Theory(DisplayName = "Common workflow functions should be available")]
|
||||
[InlineData("getWorkflowDefinitionId")]
|
||||
[InlineData("getWorkflowDefinitionVersionId")]
|
||||
[InlineData("getWorkflowDefinitionVersion")]
|
||||
[InlineData("getWorkflowInstanceId")]
|
||||
[InlineData("getCorrelationId")]
|
||||
[InlineData("getWorkflowInstanceName")]
|
||||
public async Task Common_Workflow_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Workflow mutator functions should be available")]
|
||||
[InlineData("setCorrelationId")]
|
||||
[InlineData("setWorkflowInstanceName")]
|
||||
[InlineData("setVariable")]
|
||||
public async Task Workflow_Mutator_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Variable and input/output accessor functions should be available")]
|
||||
[InlineData("getVariable")]
|
||||
[InlineData("getInput")]
|
||||
[InlineData("getOutputFrom")]
|
||||
[InlineData("getLastResult")]
|
||||
public async Task Variable_And_IO_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "String utility functions should be available")]
|
||||
[InlineData("isNullOrWhiteSpace")]
|
||||
[InlineData("isNullOrEmpty")]
|
||||
public async Task String_Utility_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "GUID functions should be available")]
|
||||
[InlineData("parseGuid")]
|
||||
[InlineData("newGuid")]
|
||||
[InlineData("newGuidString")]
|
||||
[InlineData("newShortGuid")]
|
||||
[InlineData("getGuidString")] // Deprecated but should still exist
|
||||
[InlineData("getShortGuid")] // Deprecated but should still exist
|
||||
public async Task GUID_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Encoding and serialization functions should be available")]
|
||||
[InlineData("toJson")]
|
||||
[InlineData("bytesToString")]
|
||||
[InlineData("bytesFromString")]
|
||||
[InlineData("bytesToBase64")]
|
||||
[InlineData("bytesFromBase64")]
|
||||
[InlineData("stringToBase64")]
|
||||
[InlineData("stringFromBase64")]
|
||||
[InlineData("streamToBytes")]
|
||||
[InlineData("streamToBase64")]
|
||||
public async Task Encoding_Functions_Should_Be_Available(string functionName)
|
||||
{
|
||||
// Arrange
|
||||
var script = $"return typeof {functionName};";
|
||||
var context = await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - function should exist (not 'undefined')
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Variable accessors should be created for workflow variables")]
|
||||
public async Task Variable_Accessors_Should_Be_Created()
|
||||
{
|
||||
// Arrange
|
||||
var script = "return typeof getMyVariable;";
|
||||
var context = await CreateExpressionExecutionContextAsync(variables:
|
||||
[
|
||||
new Variable<int>("MyVariable", 42)
|
||||
]);
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - getter function should be created for the variable
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Variable setter accessors should be created for workflow variables")]
|
||||
public async Task Variable_Setter_Accessors_Should_Be_Created()
|
||||
{
|
||||
// Arrange
|
||||
var script = "return typeof setMyVariable;";
|
||||
var context = await CreateExpressionExecutionContextAsync(variables:
|
||||
[
|
||||
new Variable<int>("MyVariable", 42)
|
||||
]);
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(string), context) as string;
|
||||
|
||||
// Assert - setter function should be created for the variable
|
||||
Assert.Equal("function", result);
|
||||
}
|
||||
|
||||
private async Task<ExpressionExecutionContext> CreateExpressionExecutionContextAsync(Variable[]? variables = null)
|
||||
{
|
||||
await _fixture.BuildAsync();
|
||||
|
||||
var workflow = new Elsa.Workflows.Activities.Workflow();
|
||||
|
||||
if (variables != null)
|
||||
{
|
||||
foreach (var variable in variables)
|
||||
{
|
||||
workflow.Variables.Add(variable);
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _fixture.RunActivityAsync(workflow);
|
||||
var activityContext = result.Journal.ActivityExecutionContexts.First();
|
||||
|
||||
return new ExpressionExecutionContext(
|
||||
_fixture.Services,
|
||||
activityContext.ExpressionExecutionContext.Memory,
|
||||
cancellationToken: default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
using Elsa.Expressions.JavaScript.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.JavaScript.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that validate the behavior of JavaScript custom functions, not just their existence.
|
||||
/// </summary>
|
||||
public class JintJavaScriptFunctionBehaviorTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
[Fact(DisplayName = "All JavaScript functions should execute without errors (smoke test)")]
|
||||
public async Task All_Functions_Should_Execute_Without_Errors()
|
||||
{
|
||||
// Arrange
|
||||
var script = @"
|
||||
// Execute all functions that don't require arguments to ensure they work
|
||||
var results = {
|
||||
// Workflow info functions
|
||||
workflowDefId: getWorkflowDefinitionId(),
|
||||
workflowDefVersionId: getWorkflowDefinitionVersionId(),
|
||||
workflowDefVersion: getWorkflowDefinitionVersion(),
|
||||
workflowInstanceId: getWorkflowInstanceId(),
|
||||
correlationId: getCorrelationId(),
|
||||
workflowName: getWorkflowInstanceName(),
|
||||
|
||||
// GUID functions
|
||||
newGuidValue: newGuid(),
|
||||
newGuidStringValue: newGuidString(),
|
||||
newShortGuidValue: newShortGuid(),
|
||||
|
||||
// Deprecated GUID functions
|
||||
getGuidStringValue: getGuidString(),
|
||||
getShortGuidValue: getShortGuid(),
|
||||
|
||||
// String utility functions
|
||||
isNullOrWhiteSpaceEmpty: isNullOrWhiteSpace(''),
|
||||
isNullOrWhiteSpaceText: isNullOrWhiteSpace('text'),
|
||||
isNullOrEmptyEmpty: isNullOrEmpty(''),
|
||||
isNullOrEmptyText: isNullOrEmpty('text'),
|
||||
|
||||
// Encoding functions
|
||||
toJsonValue: toJson({ key: 'value' }),
|
||||
stringToBase64Value: stringToBase64('test'),
|
||||
bytesToStringValue: bytesToString([72, 101, 108, 108, 111]),
|
||||
bytesToBase64Value: bytesToBase64([72, 101, 108, 108, 111]),
|
||||
};
|
||||
|
||||
return toJson(results);
|
||||
";
|
||||
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<string>(script);
|
||||
|
||||
// Assert - script should execute and return JSON
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("workflowDefId", result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GUID functions should return valid formats")]
|
||||
public async Task Guid_Functions_Should_Return_Valid_Formats()
|
||||
{
|
||||
// Arrange
|
||||
var script = @"
|
||||
return {
|
||||
guid: newGuid().toString(),
|
||||
guidString: newGuidString(),
|
||||
shortGuid: newShortGuid(),
|
||||
parsedGuid: parseGuid('12345678-1234-1234-1234-123456789abc').toString()
|
||||
};
|
||||
";
|
||||
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<object>(script);
|
||||
|
||||
// Assert
|
||||
var dict = result as IDictionary<string, object>;
|
||||
Assert.NotNull(dict);
|
||||
|
||||
// Validate GUID formats
|
||||
var guidString = dict["guidString"].ToString();
|
||||
Assert.Matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", guidString ?? "");
|
||||
|
||||
var shortGuid = dict["shortGuid"]?.ToString();
|
||||
Assert.NotNull(shortGuid);
|
||||
Assert.InRange(shortGuid.Length, 20, 22); // Base64 GUID without padding (can be 20-22 chars)
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Encoding functions should round-trip correctly")]
|
||||
[InlineData("String encoding", @"
|
||||
var original = 'Hello, World!';
|
||||
var base64 = stringToBase64(original);
|
||||
var decoded = stringFromBase64(base64);
|
||||
return decoded === original;
|
||||
")]
|
||||
[InlineData("Bytes encoding", @"
|
||||
var original = bytesFromString('Hello');
|
||||
var base64 = bytesToBase64(original);
|
||||
var decoded = bytesFromBase64(base64);
|
||||
var text = bytesToString(decoded);
|
||||
return text === 'Hello';
|
||||
")]
|
||||
public async Task Encoding_Functions_Should_Round_Trip(string scenario, string script)
|
||||
{
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<bool>(script);
|
||||
|
||||
// Assert - round-trip should preserve the original data
|
||||
Assert.True(result, $"{scenario} failed to round-trip correctly");
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Setter/getter functions should update and retrieve values correctly")]
|
||||
[InlineData("setVariable/getVariable", "setVariable('MyVar', 200); return getVariable('MyVar');", 200, "MyVar", 100)]
|
||||
[InlineData("Dynamic variable accessors", "setMyVariable(999); return getMyVariable();", 999, "MyVariable", 42)]
|
||||
public async Task Variable_Setters_Should_Update_Values(string scenario, string script, int expectedValue, string variableName, int initialValue)
|
||||
{
|
||||
// Arrange - Create context with variable
|
||||
var context = await CreateExpressionExecutionContextAsync(variables:
|
||||
[
|
||||
new Variable<int>(variableName, initialValue)
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<int>(script, context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result == expectedValue, $"{scenario}: Expected {expectedValue} but got {result}");
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Workflow mutator functions should update workflow properties")]
|
||||
[InlineData("setCorrelationId", "setCorrelationId('my-correlation-id'); return getCorrelationId();", "my-correlation-id")]
|
||||
[InlineData("setWorkflowInstanceName", "setWorkflowInstanceName('My Custom Workflow Name'); return getWorkflowInstanceName();", "My Custom Workflow Name")]
|
||||
public async Task Workflow_Mutators_Should_Update_Properties(string functionName, string script, string expectedValue)
|
||||
{
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<string>(script);
|
||||
|
||||
// Assert
|
||||
Assert.True(result == expectedValue, $"{functionName}: Expected '{expectedValue}' but got '{result}'");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "String utility functions should validate correctly")]
|
||||
public async Task String_Utility_Functions_Should_Validate_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
var script = @"
|
||||
return {
|
||||
emptyIsNullOrWhiteSpace: isNullOrWhiteSpace(''),
|
||||
whitespaceIsNullOrWhiteSpace: isNullOrWhiteSpace(' '),
|
||||
textIsNullOrWhiteSpace: isNullOrWhiteSpace('text'),
|
||||
emptyIsNullOrEmpty: isNullOrEmpty(''),
|
||||
whitespaceIsNullOrEmpty: isNullOrEmpty(' '),
|
||||
textIsNullOrEmpty: isNullOrEmpty('text')
|
||||
};
|
||||
";
|
||||
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<object>(script);
|
||||
|
||||
// Assert
|
||||
var dict = result as IDictionary<string, object>;
|
||||
Assert.NotNull(dict);
|
||||
|
||||
Assert.True((bool)dict["emptyIsNullOrWhiteSpace"]);
|
||||
Assert.True((bool)dict["whitespaceIsNullOrWhiteSpace"]);
|
||||
Assert.False((bool)dict["textIsNullOrWhiteSpace"]);
|
||||
|
||||
Assert.True((bool)dict["emptyIsNullOrEmpty"]);
|
||||
Assert.False((bool)dict["whitespaceIsNullOrEmpty"]); // Whitespace is not considered empty
|
||||
Assert.False((bool)dict["textIsNullOrEmpty"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "toJson should serialize objects correctly")]
|
||||
public async Task ToJson_Should_Serialize_Objects()
|
||||
{
|
||||
// Arrange
|
||||
var script = @"
|
||||
var obj = {
|
||||
name: 'Test',
|
||||
value: 42,
|
||||
nested: { inner: true }
|
||||
};
|
||||
return toJson(obj);
|
||||
";
|
||||
|
||||
// Act
|
||||
var result = await EvaluateScriptAsync<string>(script);
|
||||
|
||||
// Assert - should produce valid JSON
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"name\"", result);
|
||||
Assert.Contains("\"Test\"", result);
|
||||
Assert.Contains("\"value\"", result);
|
||||
Assert.Contains("42", result);
|
||||
}
|
||||
|
||||
private Task<ExpressionExecutionContext> CreateExpressionExecutionContextAsync(Variable[]? variables = null)
|
||||
{
|
||||
return _fixture.CreateExpressionExecutionContextAsync(variables);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to evaluate a JavaScript script and return the result as the specified type.
|
||||
/// Reduces boilerplate code in tests.
|
||||
/// </summary>
|
||||
private async Task<T> EvaluateScriptAsync<T>(string script, ExpressionExecutionContext? context = null)
|
||||
{
|
||||
context ??= await CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(T), context);
|
||||
|
||||
return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue