Skip to content

Commit a8a8c7b

Browse files
committed
fix(webview): support input for elements inside cross-origin <iframe>
1 parent 9fb3602 commit a8a8c7b

4 files changed

Lines changed: 123 additions & 71 deletions

File tree

packages/injected/src/webview/webViewInput.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ function markAndDispatch(node: EventTarget, event: Event): boolean {
6666
return node.dispatchEvent(event);
6767
}
6868

69+
function isFrameOwner(element: Element | null): element is HTMLIFrameElement | HTMLFrameElement {
70+
return !!element && (element.localName === 'iframe' || element.localName === 'frame');
71+
}
72+
6973
// Legacy WebKit-only KeyboardEvent.keyIdentifier (a DOM Level 3 draft property
7074
// dropped by every other engine). It cannot be supplied via the constructor, so
7175
// compute it from the virtual key code and define it on the event before
@@ -145,6 +149,19 @@ export class WebViewInput {
145149
return el;
146150
}
147151

152+
positionInIFrame(x: number, y: number): { iframe: HTMLIFrameElement | HTMLFrameElement | null, x: number, y: number } {
153+
const target = this._deepElementFromPoint(x, y);
154+
if (!isFrameOwner(target))
155+
return { iframe: null, x, y };
156+
const frameRect = target.getBoundingClientRect();
157+
const frameStyle = this._window.getComputedStyle(target);
158+
return {
159+
iframe: target,
160+
x: x - frameRect.left - parseFloat(frameStyle.borderLeftWidth) - parseFloat(frameStyle.paddingLeft),
161+
y: y - frameRect.top - parseFloat(frameStyle.borderTopWidth) - parseFloat(frameStyle.paddingTop),
162+
};
163+
}
164+
148165
// The focused element may live inside one or more shadow roots, where
149166
// document.activeElement only reports the outermost shadow host.
150167
private _deepActiveElement(): Element | null {
@@ -154,6 +171,11 @@ export class WebViewInput {
154171
return active;
155172
}
156173

174+
activeIFrame(): HTMLIFrameElement | HTMLFrameElement | null {
175+
const active = this._deepActiveElement();
176+
return isFrameOwner(active) ? active : null;
177+
}
178+
157179
private _insertText(target: Element | null, text: string) {
158180
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
159181
const start = target.selectionStart ?? target.value.length;
@@ -350,7 +372,7 @@ export class WebViewInput {
350372
metaKey: params.metaKey,
351373
};
352374
try {
353-
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 });
375+
const touch = new Touch({ identifier: 0, target, clientX: params.x, clientY: params.y, screenX: params.x, screenY: params.y, pageX: params.x + this._window.scrollX, pageY: params.y + this._window.scrollY, radiusX: 1, radiusY: 1, rotationAngle: 0, force: 1 });
354376
void this._postTask(() => markAndDispatch(target, new TouchEvent('touchstart', { ...init, touches: [touch], targetTouches: [touch], changedTouches: [touch] })));
355377
void this._postTask(() => markAndDispatch(target, new TouchEvent('touchend', { ...init, touches: [], targetTouches: [], changedTouches: [touch] })));
356378
} catch {

packages/playwright-core/src/server/webkit/webview/wvInput.ts

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import * as input from '../../input';
1919

2020
import type * as types from '../../types';
21-
import type { WVSession } from './wvConnection';
21+
import type * as frames from '../../frames';
22+
import type { Func1 } from '../../javascript';
2223
import type { Progress } from '../../progress';
24+
import type { WVPage } from './wvPage';
2325

2426
function modifierFlags(modifiers: Set<types.KeyboardModifier>) {
2527
return {
@@ -51,11 +53,16 @@ function toButtonsMask(buttons: Set<types.MouseButton>): number {
5153
return mask;
5254
}
5355

56+
async function evaluateInFrame<Arg>(progress: Progress, frame: frames.Frame, pageFunction: Func1<Arg, void>, arg: Arg): Promise<void> {
57+
const context = await progress.race(frame.mainContext());
58+
await progress.race(context.evaluate(pageFunction, arg));
59+
}
60+
5461
export class RawKeyboardImpl implements input.RawKeyboard {
55-
private _session: WVSession | undefined;
62+
private _page: WVPage;
5663

57-
setSession(session: WVSession) {
58-
this._session = session;
64+
constructor(page: WVPage) {
65+
this._page = page;
5966
}
6067

6168
async keydown(progress: Progress, modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
@@ -65,31 +72,33 @@ export class RawKeyboardImpl implements input.RawKeyboard {
6572
...modifierFlags(modifiers),
6673
...(text ? { text } : {}),
6774
};
68-
await callWebViewInput(progress, this._session, 'keydown', params);
75+
const frame = await this._page.deepestFocusedFrame(progress);
76+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.keydown(p), params);
6977
}
7078

7179
async keyup(progress: Progress, modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription): Promise<void> {
7280
const { code, keyCode, key, location } = description;
7381
const params = { code, key, keyCode, location, ...modifierFlags(modifiers) };
74-
await callWebViewInput(progress, this._session, 'keyup', params);
82+
const frame = await this._page.deepestFocusedFrame(progress);
83+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.keyup(p), params);
7584
}
7685

7786
async sendText(progress: Progress, text: string): Promise<void> {
78-
await callWebViewInput(progress, this._session, 'insertText', text);
87+
const frame = await this._page.deepestFocusedFrame(progress);
88+
await evaluateInFrame(progress, frame, t => (globalThis as any).__pwWebViewInput.insertText(t), text);
7989
}
8090
}
8191

8292
export class RawMouseImpl implements input.RawMouse {
83-
private _session: WVSession | undefined;
93+
private _page: WVPage;
8494

85-
setSession(session: WVSession) {
86-
this._session = session;
95+
constructor(page: WVPage) {
96+
this._page = page;
8797
}
8898

8999
async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
90-
await callWebViewInput(progress, this._session, 'mouseMove', {
91-
x, y, button: buttonToNumber(button), buttons: toButtonsMask(buttons), ...modifierFlags(modifiers),
92-
});
100+
const { frame, point } = await this._page.deepestFrameForPoint(progress, x, y);
101+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.mouseMove(p), { ...point, button: buttonToNumber(button), buttons: toButtonsMask(buttons), ...modifierFlags(modifiers) });
93102
}
94103

95104
async down(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
@@ -114,43 +123,27 @@ export class RawMouseImpl implements input.RawMouse {
114123
}
115124

116125
async wheel(progress: Progress, x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
117-
await callWebViewInput(progress, this._session, 'wheel', { x, y, deltaX, deltaY, ...modifierFlags(modifiers) });
126+
const { frame, point } = await this._page.deepestFrameForPoint(progress, x, y);
127+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.wheel(p), { ...point, deltaX, deltaY, ...modifierFlags(modifiers) });
118128
}
119129

120130
private async _mouseEvent(progress: Progress, type: string, x: number, y: number, button: number, buttons: number, modifiers: Set<types.KeyboardModifier>, clickCount: number) {
121-
await callWebViewInput(progress, this._session, 'mouseEvent', {
122-
type, x, y, button, buttons, clickCount, ...modifierFlags(modifiers),
131+
const { frame, point } = await this._page.deepestFrameForPoint(progress, x, y);
132+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.mouseEvent(p), {
133+
type, ...point, button, buttons, clickCount, ...modifierFlags(modifiers),
123134
});
124135
}
125136
}
126137

127138
export class RawTouchscreenImpl implements input.RawTouchscreen {
128-
private _session: WVSession | undefined;
139+
private _page: WVPage;
129140

130-
setSession(session: WVSession) {
131-
this._session = session;
141+
constructor(page: WVPage) {
142+
this._page = page;
132143
}
133144

134145
async tap(progress: Progress, x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
135-
await callWebViewInput(progress, this._session, 'tap', { x, y, ...modifierFlags(modifiers) });
136-
}
137-
}
138-
139-
async function callWebViewInput(progress: Progress, session: WVSession | undefined, method: string, arg: any): Promise<void> {
140-
if (!session)
141-
throw new Error('Page is not initialized');
142-
const expression = `window.__pwWebViewInput.${method}(${JSON.stringify(arg)})`;
143-
// Some dispatchers are async — they spread events across event-loop tasks the
144-
// way a real device does. Await the returned promise so the action only
145-
// resolves once every event has been delivered. Stock WebKit's Runtime.evaluate
146-
// has no awaitPromise option, so use the separate Runtime.awaitPromise command.
147-
const { result } = await progress.race(session.send('Runtime.evaluate', { expression, returnByValue: false }));
148-
// `result` is absent if evaluation failed (e.g. the frame navigated away).
149-
// Only promises carry an objectId here — every __pwWebViewInput method returns
150-
// void or a Promise — so this both awaits async dispatch and avoids leaking a
151-
// handle for the synchronous (void) case.
152-
if (result?.className === 'Promise' && result.objectId) {
153-
await progress.race(session.send('Runtime.awaitPromise', { promiseObjectId: result.objectId, returnByValue: true }));
154-
session.sendMayFail('Runtime.releaseObject', { objectId: result.objectId });
146+
const { frame, point } = await this._page.deepestFrameForPoint(progress, x, y);
147+
await evaluateInFrame(progress, frame, p => (globalThis as any).__pwWebViewInput.tap(p), { ...point, ...modifierFlags(modifiers) });
155148
}
156149
}

packages/playwright-core/src/server/webkit/webview/wvPage.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ export class WVPage implements PageDelegate {
7979
constructor(browserContext: WVBrowserContext, outerSession: WVSession, dialogEndpoint?: string) {
8080
this._outerSession = outerSession;
8181
this._dialogEndpoint = dialogEndpoint;
82-
this.rawKeyboard = new RawKeyboardImpl();
83-
this.rawMouse = new RawMouseImpl();
84-
this.rawTouchscreen = new RawTouchscreenImpl();
82+
this.rawKeyboard = new RawKeyboardImpl(this);
83+
this.rawMouse = new RawMouseImpl(this);
84+
this.rawTouchscreen = new RawTouchscreenImpl(this);
8585
this._contextIdToContext = new Map();
8686
this._page = new Page(this, browserContext);
8787
this._workers = new WVWorkers(this._page, outerSession);
@@ -192,9 +192,6 @@ export class WVPage implements PageDelegate {
192192
private _setSession(session: WVSession) {
193193
eventsHelper.removeEventListeners(this._sessionListeners);
194194
this._session = session;
195-
this.rawKeyboard.setSession(session);
196-
this.rawMouse.setSession(session);
197-
this.rawTouchscreen.setSession(session);
198195
this._workers.setSession(session);
199196
this._addSessionListeners();
200197
}
@@ -917,6 +914,56 @@ export class WVPage implements PageDelegate {
917914
async inputActionEpilogue(): Promise<void> {
918915
}
919916

917+
async deepestFrameForPoint(progress: Progress, x: number, y: number): Promise<{ frame: frames.Frame, point: types.Point }> {
918+
const path = await this.framePointerPath(progress, x, y);
919+
return path[path.length - 1];
920+
}
921+
922+
async deepestFocusedFrame(progress: Progress): Promise<frames.Frame> {
923+
let frame: frames.Frame = this._page.mainFrame();
924+
for (;;) {
925+
const context = await progress.race(frame.mainContext());
926+
const iframe = await progress.race(context.evaluateHandle(() => (globalThis as any).__pwWebViewInput.activeIFrame())) as dom.ElementHandle;
927+
const childFrame = await this._childFrameAndDispose(progress, iframe);
928+
if (!childFrame)
929+
break;
930+
frame = childFrame;
931+
}
932+
return frame;
933+
}
934+
935+
// Walk from the main frame into the deepest <iframe> that contains the point,
936+
// recording each frame and the point translated into that frame's coordinates.
937+
async framePointerPath(progress: Progress, x: number, y: number): Promise<{ frame: frames.Frame, point: types.Point }[]> {
938+
const path: { frame: frames.Frame, point: types.Point }[] = [];
939+
let frame: frames.Frame = this._page.mainFrame();
940+
let point: types.Point = { x, y };
941+
for (;;) {
942+
path.push({ frame, point });
943+
const context = await progress.race(frame.mainContext());
944+
const position = await progress.race(context.evaluateHandle(p => (globalThis as any).__pwWebViewInput.positionInIFrame(p.x, p.y), point));
945+
try {
946+
const iframe = await position.getProperty(progress, 'iframe') as dom.ElementHandle;
947+
const childFrame = await this._childFrameAndDispose(progress, iframe);
948+
if (!childFrame)
949+
break;
950+
frame = childFrame;
951+
point = await progress.race(position.evaluate(result => ({ x: result.x, y: result.y })));
952+
} finally {
953+
position.dispose();
954+
}
955+
}
956+
return path;
957+
}
958+
959+
private async _childFrameAndDispose(progress: Progress, iframe: dom.ElementHandle): Promise<frames.Frame | null> {
960+
try {
961+
return await progress.race(this.getContentFrame(iframe));
962+
} finally {
963+
iframe.dispose();
964+
}
965+
}
966+
920967
async resetForReuse(progress: Progress): Promise<void> {
921968
}
922969

0 commit comments

Comments
 (0)