diff --git a/packages/injected/src/webview/webViewInput.ts b/packages/injected/src/webview/webViewInput.ts index bddc78ae40621..6c4972f246b18 100644 --- a/packages/injected/src/webview/webViewInput.ts +++ b/packages/injected/src/webview/webViewInput.ts @@ -66,6 +66,10 @@ function markAndDispatch(node: EventTarget, event: Event): boolean { return node.dispatchEvent(event); } +function modifiersOf(modifiers: Modifiers): Modifiers { + return { ctrlKey: modifiers.ctrlKey, shiftKey: modifiers.shiftKey, altKey: modifiers.altKey, metaKey: modifiers.metaKey }; +} + // 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 @@ -101,8 +105,8 @@ function keyIdentifierFor(keyCode: number, key: string): string { return ''; } -function dispatchKeyEvent(node: EventTarget, type: string, init: KeyboardEventInit, keyCode: number, key: string): boolean { - const event = new KeyboardEvent(type, init); +function dispatchKeyEvent(view: Window & typeof globalThis, node: EventTarget, type: string, init: KeyboardEventInit, keyCode: number, key: string): boolean { + const event = new view.KeyboardEvent(type, init); Object.defineProperty(event, 'keyIdentifier', { value: keyIdentifierFor(keyCode, key), configurable: true }); return markAndDispatch(node, event); } @@ -132,67 +136,141 @@ export class WebViewInput { }); } - // 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) + private _deepHitTestStep(doc: Document | null, x: number, y: number): { target: Element | null, frame: Element | null, doc: Document | null, x: number, y: number } { + let target = doc?.elementFromPoint(x, y) ?? null; + while (target?.shadowRoot) { + const inner = target.shadowRoot.elementFromPoint(x, y); + if (!inner || inner === target) break; - el = inner; + target = inner; + } + if (!target || (target.localName !== 'iframe' && target.localName !== 'frame')) + return { target, frame: null, doc: null, x, y }; + const frame = target as HTMLIFrameElement | HTMLFrameElement; + const frameRect = frame.getBoundingClientRect(); + const frameStyle = frame.ownerDocument.defaultView!.getComputedStyle(frame); + x -= frameRect.left + parseFloat(frameStyle.borderLeftWidth) + parseFloat(frameStyle.paddingLeft); + y -= frameRect.top + parseFloat(frameStyle.borderTopWidth) + parseFloat(frameStyle.paddingTop); + try { + doc = frame.contentDocument; + } catch { + doc = null; + } + return { target, frame, doc, x, y }; + } + + private _deepHitTest(x: number, y: number): { target: Element | null, view: Window & typeof globalThis, x: number, y: number, crossOriginFrame: Element | null, crossOriginFrameX: number, crossOriginFrameY: number } { + let doc: Document = this._document; + for (;;) { + const hit = this._deepHitTestStep(doc, x, y); + const frameWindow = hit.doc?.defaultView; + const isInsideFrameBounds = !!frameWindow && + hit.x >= 0 && hit.x < frameWindow.innerWidth && + hit.y >= 0 && hit.y < frameWindow.innerHeight; + if (!hit.frame || !hit.doc || !isInsideFrameBounds) { + return { + target: hit.target, + view: doc.defaultView as (Window & typeof globalThis), + x, + y, + crossOriginFrame: (hit.frame && !frameWindow) ? hit.frame : null, + crossOriginFrameX: hit.x, + crossOriginFrameY: hit.y + }; + } + doc = hit.doc; + x = hit.x; + y = hit.y; } - return el; } - // The focused element may live inside one or more shadow roots, where - // document.activeElement only reports the outermost shadow host. + positionInIFrame(x: number, y: number): { iframe: Element | null, x: number, y: number } { + const hit = this._deepHitTest(x, y); + return { + iframe: hit.crossOriginFrame, + x: hit.crossOriginFrameX, + y: hit.crossOriginFrameY, + }; + } + private _deepActiveElement(): Element | null { let active = this._document.activeElement; - while (active && active.shadowRoot && active.shadowRoot.activeElement) - active = active.shadowRoot.activeElement; + for (;;) { + if (active && active.shadowRoot && active.shadowRoot.activeElement) { + active = active.shadowRoot.activeElement; + continue; + } + if (active && (active.localName === 'iframe' || active.localName === 'frame')) { + let inner: Element | null = null; + try { + const doc = (active as HTMLIFrameElement | HTMLFrameElement).contentDocument; + inner = doc ? doc.activeElement : null; + } catch { + inner = null; + } + if (inner && inner !== active) { + active = inner; + continue; + } + } + break; + } 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); + if (!target) + return; + const view = target.ownerDocument.defaultView; + const HTMLInputElementConstructor = view?.HTMLInputElement ?? HTMLInputElement; + const HTMLTextAreaElementConstructor = view?.HTMLTextAreaElement ?? HTMLTextAreaElement; + const InputEventConstructor = view?.InputEvent ?? InputEvent; + if (target instanceof HTMLInputElementConstructor || target instanceof HTMLTextAreaElementConstructor) { + const field = target as HTMLInputElement | HTMLTextAreaElement; + const start = field.selectionStart ?? field.value.length; + const end = field.selectionEnd ?? field.value.length; + field.value = field.value.slice(0, start) + text + field.value.slice(end); const pos = start + text.length; try { - target.setSelectionRange(pos, pos); + field.setSelectionRange(pos, pos); } catch { } - target.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: false, data: text, inputType: 'insertText' })); + field.dispatchEvent(new InputEventConstructor('input', { bubbles: true, cancelable: false, data: text, inputType: 'insertText' })); } else if (target && (target as HTMLElement).isContentEditable) { - this._document.execCommand('insertText', false, text); + target.ownerDocument.execCommand('insertText', false, text); } } - keydown(params: KeyEventParams): Promise { + private _resolveActive(): { target: Element, doc: Document, view: Window & typeof globalThis } | null { const target = this._deepActiveElement() || this._document.body; if (!target) + return null; + const doc = target.ownerDocument; + const view = (doc.defaultView || this._window) as Window & typeof globalThis; + return { target, doc, view }; + } + + keydown(params: KeyEventParams): Promise { + const active = this._resolveActive(); + if (!active) return Promise.resolve(); + const { target, doc, view } = active; const init: KeyboardEventInit = { bubbles: true, cancelable: true, - view: this._window, + view, 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, + ...modifiersOf(params), }; let notPrevented = true; let charNotPrevented = true; let lastTask = this._postTask(() => { - notPrevented = dispatchKeyEvent(target, 'keydown', init, params.keyCode, params.key); + notPrevented = dispatchKeyEvent(view, target, 'keydown', init, params.keyCode, params.key); }); // Non-text keys produce only keydown; a cancelled keydown also suppresses the // keypress and the default text insertion. @@ -202,7 +280,7 @@ export class WebViewInput { if (!notPrevented) return; const charCode = text.charCodeAt(0); - charNotPrevented = markAndDispatch(target, new KeyboardEvent('keypress', { ...init, charCode, keyCode: charCode, which: charCode })); + charNotPrevented = markAndDispatch(target, new view.KeyboardEvent('keypress', { ...init, charCode, keyCode: charCode, which: charCode })); }); lastTask = this._postTask(() => { if (!notPrevented || !charNotPrevented) @@ -211,38 +289,36 @@ export class WebViewInput { // 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, text === '\r' ? '\n' : text); + this._dispatchTextInput(doc, view, target, text === '\r' ? '\n' : text); }); } return lastTask; } - private _dispatchTextInput(target: EventTarget, text: string) { + private _dispatchTextInput(doc: Document, view: Window & typeof globalThis, 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); + const event = doc.createEvent('TextEvent') as any; + event.initTextEvent('textInput', true, true, view, text); markAndDispatch(target, event); } keyup(params: KeyEventParams): Promise { - const target = this._deepActiveElement() || this._document.body; - if (!target) + const active = this._resolveActive(); + if (!active) return Promise.resolve(); + const { target, view } = active; return this._postTask(() => { - dispatchKeyEvent(target, 'keyup', { + dispatchKeyEvent(view, target, 'keyup', { bubbles: true, cancelable: true, - view: this._window, + view, 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, + ...modifiersOf(params), }, params.keyCode, params.key); }); } @@ -251,113 +327,82 @@ export class WebViewInput { return this._postTask(() => this._insertText(this._deepActiveElement(), text)); } - mouseMove(params: MouseMoveParams): Promise { - 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, + private _resolveMouse(params: Modifiers & { x: number, y: number }): { view: Window & typeof globalThis, target: Element, x: number, y: number, init: MouseEventInit } { + const hit = this._deepHitTest(params.x, params.y); + const view = hit.view; + const target = hit.target || view.document.documentElement; + return { + view, + target, + x: hit.x, + y: hit.y, + init: { + bubbles: true, + cancelable: true, + view, + clientX: hit.x, + clientY: hit.y, + screenX: params.x, + screenY: params.y, + ...modifiersOf(params), + }, }; + } + + mouseMove(params: MouseMoveParams): Promise { + const { view, target, init } = this._resolveMouse(params); + const base: MouseEventInit = { ...init, button: params.button, buttons: params.buttons }; const pointer: PointerEventInit = { ...base, pointerId: 1, pointerType: 'mouse', isPrimary: true }; const prev = this._hoverTarget; if (prev !== target) { + const sameDocument = prev?.ownerDocument === target.ownerDocument; if (prev && prev.isConnected) { - void this._postTask(() => markAndDispatch(prev, new PointerEvent('pointerout', { ...pointer, relatedTarget: target }))); - void this._postTask(() => markAndDispatch(prev, new MouseEvent('mouseout', { ...base, relatedTarget: target }))); - void this._postTask(() => markAndDispatch(prev, new PointerEvent('pointerleave', { ...pointer, bubbles: false, cancelable: false, relatedTarget: target }))); - void this._postTask(() => markAndDispatch(prev, new MouseEvent('mouseleave', { ...base, bubbles: false, cancelable: false, relatedTarget: target }))); + const prevView = (prev.ownerDocument.defaultView || this._window) as Window & typeof globalThis; + const relatedTarget = sameDocument ? target : null; + void this._postTask(() => markAndDispatch(prev, new prevView.PointerEvent('pointerout', { ...pointer, view: prevView, relatedTarget }))); + void this._postTask(() => markAndDispatch(prev, new prevView.MouseEvent('mouseout', { ...base, view: prevView, relatedTarget }))); + void this._postTask(() => markAndDispatch(prev, new prevView.PointerEvent('pointerleave', { ...pointer, view: prevView, bubbles: false, cancelable: false, relatedTarget }))); + void this._postTask(() => markAndDispatch(prev, new prevView.MouseEvent('mouseleave', { ...base, view: prevView, bubbles: false, cancelable: false, relatedTarget }))); } - void this._postTask(() => markAndDispatch(target, new PointerEvent('pointerover', { ...pointer, relatedTarget: prev }))); - void this._postTask(() => markAndDispatch(target, new MouseEvent('mouseover', { ...base, relatedTarget: prev }))); - void this._postTask(() => markAndDispatch(target, new PointerEvent('pointerenter', { ...pointer, bubbles: false, cancelable: false, relatedTarget: prev }))); - void this._postTask(() => markAndDispatch(target, new MouseEvent('mouseenter', { ...base, bubbles: false, cancelable: false, relatedTarget: prev }))); + const relatedTarget = sameDocument ? prev : null; + void this._postTask(() => markAndDispatch(target, new view.PointerEvent('pointerover', { ...pointer, relatedTarget }))); + void this._postTask(() => markAndDispatch(target, new view.MouseEvent('mouseover', { ...base, relatedTarget }))); + void this._postTask(() => markAndDispatch(target, new view.PointerEvent('pointerenter', { ...pointer, bubbles: false, cancelable: false, relatedTarget }))); + void this._postTask(() => markAndDispatch(target, new view.MouseEvent('mouseenter', { ...base, bubbles: false, cancelable: false, relatedTarget }))); this._hoverTarget = target; } - void this._postTask(() => markAndDispatch(target, new PointerEvent('pointermove', pointer))); - return this._postTask(() => markAndDispatch(target, new MouseEvent('mousemove', base))); + void this._postTask(() => markAndDispatch(target, new view.PointerEvent('pointermove', pointer))); + return this._postTask(() => markAndDispatch(target, new view.MouseEvent('mousemove', base))); } mouseEvent(params: MouseEventParams): Promise { // Resolve the hit target at dispatch time, not enqueue time: a queued move // ahead of this may reveal an overlay that should receive the press. return this._postTask(() => { - const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; - markAndDispatch(target, 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, - })); + const { view, target, init } = this._resolveMouse(params); + markAndDispatch(target, new view.MouseEvent(params.type, { ...init, button: params.button, buttons: params.buttons, detail: params.clickCount })); }); } wheel(params: WheelParams): Promise { return this._postTask(() => { - const target = this._deepElementFromPoint(params.x, params.y) || this._document.documentElement; - markAndDispatch(target, 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, - })); - this._window.scrollBy(params.deltaX, params.deltaY); + const { view, target, init } = this._resolveMouse(params); + markAndDispatch(target, new view.WheelEvent('wheel', { ...init, deltaX: params.deltaX, deltaY: params.deltaY, deltaMode: 0 })); + view.scrollBy(params.deltaX, params.deltaY); }); } tap(params: TapParams): Promise { - 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, - }; + const { view, target, x, y, init } = this._resolveMouse(params); 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 }); - void this._postTask(() => markAndDispatch(target, new TouchEvent('touchstart', { ...init, touches: [touch], targetTouches: [touch], changedTouches: [touch] }))); - void this._postTask(() => markAndDispatch(target, new TouchEvent('touchend', { ...init, touches: [], targetTouches: [], changedTouches: [touch] }))); + const touch = new view.Touch({ identifier: 0, target, clientX: x, clientY: y, screenX: params.x, screenY: params.y, pageX: x + view.scrollX, pageY: y + view.scrollY, radiusX: 1, radiusY: 1, rotationAngle: 0, force: 1 }); + void this._postTask(() => markAndDispatch(target, new view.TouchEvent('touchstart', { ...init, touches: [touch], targetTouches: [touch], changedTouches: [touch] }))); + void this._postTask(() => markAndDispatch(target, new view.TouchEvent('touchend', { ...init, touches: [], targetTouches: [], changedTouches: [touch] }))); } catch { } - void this._postTask(() => markAndDispatch(target, new MouseEvent('mousedown', { ...init, button: 0, buttons: 1, detail: 1 }))); - void this._postTask(() => markAndDispatch(target, new MouseEvent('mouseup', { ...init, button: 0, buttons: 0, detail: 1 }))); - return this._postTask(() => markAndDispatch(target, new MouseEvent('click', { ...init, button: 0, buttons: 0, detail: 1 }))); + void this._postTask(() => markAndDispatch(target, new view.MouseEvent('mousedown', { ...init, button: 0, buttons: 1, detail: 1 }))); + void this._postTask(() => markAndDispatch(target, new view.MouseEvent('mouseup', { ...init, button: 0, buttons: 0, detail: 1 }))); + return this._postTask(() => markAndDispatch(target, new view.MouseEvent('click', { ...init, button: 0, buttons: 0, detail: 1 }))); } } diff --git a/packages/playwright-core/src/server/webkit/webview/wvInput.ts b/packages/playwright-core/src/server/webkit/webview/wvInput.ts index 32ef4b46a8dae..f0868431cba5a 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvInput.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvInput.ts @@ -51,6 +51,8 @@ function toButtonsMask(buttons: Set): number { return mask; } +export type DispatchWebViewInput = (progress: Progress, method: string, params: Record) => Promise; + export class RawKeyboardImpl implements input.RawKeyboard { private _session: WVSession | undefined; @@ -80,14 +82,25 @@ export class RawKeyboardImpl implements input.RawKeyboard { } export class RawMouseImpl implements input.RawMouse { + private _dispatcher: DispatchWebViewInput | undefined; private _session: WVSession | undefined; + constructor(dispatcher?: DispatchWebViewInput) { + this._dispatcher = dispatcher; + } + setSession(session: WVSession) { this._session = session; } + private _send(progress: Progress, method: string, params: Record): Promise { + if (this._dispatcher) + return this._dispatcher(progress, method, params); + return callWebViewInput(progress, this._session, method, params); + } + async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise { - await callWebViewInput(progress, this._session, 'mouseMove', { + await this._send(progress, 'mouseMove', { x, y, button: buttonToNumber(button), buttons: toButtonsMask(buttons), ...modifierFlags(modifiers), }); } @@ -114,11 +127,11 @@ export class RawMouseImpl implements input.RawMouse { } async wheel(progress: Progress, x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise { - await callWebViewInput(progress, this._session, 'wheel', { x, y, deltaX, deltaY, ...modifierFlags(modifiers) }); + await this._send(progress, 'wheel', { x, y, deltaX, deltaY, ...modifierFlags(modifiers) }); } 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', { + await this._send(progress, 'mouseEvent', { type, x, y, button, buttons, clickCount, ...modifierFlags(modifiers), }); } diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index 4c68876f60452..f183af6fff9cd 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -80,7 +80,7 @@ export class WVPage implements PageDelegate { this._outerSession = outerSession; this._dialogEndpoint = dialogEndpoint; this.rawKeyboard = new RawKeyboardImpl(); - this.rawMouse = new RawMouseImpl(); + this.rawMouse = new RawMouseImpl(this._dispatchWebViewInput.bind(this)); this.rawTouchscreen = new RawTouchscreenImpl(); this._contextIdToContext = new Map(); this._page = new Page(this, browserContext); @@ -824,7 +824,7 @@ export class WVPage implements PageDelegate { async getBoundingBox(handle: dom.ElementHandle): Promise { const quads = await this.getContentQuads(handle); - if (!quads || !quads.length) + if (quads === 'error:notconnected' || !quads || !quads.length) return null; let minX = Infinity; let maxX = -Infinity; @@ -869,7 +869,7 @@ export class WVPage implements PageDelegate { return 1; } - async getContentQuads(handle: dom.ElementHandle): Promise { + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await handle.evaluateInUtility(([, node]) => { let element: Element | null = node as Element; while (element && element.nodeType !== 1 /* Node.ELEMENT_NODE */) @@ -886,9 +886,32 @@ export class WVPage implements PageDelegate { { x: r.left, y: r.bottom }, ]); }, {}); + if (result === 'error:notconnected') + return result; if (!result || typeof result === 'string') return null; - return result as types.Quad[]; + let quads = result as types.Quad[]; + let frame: frames.Frame | null = handle._frame; + while (frame?.parentFrame()) { + const frameElement = await this.getFrameElement(frame).catch(() => null); + if (!frameElement) + return null; + const offset = await frameElement.evaluateInUtility(([injected, iframe]) => { + const element = iframe as Element; + const style = injected.describeIFrameStyle(element); + if (style === 'error:notconnected' || style === 'transformed') + return style; + const rect = element.getBoundingClientRect(); + return { x: rect.left + style.left, y: rect.top + style.top }; + }, {}).finally(() => frameElement.dispose()); + if (offset === 'error:notconnected') + return offset; + if (!offset || typeof offset === 'string') + return null; + quads = quads.map(quad => quad.map(point => ({ x: point.x + offset.x, y: point.y + offset.y })) as types.Quad); + frame = frame.parentFrame(); + } + return quads; } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { @@ -898,6 +921,33 @@ export class WVPage implements PageDelegate { async inputActionEpilogue(): Promise { } + private async _dispatchWebViewInput(progress: Progress, method: string, params: Record): Promise { + let frame: frames.Frame = this._page.mainFrame(); + let x = params.x as number; + let y = params.y as number; + for (;;) { + const context = await progress.race(frame.mainContext()); + const position = await progress.race(context.evaluateHandle(([px, py]) => (globalThis as any).__pwWebViewInput.positionInIFrame(px, py), [x, y])); + try { + const iframe = await position.getProperty(progress, 'iframe') as dom.ElementHandle; + let childFrame: frames.Frame | null; + try { + childFrame = await progress.race(this.getContentFrame(iframe)); + } finally { + iframe.dispose(); + } + if (!childFrame) + break; + ({ x, y } = await progress.race(position.evaluate(result => ({ x: result.x, y: result.y })))); + frame = childFrame; + } finally { + position.dispose(); + } + } + const context = await progress.race(frame.mainContext()); + await progress.race(context.evaluate(({ method, params }) => (globalThis as any).__pwWebViewInput[method](params), { method, params: { ...params, x, y } })); + } + async resetForReuse(progress: Progress): Promise { } diff --git a/tests/page/elementhandle-bounding-box.spec.ts b/tests/page/elementhandle-bounding-box.spec.ts index edd7f0fd68bdf..dd70c89eebe3f 100644 --- a/tests/page/elementhandle-bounding-box.spec.ts +++ b/tests/page/elementhandle-bounding-box.spec.ts @@ -84,6 +84,36 @@ it('should return null for invisible elements', async ({ page, server }) => { expect(await element.boundingBox()).toBe(null); }); +it('should get bounding box of element inside an iframe', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` +
+ + `); + const frame = page.frames()[1]; + const button = await frame.waitForSelector('button'); + const iframeBox = (await (await page.$('iframe')).boundingBox())!; + const inner = await button.evaluate(b => { const r = b.getBoundingClientRect(); return { x: r.left, y: r.top }; }); + const box = (await button.boundingBox())!; + expect(Math.round(box.x)).toBe(Math.round(iframeBox.x + inner.x)); + expect(Math.round(box.y)).toBe(Math.round(iframeBox.y + inner.y)); +}); + +it('should get bounding box of element inside a cross-origin iframe', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` +
+ + `); + const frame = page.frames()[1]; + const button = await frame.waitForSelector('button'); + const iframeBox = (await (await page.$('iframe')).boundingBox())!; + const inner = await button.evaluate(b => { const r = b.getBoundingClientRect(); return { x: r.left, y: r.top }; }); + const box = (await button.boundingBox())!; + expect(Math.round(box.x)).toBe(Math.round(iframeBox.x + inner.x)); + expect(Math.round(box.y)).toBe(Math.round(iframeBox.y + inner.y)); +}); + it('should force a layout', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.setContent('
hello
'); diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index 89512c7e1977d..25c5a644d36d2 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -89,11 +89,8 @@ page/frame-evaluate.spec.ts › should not allow cross-frame element handles whe page/frame-frame-element.spec.ts › should work inside closed shadow root [fail] page/frame-frame-element.spec.ts › should work inside declarative shadow root [fail] page/frame-goto.spec.ts › should return matching responses [fail] -page/page-click.spec.ts › should click button inside frameset [fail] -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-keyboard.spec.ts › should type emoji into an iframe [fail] page/page-wait-for-selector-1.spec.ts › page.waitForSelector is shortcut for main frame [fail] # ============================================================================ @@ -540,7 +537,6 @@ page/to-match-aria-snapshot.spec.ts › should not match what is not matched [fa page/frame-evaluate.spec.ts › should work in iframes that interrupted initial javascript url navigation [fail] page/locator-frame.spec.ts › should work for iframe @smoke [fail] page/locator-frame.spec.ts › should work for nested iframe [fail] -page/locator-frame.spec.ts › should work with COEP/COOP/CORP isolated iframe [fail] page/page-add-init-script.spec.ts › init script should run only once in iframe [fail] page/page-aria-snapshot-ai.spec.ts › return empty snapshot when iframe is not loaded [fail] page/page-aria-snapshot-ai.spec.ts › should limit depth across iframe boundary [fail] @@ -558,9 +554,6 @@ page/page-click.spec.ts › should click a button that is overlaid by a permissi page/page-click.spec.ts › should click in a nested transformed iframe [fail] page/page-click.spec.ts › should click in a transformed iframe [fail] page/page-click.spec.ts › should click in a transformed iframe with force [fail] -page/page-click.spec.ts › should click in an iframe with border [fail] -page/page-click.spec.ts › should click in an iframe with border 2 [fail] -page/page-click.spec.ts › should click the button inside an iframe [fail] page/page-click.spec.ts › should click the button with fixed position inside an iframe [fail] page/page-click.spec.ts › should issue clicks in parallel in page and popup [fail] page/page-drag.spec.ts › Drag and drop › iframe › should drag into an iframe [fail]