diff --git a/CHANGELOG.md b/CHANGELOG.md index ddfda590..05497ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **WDA `launchApp` restarts a running app unless `stopApp: false`, as Maestro does.** It only activated the running app, so a relaunch left the app on the screen it was already on, and a flow checking what survives a restart restarted nothing. - **WDA `notVisible` passes only when a lookup finds the element absent.** `assertNotVisible` and `extendedWaitUntil: notVisible` treated any failed lookup, such as an unreadable page source or a dropped connection, as the element being gone, so they could pass without the screen being looked at. Other errors are now retried until the timeout, and then fail the step. - **`checked` selectors work on iOS.** The iOS drivers dropped `checked` with a warning, so `checked: true` matched a switch in either state. WDA now derives checked from a CheckBox, Switch or Toggle whose value is 1, as Maestro does, and filters on it on every path (tap, assert, relative). +- **`runScript` runs a script file as written, as Maestro does.** The runner expanded `${...}` across the whole file before running it, so a template literal that used the script's own variables, such as `${encodeURIComponent(email)}`, was replaced ahead of the script, against variables that did not exist yet, and came out as `undefined`. A script file now runs as plain JavaScript. Inline script text keeps its `${...}` expansion. ## [1.1.28] - 2026-09-30 diff --git a/pkg/driver/uiautomator2/driver.go b/pkg/driver/uiautomator2/driver.go index baad23df..d34f3f76 100644 --- a/pkg/driver/uiautomator2/driver.go +++ b/pkg/driver/uiautomator2/driver.go @@ -1280,12 +1280,6 @@ func looksLikeRegex(text string) bool { return false } -// escapeUIAutomatorString escapes only the double quotes for UiAutomator string. -// Used when the text is already a regex pattern. -func escapeUIAutomatorString(s string) string { - return strings.ReplaceAll(s, `"`, `\"`) -} - // buildStateFilters returns UiSelector chain for state filters. // e.g., ".enabled(true).checked(false)" func buildStateFilters(sel flow.Selector) string { diff --git a/pkg/executor/scripting.go b/pkg/executor/scripting.go index d453cb89..0804fa7d 100644 --- a/pkg/executor/scripting.go +++ b/pkg/executor/scripting.go @@ -251,8 +251,18 @@ func expandDollarVar(text, name, value string) string { // outlive a single runScript call still goes through the global `output` // bag, exactly as documented. func (se *ScriptEngine) RunScript(script string, env map[string]string) error { - // Expand variables in script - script = se.ExpandVariables(script) + return se.runScript(script, env, true) +} + +// runScript runs a script with its env. expandBody expands ${...} and $VAR in the script text +// first, which suits inline script text. A script file is plain JavaScript and runs as written, +// as in Maestro: expanding it first replaced the file's own template literals (`${localVar}`) +// ahead of the script, against variables that did not exist yet. +func (se *ScriptEngine) runScript(script string, env map[string]string, expandBody bool) error { + if expandBody { + // Expand variables in script + script = se.ExpandVariables(script) + } // Apply env variables for the duration of THIS script only, expanded so // values like "mockoon-cli start --port ${output.port}" resolve before the @@ -422,7 +432,8 @@ func (se *ScriptEngine) ExecuteRunScript(step *flow.RunScriptStep) *core.Command script := step.ScriptPath() // Check if it's a file path (ends with .js) - if strings.HasSuffix(script, ".js") { + isFile := strings.HasSuffix(script, ".js") + if isFile { filePath := se.ResolvePath(script) content, err := os.ReadFile(filePath) if err != nil { @@ -435,7 +446,7 @@ func (se *ScriptEngine) ExecuteRunScript(step *flow.RunScriptStep) *core.Command script = string(content) } - if err := se.RunScript(script, step.Env); err != nil { + if err := se.runScript(script, step.Env, !isFile); err != nil { return &core.CommandResult{ Success: false, Error: err, diff --git a/pkg/executor/scripting_test.go b/pkg/executor/scripting_test.go index 5e4eaf71..82d5a96f 100644 --- a/pkg/executor/scripting_test.go +++ b/pkg/executor/scripting_test.go @@ -792,6 +792,29 @@ func TestScriptEngine_ExecuteRunScript_File(t *testing.T) { } } +// A script file's own template literals are plain JavaScript: they must see the script's local +// variables, not be expanded ahead of the script against the flow's variables. +func TestScriptEngine_ExecuteRunScript_FileTemplateLiteral(t *testing.T) { + se := NewScriptEngine() + defer se.Close() + + tmpDir := t.TempDir() + src := "const who = EMAIL;\nconst state = 'onboarded';\n" + + "output.url = `/v1/x?email=${encodeURIComponent(who)}&state=${state}`;\n" + if err := os.WriteFile(filepath.Join(tmpDir, "tl.js"), []byte(src), 0o644); err != nil { + t.Fatalf("Failed to create test script: %v", err) + } + se.SetFlowDir(tmpDir) + + step := &flow.RunScriptStep{Script: "tl.js", Env: map[string]string{"EMAIL": "a+b@x.io"}} + if result := se.ExecuteRunScript(step); !result.Success { + t.Fatalf("ExecuteRunScript() success = false, error = %v", result.Error) + } + if got, want := se.GetVariable("url"), "/v1/x?email=a%2Bb%40x.io&state=onboarded"; got != want { + t.Errorf("url = %q, want %q", got, want) + } +} + func TestScriptEngine_ExecuteRunScript_FileNotFound(t *testing.T) { se := NewScriptEngine() defer se.Close()