From 6fac7f719acb994334c4a535a6a59533b614d214 Mon Sep 17 00:00:00 2001 From: lisa0314 Date: Wed, 29 Jul 2026 20:19:21 +0800 Subject: [PATCH] fix(mcp): fill React controlled inputs --- crates/obscura-mcp/src/lib.rs | 94 ++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/crates/obscura-mcp/src/lib.rs b/crates/obscura-mcp/src/lib.rs index 9543068..4937da4 100644 --- a/crates/obscura-mcp/src/lib.rs +++ b/crates/obscura-mcp/src/lib.rs @@ -770,9 +770,9 @@ fn tool_fill(args: &Value, state: &mut BrowserState) -> Result { r#"(function(){{ var el = document.querySelector({sel}); if (!el) return "error:element not found"; - el.value = {val}; - el.dispatchEvent(new Event("input", {{bubbles:true}})); - el.dispatchEvent(new Event("change", {{bubbles:true}})); + globalThis.__obscura_setFieldValue(el, "value", {val}); + el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("input", {{bubbles:true}}))); + el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("change", {{bubbles:true}}))); return "ok"; }})()"#, sel = serde_json::to_string(&selector).unwrap(), @@ -796,8 +796,8 @@ fn tool_type(args: &Value, state: &mut BrowserState) -> Result { r#"(function(){{ var el = document.querySelector({sel}); if (!el) return "error:element not found"; - el.value = (el.value || "") + {txt}; - el.dispatchEvent(new Event("input", {{bubbles:true}})); + globalThis.__obscura_setFieldValue(el, "value", (el.value || "") + {txt}); + el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("input", {{bubbles:true}}))); return "ok"; }})()"#, sel = serde_json::to_string(&selector).unwrap(), @@ -1329,9 +1329,9 @@ fn tool_fill_form(args: &Value, state: &mut BrowserState) -> Result format!(r#"(function(){{ var el = document.querySelector({sel}); if (!el) return "error:not found"; - el.value = {val}; - el.dispatchEvent(new Event('input', {{bubbles:true}})); - el.dispatchEvent(new Event('change', {{bubbles:true}})); + globalThis.__obscura_setFieldValue(el, 'value', {val}); + el.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {{bubbles:true}}))); + el.dispatchEvent(globalThis.__obscura_markTrusted(new Event('change', {{bubbles:true}}))); return "ok"; }})()"#, sel = serde_json::to_string(&selector).unwrap(), val = serde_json::to_string(value).unwrap()), }; @@ -1769,3 +1769,81 @@ fn tool_set_storage_state(args: &Value, state: &mut BrowserState) -> Result") + .await + .expect("test page should navigate"); + + state.page_mut().evaluate( + r#"(function () { + var root = document.getElementById('root'); + var input = document.getElementById('field'); + var descriptor = Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value'); + var tracked = String(input.value); + Object.defineProperty(input, 'value', { + configurable: true, + get: function () { return descriptor.get.call(this); }, + set: function (value) { + tracked = String(value); + descriptor.set.call(this, value); + } + }); + window.__controlledState = ''; + window.__controlledUpdates = 0; + window.__lastInputTarget = ''; + window.__lastInputTrusted = false; + root.addEventListener('input', function (event) { + window.__lastInputTarget = event.target.id; + window.__lastInputTrusted = event.isTrusted; + var next = String(event.target.value); + if (next !== tracked) { + tracked = next; + window.__controlledState = next; + window.__controlledUpdates++; + } + }); + })()"#, + ); + + tool_fill( + &json!({ "selector": "#field", "value": "filled" }), + &mut state, + ) + .expect("browser_fill should succeed"); + tool_type( + &json!({ "selector": "#field", "text": "-typed" }), + &mut state, + ) + .expect("browser_type should succeed"); + tool_fill_form( + &json!({ + "fields": [{ "selector": "#field", "value": "form-filled" }] + }), + &mut state, + ) + .expect("browser_fill_form should succeed"); + + let actual = state.page_mut().evaluate( + r#"JSON.stringify({ + domValue: document.getElementById('field').value, + controlledState: window.__controlledState, + controlledUpdates: window.__controlledUpdates, + lastInputTarget: window.__lastInputTarget, + lastInputTrusted: window.__lastInputTrusted + })"#, + ); + assert_eq!( + actual, + json!(r#"{"domValue":"form-filled","controlledState":"form-filled","controlledUpdates":3,"lastInputTarget":"field","lastInputTrusted":true}"#), + ); + } +}