fix(js): HTMLFormElement.submit() must not fire a cancelable submit event; add requestSubmit() (#395)

* fix(js): HTMLFormElement.submit() must not fire a cancelable submit event; add requestSubmit()

Per the HTML standard, the submit() method submits without firing a submit
event and cannot be canceled by a page submit listener; only requestSubmit()
and user-initiated submits fire the cancelable event. bootstrap.js dispatched a
cancelable submit event from submit() and aborted navigation when canceled,
which breaks the common pattern where a submit listener preventDefault()s the
native submit and a callback later calls form.submit() to actually send it
(e.g. an invisible-reCAPTCHA data-callback): the listener also canceled the
programmatic submit, so no navigation was ever issued.

- submit(): navigate unconditionally, no submit event
- requestSubmit(): fire the cancelable submit event, navigate only if not canceled
- submit-button clicks route through requestSubmit() (a real click fires the event)

Adds a regression test; full obscura-cdp suite (56 tests) passes.

* fix(cdp): route synthetic submit-button clicks and Enter through requestSubmit

A CDP Input.dispatchMouseEvent on a submit button and an Enter key in a
form field are user-initiated submits, so per the HTML spec they must
fire the cancelable submit event. input.rs called form.submit() (the
method) directly, bypassing any preventDefault() submit listener, which
mirrors the bootstrap.js bug fixed in the parent commit. Use
requestSubmit() with a submit() fallback so the listener can veto.

---------

Co-authored-by: SGavrl <[email protected]>
This commit is contained in:
sickoandSGavrl authored and GitHub committed 2026-07-13 23:21:15 +00:00
1 parent 87fefef1ec
commit 7e2cabf411
3 files changed
+204 -5

No files matched your search

+3 -3
View File
@@ -93,10 +93,10 @@ pub async fn handle(
var type = (target.getAttribute && target.getAttribute('type') || '').toLowerCase();\
if (tag === 'BUTTON' && type !== 'button' && type !== 'reset') {{\
var form = target.closest ? target.closest('form') : null;\
if (form && typeof form.submit === 'function') {{ try {{ form.submit(target); }} catch(e) {{}} }}\
if (form) {{ try {{ if (typeof form.requestSubmit === 'function') {{ form.requestSubmit(target); }} else {{ form.submit(target); }} }} catch(e) {{}} }}\
}} else if (tag === 'INPUT' && (type === 'submit' || type === 'image')) {{\
var form2 = target.closest ? target.closest('form') : null;\
if (form2 && typeof form2.submit === 'function') {{ try {{ form2.submit(target); }} catch(e) {{}} }}\
if (form2) {{ try {{ if (typeof form2.requestSubmit === 'function') {{ form2.requestSubmit(target); }} else {{ form2.submit(target); }} }} catch(e) {{}} }}\
}} else if (tag === 'INPUT' && (type === 'checkbox' || type === 'radio')) {{\
target.checked = !target.checked;\
try {{ target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('change', {{bubbles:true}}))); }} catch(e) {{}}\
@@ -177,7 +177,7 @@ pub async fn handle(
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {bubbles:true})));\
} else {\
var form = target.form || (target.closest && target.closest('form'));\
if (form && typeof form.submit === 'function') form.submit();\
if (form) {{ try {{ if (typeof form.requestSubmit === 'function') {{ form.requestSubmit(); }} else {{ form.submit(); }} }} catch(e) {{}} }}\
}\
})()";
page.evaluate(js);