Skip to content

Commit 401d6fc

Browse files
committed
fix(webview): route exposeFunctions callbacks to the originating frame
WebKit has no `Runtime.addBinding`, so the webview delegate has to handle bindings through `console.debug(BINDING_CALL_TAG, payload)` instead the console message carries no execution context id, so every call was dispatched against the main frame, meaning a callback fired from a child frame (e.g. `frame.evaluate(..., { exposeFunctions: true })`) reached the wrong `BindingsController` and never resolved the calling frame's pending promise, hanging until the 30s timeout tore the page down tag each call with an empty object created in the calling context, as an `objectId` carries the injected script id, meaning that the host can route the call to the frame it came from and release the marker afterwards
1 parent 129717a commit 401d6fc

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ export class WVPage implements PageDelegate {
538538
const [bindingName, bindingArg] = parameters;
539539

540540
if (bindingName.value === BINDING_CALL_TAG && bindingArg.type === 'string') {
541-
const context = [...this._contextIdToContext.values()].find(c => c.frame === this._page.mainFrame());
541+
const context = this._bindingCallContext(parameters);
542542
if (context)
543543
this._page.onBindingCalled(bindingArg.value, context).catch(e => debugLogger.log('error', e));
544544
return;
@@ -609,6 +609,15 @@ export class WVPage implements PageDelegate {
609609
this._onConsoleRepeatCountUpdated({ count: 1, timestamp: event.message.timestamp });
610610
}
611611

612+
private _bindingCallContext(parameters: Protocol.Runtime.RemoteObject[]): dom.FrameExecutionContext | undefined {
613+
const contextObject = parameters[2];
614+
if (contextObject?.objectId) {
615+
this._session.sendMayFail('Runtime.releaseObject', { objectId: contextObject.objectId });
616+
return this._contextIdToContext.get(JSON.parse(contextObject.objectId).injectedScriptId);
617+
}
618+
return [...this._contextIdToContext.values()].find(c => c.frame === this._page.mainFrame());
619+
}
620+
612621
_onConsoleRepeatCountUpdated(event: Protocol.Console.messageRepeatCountUpdatedPayload) {
613622
if (this._lastConsoleMessage) {
614623
const {
@@ -1261,7 +1270,10 @@ const bindingBridgeSource = `
12611270
Object.defineProperty(window, '${PageBinding.kBindingName}', {
12621271
configurable: true,
12631272
writable: false,
1264-
value: function(payload) { console.debug('${BINDING_CALL_TAG}', payload); },
1273+
value: function(payload) {
1274+
const contextObject = {};
1275+
console.debug('${BINDING_CALL_TAG}', payload, contextObject);
1276+
},
12651277
});
12661278
}
12671279
`;

‎tests/page/page-evaluate-callback.spec.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ it('should work in a child frame', async ({ page, server }) => {
132132
expect(received).toEqual([42]);
133133
});
134134

135+
it('should route callbacks back to the calling frame', async ({ page, server }) => {
136+
await page.goto(server.EMPTY_PAGE);
137+
const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE);
138+
const greet = async (where: string) => `hello ${where}`;
139+
const [fromMain, fromChild] = await Promise.all([
140+
page.evaluate(async ({ cb }) => await cb('main'), { cb: greet }, { exposeFunctions: true }),
141+
frame.evaluate(async ({ cb }) => await cb('child'), { cb: greet }, { exposeFunctions: true }),
142+
]);
143+
expect(fromMain).toBe('hello main');
144+
expect(fromChild).toBe('hello child');
145+
});
146+
135147
it('should work with jsHandle.evaluate', async ({ page }) => {
136148
const handle = await page.evaluateHandle(() => window);
137149
const received: number[] = [];

0 commit comments

Comments
 (0)