Skip to content
Merged
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
90 changes: 71 additions & 19 deletions src/chrome/src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -3814,6 +3814,17 @@
return typeof el.innerText === 'string' ? el.innerText : (el.textContent || '');
}

// Pick the single commit path for set_field({submit:true}). Synthetic
// (isTrusted:false) Enter events never trigger native submission —
// they only reach the page's own keydown listeners. A non-combobox field
// inside a form that has requestSubmit therefore needs a native submit as
// its only reliable commit path; comboboxes are committed by page JS
// listeners instead, because submitting the enclosing form while a picker
// popup is open is usually wrong.
function _setFieldUsesNativeSubmit(isCombobox, isContentEditable, form) {
return !isCombobox && !isContentEditable && !!form && typeof form.requestSubmit === 'function';
}

// The rich-text toolbar heuristic lives in one file shared by both builds
// and by the CDP main-world probe — see
// src/content/rich-text-toolbar-heuristic.js. Delegating keeps the scoring
Expand Down Expand Up @@ -5711,7 +5722,10 @@
const actual = el.isContentEditable ? _editableTextValue(el) : (el.value || '');
const verified = _setFieldValueMatches(actual, prevValue, text, clear, el.isContentEditable);
const fallbackAttempted = false;
let nativeSubmitAttempted = false;
let submissionOutcomeUnknown = false;
if (submit && verified) {
submissionOutcomeUnknown = true;
try {
// Detect combobox/searchbox pattern: if the element is a searchbox,
// has role=combobox, has aria-controls pointing to a listbox, or a
Expand All @@ -5738,16 +5752,12 @@
} catch {}
}
const dispatchKey = (type, key, keyCode) => {
el.dispatchEvent(new KeyboardEvent(type, { key, code: key, keyCode, bubbles: true, cancelable: true }));
return el.dispatchEvent(new KeyboardEvent(type, { key, code: key, keyCode, bubbles: true, cancelable: true }));
};
if (isCombobox) {
// Give the listbox a tick to filter, then highlight the first
// option with ArrowDown, then commit with Enter.
await new Promise(r => setTimeout(r, 80));
dispatchKey('keydown', 'ArrowDown', 40);
dispatchKey('keyup', 'ArrowDown', 40);
await new Promise(r => setTimeout(r, 30));
}
const form = el.form || (el.closest && el.closest('form'));
const usesNativeSubmit = _setFieldUsesNativeSubmit(isCombobox, el.isContentEditable, form);
let submissionObserved = false;
let submissionCancelled = false;
if (msg.params?.messageRecipientGuardRequired === true) {
const recipientValidation = _consumeMessageRecipientDispatchBinding(msg.params, el);
if (recipientValidation.success !== true) {
Expand All @@ -5760,17 +5770,57 @@
});
}
}
dispatchKey('keydown', 'Enter', 13);
dispatchKey('keypress', 'Enter', 13);
dispatchKey('keyup', 'Enter', 13);
// Form submission: only fall back to requestSubmit for non-combobox
// inputs. Submitting a form while a combobox popup is open is
// usually wrong and can prematurely post the enclosing form.
if (!isCombobox) {
const form = el.form || (el.closest && el.closest('form'));
if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
let removeSubmitObserver = () => {};
let submitEvent = null;
if (form && typeof form.addEventListener === 'function') {
const onSubmit = event => {
submissionObserved = true;
submitEvent = event;
};
form.addEventListener('submit', onSubmit, true);
removeSubmitObserver = () => form.removeEventListener?.('submit', onSubmit, true);
}
} catch {}
try {
if (usesNativeSubmit) {
// Ordinary form controls use one native path. Dispatching a
// synthetic Enter first could make page code act and then
// make this fallback repeat the consequential action.
// requestSubmit performs interactive constraint validation and
// silently aborts on an invalid form; surface that instead of
// reporting a successful submission.
if (form.noValidate !== true && typeof form.checkValidity === 'function' && !form.checkValidity()) {
return failure(
'The form did not submit: a required field is empty or a value is invalid. Fix the field and retry with a fresh ref_id.',
{ verified: true, submitted: false, invalid: true, ref_id, rect },
);
}
try {
form.requestSubmit();
} catch {}
} else {
// Comboboxes, contenteditables, and form-less widgets are
// committed by page-owned keyboard handlers. Never follow
// this path with requestSubmit: cancellation is not proof of
// submission, and an unobserved handler may already have acted.
if (isCombobox) {
await new Promise(r => setTimeout(r, 80));
dispatchKey('keydown', 'ArrowDown', 40);
dispatchKey('keyup', 'ArrowDown', 40);
await new Promise(r => setTimeout(r, 30));
}
dispatchKey('keydown', 'Enter', 13);
dispatchKey('keypress', 'Enter', 13);
dispatchKey('keyup', 'Enter', 13);
}
submissionCancelled = submitEvent?.defaultPrevented === true;
nativeSubmitAttempted = submissionObserved && !submissionCancelled;
submissionOutcomeUnknown = !nativeSubmitAttempted;
} finally {
removeSubmitObserver();
}
} catch {
submissionOutcomeUnknown = true;
}
}
if (!verified) {
return failure(
Expand Down Expand Up @@ -5798,6 +5848,8 @@
verified: true,
fieldMeta,
fallbackAttempted,
submitted: nativeSubmitAttempted || undefined,
outcomeUnknown: submissionOutcomeUnknown || undefined,
};
} catch (e) {
return failure(e && e.message || String(e));
Expand Down
84 changes: 70 additions & 14 deletions src/firefox/src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -3080,6 +3080,16 @@
return typeof el.innerText === 'string' ? el.innerText : (el.textContent || '');
}

// Synthetic (isTrusted:false) Enter events never trigger native submission —
// they only reach the page's own keydown listeners. A non-combobox field
// inside a form that has requestSubmit therefore needs a native submit as
// its only reliable commit path; comboboxes are committed by page JS
// listeners instead, because submitting the enclosing form while a picker
// popup is open is usually wrong.
function _setFieldUsesNativeSubmit(isCombobox, isContentEditable, form) {
return !isCombobox && !isContentEditable && !!form && typeof form.requestSubmit === 'function';
}

// Above this length the per-candidate rescan below stops being worth its
// cost. The edit still succeeds; it is reported unproven, which the callers
// treat as "no positive proof", not as a failure.
Expand Down Expand Up @@ -4840,6 +4850,8 @@
let actual = el.isContentEditable ? _editableTextValue(el) : (el.value || '');
let verified = _setFieldValueMatches(actual, prevValue, text, clear, el.isContentEditable);
let fallbackAttempted = false;
let nativeSubmitAttempted = false;
let submissionOutcomeUnknown = false;
if (!verified) {
fallbackAttempted = true;
await _retryFieldWithExecCommand(el, (clear ? '' : prevValue) + text);
Expand All @@ -4848,6 +4860,7 @@
}

if (submit && verified) {
submissionOutcomeUnknown = true;
try {
const roleAttr = (el.getAttribute && el.getAttribute('role') || '').toLowerCase();
const controls = el.getAttribute && el.getAttribute('aria-controls');
Expand All @@ -4867,14 +4880,12 @@
} catch {}
}
const dispatchKey = (type, key, keyCode) => {
el.dispatchEvent(new KeyboardEvent(type, { key, code: key, keyCode, bubbles: true, cancelable: true }));
return el.dispatchEvent(new KeyboardEvent(type, { key, code: key, keyCode, bubbles: true, cancelable: true }));
};
if (isCombobox) {
await new Promise(r => setTimeout(r, 80));
dispatchKey('keydown', 'ArrowDown', 40);
dispatchKey('keyup', 'ArrowDown', 40);
await new Promise(r => setTimeout(r, 30));
}
const form = el.form || (el.closest && el.closest('form'));
const usesNativeSubmit = _setFieldUsesNativeSubmit(isCombobox, el.isContentEditable, form);
let submissionObserved = false;
let submissionCancelled = false;
if (msg.params?.messageRecipientGuardRequired === true) {
const recipientValidation = _consumeMessageRecipientDispatchBinding(msg.params, el);
if (recipientValidation.success !== true) {
Expand All @@ -4887,14 +4898,57 @@
});
}
}
dispatchKey('keydown', 'Enter', 13);
dispatchKey('keypress', 'Enter', 13);
dispatchKey('keyup', 'Enter', 13);
if (!isCombobox) {
const form = el.form || (el.closest && el.closest('form'));
if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
let removeSubmitObserver = () => {};
let submitEvent = null;
if (form && typeof form.addEventListener === 'function') {
const onSubmit = event => {
submissionObserved = true;
submitEvent = event;
};
form.addEventListener('submit', onSubmit, true);
removeSubmitObserver = () => form.removeEventListener?.('submit', onSubmit, true);
}
} catch {}
try {
if (usesNativeSubmit) {
// Ordinary form controls use one native path. Dispatching a
// synthetic Enter first could make page code act and then
// make this fallback repeat the consequential action.
// requestSubmit performs interactive constraint validation and
// silently aborts on an invalid form; surface that instead of
// reporting a successful submission.
if (form.noValidate !== true && typeof form.checkValidity === 'function' && !form.checkValidity()) {
return failure(
'The form did not submit: a required field is empty or a value is invalid. Fix the field and retry with a fresh ref_id.',
{ verified: true, submitted: false, invalid: true, ref_id, rect },
);
}
try {
form.requestSubmit();
} catch {}
} else {
// Comboboxes, contenteditables, and form-less widgets are
// committed by page-owned keyboard handlers. Never follow
// this path with requestSubmit: cancellation is not proof of
// submission, and an unobserved handler may already have acted.
if (isCombobox) {
await new Promise(r => setTimeout(r, 80));
dispatchKey('keydown', 'ArrowDown', 40);
dispatchKey('keyup', 'ArrowDown', 40);
await new Promise(r => setTimeout(r, 30));
}
dispatchKey('keydown', 'Enter', 13);
dispatchKey('keypress', 'Enter', 13);
dispatchKey('keyup', 'Enter', 13);
}
submissionCancelled = submitEvent?.defaultPrevented === true;
nativeSubmitAttempted = submissionObserved && !submissionCancelled;
submissionOutcomeUnknown = !nativeSubmitAttempted;
} finally {
removeSubmitObserver();
}
} catch {
submissionOutcomeUnknown = true;
}
}
if (!verified) {
return failure(
Expand All @@ -4921,6 +4975,8 @@
verified: true,
fieldMeta,
fallbackAttempted,
submitted: nativeSubmitAttempted || undefined,
outcomeUnknown: submissionOutcomeUnknown || undefined,
};
} catch (e) {
return failure(e && e.message || String(e));
Expand Down
Loading
Loading