Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions crates/tauri-plugin-webdriver-automation/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,13 +752,38 @@ async fn element_selected<R: Runtime>(

// --- 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<v.length;i++){v[i]=__wdResolve(v[i])}return v}\
if(v&&typeof v==='object'&&v.__wd_resolve){\
var r=v.__wd_resolve;\
if(r.using==='xpath'){\
var xr=document.evaluate(r.selector,document,null,\
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);\
return xr.snapshotItem(r.index)\
}\
return document.querySelectorAll(r.selector)[r.index]\
}\
if(v&&typeof v==='object'&&!Array.isArray(v)){\
for(var k in v){if(v.hasOwnProperty(k)){v[k]=__wdResolve(v[k])}}\
}\
return v\
}";

async fn script_execute<R: Runtime>(
AxumState(state): AxumState<SharedState<R>>,
Json(body): Json<ScriptReq>,
) -> 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?;
Expand Down Expand Up @@ -790,7 +815,8 @@ async fn script_execute_async<R: Runtime>(

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)}}\
Expand Down
38 changes: 36 additions & 2 deletions crates/tauri-webdriver-automation/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,38 @@ async fn is_element_displayed(

// --- Script handlers ---

/// Recursively walk a JSON value and replace W3C element references
/// (`{"element-6066-...": "<uuid>"}`) 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<SharedState>,
Path(sid): Path<String>,
Expand All @@ -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",
Expand All @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions tests/wdio/specs/script.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});