diff --git a/packages/injected/src/webview/webViewInput.ts b/packages/injected/src/webview/webViewInput.ts new file mode 100644 index 0000000000000..4b293c0059997 --- /dev/null +++ b/packages/injected/src/webview/webViewInput.ts @@ -0,0 +1,329 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Modifiers = { + ctrlKey: boolean; + shiftKey: boolean; + altKey: boolean; + metaKey: boolean; +}; + +export type KeyEventParams = Modifiers & { + code: string; + key: string; + keyCode: number; + location: number; + repeat: boolean; + // Present for printable keys; absent for non-text keys (arrows, modifiers, etc.). + text?: string; +}; + +export type MouseEventParams = Modifiers & { + type: 'mousedown' | 'mouseup' | 'click' | 'auxclick' | 'dblclick' | 'contextmenu'; + x: number; + y: number; + button: number; + buttons: number; + clickCount: number; +}; + +export type MouseMoveParams = Modifiers & { + x: number; + y: number; + button: number; + buttons: number; +}; + +export type WheelParams = Modifiers & { + x: number; + y: number; + deltaX: number; + deltaY: number; +}; + +export type TapParams = Modifiers & { + x: number; + y: number; +}; + +const kTrustedSynthetic = '__pwTrustedSynthetic'; + +function markAndDispatch(node: EventTarget, event: Event): boolean { + Object.defineProperty(event, kTrustedSynthetic, { value: true }); + return node.dispatchEvent(event); +} + +// Legacy WebKit-only KeyboardEvent.keyIdentifier (a DOM Level 3 draft property +// dropped by every other engine). It cannot be supplied via the constructor, so +// compute it from the virtual key code and define it on the event before +// dispatch. Mirrors WebCore's keyIdentifierForWindowsKeyCode. +const kNamedKeyIdentifiers: Record = { + 8: 'U+0008', // Backspace + 9: 'U+0009', // Tab + 13: 'Enter', + 16: 'Shift', + 17: 'Control', + 18: 'Alt', + 27: 'U+001B', // Escape + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'Left', + 38: 'Up', + 39: 'Right', + 40: 'Down', + 45: 'Insert', + 46: 'U+007F', // Delete +}; + +function keyIdentifierFor(keyCode: number, key: string): string { + const named = kNamedKeyIdentifiers[keyCode]; + if (named !== undefined) + return named; + if (keyCode >= 112 && keyCode <= 135) + return 'F' + (keyCode - 111); + if (key.length === 1) + return 'U+' + key.toUpperCase().charCodeAt(0).toString(16).toUpperCase().padStart(4, '0'); + return ''; +} + +function dispatchKeyEvent(node: EventTarget, type: string, init: KeyboardEventInit, keyCode: number, key: string): boolean { + const event = new KeyboardEvent(type, init); + Object.defineProperty(event, 'keyIdentifier', { value: keyIdentifierFor(keyCode, key), configurable: true }); + return markAndDispatch(node, event); +} + +export class WebViewInput { + private _window: Window & typeof globalThis; + private _document: Document; + private _hoverTarget: Element | null = null; + + constructor(window: Window & typeof globalThis, document: Document) { + this._window = window; + this._document = document; + } + + // Descend through open shadow roots so synthetic events land on the actual + // element under the pointer rather than on the shadow host. + private _deepElementFromPoint(x: number, y: number): Element | null { + let el = this._document.elementFromPoint(x, y); + while (el && el.shadowRoot) { + const inner = el.shadowRoot.elementFromPoint(x, y); + if (!inner || inner === el) + break; + el = inner; + } + return el; + } + + // The focused element may live inside one or more shadow roots, where + // document.activeElement only reports the outermost shadow host. + private _deepActiveElement(): Element | null { + let active = this._document.activeElement; + while (active && active.shadowRoot && active.shadowRoot.activeElement) + active = active.shadowRoot.activeElement; + return active; + } + + private _insertText(target: Element | null, text: string) { + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + const start = target.selectionStart ?? target.value.length; + const end = target.selectionEnd ?? target.value.length; + target.value = target.value.slice(0, start) + text + target.value.slice(end); + const pos = start + text.length; + try { + target.setSelectionRange(pos, pos); + } catch { + } + target.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: false, data: text, inputType: 'insertText' })); + } else if (target && (target as HTMLElement).isContentEditable) { + this._document.execCommand('insertText', false, text); + } + } + + keydown(params: KeyEventParams) { + const target = this._deepActiveElement() || this._document.body; + if (!target) + return; + const init: KeyboardEventInit = { + bubbles: true, + cancelable: true, + view: this._window, + code: params.code, + key: params.key, + keyCode: params.keyCode, + which: params.keyCode, + location: params.location, + repeat: params.repeat, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }; + const notPrevented = dispatchKeyEvent(target, 'keydown', init, params.keyCode, params.key); + if (params.text === undefined) + return; + const charCode = params.text.charCodeAt(0); + const charNotPrevented = markAndDispatch(target, new KeyboardEvent('keypress', { ...init, charCode, keyCode: charCode, which: charCode })); + if (!notPrevented || !charNotPrevented) + return; + // Real WebKit fires a `textInput` (TextEvent) whose default action performs + // the insertion (and the subsequent beforeinput/input). Replicate it; the + // event's default does the insertion, so we do not insert manually. Enter's + // text is '\r' but the inserted/textInput data is a newline. + this._dispatchTextInput(target, params.text === '\r' ? '\n' : params.text); + } + + private _dispatchTextInput(target: EventTarget, text: string) { + // TextEvent has no usable constructor in WebKit — initTextEvent is the only + // way to create one (initTextEvent(type, bubbles, cancelable, view, data)). + const event = this._document.createEvent('TextEvent') as any; + event.initTextEvent('textInput', true, true, this._window, text); + markAndDispatch(target, event); + } + + keyup(params: KeyEventParams) { + const target = this._deepActiveElement() || this._document.body; + if (!target) + return; + dispatchKeyEvent(target, 'keyup', { + bubbles: true, + cancelable: true, + view: this._window, + code: params.code, + key: params.key, + keyCode: params.keyCode, + which: params.keyCode, + location: params.location, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }, params.keyCode, params.key); + } + + insertText(text: string) { + this._insertText(this._deepActiveElement(), text); + } + + mouseMove(params: MouseMoveParams) { + const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; + const base: MouseEventInit = { + bubbles: true, + cancelable: true, + view: this._window, + clientX: params.x, + clientY: params.y, + screenX: params.x, + screenY: params.y, + button: params.button, + buttons: params.buttons, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }; + const pointer: PointerEventInit = { ...base, pointerId: 1, pointerType: 'mouse', isPrimary: true }; + const prev = this._hoverTarget; + if (prev !== target) { + if (prev && prev.isConnected) { + markAndDispatch(prev, new PointerEvent('pointerout', { ...pointer, relatedTarget: target })); + markAndDispatch(prev, new MouseEvent('mouseout', { ...base, relatedTarget: target })); + markAndDispatch(prev, new PointerEvent('pointerleave', { ...pointer, bubbles: false, cancelable: false, relatedTarget: target })); + markAndDispatch(prev, new MouseEvent('mouseleave', { ...base, bubbles: false, cancelable: false, relatedTarget: target })); + } + markAndDispatch(target, new PointerEvent('pointerover', { ...pointer, relatedTarget: prev })); + markAndDispatch(target, new MouseEvent('mouseover', { ...base, relatedTarget: prev })); + markAndDispatch(target, new PointerEvent('pointerenter', { ...pointer, bubbles: false, cancelable: false, relatedTarget: prev })); + markAndDispatch(target, new MouseEvent('mouseenter', { ...base, bubbles: false, cancelable: false, relatedTarget: prev })); + this._hoverTarget = target; + } + markAndDispatch(target, new PointerEvent('pointermove', pointer)); + markAndDispatch(target, new MouseEvent('mousemove', base)); + } + + mouseEvent(params: MouseEventParams) { + const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; + const event = new MouseEvent(params.type, { + bubbles: true, + cancelable: true, + view: this._window, + clientX: params.x, + clientY: params.y, + screenX: params.x, + screenY: params.y, + button: params.button, + buttons: params.buttons, + detail: params.clickCount, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }); + markAndDispatch(target, event); + } + + wheel(params: WheelParams) { + const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; + const event = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + view: this._window, + clientX: params.x, + clientY: params.y, + screenX: params.x, + screenY: params.y, + deltaX: params.deltaX, + deltaY: params.deltaY, + deltaMode: 0, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }); + markAndDispatch(target, event); + this._window.scrollBy(params.deltaX, params.deltaY); + } + + tap(params: TapParams) { + const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; + const init: MouseEventInit = { + bubbles: true, + cancelable: true, + view: this._window, + clientX: params.x, + clientY: params.y, + screenX: params.x, + screenY: params.y, + ctrlKey: params.ctrlKey, + shiftKey: params.shiftKey, + altKey: params.altKey, + metaKey: params.metaKey, + }; + try { + const touch = new Touch({ identifier: 0, target, clientX: params.x, clientY: params.y, screenX: params.x, screenY: params.y, pageX: params.x, pageY: params.y, radiusX: 1, radiusY: 1, rotationAngle: 0, force: 1 }); + markAndDispatch(target, new TouchEvent('touchstart', { ...init, touches: [touch], targetTouches: [touch], changedTouches: [touch] })); + markAndDispatch(target, new TouchEvent('touchend', { ...init, touches: [], targetTouches: [], changedTouches: [touch] })); + } catch { + } + markAndDispatch(target, new MouseEvent('mousedown', { ...init, button: 0, buttons: 1, detail: 1 })); + markAndDispatch(target, new MouseEvent('mouseup', { ...init, button: 0, buttons: 0, detail: 1 })); + markAndDispatch(target, new MouseEvent('click', { ...init, button: 0, buttons: 0, detail: 1 })); + } +} + +export default WebViewInput; diff --git a/packages/playwright-core/src/server/webkit/DEPS.list b/packages/playwright-core/src/server/webkit/DEPS.list index 67e194822dae6..6935c13606896 100644 --- a/packages/playwright-core/src/server/webkit/DEPS.list +++ b/packages/playwright-core/src/server/webkit/DEPS.list @@ -2,6 +2,7 @@ @isomorphic/** @utils/** ../ +../../generated/ ../registry/ node_modules/jpeg-js node_modules/pngjs diff --git a/packages/playwright-core/src/server/webkit/webview/wvInput.ts b/packages/playwright-core/src/server/webkit/webview/wvInput.ts index afce325482fc7..526ba8e297eaa 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvInput.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvInput.ts @@ -30,18 +30,6 @@ function modifierFlags(modifiers: Set) { }; } -// Inlined into the page-side script; descends through open shadow roots so synthetic -// events land on the actual element under the pointer rather than on the shadow host. -const kDeepElementFromPointSrc = `(x, y) => { - let el = document.elementFromPoint(x, y); - while (el && el.shadowRoot) { - const inner = el.shadowRoot.elementFromPoint(x, y); - if (!inner || inner === el) break; - el = inner; - } - return el; -}`; - function buttonToNumber(button: types.MouseButton | 'none'): number { if (button === 'left') return 0; @@ -72,46 +60,22 @@ export class RawKeyboardImpl implements input.RawKeyboard { async keydown(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise { const { code, keyCode, key, text, location } = description; - const mods = modifierFlags(modifiers); - const charCode = text ? text.charCodeAt(0) : 0; - const expr = `(() => { - const t = document.activeElement || document.body; - const init = { bubbles: true, cancelable: true, view: window, code: ${JSON.stringify(code)}, key: ${JSON.stringify(key)}, keyCode: ${keyCode}, which: ${keyCode}, location: ${location}, repeat: ${autoRepeat}, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} }; - const dispatch = e => { Object.defineProperty(e, '__pwTrustedSynthetic', { value: true }); t.dispatchEvent(e); }; - dispatch(new KeyboardEvent('keydown', init)); - ${text ? `dispatch(new KeyboardEvent('keypress', { ...init, charCode: ${charCode}, keyCode: ${charCode}, which: ${charCode} }));` : ''} - })()`; - await evalInPage(progress, this._session, expr); + const params = { + code, key, keyCode, location, repeat: autoRepeat, + ...modifierFlags(modifiers), + ...(text ? { text } : {}), + }; + await callWebViewInput(progress, this._session, 'keydown', params); } async keyup(progress: Progress, modifiers: Set, keyName: string, description: input.KeyDescription): Promise { const { code, keyCode, key, location } = description; - const mods = modifierFlags(modifiers); - const expr = `(() => { - const t = document.activeElement || document.body; - const event = new KeyboardEvent('keyup', { bubbles: true, cancelable: true, view: window, code: ${JSON.stringify(code)}, key: ${JSON.stringify(key)}, keyCode: ${keyCode}, which: ${keyCode}, location: ${location}, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} }); - Object.defineProperty(event, '__pwTrustedSynthetic', { value: true }); - t.dispatchEvent(event); - })()`; - await evalInPage(progress, this._session, expr); + const params = { code, key, keyCode, location, ...modifierFlags(modifiers) }; + await callWebViewInput(progress, this._session, 'keyup', params); } async sendText(progress: Progress, text: string): Promise { - const expr = `(() => { - const text = ${JSON.stringify(text)}; - const t = document.activeElement; - if (t && (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement)) { - const start = t.selectionStart ?? t.value.length; - const end = t.selectionEnd ?? t.value.length; - t.value = t.value.slice(0, start) + text + t.value.slice(end); - const pos = start + text.length; - try { t.setSelectionRange(pos, pos); } catch {} - t.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: false, data: text, inputType: 'insertText' })); - } else if (t && t.isContentEditable) { - document.execCommand('insertText', false, text); - } - })()`; - await evalInPage(progress, this._session, expr); + await callWebViewInput(progress, this._session, 'insertText', text); } } @@ -123,52 +87,40 @@ export class RawMouseImpl implements input.RawMouse { } async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise { - await this._dispatchMouse(progress, 'mousemove', x, y, buttonToNumber(button), toButtonsMask(buttons), modifiers, 0); + await callWebViewInput(progress, this._session, 'mouseMove', { + x, y, button: buttonToNumber(button), buttons: toButtonsMask(buttons), ...modifierFlags(modifiers), + }); } async down(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise { const buttonCode = buttonToNumber(button); - await this._dispatchMouse(progress, 'mousedown', x, y, buttonCode, toButtonsMask(buttons), modifiers, clickCount); + const buttonsMask = toButtonsMask(buttons); + await this._mouseEvent(progress, 'mousedown', x, y, buttonCode, buttonsMask, modifiers, clickCount); if (button === 'right') - await this._dispatchMouse(progress, 'contextmenu', x, y, buttonCode, toButtonsMask(buttons), modifiers, clickCount); + await this._mouseEvent(progress, 'contextmenu', x, y, buttonCode, buttonsMask, modifiers, clickCount); } async up(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise { const buttonCode = buttonToNumber(button); const buttonsMask = toButtonsMask(buttons); - await this._dispatchMouse(progress, 'mouseup', x, y, buttonCode, buttonsMask, modifiers, clickCount); + await this._mouseEvent(progress, 'mouseup', x, y, buttonCode, buttonsMask, modifiers, clickCount); if (clickCount > 0) { // Non-primary buttons fire 'auxclick'; primary fires 'click'. const clickType = button === 'left' ? 'click' : 'auxclick'; - await this._dispatchMouse(progress, clickType, x, y, buttonCode, buttonsMask, modifiers, clickCount); + await this._mouseEvent(progress, clickType, x, y, buttonCode, buttonsMask, modifiers, clickCount); if (clickCount === 2) - await this._dispatchMouse(progress, 'dblclick', x, y, buttonCode, buttonsMask, modifiers, clickCount); + await this._mouseEvent(progress, 'dblclick', x, y, buttonCode, buttonsMask, modifiers, clickCount); } } async wheel(progress: Progress, x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise { - const mods = modifierFlags(modifiers); - const expr = `(() => { - const x = ${x}, y = ${y}; - const target = (${kDeepElementFromPointSrc})(x, y) || document.documentElement; - const event = new WheelEvent('wheel', { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, deltaX: ${deltaX}, deltaY: ${deltaY}, deltaMode: 0, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} }); - Object.defineProperty(event, '__pwTrustedSynthetic', { value: true }); - target.dispatchEvent(event); - window.scrollBy(${deltaX}, ${deltaY}); - })()`; - await evalInPage(progress, this._session, expr); + await callWebViewInput(progress, this._session, 'wheel', { x, y, deltaX, deltaY, ...modifierFlags(modifiers) }); } - private async _dispatchMouse(progress: Progress, type: string, x: number, y: number, button: number, buttons: number, modifiers: Set, clickCount: number) { - const mods = modifierFlags(modifiers); - const expr = `(() => { - const x = ${x}, y = ${y}; - const target = (${kDeepElementFromPointSrc})(x, y) || document.documentElement; - const event = new MouseEvent(${JSON.stringify(type)}, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, button: ${button}, buttons: ${buttons}, detail: ${clickCount}, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} }); - Object.defineProperty(event, '__pwTrustedSynthetic', { value: true }); - target.dispatchEvent(event); - })()`; - await evalInPage(progress, this._session, expr); + private async _mouseEvent(progress: Progress, type: string, x: number, y: number, button: number, buttons: number, modifiers: Set, clickCount: number) { + await callWebViewInput(progress, this._session, 'mouseEvent', { + type, x, y, button, buttons, clickCount, ...modifierFlags(modifiers), + }); } } @@ -180,27 +132,13 @@ export class RawTouchscreenImpl implements input.RawTouchscreen { } async tap(progress: Progress, x: number, y: number, modifiers: Set) { - const mods = modifierFlags(modifiers); - const expr = `(() => { - const x = ${x}, y = ${y}; - const target = (${kDeepElementFromPointSrc})(x, y) || document.documentElement; - const init = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} }; - const dispatch = e => { Object.defineProperty(e, '__pwTrustedSynthetic', { value: true }); target.dispatchEvent(e); }; - try { - const touch = new Touch({ identifier: 0, target, clientX: x, clientY: y, screenX: x, screenY: y, pageX: x, pageY: y, radiusX: 1, radiusY: 1, rotationAngle: 0, force: 1 }); - dispatch(new TouchEvent('touchstart', { ...init, touches: [touch], targetTouches: [touch], changedTouches: [touch] })); - dispatch(new TouchEvent('touchend', { ...init, touches: [], targetTouches: [], changedTouches: [touch] })); - } catch {} - dispatch(new MouseEvent('mousedown', { ...init, button: 0, buttons: 1, detail: 1 })); - dispatch(new MouseEvent('mouseup', { ...init, button: 0, buttons: 0, detail: 1 })); - dispatch(new MouseEvent('click', { ...init, button: 0, buttons: 0, detail: 1 })); - })()`; - await evalInPage(progress, this._session, expr); + await callWebViewInput(progress, this._session, 'tap', { x, y, ...modifierFlags(modifiers) }); } } -async function evalInPage(progress: Progress, session: WVSession | undefined, expression: string): Promise { +async function callWebViewInput(progress: Progress, session: WVSession | undefined, method: string, arg: any): Promise { if (!session) throw new Error('Page is not initialized'); + const expression = `window.__pwWebViewInput.${method}(${JSON.stringify(arg)})`; await progress.race(session.send('Runtime.evaluate', { expression, returnByValue: true } as any)); } diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index ac5f195c82530..e043cd4635b0e 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -28,6 +28,7 @@ import * as dom from '../../dom'; import { TargetClosedError } from '../../errors'; import { helper } from '../../helper'; import { saveGlobalsSnapshotSource } from '../../javascript'; +import * as rawWebViewInputSource from '../../../generated/webViewInputSource'; import * as network from '../../network'; import { Page, PageBinding } from '../../page'; import { WVSession } from './wvConnection'; @@ -186,8 +187,9 @@ export class WVPage implements PageDelegate { session.sendMayFail('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }), ]); } - // Inject the dialog bridge into the currently-loaded document too — - // bootstrap only applies to future navigations. + // Inject the page-side input dispatcher and dialog bridge into the + // currently-loaded document too — bootstrap only applies to future navigations. + await session.sendMayFail('Runtime.evaluate', { expression: webViewInputBootstrapSource, returnByValue: true } as any); if (this._dialogEndpoint) { await session.sendMayFail('Runtime.evaluate', { expression: dialogBridgeSource(this._dialogEndpoint), @@ -612,6 +614,7 @@ export class WVPage implements PageDelegate { scripts.push('if (!window.GestureEvent) window.GestureEvent = function GestureEvent() {};'); scripts.push(this._publicKeyCredentialScript()); scripts.push(bindingBridgeSource); + scripts.push(webViewInputBootstrapSource); if (this._dialogEndpoint) scripts.push(dialogBridgeSource(this._dialogEndpoint)); scripts.push(...this._page.allInitScripts().map(script => script.source)); @@ -996,6 +999,12 @@ function isLoadedSecurely(url: string, timing: network.ResourceTiming) { } const BINDING_CALL_TAG = '__pw_binding_call__'; +const webViewInputBootstrapSource = `(() => { + const module = {}; + ${rawWebViewInputSource.source} + window.__pwWebViewInput = new (module.exports.default())(window, document); +})()`; + const bindingBridgeSource = ` if (!window['${PageBinding.kBindingName}']) { Object.defineProperty(window, '${PageBinding.kBindingName}', { diff --git a/tests/page/page-keyboard.spec.ts b/tests/page/page-keyboard.spec.ts index 954b590c51bad..56f879afb2fba 100644 --- a/tests/page/page-keyboard.spec.ts +++ b/tests/page/page-keyboard.spec.ts @@ -85,6 +85,19 @@ it('insertText should only emit input event', async ({ page, server }) => { expect(await events.jsonValue()).toEqual(['input']); }); +it('should emit keydown, keypress, textInput and input when typing a character', async ({ page }) => { + await page.setContent(``); + const events = await page.evaluateHandle(() => { + const events: string[] = []; + for (const type of ['keydown', 'keypress', 'textInput', 'input', 'keyup']) + document.querySelector('input').addEventListener(type, () => events.push(type)); + return events; + }); + await page.focus('input'); + await page.keyboard.press('f'); + expect(await events.jsonValue()).toEqual(['keydown', 'keypress', 'textInput', 'input', 'keyup']); +}); + it('should report shiftKey', async ({ page, server, browserName, platform }) => { it.fail(browserName === 'firefox' && platform === 'darwin'); diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index ea66ffa6073dc..352488eb3e263 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -106,6 +106,7 @@ page/page-click.spec.ts › should click the button inside an iframe [fail] page/page-click.spec.ts › should not hang when frame is detached [fail] page/page-drag.spec.ts › Drag and drop › should work inside iframe [fail] page/page-evaluate.spec.ts › should throw when frame is detached [fail] +page/page-keyboard.spec.ts › should type emoji into an iframe [fail] page/page-network-request.spec.ts › should work for subframe navigation request [fail] page/page-wait-for-function.spec.ts › should throw when frame is detached [fail] page/page-wait-for-selector-1.spec.ts › page.waitForSelector is shortcut for main frame [fail] @@ -216,14 +217,6 @@ page/page-screenshot.spec.ts › page screenshot animations › should not captu # deserves individual triage. # ============================================================================ page/elementhandle-bounding-box.spec.ts › should handle scroll offset and click [fail] -page/elementhandle-press.spec.ts › should not modify selection when focused [fail] -page/elementhandle-press.spec.ts › should not select existing value [fail] -page/elementhandle-press.spec.ts › should reset selection when not focused [fail] -page/elementhandle-press.spec.ts › should work [fail] -page/elementhandle-type.spec.ts › should not modify selection when focused [fail] -page/elementhandle-type.spec.ts › should not select existing value [fail] -page/elementhandle-type.spec.ts › should reset selection when not focused [fail] -page/elementhandle-type.spec.ts › should work [fail] page/expect-boolean.spec.ts › toBeAttached › with frameLocator [fail] page/expect-boolean.spec.ts › toBeVisible › with frameLocator [fail] page/expect-boolean.spec.ts › toBeVisible › with frameLocator 2 [fail] @@ -247,19 +240,13 @@ page/frame-goto.spec.ts › should continue after client redirect [fail] page/interception.spec.ts › should intercept network activity from worker 2 [fail] page/locator-frame.spec.ts › should click in lazy iframe [fail] page/locator-misc-1.spec.ts › should clear input [fail] -page/locator-misc-2.spec.ts › should press @smoke [fail] page/locator-misc-2.spec.ts › should scroll zero-sized element into view [fail] -page/locator-misc-2.spec.ts › should type [fail] page/locator-query.spec.ts › should allow some, but not all nested frameLocators [fail] page/network-post-data.spec.ts › should return correct postData buffer for utf-8 body [fail] page/network-post-data.spec.ts › should throw on invalid JSON in post data [fail] page/page-add-init-script.spec.ts › init script should run only once in iframe [fail] -page/page-add-locator-handler.spec.ts › should wait for hidden by default 2 [fail] page/page-add-locator-handler.spec.ts › should work when owner frame detaches [fail] page/page-add-locator-handler.spec.ts › should work with locator.hover() [fail] -page/page-add-locator-handler.spec.ts › should work with locator.waitFor [fail] -page/page-add-locator-handler.spec.ts › should work with times: option [fail] -page/page-add-locator-handler.spec.ts › should work › mouseover 1 times [fail] page/page-aria-snapshot-ai.spec.ts › emit generic roles for nodes w/o roles [fail] page/page-aria-snapshot-ai.spec.ts › should include cursor pointer hint [fail] page/page-aria-snapshot-ai.spec.ts › should not generate refs for elements with pointer-events:none [fail] @@ -270,7 +257,6 @@ page/page-autowaiting-basic.spec.ts › should report navigation in the log when page/page-autowaiting-basic.spec.ts › should work with waitForLoadState(load) [fail] page/page-basic.spec.ts › async stacks should work [fail] page/page-basic.spec.ts › has navigator.webdriver set to true [fail] -page/page-basic.spec.ts › page.press should work [fail] page/page-basic.spec.ts › page.url should include hashes [fail] page/page-basic.spec.ts › should have sane user agent [fail] page/page-click-react.spec.ts › should not retarget when element is recycled on hover [fail] @@ -325,28 +311,17 @@ page/page-goto.spec.ts › should send referer of cross-origin URL [fail] page/page-goto.spec.ts › should work with cross-process that fails before committing [fail] page/page-history.spec.ts › page.goBack should work with HistoryAPI [fail] page/page-history.spec.ts › page.reload should work with data url [fail] -page/page-keyboard.spec.ts › locator should pressSequentially with namedKeys [fail] page/page-keyboard.spec.ts › should be able to prevent selectAll [fail] page/page-keyboard.spec.ts › should dispatch a click event on a button when Space gets pressed [fail] page/page-keyboard.spec.ts › should dispatch insertText after context menu was opened [fail] -page/page-keyboard.spec.ts › should expose keyIdentifier in webkit [fail] page/page-keyboard.spec.ts › should move around the selection in a contenteditable [fail] page/page-keyboard.spec.ts › should move to the start of the document [fail] page/page-keyboard.spec.ts › should move with the arrow keys [fail] -page/page-keyboard.spec.ts › should not type canceled events [fail] -page/page-keyboard.spec.ts › should press Enter [fail] -page/page-keyboard.spec.ts › should send a character with ElementHandle.press [fail] +page/page-keyboard.spec.ts › should scroll with PageDown [fail] page/page-keyboard.spec.ts › should support MacOS shortcuts [fail] page/page-keyboard.spec.ts › should support simple copy-pasting [fail] page/page-keyboard.spec.ts › should support simple cut-pasting [fail] page/page-keyboard.spec.ts › should support undo-redo [fail] -page/page-keyboard.spec.ts › should type all kinds of characters [fail] -page/page-keyboard.spec.ts › should type emoji [fail] -page/page-keyboard.spec.ts › should type into a textarea @smoke [fail] -page/page-keyboard.spec.ts › should type repeatedly in contenteditable in shadow dom [fail] -page/page-keyboard.spec.ts › should type repeatedly in contenteditable in shadow dom with nested elements [fail] -page/page-keyboard.spec.ts › should type repeatedly in input in shadow dom [fail] -page/page-keyboard.spec.ts › should type with namedKeys [fail] page/page-localstorage.spec.ts › localStorage.removeItem removes a single item [fail] page/page-mouse.spec.ts › down and up should generate click [fail] page/page-mouse.spec.ts › should click the document @smoke [fail] @@ -408,8 +383,6 @@ page/elementhandle-scroll-into-view.spec.ts › should throw for detached elemen page/elementhandle-scroll-into-view.spec.ts › should wait for display:none to become visible [fail] page/frame-evaluate.spec.ts › should work in iframes that interrupted initial javascript url navigation [fail] page/frame-goto.spec.ts › should reject when frame detaches [fail] -page/locator-misc-2.spec.ts › should pressSequentially [fail] -page/page-add-locator-handler.spec.ts › should throw when handler times out [fail] page/page-aria-snapshot-ai.spec.ts › should snapshot a locator inside an iframe [fail] page/page-click-react.spec.ts › should not retarget when element changes on hover [fail] page/page-click.spec.ts › should not wait with noAutoWaiting 2 [fail] @@ -465,8 +438,6 @@ page/page-history.spec.ts › page.goForward during renderer-initiated navigatio page/page-history.spec.ts › page.reload should work on a page with a hash [fail] page/page-history.spec.ts › page.reload should work with cross-origin redirect [fail] page/page-history.spec.ts › should reload proper page [fail] -page/page-keyboard.spec.ts › should scroll with PageDown [fail] -page/page-keyboard.spec.ts › should work after a cross origin navigation [fail] page/page-leaks.spec.ts › click should not leak [fail] page/page-leaks.spec.ts › expect should not leak [fail] page/page-leaks.spec.ts › fill should not leak [fail] @@ -685,7 +656,6 @@ page/page-goto.spec.ts › should capture cross-process iframe navigation reques page/page-goto.spec.ts › should capture iframe navigation request [fail] page/page-goto.spec.ts › should wait for load when iframe attaches and detaches [fail] page/page-goto.spec.ts › should work with lazy loading iframes [fail] -page/page-keyboard.spec.ts › should type emoji into an iframe [fail] page/page-network-idle.spec.ts › should wait for networkidle from the popup [fail] page/page-network-idle.spec.ts › should wait for networkidle when iframe attaches and detaches [fail] page/page-network-idle.spec.ts › should wait for networkidle when navigating iframe [fail] @@ -770,7 +740,6 @@ page/network-post-data.spec.ts › should get post data for file/blob [fail] page/network-post-data.spec.ts › should get post data for navigator.sendBeacon api calls [fail] page/page-add-init-script.spec.ts › should remove init script after dispose [fail] page/page-add-init-script.spec.ts › should work after a cross origin navigation [fail] -page/page-add-locator-handler.spec.ts › should work [fail] page/page-autowaiting-basic.spec.ts › should await cross-process navigation when clicking anchor [fail] page/page-autowaiting-basic.spec.ts › should await form-get on click [fail] page/page-autowaiting-no-hang.spec.ts › clicking in the middle of navigation that commits [fail] diff --git a/utils/generate_injected.js b/utils/generate_injected.js index e661df9d5e7f0..022e069a83b97 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -74,6 +74,12 @@ const injectedScripts = [ path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), true, ], + [ + path.join(ROOT, 'packages', 'injected', 'src', 'webview', 'webViewInput.ts'), + path.join(ROOT, 'packages', 'injected', 'lib'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), + true, + ], [ path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'), path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),