From fdd990a4a77441a43a9c45556c343152ada4d00e Mon Sep 17 00:00:00 2001 From: Peo Orvendal Date: Mon, 2 Mar 2026 13:32:40 -0800 Subject: [PATCH] Fix: resolve W3C element references in execute/sync and execute/async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When WebdriverIO calls browser.execute(script, element), it serializes the element as a W3C element reference object. Per the W3C WebDriver spec, the server must deserialize this back to a real DOM node before injecting it into the script. The perform_actions handler already does this, but execute_sync and execute_async did not, causing every element-based script execution to fail with "Argument 1 ('other') to Node.contains must be an instance of Node". Changes: - CLI (tauri-wd): Add resolve_script_args() that recursively walks the args array and replaces W3C element references with __wd_resolve markers containing selector/index/using info. - Plugin: Add RESOLVE_ARGS_JS constant — a JavaScript preamble injected into script_execute and script_execute_async that resolves __wd_resolve markers back to real DOM nodes via querySelectorAll or XPath evaluate. - Tests: Add regression tests for element refs in sync/async scripts and isDisplayed(). Fixes #2 Co-Authored-By: Claude Opus 4.6 --- .../src/server.rs | 30 ++++++++++++++- crates/tauri-webdriver-automation/src/main.rs | 38 ++++++++++++++++++- tests/wdio/specs/script.spec.mjs | 23 +++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/tauri-plugin-webdriver-automation/src/server.rs b/crates/tauri-plugin-webdriver-automation/src/server.rs index dd8b292..7474e79 100644 --- a/crates/tauri-plugin-webdriver-automation/src/server.rs +++ b/crates/tauri-plugin-webdriver-automation/src/server.rs @@ -752,13 +752,38 @@ async fn element_selected( // --- Script handlers --- +/// JavaScript snippet that resolves `__wd_resolve` marker objects in `__args` +/// back to real DOM nodes. The CLI replaces W3C element references with +/// `{"__wd_resolve": {"selector": "...", "index": N, "using": "..."}}` markers; +/// this resolver walks the args array and replaces each marker with the actual +/// DOM element found via `querySelectorAll` or XPath `evaluate`. +const RESOLVE_ARGS_JS: &str = "\ + function __wdResolve(v){\ + if(Array.isArray(v)){for(var i=0;i( AxumState(state): AxumState>, Json(body): Json, ) -> ApiResult { let args_json = serde_json::to_string(&body.args).unwrap(); let script = format!( - "var __args={args_json};return (function(){{{}}}).apply(null,__args)", + "{RESOLVE_ARGS_JS}\ + var __args=__wdResolve({args_json});\ + return (function(){{{}}}).apply(null,__args)", body.script ); let result = eval_js(&state, &script).await?; @@ -790,7 +815,8 @@ async fn script_execute_async( let args_json = serde_json::to_string(&body.args).unwrap(); let script = format!( - "(function(){{var __args={args_json};\ + "(function(){{{RESOLVE_ARGS_JS}\ + var __args=__wdResolve({args_json});\ var __done=function(r){{window.__WEBDRIVER__.resolve(\"{id}\",r)}};\ __args.push(__done);\ try{{(function(){{{user_script}}}).apply(null,__args)}}\ diff --git a/crates/tauri-webdriver-automation/src/main.rs b/crates/tauri-webdriver-automation/src/main.rs index 3bd0dad..7b44061 100644 --- a/crates/tauri-webdriver-automation/src/main.rs +++ b/crates/tauri-webdriver-automation/src/main.rs @@ -963,6 +963,38 @@ async fn is_element_displayed( // --- Script handlers --- +/// Recursively walk a JSON value and replace W3C element references +/// (`{"element-6066-...": ""}`) with `{"__wd_resolve": {"selector", "index", "using"}}` +/// markers so the plugin can resolve them to real DOM nodes. +fn resolve_script_args(value: &mut Value, session: &Session) { + match value { + Value::Array(arr) => { + for item in arr.iter_mut() { + resolve_script_args(item, session); + } + } + Value::Object(map) => { + if let Some(eid) = map.get(W3C_ELEMENT_KEY).and_then(|v| v.as_str()) { + if let Some(elem_ref) = session.elements.get(eid) { + let marker = json!({ + "__wd_resolve": { + "selector": elem_ref.selector, + "index": elem_ref.index, + "using": elem_ref.using, + } + }); + *value = marker; + return; + } + } + for val in map.values_mut() { + resolve_script_args(val, session); + } + } + _ => {} + } +} + async fn execute_sync( AxumState(state): AxumState, Path(sid): Path, @@ -971,7 +1003,8 @@ async fn execute_sync( let guard = state.sessions.lock().await; let session = get_session(&guard, &sid)?; let script = body.get("script").and_then(|v| v.as_str()).unwrap_or(""); - let args = body.get("args").cloned().unwrap_or(json!([])); + let mut args = body.get("args").cloned().unwrap_or(json!([])); + resolve_script_args(&mut args, session); let result = plugin_post( session, "/script/execute", @@ -992,7 +1025,8 @@ async fn execute_async( let guard = state.sessions.lock().await; let session = get_session(&guard, &sid)?; let script = body.get("script").and_then(|v| v.as_str()).unwrap_or(""); - let args = body.get("args").cloned().unwrap_or(json!([])); + let mut args = body.get("args").cloned().unwrap_or(json!([])); + resolve_script_args(&mut args, session); let result = plugin_post( session, "/script/execute-async", diff --git a/tests/wdio/specs/script.spec.mjs b/tests/wdio/specs/script.spec.mjs index 0462ecc..2e97259 100644 --- a/tests/wdio/specs/script.spec.mjs +++ b/tests/wdio/specs/script.spec.mjs @@ -22,4 +22,27 @@ describe('Script Execution', () => { }); expect(result).toBe(42); }); + + it('should resolve W3C element references passed as script args', async () => { + // This is the core regression test: WebdriverIO passes element objects + // to browser.execute() which must be resolved to real DOM nodes. + const heading = await $('h1'); + const text = await browser.execute((el) => el.textContent, heading); + expect(text).toBe('WebDriver Test App'); + }); + + it('should resolve element refs in async scripts', async () => { + const heading = await $('h1'); + const text = await browser.executeAsync((el, done) => { + done(el.textContent); + }, heading); + expect(text).toBe('WebDriver Test App'); + }); + + it('should support isDisplayed on elements', async () => { + // isDisplayed() internally calls browser.execute(isElementDisplayed, this) + // which was the original failing case. + const heading = await $('h1'); + expect(await heading.isDisplayed()).toBe(true); + }); });