feat: optional typically sync callback for WebFrame#executeJavaScript* (#21423)
This commit is contained in:
		
					parent
					
						
							
								748a917ffd
							
						
					
				
			
			
				commit
				
					
						84126a4f23
					
				
			
		
					 3 changed files with 197 additions and 23 deletions
				
			
		|  | @ -122,13 +122,20 @@ by its key, which is returned from `webFrame.insertCSS(css)`. | ||||||
| 
 | 
 | ||||||
| Inserts `text` to the focused element. | Inserts `text` to the focused element. | ||||||
| 
 | 
 | ||||||
| ### `webFrame.executeJavaScript(code[, userGesture])` | ### `webFrame.executeJavaScript(code[, userGesture, callback])` | ||||||
| 
 | 
 | ||||||
| * `code` String | * `code` String | ||||||
| * `userGesture` Boolean (optional) - Default is `false`. | * `userGesture` Boolean (optional) - Default is `false`. | ||||||
|  | * `callback` Function (optional) - Called after script has been executed. Unless | ||||||
|  |   the frame is suspended (e.g. showing a modal alert), execution will be | ||||||
|  |   synchronous and the callback will be invoked before the method returns. For | ||||||
|  |   compatibility with an older version of this method, the error parameter is | ||||||
|  |   second. | ||||||
|  |   * `result` Any | ||||||
|  |   * `error` Error | ||||||
| 
 | 
 | ||||||
| Returns `Promise<any>` - A promise that resolves with the result of the executed code | Returns `Promise<any>` - A promise that resolves with the result of the executed | ||||||
| or is rejected if the result of the code is a rejected promise. | code or is rejected if execution throws or results in a rejected promise. | ||||||
| 
 | 
 | ||||||
| Evaluates `code` in page. | Evaluates `code` in page. | ||||||
| 
 | 
 | ||||||
|  | @ -136,14 +143,24 @@ In the browser window some HTML APIs like `requestFullScreen` can only be | ||||||
| invoked by a gesture from the user. Setting `userGesture` to `true` will remove | invoked by a gesture from the user. Setting `userGesture` to `true` will remove | ||||||
| this limitation. | this limitation. | ||||||
| 
 | 
 | ||||||
| ### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture])` | ### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])` | ||||||
| 
 | 
 | ||||||
| * `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature.  You can provide any integer here. | * `worldId` Integer - The ID of the world to run the javascript | ||||||
|  |             in, `0` is the default main world (where content runs), `999` is the | ||||||
|  |             world used by Electron's `contextIsolation` feature. Accepts values | ||||||
|  |             in the range 1..536870911. | ||||||
| * `scripts` [WebSource[]](structures/web-source.md) | * `scripts` [WebSource[]](structures/web-source.md) | ||||||
| * `userGesture` Boolean (optional) - Default is `false`. | * `userGesture` Boolean (optional) - Default is `false`. | ||||||
|  | * `callback` Function (optional) - Called after script has been executed. Unless | ||||||
|  |   the frame is suspended (e.g. showing a modal alert), execution will be | ||||||
|  |   synchronous and the callback will be invoked before the method returns.  For | ||||||
|  |   compatibility with an older version of this method, the error parameter is | ||||||
|  |   second. | ||||||
|  |   * `result` Any | ||||||
|  |   * `error` Error | ||||||
| 
 | 
 | ||||||
| Returns `Promise<any>` - A promise that resolves with the result of the executed code | Returns `Promise<any>` - A promise that resolves with the result of the executed | ||||||
| or is rejected if the result of the code is a rejected promise. | code or is rejected if execution throws or results in a rejected promise. | ||||||
| 
 | 
 | ||||||
| Works like `executeJavaScript` but evaluates `scripts` in an isolated context. | Works like `executeJavaScript` but evaluates `scripts` in an isolated context. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| #include "services/service_manager/public/cpp/interface_provider.h" | #include "services/service_manager/public/cpp/interface_provider.h" | ||||||
| #include "shell/common/api/api.mojom.h" | #include "shell/common/api/api.mojom.h" | ||||||
| #include "shell/common/gin_converters/blink_converter.h" | #include "shell/common/gin_converters/blink_converter.h" | ||||||
|  | #include "shell/common/gin_converters/callback_converter.h" | ||||||
| #include "shell/common/gin_helper/dictionary.h" | #include "shell/common/gin_helper/dictionary.h" | ||||||
| #include "shell/common/gin_helper/promise.h" | #include "shell/common/gin_helper/promise.h" | ||||||
| #include "shell/common/node_includes.h" | #include "shell/common/node_includes.h" | ||||||
|  | @ -110,32 +111,58 @@ class RenderFrameStatus final : public content::RenderFrameObserver { | ||||||
| 
 | 
 | ||||||
| class ScriptExecutionCallback : public blink::WebScriptExecutionCallback { | class ScriptExecutionCallback : public blink::WebScriptExecutionCallback { | ||||||
|  public: |  public: | ||||||
|  |   // for compatibility with the older version of this, error is after result
 | ||||||
|  |   using CompletionCallback = | ||||||
|  |       base::OnceCallback<void(const v8::Local<v8::Value>& result, | ||||||
|  |                               const v8::Local<v8::Value>& error)>; | ||||||
|  | 
 | ||||||
|   explicit ScriptExecutionCallback( |   explicit ScriptExecutionCallback( | ||||||
|       gin_helper::Promise<v8::Local<v8::Value>> promise) |       gin_helper::Promise<v8::Local<v8::Value>> promise, | ||||||
|       : promise_(std::move(promise)) {} |       CompletionCallback callback) | ||||||
|  |       : promise_(std::move(promise)), callback_(std::move(callback)) {} | ||||||
|  | 
 | ||||||
|   ~ScriptExecutionCallback() override = default; |   ~ScriptExecutionCallback() override = default; | ||||||
| 
 | 
 | ||||||
|   void Completed( |   void Completed( | ||||||
|       const blink::WebVector<v8::Local<v8::Value>>& result) override { |       const blink::WebVector<v8::Local<v8::Value>>& result) override { | ||||||
|  |     v8::Isolate* isolate = v8::Isolate::GetCurrent(); | ||||||
|     if (!result.empty()) { |     if (!result.empty()) { | ||||||
|       if (!result[0].IsEmpty()) { |       if (!result[0].IsEmpty()) { | ||||||
|         // Right now only single results per frame is supported.
 |         // Right now only single results per frame is supported.
 | ||||||
|  |         if (!callback_.is_null()) | ||||||
|  |           std::move(callback_).Run(result[0], v8::Undefined(isolate)); | ||||||
|         promise_.Resolve(result[0]); |         promise_.Resolve(result[0]); | ||||||
|       } else { |       } else { | ||||||
|         promise_.RejectWithErrorMessage( |         const char* error_message = | ||||||
|             "Script failed to execute, this normally means an error " |             "Script failed to execute, this normally means an error " | ||||||
|             "was thrown. Check the renderer console for the error."); |             "was thrown. Check the renderer console for the error."; | ||||||
|  |         if (!callback_.is_null()) { | ||||||
|  |           std::move(callback_).Run( | ||||||
|  |               v8::Undefined(isolate), | ||||||
|  |               v8::Exception::Error( | ||||||
|  |                   v8::String::NewFromUtf8(isolate, error_message) | ||||||
|  |                       .ToLocalChecked())); | ||||||
|  |         } | ||||||
|  |         promise_.RejectWithErrorMessage(error_message); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       promise_.RejectWithErrorMessage( |       const char* error_message = | ||||||
|           "WebFrame was removed before script could run. This normally means " |           "WebFrame was removed before script could run. This normally means " | ||||||
|           "the underlying frame was destroyed"); |           "the underlying frame was destroyed"; | ||||||
|  |       if (!callback_.is_null()) { | ||||||
|  |         std::move(callback_).Run( | ||||||
|  |             v8::Undefined(isolate), | ||||||
|  |             v8::Exception::Error(v8::String::NewFromUtf8(isolate, error_message) | ||||||
|  |                                      .ToLocalChecked())); | ||||||
|  |       } | ||||||
|  |       promise_.RejectWithErrorMessage(error_message); | ||||||
|     } |     } | ||||||
|     delete this; |     delete this; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  private: |  private: | ||||||
|   gin_helper::Promise<v8::Local<v8::Value>> promise_; |   gin_helper::Promise<v8::Local<v8::Value>> promise_; | ||||||
|  |   CompletionCallback callback_; | ||||||
| 
 | 
 | ||||||
|   DISALLOW_COPY_AND_ASSIGN(ScriptExecutionCallback); |   DISALLOW_COPY_AND_ASSIGN(ScriptExecutionCallback); | ||||||
| }; | }; | ||||||
|  | @ -373,9 +400,14 @@ v8::Local<v8::Promise> ExecuteJavaScript(gin_helper::Arguments* args, | ||||||
|   bool has_user_gesture = false; |   bool has_user_gesture = false; | ||||||
|   args->GetNext(&has_user_gesture); |   args->GetNext(&has_user_gesture); | ||||||
| 
 | 
 | ||||||
|  |   ScriptExecutionCallback::CompletionCallback completion_callback; | ||||||
|  |   args->GetNext(&completion_callback); | ||||||
|  | 
 | ||||||
|   GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptAndReturnValue( |   GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptAndReturnValue( | ||||||
|       blink::WebScriptSource(blink::WebString::FromUTF16(code)), |       blink::WebScriptSource(blink::WebString::FromUTF16(code)), | ||||||
|       has_user_gesture, new ScriptExecutionCallback(std::move(promise))); |       has_user_gesture, | ||||||
|  |       new ScriptExecutionCallback(std::move(promise), | ||||||
|  |                                   std::move(completion_callback))); | ||||||
| 
 | 
 | ||||||
|   return handle; |   return handle; | ||||||
| } | } | ||||||
|  | @ -389,6 +421,16 @@ v8::Local<v8::Promise> ExecuteJavaScriptInIsolatedWorld( | ||||||
|   gin_helper::Promise<v8::Local<v8::Value>> promise(isolate); |   gin_helper::Promise<v8::Local<v8::Value>> promise(isolate); | ||||||
|   v8::Local<v8::Promise> handle = promise.GetHandle(); |   v8::Local<v8::Promise> handle = promise.GetHandle(); | ||||||
| 
 | 
 | ||||||
|  |   bool has_user_gesture = false; | ||||||
|  |   args->GetNext(&has_user_gesture); | ||||||
|  | 
 | ||||||
|  |   blink::WebLocalFrame::ScriptExecutionType scriptExecutionType = | ||||||
|  |       blink::WebLocalFrame::kSynchronous; | ||||||
|  |   args->GetNext(&scriptExecutionType); | ||||||
|  | 
 | ||||||
|  |   ScriptExecutionCallback::CompletionCallback completion_callback; | ||||||
|  |   args->GetNext(&completion_callback); | ||||||
|  | 
 | ||||||
|   std::vector<blink::WebScriptSource> sources; |   std::vector<blink::WebScriptSource> sources; | ||||||
| 
 | 
 | ||||||
|   for (const auto& script : scripts) { |   for (const auto& script : scripts) { | ||||||
|  | @ -399,7 +441,15 @@ v8::Local<v8::Promise> ExecuteJavaScriptInIsolatedWorld( | ||||||
|     script.Get("startLine", &start_line); |     script.Get("startLine", &start_line); | ||||||
| 
 | 
 | ||||||
|     if (!script.Get("code", &code)) { |     if (!script.Get("code", &code)) { | ||||||
|       promise.RejectWithErrorMessage("Invalid 'code'"); |       const char* error_message = "Invalid 'code'"; | ||||||
|  |       if (!completion_callback.is_null()) { | ||||||
|  |         std::move(completion_callback) | ||||||
|  |             .Run(v8::Undefined(isolate), | ||||||
|  |                  v8::Exception::Error( | ||||||
|  |                      v8::String::NewFromUtf8(isolate, error_message) | ||||||
|  |                          .ToLocalChecked())); | ||||||
|  |       } | ||||||
|  |       promise.RejectWithErrorMessage(error_message); | ||||||
|       return handle; |       return handle; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -408,19 +458,14 @@ v8::Local<v8::Promise> ExecuteJavaScriptInIsolatedWorld( | ||||||
|                                blink::WebURL(GURL(url)), start_line)); |                                blink::WebURL(GURL(url)), start_line)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   bool has_user_gesture = false; |  | ||||||
|   args->GetNext(&has_user_gesture); |  | ||||||
| 
 |  | ||||||
|   blink::WebLocalFrame::ScriptExecutionType scriptExecutionType = |  | ||||||
|       blink::WebLocalFrame::kSynchronous; |  | ||||||
|   args->GetNext(&scriptExecutionType); |  | ||||||
| 
 |  | ||||||
|   // Debugging tip: if you see a crash stack trace beginning from this call,
 |   // Debugging tip: if you see a crash stack trace beginning from this call,
 | ||||||
|   // then it is very likely that some exception happened when executing the
 |   // then it is very likely that some exception happened when executing the
 | ||||||
|   // "content_script/init.js" script.
 |   // "content_script/init.js" script.
 | ||||||
|   GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptInIsolatedWorld( |   GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptInIsolatedWorld( | ||||||
|       world_id, &sources.front(), sources.size(), has_user_gesture, |       world_id, &sources.front(), sources.size(), has_user_gesture, | ||||||
|       scriptExecutionType, new ScriptExecutionCallback(std::move(promise))); |       scriptExecutionType, | ||||||
|  |       new ScriptExecutionCallback(std::move(promise), | ||||||
|  |                                   std::move(completion_callback))); | ||||||
| 
 | 
 | ||||||
|   return handle; |   return handle; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,4 +25,116 @@ describe('webFrame module', function () { | ||||||
|   it('findFrameByRoutingId() does not crash when not found', () => { |   it('findFrameByRoutingId() does not crash when not found', () => { | ||||||
|     expect(webFrame.findFrameByRoutingId(-1)).to.be.null() |     expect(webFrame.findFrameByRoutingId(-1)).to.be.null() | ||||||
|   }) |   }) | ||||||
|  | 
 | ||||||
|  |   describe('executeJavaScript', () => { | ||||||
|  |     let childFrameElement, childFrame | ||||||
|  | 
 | ||||||
|  |     before(() => { | ||||||
|  |       childFrameElement = document.createElement('iframe') | ||||||
|  |       document.body.appendChild(childFrameElement) | ||||||
|  |       childFrame = webFrame.firstChild | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     after(() => { | ||||||
|  |       childFrameElement.remove() | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('executeJavaScript() yields results via a promise and a sync callback', done => { | ||||||
|  |       let callbackResult, callbackError | ||||||
|  | 
 | ||||||
|  |       childFrame | ||||||
|  |         .executeJavaScript('1 + 1', (result, error) => { | ||||||
|  |           callbackResult = result | ||||||
|  |           callbackError = error | ||||||
|  |         }) | ||||||
|  |         .then( | ||||||
|  |           promiseResult => { | ||||||
|  |             expect(promiseResult).to.equal(2) | ||||||
|  |             done() | ||||||
|  |           }, | ||||||
|  |           promiseError => { | ||||||
|  |             done(promiseError) | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |       expect(callbackResult).to.equal(2) | ||||||
|  |       expect(callbackError).to.be.undefined() | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('executeJavaScriptInIsolatedWorld() yields results via a promise and a sync callback', done => { | ||||||
|  |       let callbackResult, callbackError | ||||||
|  | 
 | ||||||
|  |       childFrame | ||||||
|  |         .executeJavaScriptInIsolatedWorld(999, [{ code: '1 + 1' }], (result, error) => { | ||||||
|  |           callbackResult = result | ||||||
|  |           callbackError = error | ||||||
|  |         }) | ||||||
|  |         .then( | ||||||
|  |           promiseResult => { | ||||||
|  |             expect(promiseResult).to.equal(2) | ||||||
|  |             done() | ||||||
|  |           }, | ||||||
|  |           promiseError => { | ||||||
|  |             done(promiseError) | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |       expect(callbackResult).to.equal(2) | ||||||
|  |       expect(callbackError).to.be.undefined() | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('executeJavaScript() yields errors via a promise and a sync callback', done => { | ||||||
|  |       let callbackResult, callbackError | ||||||
|  | 
 | ||||||
|  |       childFrame | ||||||
|  |         .executeJavaScript('thisShouldProduceAnError()', (result, error) => { | ||||||
|  |           callbackResult = result | ||||||
|  |           callbackError = error | ||||||
|  |         }) | ||||||
|  |         .then( | ||||||
|  |           promiseResult => { | ||||||
|  |             done(new Error('error is expected')) | ||||||
|  |           }, | ||||||
|  |           promiseError => { | ||||||
|  |             expect(promiseError).to.be.an('error') | ||||||
|  |             done() | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |       expect(callbackResult).to.be.undefined() | ||||||
|  |       expect(callbackError).to.be.an('error') | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     // executeJavaScriptInIsolatedWorld is failing to detect exec errors and is neither
 | ||||||
|  |     // rejecting nor passing the error to the callback. This predates the reintroduction
 | ||||||
|  |     // of the callback so will not be fixed as part of the callback PR
 | ||||||
|  |     // if/when this is fixed the test can be uncommented.
 | ||||||
|  |     //
 | ||||||
|  |     // it('executeJavaScriptInIsolatedWorld() yields errors via a promise and a sync callback', done => {
 | ||||||
|  |     //   let callbackResult, callbackError
 | ||||||
|  |     //
 | ||||||
|  |     //   childFrame
 | ||||||
|  |     //     .executeJavaScriptInIsolatedWorld(999, [{ code: 'thisShouldProduceAnError()' }], (result, error) => {
 | ||||||
|  |     //       callbackResult = result
 | ||||||
|  |     //       callbackError = error
 | ||||||
|  |     //     })
 | ||||||
|  |     //     .then(
 | ||||||
|  |     //       promiseResult => {
 | ||||||
|  |     //         done(new Error('error is expected'))
 | ||||||
|  |     //       },
 | ||||||
|  |     //       promiseError => {
 | ||||||
|  |     //         expect(promiseError).to.be.an('error')
 | ||||||
|  |     //         done()
 | ||||||
|  |     //       }
 | ||||||
|  |     //     )
 | ||||||
|  |     //
 | ||||||
|  |     //   expect(callbackResult).to.be.undefined()
 | ||||||
|  |     //   expect(callbackError).to.be.an('error')
 | ||||||
|  |     // })
 | ||||||
|  | 
 | ||||||
|  |     it('executeJavaScript(InIsolatedWorld) can be used without a callback', async () => { | ||||||
|  |       expect(await webFrame.executeJavaScript('1 + 1')).to.equal(2) | ||||||
|  |       expect(await webFrame.executeJavaScriptInIsolatedWorld(999, [{ code: '1 + 1' }])).to.equal(2) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 bughit
				bughit