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:
Sipke Schoorstra 2025-10-21 21:31:47 +02:00
parent ad0dfad77e
commit d1f3ca7e41
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
4 changed files with 605 additions and 4 deletions

View file

@ -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)

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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))!;
}
}