Repository: electron/electron (language: cpp)

Issue:
feat: expose frame and stack trace in the console-message event
Notes: Added a new `details` object to the `console-message` event that contains frame and stack trace information


Discussion / hints:
> I don't think we should stop passing the message/line/etc. to this function. It's not hard to maintain and it's annoying to break existing usages of this event.

We aren't stopping that? This removes the documentation of those parameters but we still emit them, existing code will not break
TypeScript code would raise type errors if you tried to type those parameters with the old types, right? It would be pretty easy to update your callback to fix the issue, but if it breaks type checking that's probably a breaking change right?
I don't think typescript breakages are considered breaking changes if the underlying JS code still works.  Otherwise every type fix we ever make is a "breaking change" because `tsc` suddenly isn't happy anymore.
If type definitions change in a way that's likely to require a code change to get TypeScript to compile, it doesn't seem unreasonable to treat that as a breaking change and at least document it so it's easier to anticipate the work that will be required in an Electron upgrade.

If the current norm is to treat them as non-breaking, then that's fine for this PR, but I may start a broader discussion about whether documenting type breaks would be useful for helping apps stay up-to-date 🙂
> If the current norm is to treat them as non-breaking

This is definitely how I've treated them and I can give a good example as to why.

Take the following change

```typescript
// Before
interface Options {
  a: string;
}

// After
interface Options {
  a: 'foo' | 'bar';
}
```

In this case we made a docs improvement that resulted in a type improvement.  However for the following code

```typescript
let a = 'foo';
const foo: Options = {
    a,
}
```

The type checker would be happy before, and unhappy after.  This is no way is a breaking change, but typescript is now unhappy 🤷 IMO there is no way we should ever call that out as a breaking change, that's **why** typescript exists.  It exists to identify bugs, a **fix** in the types that identifies more bugs / type issues is **not** a breaking change.  It's a fix.

Admittedly the scenario in this PR is different in that the type checker isn't complaining because of a bug it's complaining because of an API surface change but the logic still stands.  It's a _fix_ in the docs to point people at the new way of using the event, not a breaking change in the API itself.

Fix patch:
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index e76ecf0587fb8..000c6b9a766bb 100644
--- a/docs/api/web-contents.md
+++ b/docs/api/web-contents.md
@@ -955,11 +955,17 @@ Emitted when a `<webview>` has been attached to this web contents.
 
 Returns:
 
-* `event` Event
-* `level` Integer - The log level, from 0 to 3. In order it matches `verbose`, `info`, `warning` and `error`.
-* `message` string - The actual console message
-* `line` Integer - The line number of the source that triggered this console message
-* `sourceId` string
+* `details` Event\<\>
+  * `message` string - Message text
+  * `level` string - Message severity
+    Possible values include `info`, `warning`, `error`, and `debug`.
+  * `lineNumber` Integer - Line number in the log source
+  * `sourceId` string - URL of the log source
+  * `frame` WebFrameMain - Frame that logged the message
+* `level` Integer _Deprecated_ - The log level, from 0 to 3. In order it matches `verbose`, `info`, `warning` and `error`.
+* `message` string _Deprecated_ - The actual console message
+* `line` Integer _Deprecated_ - The line number of the source that triggered this console message
+* `sourceId` string _Deprecated_
 
 Emitted when the associated window logs a console message.
 
diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md
index d8dcb2e98ec50..9e32e6e8b4500 100644
--- a/docs/breaking-changes.md
+++ b/docs/breaking-changes.md
@@ -12,6 +12,23 @@ This document uses the following convention to categorize breaking changes:
 * **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
 * **Removed:** An API or feature was removed, and is no longer supported by Electron.
 
+## Planned Breaking API Changes (34.0)
+
+### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`
+
+The `console-message` event on `WebContents` has been updated to provide details on the `Event`
+argument.
+
+```js
+// Deprecated
+webContents.on('console-message', (event, level, message, line, sourceId) => {})
+
+// Replace with:
+webContents.on('console-message', ({ level, message, lineNumber, sourceId, frame }) => {})
+```
+
+Additionally, `level` is now a string with possible values of `info`, `warning`, `error`, and `debug`.
+
 ## Planned Breaking API Changes (33.0)
 
 ### Behavior Changed: frame properties may retrieve detached WebFrameMain instances or none at all
diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts
index ed519db1e8a9d..55b78302bd3fc 100644
--- a/lib/browser/api/web-contents.ts
+++ b/lib/browser/api/web-contents.ts
@@ -549,6 +549,8 @@ WebContents.prototype.goToOffset = function (index: number) {
   return this._goToOffset(index);
 };
 
+const consoleMessageDeprecated = deprecate.warnOnceMessage('\'console-message\' arguments are deprecated and will be removed. Please use Event<WebContentsConsoleMessageEventParams> object instead.');
+
 // Add JavaScript wrappers for WebContents class.
 WebContents.prototype._init = function () {
   const prefs = this.getLastWebPreferences() || {};
@@ -909,6 +911,15 @@ WebContents.prototype._init = function () {
     openDialogs.clear();
   });
 
+  // TODO(samuelmaddock): remove deprecated 'console-message' arguments
+  this.on('-console-message' as any, (event: Electron.Event<Electron.WebContentsConsoleMessageEventParams>) => {
+    const hasDeprecatedListener = this.listeners('console-message').some(listener => listener.length > 1);
+    if (hasDeprecatedListener) {
+      consoleMessageDeprecated();
+    }
+    this.emit('console-message', event, (event as any)._level, event.message, event.lineNumber, event.sourceId);
+  });
+
   app.emit('web-contents-created', { sender: this, preventDefault () {}, get defaultPrevented () { return false; } }, this);
 
   // Properties
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index 1cbd5502a653f..fb8d3ff541b7a 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -1068,14 +1068,31 @@ void WebContents::Close(std::optional<gin_helper::Dictionary> options) {
   }
 }
 
-bool WebContents::DidAddMessageToConsole(
-    content::WebContents* source,
+void WebContents::OnDidAddMessageToConsole(
+    content::RenderFrameHost* source_frame,
     blink::mojom::ConsoleMessageLevel level,
     const std::u16string& message,
     int32_t line_no,
-    const std::u16string& source_id) {
-  return Emit("console-message", static_cast<int32_t>(level), message, line_no,
-              source_id);
+    const std::u16string& source_id,
+    const std::optional<std::u16string>& untrusted_stack_trace) {
+  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+  v8::HandleScope handle_scope(isolate);
+
+  gin::Handle<gin_helper::internal::Event> event =
+      gin_helper::internal::Event::New(isolate);
+  v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
+
+  gin_helper::Dictionary dict(isolate, event_object);
+  dict.SetGetter("frame", source_frame);
+  dict.Set("level", level);
+  dict.Set("message", message);
+  dict.Set("lineNumber", line_no);
+  dict.Set("sourceId", source_id);
+
+  // TODO(samuelmaddock): Delete when deprecated arguments are fully removed.
+  dict.Set("_level", static_cast<int32_t>(level));
+
+  EmitWithoutEvent("-console-message", event);
 }
 
 void WebContents::OnCreateWindow(
diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h
index 44252d7ba56c0..fd8fbc065b061 100644
--- a/shell/browser/api/electron_api_web_contents.h
+++ b/shell/browser/api/electron_api_web_contents.h
@@ -530,11 +530,6 @@ class WebContents final : public ExclusiveAccessContext,
 #endif
 
   // content::WebContentsDelegate:
-  bool DidAddMessageToConsole(content::WebContents* source,
-                              blink::mojom::ConsoleMessageLevel level,
-                              const std::u16string& message,
-                              int32_t line_no,
-                              const std::u16string& source_id) override;
   bool IsWebContentsCreationOverridden(
       content::SiteInstance* source_site_instance,
       content::mojom::WindowContainerType window_container_type,
@@ -680,6 +675,13 @@ class WebContents final : public ExclusiveAccessContext,
       content::RenderWidgetHost* render_widget_host) override;
   void OnWebContentsLostFocus(
       content::RenderWidgetHost* render_widget_host) override;
+  void OnDidAddMessageToConsole(
+      content::RenderFrameHost* source_frame,
+      blink::mojom::ConsoleMessageLevel level,
+      const std::u16string& message,
+      int32_t line_no,
+      const std::u16string& source_id,
+      const std::optional<std::u16string>& untrusted_stack_trace) override;
 
   // InspectableWebContentsDelegate:
   void DevToolsReloadPage() override;
diff --git a/shell/common/gin_converters/blink_converter.cc b/shell/common/gin_converters/blink_converter.cc
index c0d2648e085e5..5098599e98232 100644
--- a/shell/common/gin_converters/blink_converter.cc
+++ b/shell/common/gin_converters/blink_converter.cc
@@ -28,6 +28,7 @@
 #include "third_party/blink/public/common/input/web_mouse_event.h"
 #include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
 #include "third_party/blink/public/common/widget/device_emulation_params.h"
+#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
 #include "ui/base/clipboard/clipboard.h"
 #include "ui/events/blink/blink_event_util.h"
@@ -699,4 +700,18 @@ bool Converter<blink::CloneableMessage>::FromV8(v8::Isolate* isolate,
   return electron::SerializeV8Value(isolate, val, out);
 }
 
+// static
+v8::Local<v8::Value> Converter<blink::mojom::ConsoleMessageLevel>::ToV8(
+    v8::Isolate* isolate,
+    const blink::mojom::ConsoleMessageLevel& in) {
+  using Val = blink::mojom::ConsoleMessageLevel;
+  static constexpr auto Lookup = base::MakeFixedFlatMap<Val, std::string_view>({
+      {Val::kVerbose, "debug"},
+      {Val::kInfo, "info"},
+      {Val::kWarning, "warning"},
+      {Val::kError, "error"},
+  });
+  return StringToV8(isolate, Lookup.at(in));
+}
+
 }  // namespace gin
diff --git a/shell/common/gin_converters/blink_converter.h b/shell/common/gin_converters/blink_converter.h
index 1dd38e9de4af2..f293181c4a6be 100644
--- a/shell/common/gin_converters/blink_converter.h
+++ b/shell/common/gin_converters/blink_converter.h
@@ -10,6 +10,7 @@
 #include "third_party/blink/public/common/input/web_input_event.h"
 #include "third_party/blink/public/common/messaging/cloneable_message.h"
 #include "third_party/blink/public/common/web_cache/web_cache_resource_type_stats.h"
+#include "third_party/blink/public/mojom/devtools/console_message.mojom-forward.h"
 #include "third_party/blink/public/mojom/loader/referrer.mojom-forward.h"
 
 namespace blink {
@@ -126,6 +127,12 @@ struct Converter<blink::CloneableMessage> {
                      blink::CloneableMessage* out);
 };
 
+template <>
+struct Converter<blink::mojom::ConsoleMessageLevel> {
+  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
+                                   const blink::mojom::ConsoleMessageLevel& in);
+};
+
 v8::Local<v8::Value> EditFlagsToV8(v8::Isolate* isolate, int editFlags);
 v8::Local<v8::Value> MediaFlagsToV8(v8::Isolate* isolate, int mediaFlags);
 
diff --git a/spec/api-protocol-spec.ts b/spec/api-protocol-spec.ts
index 1ebcd855fd34d..300706a647ba1 100644
--- a/spec/api-protocol-spec.ts
+++ b/spec/api-protocol-spec.ts
@@ -977,7 +977,7 @@ describe('protocol module', () => {
         contextIsolation: false
       });
       const consoleMessages: string[] = [];
-      newContents.on('console-message', (e, level, message) => consoleMessages.push(message));
+      newContents.on('console-message', (e) => consoleMessages.push(e.message));
       try {
         newContents.loadURL(standardScheme + '://fake-host');
         const [, response] = await once(ipcMain, 'response');
@@ -1631,7 +1631,7 @@ describe('protocol module', () => {
       defer(() => { protocol.unhandle('cors'); });
 
       await contents.loadFile(path.resolve(fixturesPath, 'pages', 'base-page.html'));
-      contents.on('console-message', (e, level, message) => console.log(message));
+      contents.on('console-message', (e) => console.log(e.message));
       const ok = await contents.executeJavaScript(`(async () => {
         function wait(milliseconds) {
           return new Promise((resolve) => setTimeout(resolve, milliseconds));
diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts
index 2e584c309c7f0..b074c31dab000 100644
--- a/spec/api-web-contents-spec.ts
+++ b/spec/api-web-contents-spec.ts
@@ -1965,9 +1965,9 @@ describe('webContents module', () => {
     afterEach(closeAllWindows);
     it('is triggered with correct log message', (done) => {
       const w = new BrowserWindow({ show: true });
-      w.webContents.on('console-message', (e, level, message) => {
+      w.webContents.on('console-message', (e) => {
         // Don't just assert as Chromium might emit other logs that we should ignore.
-        if (message === 'a') {
+        if (e.message === 'a') {
           done();
         }
       });
diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts
index ce7f6e5023ddc..049504001bbc9 100644
--- a/spec/chromium-spec.ts
+++ b/spec/chromium-spec.ts
@@ -379,7 +379,7 @@ describe('web security', () => {
                 console.log(e.message)
               }
             </script>`);
-              const [,, message] = await once(w.webContents, 'console-message');
+              const [{ message }] = await once(w.webContents, 'console-message');
               expect(message).to.match(/Refused to evaluate a string/);
             });
 
@@ -399,7 +399,7 @@ describe('web security', () => {
                 console.log(e.message)
               }
             </script>`);
-              const [,, message] = await once(w.webContents, 'console-message');
+              const [{ message }] = await once(w.webContents, 'console-message');
               expect(message).to.equal('true');
             });
 
@@ -1428,7 +1428,7 @@ describe('chromium features', () => {
       w.loadURL('about:blank');
       w.webContents.executeJavaScript('window.child = window.open(); child.opener = null');
       const [, { webContents }] = await once(app, 'browser-window-created');
-      const [,, message] = await once(webContents, 'console-message');
+      const [{ message }] = await once(webContents, 'console-message');
       expect(message).to.equal('{"require":"function","module":"object","exports":"object","process":"object","Buffer":"function"}');
     });
 
diff --git a/spec/extensions-spec.ts b/spec/extensions-spec.ts
index 6d78104cde961..a8cea93bbe06d 100644
--- a/spec/extensions-spec.ts
+++ b/spec/extensions-spec.ts
@@ -111,7 +111,7 @@ describe('chrome extensions', () => {
       const message = { method: 'query' };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-      const [,, responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const response = JSON.parse(responseString);
 
       expect(response).to.have.lengthOf(1);
@@ -434,7 +434,7 @@ describe('chrome extensions', () => {
       const message = { method: 'executeScript', args: ['1 + 2'] };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-      const [, , responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const response = JSON.parse(responseString);
 
       expect(response).to.equal(3);
@@ -448,7 +448,7 @@ describe('chrome extensions', () => {
       const message = { method: 'connectTab', args: [portName] };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-      const [,, responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const response = responseString.split(',');
       expect(response[0]).to.equal(portName);
       expect(response[1]).to.equal('howdy');
@@ -461,7 +461,7 @@ describe('chrome extensions', () => {
       const message = { method: 'sendMessage', args: ['Hello World!'] };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-      const [,, responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const response = JSON.parse(responseString);
 
       expect(response.message).to.equal('Hello World!');
@@ -480,7 +480,7 @@ describe('chrome extensions', () => {
       const message = { method: 'update', args: [w2.webContents.id, { url }] };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-      const [,, responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const response = JSON.parse(responseString);
 
       await w2Navigated;
@@ -801,7 +801,7 @@ describe('chrome extensions', () => {
 
       w.webContents.executeJavaScript('window.postMessage(\'fetch-confirmation\', \'*\')');
 
-      const [, , responseString] = await once(w.webContents, 'console-message');
+      const [{ message: responseString }] = await once(w.webContents, 'console-message');
       const { message } = JSON.parse(responseString);
 
       expect(message).to.equal('Hello from background.js');
@@ -834,7 +834,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getAcceptLanguages' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
 
         expect(response).to.be.an('array').that.is.not.empty('languages array is empty');
@@ -846,7 +846,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getUILanguage' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
 
         expect(response).to.be.a('string');
@@ -858,7 +858,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getMessage' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [, , responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
 
         expect(response).to.equal('Hola mundo!!');
@@ -877,7 +877,7 @@ describe('chrome extensions', () => {
         const message = { method: 'detectLanguage', args: [greetings] };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [, , responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
 
         expect(response).to.be.an('array');
@@ -925,7 +925,7 @@ describe('chrome extensions', () => {
         const message = { method: 'isEnabled' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [, , responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.equal(false);
@@ -937,7 +937,7 @@ describe('chrome extensions', () => {
         const message = { method: 'setIcon' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [, , responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.equal(null);
@@ -949,7 +949,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getBadgeText' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [, , responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.equal('');
@@ -982,7 +982,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getZoom' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.equal(1);
@@ -994,7 +994,7 @@ describe('chrome extensions', () => {
         const message = { method: 'setZoom', args: [2] };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.deep.equal(2);
@@ -1006,7 +1006,7 @@ describe('chrome extensions', () => {
         const message = { method: 'getZoomSettings' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.deep.equal({
@@ -1022,7 +1022,7 @@ describe('chrome extensions', () => {
         const message = { method: 'setZoomSettings', args: [{ mode: 'disabled' }] };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
         const response = JSON.parse(responseString);
         expect(response).to.deep.equal({
@@ -1039,7 +1039,7 @@ describe('chrome extensions', () => {
           const message = { method: 'get' };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [,, responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
 
           const response = JSON.parse(responseString);
           expect(response).to.have.property('url').that.is.a('string');
@@ -1065,7 +1065,7 @@ describe('chrome extensions', () => {
           await w.loadURL(url);
 
           w.webContents.executeJavaScript('window.postMessage(\'{}\', \'*\')');
-          const [,, responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const response = JSON.parse(responseString);
           expect(response).not.to.have.property('url');
           expect(response).not.to.have.property('title');
@@ -1092,7 +1092,7 @@ describe('chrome extensions', () => {
         const consoleMessage = once(w.webContents, 'console-message');
         const finish = once(w.webContents, 'did-finish-load');
 
-        await Promise.all([consoleMessage, finish]).then(([[,, responseString]]) => {
+        await Promise.all([consoleMessage, finish]).then(([[{ message: responseString }]]) => {
           const response = JSON.parse(responseString);
           expect(response.status).to.equal('reloaded');
         });
@@ -1105,7 +1105,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ muted: true }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [,, responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const response = JSON.parse(responseString);
 
           expect(response).to.have.property('mutedInfo').that.is.a('object');
@@ -1122,7 +1122,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ url: 'chrome://crash' }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [,, responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const { error } = JSON.parse(responseString);
           expect(error).to.eq('I\'m sorry. I\'m afraid I can\'t do that.');
         });
@@ -1133,7 +1133,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ url: 'chrome://crash' }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [,, responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const { error } = JSON.parse(responseString);
           expect(error).to.eq('I\'m sorry. I\'m afraid I can\'t do that.');
         });
@@ -1144,7 +1144,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ url: 'devtools://blah' }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [, , responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const { error } = JSON.parse(responseString);
           expect(error).to.eq('Cannot navigate to a devtools:// page without either the devtools or debugger permission.');
         });
@@ -1155,7 +1155,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ url: 'chrome-untrusted://blah' }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [, , responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const { error } = JSON.parse(responseString);
           expect(error).to.eq('Cannot navigate to a chrome-untrusted:// page.');
         });
@@ -1166,7 +1166,7 @@ describe('chrome extensions', () => {
           const message = { method: 'update', args: [{ url: 'file://blah' }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [, , responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const { error } = JSON.parse(responseString);
           expect(error).to.eq('Cannot navigate to a file URL without local file access.');
         });
@@ -1183,7 +1183,7 @@ describe('chrome extensions', () => {
           const message = { method: 'query', args: [{ muted: true }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [, , responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const response = JSON.parse(responseString);
           expect(response).to.have.lengthOf(1);
 
@@ -1220,7 +1220,7 @@ describe('chrome extensions', () => {
           const message = { method: 'query', args: [{ muted: true }] };
           w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-          const [, , responseString] = await once(w.webContents, 'console-message');
+          const [{ message: responseString }] = await once(w.webContents, 'console-message');
           const response = JSON.parse(responseString);
           expect(response).to.have.lengthOf(2);
           for (const tab of response) {
@@ -1270,7 +1270,7 @@ describe('chrome extensions', () => {
         const message = { method: 'registerContentScripts' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
         expect(response).to.be.an('array').with.lengthOf(1);
         expect(response[0]).to.deep.equal({
@@ -1290,7 +1290,7 @@ describe('chrome extensions', () => {
 
         const message = { method: 'globalParams' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
         expect(response).to.deep.equal({ changed: true });
       });
@@ -1304,7 +1304,7 @@ describe('chrome extensions', () => {
         const message = { method: 'insertCSS' };
         w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
-        const [,, responseString] = await once(w.webContents, 'console-message');
+        const [{ message: responseString }] = await once(w.webContents, 'console-message');
         const response = JSON.parse(responseString);
         expect(response.success).to.be.true();
 
diff --git a/spec/fixtures/api/context-bridge/context-bridge-mutability/main.js b/spec/fixtures/api/context-bridge/context-bridge-mutability/main.js
index 41e4ac1222f6f..7eb4cf264e234 100644
--- a/spec/fixtures/api/context-bridge/context-bridge-mutability/main.js
+++ b/spec/fixtures/api/context-bridge/context-bridge-mutability/main.js
@@ -13,8 +13,8 @@ app.whenReady().then(function () {
 
   win.loadFile('index.html');
 
-  win.webContents.on('console-message', (event, level, message) => {
-    console.log(message);
+  win.webContents.on('console-message', (event) => {
+    console.log(event.message);
   });
 
   win.webContents.on('did-finish-load', () => app.quit());
diff --git a/spec/security-warnings-spec.ts b/spec/security-warnings-spec.ts
index 19e563b41dbf4..3ca40ef325f3b 100644
--- a/spec/security-warnings-spec.ts
+++ b/spec/security-warnings-spec.ts
@@ -75,7 +75,7 @@ describe('security warnings', () => {
     });
 
     w.loadURL(`${serverUrl}/base-page-security.html`);
-    const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+    const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
     expect(message).to.include('Node.js Integration with Remote Content');
   });
 
@@ -88,7 +88,7 @@ describe('security warnings', () => {
     });
 
     w.loadURL(`${serverUrl}/base-page-security-onload-message.html`);
-    const [,, message] = await emittedUntil(w.webContents, 'console-message', isLoaded);
+    const [{ message }] = await emittedUntil(w.webContents, 'console-message', isLoaded);
     expect(message).to.not.include('Node.js Integration with Remote Content');
   });
 
@@ -104,7 +104,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/base-page-security.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('Disabled webSecurity');
       });
 
@@ -116,7 +116,7 @@ describe('security warnings', () => {
 
         useCsp = false;
         w.loadURL(`${serverUrl}/base-page-security.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('Insecure Content-Security-Policy');
       });
 
@@ -146,7 +146,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/base-page-security.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('allowRunningInsecureContent');
       });
 
@@ -160,7 +160,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/base-page-security.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('experimentalFeatures');
       });
 
@@ -174,7 +174,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/base-page-security.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('enableBlinkFeatures');
       });
 
@@ -185,7 +185,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/webview-allowpopups.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('allowpopups');
       });
 
@@ -196,7 +196,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/insecure-resources.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.include('Insecure Resources');
       });
 
@@ -207,7 +207,7 @@ describe('security warnings', () => {
         });
 
         w.loadURL(`${serverUrl}/insecure-resources.html`);
-        const [,, message] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
+        const [{ message }] = await emittedUntil(w.webContents, 'console-message', messageContainsSecurityWarning);
         expect(message).to.not.include('insecure-resources.html');
       });
     });


Test patch: