diff --git a/atom/browser/api/lib/web-contents.js b/atom/browser/api/lib/web-contents.js index e0c16999c7..9486378264 100644 --- a/atom/browser/api/lib/web-contents.js +++ b/atom/browser/api/lib/web-contents.js @@ -58,7 +58,6 @@ let PDFPageSize = { // Following methods are mapped to webFrame. const webFrameMethods = [ - 'executeJavaScript', 'insertText', 'setZoomFactor', 'setZoomLevel', @@ -106,14 +105,26 @@ let wrapWebContents = function(webContents) { }; } + const asyncWebFrameMethods = function(requestId, method, callback, ...args) { + this.send('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args); + ipcMain.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function(event, result) { + if (callback) + callback(result); + }); + }; + // Make sure webContents.executeJavaScript would run the code only when the // webContents has been loaded. - const executeJavaScript = webContents.executeJavaScript; - webContents.executeJavaScript = function(code, hasUserGesture) { + webContents.executeJavaScript = function(code, hasUserGesture, callback) { + let requestId = getNextId(); + if (typeof hasUserGesture === "function") { + callback = hasUserGesture; + hasUserGesture = false; + } if (this.getURL() && !this.isLoading()) - return executeJavaScript.call(this, code, hasUserGesture); + return asyncWebFrameMethods.call(this, requestId, "executeJavaScript", callback, code, hasUserGesture); else - return this.once('did-finish-load', executeJavaScript.bind(this, code, hasUserGesture)); + return this.once('did-finish-load', asyncWebFrameMethods.bind(this, requestId, "executeJavaScript", callback, code, hasUserGesture)); }; // Dispatch IPC messages to the ipc module. diff --git a/atom/browser/lib/rpc-server.js b/atom/browser/lib/rpc-server.js index 20ee8fbdea..a31c6e146e 100644 --- a/atom/browser/lib/rpc-server.js +++ b/atom/browser/lib/rpc-server.js @@ -354,11 +354,17 @@ ipcMain.on('ATOM_BROWSER_GUEST_WEB_CONTENTS', function(event, guestInstanceId) { } }); -ipcMain.on('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function(event, guestInstanceId, method, ...args) { +ipcMain.on('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function(event, requestId, guestInstanceId, method, ...args) { try { let guestViewManager = require('./guest-view-manager'); let guest = guestViewManager.getGuest(guestInstanceId); - return guest[method].apply(guest, args); + if (requestId) { + const responseCallback = function(result) { + event.sender.send(`ATOM_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, result); + }; + args.push(responseCallback); + } + guest[method].apply(guest, args); } catch (error) { return event.returnValue = exceptionToMeta(error); } diff --git a/atom/renderer/api/atom_api_web_frame.cc b/atom/renderer/api/atom_api_web_frame.cc index c72882886b..e00b901bff 100644 --- a/atom/renderer/api/atom_api_web_frame.cc +++ b/atom/renderer/api/atom_api_web_frame.cc @@ -15,7 +15,7 @@ #include "native_mate/object_template_builder.h" #include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebLocalFrame.h" -#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebScriptExecutionCallback.h" #include "third_party/WebKit/public/web/WebScriptSource.h" #include "third_party/WebKit/public/web/WebSecurityPolicy.h" #include "third_party/WebKit/public/web/WebView.h" @@ -26,6 +26,34 @@ namespace atom { namespace api { +namespace { + +class ScriptExecutionCallback : public blink::WebScriptExecutionCallback { + public: + using CompletionCallback = + base::Callback& result)>; + + explicit ScriptExecutionCallback(const CompletionCallback& callback) + : callback_(callback) {} + ~ScriptExecutionCallback() {} + + void completed( + const blink::WebVector>& result) override { + if (!callback_.is_null() && !result.isEmpty() && !result[0].IsEmpty()) + // Right now only single results per frame is supported. + callback_.Run(result[0]); + delete this; + } + + private: + CompletionCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(ScriptExecutionCallback); +}; + +} // namespace + WebFrame::WebFrame() : web_frame_(blink::WebLocalFrame::frameForCurrentContext()) { } @@ -124,9 +152,14 @@ void WebFrame::ExecuteJavaScript(const base::string16& code, mate::Arguments* args) { bool has_user_gesture = false; args->GetNext(&has_user_gesture); - scoped_ptr gesture( - has_user_gesture ? new blink::WebScopedUserGesture : nullptr); - web_frame_->executeScriptAndReturnValue(blink::WebScriptSource(code)); + ScriptExecutionCallback::CompletionCallback completion_callback; + args->GetNext(&completion_callback); + scoped_ptr callback( + new ScriptExecutionCallback(completion_callback)); + web_frame_->requestExecuteScriptAndReturnValue( + blink::WebScriptSource(code), + has_user_gesture, + callback.release()); } mate::ObjectTemplateBuilder WebFrame::GetObjectTemplateBuilder( diff --git a/atom/renderer/lib/init.js b/atom/renderer/lib/init.js index 340a1ef505..166e64237d 100644 --- a/atom/renderer/lib/init.js +++ b/atom/renderer/lib/init.js @@ -36,6 +36,14 @@ electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', (event, m electron.webFrame[method].apply(electron.webFrame, args); }); +electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (event, requestId, method, args) => { + const responseCallback = function(result) { + event.sender.send(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, result); + }; + args.push(responseCallback); + electron.webFrame[method].apply(electron.webFrame, args); +}); + // Process command line arguments. var nodeIntegration = 'false'; var preloadScript = null; diff --git a/atom/renderer/lib/web-view/web-view.js b/atom/renderer/lib/web-view/web-view.js index 44fc104216..bd128f6fff 100644 --- a/atom/renderer/lib/web-view/web-view.js +++ b/atom/renderer/lib/web-view/web-view.js @@ -391,7 +391,6 @@ var registerWebViewElement = function() { 'printToPDF', ]; nonblockMethods = [ - 'executeJavaScript', 'insertCSS', 'insertText', 'send', @@ -422,7 +421,7 @@ var registerWebViewElement = function() { var args, internal; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; internal = v8Util.getHiddenValue(this, 'internal'); - return ipcRenderer.send.apply(ipcRenderer, ['ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m].concat(slice.call(args))); + return ipcRenderer.send.apply(ipcRenderer, ['ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', null, internal.guestInstanceId, m].concat(slice.call(args))); }; }; for (j = 0, len1 = nonblockMethods.length; j < len1; j++) { @@ -430,6 +429,20 @@ var registerWebViewElement = function() { proto[m] = createNonBlockHandler(m); } + proto.executeJavaScript = function(code, hasUserGesture, callback) { + var internal = v8Util.getHiddenValue(this, 'internal'); + if (typeof hasUserGesture === "function") { + callback = hasUserGesture; + hasUserGesture = false; + } + let requestId = getNextId(); + ipcRenderer.send('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', requestId, internal.guestInstanceId, "executeJavaScript", code, hasUserGesture); + ipcRenderer.once(`ATOM_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, function(event, result) { + if (callback) + callback(result); + }); + }; + // WebContents associated with this webview. proto.getWebContents = function() { var internal = v8Util.getHiddenValue(this, 'internal'); diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 31c55801aa..25597d9b6c 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -425,10 +425,12 @@ Returns a `String` representing the user agent for this web page. Injects CSS into the current web page. -### `webContents.executeJavaScript(code[, userGesture])` +### `webContents.executeJavaScript(code[, userGesture, callback])` * `code` String * `userGesture` Boolean (optional) +* `callback` Function (optional) - Called after script has been executed. + * `result` Evaluates `code` in page. diff --git a/docs/api/web-view-tag.md b/docs/api/web-view-tag.md index 4a0697ad84..fa48ef60f3 100644 --- a/docs/api/web-view-tag.md +++ b/docs/api/web-view-tag.md @@ -279,10 +279,12 @@ Returns a `String` representing the user agent for guest page. Injects CSS into the guest page. -### `.executeJavaScript(code, userGesture)` +### `.executeJavaScript(code, userGesture, callback)` * `code` String * `userGesture` Boolean - Default `false`. +* `callback` Function (optional) - Called after script has been executed. + * `result` Evaluates `code` in page. If `userGesture` is set, it will create the user gesture context in the page. HTML APIs like `requestFullScreen`, which require diff --git a/spec/api-browser-window-spec.js b/spec/api-browser-window-spec.js index 58cf57cc8d..25e92382cc 100644 --- a/spec/api-browser-window-spec.js +++ b/spec/api-browser-window-spec.js @@ -10,6 +10,7 @@ const screen = require('electron').screen; const app = remote.require('electron').app; const ipcMain = remote.require('electron').ipcMain; +const ipcRenderer = require('electron').ipcRenderer; const BrowserWindow = remote.require('electron').BrowserWindow; const isCI = remote.getGlobal('isCi'); @@ -690,4 +691,22 @@ describe('browser-window module', function() { assert.equal(fs.existsSync(serializedPath), false); }); }); + + describe('window.webContents.executeJavaScript', function() { + var expected = 'hello, world!'; + var code = '(() => \"' + expected + '\")()'; + + it('doesnt throw when no calback is provided', function() { + const result = ipcRenderer.sendSync('executeJavaScript', code, false); + assert.equal(result, 'success'); + }); + + it('returns result when calback is provided', function(done) { + ipcRenderer.send('executeJavaScript', code, true); + ipcRenderer.once('executeJavaScript-response', function(event, result) { + assert.equal(result, expected); + done(); + }); + }); + }); }); diff --git a/spec/static/main.js b/spec/static/main.js index 48fdf17c3d..9a049e3f10 100644 --- a/spec/static/main.js +++ b/spec/static/main.js @@ -138,4 +138,15 @@ app.on('ready', function() { }); event.returnValue = "done"; }); + + ipcMain.on('executeJavaScript', function(event, code, hasCallback) { + if (hasCallback) { + window.webContents.executeJavaScript(code, (result) => { + window.webContents.send('executeJavaScript-response', result); + }); + } else { + window.webContents.executeJavaScript(code); + event.returnValue = "success"; + } + }); }); diff --git a/spec/webview-spec.js b/spec/webview-spec.js index d73a177d09..cd2be69d73 100644 --- a/spec/webview-spec.js +++ b/spec/webview-spec.js @@ -194,6 +194,7 @@ describe(' tag', function() { document.body.appendChild(webview); }); }); + describe('partition attribute', function() { it('inserts no node symbols when not set', function(done) { webview.addEventListener('console-message', function(e) { @@ -356,6 +357,7 @@ describe(' tag', function() { document.body.appendChild(webview); }); }); + describe('did-navigate-in-page event', function() { it('emits when an anchor link is clicked', function(done) { var p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html'); @@ -556,7 +558,7 @@ describe(' tag', function() { done(); }; var listener2 = function() { - var jsScript = 'document.getElementsByTagName("video")[0].webkitRequestFullScreen()'; + var jsScript = "document.querySelector('video').webkitRequestFullscreen()"; webview.executeJavaScript(jsScript, true); webview.removeEventListener('did-finish-load', listener2); }; @@ -565,6 +567,20 @@ describe(' tag', function() { webview.src = "file://" + fixtures + "/pages/fullscreen.html"; document.body.appendChild(webview); }); + + it('can return the result of the executed script', function(done) { + var listener = function() { + var jsScript = "'4'+2"; + webview.executeJavaScript(jsScript, false, function(result) { + assert.equal(result, '42'); + done(); + }); + webview.removeEventListener('did-finish-load', listener); + }; + webview.addEventListener('did-finish-load', listener); + webview.src = "about:blank"; + document.body.appendChild(webview); + }); }); describe('sendInputEvent', function() {