From 5c818368a597c55e5a93943fe3fbf31b4fc0e6c0 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Fri, 22 Feb 2019 00:04:25 -0800 Subject: [PATCH] feat(firefox): implement page.exposeFunction (#4052) --- experimental/puppeteer-firefox/lib/Page.js | 88 ++++++++++++++++++++- experimental/puppeteer-firefox/package.json | 2 +- lib/Page.js | 2 +- test/page.spec.js | 2 +- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/experimental/puppeteer-firefox/lib/Page.js b/experimental/puppeteer-firefox/lib/Page.js index 8bcd400981078..bc733a1bb892e 100644 --- a/experimental/puppeteer-firefox/lib/Page.js +++ b/experimental/puppeteer-firefox/lib/Page.js @@ -1,4 +1,4 @@ -const {helper} = require('./helper'); +const {helper, debugError} = require('./helper'); const {Keyboard, Mouse} = require('./Input'); const {Dialog} = require('./Dialog'); const {TimeoutError} = require('./Errors'); @@ -46,6 +46,8 @@ class Page extends EventEmitter { this._keyboard = new Keyboard(session); this._mouse = new Mouse(session, this._keyboard); this._closed = false; + /** @type {!Map} */ + this._pageBindings = new Map(); this._networkManager = new NetworkManager(session); this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings); this._networkManager.setFrameManager(this._frameManager); @@ -53,6 +55,7 @@ class Page extends EventEmitter { helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), helper.addEventListener(this._session, 'Page.console', this._onConsole.bind(this)), helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), + helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), helper.addEventListener(this._frameManager, Events.FrameManager.Load, () => this.emit(Events.Page.Load)), helper.addEventListener(this._frameManager, Events.FrameManager.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)), helper.addEventListener(this._frameManager, Events.FrameManager.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)), @@ -81,6 +84,89 @@ class Page extends EventEmitter { await this._networkManager.setExtraHTTPHeaders(headers); } + /** + * @param {string} name + * @param {Function} puppeteerFunction + */ + async exposeFunction(name, puppeteerFunction) { + if (this._pageBindings.has(name)) + throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); + this._pageBindings.set(name, puppeteerFunction); + + const expression = helper.evaluationString(addPageBinding, name); + await this._session.send('Page.addBinding', {name: name}); + await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: expression}); + await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError))); + + function addPageBinding(bindingName) { + const binding = window[bindingName]; + window[bindingName] = (...args) => { + const me = window[bindingName]; + let callbacks = me['callbacks']; + if (!callbacks) { + callbacks = new Map(); + me['callbacks'] = callbacks; + } + const seq = (me['lastSeq'] || 0) + 1; + me['lastSeq'] = seq; + const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); + binding(JSON.stringify({name: bindingName, seq, args})); + return promise; + }; + } + } + + /** + * @param {!Protocol.Runtime.bindingCalledPayload} event + */ + async _onBindingCalled(event) { + const {name, seq, args} = JSON.parse(event.payload); + let expression = null; + try { + const result = await this._pageBindings.get(name)(...args); + expression = helper.evaluationString(deliverResult, name, seq, result); + } catch (error) { + if (error instanceof Error) + expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); + else + expression = helper.evaluationString(deliverErrorValue, name, seq, error); + } + this._session.send('Page.evaluate', { script: expression, executionContextId: event.frameId }).catch(debugError); + + /** + * @param {string} name + * @param {number} seq + * @param {*} result + */ + function deliverResult(name, seq, result) { + window[name]['callbacks'].get(seq).resolve(result); + window[name]['callbacks'].delete(seq); + } + + /** + * @param {string} name + * @param {number} seq + * @param {string} message + * @param {string} stack + */ + function deliverError(name, seq, message, stack) { + const error = new Error(message); + error.stack = stack; + window[name]['callbacks'].get(seq).reject(error); + window[name]['callbacks'].delete(seq); + } + + /** + * @param {string} name + * @param {number} seq + * @param {*} value + */ + function deliverErrorValue(name, seq, value) { + window[name]['callbacks'].get(seq).reject(value); + window[name]['callbacks'].delete(seq); + } + } + /** * @param {(string|Function)} urlOrPredicate * @param {!{timeout?: number}=} options diff --git a/experimental/puppeteer-firefox/package.json b/experimental/puppeteer-firefox/package.json index 035dcc69f97c4..ec7d8e9d451e2 100644 --- a/experimental/puppeteer-firefox/package.json +++ b/experimental/puppeteer-firefox/package.json @@ -9,7 +9,7 @@ "node": ">=8.9.4" }, "puppeteer": { - "firefox_revision": "e78e4cefab9d40e70bb80b3e649dcba7a7c8ee8f" + "firefox_revision": "f8e2e3a2e86cd47766cd839624b3f08e093c1f27" }, "scripts": { "install": "node install.js", diff --git a/lib/Page.js b/lib/Page.js index 89f343b9c5156..b2704fdca13c8 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -435,7 +435,7 @@ class Page extends EventEmitter { function addPageBinding(bindingName) { const binding = window[bindingName]; - window[bindingName] = async(...args) => { + window[bindingName] = (...args) => { const me = window[bindingName]; let callbacks = me['callbacks']; if (!callbacks) { diff --git a/test/page.spec.js b/test/page.spec.js index 8fe8a7f56b99f..57b55eed02b79 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -538,7 +538,7 @@ module.exports.addTests = function({testRunner, expect, headless, Errors, Device }); }); - describe_fails_ffox('Page.exposeFunction', function() { + describe('Page.exposeFunction', function() { it('should work', async({page, server}) => { await page.exposeFunction('compute', function(a, b) { return a * b;