using Elsa.Expressions.JavaScript.Contracts;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Memory;
using Jint;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.JavaScript.IntegrationTests;
///
/// The generated variable, input and activity output accessors are registered only when the expression names
/// them, which by construction is invisible to an expression that names them the ordinary way. These tests pin
/// that invisibility, and pin the four escapes that turn the filtering off for expressions which reach a global
/// without naming it: globalThis, direct eval, indirect eval and the Function constructor.
///
public class ReferencedGlobalTests(ITestOutputHelper testOutputHelper)
{
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "A variable accessor the expression names is registered")]
public async Task NamedVariableAccessorsAreRegistered()
{
Assert.Equal(42, await EvaluateAsync("return getMyVariable();"));
}
[Fact(DisplayName = "A variable mutator the expression names is registered")]
public async Task NamedVariableMutatorsAreRegistered()
{
Assert.Equal(99, await EvaluateAsync("setMyVariable(99); return getMyVariable();"));
}
[Fact(DisplayName = "An accessor is reachable through the dynamic functions without being named")]
public async Task DynamicAccessorFunctionsDoNotDependOnTheGeneratedAccessors()
{
// getVariable is a common function rather than a generated accessor, so it is registered whatever the
// expression references. It is what an expression that cannot name an accessor statically should use.
Assert.Equal(42, await EvaluateAsync("return getVariable('MyVariable');"));
}
[Fact(DisplayName = "An expression that references globalThis sees an accessor it does not name")]
public async Task ReferencingGlobalThisRegistersEveryAccessor()
{
Assert.Equal("function", await EvaluateAsync("return typeof globalThis['getMyVariable'];"));
}
[Fact(DisplayName = "An expression that calls eval directly sees an accessor it does not name")]
public async Task CallingEvalDirectlyRegistersEveryAccessor()
{
Assert.Equal("function", await EvaluateAsync("return eval(\"typeof getMyVariable\");"));
}
[Fact(DisplayName = "An expression that uses the Function constructor sees an accessor it does not name")]
public async Task UsingTheFunctionConstructorRegistersEveryAccessor()
{
// Code built by the Function constructor resolves only against the global scope, so the accessor has to
// be there. The parser does not flag this as a dynamic-code call the way it flags direct eval; it reports
// the identifier Function instead, which is the signal to act on.
Assert.Equal(42, await EvaluateAsync("return new Function('return getMyVariable()')();"));
}
[Fact(DisplayName = "An expression that calls eval indirectly sees an accessor it does not name")]
public async Task CallingEvalIndirectlyRegistersEveryAccessor()
{
// Indirect eval is not a direct eval call, so HasDirectEvalCall is false. As with the Function
// constructor, the identifier eval appearing in the set is the signal.
Assert.Equal("function", await EvaluateAsync("var e = eval; return e('typeof getMyVariable');"));
}
[Fact(DisplayName = "An accessor the expression does not name is never registered")]
public async Task UnnamedAccessorsAreNotRegistered()
{
// The tests above show the filtering is invisible from inside a script, which is also what makes it
// unprovable from there. Holding on to the engine is the only way to see that the accessor really was
// skipped rather than merely unused.
var engine = await EvaluateAndCaptureEngineAsync("return 1;");
Assert.True(engine.GetValue("getMyVariable").IsUndefined());
Assert.True(engine.GetValue("setMyVariable").IsUndefined());
}
[Fact(DisplayName = "An accessor the expression names is registered on the engine")]
public async Task NamedAccessorsAreRegisteredOnTheEngine()
{
var engine = await EvaluateAndCaptureEngineAsync("return getMyVariable();");
Assert.True(engine.GetValue("getMyVariable").IsObject());
Assert.True(engine.GetValue("setMyVariable").IsUndefined());
}
private async Task EvaluateAsync(string script)
{
var context = await _fixture.CreateExpressionExecutionContextAsync([new Variable("MyVariable", 42)]);
var evaluator = _fixture.Services.GetRequiredService();
var result = await evaluator.EvaluateAsync(script, typeof(T), context);
return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!;
}
private async Task EvaluateAndCaptureEngineAsync(string script)
{
Engine? engine = null;
var fixture = new WorkflowTestFixture(testOutputHelper);
fixture.ConfigureElsa(elsa => elsa.UseJavaScript(jintOptions => jintOptions.ConfigureEngine(e => engine = e)));
var context = await fixture.CreateExpressionExecutionContextAsync([new Variable("MyVariable", 42)]);
var evaluator = fixture.Services.GetRequiredService();
await evaluator.EvaluateAsync(script, typeof(object), context);
return engine!;
}
}