From 64878d4311fe3ad8a9961c3bec04595cc1fc67dc Mon Sep 17 00:00:00 2001 From: salch-cred Date: Mon, 31 Aug 2026 00:41:45 +0530 Subject: [PATCH] fix(preview): use proper input events for React/contenteditable/sumbit in drive_preview type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/lib/preview-act/act-in-page.test.ts | 48 ++++++++++++++++++- .../src/lib/preview-act/act-in-page.ts | 27 +++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/preview-act/act-in-page.test.ts b/apps/desktop/src/lib/preview-act/act-in-page.test.ts index f92b1efcc3..f15ee0ff6e 100644 --- a/apps/desktop/src/lib/preview-act/act-in-page.test.ts +++ b/apps/desktop/src/lib/preview-act/act-in-page.test.ts @@ -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('
') + 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) + }) + }) diff --git a/apps/desktop/src/lib/preview-act/act-in-page.ts b/apps/desktop/src/lib/preview-act/act-in-page.ts index 1d80c52bc2..9c6bd56c95 100644 --- a/apps/desktop/src/lib/preview-act/act-in-page.ts +++ b/apps/desktop/src/lib/preview-act/act-in-page.ts @@ -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))