Skip to content

Commit 7ecf0b7

Browse files
kblokclaude
andauthored
Implement upstream PR #14784: console event on WebWorkers (#3408)
* Implement upstream PR #14784: console event on WebWorkers Ports puppeteer/puppeteer#14784 to PuppeteerSharp. Adds Console event to WebWorker, wired up in CdpWebWorker and BidiWorkerRealm. Adds DedicatedWorkerRealm.Log event filtered by realm ID. Adds 11 console tests under Workers > console describe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove invalid event access from subclass; fix describe chain - Remove if (Console != null) check in CdpWebWorker since events can only be accessed from their declaring class (CS0070) - Fix PuppeteerTest describe chain from 'Workers > console' to 'Workers console' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix CS0070, CS0103, SA1204 in BidiWorkerRealm - Add WebDriverBiDi.Script using for RemoteValueType - Remove invalid Console null-check (CS0070: can only be done in declaring class) - Move static methods before instance methods (SA1204) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 025bfd8 commit 7ecf0b7

6 files changed

Lines changed: 335 additions & 10 deletions

File tree

lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3933,6 +3933,13 @@
39333933
],
39343934
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
39353935
},
3936+
{
3937+
"testIdPattern": "[worker.spec] Workers console should return remote objects",
3938+
"platforms": ["darwin", "linux", "win32"],
3939+
"parameters": ["webDriverBiDi"],
3940+
"expectations": ["FAIL"],
3941+
"comment": "BiDi does not support getting a Handle for log args"
3942+
},
39363943
{
39373944
"testIdPattern": "[worker.spec] Workers should report errors",
39383945
"platforms": [

lib/PuppeteerSharp.Tests/WorkerTests/PageWorkerTests.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Threading.Tasks;
23
using NUnit.Framework;
34
using PuppeteerSharp.Helpers;
@@ -11,6 +12,14 @@ public PageWorkerTests() : base()
1112
{
1213
}
1314

15+
private async Task<WebWorker> CreateWorkerAsync()
16+
{
17+
var workerCreatedTcs = new TaskCompletionSource<WebWorker>();
18+
Page.WorkerCreated += (_, e) => workerCreatedTcs.TrySetResult(e.Worker);
19+
await Page.EvaluateFunctionAsync("() => new Worker('data:text/javascript,1')");
20+
return await workerCreatedTcs.Task;
21+
}
22+
1423
[Test, PuppeteerTest("worker.spec", "Workers", "Page.workers")]
1524
[Ignore("TODO: Fix me. Too flaky")]
1625
public async Task PageWorkers()
@@ -134,6 +143,174 @@ public async Task CanBeClosed()
134143
Assert.That(await workerClosedTcs.Task, Is.SameAs(worker));
135144
}
136145

146+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work")]
147+
public async Task ConsoleShouldWork()
148+
{
149+
var worker = await CreateWorkerAsync();
150+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
151+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
152+
153+
await worker.EvaluateFunctionAsync("() => console.log('hello', 5, {foo: 'bar'})");
154+
155+
var message = await consoleTcs.Task.WithTimeout();
156+
Assert.That(message.Text, Does.Contain("hello").And.Contain("5"));
157+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Log));
158+
Assert.That(message.Args, Has.Count.EqualTo(3));
159+
Assert.That(await message.Args[0].JsonValueAsync<string>(), Is.EqualTo("hello"));
160+
Assert.That(await message.Args[1].JsonValueAsync<int>(), Is.EqualTo(5));
161+
}
162+
163+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for Error instances")]
164+
public async Task ConsoleShouldWorkForErrorInstances()
165+
{
166+
var worker = await CreateWorkerAsync();
167+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
168+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
169+
170+
await worker.EvaluateFunctionAsync("() => console.log(new Error('test error'))");
171+
172+
var message = await consoleTcs.Task.WithTimeout();
173+
Assert.That(message.Text, Does.Contain("test error").Or.EqualTo("JSHandle@error"));
174+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Log));
175+
Assert.That(message.Args, Has.Count.EqualTo(1));
176+
}
177+
178+
[Test, PuppeteerTest("worker.spec", "Workers console", "should return the first line of the error message in text()")]
179+
public async Task ConsoleShouldReturnFirstLineOfErrorMessageInText()
180+
{
181+
var worker = await CreateWorkerAsync();
182+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
183+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
184+
185+
await worker.EvaluateFunctionAsync("() => console.log(new Error('test error\\nsecond line'))");
186+
187+
var message = await consoleTcs.Task.WithTimeout();
188+
Assert.That(message.Text, Does.Contain("test error").Or.EqualTo("JSHandle@error"));
189+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Log));
190+
}
191+
192+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for console.trace")]
193+
public async Task ConsoleShouldWorkForConsoleTrace()
194+
{
195+
var worker = await CreateWorkerAsync();
196+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
197+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
198+
199+
await worker.EvaluateFunctionAsync("() => console.trace('calling console.trace')");
200+
201+
var message = await consoleTcs.Task.WithTimeout();
202+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Trace));
203+
Assert.That(message.Text, Is.EqualTo("calling console.trace"));
204+
}
205+
206+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for console.dir")]
207+
public async Task ConsoleShouldWorkForConsoleDir()
208+
{
209+
var worker = await CreateWorkerAsync();
210+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
211+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
212+
213+
await worker.EvaluateFunctionAsync("() => console.dir('calling console.dir')");
214+
215+
var message = await consoleTcs.Task.WithTimeout();
216+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Dir));
217+
Assert.That(message.Text, Is.EqualTo("calling console.dir"));
218+
}
219+
220+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for console.warn")]
221+
public async Task ConsoleShouldWorkForConsoleWarn()
222+
{
223+
var worker = await CreateWorkerAsync();
224+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
225+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
226+
227+
await worker.EvaluateFunctionAsync("() => console.warn('calling console.warn')");
228+
229+
var message = await consoleTcs.Task.WithTimeout();
230+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Warning));
231+
Assert.That(message.Text, Is.EqualTo("calling console.warn"));
232+
}
233+
234+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for console.error")]
235+
public async Task ConsoleShouldWorkForConsoleError()
236+
{
237+
var worker = await CreateWorkerAsync();
238+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
239+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
240+
241+
await worker.EvaluateFunctionAsync("() => console.error('calling console.error')");
242+
243+
var message = await consoleTcs.Task.WithTimeout();
244+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Error));
245+
Assert.That(message.Text, Is.EqualTo("calling console.error"));
246+
}
247+
248+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for console.log with promise")]
249+
public async Task ConsoleShouldWorkForConsoleLogWithPromise()
250+
{
251+
var worker = await CreateWorkerAsync();
252+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
253+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
254+
255+
await worker.EvaluateFunctionAsync("() => console.log(Promise.resolve('should not wait until resolved!'))");
256+
257+
var message = await consoleTcs.Task.WithTimeout();
258+
Assert.That(message.Type, Is.EqualTo(ConsoleType.Log));
259+
Assert.That(message.Text, Does.Contain("promise").Or.Contain("Promise"));
260+
}
261+
262+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for different console API calls with timing functions")]
263+
public async Task ConsoleShouldWorkForTimingFunctions()
264+
{
265+
var worker = await CreateWorkerAsync();
266+
var messages = new List<ConsoleMessage>();
267+
worker.Console += (_, e) => messages.Add(e.Message);
268+
269+
await worker.EvaluateFunctionAsync(@"() => {
270+
console.time('calling console.time');
271+
console.timeEnd('calling console.time');
272+
}");
273+
274+
Assert.That(messages, Has.Count.EqualTo(1));
275+
Assert.That(messages[0].Type, Is.EqualTo(ConsoleType.TimeEnd));
276+
Assert.That(messages[0].Text, Does.Contain("calling console.time"));
277+
}
278+
279+
[Test, PuppeteerTest("worker.spec", "Workers console", "should work for different console API calls with group functions")]
280+
public async Task ConsoleShouldWorkForGroupFunctions()
281+
{
282+
var worker = await CreateWorkerAsync();
283+
var messages = new List<ConsoleMessage>();
284+
worker.Console += (_, e) => messages.Add(e.Message);
285+
286+
await worker.EvaluateFunctionAsync(@"() => {
287+
console.group('calling console.group');
288+
console.groupEnd();
289+
}");
290+
291+
Assert.That(messages, Has.Count.EqualTo(2));
292+
Assert.That(messages[0].Type, Is.EqualTo(ConsoleType.StartGroup));
293+
Assert.That(messages[1].Type, Is.EqualTo(ConsoleType.EndGroup));
294+
Assert.That(messages[0].Text, Does.Contain("calling console.group"));
295+
}
296+
297+
[Test, PuppeteerTest("worker.spec", "Workers console", "should return remote objects")]
298+
public async Task ConsoleShouldReturnRemoteObjects()
299+
{
300+
var worker = await CreateWorkerAsync();
301+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
302+
worker.Console += (_, e) => consoleTcs.TrySetResult(e.Message);
303+
304+
await worker.EvaluateFunctionAsync(@"() => {
305+
globalThis.test = 1;
306+
console.log(1, 2, 3, globalThis);
307+
}");
308+
309+
var message = await consoleTcs.Task.WithTimeout();
310+
Assert.That(message.Text, Does.Contain("1 2 3"));
311+
Assert.That(message.Args, Has.Count.EqualTo(4));
312+
}
313+
137314
[Test, PuppeteerTest("worker.spec", "Workers", "should work with waitForNetworkIdle")]
138315
public async Task ShouldWorkWithWaitForNetworkIdle()
139316
{

lib/PuppeteerSharp/Bidi/BidiWorkerRealm.cs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222

2323
#if !CDP_ONLY
2424

25+
using System.Collections.Generic;
26+
using System.Linq;
2527
using System.Threading.Tasks;
2628
using PuppeteerSharp.Bidi.Core;
2729
using PuppeteerSharp.Helpers;
30+
using WebDriverBiDi.Script;
2831

2932
namespace PuppeteerSharp.Bidi;
3033

@@ -85,10 +88,99 @@ protected override void Initialize()
8588
_realm.Destroyed += (sender, args) => Dispose();
8689
_realm.Updated += (sender, args) =>
8790
{
88-
// Reset PuppeteerUtil when the realm is updated
8991
_puppeteerUtil = null;
9092
TaskManager.RerunAll();
9193
};
94+
_realm.Log += OnLog;
95+
}
96+
97+
private static ConsoleType ConvertConsoleMessageLevel(string method) => method switch
98+
{
99+
"group" => ConsoleType.StartGroup,
100+
"groupCollapsed" => ConsoleType.StartGroupCollapsed,
101+
"groupEnd" => ConsoleType.EndGroup,
102+
"log" => ConsoleType.Log,
103+
"debug" => ConsoleType.Debug,
104+
"info" => ConsoleType.Info,
105+
"error" => ConsoleType.Error,
106+
"warn" => ConsoleType.Warning,
107+
"dir" => ConsoleType.Dir,
108+
"dirxml" => ConsoleType.Dirxml,
109+
"table" => ConsoleType.Table,
110+
"trace" => ConsoleType.Trace,
111+
"clear" => ConsoleType.Clear,
112+
"assert" => ConsoleType.Assert,
113+
"profile" => ConsoleType.Profile,
114+
"profileEnd" => ConsoleType.ProfileEnd,
115+
"count" => ConsoleType.Count,
116+
"timeEnd" => ConsoleType.TimeEnd,
117+
"verbose" => ConsoleType.Verbose,
118+
"timeStamp" => ConsoleType.Timestamp,
119+
_ => ConsoleType.Log,
120+
};
121+
122+
private static ConsoleMessageLocation GetStackTraceLocation(StackTrace stackTrace)
123+
{
124+
if (stackTrace?.CallFrames?.Count > 0)
125+
{
126+
var callFrame = stackTrace.CallFrames[0];
127+
return new ConsoleMessageLocation
128+
{
129+
URL = callFrame.Url,
130+
LineNumber = (int)callFrame.LineNumber,
131+
ColumnNumber = (int)callFrame.ColumnNumber,
132+
};
133+
}
134+
135+
return null;
136+
}
137+
138+
private static IList<ConsoleMessageLocation> GetStackTrace(StackTrace stackTrace)
139+
{
140+
if (stackTrace?.CallFrames?.Count > 0)
141+
{
142+
return stackTrace.CallFrames.Select(callFrame => new ConsoleMessageLocation
143+
{
144+
URL = callFrame.Url,
145+
LineNumber = (int)callFrame.LineNumber,
146+
ColumnNumber = (int)callFrame.ColumnNumber,
147+
}).ToList();
148+
}
149+
150+
return [];
151+
}
152+
153+
private void OnLog(object sender, WebDriverBiDi.Log.EntryAddedEventArgs args)
154+
{
155+
if (args.Type != "console")
156+
{
157+
return;
158+
}
159+
160+
var handleArgs = args.Arguments?.Select(arg => (IJSHandle)CreateHandle(arg)).ToArray() ?? [];
161+
162+
var logEntryText = args.Text;
163+
var text = string.Join(
164+
" ",
165+
handleArgs.Select(arg =>
166+
{
167+
if (arg is BidiJSHandle { IsPrimitiveValue: true } jsHandle)
168+
{
169+
return BidiDeserializer.Deserialize(jsHandle.RemoteValue);
170+
}
171+
172+
if (arg is BidiJSHandle { RemoteValue.Type: RemoteValueType.Error } && !string.IsNullOrEmpty(logEntryText))
173+
{
174+
return (object)logEntryText.Split('\n')[0];
175+
}
176+
177+
return arg.ToString();
178+
})).Trim();
179+
180+
var location = GetStackTraceLocation(args.StackTrace);
181+
var stackTrace = GetStackTrace(args.StackTrace);
182+
var consoleMessage = new ConsoleMessage(ConvertConsoleMessageLevel(args.Method), text, handleArgs, location, stackTrace);
183+
_worker.OnConsole(new ConsoleEventArgs(consoleMessage));
92184
}
93185
}
94186

lib/PuppeteerSharp/Bidi/Core/DedicatedWorkerRealm.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using System.Collections.Concurrent;
2727
using System.Linq;
2828
using PuppeteerSharp.Helpers;
29+
using WebDriverBiDi.Log;
2930
using WebDriverBiDi.Script;
3031

3132
namespace PuppeteerSharp.Bidi.Core;
@@ -43,6 +44,8 @@ private DedicatedWorkerRealm(BrowsingContext context, IDedicatedWorkerOwnerRealm
4344

4445
public event EventHandler<WorkerRealmEventArgs> Worker;
4546

47+
public event EventHandler<EntryAddedEventArgs> Log;
48+
4649
public override Session Session => _owners.FirstOrDefault()?.Session;
4750

4851
public static DedicatedWorkerRealm From(BrowsingContext context, IDedicatedWorkerOwnerRealm owner, string id, string origin)
@@ -64,11 +67,17 @@ public override void Dispose()
6467

6568
private void Initialize()
6669
{
67-
// Listen to realm destruction
6870
Session.Driver.Script.OnRealmDestroyed.AddObserver(OnRealmDestroyed);
69-
70-
// Listen to nested worker creation
7171
Session.Driver.Script.OnRealmCreated.AddObserver(OnDedicatedRealmCreated);
72+
Session.LogEntryAdded += OnLogEntryAdded;
73+
}
74+
75+
private void OnLogEntryAdded(object sender, EntryAddedEventArgs args)
76+
{
77+
if (args.Source.RealmId == Id)
78+
{
79+
Log?.Invoke(this, args);
80+
}
7281
}
7382

7483
private void OnRealmDestroyed(RealmDestroyedEventArgs args)

0 commit comments

Comments
 (0)