using System.Dynamic; 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; /// /// Pins how an object-valued workflow variable is marshalled into JavaScript. /// public class VariableMarshallingTests(ITestOutputHelper testOutputHelper) { [Fact(DisplayName = "An object-valued variable is readable from a script")] public async Task ObjectValuedVariablesAreReadable() { Assert.Equal("Alice", await EvaluateAsync("return variables.Person.Name;")); } [Fact(DisplayName = "An object-valued variable is built in the shaped representation")] public async Task ObjectValuedVariablesAreShaped() { // Building into the shared-layout representation is a silent optimisation: JsObject.CreateFromEntries // falls back to the ordinary per-object property dictionary whenever it meets something a layout cannot // express, and the object behaves identically either way. Asking the engine is the only way to know the // fast one was reached. HasSharedShape is the part of that answer Jint documents as a contract — the // finer-grained GetObjectRepresentation names an internal representation that may be renamed or // subdivided in any release — and CreateFromEntries is one of its three documented success cases. var engine = await EvaluateAndCaptureEngineAsync("return variables.Person.Name;"); var person = engine.Evaluate("variables.Person").AsObject(); Assert.True(engine.Advanced.HasSharedShape(person)); } private static ExpandoObject CreatePerson() { var person = new ExpandoObject() as IDictionary; person["Name"] = "Alice"; person["Age"] = 30; return (ExpandoObject)person; } private async Task EvaluateAsync(string script) { var fixture = new WorkflowTestFixture(testOutputHelper); var context = await fixture.CreateExpressionExecutionContextAsync([new Variable("Person", CreatePerson())]); 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("Person", CreatePerson())]); var evaluator = fixture.Services.GetRequiredService(); await evaluator.EvaluateAsync(script, typeof(object), context); return engine!; } }