From 85bc005cd64ddcddb030360c532cc37d8d53e487 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Tue, 5 Dec 2023 17:36:23 -0800 Subject: [PATCH] refactor: move JS dialog handling to JS (#40598) --- filenames.gni | 2 - lib/browser/api/web-contents.ts | 54 +++++- .../browser/api/electron_api_web_contents.cc | 60 +++++- shell/browser/api/electron_api_web_contents.h | 19 +- .../electron_javascript_dialog_manager.cc | 137 -------------- .../electron_javascript_dialog_manager.h | 51 ----- shell/browser/web_contents_preferences.cc | 3 + spec/chromium-spec.ts | 176 +++++++++++++++++- 8 files changed, 302 insertions(+), 200 deletions(-) delete mode 100644 shell/browser/electron_javascript_dialog_manager.cc delete mode 100644 shell/browser/electron_javascript_dialog_manager.h diff --git a/filenames.gni b/filenames.gni index 0146238c36cf..7689578208b4 100644 --- a/filenames.gni +++ b/filenames.gni @@ -372,8 +372,6 @@ filenames = { "shell/browser/electron_download_manager_delegate.h", "shell/browser/electron_gpu_client.cc", "shell/browser/electron_gpu_client.h", - "shell/browser/electron_javascript_dialog_manager.cc", - "shell/browser/electron_javascript_dialog_manager.h", "shell/browser/electron_navigation_throttle.cc", "shell/browser/electron_navigation_throttle.h", "shell/browser/electron_permission_manager.cc", diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts index a9f5a52213b5..c7666d9b9c5d 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -1,5 +1,5 @@ -import { app, ipcMain, session, webFrameMain } from 'electron/main'; -import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron/main'; +import { app, ipcMain, session, webFrameMain, dialog } from 'electron/main'; +import type { BrowserWindowConstructorOptions, LoadURLOptions, MessageBoxOptions, WebFrameMain } from 'electron/main'; import * as url from 'url'; import * as path from 'path'; @@ -729,6 +729,56 @@ WebContents.prototype._init = function () { } }); + const originCounts = new Map(); + const openDialogs = new Set(); + this.on('-run-dialog' as any, async (info: {frame: WebFrameMain, dialogType: 'prompt' | 'confirm' | 'alert', messageText: string, defaultPromptText: string}, callback: (success: boolean, user_input: string) => void) => { + const originUrl = new URL(info.frame.url); + const origin = originUrl.protocol === 'file:' ? originUrl.href : originUrl.origin; + if ((originCounts.get(origin) ?? 0) < 0) return callback(false, ''); + + const prefs = this.getLastWebPreferences(); + if (!prefs || prefs.disableDialogs) return callback(false, ''); + + // We don't support prompt() for some reason :) + if (info.dialogType === 'prompt') return callback(false, ''); + + originCounts.set(origin, (originCounts.get(origin) ?? 0) + 1); + + // TODO: translate? + const checkbox = originCounts.get(origin)! > 1 && prefs.safeDialogs ? prefs.safeDialogsMessage || 'Prevent this app from creating additional dialogs' : ''; + const parent = this.getOwnerBrowserWindow(); + const abortController = new AbortController(); + const options: MessageBoxOptions = { + message: info.messageText, + checkboxLabel: checkbox, + signal: abortController.signal, + ...(info.dialogType === 'confirm') ? { + buttons: ['OK', 'Cancel'], + defaultId: 0, + cancelId: 1 + } : { + buttons: ['OK'], + defaultId: -1, // No default button + cancelId: 0 + } + }; + openDialogs.add(abortController); + const promise = parent && !prefs.offscreen ? dialog.showMessageBox(parent, options) : dialog.showMessageBox(options); + try { + const result = await promise; + if (abortController.signal.aborted) return; + if (result.checkboxChecked) originCounts.set(origin, -1); + return callback(result.response === 0, ''); + } finally { + openDialogs.delete(abortController); + } + }); + + this.on('-cancel-dialogs' as any, () => { + for (const controller of openDialogs) { controller.abort(); } + openDialogs.clear(); + }); + 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 bf565f2aa77e..5445faea4681 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -88,7 +88,6 @@ #include "shell/browser/electron_browser_client.h" #include "shell/browser/electron_browser_context.h" #include "shell/browser/electron_browser_main_parts.h" -#include "shell/browser/electron_javascript_dialog_manager.h" #include "shell/browser/electron_navigation_throttle.h" #include "shell/browser/file_select_helper.h" #include "shell/browser/native_window.h" @@ -263,6 +262,21 @@ struct Converter { } }; +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + content::JavaScriptDialogType val) { + switch (val) { + case content::JAVASCRIPT_DIALOG_TYPE_ALERT: + return gin::ConvertToV8(isolate, "alert"); + case content::JAVASCRIPT_DIALOG_TYPE_CONFIRM: + return gin::ConvertToV8(isolate, "confirm"); + case content::JAVASCRIPT_DIALOG_TYPE_PROMPT: + return gin::ConvertToV8(isolate, "prompt"); + } + } +}; + template <> struct Converter { static bool FromV8(v8::Isolate* isolate, @@ -1587,10 +1601,7 @@ void WebContents::RequestMediaAccessPermission( content::JavaScriptDialogManager* WebContents::GetJavaScriptDialogManager( content::WebContents* source) { - if (!dialog_manager_) - dialog_manager_ = std::make_unique(); - - return dialog_manager_.get(); + return this; } void WebContents::OnAudioStateChanged(bool audible) { @@ -3747,6 +3758,45 @@ void WebContents::OnInputEvent(const blink::WebInputEvent& event) { Emit("input-event", event); } +void WebContents::RunJavaScriptDialog(content::WebContents* web_contents, + content::RenderFrameHost* rfh, + content::JavaScriptDialogType dialog_type, + const std::u16string& message_text, + const std::u16string& default_prompt_text, + DialogClosedCallback callback, + bool* did_suppress_message) { + CHECK_EQ(web_contents, this->web_contents()); + + auto* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + auto info = gin::DataObjectBuilder(isolate) + .Set("frame", rfh) + .Set("dialogType", dialog_type) + .Set("messageText", message_text) + .Set("defaultPromptText", default_prompt_text) + .Build(); + + EmitWithoutEvent("-run-dialog", info, std::move(callback)); +} + +void WebContents::RunBeforeUnloadDialog(content::WebContents* web_contents, + content::RenderFrameHost* rfh, + bool is_reload, + DialogClosedCallback callback) { + // TODO: asyncify? + bool default_prevented = Emit("will-prevent-unload"); + std::move(callback).Run(default_prevented, std::u16string()); +} + +void WebContents::CancelDialogs(content::WebContents* web_contents, + bool reset_state) { + auto* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + EmitWithoutEvent( + "-cancel-dialogs", + gin::DataObjectBuilder(isolate).Set("resetState", reset_state).Build()); +} + v8::Local WebContents::GetProcessMemoryInfo(v8::Isolate* isolate) { gin_helper::Promise promise(isolate); v8::Local handle = promise.GetHandle(); diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index c9247f0df411..ec380f204aee 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -23,6 +23,7 @@ #include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h" #include "content/common/frame.mojom.h" #include "content/public/browser/devtools_agent_host.h" +#include "content/public/browser/javascript_dialog_manager.h" #include "content/public/browser/keyboard_event_processing_result.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/web_contents.h" @@ -85,7 +86,6 @@ class SkRegion; namespace electron { class ElectronBrowserContext; -class ElectronJavaScriptDialogManager; class InspectableWebContents; class WebContentsZoomController; class WebViewGuestDelegate; @@ -107,6 +107,7 @@ class WebContents : public ExclusiveAccessContext, public content::WebContentsObserver, public content::WebContentsDelegate, public content::RenderWidgetHost::InputEventObserver, + public content::JavaScriptDialogManager, public InspectableWebContentsDelegate, public InspectableWebContentsViewDelegate, public BackgroundThrottlingSource { @@ -453,6 +454,21 @@ class WebContents : public ExclusiveAccessContext, // content::RenderWidgetHost::InputEventObserver: void OnInputEvent(const blink::WebInputEvent& event) override; + // content::JavaScriptDialogManager: + void RunJavaScriptDialog(content::WebContents* web_contents, + content::RenderFrameHost* rfh, + content::JavaScriptDialogType dialog_type, + const std::u16string& message_text, + const std::u16string& default_prompt_text, + DialogClosedCallback callback, + bool* did_suppress_message) override; + void RunBeforeUnloadDialog(content::WebContents* web_contents, + content::RenderFrameHost* rfh, + bool is_reload, + DialogClosedCallback callback) override; + void CancelDialogs(content::WebContents* web_contents, + bool reset_state) override; + SkRegion* draggable_region() { return force_non_draggable_ ? nullptr : draggable_region_.get(); } @@ -762,7 +778,6 @@ class WebContents : public ExclusiveAccessContext, v8::Global devtools_web_contents_; v8::Global debugger_; - std::unique_ptr dialog_manager_; std::unique_ptr guest_delegate_; std::unique_ptr frame_subscriber_; diff --git a/shell/browser/electron_javascript_dialog_manager.cc b/shell/browser/electron_javascript_dialog_manager.cc deleted file mode 100644 index bf37e9014fc9..000000000000 --- a/shell/browser/electron_javascript_dialog_manager.cc +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2013 GitHub, Inc. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#include "shell/browser/electron_javascript_dialog_manager.h" - -#include -#include -#include - -#include "base/functional/bind.h" -#include "base/strings/utf_string_conversions.h" -#include "shell/browser/api/electron_api_web_contents.h" -#include "shell/browser/native_window.h" -#include "shell/browser/ui/message_box.h" -#include "shell/browser/web_contents_preferences.h" -#include "shell/common/options_switches.h" -#include "ui/gfx/image/image_skia.h" - -using content::JavaScriptDialogType; - -namespace electron { - -namespace { - -constexpr int kUserWantsNoMoreDialogs = -1; - -} // namespace - -ElectronJavaScriptDialogManager::ElectronJavaScriptDialogManager() = default; -ElectronJavaScriptDialogManager::~ElectronJavaScriptDialogManager() = default; - -void ElectronJavaScriptDialogManager::RunJavaScriptDialog( - content::WebContents* web_contents, - content::RenderFrameHost* rfh, - JavaScriptDialogType dialog_type, - const std::u16string& message_text, - const std::u16string& default_prompt_text, - DialogClosedCallback callback, - bool* did_suppress_message) { - auto origin_url = rfh->GetLastCommittedURL(); - - std::string origin; - // For file:// URLs we do the alert filtering by the - // file path currently loaded - if (origin_url.SchemeIsFile()) { - origin = origin_url.path(); - } else { - origin = origin_url.DeprecatedGetOriginAsURL().spec(); - } - - if (origin_counts_[origin] == kUserWantsNoMoreDialogs) { - return std::move(callback).Run(false, std::u16string()); - } - - if (dialog_type != JavaScriptDialogType::JAVASCRIPT_DIALOG_TYPE_ALERT && - dialog_type != JavaScriptDialogType::JAVASCRIPT_DIALOG_TYPE_CONFIRM) { - std::move(callback).Run(false, std::u16string()); - return; - } - - auto* web_preferences = WebContentsPreferences::From(web_contents); - - if (web_preferences && web_preferences->ShouldDisableDialogs()) { - return std::move(callback).Run(false, std::u16string()); - } - - // No default button - int default_id = -1; - int cancel_id = 0; - - std::vector buttons = {"OK"}; - if (dialog_type == JavaScriptDialogType::JAVASCRIPT_DIALOG_TYPE_CONFIRM) { - buttons.emplace_back("Cancel"); - // First button is default, second button is cancel - default_id = 0; - cancel_id = 1; - } - - origin_counts_[origin]++; - - std::string checkbox; - if (origin_counts_[origin] > 1 && web_preferences && - web_preferences->ShouldUseSafeDialogs() && - !web_preferences->GetSafeDialogsMessage(&checkbox)) { - checkbox = "Prevent this app from creating additional dialogs"; - } - - // Don't set parent for offscreen window. - NativeWindow* window = nullptr; - if (web_preferences && !web_preferences->IsOffscreen()) { - auto* relay = NativeWindowRelay::FromWebContents(web_contents); - if (relay) - window = relay->GetNativeWindow(); - } - - electron::MessageBoxSettings settings; - settings.parent_window = window; - settings.checkbox_label = checkbox; - settings.buttons = buttons; - settings.default_id = default_id; - settings.cancel_id = cancel_id; - settings.message = base::UTF16ToUTF8(message_text); - - electron::ShowMessageBox( - settings, - base::BindOnce(&ElectronJavaScriptDialogManager::OnMessageBoxCallback, - base::Unretained(this), std::move(callback), origin)); -} - -void ElectronJavaScriptDialogManager::RunBeforeUnloadDialog( - content::WebContents* web_contents, - content::RenderFrameHost* rfh, - bool is_reload, - DialogClosedCallback callback) { - auto* api_web_contents = api::WebContents::From(web_contents); - if (api_web_contents) { - bool default_prevented = api_web_contents->Emit("will-prevent-unload"); - std::move(callback).Run(default_prevented, std::u16string()); - } -} - -void ElectronJavaScriptDialogManager::CancelDialogs( - content::WebContents* web_contents, - bool reset_state) {} - -void ElectronJavaScriptDialogManager::OnMessageBoxCallback( - DialogClosedCallback callback, - const std::string& origin, - int code, - bool checkbox_checked) { - if (checkbox_checked) - origin_counts_[origin] = kUserWantsNoMoreDialogs; - std::move(callback).Run(code == 0, std::u16string()); -} - -} // namespace electron diff --git a/shell/browser/electron_javascript_dialog_manager.h b/shell/browser/electron_javascript_dialog_manager.h deleted file mode 100644 index 42e29d9e40e1..000000000000 --- a/shell/browser/electron_javascript_dialog_manager.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2013 GitHub, Inc. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#ifndef ELECTRON_SHELL_BROWSER_ELECTRON_JAVASCRIPT_DIALOG_MANAGER_H_ -#define ELECTRON_SHELL_BROWSER_ELECTRON_JAVASCRIPT_DIALOG_MANAGER_H_ - -#include -#include - -#include "content/public/browser/javascript_dialog_manager.h" - -namespace content { -class WebContents; -} - -namespace electron { - -class ElectronJavaScriptDialogManager - : public content::JavaScriptDialogManager { - public: - ElectronJavaScriptDialogManager(); - ~ElectronJavaScriptDialogManager() override; - - // content::JavaScriptDialogManager implementations. - void RunJavaScriptDialog(content::WebContents* web_contents, - content::RenderFrameHost* rfh, - content::JavaScriptDialogType dialog_type, - const std::u16string& message_text, - const std::u16string& default_prompt_text, - DialogClosedCallback callback, - bool* did_suppress_message) override; - void RunBeforeUnloadDialog(content::WebContents* web_contents, - content::RenderFrameHost* rfh, - bool is_reload, - DialogClosedCallback callback) override; - void CancelDialogs(content::WebContents* web_contents, - bool reset_state) override; - - private: - void OnMessageBoxCallback(DialogClosedCallback callback, - const std::string& origin, - int code, - bool checkbox_checked); - - std::map origin_counts_; -}; - -} // namespace electron - -#endif // ELECTRON_SHELL_BROWSER_ELECTRON_JAVASCRIPT_DIALOG_MANAGER_H_ diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index 8a20e5165924..8c43cd3da1f0 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -391,6 +391,9 @@ void WebContentsPreferences::SaveLastPreferences() { allow_running_insecure_content_); dict.Set(options::kExperimentalFeatures, experimental_features_); dict.Set(options::kEnableBlinkFeatures, enable_blink_features_.value_or("")); + dict.Set("disableDialogs", disable_dialogs_); + dict.Set("safeDialogs", safe_dialogs_); + dict.Set("safeDialogsMessage", safe_dialogs_message_.value_or("")); last_web_preferences_ = base::Value(std::move(dict)); } diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index 3081b5204b74..8cadc48d3df1 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main'; +import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents, dialog, MessageBoxOptions } from 'electron/main'; import { closeAllWindows } from './lib/window-helpers'; import * as https from 'node:https'; import * as http from 'node:http'; @@ -2396,6 +2396,26 @@ describe('chromium features', () => { window.alert({ toString: null }); }).to.throw('Cannot convert object to primitive value'); }); + + it('shows a message box', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + const p = once(w.webContents, '-run-dialog'); + w.webContents.executeJavaScript('alert("hello")', true); + const [info] = await p; + expect(info.frame).to.equal(w.webContents.mainFrame); + expect(info.messageText).to.equal('hello'); + expect(info.dialogType).to.equal('alert'); + }); + + it('does not crash if a webContents is destroyed while an alert is showing', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + const p = once(w.webContents, '-run-dialog'); + w.webContents.executeJavaScript('alert("hello")', true); + await p; + w.webContents.close(); + }); }); describe('window.confirm(message, title)', () => { @@ -2404,6 +2424,160 @@ describe('chromium features', () => { (window.confirm as any)({ toString: null }, 'title'); }).to.throw('Cannot convert object to primitive value'); }); + + it('shows a message box', async () => { + const w = new BrowserWindow({ show: false }); + w.loadURL('about:blank'); + const p = once(w.webContents, '-run-dialog'); + const resultPromise = w.webContents.executeJavaScript('confirm("hello")', true); + const [info, cb] = await p; + expect(info.frame).to.equal(w.webContents.mainFrame); + expect(info.messageText).to.equal('hello'); + expect(info.dialogType).to.equal('confirm'); + cb(true, ''); + const result = await resultPromise; + expect(result).to.be.true(); + }); + }); + + describe('safeDialogs web preference', () => { + const originalShowMessageBox = dialog.showMessageBox; + afterEach(() => { + dialog.showMessageBox = originalShowMessageBox; + if (protocol.isProtocolHandled('https')) protocol.unhandle('https'); + if (protocol.isProtocolHandled('file')) protocol.unhandle('file'); + }); + it('does not show the checkbox if not enabled', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: false } }); + w.loadURL('about:blank'); + // 1. The first alert() doesn't show the safeDialogs message. + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + + let recordedOpts: MessageBoxOptions | undefined; + dialog.showMessageBox = (bw, opts?: MessageBoxOptions) => { + recordedOpts = opts; + return Promise.resolve({ response: 0, checkboxChecked: false }); + }; + await w.webContents.executeJavaScript('alert("hi")'); + expect(recordedOpts?.checkboxLabel).to.equal(''); + }); + + it('is respected', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: true } }); + w.loadURL('about:blank'); + // 1. The first alert() doesn't show the safeDialogs message. + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + + // 2. The second alert() shows the message with a checkbox. Respond that we checked it. + let recordedOpts: MessageBoxOptions | undefined; + dialog.showMessageBox = (bw, opts?: MessageBoxOptions) => { + recordedOpts = opts; + return Promise.resolve({ response: 0, checkboxChecked: true }); + }; + await w.webContents.executeJavaScript('alert("hi")'); + expect(recordedOpts?.checkboxLabel).to.be.a('string').with.length.above(0); + + // 3. The third alert() shouldn't show a dialog. + dialog.showMessageBox = () => Promise.reject(new Error('unexpected showMessageBox')); + await w.webContents.executeJavaScript('alert("hi")'); + }); + + it('shows the safeDialogMessage', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: true, safeDialogsMessage: 'foo bar' } }); + w.loadURL('about:blank'); + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + let recordedOpts: MessageBoxOptions | undefined; + dialog.showMessageBox = (bw, opts?: MessageBoxOptions) => { + recordedOpts = opts; + return Promise.resolve({ response: 0, checkboxChecked: true }); + }; + await w.webContents.executeJavaScript('alert("hi")'); + expect(recordedOpts?.checkboxLabel).to.equal('foo bar'); + }); + + it('has persistent state across navigations', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: true } }); + w.loadURL('about:blank'); + // 1. The first alert() doesn't show the safeDialogs message. + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + + // 2. The second alert() shows the message with a checkbox. Respond that we checked it. + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: true }); + await w.webContents.executeJavaScript('alert("hi")'); + + // 3. The third alert() shouldn't show a dialog. + dialog.showMessageBox = () => Promise.reject(new Error('unexpected showMessageBox')); + await w.webContents.executeJavaScript('alert("hi")'); + + // 4. After navigating to the same origin, message boxes should still be hidden. + w.loadURL('about:blank'); + await w.webContents.executeJavaScript('alert("hi")'); + }); + + it('is separated by origin', async () => { + protocol.handle('https', () => new Response('')); + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: true } }); + w.loadURL('https://example1'); + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: true }); + await w.webContents.executeJavaScript('alert("hi")'); + dialog.showMessageBox = () => Promise.reject(new Error('unexpected showMessageBox')); + await w.webContents.executeJavaScript('alert("hi")'); + + // A different origin is allowed to show message boxes after navigation. + w.loadURL('https://example2'); + let dialogWasShown = false; + dialog.showMessageBox = () => { + dialogWasShown = true; + return Promise.resolve({ response: 0, checkboxChecked: false }); + }; + await w.webContents.executeJavaScript('alert("hi")'); + expect(dialogWasShown).to.be.true(); + + // Navigating back to the first origin means alerts are blocked again. + w.loadURL('https://example1'); + dialog.showMessageBox = () => Promise.reject(new Error('unexpected showMessageBox')); + await w.webContents.executeJavaScript('alert("hi")'); + }); + + it('treats different file: paths as different origins', async () => { + protocol.handle('file', () => new Response('')); + const w = new BrowserWindow({ show: false, webPreferences: { safeDialogs: true } }); + w.loadURL('file:///path/1'); + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + await w.webContents.executeJavaScript('alert("hi")'); + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: true }); + await w.webContents.executeJavaScript('alert("hi")'); + dialog.showMessageBox = () => Promise.reject(new Error('unexpected showMessageBox')); + await w.webContents.executeJavaScript('alert("hi")'); + + w.loadURL('file:///path/2'); + let dialogWasShown = false; + dialog.showMessageBox = () => { + dialogWasShown = true; + return Promise.resolve({ response: 0, checkboxChecked: false }); + }; + await w.webContents.executeJavaScript('alert("hi")'); + expect(dialogWasShown).to.be.true(); + }); + }); + describe('disableDialogs web preference', () => { + const originalShowMessageBox = dialog.showMessageBox; + afterEach(() => { + dialog.showMessageBox = originalShowMessageBox; + if (protocol.isProtocolHandled('https')) protocol.unhandle('https'); + }); + it('is respected', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { disableDialogs: true } }); + w.loadURL('about:blank'); + dialog.showMessageBox = () => Promise.reject(new Error('unexpected message box')); + await w.webContents.executeJavaScript('alert("hi")'); + }); }); });