From 95f80c9645a7102640cc3b57e19f3e1d228872a1 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Fri, 3 Apr 2026 16:59:21 -0700 Subject: [PATCH 1/2] feat: emit Page.javascriptDialogOpening CDP events for JS dialogs window.alert(), confirm(), and prompt() now dispatch a javascript_dialog_opening notification that the CDP layer forwards as a Page.javascriptDialogOpening event. This enables Puppeteer's page.on('dialog') to fire when JS dialogs open. Also adds Page.handleJavaScriptDialog as a CDP method. Dialogs still auto-dismiss in headless mode (alert is void, confirm returns false, prompt returns null), so handleJavaScriptDialog is an acknowledgement rather than a blocking gate. Changes: - Notification.zig: add JavascriptDialogOpening event type - CDP.zig: register listener, forward to page domain - page.zig: handleJavaScriptDialog handler + event emitter - Window.zig: alert/confirm/prompt dispatch the notification Fixes #2082 Ref #2043 --- src/Notification.zig | 8 ++++++++ src/browser/webapi/Window.zig | 24 ++++++++++++++++++++---- src/cdp/CDP.zig | 6 ++++++ src/cdp/domains/page.zig | 28 ++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/Notification.zig b/src/Notification.zig index 6adbfd81..e7bf1e33 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -83,6 +83,7 @@ const EventListeners = struct { http_request_auth_required: List = .{}, http_response_data: List = .{}, http_response_header_done: List = .{}, + javascript_dialog_opening: List = .{}, }; const Events = union(enum) { @@ -102,6 +103,7 @@ const Events = union(enum) { http_request_done: *const RequestDone, http_response_data: *const ResponseData, http_response_header_done: *const ResponseHeaderDone, + javascript_dialog_opening: *const JavascriptDialogOpening, }; const EventType = std.meta.FieldEnum(Events); @@ -185,6 +187,12 @@ pub const RequestFail = struct { err: anyerror, }; +pub const JavascriptDialogOpening = struct { + url: [:0]const u8, + message: []const u8, + dialog_type: []const u8, +}; + pub fn init(allocator: Allocator) !*Notification { const notification = try allocator.create(Notification); errdefer allocator.destroy(notification); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index fb3ec8f8..582ed659 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -903,15 +903,31 @@ pub const JsApi = struct { pub const opener = bridge.property(null, .{ .template = false }); pub const alert = bridge.function(struct { - fn alert(_: *const Window, _: ?[]const u8) void {} - }.alert, .{ .noop = true }); + fn alert(_: *const Window, message: ?[]const u8, page: *Page) void { + page._session.notification.dispatch(.javascript_dialog_opening, &.{ + .url = page.url, + .message = message orelse "", + .dialog_type = "alert", + }); + } + }.alert, .{}); pub const confirm = bridge.function(struct { - fn confirm(_: *const Window, _: ?[]const u8) bool { + fn confirm(_: *const Window, message: ?[]const u8, page: *Page) bool { + page._session.notification.dispatch(.javascript_dialog_opening, &.{ + .url = page.url, + .message = message orelse "", + .dialog_type = "confirm", + }); return false; } }.confirm, .{}); pub const prompt = bridge.function(struct { - fn prompt(_: *const Window, _: ?[]const u8, _: ?[]const u8) ?[]const u8 { + fn prompt(_: *const Window, message: ?[]const u8, _: ?[]const u8, page: *Page) ?[]const u8 { + page._session.notification.dispatch(.javascript_dialog_opening, &.{ + .url = page.url, + .message = message orelse "", + .dialog_type = "prompt", + }); return null; } }.prompt, .{}); diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 09fa8324..89efcef1 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -431,6 +431,7 @@ pub const BrowserContext = struct { try notification.register(.page_frame_created, self, onPageFrameCreated); try notification.register(.page_dom_content_loaded, self, onPageDOMContentLoaded); try notification.register(.page_loaded, self, onPageLoaded); + try notification.register(.javascript_dialog_opening, self, onJavascriptDialogOpening); } pub fn deinit(self: *BrowserContext) void { @@ -641,6 +642,11 @@ pub const BrowserContext = struct { return @import("domains/page.zig").pageLoaded(self, msg); } + pub fn onJavascriptDialogOpening(ctx: *anyopaque, msg: *const Notification.JavascriptDialogOpening) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").javascriptDialogOpening(self, msg); + } + pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { const self: *BrowserContext = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index cf3cdd7d..d5fb8c7b 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -48,6 +48,7 @@ pub fn processMessage(cmd: *CDP.Command) !void { close, captureScreenshot, getLayoutMetrics, + handleJavaScriptDialog, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -63,6 +64,7 @@ pub fn processMessage(cmd: *CDP.Command) !void { .close => return close(cmd), .captureScreenshot => return captureScreenshot(cmd), .getLayoutMetrics => return getLayoutMetrics(cmd), + .handleJavaScriptDialog => return handleJavaScriptDialog(cmd), } } @@ -642,6 +644,32 @@ fn sendPageLifecycle(bc: *CDP.BrowserContext, name: []const u8, timestamp: u64, }, .{ .session_id = session_id }); } +// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-handleJavaScriptDialog +fn handleJavaScriptDialog(cmd: *CDP.Command) !void { + // Dialogs auto-dismiss in headless mode, so this is an acknowledgement. + // accept and promptText params are parsed but not used since the dialog + // already returned by the time the CDP client sends this. + _ = try cmd.params(struct { + accept: bool, + promptText: ?[]const u8 = null, + }); + try cmd.sendResult(null, .{}); +} + +// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-javascriptDialogOpening +pub fn javascriptDialogOpening(bc: anytype, event: *const Notification.JavascriptDialogOpening) !void { + const session_id = bc.session_id orelse return; + var cdp = bc.cdp; + + try cdp.sendEvent("Page.javascriptDialogOpening", .{ + .url = event.url, + .message = event.message, + .type = event.dialog_type, + .hasBrowserHandler = false, + .defaultPrompt = "", + }, .{ .session_id = session_id }); +} + const LifecycleEvent = struct { frameId: []const u8, loaderId: ?[]const u8, From 7208934bda254fde89b1229716f1999b159a8884 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Mon, 6 Apr 2026 11:08:27 -0700 Subject: [PATCH 2/2] fix: return CDP error from handleJavaScriptDialog instead of silent no-op Dialogs auto-dismiss in headless mode, so there is no pending dialog by the time the CDP client sends Page.handleJavaScriptDialog. Return an explicit error so the client knows the action had no effect. --- src/cdp/domains/page.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index d5fb8c7b..c306ead7 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -646,14 +646,14 @@ fn sendPageLifecycle(bc: *CDP.BrowserContext, name: []const u8, timestamp: u64, // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-handleJavaScriptDialog fn handleJavaScriptDialog(cmd: *CDP.Command) !void { - // Dialogs auto-dismiss in headless mode, so this is an acknowledgement. - // accept and promptText params are parsed but not used since the dialog - // already returned by the time the CDP client sends this. + // Dialogs auto-dismiss in headless mode. By the time the CDP client + // sends this command, the dialog has already returned and there is + // no pending dialog to accept or dismiss. _ = try cmd.params(struct { accept: bool, promptText: ?[]const u8 = null, }); - try cmd.sendResult(null, .{}); + return cmd.sendError(-32000, "No dialog is showing", .{}); } // https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-javascriptDialogOpening