using Elsa.Expressions.JavaScript.Contracts; using Elsa.Testing.Shared; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; namespace Elsa.JavaScript.IntegrationTests; /// /// The common functions — getVariable, toJson, newGuid and the rest — are installed as /// lazily materialised globals, so a fresh engine builds an interop function wrapper only for the ones an /// expression actually reads. These tests pin that the laziness is invisible: same attributes, same existence /// and enumeration answers, and a value that is produced once and then stays put. /// public class CommonFunctionGlobalTests(ITestOutputHelper testOutputHelper) { private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Theory(DisplayName = "A common function is a callable global")] [InlineData("getVariable")] [InlineData("getInput")] [InlineData("getOutputFrom")] [InlineData("getLastResult")] [InlineData("setVariable")] [InlineData("toJson")] [InlineData("newGuid")] [InlineData("parseGuid")] [InlineData("isNullOrEmpty")] [InlineData("streamToBytes")] [InlineData("getShortGuid")] public async Task CommonFunctionsAreCallableGlobals(string functionName) { Assert.Equal("function", await EvaluateAsync($"return typeof {functionName};")); } [Fact(DisplayName = "A common function is present before anything reads its value")] public async Task CommonFunctionsExistWithoutBeingRead() { // The property is installed eagerly and only its value is deferred, so every existence question is // answered without materialising anything. `in` and getOwnPropertyNames are the questions a script — or // Elsa's own tooling — would ask about a function it has not called. Assert.Equal("true true", await EvaluateAsync( "return ('getVariable' in globalThis) + ' ' + Object.getOwnPropertyNames(globalThis).includes('toJson');")); } [Fact(DisplayName = "A common function is not enumerable on the global object")] public async Task CommonFunctionsAreNotEnumerable() { // engine.SetValue(string, Delegate) installs a non-enumerable property, so these have never shown up in // Object.keys(globalThis). The lazy registration passes the same flag, and this is the assertion that // would fail if it stopped doing so. Contrast LazyTypeGlobalTests, where the type globals are enumerable // because engine.SetValue(string, JsValue) makes them so. Assert.Equal("false", await EvaluateAsync("return '' + Object.keys(globalThis).includes('getVariable');")); } [Fact(DisplayName = "A common function keeps the attributes the eager registration produced")] public async Task CommonFunctionsKeepTheirPropertyAttributes() { Assert.Equal("true false true", await EvaluateAsync( """ var d = Object.getOwnPropertyDescriptor(globalThis, 'getVariable'); return d.writable + ' ' + d.enumerable + ' ' + d.configurable; """)); } [Fact(DisplayName = "A common function is materialised once and then stays the same value")] public async Task CommonFunctionsAreMaterialisedOnce() { // The factory runs at most once and the produced value is stored in the descriptor, so two reads see one // wrapper. A factory that re-ran per read would hand out a fresh function object each time and fail this. Assert.Equal("true", await EvaluateAsync("return '' + (getVariable === getVariable);")); } [Fact(DisplayName = "A common function can be overwritten by a script")] public async Task CommonFunctionsCanBeOverwritten() { // Writable and configurable, exactly as before: a script that replaces the binding wins, whether or not // the original value had been materialised first. Assert.Equal("replaced", await EvaluateAsync("toJson = function() { return 'replaced'; }; return toJson();")); } [Fact(DisplayName = "A common function that is never read still evaluates the expression")] public async Task UnreadCommonFunctionsDoNotAffectEvaluation() { Assert.Equal(3, await EvaluateAsync("return 1 + 2;")); } private async Task EvaluateAsync(string script) { var context = await _fixture.CreateExpressionExecutionContextAsync(); 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))!; } }