fix(preview): use proper input events for React/contenteditable/sumbit in drive_preview type

drive_preview type/press/submit silently failed on React controlled inputs,
contenteditable rich-text editors, and Enter-to-submit forms — the tool
reported success but the text never landed and no DOM events fired (#98048).

Three fixes in the injected type handler:

1. contenteditable: use document.execCommand('insertText', false, text)
   instead of el.textContent = text.  execCommand fires beforeinput/
   insertText composition events that Lexical/ProseMirror/Draft editors
   (DeepSeek, ChatZhipu, Kimi) subscribe to.  Fallback to textContent
   + InputEvent for sandboxed pages where execCommand fails.

2. INPUT/TEXTAREA: replace new Event('input') with new InputEvent('input',
   {data: text, inputType: 'insertText', bubbles: true}).  React 16+
   controlled-input trackers compare lastTrackedValue against the event's
   .data property — a plain Event has no .data, so the value-change is
   deduplicated.

3. submit: add keyCode: 13 and which: 13 to the KeyboardEvent init dict.
   Modern React forms check both event.key AND event.keyCode for Enter;
   constructed KeyboardEvents read keyCode as 0 (read-only), so the
   event was silently swallowed.

Regression tests added in act-in-page.test.ts:
- contenteditable execCommand path
- InputEvent data property set correctly
- submit KeyboardEvent keyCode/which === 13

Fixes #98048
This commit is contained in:
salch-cred
2026-08-31 00:41:45 +05:30
committed by brooklyn!
parent f91451ba93
commit 64878d4311
2 changed files with 71 additions and 4 deletions

View File

@@ -727,4 +727,50 @@ describe('scroll', () => {
expect(pageScroll).not.toHaveBeenCalled()
expect(result.acted).toContain('Results')
})
})
// Regression: drive_preview type must work on React controlled inputs,
// contenteditable rich-text editors, and submit-with-Enter forms (#98048).
describe('type (regression #98048)', () => {
it('contenteditable uses execCommand insertText', () => {
const execSpy = vi.spyOn(document, 'execCommand').mockImplementation(() => true)
const holder = page('<div id="ced" contenteditable="true"></div>')
const ref = firstRef(holder)
const result = actInPage(document, holder, { kind: 'type', ref, text: 'hello' })
expect(execSpy).toHaveBeenCalledWith('insertText', false, 'hello')
expect(result.success).toBe(true)
})
it('input elements dispatch InputEvent with data property', () => {
const $el = document.createElement('input')
$el.setAttribute('type', 'text')
document.body.appendChild($el)
const holder: PreviewActHolder = {
handles: [{ ref: 'inp-1', label: 'Name', node: $el, selector: null }],
lastInventoryAt: null,
}
const dispatched: Event[] = []
$el.dispatchEvent = vi.fn((ev: Event) => { dispatched.push(ev) })
const result = actInPage(document, holder, { kind: 'type', ref: 'inp-1', text: 'test' })
const inputEv = dispatched.find(e => e.type === 'input') as InputEvent | undefined
expect(inputEv).toBeDefined()
expect(inputEv?.data).toBe('test')
expect(inputEv?.inputType).toBe('insertText')
expect(result.success).toBe(true)
})
it('submit KeyboardEvent includes keyCode 13', () => {
const $el = document.createElement('input')
$el.setAttribute('type', 'text')
document.body.appendChild($el)
const holder: PreviewActHolder = {
handles: [{ ref: 'inp-1', label: 'Name', node: $el, selector: null }],
lastInventoryAt: null,
}
const dispatched: KeyboardEvent[] = []
$el.dispatchEvent = vi.fn((ev: Event) => { if (ev instanceof KeyboardEvent) dispatched.push(ev) })
actInPage(document, holder, { kind: 'type', ref: 'inp-1', text: 'test', submit: true })
expect(dispatched.length).toBeGreaterThanOrEqual(2)
expect(dispatched[0].keyCode).toBe(13)
expect(dispatched[0].which).toBe(13)
})
})

View File

@@ -557,7 +557,23 @@ export function actInPageCore(
el.focus()
if (editable) {
el.textContent = text
// contenteditable rich-text editors (Lexical, ProseMirror, Draft —
// used by DeepSeek, ChatZhipu, Kimi) ignore plain DOM writes because
// their internal model never sees a `beforeinput`/`insertText`
// composition. Use document.execCommand('insertText') which fires the
// real editing pipeline the frameworks subscribe to (#98048).
// ExecCommand is still supported in Chromium webviews (Electron ≥ 33).
try {
document.execCommand('insertText', false, text)
} catch {
// Fallback: direct textContent write + explicit InputEvent for
// environments where execCommand fails (about:blank, some sandboxed
// pages).
el.textContent = text
el.dispatchEvent(
new InputEvent('input', { data: text, inputType: 'insertText', bubbles: true })
)
}
} else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
// Assign through the prototype's setter: React (and anything else that
// tracks the DOM value) shadows `value` with its own accessor and ignores
@@ -577,11 +593,16 @@ export function actInPageCore(
return fail(describe(el) + ' is not a text field.')
}
el.dispatchEvent(new Event('input', { bubbles: true }))
el.dispatchEvent(new InputEvent('input', { data: text, inputType: 'insertText', bubbles: true }))
el.dispatchEvent(new Event('change', { bubbles: true }))
if (action.submit) {
const enter = { bubbles: true, cancelable: true, code: 'Enter', key: 'Enter' }
// Synthetic KeyboardEvent — keyCode and which are read-only on
// constructed events (always 0) unless set via defineProperty.
// Many React input components (DeepSeek, ChatZhipu, Kimi) still
// check keyCode === 13 for Enter; 0 means the event never reaches
// the submit handler (#98048).
const enter = { bubbles: true, cancelable: true, code: 'Enter', key: 'Enter', keyCode: 13, which: 13 } as KeyboardEventInit
el.dispatchEvent(new KeyboardEvent('keydown', enter))
el.dispatchEvent(new KeyboardEvent('keyup', enter))