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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user