From 15d712868a190b9320cc3e97907c1f436f78604e Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 21 May 2021 10:41:22 -0400 Subject: [PATCH 01/28] Catch promise errors better --- app/src/components/contextMenu.ts | 5 ++- app/src/components/loginWindow.ts | 7 ++-- app/src/components/mainWindow.ts | 59 ++++++++++++++++--------------- app/src/components/menu.ts | 10 +++--- app/src/main.ts | 28 +++++++++------ src/helpers/helpers.ts | 2 +- 6 files changed, 63 insertions(+), 48 deletions(-) diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts index 6b8d3955a2..ccf2de6072 100644 --- a/app/src/components/contextMenu.ts +++ b/app/src/components/contextMenu.ts @@ -1,5 +1,6 @@ import { shell } from 'electron'; import contextMenu from 'electron-context-menu'; +import * as log from 'loglevel'; export function initContextMenu(createNewWindow, createNewTab): void { contextMenu({ @@ -9,7 +10,9 @@ export function initContextMenu(createNewWindow, createNewTab): void { items.push({ label: 'Open Link in Default Browser', click: () => { - shell.openExternal(params.linkURL); // eslint-disable-line @typescript-eslint/no-floating-promises + shell + .openExternal(params.linkURL) + .catch((err) => log.error('shell.openExternal ERROR', err)); }, }); items.push({ diff --git a/app/src/components/loginWindow.ts b/app/src/components/loginWindow.ts index 272bffaf37..4800450fa1 100644 --- a/app/src/components/loginWindow.ts +++ b/app/src/components/loginWindow.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { BrowserWindow, ipcMain } from 'electron'; -export function createLoginWindow(loginCallback): BrowserWindow { +export async function createLoginWindow(loginCallback): Promise { const loginWindow = new BrowserWindow({ width: 300, height: 400, @@ -12,8 +12,9 @@ export function createLoginWindow(loginCallback): BrowserWindow { nodeIntegration: true, // TODO work around this; insecure }, }); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - loginWindow.loadURL(`file://${path.join(__dirname, 'static/login.html')}`); + await loginWindow.loadURL( + `file://${path.join(__dirname, 'static/login.html')}`, + ); ipcMain.once('login-message', (event, usernameAndPassword) => { loginCallback(usernameAndPassword[0], usernameAndPassword[1]); diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 8320fb1c53..122e45d8a2 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -26,6 +26,7 @@ import { import { initContextMenu } from './contextMenu'; import { onNewWindowHelper } from './mainWindowHelpers'; import { createMenu } from './menu'; +import { BrowserWindowConstructorOptions } from 'electron/main'; export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); const ZOOM_INTERVAL = 0.1; @@ -83,8 +84,9 @@ function injectCss(browserWindow: BrowserWindow): void { { details, callback }, ); if (details.webContents) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - details.webContents.insertCSS(cssToInject); + details.webContents + .insertCSS(cssToInject) + .catch((err) => log.error('webContents.insertCSS ERROR', err)); } callback({ cancel: false, responseHeaders: details.responseHeaders }); }, @@ -99,12 +101,13 @@ async function clearCache(browserWindow: BrowserWindow): Promise { } function setProxyRules(browserWindow: BrowserWindow, proxyRules): void { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - browserWindow.webContents.session.setProxy({ - proxyRules, - pacScript: '', - proxyBypassRules: '', - }); + browserWindow.webContents.session + .setProxy({ + proxyRules, + pacScript: '', + proxyBypassRules: '', + }) + .catch((err) => log.error('session.setProxy ERROR', err)); } export function saveAppArgs(newAppArgs: any) { @@ -130,18 +133,18 @@ export type createWindowResult = { * @param {function} onAppQuit * @param {function} setDockBadge */ -export function createMainWindow( +export async function createMainWindow( nativefierOptions, onAppQuit, setDockBadge, -): createWindowResult { +): Promise { const options = { ...nativefierOptions }; const mainWindowState = windowStateKeeper({ defaultWidth: options.width || 1280, defaultHeight: options.height || 800, }); - const DEFAULT_WINDOW_OPTIONS = { + const DEFAULT_WINDOW_OPTIONS: BrowserWindowConstructorOptions = { // Convert dashes to spaces because on linux the app name is joined with dashes title: options.name, tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, @@ -153,10 +156,9 @@ export function createMainWindow( preload: path.join(__dirname, 'preload.js'), zoomFactor: options.zoom, }, + ...options.browserwindowOptions, }; - const browserwindowOptions = { ...options.browserwindowOptions }; - const mainWindow = new BrowserWindow({ frame: !options.hideWindowFrame, width: mainWindowState.width, @@ -177,7 +179,6 @@ export function createMainWindow( show: options.tray !== 'start-in-tray', backgroundColor: options.backgroundColor, ...DEFAULT_WINDOW_OPTIONS, - ...browserwindowOptions, }); mainWindowState.manage(mainWindow); @@ -498,11 +499,15 @@ export function createMainWindow( typeof result.value['then'] === 'function' ) { // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply - result.value.then((trueResultValue) => { - result.value = trueResultValue; - log.debug('ipcMain.session-interaction:result', result); - event.reply('session-interaction-reply', result); - }); + result.value + .then((trueResultValue) => { + result.value = trueResultValue; + log.debug('ipcMain.session-interaction:result', result); + event.reply('session-interaction-reply', result); + }) + .catch((err) => + log.error('session-interaction ERROR', request, err), + ); awaitingPromise = true; } } else if (request.property !== undefined) { @@ -543,22 +548,21 @@ export function createMainWindow( // Restore pinch-to-zoom, disabled by default in recent Electron. // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 // and https://github.com/electron/electron/pull/12679 - // eslint-disable-next-line @typescript-eslint/no-floating-promises - mainWindow.webContents.setVisualZoomLevelLimits(1, 3); + mainWindow.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); // Remove potential css injection code set in `did-navigate`) (see injectCss code) mainWindow.webContents.session.webRequest.onHeadersReceived(null); }); if (options.clearCache) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - clearCache(mainWindow); + await clearCache(mainWindow); } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - mainWindow.loadURL(options.targetUrl); + await mainWindow.loadURL(options.targetUrl); - // @ts-ignore + // @ts-ignore new-tab isn't in the type definition, but it does exist mainWindow.on('new-tab', () => createNewTab(options.targetUrl, true)); mainWindow.on('close', (event) => { @@ -576,8 +580,7 @@ export function createMainWindow( hideWindow(mainWindow, event, options.fastQuit, options.tray); if (options.clearCache) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - clearCache(mainWindow); + clearCache(mainWindow).catch((err) => log.error('clearCache ERROR', err)); } }); diff --git a/app/src/components/menu.ts b/app/src/components/menu.ts index d8db8fb56c..674c2309c1 100644 --- a/app/src/components/menu.ts +++ b/app/src/components/menu.ts @@ -240,15 +240,17 @@ export function createMenu({ { label: `Built with Nativefier v${nativefierVersion}`, click: () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal('https://github.com/nativefier/nativefier'); + shell + .openExternal('https://github.com/nativefier/nativefier') + .catch((err) => log.error('shell.openExternal ERROR', err)); }, }, { label: 'Report an Issue', click: () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal('https://github.com/nativefier/nativefier/issues'); + shell + .openExternal('https://github.com/nativefier/nativefier/issues') + .catch((err) => log.error('shell.openExternal ERROR', err)); }, }, ], diff --git a/app/src/main.ts b/app/src/main.ts index 6fe337e816..656fc72b9d 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -31,6 +31,8 @@ if (require('electron-squirrel-startup')) { if (process.argv.indexOf('--verbose') > -1) { log.setLevel('DEBUG'); + process.traceDeprecation = true; + process.traceProcessWarnings = true; } const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8')); @@ -225,7 +227,7 @@ if (shouldQuit) { // @ts-ignore This event only appears on the widevine version of electron, which we'd see at runtime app.on('widevine-ready', (version: string, lastVersion: string) => { log.debug('app.widevine-ready', { version, lastVersion }); - onReady(); + onReady().catch((err) => log.error('onReady ERROR', err)); }); app.on( @@ -246,17 +248,18 @@ if (shouldQuit) { } else { app.on('ready', () => { log.debug('ready'); - onReady(); + onReady().catch((err) => log.error('onReady ERROR', err)); }); } } -function onReady(): void { - const createWindowResult = createMainWindow( +async function onReady(): Promise { + const createWindowResult = await createMainWindow( appArgs, app.quit.bind(this), setDockBadge, ); + log.debug('onReady', createWindowResult); mainWindow = createWindowResult.window; setupWindow = createWindowResult.setupWindow; @@ -325,12 +328,13 @@ function onReady(): void { const oldBuildWarningText = appArgs.oldBuildWarningText || 'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - dialog.showMessageBox(null, { - type: 'warning', - message: 'Old build detected', - detail: oldBuildWarningText, - }); + dialog + .showMessageBox(null, { + type: 'warning', + message: 'Old build detected', + detail: oldBuildWarningText, + }) + .catch((err) => log.error('dialog.showMessageBox ERROR', err)); } } app.on('new-window-for-tab', () => { @@ -349,7 +353,9 @@ app.on('login', (event, webContents, request, authInfo, callback) => { ) { callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword); } else { - createLoginWindow(callback); + createLoginWindow(callback).catch((err) => + log.error('createLoginWindow ERROR', err), + ); } }); diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index 213981c381..ae025560b3 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -66,7 +66,7 @@ export async function copyFileOrDir( }); } -export async function downloadFile(fileUrl: string): Promise { +export function downloadFile(fileUrl: string): Promise { log.debug(`Downloading ${fileUrl}`); return axios .get(fileUrl, { From 46ca0b93b57efda5754990453d30e1fb0fc2eb4d Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 21 May 2021 10:42:02 -0400 Subject: [PATCH 02/28] Move subFunctions to bottom of createNewWindow --- app/src/components/mainWindow.ts | 429 ++++++++++++++++--------------- 1 file changed, 217 insertions(+), 212 deletions(-) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 122e45d8a2..eb8a6fb36c 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -194,218 +194,6 @@ export async function createMainWindow( mainWindow.hide(); } - const withFocusedWindow = (block: (window: BrowserWindow) => void): void => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - return block(focusedWindow); - } - return undefined; - }; - - const adjustWindowZoom = ( - window: BrowserWindow, - adjustment: number, - ): void => { - window.webContents.zoomFactor = window.webContents.zoomFactor + adjustment; - }; - - const onZoomIn = (): void => { - log.debug('onZoomIn'); - withFocusedWindow((focusedWindow: BrowserWindow) => - adjustWindowZoom(focusedWindow, ZOOM_INTERVAL), - ); - }; - - const onZoomOut = (): void => { - log.debug('onZoomOut'); - withFocusedWindow((focusedWindow: BrowserWindow) => - adjustWindowZoom(focusedWindow, -ZOOM_INTERVAL), - ); - }; - - const onZoomReset = (): void => { - log.debug('onZoomReset'); - withFocusedWindow((focusedWindow: BrowserWindow) => { - focusedWindow.webContents.zoomFactor = options.zoom; - }); - }; - - const clearAppData = async (): Promise => { - const response = await dialog.showMessageBox(mainWindow, { - type: 'warning', - buttons: ['Yes', 'Cancel'], - defaultId: 1, - title: 'Clear cache confirmation', - message: - 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', - }); - - if (response.response !== 0) { - return; - } - await clearCache(mainWindow); - }; - - const onGoBack = (): void => { - log.debug('onGoBack'); - withFocusedWindow((focusedWindow) => { - focusedWindow.webContents.goBack(); - }); - }; - - const onGoForward = (): void => { - log.debug('onGoForward'); - withFocusedWindow((focusedWindow) => { - focusedWindow.webContents.goForward(); - }); - }; - - const getCurrentUrl = (): void => - withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL()); - - const gotoUrl = (url: string): void => - withFocusedWindow((focusedWindow) => void focusedWindow.loadURL(url)); - - const onBlockedExternalUrl = (url: string) => { - log.debug('onBlockedExternalUrl', url); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - dialog.showMessageBox(mainWindow, { - message: `Cannot navigate to external URL: ${url}`, - type: 'error', - title: 'Navigation blocked', - }); - }; - - const onWillNavigate = (event: Event, urlToGo: string): void => { - log.debug('onWillNavigate', { event, urlToGo }); - if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { - event.preventDefault(); - if (options.blockExternalUrls) { - onBlockedExternalUrl(urlToGo); - } else { - shell.openExternal(urlToGo); // eslint-disable-line @typescript-eslint/no-floating-promises - } - } - }; - - const onWillPreventUnload = (event: Event): void => { - log.debug('onWillPreventUnload', event); - const eventAny = event as any; - if (eventAny.sender === undefined) { - return; - } - const webContents: WebContents = eventAny.sender; - const browserWindow = BrowserWindow.fromWebContents(webContents); - const choice = dialog.showMessageBoxSync(browserWindow, { - type: 'question', - buttons: ['Proceed', 'Stay'], - message: - 'You may have unsaved changes, are you sure you want to proceed?', - title: 'Changes you made may not be saved.', - defaultId: 0, - cancelId: 1, - }); - if (choice === 0) { - event.preventDefault(); - } - }; - - const createNewWindow: (url: string) => BrowserWindow = (url: string) => { - const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS); - setupWindow(window); - window.loadURL(url); // eslint-disable-line @typescript-eslint/no-floating-promises - return window; - }; - - function setupWindow(window: BrowserWindow): void { - if (options.userAgent) { - window.webContents.userAgent = options.userAgent; - } - - if (options.proxyRules) { - setProxyRules(window, options.proxyRules); - } - - injectCss(window); - sendParamsOnDidFinishLoad(window); - window.webContents.on('new-window', onNewWindow); - window.webContents.on('will-navigate', onWillNavigate); - window.webContents.on('will-prevent-unload', onWillPreventUnload); - } - - const createNewTab = (url: string, foreground: boolean): BrowserWindow => { - log.debug('createNewTab', { url, foreground }); - withFocusedWindow((focusedWindow) => { - const newTab = createNewWindow(url); - focusedWindow.addTabbedWindow(newTab); - if (!foreground) { - focusedWindow.focus(); - } - return newTab; - }); - return undefined; - }; - - const createAboutBlankWindow = (): BrowserWindow => { - const window = createNewWindow('about:blank'); - window.hide(); - window.webContents.once('did-stop-loading', () => { - if (window.webContents.getURL() === 'about:blank') { - window.close(); - } else { - window.show(); - } - }); - return window; - }; - - const onNewWindow = ( - event: Event & { newGuest?: any }, - urlToGo: string, - frameName: string, - disposition: - | 'default' - | 'foreground-tab' - | 'background-tab' - | 'new-window' - | 'save-to-disk' - | 'other', - ): void => { - log.debug('onNewWindow', { event, urlToGo, frameName, disposition }); - const preventDefault = (newGuest: any): void => { - event.preventDefault(); - if (newGuest) { - event.newGuest = newGuest; - } - }; - onNewWindowHelper( - urlToGo, - disposition, - options.targetUrl, - options.internalUrls, - preventDefault, - shell.openExternal.bind(this), - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - options.blockExternalUrls, - onBlockedExternalUrl, - ); - }; - - const sendParamsOnDidFinishLoad = (window: BrowserWindow): void => { - window.webContents.on('did-finish-load', () => { - log.debug('sendParamsOnDidFinishLoad.window.webContents.did-finish-load'); - // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 - // and https://github.com/electron/electron/pull/12679 - // eslint-disable-next-line @typescript-eslint/no-floating-promises - window.webContents.setVisualZoomLevelLimits(1, 3); - - window.webContents.send('params', JSON.stringify(options)); - }); - }; - const menuOptions = { nativefierVersion: options.nativefierVersion, appQuit: onAppQuit, @@ -585,4 +373,221 @@ export async function createMainWindow( }); return { window: mainWindow, setupWindow }; + + function withFocusedWindow(block: (window: BrowserWindow) => void): void { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + return block(focusedWindow); + } + return undefined; + } + + function adjustWindowZoom(window: BrowserWindow, adjustment: number): void { + window.webContents.zoomFactor = window.webContents.zoomFactor + adjustment; + } + + function onZoomIn(): void { + log.debug('onZoomIn'); + withFocusedWindow((focusedWindow: BrowserWindow) => + adjustWindowZoom(focusedWindow, ZOOM_INTERVAL), + ); + } + + function onZoomOut(): void { + log.debug('onZoomOut'); + withFocusedWindow((focusedWindow: BrowserWindow) => + adjustWindowZoom(focusedWindow, -ZOOM_INTERVAL), + ); + } + + function onZoomReset(): void { + log.debug('onZoomReset'); + withFocusedWindow((focusedWindow: BrowserWindow) => { + focusedWindow.webContents.zoomFactor = options.zoom; + }); + } + + async function clearAppData(): Promise { + const response = await dialog.showMessageBox(mainWindow, { + type: 'warning', + buttons: ['Yes', 'Cancel'], + defaultId: 1, + title: 'Clear cache confirmation', + message: + 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', + }); + + if (response.response !== 0) { + return; + } + await clearCache(mainWindow); + } + + function onGoBack(): void { + log.debug('onGoBack'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goBack(); + }); + } + + function onGoForward(): void { + log.debug('onGoForward'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goForward(); + }); + } + + function getCurrentUrl(): void { + return withFocusedWindow((focusedWindow) => + focusedWindow.webContents.getURL(), + ); + } + + function gotoUrl(url: string): void { + return withFocusedWindow( + (focusedWindow) => void focusedWindow.loadURL(url), + ); + } + + function onBlockedExternalUrl(url: string) { + log.debug('onBlockedExternalUrl', url); + dialog + .showMessageBox(mainWindow, { + message: `Cannot navigate to external URL: ${url}`, + type: 'error', + title: 'Navigation blocked', + }) + .catch((err) => log.error('dialog.showMessageBox ERROR', err)); + } + + function onWillNavigate(event: Event, urlToGo: string): void { + log.debug('onWillNavigate', { event, urlToGo }); + if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { + event.preventDefault(); + if (options.blockExternalUrls) { + onBlockedExternalUrl(urlToGo); + } else { + shell + .openExternal(urlToGo) + .catch((err) => log.error('shell.openExternal ERROR', err)); + } + } + } + + function onWillPreventUnload(event: Event): void { + log.debug('onWillPreventUnload', event); + const eventAny = event as any; + if (eventAny.sender === undefined) { + return; + } + const webContents: WebContents = eventAny.sender; + const browserWindow = BrowserWindow.fromWebContents(webContents); + const choice = dialog.showMessageBoxSync(browserWindow, { + type: 'question', + buttons: ['Proceed', 'Stay'], + message: + 'You may have unsaved changes, are you sure you want to proceed?', + title: 'Changes you made may not be saved.', + defaultId: 0, + cancelId: 1, + }); + if (choice === 0) { + event.preventDefault(); + } + } + + function createNewWindow(url: string): BrowserWindow { + const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS); + setupWindow(window); + window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); + return window; + } + + function setupWindow(window: BrowserWindow): void { + if (options.userAgent) { + window.webContents.userAgent = options.userAgent; + } + + if (options.proxyRules) { + setProxyRules(window, options.proxyRules); + } + + injectCss(window); + sendParamsOnDidFinishLoad(window); + window.webContents.on('new-window', onNewWindow); + window.webContents.on('will-navigate', onWillNavigate); + window.webContents.on('will-prevent-unload', onWillPreventUnload); + } + + function createNewTab(url: string, foreground: boolean): BrowserWindow { + log.debug('createNewTab', { url, foreground }); + withFocusedWindow((focusedWindow) => { + const newTab = createNewWindow(url); + focusedWindow.addTabbedWindow(newTab); + if (!foreground) { + focusedWindow.focus(); + } + return newTab; + }); + return undefined; + } + + function createAboutBlankWindow(): BrowserWindow { + const window = createNewWindow('about:blank'); + setupWindow(window); + window.show(); + window.focus(); + return window; + } + + function onNewWindow( + event: Event & { newGuest?: any }, + urlToGo: string, + frameName: string, + disposition: + | 'default' + | 'foreground-tab' + | 'background-tab' + | 'new-window' + | 'save-to-disk' + | 'other', + ): void { + log.debug('onNewWindow', { event, urlToGo, frameName, disposition }); + const preventDefault = (newGuest: any): void => { + event.preventDefault(); + if (newGuest) { + event.newGuest = newGuest; + } + }; + onNewWindowHelper( + urlToGo, + disposition, + options.targetUrl, + options.internalUrls, + preventDefault, + shell.openExternal.bind(this), + createAboutBlankWindow, + nativeTabsSupported, + createNewTab, + options.blockExternalUrls, + onBlockedExternalUrl, + ); + } + + function sendParamsOnDidFinishLoad(window: BrowserWindow): void { + window.webContents.on('did-finish-load', () => { + log.debug( + 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', + window.webContents.getURL(), + ); + // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 + // and https://github.com/electron/electron/pull/12679 + window.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); + + window.webContents.send('params', JSON.stringify(options)); + }); + } } From 01acce908154a54f6644cf7a1d38d7c9415f94cb Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 21 May 2021 17:43:57 -0400 Subject: [PATCH 03/28] Use parents when creating child BrowserWindow instances --- app/src/components/contextMenu.ts | 18 ++++---- app/src/components/loginWindow.ts | 10 ++++- app/src/components/mainWindow.ts | 57 +++++++++++++++++++------ app/src/components/mainWindowHelpers.ts | 22 ++++++++-- app/src/components/menu.ts | 11 ++--- app/src/main.ts | 2 +- 6 files changed, 86 insertions(+), 34 deletions(-) diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts index ccf2de6072..9e9c1f8705 100644 --- a/app/src/components/contextMenu.ts +++ b/app/src/components/contextMenu.ts @@ -1,8 +1,12 @@ -import { shell } from 'electron'; +import { BrowserWindow } from 'electron'; import contextMenu from 'electron-context-menu'; -import * as log from 'loglevel'; -export function initContextMenu(createNewWindow, createNewTab): void { +export function initContextMenu( + createNewWindow, + createNewTab, + openExternal, + window?: BrowserWindow, +): void { contextMenu({ prepend: (actions, params) => { const items = []; @@ -10,22 +14,20 @@ export function initContextMenu(createNewWindow, createNewTab): void { items.push({ label: 'Open Link in Default Browser', click: () => { - shell - .openExternal(params.linkURL) - .catch((err) => log.error('shell.openExternal ERROR', err)); + openExternal(params.linkURL); }, }); items.push({ label: 'Open Link in New Window', click: () => { - createNewWindow(params.linkURL); + createNewWindow(params.linkURL, window); }, }); if (createNewTab) { items.push({ label: 'Open Link in New Tab', click: () => { - createNewTab(params.linkURL, false); + createNewTab(params.linkURL, false, window); }, }); } diff --git a/app/src/components/loginWindow.ts b/app/src/components/loginWindow.ts index 4800450fa1..d8b361af28 100644 --- a/app/src/components/loginWindow.ts +++ b/app/src/components/loginWindow.ts @@ -1,9 +1,17 @@ import * as path from 'path'; +import * as log from 'loglevel'; + import { BrowserWindow, ipcMain } from 'electron'; -export async function createLoginWindow(loginCallback): Promise { +export async function createLoginWindow( + loginCallback, + parent?: BrowserWindow, +): Promise { + log.debug('createLoginWindow', loginCallback, parent); + const loginWindow = new BrowserWindow({ + parent, width: 300, height: 400, frame: false, diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index eb8a6fb36c..f85522303c 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -9,6 +9,7 @@ import { Event, HeadersReceivedResponse, OnHeadersReceivedListenerDetails, + OpenExternalOptions, WebContents, } from 'electron'; import windowStateKeeper from 'electron-window-state'; @@ -70,7 +71,10 @@ function injectCss(browserWindow: BrowserWindow): void { const cssToInject = getCssToInject(); browserWindow.webContents.on('did-navigate', () => { - log.debug('browserWindow.webContents.did-navigate'); + log.debug( + 'browserWindow.webContents.did-navigate', + browserWindow.webContents.getURL(), + ); // We must inject css early enough; so onHeadersReceived is a good place. // Will run multiple times, see `did-finish-load` below that unsets this handler. browserWindow.webContents.session.webRequest.onHeadersReceived( @@ -207,6 +211,7 @@ export async function createMainWindow( gotoUrl, clearAppData, disableDevTools: options.disableDevTools, + openExternal, }; createMenu(menuOptions); @@ -214,6 +219,8 @@ export async function createMainWindow( initContextMenu( createNewWindow, nativeTabsSupported() ? createNewTab : undefined, + openExternal, + mainWindow, ); } @@ -351,7 +358,9 @@ export async function createMainWindow( await mainWindow.loadURL(options.targetUrl); // @ts-ignore new-tab isn't in the type definition, but it does exist - mainWindow.on('new-tab', () => createNewTab(options.targetUrl, true)); + mainWindow.on('new-tab', () => + createNewTab(options.targetUrl, true, mainWindow), + ); mainWindow.on('close', (event) => { log.debug('mainWindow.close', event); @@ -467,9 +476,7 @@ export async function createMainWindow( if (options.blockExternalUrls) { onBlockedExternalUrl(urlToGo); } else { - shell - .openExternal(urlToGo) - .catch((err) => log.error('shell.openExternal ERROR', err)); + openExternal(urlToGo); } } } @@ -496,8 +503,9 @@ export async function createMainWindow( } } - function createNewWindow(url: string): BrowserWindow { - const window = new BrowserWindow(DEFAULT_WINDOW_OPTIONS); + function createNewWindow(url: string, parent?: BrowserWindow): BrowserWindow { + log.debug('createNewWindow', { url, parent, DEFAULT_WINDOW_OPTIONS }); + const window = new BrowserWindow({ parent, ...DEFAULT_WINDOW_OPTIONS }); setupWindow(window); window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); return window; @@ -514,15 +522,30 @@ export async function createMainWindow( injectCss(window); sendParamsOnDidFinishLoad(window); + + // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) + // We can't quite cut over to that yet for a few reasons: + // 1. Our version of Electron does not yet support a parameter to + // setWindowOpenHandler that contains `disposition', which we need. + // See https://github.com/electron/electron/issues/28380 + // 2. setWindowOpenHandler doesn't support newGuest as well + // Though at this point, 'new-window' bugs seem to be coming up and downstream + // users are being pointed to use setWindowOpenHandler. + // E.g., https://github.com/electron/electron/issues/28374 + window.webContents.on('new-window', onNewWindow); window.webContents.on('will-navigate', onWillNavigate); window.webContents.on('will-prevent-unload', onWillPreventUnload); } - function createNewTab(url: string, foreground: boolean): BrowserWindow { - log.debug('createNewTab', { url, foreground }); + function createNewTab( + url: string, + foreground: boolean, + parent?: BrowserWindow, + ): BrowserWindow { + log.debug('createNewTab', { url, foreground, parent }); withFocusedWindow((focusedWindow) => { - const newTab = createNewWindow(url); + const newTab = createNewWindow(url, parent); focusedWindow.addTabbedWindow(newTab); if (!foreground) { focusedWindow.focus(); @@ -532,8 +555,8 @@ export async function createMainWindow( return undefined; } - function createAboutBlankWindow(): BrowserWindow { - const window = createNewWindow('about:blank'); + function createAboutBlankWindow(parent?: BrowserWindow): BrowserWindow { + const window = createNewWindow('about:blank', parent); setupWindow(window); window.show(); window.focus(); @@ -565,15 +588,23 @@ export async function createMainWindow( options.targetUrl, options.internalUrls, preventDefault, - shell.openExternal.bind(this), + openExternal, createAboutBlankWindow, nativeTabsSupported, createNewTab, options.blockExternalUrls, onBlockedExternalUrl, + mainWindow, ); } + function openExternal(url: string, options?: OpenExternalOptions): void { + log.debug('openExternal', { url, options }); + shell + .openExternal(url, options) + .catch((err) => log.error('openExternal ERROR', err)); + } + function sendParamsOnDidFinishLoad(window: BrowserWindow): void { window.webContents.on('did-finish-load', () => { log.debug( diff --git a/app/src/components/mainWindowHelpers.ts b/app/src/components/mainWindowHelpers.ts index 8735bde0d5..a843e226c0 100644 --- a/app/src/components/mainWindowHelpers.ts +++ b/app/src/components/mainWindowHelpers.ts @@ -12,7 +12,22 @@ export function onNewWindowHelper( createNewTab, blockExternal: boolean, onBlockedExternalUrl: (url: string) => void, + parent?: BrowserWindow, ): void { + log.debug('onNewWindowHelper', { + urlToGo, + disposition, + targetUrl, + internalUrls, + preventDefault, + openExternal, + createAboutBlankWindow, + nativeTabsSupported, + createNewTab, + blockExternal, + onBlockedExternalUrl, + parent, + }); if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) { preventDefault(); if (blockExternal) { @@ -20,15 +35,14 @@ export function onNewWindowHelper( } else { openExternal(urlToGo); } - } else if (urlToGo === 'about:blank') { - const newWindow = createAboutBlankWindow(); + const newWindow = createAboutBlankWindow(parent); preventDefault(newWindow); } else if (nativeTabsSupported()) { if (disposition === 'background-tab') { - const newTab = createNewTab(urlToGo, false); + const newTab = createNewTab(urlToGo, false, parent); preventDefault(newTab); } else if (disposition === 'foreground-tab') { - const newTab = createNewTab(urlToGo, true); + const newTab = createNewTab(urlToGo, true, parent); preventDefault(newTab); } } diff --git a/app/src/components/menu.ts b/app/src/components/menu.ts index 674c2309c1..e31d71c729 100644 --- a/app/src/components/menu.ts +++ b/app/src/components/menu.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import path from 'path'; -import { Menu, clipboard, shell, MenuItemConstructorOptions } from 'electron'; +import { clipboard, Menu, MenuItemConstructorOptions } from 'electron'; import * as log from 'loglevel'; type BookmarksLink = { @@ -32,6 +32,7 @@ export function createMenu({ gotoUrl, clearAppData, disableDevTools, + openExternal, }): void { const zoomResetLabel = zoomBuildTimeValue === 1.0 @@ -240,17 +241,13 @@ export function createMenu({ { label: `Built with Nativefier v${nativefierVersion}`, click: () => { - shell - .openExternal('https://github.com/nativefier/nativefier') - .catch((err) => log.error('shell.openExternal ERROR', err)); + openExternal('https://github.com/nativefier/nativefier'); }, }, { label: 'Report an Issue', click: () => { - shell - .openExternal('https://github.com/nativefier/nativefier/issues') - .catch((err) => log.error('shell.openExternal ERROR', err)); + openExternal('https://github.com/nativefier/nativefier/issues'); }, }, ], diff --git a/app/src/main.ts b/app/src/main.ts index 656fc72b9d..1711daaa93 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -353,7 +353,7 @@ app.on('login', (event, webContents, request, authInfo, callback) => { ) { callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword); } else { - createLoginWindow(callback).catch((err) => + createLoginWindow(callback, mainWindow).catch((err) => log.error('createLoginWindow ERROR', err), ); } From 9c9be2a172d38bfc49abdbfdffca62f4da0aaa01 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 21 May 2021 17:44:58 -0400 Subject: [PATCH 04/28] Some about:blank pages have an anchor (for some reason) --- app/src/components/mainWindowHelpers.ts | 4 ++++ app/src/helpers/helpers.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/components/mainWindowHelpers.ts b/app/src/components/mainWindowHelpers.ts index a843e226c0..1702cd1eca 100644 --- a/app/src/components/mainWindowHelpers.ts +++ b/app/src/components/mainWindowHelpers.ts @@ -1,3 +1,6 @@ +import { BrowserWindow } from 'electron'; +import * as log from 'loglevel'; + import { linkIsInternal } from '../helpers/helpers'; export function onNewWindowHelper( @@ -35,6 +38,7 @@ export function onNewWindowHelper( } else { openExternal(urlToGo); } + } else if (urlToGo.split('#')[0] === 'about:blank') { const newWindow = createAboutBlankWindow(parent); preventDefault(newWindow); } else if (nativeTabsSupported()) { diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index 027a085c68..f729acda56 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -42,7 +42,7 @@ export function linkIsInternal( newUrl: string, internalUrlRegex: string | RegExp, ): boolean { - if (newUrl === 'about:blank') { + if (newUrl.split('#')[0] === 'about:blank') { return true; } From 1064c80ceb81f5f0011ed8f60a316c4e61ef4573 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Fri, 21 May 2021 17:47:27 -0400 Subject: [PATCH 05/28] Inject browserWindowOptions better --- app/src/components/mainWindow.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index f85522303c..70f1d4596a 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -148,6 +148,14 @@ export async function createMainWindow( defaultHeight: options.height || 800, }); + const browserwindowOptions: BrowserWindowConstructorOptions = { + ...options.browserwindowOptions, + }; + // We're going to remove this an append it separately + // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences + // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself + delete browserwindowOptions.webPreferences; + const DEFAULT_WINDOW_OPTIONS: BrowserWindowConstructorOptions = { // Convert dashes to spaces because on linux the app name is joined with dashes title: options.name, @@ -159,8 +167,11 @@ export async function createMainWindow( webSecurity: !options.insecure, preload: path.join(__dirname, 'preload.js'), zoomFactor: options.zoom, + ...(options.browserwindowOptions.webPreferences + ? options.browserwindowOptions.webPreferences + : {}), }, - ...options.browserwindowOptions, + ...browserwindowOptions, }; const mainWindow = new BrowserWindow({ From b88786aa146d5a4e48715a0b44d30cb00243ac8b Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Mon, 24 May 2021 17:06:34 -0400 Subject: [PATCH 06/28] Interim refactor to MainWindow object --- app/src/components/contextMenu.ts | 6 +- app/src/components/mainWindow.test.ts | 220 ++++ app/src/components/mainWindow.ts | 991 ++++++++++--------- app/src/components/mainWindowHelpers.test.ts | 238 ----- app/src/components/mainWindowHelpers.ts | 53 - app/src/helpers/helpers.ts | 157 +-- app/src/helpers/windowHelpers.ts | 93 ++ app/src/main.ts | 14 +- 8 files changed, 914 insertions(+), 858 deletions(-) create mode 100644 app/src/components/mainWindow.test.ts delete mode 100644 app/src/components/mainWindowHelpers.test.ts delete mode 100644 app/src/components/mainWindowHelpers.ts create mode 100644 app/src/helpers/windowHelpers.ts diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts index 9e9c1f8705..8faa3ec2c2 100644 --- a/app/src/components/contextMenu.ts +++ b/app/src/components/contextMenu.ts @@ -1,5 +1,4 @@ import { BrowserWindow } from 'electron'; -import contextMenu from 'electron-context-menu'; export function initContextMenu( createNewWindow, @@ -7,6 +6,11 @@ export function initContextMenu( openExternal, window?: BrowserWindow, ): void { + // Require this at call time, otherwise its child dependency 'electron-is-dev' + // throws an error during unit testing. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { contextMenu } = require('electron-context-menu'); + contextMenu({ prepend: (actions, params) => { const items = []; diff --git a/app/src/components/mainWindow.test.ts b/app/src/components/mainWindow.test.ts new file mode 100644 index 0000000000..1736b2c74f --- /dev/null +++ b/app/src/components/mainWindow.test.ts @@ -0,0 +1,220 @@ +jest.mock('../helpers/windowHelpers'); + +import { MainWindow } from './mainWindow'; +import * as helpers from '../helpers/helpers'; +import { createAboutBlankWindow, createNewTab } from '../helpers/windowHelpers'; + +describe('onNewWindowHelper', () => { + const originalUrl = 'https://medium.com/'; + const internalUrl = 'https://medium.com/topics/technology'; + const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; + const foregroundDisposition = 'foreground-tab'; + const backgroundDisposition = 'background-tab'; + + const mockCreateAboutBlank: jest.SpyInstance = + createAboutBlankWindow as jest.Mock; + const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock; + let mockNativeTabsSupported: jest.SpyInstance = jest + .spyOn(helpers, 'nativeTabsSupported') + .mockImplementation(() => false); + let mockOnBlockedExternal: jest.SpyInstance; + const mockOpenExternal: jest.SpyInstance = jest.spyOn( + helpers, + 'openExternal', + ); + const preventDefault = jest.fn(); + + beforeEach(() => { + mockNativeTabsSupported.mockImplementation(() => false); + mockOnBlockedExternal = jest + .spyOn(MainWindow, 'onBlockedExternalUrl') + .mockImplementation(); + }); + + afterEach(() => { + mockCreateAboutBlank.mockReset(); + mockCreateNewTab.mockReset(); + mockNativeTabsSupported.mockReset(); + mockOnBlockedExternal.mockReset(); + mockOpenExternal.mockReset(); + + preventDefault.mockReset(); + }); + + test('internal urls should not be handled', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + + MainWindow.onNewWindowHelper( + options, + internalUrl, + undefined, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('external urls should be opened externally', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + externalUrl, + undefined, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('external urls should be ignored if blockExternalUrls is true', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: true, + }; + MainWindow.onNewWindowHelper( + options, + externalUrl, + undefined, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).toHaveBeenCalledTimes(1); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('tab disposition should be ignored if tabs are not enabled', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + internalUrl, + foregroundDisposition, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('tab disposition should be ignored if url is external', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + externalUrl, + foregroundDisposition, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('foreground tabs with internal urls should be opened in the foreground', () => { + mockNativeTabsSupported = mockNativeTabsSupported.mockImplementation( + () => true, + ); + + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + internalUrl, + foregroundDisposition, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).toHaveBeenCalledWith( + options, + MainWindow.getDefaultWindowOptions(options), + MainWindow.setupWindow, + internalUrl, + true, + undefined, + ); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('background tabs with internal urls should be opened in background tabs', () => { + mockNativeTabsSupported = mockNativeTabsSupported.mockImplementation( + () => true, + ); + + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + internalUrl, + backgroundDisposition, + preventDefault, + ); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).toHaveBeenCalledWith( + options, + MainWindow.getDefaultWindowOptions(options), + MainWindow.setupWindow, + internalUrl, + false, + undefined, + ); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('about:blank urls should be handled', () => { + const options = { + targetUrl: originalUrl, + blockExternalUrls: false, + }; + MainWindow.onNewWindowHelper( + options, + 'about:blank', + undefined, + preventDefault, + ); + + expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 70f1d4596a..eed3d4e223 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -3,31 +3,38 @@ import * as path from 'path'; import { BrowserWindow, - shell, ipcMain, dialog, + BrowserWindowConstructorOptions, Event, HeadersReceivedResponse, OnHeadersReceivedListenerDetails, - OpenExternalOptions, WebContents, } from 'electron'; import windowStateKeeper from 'electron-window-state'; import log from 'loglevel'; import { + getAppIcon, + getCounterValue, + getCSSToInject, isOSX, linkIsInternal, - getCssToInject, - shouldInjectCss, - getAppIcon, nativeTabsSupported, - getCounterValue, + openExternal, + shouldInjectCss, } from '../helpers/helpers'; +import { + adjustWindowZoom, + createAboutBlankWindow, + createNewTab, + createNewWindow, + getCurrentUrl, + gotoUrl, + withFocusedWindow, +} from '../helpers/windowHelpers'; import { initContextMenu } from './contextMenu'; -import { onNewWindowHelper } from './mainWindowHelpers'; import { createMenu } from './menu'; -import { BrowserWindowConstructorOptions } from 'electron/main'; export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); const ZOOM_INTERVAL = 0.1; @@ -46,453 +53,499 @@ type SessionInteractionResult = { error?: Error; }; -function hideWindow( - window: BrowserWindow, - event: Event, - fastQuit: boolean, - tray, -): void { - if (isOSX() && !fastQuit) { - // this is called when exiting from clicking the cross button on the window - event.preventDefault(); - window.hide(); - } else if (!fastQuit && tray) { - event.preventDefault(); - window.hide(); +export class MainWindow { + private readonly options; + private readonly onAppQuit; + private readonly setDockBadge; + private window: BrowserWindow; + + /** + * @param {{}} nativefierOptions AppArgs from nativefier.json + * @param {function} onAppQuit + * @param {function} setDockBadge + */ + constructor(nativefierOptions, onAppQuit, setDockBadge) { + this.options = { ...nativefierOptions }; + this.onAppQuit = onAppQuit; + this.setDockBadge = setDockBadge; } - // will close the window on other platforms -} -function injectCss(browserWindow: BrowserWindow): void { - if (!shouldInjectCss()) { - return; + async clearAppData(): Promise { + const response = await dialog.showMessageBox(this.window, { + type: 'warning', + buttons: ['Yes', 'Cancel'], + defaultId: 1, + title: 'Clear cache confirmation', + message: + 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', + }); + + if (response.response !== 0) { + return; + } + await this.clearCache(); } - const cssToInject = getCssToInject(); + async clearCache(): Promise { + const { session } = this.window.webContents; + await session.clearStorageData(); + await session.clearCache(); + } - browserWindow.webContents.on('did-navigate', () => { - log.debug( - 'browserWindow.webContents.did-navigate', - browserWindow.webContents.getURL(), - ); - // We must inject css early enough; so onHeadersReceived is a good place. - // Will run multiple times, see `did-finish-load` below that unsets this handler. - browserWindow.webContents.session.webRequest.onHeadersReceived( - { urls: [] }, // Pass an empty filter list; null will not match _any_ urls - ( - details: OnHeadersReceivedListenerDetails, - callback: (headersReceivedResponse: HeadersReceivedResponse) => void, - ) => { - log.debug( - 'browserWindow.webContents.session.webRequest.onHeadersReceived', - { details, callback }, - ); - if (details.webContents) { - details.webContents - .insertCSS(cssToInject) - .catch((err) => log.error('webContents.insertCSS ERROR', err)); - } - callback({ cancel: false, responseHeaders: details.responseHeaders }); + static getDefaultWindowOptions = ( + options, + ): BrowserWindowConstructorOptions => { + const browserwindowOptions: BrowserWindowConstructorOptions = { + ...options.browserwindowOptions, + }; + // We're going to remove this an merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences + // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences + // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself + delete browserwindowOptions.webPreferences; + + return { + // Convert dashes to spaces because on linux the app name is joined with dashes + title: options.name, + tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, + webPreferences: { + javascript: true, + plugins: true, + nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com + webSecurity: !options.insecure, + preload: path.join(__dirname, 'preload.js'), + zoomFactor: options.zoom, + ...(options.browserWindowOptions && + options.browserwindowOptions.webPreferences + ? options.browserwindowOptions.webPreferences + : {}), }, - ); - }); -} - -async function clearCache(browserWindow: BrowserWindow): Promise { - const { session } = browserWindow.webContents; - await session.clearStorageData(); - await session.clearCache(); -} + ...browserwindowOptions, + }; + }; -function setProxyRules(browserWindow: BrowserWindow, proxyRules): void { - browserWindow.webContents.session - .setProxy({ - proxyRules, - pacScript: '', - proxyBypassRules: '', - }) - .catch((err) => log.error('session.setProxy ERROR', err)); -} + static hideWindow = ( + window: BrowserWindow, + event: Event, + fastQuit: boolean, + tray, + ): void => { + if (isOSX() && !fastQuit) { + // this is called when exiting from clicking the cross button on the window + event.preventDefault(); + window.hide(); + } else if (!fastQuit && tray) { + event.preventDefault(); + window.hide(); + } + // will close the window on other platforms + }; -export function saveAppArgs(newAppArgs: any) { - try { - fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs)); - } catch (err) { - // eslint-disable-next-line no-console - log.warn( - `WARNING: Ignored nativefier.json rewrital (${( - err as Error - ).toString()})`, - ); - } -} + static injectCss = (browserWindow: BrowserWindow): void => { + if (!shouldInjectCss()) { + return; + } -export type createWindowResult = { - window: BrowserWindow; - setupWindow: (window: BrowserWindow) => void; -}; + const cssToInject = getCSSToInject(); -/** - * @param {{}} nativefierOptions AppArgs from nativefier.json - * @param {function} onAppQuit - * @param {function} setDockBadge - */ -export async function createMainWindow( - nativefierOptions, - onAppQuit, - setDockBadge, -): Promise { - const options = { ...nativefierOptions }; - const mainWindowState = windowStateKeeper({ - defaultWidth: options.width || 1280, - defaultHeight: options.height || 800, - }); - - const browserwindowOptions: BrowserWindowConstructorOptions = { - ...options.browserwindowOptions, - }; - // We're going to remove this an append it separately - // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences - // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself - delete browserwindowOptions.webPreferences; - - const DEFAULT_WINDOW_OPTIONS: BrowserWindowConstructorOptions = { - // Convert dashes to spaces because on linux the app name is joined with dashes - title: options.name, - tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, - webPreferences: { - javascript: true, - plugins: true, - nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com - webSecurity: !options.insecure, - preload: path.join(__dirname, 'preload.js'), - zoomFactor: options.zoom, - ...(options.browserwindowOptions.webPreferences - ? options.browserwindowOptions.webPreferences - : {}), - }, - ...browserwindowOptions, + browserWindow.webContents.on('did-navigate', () => { + log.debug( + 'browserWindow.webContents.did-navigate', + browserWindow.webContents.getURL(), + ); + // We must inject css early enough; so onHeadersReceived is a good place. + // Will run multiple times, see `did-finish-load` below that unsets this handler. + browserWindow.webContents.session.webRequest.onHeadersReceived( + { urls: [] }, // Pass an empty filter list; null will not match _any_ urls + ( + details: OnHeadersReceivedListenerDetails, + callback: (headersReceivedResponse: HeadersReceivedResponse) => void, + ) => { + log.debug( + 'browserWindow.webContents.session.webRequest.onHeadersReceived', + { details, callback }, + ); + if (details.webContents) { + details.webContents + .insertCSS(cssToInject) + .catch((err) => log.error('webContents.insertCSS ERROR', err)); + } + callback({ cancel: false, responseHeaders: details.responseHeaders }); + }, + ); + }); }; - const mainWindow = new BrowserWindow({ - frame: !options.hideWindowFrame, - width: mainWindowState.width, - height: mainWindowState.height, - minWidth: options.minWidth, - minHeight: options.minHeight, - maxWidth: options.maxWidth, - maxHeight: options.maxHeight, - x: options.x, - y: options.y, - autoHideMenuBar: !options.showMenuBar, - icon: getAppIcon(), - // set to undefined and not false because explicitly setting to false will disable full screen - fullscreen: options.fullScreen || undefined, - // Whether the window should always stay on top of other windows. Default is false. - alwaysOnTop: options.alwaysOnTop, - titleBarStyle: options.titleBarStyle, - show: options.tray !== 'start-in-tray', - backgroundColor: options.backgroundColor, - ...DEFAULT_WINDOW_OPTIONS, - }); - - mainWindowState.manage(mainWindow); - - // after first run, no longer force maximize to be true - if (options.maximize) { - mainWindow.maximize(); - options.maximize = undefined; - saveAppArgs(options); - } + static sendParamsOnDidFinishLoad = (options, window: BrowserWindow): void => { + window.webContents.on('did-finish-load', () => { + log.debug( + 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', + window.webContents.getURL(), + ); + // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 + // and https://github.com/electron/electron/pull/12679 + window.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - if (options.tray === 'start-in-tray') { - mainWindow.hide(); - } + window.webContents.send('params', JSON.stringify(options)); + }); + }; - const menuOptions = { - nativefierVersion: options.nativefierVersion, - appQuit: onAppQuit, - zoomIn: onZoomIn, - zoomOut: onZoomOut, - zoomReset: onZoomReset, - zoomBuildTimeValue: options.zoom, - goBack: onGoBack, - goForward: onGoForward, - getCurrentUrl, - gotoUrl, - clearAppData, - disableDevTools: options.disableDevTools, - openExternal, + static setProxyRules = (browserWindow: BrowserWindow, proxyRules): void => { + browserWindow.webContents.session + .setProxy({ + proxyRules, + pacScript: '', + proxyBypassRules: '', + }) + .catch((err) => log.error('session.setProxy ERROR', err)); }; - createMenu(menuOptions); - if (!options.disableContextMenu) { - initContextMenu( - createNewWindow, - nativeTabsSupported() ? createNewTab : undefined, - openExternal, - mainWindow, - ); - } + async create(): Promise { + const mainWindowState = windowStateKeeper({ + defaultWidth: this.options.width || 1280, + defaultHeight: this.options.height || 800, + }); - if (options.userAgent) { - mainWindow.webContents.userAgent = options.userAgent; - } + const defaultWindowOptions = MainWindow.getDefaultWindowOptions( + this.options, + ); - if (options.proxyRules) { - setProxyRules(mainWindow, options.proxyRules); - } + this.window = new BrowserWindow({ + frame: !this.options.hideWindowFrame, + width: mainWindowState.width, + height: mainWindowState.height, + minWidth: this.options.minWidth, + minHeight: this.options.minHeight, + maxWidth: this.options.maxWidth, + maxHeight: this.options.maxHeight, + x: this.options.x, + y: this.options.y, + autoHideMenuBar: !this.options.showMenuBar, + icon: getAppIcon(), + // set to undefined and not false because explicitly setting to false will disable full screen + fullscreen: this.options.fullScreen || undefined, + // Whether the window should always stay on top of other windows. Default is false. + alwaysOnTop: this.options.alwaysOnTop, + titleBarStyle: this.options.titleBarStyle, + show: this.options.tray !== 'start-in-tray', + backgroundColor: this.options.backgroundColor, + ...defaultWindowOptions, + }); - injectCss(mainWindow); - sendParamsOnDidFinishLoad(mainWindow); + mainWindowState.manage(this.window); - if (options.counter) { - mainWindow.on('page-title-updated', (event, title) => { - log.debug('mainWindow.page-title-updated', { event, title }); - const counterValue = getCounterValue(title); - if (counterValue) { - setDockBadge(counterValue, options.bounce); - } else { - setDockBadge(''); - } - }); - } else { - ipcMain.on('notification', () => { - log.debug('ipcMain.notification'); - if (!isOSX() || mainWindow.isFocused()) { - return; - } - setDockBadge('•', options.bounce); - }); - mainWindow.on('focus', () => { - log.debug('mainWindow.focus'); - setDockBadge(''); - }); - } + // after first run, no longer force maximize to be true + if (this.options.maximize) { + this.window.maximize(); + this.options.maximize = undefined; + saveAppArgs(this.options); + } - ipcMain.on('notification-click', () => { - log.debug('ipcMain.notification-click'); - mainWindow.show(); - }); - - // See API.md / "Accessing The Electron Session" - ipcMain.on( - 'session-interaction', - (event, request: SessionInteractionRequest) => { - log.debug('ipcMain.session-interaction', { event, request }); - - const result: SessionInteractionResult = { id: request.id }; - let awaitingPromise = false; - try { - if (request.func !== undefined) { - // If no funcArgs provided, we'll just use an empty array - if (request.funcArgs === undefined || request.funcArgs === null) { - request.funcArgs = []; - } + if (this.options.tray === 'start-in-tray') { + this.window.hide(); + } - // If funcArgs isn't an array, we'll be nice and make it a single item array - if (typeof request.funcArgs[Symbol.iterator] !== 'function') { - request.funcArgs = [request.funcArgs]; - } + const menuOptions = { + nativefierVersion: this.options.nativefierVersion, + appQuit: this.onAppQuit, + clearAppData: this.clearAppData.bind(this), + disableDevTools: this.options.disableDevTools, + getCurrentUrl, + goBack: MainWindow.onGoBack, + goForward: MainWindow.onGoForward, + gotoUrl, + openExternal, + zoomBuildTimeValue: this.options.zoom, + zoomIn: MainWindow.onZoomIn, + zoomOut: MainWindow.onZoomOut, + zoomReset: MainWindow.onZoomReset, + }; - // Call func with funcArgs - result.value = mainWindow.webContents.session[request.func]( - ...request.funcArgs, - ); + createMenu(menuOptions); + if (!this.options.disableContextMenu) { + initContextMenu( + createNewWindow, + nativeTabsSupported() + ? (url: string, foreground: boolean) => + createNewTab( + this.options, + MainWindow.getDefaultWindowOptions(this.options), + MainWindow.setupWindow, + url, + foreground, + this.window, + ) + : undefined, + openExternal, + this.window, + ); + } - if ( - result.value !== undefined && - typeof result.value['then'] === 'function' - ) { - // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply - result.value - .then((trueResultValue) => { - result.value = trueResultValue; - log.debug('ipcMain.session-interaction:result', result); - event.reply('session-interaction-reply', result); - }) - .catch((err) => - log.error('session-interaction ERROR', request, err), - ); - awaitingPromise = true; - } - } else if (request.property !== undefined) { - if (request.propertyValue !== undefined) { - // Set the property - mainWindow.webContents.session[request.property] = - request.propertyValue; - } + MainWindow.setupWindow(this.options, this.window); - // Get the property value - result.value = mainWindow.webContents.session[request.property]; + if (this.options.counter) { + this.window.on('page-title-updated', (event, title) => { + log.debug('mainWindow.page-title-updated', { event, title }); + const counterValue = getCounterValue(title); + if (counterValue) { + this.setDockBadge(counterValue, this.options.bounce); } else { - // Why even send the event if you're going to do this? You're just wasting time! ;) - throw Error( - 'Received neither a func nor a property in the request. Unable to process.', - ); + this.setDockBadge(''); } - - // If we are awaiting a promise, that will return the reply instead, else - if (!awaitingPromise) { - log.debug('session-interaction:result', result); - event.reply('session-interaction-reply', result); + }); + } else { + ipcMain.on('notification', () => { + log.debug('ipcMain.notification'); + if (!isOSX() || this.window.isFocused()) { + return; } - } catch (error) { - log.error('session-interaction:error', error, event, request); - result.error = error; - result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place - event.reply('session-interaction-reply', result); - } - }, - ); - - mainWindow.webContents.on('new-window', onNewWindow); - mainWindow.webContents.on('will-navigate', onWillNavigate); - mainWindow.webContents.on('will-prevent-unload', onWillPreventUnload); - mainWindow.webContents.on('did-finish-load', () => { - log.debug('mainWindow.webContents.did-finish-load'); - // Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 - // and https://github.com/electron/electron/pull/12679 - mainWindow.webContents - .setVisualZoomLevelLimits(1, 3) - .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - - // Remove potential css injection code set in `did-navigate`) (see injectCss code) - mainWindow.webContents.session.webRequest.onHeadersReceived(null); - }); - - if (options.clearCache) { - await clearCache(mainWindow); - } - - await mainWindow.loadURL(options.targetUrl); - - // @ts-ignore new-tab isn't in the type definition, but it does exist - mainWindow.on('new-tab', () => - createNewTab(options.targetUrl, true, mainWindow), - ); - - mainWindow.on('close', (event) => { - log.debug('mainWindow.close', event); - if (mainWindow.isFullScreen()) { - if (nativeTabsSupported()) { - mainWindow.moveTabToNewWindow(); - } - mainWindow.setFullScreen(false); - mainWindow.once( - 'leave-full-screen', - hideWindow.bind(this, mainWindow, event, options.fastQuit), - ); + this.setDockBadge('•', this.options.bounce); + }); + this.window.on('focus', () => { + log.debug('mainWindow.focus'); + this.setDockBadge(''); + }); } - hideWindow(mainWindow, event, options.fastQuit, options.tray); - if (options.clearCache) { - clearCache(mainWindow).catch((err) => log.error('clearCache ERROR', err)); - } - }); + ipcMain.on('notification-click', () => { + log.debug('ipcMain.notification-click'); + this.window.show(); + }); - return { window: mainWindow, setupWindow }; + // See API.md / "Accessing The Electron Session" + ipcMain.on( + 'session-interaction', + (event, request: SessionInteractionRequest) => { + log.debug('ipcMain.session-interaction', { event, request }); + + const result: SessionInteractionResult = { id: request.id }; + let awaitingPromise = false; + try { + if (request.func !== undefined) { + // If no funcArgs provided, we'll just use an empty array + if (request.funcArgs === undefined || request.funcArgs === null) { + request.funcArgs = []; + } + + // If funcArgs isn't an array, we'll be nice and make it a single item array + if (typeof request.funcArgs[Symbol.iterator] !== 'function') { + request.funcArgs = [request.funcArgs]; + } + + // Call func with funcArgs + result.value = this.window.webContents.session[request.func]( + ...request.funcArgs, + ); + + if ( + result.value !== undefined && + typeof result.value['then'] === 'function' + ) { + // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply + result.value + .then((trueResultValue) => { + result.value = trueResultValue; + log.debug('ipcMain.session-interaction:result', result); + event.reply('session-interaction-reply', result); + }) + .catch((err) => + log.error('session-interaction ERROR', request, err), + ); + awaitingPromise = true; + } + } else if (request.property !== undefined) { + if (request.propertyValue !== undefined) { + // Set the property + this.window.webContents.session[request.property] = + request.propertyValue; + } + + // Get the property value + result.value = this.window.webContents.session[request.property]; + } else { + // Why even send the event if you're going to do this? You're just wasting time! ;) + throw Error( + 'Received neither a func nor a property in the request. Unable to process.', + ); + } - function withFocusedWindow(block: (window: BrowserWindow) => void): void { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - return block(focusedWindow); - } - return undefined; - } + // If we are awaiting a promise, that will return the reply instead, else + if (!awaitingPromise) { + log.debug('session-interaction:result', result); + event.reply('session-interaction-reply', result); + } + } catch (error) { + log.error('session-interaction:error', error, event, request); + result.error = error; + result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place + event.reply('session-interaction-reply', result); + } + }, + ); - function adjustWindowZoom(window: BrowserWindow, adjustment: number): void { - window.webContents.zoomFactor = window.webContents.zoomFactor + adjustment; - } + if (this.options.clearCache) { + await this.clearCache(); + } - function onZoomIn(): void { - log.debug('onZoomIn'); - withFocusedWindow((focusedWindow: BrowserWindow) => - adjustWindowZoom(focusedWindow, ZOOM_INTERVAL), - ); - } + await this.window.loadURL(this.options.targetUrl); - function onZoomOut(): void { - log.debug('onZoomOut'); - withFocusedWindow((focusedWindow: BrowserWindow) => - adjustWindowZoom(focusedWindow, -ZOOM_INTERVAL), - ); - } + this.window.on('close', (event) => { + log.debug('mainWindow.close', event); + if (this.window.isFullScreen()) { + if (nativeTabsSupported()) { + this.window.moveTabToNewWindow(); + } + this.window.setFullScreen(false); + this.window.once( + 'leave-full-screen', + MainWindow.hideWindow.bind(this.window, event, this.options.fastQuit), + ); + } + MainWindow.hideWindow( + this.window, + event, + this.options.fastQuit, + this.options.tray, + ); - function onZoomReset(): void { - log.debug('onZoomReset'); - withFocusedWindow((focusedWindow: BrowserWindow) => { - focusedWindow.webContents.zoomFactor = options.zoom; + if (this.options.clearCache) { + this.clearCache().catch((err) => log.error('clearCache ERROR', err)); + } }); + + return this.window; } - async function clearAppData(): Promise { - const response = await dialog.showMessageBox(mainWindow, { - type: 'warning', - buttons: ['Yes', 'Cancel'], - defaultId: 1, - title: 'Clear cache confirmation', - message: - 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', + static onBlockedExternalUrl = (url: string) => { + log.debug('onBlockedExternalUrl', url); + withFocusedWindow((focusedWindow) => { + dialog + .showMessageBox(focusedWindow, { + message: `Cannot navigate to external URL: ${url}`, + type: 'error', + title: 'Navigation blocked', + }) + .catch((err) => log.error('dialog.showMessageBox ERROR', err)); }); + }; - if (response.response !== 0) { - return; - } - await clearCache(mainWindow); - } - - function onGoBack(): void { + static onGoBack = (): void => { log.debug('onGoBack'); withFocusedWindow((focusedWindow) => { focusedWindow.webContents.goBack(); }); - } + }; - function onGoForward(): void { + static onGoForward = (): void => { log.debug('onGoForward'); withFocusedWindow((focusedWindow) => { focusedWindow.webContents.goForward(); }); - } - - function getCurrentUrl(): void { - return withFocusedWindow((focusedWindow) => - focusedWindow.webContents.getURL(), - ); - } + }; - function gotoUrl(url: string): void { - return withFocusedWindow( - (focusedWindow) => void focusedWindow.loadURL(url), + static onNewWindow = ( + options, + event: Event & { newGuest?: any }, + urlToGo: string, + frameName: string, + disposition: + | 'default' + | 'foreground-tab' + | 'background-tab' + | 'new-window' + | 'save-to-disk' + | 'other', + parent?: BrowserWindow, + ): void => { + log.debug('onNewWindow', { + event, + urlToGo, + frameName, + disposition, + parent, + }); + const preventDefault = (newGuest: any): void => { + event.preventDefault(); + if (newGuest) { + event.newGuest = newGuest; + } + }; + MainWindow.onNewWindowHelper( + options, + urlToGo, + disposition, + preventDefault, + parent, ); - } + }; - function onBlockedExternalUrl(url: string) { - log.debug('onBlockedExternalUrl', url); - dialog - .showMessageBox(mainWindow, { - message: `Cannot navigate to external URL: ${url}`, - type: 'error', - title: 'Navigation blocked', - }) - .catch((err) => log.error('dialog.showMessageBox ERROR', err)); - } + static onNewWindowHelper = ( + options, + urlToGo: string, + disposition: string, + preventDefault, + parent?: BrowserWindow, + ): void => { + log.debug('onNewWindowHelper', { + urlToGo, + disposition, + preventDefault, + parent, + }); + if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { + preventDefault(); + if (options.blockExternalUrls) { + MainWindow.onBlockedExternalUrl(urlToGo); + } else { + openExternal(urlToGo); + } + } else if (urlToGo === 'about:blank') { + const newWindow = createAboutBlankWindow( + options, + MainWindow.getDefaultWindowOptions(options), + parent, + ); + preventDefault(newWindow); + } else if (nativeTabsSupported()) { + if (disposition === 'background-tab') { + const newTab = createNewTab( + options, + MainWindow.getDefaultWindowOptions(options), + MainWindow.setupWindow, + urlToGo, + false, + parent, + ); + preventDefault(newTab); + } else if (disposition === 'foreground-tab') { + const newTab = createNewTab( + options, + MainWindow.getDefaultWindowOptions(options), + MainWindow.setupWindow, + urlToGo, + true, + parent, + ); + preventDefault(newTab); + } + } + }; - function onWillNavigate(event: Event, urlToGo: string): void { - log.debug('onWillNavigate', { event, urlToGo }); + static onWillNavigate = (options, event: Event, urlToGo: string): void => { + log.debug('onWillNavigate', { options, event, urlToGo }); if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { event.preventDefault(); if (options.blockExternalUrls) { - onBlockedExternalUrl(urlToGo); + MainWindow.onBlockedExternalUrl(urlToGo); } else { openExternal(urlToGo); } } - } + }; - function onWillPreventUnload(event: Event): void { + static onWillPreventUnload = (event: Event): void => { log.debug('onWillPreventUnload', event); const eventAny = event as any; if (eventAny.sender === undefined) { @@ -512,27 +565,36 @@ export async function createMainWindow( if (choice === 0) { event.preventDefault(); } - } + }; - function createNewWindow(url: string, parent?: BrowserWindow): BrowserWindow { - log.debug('createNewWindow', { url, parent, DEFAULT_WINDOW_OPTIONS }); - const window = new BrowserWindow({ parent, ...DEFAULT_WINDOW_OPTIONS }); - setupWindow(window); - window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); - return window; - } + static onZoomOut = (): void => { + log.debug('onZoomOut'); + adjustWindowZoom(-ZOOM_INTERVAL); + }; + + static onZoomReset = (options): void => { + log.debug('onZoomReset'); + withFocusedWindow((focusedWindow: BrowserWindow) => { + focusedWindow.webContents.zoomFactor = options.zoom; + }); + }; - function setupWindow(window: BrowserWindow): void { + static onZoomIn = (): void => { + log.debug('onZoomIn'); + adjustWindowZoom(ZOOM_INTERVAL); + }; + + static setupWindow = (options, window: BrowserWindow): void => { if (options.userAgent) { window.webContents.userAgent = options.userAgent; } if (options.proxyRules) { - setProxyRules(window, options.proxyRules); + MainWindow.setProxyRules(window, options.proxyRules); } - injectCss(window); - sendParamsOnDidFinishLoad(window); + MainWindow.injectCss(window); + MainWindow.sendParamsOnDidFinishLoad(options, window); // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) // We can't quite cut over to that yet for a few reasons: @@ -544,92 +606,55 @@ export async function createMainWindow( // users are being pointed to use setWindowOpenHandler. // E.g., https://github.com/electron/electron/issues/28374 - window.webContents.on('new-window', onNewWindow); - window.webContents.on('will-navigate', onWillNavigate); - window.webContents.on('will-prevent-unload', onWillPreventUnload); - } - - function createNewTab( - url: string, - foreground: boolean, - parent?: BrowserWindow, - ): BrowserWindow { - log.debug('createNewTab', { url, foreground, parent }); - withFocusedWindow((focusedWindow) => { - const newTab = createNewWindow(url, parent); - focusedWindow.addTabbedWindow(newTab); - if (!foreground) { - focusedWindow.focus(); - } - return newTab; - }); - return undefined; - } - - function createAboutBlankWindow(parent?: BrowserWindow): BrowserWindow { - const window = createNewWindow('about:blank', parent); - setupWindow(window); - window.show(); - window.focus(); - return window; - } - - function onNewWindow( - event: Event & { newGuest?: any }, - urlToGo: string, - frameName: string, - disposition: - | 'default' - | 'foreground-tab' - | 'background-tab' - | 'new-window' - | 'save-to-disk' - | 'other', - ): void { - log.debug('onNewWindow', { event, urlToGo, frameName, disposition }); - const preventDefault = (newGuest: any): void => { - event.preventDefault(); - if (newGuest) { - event.newGuest = newGuest; - } - }; - onNewWindowHelper( - urlToGo, - disposition, - options.targetUrl, - options.internalUrls, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - options.blockExternalUrls, - onBlockedExternalUrl, - mainWindow, + window.webContents.on( + 'new-window', + () => (event, url, frameName, disposition) => + MainWindow.onNewWindow(options, event, url, frameName, disposition), + ); + window.webContents.on('will-navigate', (event: Event, url: string) => + MainWindow.onWillNavigate(options, event, url), + ); + window.webContents.on( + 'will-prevent-unload', + MainWindow.onWillPreventUnload, ); - } - - function openExternal(url: string, options?: OpenExternalOptions): void { - log.debug('openExternal', { url, options }); - shell - .openExternal(url, options) - .catch((err) => log.error('openExternal ERROR', err)); - } - function sendParamsOnDidFinishLoad(window: BrowserWindow): void { window.webContents.on('did-finish-load', () => { - log.debug( - 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', - window.webContents.getURL(), - ); - // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 + log.debug('mainWindow.webContents.did-finish-load'); + // Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 // and https://github.com/electron/electron/pull/12679 window.webContents .setVisualZoomLevelLimits(1, 3) .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - window.webContents.send('params', JSON.stringify(options)); + // Remove potential css injection code set in `did-navigate`) (see injectCss code) + window.webContents.session.webRequest.onHeadersReceived(null); }); + + // @ts-ignore new-tab isn't in the type definition, but it does exist + this.window.on('new-tab', () => + createNewTab( + options, + MainWindow.getDefaultWindowOptions(options), + MainWindow.setupWindow, + options.targetUrl, + true, + window, + ), + ); + }; +} + +export function saveAppArgs(newAppArgs: any) { + try { + fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs)); + } catch (err) { + // eslint-disable-next-line no-console + log.warn( + `WARNING: Ignored nativefier.json rewrital (${( + err as Error + ).toString()})`, + ); } } diff --git a/app/src/components/mainWindowHelpers.test.ts b/app/src/components/mainWindowHelpers.test.ts deleted file mode 100644 index 667aff63b3..0000000000 --- a/app/src/components/mainWindowHelpers.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { onNewWindowHelper } from './mainWindowHelpers'; - -const originalUrl = 'https://medium.com/'; -const internalUrl = 'https://medium.com/topics/technology'; -const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; -const foregroundDisposition = 'foreground-tab'; -const backgroundDisposition = 'background-tab'; -const blockExternal = false; - -const nativeTabsSupported = () => true; -const nativeTabsNotSupported = () => false; - -test('internal urls should not be handled', () => { - const preventDefault = jest.fn(); - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - internalUrl, - undefined, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsNotSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(0); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('external urls should be opened externally', () => { - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const preventDefault = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - externalUrl, - undefined, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsNotSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(1); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('external urls should be ignored if blockExternal is true', () => { - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const preventDefault = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - const blockExternal = true; - - onNewWindowHelper( - externalUrl, - undefined, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsNotSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(1); -}); - -test('tab disposition should be ignored if tabs are not enabled', () => { - const preventDefault = jest.fn(); - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - internalUrl, - foregroundDisposition, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsNotSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(0); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('tab disposition should be ignored if url is external', () => { - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const preventDefault = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - externalUrl, - foregroundDisposition, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(1); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('foreground tabs with internal urls should be opened in the foreground', () => { - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const preventDefault = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - internalUrl, - foregroundDisposition, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(1); - expect(createNewTab.mock.calls[0][1]).toBe(true); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('background tabs with internal urls should be opened in background tabs', () => { - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const preventDefault = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - internalUrl, - backgroundDisposition, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(0); - expect(createNewTab.mock.calls.length).toBe(1); - expect(createNewTab.mock.calls[0][1]).toBe(false); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); - -test('about:blank urls should be handled', () => { - const preventDefault = jest.fn(); - const openExternal = jest.fn(); - const createAboutBlankWindow = jest.fn(); - const createNewTab = jest.fn(); - const onBlockedExternalUrl = jest.fn(); - - onNewWindowHelper( - 'about:blank', - undefined, - originalUrl, - undefined, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsNotSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - ); - - expect(openExternal.mock.calls.length).toBe(0); - expect(createAboutBlankWindow.mock.calls.length).toBe(1); - expect(createNewTab.mock.calls.length).toBe(0); - expect(preventDefault.mock.calls.length).toBe(1); - expect(onBlockedExternalUrl.mock.calls.length).toBe(0); -}); diff --git a/app/src/components/mainWindowHelpers.ts b/app/src/components/mainWindowHelpers.ts deleted file mode 100644 index 1702cd1eca..0000000000 --- a/app/src/components/mainWindowHelpers.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BrowserWindow } from 'electron'; -import * as log from 'loglevel'; - -import { linkIsInternal } from '../helpers/helpers'; - -export function onNewWindowHelper( - urlToGo: string, - disposition: string, - targetUrl: string, - internalUrls: string | RegExp, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - blockExternal: boolean, - onBlockedExternalUrl: (url: string) => void, - parent?: BrowserWindow, -): void { - log.debug('onNewWindowHelper', { - urlToGo, - disposition, - targetUrl, - internalUrls, - preventDefault, - openExternal, - createAboutBlankWindow, - nativeTabsSupported, - createNewTab, - blockExternal, - onBlockedExternalUrl, - parent, - }); - if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) { - preventDefault(); - if (blockExternal) { - onBlockedExternalUrl(urlToGo); - } else { - openExternal(urlToGo); - } - } else if (urlToGo.split('#')[0] === 'about:blank') { - const newWindow = createAboutBlankWindow(parent); - preventDefault(newWindow); - } else if (nativeTabsSupported()) { - if (disposition === 'background-tab') { - const newTab = createNewTab(urlToGo, false, parent); - preventDefault(newTab); - } else if (disposition === 'foreground-tab') { - const newTab = createNewTab(urlToGo, true, parent); - preventDefault(newTab); - } - } -} diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index f729acda56..9f4454d27d 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -2,11 +2,83 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { BrowserWindow } from 'electron'; +import { BrowserWindow, OpenExternalOptions, shell } from 'electron'; import * as log from 'loglevel'; export const INJECT_DIR = path.join(__dirname, '..', 'inject'); +/** + * Helper to print debug messages from the main process in the browser window + */ +export function debugLog(browserWindow: BrowserWindow, message: string): void { + // Need a delay, as it takes time for the preloaded js to be loaded by the window + setTimeout(() => { + browserWindow.webContents.send('debug', message); + }, 3000); + log.info(message); +} + +/** + * Helper to determine domain-ish equality for many cases, the trivial ones + * and the trickier ones, e.g. `blog.foo.com` and `shop.foo.com`, + * in a way that is "good enough", and doesn't need a list of SLDs. + * See chat at https://github.com/nativefier/nativefier/pull/1171#pullrequestreview-649132523 + */ +function domainify(url: string): string { + // So here's what we're doing here: + // Get the hostname from the url + const hostname = new URL(url).hostname; + // Drop the first section if the domain + const domain = hostname.split('.').slice(1).join('.'); + // Check the length, if it's too short, the hostname was probably the domain + // Or if the domain doesn't have a . in it we went too far + if (domain.length < 6 || domain.split('.').length === 0) { + return hostname; + } + // This SHOULD be the domain, but nothing is 100% guaranteed + return domain; +} + +export function getAppIcon(): string { + // Prefer ICO under Windows, see + // https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions + // https://www.electronjs.org/docs/api/native-image#supported-formats + if (isWindows()) { + const ico = path.join(__dirname, '..', 'icon.ico'); + if (fs.existsSync(ico)) { + return ico; + } + } + const png = path.join(__dirname, '..', 'icon.png'); + if (fs.existsSync(png)) { + return png; + } +} + +export function getCounterValue(title: string): string { + const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/; + const match = itemCountRegex.exec(title); + return match ? match[1] : undefined; +} + +export function getCSSToInject(): string { + let cssToInject = ''; + const cssFiles = fs + .readdirSync(INJECT_DIR, { withFileTypes: true }) + .filter( + (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.css'), + ) + .map((cssFileStat) => + path.resolve(path.join(INJECT_DIR, cssFileStat.name)), + ); + for (const cssFile of cssFiles) { + log.debug('Injecting CSS file', cssFile); + const cssFileData = fs.readFileSync(cssFile); + cssToInject += `/* ${cssFile} */\n\n ${cssFileData}\n\n`; + } + return cssToInject; +} + export function isOSX(): boolean { return os.platform() === 'darwin'; } @@ -42,6 +114,7 @@ export function linkIsInternal( newUrl: string, internalUrlRegex: string | RegExp, ): boolean { + log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex }); if (newUrl.split('#')[0] === 'about:blank') { return true; } @@ -78,25 +151,15 @@ export function linkIsInternal( } } -/** - * Helper to determine domain-ish equality for many cases, the trivial ones - * and the trickier ones, e.g. `blog.foo.com` and `shop.foo.com`, - * in a way that is "good enough", and doesn't need a list of SLDs. - * See chat at https://github.com/nativefier/nativefier/pull/1171#pullrequestreview-649132523 - */ -function domainify(url: string): string { - // So here's what we're doing here: - // Get the hostname from the url - const hostname = new URL(url).hostname; - // Drop the first section if the domain - const domain = hostname.split('.').slice(1).join('.'); - // Check the length, if it's too short, the hostname was probably the domain - // Or if the domain doesn't have a . in it we went too far - if (domain.length < 6 || domain.split('.').length === 0) { - return hostname; - } - // This SHOULD be the domain, but nothing is 100% guaranteed - return domain; +export function nativeTabsSupported(): boolean { + return isOSX(); +} + +export function openExternal(url: string, options?: OpenExternalOptions): void { + log.debug('openExternal', { url, options }); + shell + .openExternal(url, options) + .catch((err) => log.error('openExternal ERROR', err)); } export function shouldInjectCss(): boolean { @@ -106,57 +169,3 @@ export function shouldInjectCss(): boolean { return false; } } - -export function getCssToInject(): string { - let cssToInject = ''; - const cssFiles = fs - .readdirSync(INJECT_DIR, { withFileTypes: true }) - .filter( - (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.css'), - ) - .map((cssFileStat) => - path.resolve(path.join(INJECT_DIR, cssFileStat.name)), - ); - for (const cssFile of cssFiles) { - log.debug('Injecting CSS file', cssFile); - const cssFileData = fs.readFileSync(cssFile); - cssToInject += `/* ${cssFile} */\n\n ${cssFileData}\n\n`; - } - return cssToInject; -} -/** - * Helper to print debug messages from the main process in the browser window - */ -export function debugLog(browserWindow: BrowserWindow, message: string): void { - // Need a delay, as it takes time for the preloaded js to be loaded by the window - setTimeout(() => { - browserWindow.webContents.send('debug', message); - }, 3000); - log.info(message); -} - -export function getAppIcon(): string { - // Prefer ICO under Windows, see - // https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions - // https://www.electronjs.org/docs/api/native-image#supported-formats - if (isWindows()) { - const ico = path.join(__dirname, '..', 'icon.ico'); - if (fs.existsSync(ico)) { - return ico; - } - } - const png = path.join(__dirname, '..', 'icon.png'); - if (fs.existsSync(png)) { - return png; - } -} - -export function nativeTabsSupported(): boolean { - return isOSX(); -} - -export function getCounterValue(title: string): string { - const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/; - const match = itemCountRegex.exec(title); - return match ? match[1] : undefined; -} diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts new file mode 100644 index 0000000000..22d5ec4753 --- /dev/null +++ b/app/src/helpers/windowHelpers.ts @@ -0,0 +1,93 @@ +import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron'; + +import * as log from 'loglevel'; + +export function adjustWindowZoom(adjustment: number): void { + withFocusedWindow( + (focusedWindow: BrowserWindow) => + (focusedWindow.webContents.zoomFactor = + focusedWindow.webContents.zoomFactor + adjustment), + ); +} + +export function createAboutBlankWindow( + options, + browserWindowConstructorOptions: BrowserWindowConstructorOptions, + setupWindow, + parent?: BrowserWindow, +): BrowserWindow { + const window = createNewWindow( + options, + browserWindowConstructorOptions, + setupWindow, + 'about:blank', + parent, + ); + setupWindow(options, window); + window.show(); + window.focus(); + return window; +} + +export function createNewTab( + options, + browserWindowConstructorOptions: BrowserWindowConstructorOptions, + setupWindow, + url: string, + foreground: boolean, + parent?: BrowserWindow, +): BrowserWindow { + log.debug('createNewTab', { url, foreground, parent }); + withFocusedWindow((focusedWindow) => { + const newTab = createNewWindow( + options, + browserWindowConstructorOptions, + setupWindow, + url, + parent, + ); + focusedWindow.addTabbedWindow(newTab); + if (!foreground) { + focusedWindow.focus(); + } + return newTab; + }); + return undefined; +} + +export function createNewWindow( + options, + browserWindowConstructorOptions: BrowserWindowConstructorOptions, + setupWindow, + url: string, + parent?: BrowserWindow, +): BrowserWindow { + log.debug('createNewWindow', { url, parent }); + const window = new BrowserWindow({ + parent, + ...browserWindowConstructorOptions, + }); + setupWindow(options, window); + window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); + return window; +} + +export function getCurrentUrl(): string { + return withFocusedWindow((focusedWindow) => + focusedWindow.webContents.getURL(), + ) as unknown as string; +} + +export function gotoUrl(url: string): void { + return withFocusedWindow((focusedWindow) => void focusedWindow.loadURL(url)); +} + +export function withFocusedWindow( + block: (window: BrowserWindow) => void, +): void { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + return block(focusedWindow); + } + return undefined; +} diff --git a/app/src/main.ts b/app/src/main.ts index 1711daaa93..f0ce1bb93b 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -16,8 +16,8 @@ import * as log from 'loglevel'; import { createLoginWindow } from './components/loginWindow'; import { - createMainWindow, saveAppArgs, + MainWindow, APP_ARGS_FILE_PATH, } from './components/mainWindow'; import { createTrayIcon } from './components/trayIcon'; @@ -89,7 +89,6 @@ if (appArgs.processEnvs) { } let mainWindow: BrowserWindow; -let setupWindow: (BrowserWindow) => void; if (typeof appArgs.flashPluginDir === 'string') { app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir); @@ -254,15 +253,12 @@ if (shouldQuit) { } async function onReady(): Promise { - const createWindowResult = await createMainWindow( + const mainWindow = await new MainWindow( appArgs, app.quit.bind(this), setDockBadge, - ); + ).create(); - log.debug('onReady', createWindowResult); - mainWindow = createWindowResult.window; - setupWindow = createWindowResult.setupWindow; createTrayIcon(appArgs, mainWindow); // Register global shortcuts @@ -382,8 +378,8 @@ app.on('browser-window-blur', (event: Event, window: BrowserWindow) => { app.on('browser-window-created', (event: Event, window: BrowserWindow) => { log.debug('app.browser-window-created', { event, window }); - if (setupWindow !== undefined) { - setupWindow(window); + if (MainWindow.setupWindow !== undefined) { + MainWindow.setupWindow(appArgs, window); } }); From 2dbe0e154f6f2b2d9640a98965a088204be85d63 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Mon, 24 May 2021 18:23:47 -0400 Subject: [PATCH 07/28] Split up the window functions/helpers/events some --- app/src/components/mainWindow.test.ts | 64 +++-- app/src/components/mainWindow.ts | 398 ++++---------------------- app/src/helpers/helpers.ts | 2 +- app/src/helpers/windowEvents.ts | 119 ++++++++ app/src/helpers/windowHelpers.ts | 218 ++++++++++++-- 5 files changed, 404 insertions(+), 397 deletions(-) create mode 100644 app/src/helpers/windowEvents.ts diff --git a/app/src/components/mainWindow.test.ts b/app/src/components/mainWindow.test.ts index 1736b2c74f..35056c8aa6 100644 --- a/app/src/components/mainWindow.test.ts +++ b/app/src/components/mainWindow.test.ts @@ -1,8 +1,14 @@ +jest.mock('../helpers/windowEvents'); jest.mock('../helpers/windowHelpers'); import { MainWindow } from './mainWindow'; import * as helpers from '../helpers/helpers'; -import { createAboutBlankWindow, createNewTab } from '../helpers/windowHelpers'; +const { onNewWindowHelper } = jest.requireActual('../helpers/windowEvents'); +import { + blockExternalUrl, + createAboutBlankWindow, + createNewTab, +} from '../helpers/windowHelpers'; describe('onNewWindowHelper', () => { const originalUrl = 'https://medium.com/'; @@ -17,25 +23,21 @@ describe('onNewWindowHelper', () => { let mockNativeTabsSupported: jest.SpyInstance = jest .spyOn(helpers, 'nativeTabsSupported') .mockImplementation(() => false); - let mockOnBlockedExternal: jest.SpyInstance; - const mockOpenExternal: jest.SpyInstance = jest.spyOn( - helpers, - 'openExternal', - ); + const mockBlockExternalUrl: jest.SpyInstance = blockExternalUrl as jest.Mock; + const mockOpenExternal: jest.SpyInstance = jest + .spyOn(helpers, 'openExternal') + .mockImplementation(); const preventDefault = jest.fn(); beforeEach(() => { mockNativeTabsSupported.mockImplementation(() => false); - mockOnBlockedExternal = jest - .spyOn(MainWindow, 'onBlockedExternalUrl') - .mockImplementation(); }); afterEach(() => { mockCreateAboutBlank.mockReset(); mockCreateNewTab.mockReset(); mockNativeTabsSupported.mockReset(); - mockOnBlockedExternal.mockReset(); + mockBlockExternalUrl.mockReset(); mockOpenExternal.mockReset(); preventDefault.mockReset(); @@ -47,8 +49,9 @@ describe('onNewWindowHelper', () => { blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, internalUrl, undefined, preventDefault, @@ -56,7 +59,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).not.toHaveBeenCalled(); }); @@ -66,8 +69,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, externalUrl, undefined, preventDefault, @@ -75,7 +79,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -85,8 +89,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: true, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, externalUrl, undefined, preventDefault, @@ -94,7 +99,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).toHaveBeenCalledTimes(1); + expect(mockBlockExternalUrl).toHaveBeenCalledTimes(1); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -104,8 +109,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, internalUrl, foregroundDisposition, preventDefault, @@ -113,7 +119,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).not.toHaveBeenCalled(); }); @@ -123,8 +129,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, externalUrl, foregroundDisposition, preventDefault, @@ -132,7 +139,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -146,8 +153,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, internalUrl, foregroundDisposition, preventDefault, @@ -157,13 +165,12 @@ describe('onNewWindowHelper', () => { expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledWith( options, - MainWindow.getDefaultWindowOptions(options), MainWindow.setupWindow, internalUrl, true, undefined, ); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -177,8 +184,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, internalUrl, backgroundDisposition, preventDefault, @@ -188,13 +196,12 @@ describe('onNewWindowHelper', () => { expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledWith( options, - MainWindow.getDefaultWindowOptions(options), MainWindow.setupWindow, internalUrl, false, undefined, ); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -204,8 +211,9 @@ describe('onNewWindowHelper', () => { targetUrl: originalUrl, blockExternalUrls: false, }; - MainWindow.onNewWindowHelper( + onNewWindowHelper( options, + MainWindow.setupWindow, 'about:blank', undefined, preventDefault, @@ -213,7 +221,7 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockOnBlockedExternal).not.toHaveBeenCalled(); + expect(mockBlockExternalUrl).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index eed3d4e223..476951cc72 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -1,43 +1,44 @@ import * as fs from 'fs'; import * as path from 'path'; -import { - BrowserWindow, - ipcMain, - dialog, - BrowserWindowConstructorOptions, - Event, - HeadersReceivedResponse, - OnHeadersReceivedListenerDetails, - WebContents, -} from 'electron'; +import { ipcMain, BrowserWindow, Event } from 'electron'; import windowStateKeeper from 'electron-window-state'; import log from 'loglevel'; import { getAppIcon, getCounterValue, - getCSSToInject, isOSX, - linkIsInternal, nativeTabsSupported, openExternal, - shouldInjectCss, } from '../helpers/helpers'; import { - adjustWindowZoom, - createAboutBlankWindow, + onNewWindow, + onWillNavigate, + onWillPreventUnload, +} from '../helpers/windowEvents'; +import { + clearAppData, + clearCache, createNewTab, createNewWindow, getCurrentUrl, + getDefaultWindowOptions, + goBack, + goForward, gotoUrl, - withFocusedWindow, + hideWindow, + injectCSS, + sendParamsOnDidFinishLoad, + setProxyRules, + zoomIn, + zoomOut, + zoomReset, } from '../helpers/windowHelpers'; import { initContextMenu } from './contextMenu'; import { createMenu } from './menu'; export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); -const ZOOM_INTERVAL = 0.1; type SessionInteractionRequest = { id?: string; @@ -70,148 +71,12 @@ export class MainWindow { this.setDockBadge = setDockBadge; } - async clearAppData(): Promise { - const response = await dialog.showMessageBox(this.window, { - type: 'warning', - buttons: ['Yes', 'Cancel'], - defaultId: 1, - title: 'Clear cache confirmation', - message: - 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', - }); - - if (response.response !== 0) { - return; - } - await this.clearCache(); - } - - async clearCache(): Promise { - const { session } = this.window.webContents; - await session.clearStorageData(); - await session.clearCache(); - } - - static getDefaultWindowOptions = ( - options, - ): BrowserWindowConstructorOptions => { - const browserwindowOptions: BrowserWindowConstructorOptions = { - ...options.browserwindowOptions, - }; - // We're going to remove this an merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences - // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences - // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself - delete browserwindowOptions.webPreferences; - - return { - // Convert dashes to spaces because on linux the app name is joined with dashes - title: options.name, - tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, - webPreferences: { - javascript: true, - plugins: true, - nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com - webSecurity: !options.insecure, - preload: path.join(__dirname, 'preload.js'), - zoomFactor: options.zoom, - ...(options.browserWindowOptions && - options.browserwindowOptions.webPreferences - ? options.browserwindowOptions.webPreferences - : {}), - }, - ...browserwindowOptions, - }; - }; - - static hideWindow = ( - window: BrowserWindow, - event: Event, - fastQuit: boolean, - tray, - ): void => { - if (isOSX() && !fastQuit) { - // this is called when exiting from clicking the cross button on the window - event.preventDefault(); - window.hide(); - } else if (!fastQuit && tray) { - event.preventDefault(); - window.hide(); - } - // will close the window on other platforms - }; - - static injectCss = (browserWindow: BrowserWindow): void => { - if (!shouldInjectCss()) { - return; - } - - const cssToInject = getCSSToInject(); - - browserWindow.webContents.on('did-navigate', () => { - log.debug( - 'browserWindow.webContents.did-navigate', - browserWindow.webContents.getURL(), - ); - // We must inject css early enough; so onHeadersReceived is a good place. - // Will run multiple times, see `did-finish-load` below that unsets this handler. - browserWindow.webContents.session.webRequest.onHeadersReceived( - { urls: [] }, // Pass an empty filter list; null will not match _any_ urls - ( - details: OnHeadersReceivedListenerDetails, - callback: (headersReceivedResponse: HeadersReceivedResponse) => void, - ) => { - log.debug( - 'browserWindow.webContents.session.webRequest.onHeadersReceived', - { details, callback }, - ); - if (details.webContents) { - details.webContents - .insertCSS(cssToInject) - .catch((err) => log.error('webContents.insertCSS ERROR', err)); - } - callback({ cancel: false, responseHeaders: details.responseHeaders }); - }, - ); - }); - }; - - static sendParamsOnDidFinishLoad = (options, window: BrowserWindow): void => { - window.webContents.on('did-finish-load', () => { - log.debug( - 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', - window.webContents.getURL(), - ); - // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 - // and https://github.com/electron/electron/pull/12679 - window.webContents - .setVisualZoomLevelLimits(1, 3) - .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - - window.webContents.send('params', JSON.stringify(options)); - }); - }; - - static setProxyRules = (browserWindow: BrowserWindow, proxyRules): void => { - browserWindow.webContents.session - .setProxy({ - proxyRules, - pacScript: '', - proxyBypassRules: '', - }) - .catch((err) => log.error('session.setProxy ERROR', err)); - }; - async create(): Promise { const mainWindowState = windowStateKeeper({ defaultWidth: this.options.width || 1280, defaultHeight: this.options.height || 800, }); - const defaultWindowOptions = MainWindow.getDefaultWindowOptions( - this.options, - ); - this.window = new BrowserWindow({ frame: !this.options.hideWindowFrame, width: mainWindowState.width, @@ -231,7 +96,7 @@ export class MainWindow { titleBarStyle: this.options.titleBarStyle, show: this.options.tray !== 'start-in-tray', backgroundColor: this.options.backgroundColor, - ...defaultWindowOptions, + ...getDefaultWindowOptions(this.options), }); mainWindowState.manage(this.window); @@ -250,17 +115,17 @@ export class MainWindow { const menuOptions = { nativefierVersion: this.options.nativefierVersion, appQuit: this.onAppQuit, - clearAppData: this.clearAppData.bind(this), + clearAppData: () => clearAppData(this.window), disableDevTools: this.options.disableDevTools, getCurrentUrl, - goBack: MainWindow.onGoBack, - goForward: MainWindow.onGoForward, + goBack, + goForward, gotoUrl, openExternal, zoomBuildTimeValue: this.options.zoom, - zoomIn: MainWindow.onZoomIn, - zoomOut: MainWindow.onZoomOut, - zoomReset: MainWindow.onZoomReset, + zoomIn, + zoomOut, + zoomReset, }; createMenu(menuOptions); @@ -271,7 +136,6 @@ export class MainWindow { ? (url: string, foreground: boolean) => createNewTab( this.options, - MainWindow.getDefaultWindowOptions(this.options), MainWindow.setupWindow, url, foreground, @@ -386,7 +250,7 @@ export class MainWindow { ); if (this.options.clearCache) { - await this.clearCache(); + await clearCache(this.window); } await this.window.loadURL(this.options.targetUrl); @@ -398,203 +262,38 @@ export class MainWindow { this.window.moveTabToNewWindow(); } this.window.setFullScreen(false); - this.window.once( - 'leave-full-screen', - MainWindow.hideWindow.bind(this.window, event, this.options.fastQuit), + this.window.once('leave-full-screen', (event: Event) => + hideWindow( + this.window, + event, + this.options.fastQuit, + this.options.tray, + ), ); } - MainWindow.hideWindow( - this.window, - event, - this.options.fastQuit, - this.options.tray, - ); + hideWindow(this.window, event, this.options.fastQuit, this.options.tray); if (this.options.clearCache) { - this.clearCache().catch((err) => log.error('clearCache ERROR', err)); + clearCache(this.window).catch((err) => + log.error('clearCache ERROR', err), + ); } }); return this.window; } - static onBlockedExternalUrl = (url: string) => { - log.debug('onBlockedExternalUrl', url); - withFocusedWindow((focusedWindow) => { - dialog - .showMessageBox(focusedWindow, { - message: `Cannot navigate to external URL: ${url}`, - type: 'error', - title: 'Navigation blocked', - }) - .catch((err) => log.error('dialog.showMessageBox ERROR', err)); - }); - }; - - static onGoBack = (): void => { - log.debug('onGoBack'); - withFocusedWindow((focusedWindow) => { - focusedWindow.webContents.goBack(); - }); - }; - - static onGoForward = (): void => { - log.debug('onGoForward'); - withFocusedWindow((focusedWindow) => { - focusedWindow.webContents.goForward(); - }); - }; - - static onNewWindow = ( - options, - event: Event & { newGuest?: any }, - urlToGo: string, - frameName: string, - disposition: - | 'default' - | 'foreground-tab' - | 'background-tab' - | 'new-window' - | 'save-to-disk' - | 'other', - parent?: BrowserWindow, - ): void => { - log.debug('onNewWindow', { - event, - urlToGo, - frameName, - disposition, - parent, - }); - const preventDefault = (newGuest: any): void => { - event.preventDefault(); - if (newGuest) { - event.newGuest = newGuest; - } - }; - MainWindow.onNewWindowHelper( - options, - urlToGo, - disposition, - preventDefault, - parent, - ); - }; - - static onNewWindowHelper = ( - options, - urlToGo: string, - disposition: string, - preventDefault, - parent?: BrowserWindow, - ): void => { - log.debug('onNewWindowHelper', { - urlToGo, - disposition, - preventDefault, - parent, - }); - if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { - preventDefault(); - if (options.blockExternalUrls) { - MainWindow.onBlockedExternalUrl(urlToGo); - } else { - openExternal(urlToGo); - } - } else if (urlToGo === 'about:blank') { - const newWindow = createAboutBlankWindow( - options, - MainWindow.getDefaultWindowOptions(options), - parent, - ); - preventDefault(newWindow); - } else if (nativeTabsSupported()) { - if (disposition === 'background-tab') { - const newTab = createNewTab( - options, - MainWindow.getDefaultWindowOptions(options), - MainWindow.setupWindow, - urlToGo, - false, - parent, - ); - preventDefault(newTab); - } else if (disposition === 'foreground-tab') { - const newTab = createNewTab( - options, - MainWindow.getDefaultWindowOptions(options), - MainWindow.setupWindow, - urlToGo, - true, - parent, - ); - preventDefault(newTab); - } - } - }; - - static onWillNavigate = (options, event: Event, urlToGo: string): void => { - log.debug('onWillNavigate', { options, event, urlToGo }); - if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { - event.preventDefault(); - if (options.blockExternalUrls) { - MainWindow.onBlockedExternalUrl(urlToGo); - } else { - openExternal(urlToGo); - } - } - }; - - static onWillPreventUnload = (event: Event): void => { - log.debug('onWillPreventUnload', event); - const eventAny = event as any; - if (eventAny.sender === undefined) { - return; - } - const webContents: WebContents = eventAny.sender; - const browserWindow = BrowserWindow.fromWebContents(webContents); - const choice = dialog.showMessageBoxSync(browserWindow, { - type: 'question', - buttons: ['Proceed', 'Stay'], - message: - 'You may have unsaved changes, are you sure you want to proceed?', - title: 'Changes you made may not be saved.', - defaultId: 0, - cancelId: 1, - }); - if (choice === 0) { - event.preventDefault(); - } - }; - - static onZoomOut = (): void => { - log.debug('onZoomOut'); - adjustWindowZoom(-ZOOM_INTERVAL); - }; - - static onZoomReset = (options): void => { - log.debug('onZoomReset'); - withFocusedWindow((focusedWindow: BrowserWindow) => { - focusedWindow.webContents.zoomFactor = options.zoom; - }); - }; - - static onZoomIn = (): void => { - log.debug('onZoomIn'); - adjustWindowZoom(ZOOM_INTERVAL); - }; - static setupWindow = (options, window: BrowserWindow): void => { if (options.userAgent) { window.webContents.userAgent = options.userAgent; } if (options.proxyRules) { - MainWindow.setProxyRules(window, options.proxyRules); + setProxyRules(window, options.proxyRules); } - MainWindow.injectCss(window); - MainWindow.sendParamsOnDidFinishLoad(options, window); + injectCSS(window); + sendParamsOnDidFinishLoad(options, window); // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) // We can't quite cut over to that yet for a few reasons: @@ -609,15 +308,19 @@ export class MainWindow { window.webContents.on( 'new-window', () => (event, url, frameName, disposition) => - MainWindow.onNewWindow(options, event, url, frameName, disposition), + onNewWindow( + options, + MainWindow.setupWindow, + event, + url, + frameName, + disposition, + ), ); window.webContents.on('will-navigate', (event: Event, url: string) => - MainWindow.onWillNavigate(options, event, url), - ); - window.webContents.on( - 'will-prevent-unload', - MainWindow.onWillPreventUnload, + onWillNavigate(options, event, url), ); + window.webContents.on('will-prevent-unload', onWillPreventUnload); window.webContents.on('did-finish-load', () => { log.debug('mainWindow.webContents.did-finish-load'); @@ -636,7 +339,6 @@ export class MainWindow { this.window.on('new-tab', () => createNewTab( options, - MainWindow.getDefaultWindowOptions(options), MainWindow.setupWindow, options.targetUrl, true, diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index 9f4454d27d..9b006c60fe 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -162,7 +162,7 @@ export function openExternal(url: string, options?: OpenExternalOptions): void { .catch((err) => log.error('openExternal ERROR', err)); } -export function shouldInjectCss(): boolean { +export function shouldInjectCSS(): boolean { try { return fs.existsSync(INJECT_DIR); } catch (e) { diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts new file mode 100644 index 0000000000..3a5cb53901 --- /dev/null +++ b/app/src/helpers/windowEvents.ts @@ -0,0 +1,119 @@ +import { dialog, BrowserWindow, WebContents } from 'electron'; +import log from 'loglevel'; +import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; +import { + blockExternalUrl, + createAboutBlankWindow, + createNewTab, + getDefaultWindowOptions, +} from './windowHelpers'; + +export function onNewWindow( + options, + setupWindow, + event: Event & { newGuest?: any }, + urlToGo: string, + frameName: string, + disposition: + | 'default' + | 'foreground-tab' + | 'background-tab' + | 'new-window' + | 'save-to-disk' + | 'other', + parent?: BrowserWindow, +): void { + log.debug('onNewWindow', { + event, + urlToGo, + frameName, + disposition, + parent, + }); + const preventDefault = (newGuest: any): void => { + event.preventDefault(); + if (newGuest) { + event.newGuest = newGuest; + } + }; + onNewWindowHelper( + options, + setupWindow, + urlToGo, + disposition, + preventDefault, + parent, + ); +} + +export function onNewWindowHelper( + options, + setupWindow, + urlToGo: string, + disposition: string, + preventDefault, + parent?: BrowserWindow, +): void { + log.debug('onNewWindowHelper', { + urlToGo, + disposition, + preventDefault, + parent, + }); + if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { + preventDefault(); + if (options.blockExternalUrls) { + blockExternalUrl(urlToGo); + } else { + openExternal(urlToGo); + } + } else if (urlToGo === 'about:blank') { + const newWindow = createAboutBlankWindow( + options, + getDefaultWindowOptions(options), + parent, + ); + preventDefault(newWindow); + } else if (nativeTabsSupported()) { + if (disposition === 'background-tab') { + const newTab = createNewTab(options, setupWindow, urlToGo, false, parent); + preventDefault(newTab); + } else if (disposition === 'foreground-tab') { + const newTab = createNewTab(options, setupWindow, urlToGo, true, parent); + preventDefault(newTab); + } + } +} + +export function onWillNavigate(options, event: Event, urlToGo: string): void { + log.debug('onWillNavigate', { options, event, urlToGo }); + if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { + event.preventDefault(); + if (options.blockExternalUrls) { + blockExternalUrl(urlToGo); + } else { + openExternal(urlToGo); + } + } +} + +export function onWillPreventUnload(event: Event): void { + log.debug('onWillPreventUnload', event); + const eventAny = event as any; + if (eventAny.sender === undefined) { + return; + } + const webContents: WebContents = eventAny.sender; + const browserWindow = BrowserWindow.fromWebContents(webContents); + const choice = dialog.showMessageBoxSync(browserWindow, { + type: 'question', + buttons: ['Proceed', 'Stay'], + message: 'You may have unsaved changes, are you sure you want to proceed?', + title: 'Changes you made may not be saved.', + defaultId: 0, + cancelId: 1, + }); + if (choice === 0) { + event.preventDefault(); + } +} diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index 22d5ec4753..8a4fde6d11 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -1,6 +1,21 @@ -import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron'; +import { + BrowserWindow, + BrowserWindowConstructorOptions, + dialog, + HeadersReceivedResponse, + OnHeadersReceivedListenerDetails, +} from 'electron'; -import * as log from 'loglevel'; +import log from 'loglevel'; +import path from 'path'; +import { + getCSSToInject, + isOSX, + nativeTabsSupported, + shouldInjectCSS, +} from './helpers'; + +const ZOOM_INTERVAL = 0.1; export function adjustWindowZoom(adjustment: number): void { withFocusedWindow( @@ -10,19 +25,46 @@ export function adjustWindowZoom(adjustment: number): void { ); } +export function blockExternalUrl(url: string) { + withFocusedWindow((focusedWindow) => { + dialog + .showMessageBox(focusedWindow, { + message: `Cannot navigate to external URL: ${url}`, + type: 'error', + title: 'Navigation blocked', + }) + .catch((err) => log.error('dialog.showMessageBox ERROR', err)); + }); +} + +export async function clearAppData(window: BrowserWindow): Promise { + const response = await dialog.showMessageBox(window, { + type: 'warning', + buttons: ['Yes', 'Cancel'], + defaultId: 1, + title: 'Clear cache confirmation', + message: + 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', + }); + + if (response.response !== 0) { + return; + } + await clearCache(window); +} + +export async function clearCache(window: BrowserWindow): Promise { + const { session } = window.webContents; + await session.clearStorageData(); + await session.clearCache(); +} + export function createAboutBlankWindow( options, - browserWindowConstructorOptions: BrowserWindowConstructorOptions, setupWindow, parent?: BrowserWindow, ): BrowserWindow { - const window = createNewWindow( - options, - browserWindowConstructorOptions, - setupWindow, - 'about:blank', - parent, - ); + const window = createNewWindow(options, setupWindow, 'about:blank', parent); setupWindow(options, window); window.show(); window.focus(); @@ -31,7 +73,6 @@ export function createAboutBlankWindow( export function createNewTab( options, - browserWindowConstructorOptions: BrowserWindowConstructorOptions, setupWindow, url: string, foreground: boolean, @@ -39,13 +80,7 @@ export function createNewTab( ): BrowserWindow { log.debug('createNewTab', { url, foreground, parent }); withFocusedWindow((focusedWindow) => { - const newTab = createNewWindow( - options, - browserWindowConstructorOptions, - setupWindow, - url, - parent, - ); + const newTab = createNewWindow(options, setupWindow, url, parent); focusedWindow.addTabbedWindow(newTab); if (!foreground) { focusedWindow.focus(); @@ -57,7 +92,6 @@ export function createNewTab( export function createNewWindow( options, - browserWindowConstructorOptions: BrowserWindowConstructorOptions, setupWindow, url: string, parent?: BrowserWindow, @@ -65,7 +99,7 @@ export function createNewWindow( log.debug('createNewWindow', { url, parent }); const window = new BrowserWindow({ parent, - ...browserWindowConstructorOptions, + ...getDefaultWindowOptions(options), }); setupWindow(options, window); window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); @@ -78,10 +112,137 @@ export function getCurrentUrl(): string { ) as unknown as string; } +export function getDefaultWindowOptions( + options, +): BrowserWindowConstructorOptions { + const browserwindowOptions: BrowserWindowConstructorOptions = { + ...options.browserwindowOptions, + }; + // We're going to remove this an merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences + // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences + // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself + delete browserwindowOptions.webPreferences; + + return { + // Convert dashes to spaces because on linux the app name is joined with dashes + title: options.name, + tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, + webPreferences: { + javascript: true, + plugins: true, + nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com + webSecurity: !options.insecure, + preload: path.join(__dirname, 'preload.js'), + zoomFactor: options.zoom, + ...(options.browserWindowOptions && + options.browserwindowOptions.webPreferences + ? options.browserwindowOptions.webPreferences + : {}), + }, + ...browserwindowOptions, + }; +} + +export function goBack(): void { + log.debug('onGoBack'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goBack(); + }); +} + +export function goForward(): void { + log.debug('onGoForward'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goForward(); + }); +} + export function gotoUrl(url: string): void { return withFocusedWindow((focusedWindow) => void focusedWindow.loadURL(url)); } +export function hideWindow( + window: BrowserWindow, + event: Event, + fastQuit: boolean, + tray, +): void { + if (isOSX() && !fastQuit) { + // this is called when exiting from clicking the cross button on the window + event.preventDefault(); + window.hide(); + } else if (!fastQuit && tray) { + event.preventDefault(); + window.hide(); + } + // will close the window on other platforms +} + +export function injectCSS(browserWindow: BrowserWindow): void { + if (!shouldInjectCSS()) { + return; + } + + const cssToInject = getCSSToInject(); + + browserWindow.webContents.on('did-navigate', () => { + log.debug( + 'browserWindow.webContents.did-navigate', + browserWindow.webContents.getURL(), + ); + // We must inject css early enough; so onHeadersReceived is a good place. + // Will run multiple times, see `did-finish-load` below that unsets this handler. + browserWindow.webContents.session.webRequest.onHeadersReceived( + { urls: [] }, // Pass an empty filter list; null will not match _any_ urls + ( + details: OnHeadersReceivedListenerDetails, + callback: (headersReceivedResponse: HeadersReceivedResponse) => void, + ) => { + log.debug( + 'browserWindow.webContents.session.webRequest.onHeadersReceived', + { details, callback }, + ); + if (details.webContents) { + details.webContents + .insertCSS(cssToInject) + .catch((err) => log.error('webContents.insertCSS ERROR', err)); + } + callback({ cancel: false, responseHeaders: details.responseHeaders }); + }, + ); + }); +} + +export function sendParamsOnDidFinishLoad( + options, + window: BrowserWindow, +): void { + window.webContents.on('did-finish-load', () => { + log.debug( + 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', + window.webContents.getURL(), + ); + // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 + // and https://github.com/electron/electron/pull/12679 + window.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); + + window.webContents.send('params', JSON.stringify(options)); + }); +} + +export function setProxyRules(window: BrowserWindow, proxyRules): void { + window.webContents.session + .setProxy({ + proxyRules, + pacScript: '', + proxyBypassRules: '', + }) + .catch((err) => log.error('session.setProxy ERROR', err)); +} + export function withFocusedWindow( block: (window: BrowserWindow) => void, ): void { @@ -91,3 +252,20 @@ export function withFocusedWindow( } return undefined; } + +export function zoomOut(): void { + log.debug('zoomOut'); + adjustWindowZoom(-ZOOM_INTERVAL); +} + +export function zoomReset(options): void { + log.debug('zoomReset'); + withFocusedWindow((focusedWindow: BrowserWindow) => { + focusedWindow.webContents.zoomFactor = options.zoom; + }); +} + +export function zoomIn(): void { + log.debug('zoomIn'); + adjustWindowZoom(ZOOM_INTERVAL); +} From 3d841708bfdb508b8560252f35976ad113733f55 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 10:02:01 -0400 Subject: [PATCH 08/28] Further separate out window functions + tests --- app/src/components/mainWindow.ts | 133 ++++++++---------- app/src/components/menu.ts | 8 +- .../windowEvents.test.ts} | 94 +++++++------ app/src/helpers/windowEvents.ts | 21 +-- app/src/helpers/windowHelpers.ts | 9 +- app/src/main.ts | 25 ++-- 6 files changed, 144 insertions(+), 146 deletions(-) rename app/src/{components/mainWindow.test.ts => helpers/windowEvents.test.ts} (74%) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 476951cc72..201234503e 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ipcMain, BrowserWindow, Event } from 'electron'; +import { ipcMain, BrowserWindow, IpcMainEvent } from 'electron'; import windowStateKeeper from 'electron-window-state'; import log from 'loglevel'; @@ -22,11 +22,11 @@ import { clearCache, createNewTab, createNewWindow, - getCurrentUrl, + getCurrentURL, getDefaultWindowOptions, goBack, goForward, - gotoUrl, + goToURL, hideWindow, injectCSS, sendParamsOnDidFinishLoad, @@ -117,10 +117,10 @@ export class MainWindow { appQuit: this.onAppQuit, clearAppData: () => clearAppData(this.window), disableDevTools: this.options.disableDevTools, - getCurrentUrl, + getCurrentURL, goBack, goForward, - gotoUrl, + goToURL, openExternal, zoomBuildTimeValue: this.options.zoom, zoomIn, @@ -136,7 +136,7 @@ export class MainWindow { ? (url: string, foreground: boolean) => createNewTab( this.options, - MainWindow.setupWindow, + setupWindow, url, foreground, this.window, @@ -147,7 +147,7 @@ export class MainWindow { ); } - MainWindow.setupWindow(this.options, this.window); + setupWindow(this.options, this.window); if (this.options.counter) { this.window.on('page-title-updated', (event, title) => { @@ -255,14 +255,14 @@ export class MainWindow { await this.window.loadURL(this.options.targetUrl); - this.window.on('close', (event) => { + this.window.on('close', (event: IpcMainEvent) => { log.debug('mainWindow.close', event); if (this.window.isFullScreen()) { if (nativeTabsSupported()) { this.window.moveTabToNewWindow(); } this.window.setFullScreen(false); - this.window.once('leave-full-screen', (event: Event) => + this.window.once('leave-full-screen', (event: IpcMainEvent) => hideWindow( this.window, event, @@ -282,70 +282,6 @@ export class MainWindow { return this.window; } - - static setupWindow = (options, window: BrowserWindow): void => { - if (options.userAgent) { - window.webContents.userAgent = options.userAgent; - } - - if (options.proxyRules) { - setProxyRules(window, options.proxyRules); - } - - injectCSS(window); - sendParamsOnDidFinishLoad(options, window); - - // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) - // We can't quite cut over to that yet for a few reasons: - // 1. Our version of Electron does not yet support a parameter to - // setWindowOpenHandler that contains `disposition', which we need. - // See https://github.com/electron/electron/issues/28380 - // 2. setWindowOpenHandler doesn't support newGuest as well - // Though at this point, 'new-window' bugs seem to be coming up and downstream - // users are being pointed to use setWindowOpenHandler. - // E.g., https://github.com/electron/electron/issues/28374 - - window.webContents.on( - 'new-window', - () => (event, url, frameName, disposition) => - onNewWindow( - options, - MainWindow.setupWindow, - event, - url, - frameName, - disposition, - ), - ); - window.webContents.on('will-navigate', (event: Event, url: string) => - onWillNavigate(options, event, url), - ); - window.webContents.on('will-prevent-unload', onWillPreventUnload); - - window.webContents.on('did-finish-load', () => { - log.debug('mainWindow.webContents.did-finish-load'); - // Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 - // and https://github.com/electron/electron/pull/12679 - window.webContents - .setVisualZoomLevelLimits(1, 3) - .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - - // Remove potential css injection code set in `did-navigate`) (see injectCss code) - window.webContents.session.webRequest.onHeadersReceived(null); - }); - - // @ts-ignore new-tab isn't in the type definition, but it does exist - this.window.on('new-tab', () => - createNewTab( - options, - MainWindow.setupWindow, - options.targetUrl, - true, - window, - ), - ); - }; } export function saveAppArgs(newAppArgs: any) { @@ -360,3 +296,54 @@ export function saveAppArgs(newAppArgs: any) { ); } } + +export function setupWindow(options, window: BrowserWindow): void { + if (options.userAgent) { + window.webContents.userAgent = options.userAgent; + } + + if (options.proxyRules) { + setProxyRules(window, options.proxyRules); + } + + injectCSS(window); + sendParamsOnDidFinishLoad(options, window); + + // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) + // We can't quite cut over to that yet for a few reasons: + // 1. Our version of Electron does not yet support a parameter to + // setWindowOpenHandler that contains `disposition', which we need. + // See https://github.com/electron/electron/issues/28380 + // 2. setWindowOpenHandler doesn't support newGuest as well + // Though at this point, 'new-window' bugs seem to be coming up and downstream + // users are being pointed to use setWindowOpenHandler. + // E.g., https://github.com/electron/electron/issues/28374 + + window.webContents.on( + 'new-window', + () => (event, url, frameName, disposition) => + onNewWindow(options, this, event, url, frameName, disposition), + ); + window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => + onWillNavigate(options, event, url), + ); + window.webContents.on('will-prevent-unload', onWillPreventUnload); + + window.webContents.on('did-finish-load', () => { + log.debug('mainWindow.webContents.did-finish-load'); + // Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 + // and https://github.com/electron/electron/pull/12679 + window.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); + + // Remove potential css injection code set in `did-navigate`) (see injectCss code) + window.webContents.session.webRequest.onHeadersReceived(null); + }); + + // @ts-ignore new-tab isn't in the type definition, but it does exist + this.window.on('new-tab', () => + createNewTab(options, this, options.targetUrl, true, window), + ); +} diff --git a/app/src/components/menu.ts b/app/src/components/menu.ts index e31d71c729..c7d6595d7d 100644 --- a/app/src/components/menu.ts +++ b/app/src/components/menu.ts @@ -28,8 +28,8 @@ export function createMenu({ zoomBuildTimeValue, goBack, goForward, - getCurrentUrl, - gotoUrl, + getCurrentURL, + goToURL, clearAppData, disableDevTools, openExternal, @@ -69,7 +69,7 @@ export function createMenu({ label: 'Copy Current URL', accelerator: 'CmdOrCtrl+L', click: () => { - const currentURL = getCurrentUrl(); + const currentURL = getCurrentURL(); clipboard.writeText(currentURL); }, }, @@ -332,7 +332,7 @@ export function createMenu({ return { label: bookmark.title, click: () => { - gotoUrl(bookmark.url); + goToURL(bookmark.url); }, accelerator: accelerator, }; diff --git a/app/src/components/mainWindow.test.ts b/app/src/helpers/windowEvents.test.ts similarity index 74% rename from app/src/components/mainWindow.test.ts rename to app/src/helpers/windowEvents.test.ts index 35056c8aa6..b6cbc450b0 100644 --- a/app/src/components/mainWindow.test.ts +++ b/app/src/helpers/windowEvents.test.ts @@ -1,19 +1,19 @@ jest.mock('../helpers/windowEvents'); jest.mock('../helpers/windowHelpers'); -import { MainWindow } from './mainWindow'; -import * as helpers from '../helpers/helpers'; -const { onNewWindowHelper } = jest.requireActual('../helpers/windowEvents'); +import * as helpers from './helpers'; +const { onNewWindowHelper, onWillPreventUnload } = + jest.requireActual('./windowEvents'); import { - blockExternalUrl, + blockExternalURL, createAboutBlankWindow, createNewTab, -} from '../helpers/windowHelpers'; +} from './windowHelpers'; describe('onNewWindowHelper', () => { - const originalUrl = 'https://medium.com/'; - const internalUrl = 'https://medium.com/topics/technology'; - const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; + const originalURL = 'https://medium.com/'; + const internalURL = 'https://medium.com/topics/technology'; + const externalURL = 'https://www.wikipedia.org/wiki/Electron'; const foregroundDisposition = 'foreground-tab'; const backgroundDisposition = 'background-tab'; @@ -23,11 +23,12 @@ describe('onNewWindowHelper', () => { let mockNativeTabsSupported: jest.SpyInstance = jest .spyOn(helpers, 'nativeTabsSupported') .mockImplementation(() => false); - const mockBlockExternalUrl: jest.SpyInstance = blockExternalUrl as jest.Mock; + const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock; const mockOpenExternal: jest.SpyInstance = jest .spyOn(helpers, 'openExternal') .mockImplementation(); const preventDefault = jest.fn(); + const setupWindow = jest.fn(); beforeEach(() => { mockNativeTabsSupported.mockImplementation(() => false); @@ -37,109 +38,109 @@ describe('onNewWindowHelper', () => { mockCreateAboutBlank.mockReset(); mockCreateNewTab.mockReset(); mockNativeTabsSupported.mockReset(); - mockBlockExternalUrl.mockReset(); + mockBlockExternalURL.mockReset(); mockOpenExternal.mockReset(); - preventDefault.mockReset(); + setupWindow.mockReset(); }); test('internal urls should not be handled', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, undefined, preventDefault, ); expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).not.toHaveBeenCalled(); }); test('external urls should be opened externally', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - externalUrl, + setupWindow, + externalURL, undefined, preventDefault, ); expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(preventDefault).toHaveBeenCalledTimes(1); }); test('external urls should be ignored if blockExternalUrls is true', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: true, }; onNewWindowHelper( options, - MainWindow.setupWindow, - externalUrl, + setupWindow, + externalURL, undefined, preventDefault, ); expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).toHaveBeenCalledTimes(1); + expect(mockBlockExternalURL).toHaveBeenCalledTimes(1); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); test('tab disposition should be ignored if tabs are not enabled', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, foregroundDisposition, preventDefault, ); expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).not.toHaveBeenCalled(); }); test('tab disposition should be ignored if url is external', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - externalUrl, + setupWindow, + externalURL, foregroundDisposition, preventDefault, ); expect(mockCreateAboutBlank).not.toHaveBeenCalled(); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).toHaveBeenCalledTimes(1); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -150,13 +151,13 @@ describe('onNewWindowHelper', () => { ); const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, foregroundDisposition, preventDefault, ); @@ -165,12 +166,12 @@ describe('onNewWindowHelper', () => { expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledWith( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, true, undefined, ); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); @@ -181,13 +182,13 @@ describe('onNewWindowHelper', () => { ); const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, backgroundDisposition, preventDefault, ); @@ -196,24 +197,24 @@ describe('onNewWindowHelper', () => { expect(mockCreateNewTab).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).toHaveBeenCalledWith( options, - MainWindow.setupWindow, - internalUrl, + setupWindow, + internalURL, false, undefined, ); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); test('about:blank urls should be handled', () => { const options = { - targetUrl: originalUrl, + targetUrl: originalURL, blockExternalUrls: false, }; onNewWindowHelper( options, - MainWindow.setupWindow, + setupWindow, 'about:blank', undefined, preventDefault, @@ -221,8 +222,9 @@ describe('onNewWindowHelper', () => { expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); expect(mockCreateNewTab).not.toHaveBeenCalled(); - expect(mockBlockExternalUrl).not.toHaveBeenCalled(); + expect(mockBlockExternalURL).not.toHaveBeenCalled(); expect(mockOpenExternal).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledTimes(1); }); }); + diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index 3a5cb53901..d052d9f31c 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -1,8 +1,8 @@ -import { dialog, BrowserWindow, WebContents } from 'electron'; +import { dialog, BrowserWindow, IpcMainEvent, WebContents } from 'electron'; import log from 'loglevel'; import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; import { - blockExternalUrl, + blockExternalURL, createAboutBlankWindow, createNewTab, getDefaultWindowOptions, @@ -63,7 +63,7 @@ export function onNewWindowHelper( if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { preventDefault(); if (options.blockExternalUrls) { - blockExternalUrl(urlToGo); + blockExternalURL(urlToGo); } else { openExternal(urlToGo); } @@ -85,25 +85,28 @@ export function onNewWindowHelper( } } -export function onWillNavigate(options, event: Event, urlToGo: string): void { +export function onWillNavigate( + options, + event: IpcMainEvent, + urlToGo: string, +): void { log.debug('onWillNavigate', { options, event, urlToGo }); if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { event.preventDefault(); if (options.blockExternalUrls) { - blockExternalUrl(urlToGo); + blockExternalURL(urlToGo); } else { openExternal(urlToGo); } } } -export function onWillPreventUnload(event: Event): void { +export function onWillPreventUnload(event: IpcMainEvent): void { log.debug('onWillPreventUnload', event); - const eventAny = event as any; - if (eventAny.sender === undefined) { + if (!event.sender) { return; } - const webContents: WebContents = eventAny.sender; + const webContents: WebContents = event.sender; const browserWindow = BrowserWindow.fromWebContents(webContents); const choice = dialog.showMessageBoxSync(browserWindow, { type: 'question', diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index 8a4fde6d11..b124eff517 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -3,6 +3,7 @@ import { BrowserWindowConstructorOptions, dialog, HeadersReceivedResponse, + IpcMainEvent, OnHeadersReceivedListenerDetails, } from 'electron'; @@ -25,7 +26,7 @@ export function adjustWindowZoom(adjustment: number): void { ); } -export function blockExternalUrl(url: string) { +export function blockExternalURL(url: string) { withFocusedWindow((focusedWindow) => { dialog .showMessageBox(focusedWindow, { @@ -106,7 +107,7 @@ export function createNewWindow( return window; } -export function getCurrentUrl(): string { +export function getCurrentURL(): string { return withFocusedWindow((focusedWindow) => focusedWindow.webContents.getURL(), ) as unknown as string; @@ -157,13 +158,13 @@ export function goForward(): void { }); } -export function gotoUrl(url: string): void { +export function goToURL(url: string): void { return withFocusedWindow((focusedWindow) => void focusedWindow.loadURL(url)); } export function hideWindow( window: BrowserWindow, - event: Event, + event: IpcMainEvent, fastQuit: boolean, tray, ): void { diff --git a/app/src/main.ts b/app/src/main.ts index f0ce1bb93b..23607a4d79 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -10,6 +10,7 @@ import { globalShortcut, systemPreferences, BrowserWindow, + IpcMainEvent, } from 'electron'; import electronDownload from 'electron-dl'; import * as log from 'loglevel'; @@ -19,6 +20,7 @@ import { saveAppArgs, MainWindow, APP_ARGS_FILE_PATH, + setupWindow, } from './components/mainWindow'; import { createTrayIcon } from './components/trayIcon'; import { isOSX } from './helpers/helpers'; @@ -357,7 +359,7 @@ app.on('login', (event, webContents, request, authInfo, callback) => { app.on( 'accessibility-support-changed', - (event: Event, accessibilitySupportEnabled: boolean) => { + (event: IpcMainEvent, accessibilitySupportEnabled: boolean) => { log.debug('app.accessibility-support-changed', { event, accessibilitySupportEnabled, @@ -367,22 +369,25 @@ app.on( app.on( 'activity-was-continued', - (event: Event, type: string, userInfo: any) => { + (event: IpcMainEvent, type: string, userInfo: any) => { log.debug('app.activity-was-continued', { event, type, userInfo }); }, ); -app.on('browser-window-blur', (event: Event, window: BrowserWindow) => { +app.on('browser-window-blur', (event: IpcMainEvent, window: BrowserWindow) => { log.debug('app.browser-window-blur', { event, window }); }); -app.on('browser-window-created', (event: Event, window: BrowserWindow) => { - log.debug('app.browser-window-created', { event, window }); - if (MainWindow.setupWindow !== undefined) { - MainWindow.setupWindow(appArgs, window); - } -}); +app.on( + 'browser-window-created', + (event: IpcMainEvent, window: BrowserWindow) => { + log.debug('app.browser-window-created', { event, window }); + if (setupWindow !== undefined) { + setupWindow(appArgs, window); + } + }, +); -app.on('browser-window-focus', (event: Event, window: BrowserWindow) => { +app.on('browser-window-focus', (event: IpcMainEvent, window: BrowserWindow) => { log.debug('app.browser-window-focus', { event, window }); }); From 52b5cd8ff95534b72b8b5b63f40e5df9c7d4af8f Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 12:28:37 -0400 Subject: [PATCH 09/28] Add a mock for unit testing functions that access electron --- app/src/mocks/electron.ts | 29 +++++++++++++++++++++++++++++ package.json | 3 +++ 2 files changed, 32 insertions(+) create mode 100644 app/src/mocks/electron.ts diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts new file mode 100644 index 0000000000..a7da1b2629 --- /dev/null +++ b/app/src/mocks/electron.ts @@ -0,0 +1,29 @@ +import { EventEmitter } from 'events'; + +class MockBrowserWindow extends EventEmitter { + static fromWebContents( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + webContents: MockWebContents, + ): MockBrowserWindow { + return new MockBrowserWindow(); + } +} + +class MockDialog { + static showMessageBoxSync = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + browserWindow: MockBrowserWindow, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: any, + ): number => { + return undefined; + }; +} + +class MockWebContents extends EventEmitter {} + +export { + MockDialog as dialog, + MockBrowserWindow as BrowserWindow, + MockWebContents as WebContents, +}; diff --git a/package.json b/package.json index 7516d5ca5e..d899f84bfe 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,9 @@ }, "jest": { "collectCoverage": true, + "moduleNameMapper": { + "^electron$": "/app/dist/mocks/electron.js" + }, "setupFiles": [ "./lib/jestSetupFiles" ], From 6506b271bf2f3f17a2c4644ba9bd2047a0ed633e Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 12:29:09 -0400 Subject: [PATCH 10/28] Add unit tests for onWillPreventUnload --- app/src/helpers/windowEvents.test.ts | 60 ++++++++++++++++++++++++++++ app/src/helpers/windowEvents.ts | 7 +++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/src/helpers/windowEvents.test.ts b/app/src/helpers/windowEvents.test.ts index b6cbc450b0..418bcb815b 100644 --- a/app/src/helpers/windowEvents.test.ts +++ b/app/src/helpers/windowEvents.test.ts @@ -1,6 +1,7 @@ jest.mock('../helpers/windowEvents'); jest.mock('../helpers/windowHelpers'); +import { dialog, BrowserWindow, WebContents } from 'electron'; import * as helpers from './helpers'; const { onNewWindowHelper, onWillPreventUnload } = jest.requireActual('./windowEvents'); @@ -44,6 +45,14 @@ describe('onNewWindowHelper', () => { setupWindow.mockReset(); }); + afterAll(() => { + mockCreateAboutBlank.mockRestore(); + mockCreateNewTab.mockRestore(); + mockNativeTabsSupported.mockRestore(); + mockBlockExternalURL.mockRestore(); + mockOpenExternal.mockRestore(); + }); + test('internal urls should not be handled', () => { const options = { targetUrl: originalURL, @@ -228,3 +237,54 @@ describe('onNewWindowHelper', () => { }); }); +describe('onWillPreventUnload', () => { + const mockFromWebContents: jest.SpyInstance = jest + .spyOn(BrowserWindow, 'fromWebContents') + .mockImplementation(() => new BrowserWindow()); + const mockShowDialog: jest.SpyInstance = jest + .spyOn(dialog, 'showMessageBoxSync') + .mockImplementation(); + const preventDefault: jest.SpyInstance = jest.fn(); + + afterEach(() => { + mockFromWebContents.mockReset(); + mockShowDialog.mockReset(); + preventDefault.mockReset(); + }); + + afterAll(() => { + mockFromWebContents.mockRestore(); + mockShowDialog.mockRestore(); + }); + + test('with no sender', () => { + const event = {}; + onWillPreventUnload(event); + + expect(mockFromWebContents).not.toHaveBeenCalled(); + expect(mockShowDialog).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('shows dialog and calls preventDefault on ok', () => { + mockShowDialog.mockImplementation(() => 0); + + const event = { preventDefault, sender: new WebContents() }; + onWillPreventUnload(event); + + expect(mockFromWebContents).toHaveBeenCalledWith(event.sender); + expect(mockShowDialog).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledWith(); + }); + + test('shows dialog and does not call preventDefault on cancel', () => { + mockShowDialog.mockImplementation(() => 1); + + const event = { preventDefault, sender: new WebContents() }; + onWillPreventUnload(event); + + expect(mockFromWebContents).toHaveBeenCalledWith(event.sender); + expect(mockShowDialog).toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); +}); diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index d052d9f31c..698236732f 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -1,5 +1,6 @@ import { dialog, BrowserWindow, IpcMainEvent, WebContents } from 'electron'; import log from 'loglevel'; + import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; import { blockExternalURL, @@ -103,10 +104,12 @@ export function onWillNavigate( export function onWillPreventUnload(event: IpcMainEvent): void { log.debug('onWillPreventUnload', event); - if (!event.sender) { + + const webContents: WebContents = event.sender; + if (webContents === undefined) { return; } - const webContents: WebContents = event.sender; + const browserWindow = BrowserWindow.fromWebContents(webContents); const choice = dialog.showMessageBoxSync(browserWindow, { type: 'question', From 1bd77d2abbb1340b072057ea63b399b4cf715ced Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 13:16:17 -0400 Subject: [PATCH 11/28] Improve windowEvents tests --- app/src/helpers/windowEvents.test.ts | 134 ++++++++++++++++++++------- app/src/helpers/windowEvents.ts | 14 +-- 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/app/src/helpers/windowEvents.test.ts b/app/src/helpers/windowEvents.test.ts index 418bcb815b..5fb61a34e6 100644 --- a/app/src/helpers/windowEvents.test.ts +++ b/app/src/helpers/windowEvents.test.ts @@ -1,9 +1,10 @@ -jest.mock('../helpers/windowEvents'); -jest.mock('../helpers/windowHelpers'); +jest.mock('./helpers'); +jest.mock('./windowEvents'); +jest.mock('./windowHelpers'); import { dialog, BrowserWindow, WebContents } from 'electron'; -import * as helpers from './helpers'; -const { onNewWindowHelper, onWillPreventUnload } = +import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers'; +const { onNewWindowHelper, onWillNavigate, onWillPreventUnload } = jest.requireActual('./windowEvents'); import { blockExternalURL, @@ -18,45 +19,43 @@ describe('onNewWindowHelper', () => { const foregroundDisposition = 'foreground-tab'; const backgroundDisposition = 'background-tab'; + const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock; const mockCreateAboutBlank: jest.SpyInstance = createAboutBlankWindow as jest.Mock; const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock; - let mockNativeTabsSupported: jest.SpyInstance = jest - .spyOn(helpers, 'nativeTabsSupported') - .mockImplementation(() => false); - const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock; - const mockOpenExternal: jest.SpyInstance = jest - .spyOn(helpers, 'openExternal') - .mockImplementation(); + const mockLinkIsInternal: jest.SpyInstance = ( + linkIsInternal as jest.Mock + ).mockImplementation(() => true); + const mockNativeTabsSupported: jest.SpyInstance = + nativeTabsSupported as jest.Mock; + const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock; const preventDefault = jest.fn(); const setupWindow = jest.fn(); beforeEach(() => { - mockNativeTabsSupported.mockImplementation(() => false); - }); - - afterEach(() => { + mockBlockExternalURL.mockReset(); mockCreateAboutBlank.mockReset(); mockCreateNewTab.mockReset(); - mockNativeTabsSupported.mockReset(); - mockBlockExternalURL.mockReset(); + mockLinkIsInternal.mockReset().mockReturnValue(true); + mockNativeTabsSupported.mockReset().mockReturnValue(false); mockOpenExternal.mockReset(); preventDefault.mockReset(); setupWindow.mockReset(); }); afterAll(() => { + mockBlockExternalURL.mockRestore(); mockCreateAboutBlank.mockRestore(); mockCreateNewTab.mockRestore(); + mockLinkIsInternal.mockRestore(); mockNativeTabsSupported.mockRestore(); - mockBlockExternalURL.mockRestore(); mockOpenExternal.mockRestore(); }); test('internal urls should not be handled', () => { const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( @@ -75,9 +74,10 @@ describe('onNewWindowHelper', () => { }); test('external urls should be opened externally', () => { + mockLinkIsInternal.mockReturnValue(false); const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -95,9 +95,10 @@ describe('onNewWindowHelper', () => { }); test('external urls should be ignored if blockExternalUrls is true', () => { + mockLinkIsInternal.mockReturnValue(false); const options = { - targetUrl: originalURL, blockExternalUrls: true, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -116,8 +117,8 @@ describe('onNewWindowHelper', () => { test('tab disposition should be ignored if tabs are not enabled', () => { const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -135,9 +136,10 @@ describe('onNewWindowHelper', () => { }); test('tab disposition should be ignored if url is external', () => { + mockLinkIsInternal.mockReturnValue(false); const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -155,13 +157,11 @@ describe('onNewWindowHelper', () => { }); test('foreground tabs with internal urls should be opened in the foreground', () => { - mockNativeTabsSupported = mockNativeTabsSupported.mockImplementation( - () => true, - ); + mockNativeTabsSupported.mockReturnValue(true); const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -186,13 +186,11 @@ describe('onNewWindowHelper', () => { }); test('background tabs with internal urls should be opened in background tabs', () => { - mockNativeTabsSupported = mockNativeTabsSupported.mockImplementation( - () => true, - ); + mockNativeTabsSupported.mockReturnValue(true); const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -218,8 +216,8 @@ describe('onNewWindowHelper', () => { test('about:blank urls should be handled', () => { const options = { - targetUrl: originalURL, blockExternalUrls: false, + targetUrl: originalURL, }; onNewWindowHelper( options, @@ -237,6 +235,70 @@ describe('onNewWindowHelper', () => { }); }); +describe('onWillNavigate', () => { + const originalURL = 'https://medium.com/'; + const internalURL = 'https://medium.com/topics/technology'; + const externalURL = 'https://www.wikipedia.org/wiki/Electron'; + + const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock; + const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock; + const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock; + const preventDefault = jest.fn(); + + beforeEach(() => { + mockBlockExternalURL.mockReset(); + mockLinkIsInternal.mockReset().mockReturnValue(false); + mockOpenExternal.mockReset(); + preventDefault.mockReset(); + }); + + afterAll(() => { + mockBlockExternalURL.mockRestore(); + mockLinkIsInternal.mockRestore(); + mockOpenExternal.mockRestore(); + }); + + test('internal urls should not be handled', () => { + mockLinkIsInternal.mockReturnValue(true); + const options = { + blockExternalUrls: false, + targetUrl: originalURL, + }; + const event = { preventDefault }; + onWillNavigate(options, event, internalURL); + + expect(mockBlockExternalURL).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('external urls should be opened externally', () => { + const options = { + blockExternalUrls: false, + targetUrl: originalURL, + }; + const event = { preventDefault }; + onWillNavigate(options, event, externalURL); + + expect(mockBlockExternalURL).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('external urls should be ignored if blockExternalUrls is true', () => { + const options = { + blockExternalUrls: true, + targetUrl: originalURL, + }; + const event = { preventDefault }; + onWillNavigate(options, event, externalURL); + + expect(mockBlockExternalURL).toHaveBeenCalledTimes(1); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); +}); + describe('onWillPreventUnload', () => { const mockFromWebContents: jest.SpyInstance = jest .spyOn(BrowserWindow, 'fromWebContents') @@ -246,9 +308,9 @@ describe('onWillPreventUnload', () => { .mockImplementation(); const preventDefault: jest.SpyInstance = jest.fn(); - afterEach(() => { + beforeEach(() => { mockFromWebContents.mockReset(); - mockShowDialog.mockReset(); + mockShowDialog.mockReset().mockReturnValue(undefined); preventDefault.mockReset(); }); @@ -267,7 +329,7 @@ describe('onWillPreventUnload', () => { }); test('shows dialog and calls preventDefault on ok', () => { - mockShowDialog.mockImplementation(() => 0); + mockShowDialog.mockReturnValue(0); const event = { preventDefault, sender: new WebContents() }; onWillPreventUnload(event); @@ -278,7 +340,7 @@ describe('onWillPreventUnload', () => { }); test('shows dialog and does not call preventDefault on cancel', () => { - mockShowDialog.mockImplementation(() => 1); + mockShowDialog.mockReturnValue(1); const event = { preventDefault, sender: new WebContents() }; onWillPreventUnload(event); diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index 698236732f..c6e9dc09b0 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -64,9 +64,9 @@ export function onNewWindowHelper( if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { preventDefault(); if (options.blockExternalUrls) { - blockExternalURL(urlToGo); + return blockExternalURL(urlToGo); } else { - openExternal(urlToGo); + return openExternal(urlToGo); } } else if (urlToGo === 'about:blank') { const newWindow = createAboutBlankWindow( @@ -74,14 +74,14 @@ export function onNewWindowHelper( getDefaultWindowOptions(options), parent, ); - preventDefault(newWindow); + return preventDefault(newWindow); } else if (nativeTabsSupported()) { if (disposition === 'background-tab') { const newTab = createNewTab(options, setupWindow, urlToGo, false, parent); - preventDefault(newTab); + return preventDefault(newTab); } else if (disposition === 'foreground-tab') { const newTab = createNewTab(options, setupWindow, urlToGo, true, parent); - preventDefault(newTab); + return preventDefault(newTab); } } } @@ -95,9 +95,9 @@ export function onWillNavigate( if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { event.preventDefault(); if (options.blockExternalUrls) { - blockExternalURL(urlToGo); + return blockExternalURL(urlToGo); } else { - openExternal(urlToGo); + return openExternal(urlToGo); } } } From e8d18b095f00710d0aad8b40bc804b4cbbc67884 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 14:17:20 -0400 Subject: [PATCH 12/28] Add the first test for windowHelpers --- app/src/helpers/windowHelpers.test.ts | 59 +++++++++++++++++ app/src/mocks/electron.ts | 91 ++++++++++++++++++++++++--- 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 app/src/helpers/windowHelpers.test.ts diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts new file mode 100644 index 0000000000..6ce63349a4 --- /dev/null +++ b/app/src/helpers/windowHelpers.test.ts @@ -0,0 +1,59 @@ +jest.mock('./helpers'); +jest.mock('./windowEvents'); + +import { BrowserWindow, WebContents } from 'electron'; + +import { getCSSToInject, shouldInjectCSS } from './helpers'; +import { injectCSS } from './windowHelpers'; + +describe('injectCSS', () => { + console.log({ WebContents }); + + const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; + const mockShouldInjectCSS: jest.SpyInstance = shouldInjectCSS as jest.Mock; + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(WebContents.prototype, 'insertCSS') + .mockImplementation(); + + beforeEach(() => { + mockGetCSSToInject.mockReset().mockReturnValue(''); + mockShouldInjectCSS.mockReset().mockReturnValue(true); + mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); + }); + + afterAll(() => { + mockGetCSSToInject.mockRestore(); + mockShouldInjectCSS.mockRestore(); + mockWebContentsInsertCSS.mockRestore(); + }); + + test('will not inject if shouldInjectCSS is false', () => { + mockShouldInjectCSS.mockReturnValue(false); + + const window = new BrowserWindow(); + + injectCSS(window); + + expect(mockGetCSSToInject).not.toHaveBeenCalled(); + }); + + test('will inject on did-navigate + onHeadersReceived', () => { + const css = 'body { color: white; }'; + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + window.webContents.session.webRequest.send( + 'onHeadersReceived', + { webContents: window.webContents }, + () => { + expect(mockWebContentsInsertCSS).toHaveBeenCalledWith(css); + }, + ); + }); +}); diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index a7da1b2629..7e29a57639 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -1,29 +1,106 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { EventEmitter } from 'events'; +/* + These mocks are PURPOSEFULLY minimal. A few reasons as to why: + 1. I'm l̶a̶z̶y̶ a busy person :) + 2. The less we have in here, the less we'll need to fix if an electron API changes + 3. Only mocking what we need as we need it helps reveal areas under test where electron + is being accessed in previously unaccounted for ways + 4. These mocks will get fleshed out as more unit tests are added, so if you need + something here as you are adding unit tests, then feel free to add exactly what you + need (and no more than that please). + + As well, please resist the urge to turn this into a reimplimentation of electron. + When adding functions/classes, keep your implementation to only the minimal amount of code + it takes for TypeScript to recognize what you are doing. For anything more complex (including + implementation code and return values) please do that within your tests via jest with + mockImplementation or mockReturnValue. +*/ + class MockBrowserWindow extends EventEmitter { - static fromWebContents( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - webContents: MockWebContents, - ): MockBrowserWindow { + webContents: MockWebContents; + + constructor() { + super(); + this.webContents = new MockWebContents(); + } + + static fromWebContents(webContents: MockWebContents): MockBrowserWindow { return new MockBrowserWindow(); } } class MockDialog { static showMessageBoxSync = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars browserWindow: MockBrowserWindow, - // eslint-disable-next-line @typescript-eslint/no-unused-vars options: any, ): number => { return undefined; }; } -class MockWebContents extends EventEmitter {} +class MockSession extends EventEmitter { + webRequest: MockWebRequest; + constructor() { + super(); + this.webRequest = new MockWebRequest(); + } +} + +class MockWebContents extends EventEmitter { + session: MockSession; + + constructor() { + super(); + this.session = new MockSession(); + } + + getURL(): string { + return undefined; + } + + insertCSS(css: string, options?: any): Promise { + return Promise.resolve(undefined); + } +} + +class MockWebRequest { + handlers: any; + + constructor() { + this.handlers = { onHeadersReceived: [] }; + } + + onHeadersReceived( + filter: any, + listener: + | (( + details: any, + callback: (headersReceivedResponse: any) => void, + ) => void) + | null, + ): void { + this.handlers.onHeadersReceived.push({ filter, listener }); + } + + send( + event: 'onHeadersReceived', + details: any, + callback: (headersReceivedResponse: any) => void, + ): void { + if (this.handlers[event] && this.handlers[event].length > 0) { + this.handlers[event].forEach((handler) => + handler.listener(details, callback), + ); + } + } +} export { MockDialog as dialog, MockBrowserWindow as BrowserWindow, + MockSession as Session, MockWebContents as WebContents, + MockWebRequest as WebRequest, }; From f72214462b739998d0a4929a57a78e824909073c Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 15:13:13 -0400 Subject: [PATCH 13/28] Move WebRequest event handling to node --- app/src/mocks/electron.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index 7e29a57639..53eac29ac3 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -66,10 +66,10 @@ class MockWebContents extends EventEmitter { } class MockWebRequest { - handlers: any; + emitter: InternalEmitter; constructor() { - this.handlers = { onHeadersReceived: [] }; + this.emitter = new InternalEmitter(); } onHeadersReceived( @@ -81,22 +81,20 @@ class MockWebRequest { ) => void) | null, ): void { - this.handlers.onHeadersReceived.push({ filter, listener }); + this.emitter.addListener( + 'onHeadersReceived', + (details: any, callback: (headersReceivedResponse: any) => void) => + listener(details, callback), + ); } - send( - event: 'onHeadersReceived', - details: any, - callback: (headersReceivedResponse: any) => void, - ): void { - if (this.handlers[event] && this.handlers[event].length > 0) { - this.handlers[event].forEach((handler) => - handler.listener(details, callback), - ); - } + send(event: string, ...args: any[]): void { + this.emitter.emit(event, ...args); } } +class InternalEmitter extends EventEmitter {} + export { MockDialog as dialog, MockBrowserWindow as BrowserWindow, From c9093141c4f8dd9c31dad0c2e34dda11ddcb4f73 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 15:13:34 -0400 Subject: [PATCH 14/28] insertCSS completely under test --- app/src/helpers/windowHelpers.test.ts | 66 +++++++++++++++++++++------ app/src/helpers/windowHelpers.ts | 11 ++++- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts index 6ce63349a4..4c426bd37d 100644 --- a/app/src/helpers/windowHelpers.test.ts +++ b/app/src/helpers/windowHelpers.test.ts @@ -1,28 +1,34 @@ -jest.mock('./helpers'); -jest.mock('./windowEvents'); - -import { BrowserWindow, WebContents } from 'electron'; +import { BrowserWindow, HeadersReceivedResponse, WebContents } from 'electron'; +jest.mock('loglevel'); +import { error } from 'loglevel'; +jest.mock('./helpers'); import { getCSSToInject, shouldInjectCSS } from './helpers'; +jest.mock('./windowEvents'); import { injectCSS } from './windowHelpers'; describe('injectCSS', () => { - console.log({ WebContents }); - const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; + const mockLogError: jest.SpyInstance = error as jest.Mock; const mockShouldInjectCSS: jest.SpyInstance = shouldInjectCSS as jest.Mock; - const mockWebContentsInsertCSS: jest.SpyInstance = jest - .spyOn(WebContents.prototype, 'insertCSS') - .mockImplementation(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest.spyOn( + WebContents.prototype, + 'insertCSS', + ); + + const css = 'body { color: white; }'; + const responseHeaders = { 'x-header': 'value' }; beforeEach(() => { mockGetCSSToInject.mockReset().mockReturnValue(''); + mockLogError.mockReset(); mockShouldInjectCSS.mockReset().mockReturnValue(true); mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); }); afterAll(() => { mockGetCSSToInject.mockRestore(); + mockLogError.mockRestore(); mockShouldInjectCSS.mockRestore(); mockWebContentsInsertCSS.mockRestore(); }); @@ -35,11 +41,38 @@ describe('injectCSS', () => { injectCSS(window); expect(mockGetCSSToInject).not.toHaveBeenCalled(); + expect(mockWebContentsInsertCSS).not.toHaveBeenCalled(); + }); + + test('will inject on did-navigate + onHeadersReceived', (done) => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock + window.webContents.session.webRequest.send( + 'onHeadersReceived', + { responseHeaders, webContents: window.webContents }, + (result: HeadersReceivedResponse) => { + expect(mockWebContentsInsertCSS).toHaveBeenCalledWith(css); + expect(result.cancel).toBe(false); + expect(result.responseHeaders).toBe(responseHeaders); + done(); + }, + ); }); - test('will inject on did-navigate + onHeadersReceived', () => { - const css = 'body { color: white; }'; + test('will catch errors inserting CSS', (done) => { mockGetCSSToInject.mockReturnValue(css); + + mockWebContentsInsertCSS.mockReturnValue( + Promise.reject('css insertion error'), + ); + const window = new BrowserWindow(); injectCSS(window); @@ -50,9 +83,16 @@ describe('injectCSS', () => { // @ts-ignore this function doesn't exist in the actual electron version, but will in our mock window.webContents.session.webRequest.send( 'onHeadersReceived', - { webContents: window.webContents }, - () => { + { responseHeaders, webContents: window.webContents }, + (result: HeadersReceivedResponse) => { expect(mockWebContentsInsertCSS).toHaveBeenCalledWith(css); + expect(mockLogError).toHaveBeenCalledWith( + 'webContents.insertCSS ERROR', + 'css insertion error', + ); + expect(result.cancel).toBe(false); + expect(result.responseHeaders).toBe(responseHeaders); + done(); }, ); }); diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index b124eff517..9b99c51122 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -206,9 +206,16 @@ export function injectCSS(browserWindow: BrowserWindow): void { if (details.webContents) { details.webContents .insertCSS(cssToInject) - .catch((err) => log.error('webContents.insertCSS ERROR', err)); + .catch((err) => { + log.error('webContents.insertCSS ERROR', err); + }) + .finally(() => + callback({ + cancel: false, + responseHeaders: details.responseHeaders, + }), + ); } - callback({ cancel: false, responseHeaders: details.responseHeaders }); }, ); }); From ddd364fd0bc500064db1a65a71a1c7a5746e3a76 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 15:41:58 -0400 Subject: [PATCH 15/28] clearAppData completely under test --- app/src/helpers/windowEvents.test.ts | 7 ++-- app/src/helpers/windowHelpers.test.ts | 52 +++++++++++++++++++++++++-- app/src/mocks/electron.ts | 22 ++++++++++-- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/app/src/helpers/windowEvents.test.ts b/app/src/helpers/windowEvents.test.ts index 5fb61a34e6..f4caae52e5 100644 --- a/app/src/helpers/windowEvents.test.ts +++ b/app/src/helpers/windowEvents.test.ts @@ -303,9 +303,10 @@ describe('onWillPreventUnload', () => { const mockFromWebContents: jest.SpyInstance = jest .spyOn(BrowserWindow, 'fromWebContents') .mockImplementation(() => new BrowserWindow()); - const mockShowDialog: jest.SpyInstance = jest - .spyOn(dialog, 'showMessageBoxSync') - .mockImplementation(); + const mockShowDialog: jest.SpyInstance = jest.spyOn( + dialog, + 'showMessageBoxSync', + ); const preventDefault: jest.SpyInstance = jest.fn(); beforeEach(() => { diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts index 4c426bd37d..15c19cb49e 100644 --- a/app/src/helpers/windowHelpers.test.ts +++ b/app/src/helpers/windowHelpers.test.ts @@ -1,11 +1,59 @@ -import { BrowserWindow, HeadersReceivedResponse, WebContents } from 'electron'; +import { + dialog, + BrowserWindow, + HeadersReceivedResponse, + WebContents, +} from 'electron'; jest.mock('loglevel'); import { error } from 'loglevel'; jest.mock('./helpers'); import { getCSSToInject, shouldInjectCSS } from './helpers'; jest.mock('./windowEvents'); -import { injectCSS } from './windowHelpers'; +import { clearAppData, injectCSS } from './windowHelpers'; + +describe('clearAppData', () => { + let window: BrowserWindow; + let mockClearCache: jest.SpyInstance; + let mockClearStorageData: jest.SpyInstance; + const mockShowDialog: jest.SpyInstance = jest.spyOn(dialog, 'showMessageBox'); + + beforeEach(() => { + window = new BrowserWindow(); + mockClearCache = jest.spyOn(window.webContents.session, 'clearCache'); + mockClearStorageData = jest.spyOn( + window.webContents.session, + 'clearStorageData', + ); + mockShowDialog.mockReset().mockResolvedValue(undefined); + }); + + afterAll(() => { + mockClearCache.mockRestore(); + mockClearStorageData.mockRestore(); + mockShowDialog.mockRestore(); + }); + + test('will not clear app data if dialog canceled', async () => { + mockShowDialog.mockResolvedValue(1); + + await clearAppData(window); + + expect(mockShowDialog).toHaveBeenCalledTimes(1); + expect(mockClearCache).not.toHaveBeenCalled(); + expect(mockClearStorageData).not.toHaveBeenCalled(); + }); + + test('will clear app data if ok is clicked', async () => { + mockShowDialog.mockResolvedValue(0); + + await clearAppData(window); + + expect(mockShowDialog).toHaveBeenCalledTimes(1); + expect(mockClearCache).not.toHaveBeenCalledTimes(1); + expect(mockClearStorageData).not.toHaveBeenCalledTimes(1); + }); +}); describe('injectCSS', () => { const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index 53eac29ac3..e5497e3818 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -32,20 +32,36 @@ class MockBrowserWindow extends EventEmitter { } class MockDialog { - static showMessageBoxSync = ( + static showMessageBox( browserWindow: MockBrowserWindow, options: any, - ): number => { + ): Promise { + return Promise.resolve(undefined); + } + + static showMessageBoxSync( + browserWindow: MockBrowserWindow, + options: any, + ): number { return undefined; - }; + } } class MockSession extends EventEmitter { webRequest: MockWebRequest; + constructor() { super(); this.webRequest = new MockWebRequest(); } + + clearCache(): Promise { + return Promise.resolve(); + } + + clearStorageData(): Promise { + return Promise.resolve(); + } } class MockWebContents extends EventEmitter { From 461cbb4707766c53df445cf46f9c62e7383c8e31 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 18:39:06 -0400 Subject: [PATCH 16/28] Fix contextMenu require bug --- app/src/components/contextMenu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts index 8faa3ec2c2..d5ed3271c1 100644 --- a/app/src/components/contextMenu.ts +++ b/app/src/components/contextMenu.ts @@ -6,10 +6,10 @@ export function initContextMenu( openExternal, window?: BrowserWindow, ): void { - // Require this at call time, otherwise its child dependency 'electron-is-dev' + // Require this at runtime, otherwise its child dependency 'electron-is-dev' // throws an error during unit testing. // eslint-disable-next-line @typescript-eslint/no-var-requires - const { contextMenu } = require('electron-context-menu'); + const contextMenu = require('electron-context-menu'); contextMenu({ prepend: (actions, params) => { From 2d4512126eb01a76966a302a12a9ae9dc60a41ee Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Tue, 25 May 2021 18:41:08 -0400 Subject: [PATCH 17/28] More tests + fixes --- app/src/components/mainWindow.ts | 19 +++++--- app/src/helpers/helpers.ts | 11 ++--- app/src/helpers/windowEvents.test.ts | 8 +++- app/src/helpers/windowEvents.ts | 12 ++--- app/src/helpers/windowHelpers.test.ts | 18 +++++++- app/src/helpers/windowHelpers.ts | 63 +++++++++++++++------------ app/src/mocks/electron.ts | 8 ++++ 7 files changed, 91 insertions(+), 48 deletions(-) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 201234503e..842a57f38c 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -324,9 +324,12 @@ export function setupWindow(options, window: BrowserWindow): void { () => (event, url, frameName, disposition) => onNewWindow(options, this, event, url, frameName, disposition), ); - window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => - onWillNavigate(options, event, url), - ); + window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { + onWillNavigate(options, event, url).catch((err) => { + log.error(' window.webContents.on.will-navigate ERROR', err); + event.preventDefault(); + }); + }); window.webContents.on('will-prevent-unload', onWillPreventUnload); window.webContents.on('did-finish-load', () => { @@ -338,12 +341,14 @@ export function setupWindow(options, window: BrowserWindow): void { .setVisualZoomLevelLimits(1, 3) .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - // Remove potential css injection code set in `did-navigate`) (see injectCss code) + // Remove potential css injection code set in `did-navigate`) (see injectCSS code) window.webContents.session.webRequest.onHeadersReceived(null); }); // @ts-ignore new-tab isn't in the type definition, but it does exist - this.window.on('new-tab', () => - createNewTab(options, this, options.targetUrl, true, window), - ); + window.on('new-tab', () => { + createNewTab(options, this, options.targetUrl, true, window).catch((err) => + log.error('new-tab ERROR', err), + ); + }); } diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index c93b4f79db..d583e44b7f 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -15,7 +15,7 @@ export function debugLog(browserWindow: BrowserWindow, message: string): void { setTimeout(() => { browserWindow.webContents.send('debug', message); }, 3000); - log.info(message); + log.debug(message); } /** @@ -155,11 +155,12 @@ export function nativeTabsSupported(): boolean { return isOSX(); } -export function openExternal(url: string, options?: OpenExternalOptions): void { +export function openExternal( + url: string, + options?: OpenExternalOptions, +): Promise { log.debug('openExternal', { url, options }); - shell - .openExternal(url, options) - .catch((err) => log.error('openExternal ERROR', err)); + return shell.openExternal(url, options); } export function removeUserAgentSpecifics( diff --git a/app/src/helpers/windowEvents.test.ts b/app/src/helpers/windowEvents.test.ts index f4caae52e5..24f3c2a6f4 100644 --- a/app/src/helpers/windowEvents.test.ts +++ b/app/src/helpers/windowEvents.test.ts @@ -33,7 +33,9 @@ describe('onNewWindowHelper', () => { const setupWindow = jest.fn(); beforeEach(() => { - mockBlockExternalURL.mockReset(); + mockBlockExternalURL + .mockReset() + .mockReturnValue(Promise.resolve(undefined)); mockCreateAboutBlank.mockReset(); mockCreateNewTab.mockReset(); mockLinkIsInternal.mockReset().mockReturnValue(true); @@ -246,7 +248,9 @@ describe('onWillNavigate', () => { const preventDefault = jest.fn(); beforeEach(() => { - mockBlockExternalURL.mockReset(); + mockBlockExternalURL + .mockReset() + .mockReturnValue(Promise.resolve(undefined)); mockLinkIsInternal.mockReset().mockReturnValue(false); mockOpenExternal.mockReset(); preventDefault.mockReset(); diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index c6e9dc09b0..677edec2f1 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -23,7 +23,7 @@ export function onNewWindow( | 'save-to-disk' | 'other', parent?: BrowserWindow, -): void { +): Promise { log.debug('onNewWindow', { event, urlToGo, @@ -37,7 +37,7 @@ export function onNewWindow( event.newGuest = newGuest; } }; - onNewWindowHelper( + return onNewWindowHelper( options, setupWindow, urlToGo, @@ -54,7 +54,7 @@ export function onNewWindowHelper( disposition: string, preventDefault, parent?: BrowserWindow, -): void { +): Promise { log.debug('onNewWindowHelper', { urlToGo, disposition, @@ -64,7 +64,7 @@ export function onNewWindowHelper( if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { preventDefault(); if (options.blockExternalUrls) { - return blockExternalURL(urlToGo); + return blockExternalURL(urlToGo).then(() => null); } else { return openExternal(urlToGo); } @@ -90,12 +90,12 @@ export function onWillNavigate( options, event: IpcMainEvent, urlToGo: string, -): void { +): Promise { log.debug('onWillNavigate', { options, event, urlToGo }); if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { event.preventDefault(); if (options.blockExternalUrls) { - return blockExternalURL(urlToGo); + return blockExternalURL(urlToGo).then(() => null); } else { return openExternal(urlToGo); } diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts index 15c19cb49e..5a6fc9ab2e 100644 --- a/app/src/helpers/windowHelpers.test.ts +++ b/app/src/helpers/windowHelpers.test.ts @@ -10,7 +10,7 @@ import { error } from 'loglevel'; jest.mock('./helpers'); import { getCSSToInject, shouldInjectCSS } from './helpers'; jest.mock('./windowEvents'); -import { clearAppData, injectCSS } from './windowHelpers'; +import { clearAppData, createNewTab, injectCSS } from './windowHelpers'; describe('clearAppData', () => { let window: BrowserWindow; @@ -55,6 +55,22 @@ describe('clearAppData', () => { }); }); +// describe('createNewTab', () => { +// test('creates new tab', () => { +// const window = new BrowserWindow(); +// const options = {}; +// const setupWindow = jest.fn(); +// const url = 'https://github.com/nativefier/nativefier'; +// const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn( +// BrowserWindow.prototype, +// 'addTabbedWindow', +// ); +// const tab = createNewTab(options, setupWindow, url, true, window); +// expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); +// expect(setupWindow).toHaveBeenCalledWith(tab); +// }); +// }); + describe('injectCSS', () => { const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; const mockLogError: jest.SpyInstance = error as jest.Mock; diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index 9b99c51122..bed34c6019 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -4,6 +4,7 @@ import { dialog, HeadersReceivedResponse, IpcMainEvent, + MessageBoxReturnValue, OnHeadersReceivedListenerDetails, } from 'electron'; @@ -19,22 +20,26 @@ import { const ZOOM_INTERVAL = 0.1; export function adjustWindowZoom(adjustment: number): void { - withFocusedWindow( - (focusedWindow: BrowserWindow) => - (focusedWindow.webContents.zoomFactor = - focusedWindow.webContents.zoomFactor + adjustment), - ); + withFocusedWindow((focusedWindow: BrowserWindow) => { + focusedWindow.webContents.zoomFactor = + focusedWindow.webContents.zoomFactor + adjustment; + }); } -export function blockExternalURL(url: string) { - withFocusedWindow((focusedWindow) => { - dialog - .showMessageBox(focusedWindow, { - message: `Cannot navigate to external URL: ${url}`, - type: 'error', - title: 'Navigation blocked', - }) - .catch((err) => log.error('dialog.showMessageBox ERROR', err)); +export function blockExternalURL(url: string): Promise { + return new Promise((resolve, reject) => { + withFocusedWindow((focusedWindow) => { + dialog + .showMessageBox(focusedWindow, { + message: `Cannot navigate to external URL: ${url}`, + type: 'error', + title: 'Navigation blocked', + }) + .then((result) => resolve(result)) + .catch((err) => { + reject(err); + }); + }); }); } @@ -78,9 +83,9 @@ export function createNewTab( url: string, foreground: boolean, parent?: BrowserWindow, -): BrowserWindow { +): Promise { log.debug('createNewTab', { url, foreground, parent }); - withFocusedWindow((focusedWindow) => { + return withFocusedWindow((focusedWindow) => { const newTab = createNewWindow(options, setupWindow, url, parent); focusedWindow.addTabbedWindow(newTab); if (!foreground) { @@ -88,7 +93,6 @@ export function createNewTab( } return newTab; }); - return undefined; } export function createNewWindow( @@ -158,8 +162,8 @@ export function goForward(): void { }); } -export function goToURL(url: string): void { - return withFocusedWindow((focusedWindow) => void focusedWindow.loadURL(url)); +export function goToURL(url: string): Promise { + return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url)); } export function hideWindow( @@ -192,7 +196,8 @@ export function injectCSS(browserWindow: BrowserWindow): void { browserWindow.webContents.getURL(), ); // We must inject css early enough; so onHeadersReceived is a good place. - // Will run multiple times, see `did-finish-load` below that unsets this handler. + // Will run multiple times, see `did-finish-load` event on the window + // that unsets this handler. browserWindow.webContents.session.webRequest.onHeadersReceived( { urls: [] }, // Pass an empty filter list; null will not match _any_ urls ( @@ -215,6 +220,11 @@ export function injectCSS(browserWindow: BrowserWindow): void { responseHeaders: details.responseHeaders, }), ); + } else { + callback({ + cancel: false, + responseHeaders: details.responseHeaders, + }); } }, ); @@ -251,14 +261,13 @@ export function setProxyRules(window: BrowserWindow, proxyRules): void { .catch((err) => log.error('session.setProxy ERROR', err)); } -export function withFocusedWindow( - block: (window: BrowserWindow) => void, -): void { +export function withFocusedWindow(block: (window: BrowserWindow) => any): any { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) { return block(focusedWindow); } - return undefined; + + return null; } export function zoomOut(): void { @@ -268,9 +277,9 @@ export function zoomOut(): void { export function zoomReset(options): void { log.debug('zoomReset'); - withFocusedWindow((focusedWindow: BrowserWindow) => { - focusedWindow.webContents.zoomFactor = options.zoom; - }); + withFocusedWindow( + (focusedWindow) => (focusedWindow.webContents.zoomFactor = options.zoom), + ); } export function zoomIn(): void { diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index e5497e3818..f0c647b575 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -26,9 +26,17 @@ class MockBrowserWindow extends EventEmitter { this.webContents = new MockWebContents(); } + addTabbedWindow(tab: MockBrowserWindow) { + return; + } + static fromWebContents(webContents: MockWebContents): MockBrowserWindow { return new MockBrowserWindow(); } + + static getFocusedWindow(window: MockBrowserWindow): MockBrowserWindow { + return window; + } } class MockDialog { From 80f2483a378097b86cc589785d6c9de4ab63b511 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Wed, 26 May 2021 09:47:46 -0400 Subject: [PATCH 18/28] Fix + add to createNewTab tests --- app/src/helpers/windowHelpers.test.ts | 55 +++++++++++++++++++-------- app/src/mocks/electron.ts | 10 ++++- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts index 5a6fc9ab2e..f6f6676208 100644 --- a/app/src/helpers/windowHelpers.test.ts +++ b/app/src/helpers/windowHelpers.test.ts @@ -55,21 +55,46 @@ describe('clearAppData', () => { }); }); -// describe('createNewTab', () => { -// test('creates new tab', () => { -// const window = new BrowserWindow(); -// const options = {}; -// const setupWindow = jest.fn(); -// const url = 'https://github.com/nativefier/nativefier'; -// const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn( -// BrowserWindow.prototype, -// 'addTabbedWindow', -// ); -// const tab = createNewTab(options, setupWindow, url, true, window); -// expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); -// expect(setupWindow).toHaveBeenCalledWith(tab); -// }); -// }); +describe('createNewTab', () => { + const window = new BrowserWindow(); + const options = {}; + const setupWindow = jest.fn(); + const url = 'https://github.com/nativefier/nativefier'; + const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'addTabbedWindow', + ); + const mockFocus: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'focus', + ); + const mockLoadURL: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'loadURL', + ); + + test('creates new foreground tab', () => { + const foreground = true; + + const tab = createNewTab(options, setupWindow, url, foreground, window); + + expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); + expect(setupWindow).toHaveBeenCalledWith(options, tab); + expect(mockLoadURL).toHaveBeenCalledWith(url); + expect(mockFocus).not.toHaveBeenCalled(); + }); + + test('creates new background tab', () => { + const foreground = false; + + const tab = createNewTab(options, setupWindow, url, foreground, window); + + expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); + expect(setupWindow).toHaveBeenCalledWith(options, tab); + expect(mockLoadURL).toHaveBeenCalledWith(url); + expect(mockFocus).toHaveBeenCalledTimes(1); + }); +}); describe('injectCSS', () => { const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index f0c647b575..1a0b28eb33 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -30,12 +30,20 @@ class MockBrowserWindow extends EventEmitter { return; } + focus(): void { + return; + } + static fromWebContents(webContents: MockWebContents): MockBrowserWindow { return new MockBrowserWindow(); } static getFocusedWindow(window: MockBrowserWindow): MockBrowserWindow { - return window; + return window ?? new MockBrowserWindow(); + } + + loadURL(url: string, options?: any): Promise { + return Promise.resolve(undefined); } } From c3f66ab0ee589076874ce1bdc84906656a7bd1d5 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Wed, 26 May 2021 11:52:47 -0400 Subject: [PATCH 19/28] Convert createMainWindow back to func + work out gremlins --- app/src/components/mainWindow.ts | 490 ++++++++++++++++--------------- app/src/helpers/windowEvents.ts | 61 ++-- app/src/helpers/windowHelpers.ts | 46 ++- app/src/main.ts | 16 +- app/src/mocks/electron.ts | 4 +- 5 files changed, 332 insertions(+), 285 deletions(-) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 842a57f38c..6ee25810f9 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -54,233 +54,103 @@ type SessionInteractionResult = { error?: Error; }; -export class MainWindow { - private readonly options; - private readonly onAppQuit; - private readonly setDockBadge; - private window: BrowserWindow; - - /** - * @param {{}} nativefierOptions AppArgs from nativefier.json - * @param {function} onAppQuit - * @param {function} setDockBadge - */ - constructor(nativefierOptions, onAppQuit, setDockBadge) { - this.options = { ...nativefierOptions }; - this.onAppQuit = onAppQuit; - this.setDockBadge = setDockBadge; - } - - async create(): Promise { - const mainWindowState = windowStateKeeper({ - defaultWidth: this.options.width || 1280, - defaultHeight: this.options.height || 800, - }); - - this.window = new BrowserWindow({ - frame: !this.options.hideWindowFrame, - width: mainWindowState.width, - height: mainWindowState.height, - minWidth: this.options.minWidth, - minHeight: this.options.minHeight, - maxWidth: this.options.maxWidth, - maxHeight: this.options.maxHeight, - x: this.options.x, - y: this.options.y, - autoHideMenuBar: !this.options.showMenuBar, - icon: getAppIcon(), - // set to undefined and not false because explicitly setting to false will disable full screen - fullscreen: this.options.fullScreen || undefined, - // Whether the window should always stay on top of other windows. Default is false. - alwaysOnTop: this.options.alwaysOnTop, - titleBarStyle: this.options.titleBarStyle, - show: this.options.tray !== 'start-in-tray', - backgroundColor: this.options.backgroundColor, - ...getDefaultWindowOptions(this.options), - }); - - mainWindowState.manage(this.window); +/** + * @param {{}} nativefierOptions AppArgs from nativefier.json + * @param {function} onAppQuit + * @param {function} setDockBadge + */ +export async function createMainWindow( + nativefierOptions, + onAppQuit: () => void, + setDockBadge: (value: number | string, bounce?: boolean) => void, +): Promise { + const options = { ...nativefierOptions }; + + const mainWindowState = windowStateKeeper({ + defaultWidth: options.width || 1280, + defaultHeight: options.height || 800, + }); - // after first run, no longer force maximize to be true - if (this.options.maximize) { - this.window.maximize(); - this.options.maximize = undefined; - saveAppArgs(this.options); - } + const mainWindow = new BrowserWindow({ + frame: !options.hideWindowFrame, + width: mainWindowState.width, + height: mainWindowState.height, + minWidth: options.minWidth, + minHeight: options.minHeight, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + x: options.x, + y: options.y, + autoHideMenuBar: !options.showMenuBar, + icon: getAppIcon(), + // set to undefined and not false because explicitly setting to false will disable full screen + fullscreen: options.fullScreen ?? undefined, + // Whether the window should always stay on top of other windows. Default is false. + alwaysOnTop: options.alwaysOnTop, + titleBarStyle: options.titleBarStyle, + show: options.tray !== 'start-in-tray', + backgroundColor: options.backgroundColor, + ...getDefaultWindowOptions(options), + }); - if (this.options.tray === 'start-in-tray') { - this.window.hide(); - } + mainWindowState.manage(mainWindow); - const menuOptions = { - nativefierVersion: this.options.nativefierVersion, - appQuit: this.onAppQuit, - clearAppData: () => clearAppData(this.window), - disableDevTools: this.options.disableDevTools, - getCurrentURL, - goBack, - goForward, - goToURL, - openExternal, - zoomBuildTimeValue: this.options.zoom, - zoomIn, - zoomOut, - zoomReset, - }; - - createMenu(menuOptions); - if (!this.options.disableContextMenu) { - initContextMenu( - createNewWindow, - nativeTabsSupported() - ? (url: string, foreground: boolean) => - createNewTab( - this.options, - setupWindow, - url, - foreground, - this.window, - ) - : undefined, - openExternal, - this.window, - ); - } + // after first run, no longer force maximize to be true + if (options.maximize) { + mainWindow.maximize(); + options.maximize = undefined; + saveAppArgs(options); + } - setupWindow(this.options, this.window); + if (options.tray === 'start-in-tray') { + mainWindow.hide(); + } - if (this.options.counter) { - this.window.on('page-title-updated', (event, title) => { - log.debug('mainWindow.page-title-updated', { event, title }); - const counterValue = getCounterValue(title); - if (counterValue) { - this.setDockBadge(counterValue, this.options.bounce); - } else { - this.setDockBadge(''); - } - }); - } else { - ipcMain.on('notification', () => { - log.debug('ipcMain.notification'); - if (!isOSX() || this.window.isFocused()) { - return; - } - this.setDockBadge('•', this.options.bounce); - }); - this.window.on('focus', () => { - log.debug('mainWindow.focus'); - this.setDockBadge(''); - }); - } + createMainMenu(options, mainWindow, onAppQuit); + createContextMenu(options, mainWindow); + setupNativefierWindow(options, mainWindow); - ipcMain.on('notification-click', () => { - log.debug('ipcMain.notification-click'); - this.window.show(); - }); + if (options.counter) { + setupCounter(options, mainWindow, setDockBadge); + } else { + setupNotificationBadge(options, mainWindow, setDockBadge); + } - // See API.md / "Accessing The Electron Session" - ipcMain.on( - 'session-interaction', - (event, request: SessionInteractionRequest) => { - log.debug('ipcMain.session-interaction', { event, request }); - - const result: SessionInteractionResult = { id: request.id }; - let awaitingPromise = false; - try { - if (request.func !== undefined) { - // If no funcArgs provided, we'll just use an empty array - if (request.funcArgs === undefined || request.funcArgs === null) { - request.funcArgs = []; - } - - // If funcArgs isn't an array, we'll be nice and make it a single item array - if (typeof request.funcArgs[Symbol.iterator] !== 'function') { - request.funcArgs = [request.funcArgs]; - } - - // Call func with funcArgs - result.value = this.window.webContents.session[request.func]( - ...request.funcArgs, - ); - - if ( - result.value !== undefined && - typeof result.value['then'] === 'function' - ) { - // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply - result.value - .then((trueResultValue) => { - result.value = trueResultValue; - log.debug('ipcMain.session-interaction:result', result); - event.reply('session-interaction-reply', result); - }) - .catch((err) => - log.error('session-interaction ERROR', request, err), - ); - awaitingPromise = true; - } - } else if (request.property !== undefined) { - if (request.propertyValue !== undefined) { - // Set the property - this.window.webContents.session[request.property] = - request.propertyValue; - } - - // Get the property value - result.value = this.window.webContents.session[request.property]; - } else { - // Why even send the event if you're going to do this? You're just wasting time! ;) - throw Error( - 'Received neither a func nor a property in the request. Unable to process.', - ); - } + ipcMain.on('notification-click', () => { + log.debug('ipcMain.notification-click'); + mainWindow.show(); + }); - // If we are awaiting a promise, that will return the reply instead, else - if (!awaitingPromise) { - log.debug('session-interaction:result', result); - event.reply('session-interaction-reply', result); - } - } catch (error) { - log.error('session-interaction:error', error, event, request); - result.error = error; - result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place - event.reply('session-interaction-reply', result); - } - }, - ); + setupSessionInteraction(options, mainWindow); - if (this.options.clearCache) { - await clearCache(this.window); - } + if (options.clearCache) { + await clearCache(mainWindow); + } - await this.window.loadURL(this.options.targetUrl); + await mainWindow.loadURL(options.targetUrl); - this.window.on('close', (event: IpcMainEvent) => { - log.debug('mainWindow.close', event); - if (this.window.isFullScreen()) { - if (nativeTabsSupported()) { - this.window.moveTabToNewWindow(); - } - this.window.setFullScreen(false); - this.window.once('leave-full-screen', (event: IpcMainEvent) => - hideWindow( - this.window, - event, - this.options.fastQuit, - this.options.tray, - ), - ); - } - hideWindow(this.window, event, this.options.fastQuit, this.options.tray); + setupCloseEvent(options, mainWindow); - if (this.options.clearCache) { - clearCache(this.window).catch((err) => - log.error('clearCache ERROR', err), - ); - } - }); + return mainWindow; +} - return this.window; +function createContextMenu(options, window: BrowserWindow): void { + if (!options.disableContextMenu) { + initContextMenu( + createNewWindow, + nativeTabsSupported() + ? (url: string, foreground: boolean) => + createNewTab( + options, + setupNativefierWindow, + url, + foreground, + window, + ) + : undefined, + openExternal, + window, + ); } } @@ -297,7 +167,85 @@ export function saveAppArgs(newAppArgs: any) { } } -export function setupWindow(options, window: BrowserWindow): void { +function setupCloseEvent(options, window: BrowserWindow) { + window.on('close', (event: IpcMainEvent) => { + log.debug('mainWindow.close', event); + if (window.isFullScreen()) { + if (nativeTabsSupported()) { + window.moveTabToNewWindow(); + } + window.setFullScreen(false); + window.once('leave-full-screen', (event: IpcMainEvent) => + hideWindow(window, event, options.fastQuit, options.tray), + ); + } + hideWindow(window, event, options.fastQuit, options.tray); + + if (options.clearCache) { + clearCache(window).catch((err) => log.error('clearCache ERROR', err)); + } + }); +} + +function setupCounter( + options, + window: BrowserWindow, + setDockBadge: (value: number | string, bounce?: boolean) => void, +) { + window.on('page-title-updated', (event, title) => { + log.debug('mainWindow.page-title-updated', { event, title }); + const counterValue = getCounterValue(title); + if (counterValue) { + setDockBadge(counterValue, options.bounce); + } else { + setDockBadge(''); + } + }); +} + +function createMainMenu( + options: any, + window: BrowserWindow, + onAppQuit: () => void, +) { + const menuOptions = { + nativefierVersion: options.nativefierVersion, + appQuit: onAppQuit, + clearAppData: () => clearAppData(window), + disableDevTools: options.disableDevTools, + getCurrentURL, + goBack, + goForward, + goToURL, + openExternal, + zoomBuildTimeValue: options.zoom, + zoomIn, + zoomOut, + zoomReset, + }; + + createMenu(menuOptions); +} + +function setupNotificationBadge( + options, + window: BrowserWindow, + setDockBadge: (value: number | string, bounce?: boolean) => void, +): void { + ipcMain.on('notification', () => { + log.debug('ipcMain.notification'); + if (!isOSX() || window.isFocused()) { + return; + } + setDockBadge('•', options.bounce); + }); + window.on('focus', () => { + log.debug('mainWindow.focus'); + setDockBadge(''); + }); +} + +export function setupNativefierWindow(options, window: BrowserWindow): void { if (options.userAgent) { window.webContents.userAgent = options.userAgent; } @@ -307,7 +255,6 @@ export function setupWindow(options, window: BrowserWindow): void { } injectCSS(window); - sendParamsOnDidFinishLoad(options, window); // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) // We can't quite cut over to that yet for a few reasons: @@ -319,11 +266,16 @@ export function setupWindow(options, window: BrowserWindow): void { // users are being pointed to use setWindowOpenHandler. // E.g., https://github.com/electron/electron/issues/28374 - window.webContents.on( - 'new-window', - () => (event, url, frameName, disposition) => - onNewWindow(options, this, event, url, frameName, disposition), - ); + window.webContents.on('new-window', (event, url, frameName, disposition) => { + onNewWindow( + options, + setupNativefierWindow, + event, + url, + frameName, + disposition, + ).catch((err) => log.error('onNewWindow ERROR', err)); + }); window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { onWillNavigate(options, event, url).catch((err) => { log.error(' window.webContents.on.will-navigate ERROR', err); @@ -332,23 +284,89 @@ export function setupWindow(options, window: BrowserWindow): void { }); window.webContents.on('will-prevent-unload', onWillPreventUnload); - window.webContents.on('did-finish-load', () => { - log.debug('mainWindow.webContents.did-finish-load'); - // Restore pinch-to-zoom, disabled by default in recent Electron. - // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598309817 - // and https://github.com/electron/electron/pull/12679 - window.webContents - .setVisualZoomLevelLimits(1, 3) - .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); - - // Remove potential css injection code set in `did-navigate`) (see injectCSS code) - window.webContents.session.webRequest.onHeadersReceived(null); - }); + sendParamsOnDidFinishLoad(options, window); // @ts-ignore new-tab isn't in the type definition, but it does exist window.on('new-tab', () => { - createNewTab(options, this, options.targetUrl, true, window).catch((err) => - log.error('new-tab ERROR', err), - ); + createNewTab( + options, + setupNativefierWindow, + options.targetUrl, + true, + window, + ).catch((err) => log.error('new-tab ERROR', err)); }); } + +function setupSessionInteraction(options, window: BrowserWindow): void { + // See API.md / "Accessing The Electron Session" + ipcMain.on( + 'session-interaction', + (event, request: SessionInteractionRequest) => { + log.debug('ipcMain.session-interaction', { event, request }); + + const result: SessionInteractionResult = { id: request.id }; + let awaitingPromise = false; + try { + if (request.func !== undefined) { + // If no funcArgs provided, we'll just use an empty array + if (request.funcArgs === undefined || request.funcArgs === null) { + request.funcArgs = []; + } + + // If funcArgs isn't an array, we'll be nice and make it a single item array + if (typeof request.funcArgs[Symbol.iterator] !== 'function') { + request.funcArgs = [request.funcArgs]; + } + + // Call func with funcArgs + result.value = window.webContents.session[request.func]( + ...request.funcArgs, + ); + + if ( + result.value !== undefined && + typeof result.value['then'] === 'function' + ) { + // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply + result.value + .then((trueResultValue) => { + result.value = trueResultValue; + log.debug('ipcMain.session-interaction:result', result); + event.reply('session-interaction-reply', result); + }) + .catch((err) => + log.error('session-interaction ERROR', request, err), + ); + awaitingPromise = true; + } + } else if (request.property !== undefined) { + if (request.propertyValue !== undefined) { + // Set the property + window.webContents.session[request.property] = + request.propertyValue; + } + + // Get the property value + result.value = window.webContents.session[request.property]; + } else { + // Why even send the event if you're going to do this? You're just wasting time! ;) + throw Error( + 'Received neither a func nor a property in the request. Unable to process.', + ); + } + + // If we are awaiting a promise, that will return the reply instead, else + if (!awaitingPromise) { + log.debug('session-interaction:result', result); + event.reply('session-interaction-reply', result); + } + } catch (error) { + log.error('session-interaction:error', error, event, request); + result.error = error; + result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place + event.reply('session-interaction-reply', result); + } + }, + ); +} diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index 677edec2f1..e890f8f8f6 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -6,12 +6,11 @@ import { blockExternalURL, createAboutBlankWindow, createNewTab, - getDefaultWindowOptions, } from './windowHelpers'; export function onNewWindow( options, - setupWindow, + setupWindow: (...args) => void, event: Event & { newGuest?: any }, urlToGo: string, frameName: string, @@ -49,7 +48,7 @@ export function onNewWindow( export function onNewWindowHelper( options, - setupWindow, + setupWindow: (...args) => void, urlToGo: string, disposition: string, preventDefault, @@ -61,28 +60,41 @@ export function onNewWindowHelper( preventDefault, parent, }); - if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { - preventDefault(); - if (options.blockExternalUrls) { - return blockExternalURL(urlToGo).then(() => null); - } else { - return openExternal(urlToGo); - } - } else if (urlToGo === 'about:blank') { - const newWindow = createAboutBlankWindow( - options, - getDefaultWindowOptions(options), - parent, - ); - return preventDefault(newWindow); - } else if (nativeTabsSupported()) { - if (disposition === 'background-tab') { - const newTab = createNewTab(options, setupWindow, urlToGo, false, parent); - return preventDefault(newTab); - } else if (disposition === 'foreground-tab') { - const newTab = createNewTab(options, setupWindow, urlToGo, true, parent); - return preventDefault(newTab); + try { + if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) { + preventDefault(); + if (options.blockExternalUrls) { + return blockExternalURL(urlToGo).then(() => null); + } else { + return openExternal(urlToGo); + } + } else if (urlToGo === 'about:blank') { + const newWindow = createAboutBlankWindow(options, setupWindow, parent); + return Promise.resolve(preventDefault(newWindow)); + } else if (nativeTabsSupported()) { + if (disposition === 'background-tab') { + const newTab = createNewTab( + options, + setupWindow, + urlToGo, + false, + parent, + ); + return Promise.resolve(preventDefault(newTab)); + } else if (disposition === 'foreground-tab') { + const newTab = createNewTab( + options, + setupWindow, + urlToGo, + true, + parent, + ); + return Promise.resolve(preventDefault(newTab)); + } } + return Promise.resolve(undefined); + } catch (err) { + return Promise.reject(err); } } @@ -100,6 +112,7 @@ export function onWillNavigate( return openExternal(urlToGo); } } + return Promise.resolve(undefined); } export function onWillPreventUnload(event: IpcMainEvent): void { diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts index bed34c6019..13e5615138 100644 --- a/app/src/helpers/windowHelpers.ts +++ b/app/src/helpers/windowHelpers.ts @@ -67,13 +67,18 @@ export async function clearCache(window: BrowserWindow): Promise { export function createAboutBlankWindow( options, - setupWindow, + setupWindow: (...args) => void, parent?: BrowserWindow, ): BrowserWindow { const window = createNewWindow(options, setupWindow, 'about:blank', parent); - setupWindow(options, window); - window.show(); - window.focus(); + window.hide(); + window.webContents.once('did-stop-loading', () => { + if (window.webContents.getURL() === 'about:blank') { + window.close(); + } else { + window.show(); + } + }); return window; } @@ -97,7 +102,7 @@ export function createNewTab( export function createNewWindow( options, - setupWindow, + setupWindow: (...args) => void, url: string, parent?: BrowserWindow, ): BrowserWindow { @@ -123,12 +128,16 @@ export function getDefaultWindowOptions( const browserwindowOptions: BrowserWindowConstructorOptions = { ...options.browserwindowOptions, }; - // We're going to remove this an merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences - // Otherwise browserwindowOptions.webPreferences object will eliminate the webPreferences - // specified in the DEFAULT_WINDOW_OPTIONS and replace it with itself + // We're going to remove this and merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences + // Otherwise the browserwindowOptions.webPreferences object will completely replace the + // webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself delete browserwindowOptions.webPreferences; - return { + const webPreferences = { + ...(options.browserwindowOptions?.webPreferences ?? {}), + }; + + const defaultOptions = { // Convert dashes to spaces because on linux the app name is joined with dashes title: options.name, tabbingIdentifier: nativeTabsSupported() ? options.name : undefined, @@ -139,13 +148,18 @@ export function getDefaultWindowOptions( webSecurity: !options.insecure, preload: path.join(__dirname, 'preload.js'), zoomFactor: options.zoom, - ...(options.browserWindowOptions && - options.browserwindowOptions.webPreferences - ? options.browserwindowOptions.webPreferences - : {}), + ...webPreferences, }, ...browserwindowOptions, }; + + log.debug('getDefaultWindowOptions', { + options, + webPreferences, + defaultOptions, + }); + + return defaultOptions; } export function goBack(): void { @@ -277,9 +291,9 @@ export function zoomOut(): void { export function zoomReset(options): void { log.debug('zoomReset'); - withFocusedWindow( - (focusedWindow) => (focusedWindow.webContents.zoomFactor = options.zoom), - ); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.zoomFactor = options.zoom; + }); } export function zoomIn(): void { diff --git a/app/src/main.ts b/app/src/main.ts index 827294f484..d93a97f2e8 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -18,9 +18,9 @@ import * as log from 'loglevel'; import { createLoginWindow } from './components/loginWindow'; import { saveAppArgs, - MainWindow, APP_ARGS_FILE_PATH, - setupWindow, + setupNativefierWindow, + createMainWindow, } from './components/mainWindow'; import { createTrayIcon } from './components/trayIcon'; import { isOSX, removeUserAgentSpecifics } from './helpers/helpers'; @@ -263,11 +263,11 @@ if (shouldQuit) { } async function onReady(): Promise { - const mainWindow = await new MainWindow( + const mainWindow = await createMainWindow( appArgs, app.quit.bind(this), setDockBadge, - ).create(); + ); createTrayIcon(appArgs, mainWindow); @@ -345,7 +345,9 @@ async function onReady(): Promise { } app.on('new-window-for-tab', () => { log.debug('app.new-window-for-tab'); - mainWindow.emit('new-tab'); + if (mainWindow) { + mainWindow.emit('new-tab'); + } }); app.on('login', (event, webContents, request, authInfo, callback) => { @@ -390,8 +392,8 @@ app.on( 'browser-window-created', (event: IpcMainEvent, window: BrowserWindow) => { log.debug('app.browser-window-created', { event, window }); - if (setupWindow !== undefined) { - setupWindow(appArgs, window); + if (setupNativefierWindow !== undefined) { + setupNativefierWindow(appArgs, window); } }, ); diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts index 1a0b28eb33..ac1529498c 100644 --- a/app/src/mocks/electron.ts +++ b/app/src/mocks/electron.ts @@ -21,8 +21,8 @@ import { EventEmitter } from 'events'; class MockBrowserWindow extends EventEmitter { webContents: MockWebContents; - constructor() { - super(); + constructor(options?: any) { + super(options); this.webContents = new MockWebContents(); } From facae3c0bb3eca1fa6314254f7aeec65fd9443ac Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Wed, 26 May 2021 12:48:57 -0400 Subject: [PATCH 20/28] Move setupWindow away from main since its shared --- app/src/components/mainWindow.ts | 62 +------------------------------- app/src/helpers/windowEvents.ts | 56 +++++++++++++++++++++++++++++ app/src/main.ts | 6 ++-- 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 6ee25810f9..e17ca4d2ca 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -12,11 +12,7 @@ import { nativeTabsSupported, openExternal, } from '../helpers/helpers'; -import { - onNewWindow, - onWillNavigate, - onWillPreventUnload, -} from '../helpers/windowEvents'; +import { setupNativefierWindow } from '../helpers/windowEvents'; import { clearAppData, clearCache, @@ -28,9 +24,6 @@ import { goForward, goToURL, hideWindow, - injectCSS, - sendParamsOnDidFinishLoad, - setProxyRules, zoomIn, zoomOut, zoomReset, @@ -245,59 +238,6 @@ function setupNotificationBadge( }); } -export function setupNativefierWindow(options, window: BrowserWindow): void { - if (options.userAgent) { - window.webContents.userAgent = options.userAgent; - } - - if (options.proxyRules) { - setProxyRules(window, options.proxyRules); - } - - injectCSS(window); - - // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) - // We can't quite cut over to that yet for a few reasons: - // 1. Our version of Electron does not yet support a parameter to - // setWindowOpenHandler that contains `disposition', which we need. - // See https://github.com/electron/electron/issues/28380 - // 2. setWindowOpenHandler doesn't support newGuest as well - // Though at this point, 'new-window' bugs seem to be coming up and downstream - // users are being pointed to use setWindowOpenHandler. - // E.g., https://github.com/electron/electron/issues/28374 - - window.webContents.on('new-window', (event, url, frameName, disposition) => { - onNewWindow( - options, - setupNativefierWindow, - event, - url, - frameName, - disposition, - ).catch((err) => log.error('onNewWindow ERROR', err)); - }); - window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { - onWillNavigate(options, event, url).catch((err) => { - log.error(' window.webContents.on.will-navigate ERROR', err); - event.preventDefault(); - }); - }); - window.webContents.on('will-prevent-unload', onWillPreventUnload); - - sendParamsOnDidFinishLoad(options, window); - - // @ts-ignore new-tab isn't in the type definition, but it does exist - window.on('new-tab', () => { - createNewTab( - options, - setupNativefierWindow, - options.targetUrl, - true, - window, - ).catch((err) => log.error('new-tab ERROR', err)); - }); -} - function setupSessionInteraction(options, window: BrowserWindow): void { // See API.md / "Accessing The Electron Session" ipcMain.on( diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts index e890f8f8f6..f18d16fc75 100644 --- a/app/src/helpers/windowEvents.ts +++ b/app/src/helpers/windowEvents.ts @@ -6,6 +6,9 @@ import { blockExternalURL, createAboutBlankWindow, createNewTab, + injectCSS, + sendParamsOnDidFinishLoad, + setProxyRules, } from './windowHelpers'; export function onNewWindow( @@ -136,3 +139,56 @@ export function onWillPreventUnload(event: IpcMainEvent): void { event.preventDefault(); } } + +export function setupNativefierWindow(options, window: BrowserWindow): void { + if (options.userAgent) { + window.webContents.userAgent = options.userAgent; + } + + if (options.proxyRules) { + setProxyRules(window, options.proxyRules); + } + + injectCSS(window); + + // .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...) + // We can't quite cut over to that yet for a few reasons: + // 1. Our version of Electron does not yet support a parameter to + // setWindowOpenHandler that contains `disposition', which we need. + // See https://github.com/electron/electron/issues/28380 + // 2. setWindowOpenHandler doesn't support newGuest as well + // Though at this point, 'new-window' bugs seem to be coming up and downstream + // users are being pointed to use setWindowOpenHandler. + // E.g., https://github.com/electron/electron/issues/28374 + + window.webContents.on('new-window', (event, url, frameName, disposition) => { + onNewWindow( + options, + setupNativefierWindow, + event, + url, + frameName, + disposition, + ).catch((err) => log.error('onNewWindow ERROR', err)); + }); + window.webContents.on('will-navigate', (event: IpcMainEvent, url: string) => { + onWillNavigate(options, event, url).catch((err) => { + log.error(' window.webContents.on.will-navigate ERROR', err); + event.preventDefault(); + }); + }); + window.webContents.on('will-prevent-unload', onWillPreventUnload); + + sendParamsOnDidFinishLoad(options, window); + + // @ts-ignore new-tab isn't in the type definition, but it does exist + window.on('new-tab', () => { + createNewTab( + options, + setupNativefierWindow, + options.targetUrl, + true, + window, + ).catch((err) => log.error('new-tab ERROR', err)); + }); +} diff --git a/app/src/main.ts b/app/src/main.ts index d93a97f2e8..43778d8be0 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -19,12 +19,12 @@ import { createLoginWindow } from './components/loginWindow'; import { saveAppArgs, APP_ARGS_FILE_PATH, - setupNativefierWindow, createMainWindow, } from './components/mainWindow'; import { createTrayIcon } from './components/trayIcon'; import { isOSX, removeUserAgentSpecifics } from './helpers/helpers'; import { inferFlashPath } from './helpers/inferFlash'; +import { setupNativefierWindow } from './helpers/windowEvents'; // Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744 if (require('electron-squirrel-startup')) { @@ -392,9 +392,7 @@ app.on( 'browser-window-created', (event: IpcMainEvent, window: BrowserWindow) => { log.debug('app.browser-window-created', { event, window }); - if (setupNativefierWindow !== undefined) { - setupNativefierWindow(appArgs, window); - } + setupNativefierWindow(appArgs, window); }, ); From a2dad735761ab8d9b48f67a9be264424e6cd73e1 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Wed, 26 May 2021 13:02:10 -0400 Subject: [PATCH 21/28] Make sure contextMenu is handling promises --- app/src/components/contextMenu.ts | 41 ++++++++++++++++++++----------- app/src/components/mainWindow.ts | 18 +------------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts index d5ed3271c1..d51519d6ab 100644 --- a/app/src/components/contextMenu.ts +++ b/app/src/components/contextMenu.ts @@ -1,38 +1,51 @@ import { BrowserWindow } from 'electron'; +import log from 'loglevel'; +import { nativeTabsSupported, openExternal } from '../helpers/helpers'; +import { setupNativefierWindow } from '../helpers/windowEvents'; +import { createNewTab, createNewWindow } from '../helpers/windowHelpers'; -export function initContextMenu( - createNewWindow, - createNewTab, - openExternal, - window?: BrowserWindow, -): void { +export function initContextMenu(options, window?: BrowserWindow): void { // Require this at runtime, otherwise its child dependency 'electron-is-dev' // throws an error during unit testing. // eslint-disable-next-line @typescript-eslint/no-var-requires const contextMenu = require('electron-context-menu'); + log.debug('initContextMenu', { options, window }); + contextMenu({ prepend: (actions, params) => { + log.debug('contextMenu.prepend', { actions, params }); const items = []; if (params.linkURL) { items.push({ label: 'Open Link in Default Browser', click: () => { - openExternal(params.linkURL); + openExternal(params.linkURL).catch((err) => + log.error('contextMenu Open Link in Default Browser ERROR', err), + ); }, }); items.push({ label: 'Open Link in New Window', - click: () => { - createNewWindow(params.linkURL, window); - }, + click: () => + createNewWindow( + options, + setupNativefierWindow, + params.linkURL, + window, + ), }); - if (createNewTab) { + if (nativeTabsSupported()) { items.push({ label: 'Open Link in New Tab', - click: () => { - createNewTab(params.linkURL, false, window); - }, + click: () => + createNewTab( + options, + setupNativefierWindow, + params.linkURL, + true, + window, + ), }); } } diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index e17ca4d2ca..1cb01e7626 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -16,8 +16,6 @@ import { setupNativefierWindow } from '../helpers/windowEvents'; import { clearAppData, clearCache, - createNewTab, - createNewWindow, getCurrentURL, getDefaultWindowOptions, goBack, @@ -129,21 +127,7 @@ export async function createMainWindow( function createContextMenu(options, window: BrowserWindow): void { if (!options.disableContextMenu) { - initContextMenu( - createNewWindow, - nativeTabsSupported() - ? (url: string, foreground: boolean) => - createNewTab( - options, - setupNativefierWindow, - url, - foreground, - window, - ) - : undefined, - openExternal, - window, - ); + initContextMenu(options, window); } } From 0ff369de30bcb11164945b487ac33e52877aef9f Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Wed, 16 Jun 2021 14:37:41 -0400 Subject: [PATCH 22/28] v13.1.2 --- app/package.json | 2 +- app/src/helpers/helpers.ts | 8 -------- src/constants.ts | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/package.json b/app/package.json index cbb18a2736..fd2a52cd87 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,6 @@ "source-map-support": "^0.5.19" }, "devDependencies": { - "electron": "^12.0.11" + "electron": "^13.1.2" } } diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts index 2357802b26..47e5538c60 100644 --- a/app/src/helpers/helpers.ts +++ b/app/src/helpers/helpers.ts @@ -179,11 +179,3 @@ export function removeUserAgentSpecifics( .replace(`Electron/${process.versions.electron} `, '') .replace(`${appName}/${appVersion} `, ' '); } - -export function shouldInjectCSS(): boolean { - try { - return fs.existsSync(INJECT_DIR); - } catch (e) { - return false; - } -} diff --git a/src/constants.ts b/src/constants.ts index a14295b8d0..a7db17b83f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,7 +4,7 @@ export const DEFAULT_APP_NAME = 'APP'; // Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, // and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION -export const DEFAULT_ELECTRON_VERSION = '12.0.11'; +export const DEFAULT_ELECTRON_VERSION = '13.1.2'; export const DEFAULT_CHROME_VERSION = '89.0.4389.128'; // Update each of these periodically From 6f2a750f766ad4063a9254daa8d138ccd81e9377 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Sat, 26 Jun 2021 10:20:44 -0400 Subject: [PATCH 23/28] v13.1.4 --- app/package.json | 2 +- src/constants.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/package.json b/app/package.json index fd2a52cd87..552338741c 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,6 @@ "source-map-support": "^0.5.19" }, "devDependencies": { - "electron": "^13.1.2" + "electron": "^13.1.4" } } diff --git a/src/constants.ts b/src/constants.ts index a7db17b83f..776ea8da3f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,17 +4,17 @@ export const DEFAULT_APP_NAME = 'APP'; // Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, // and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION -export const DEFAULT_ELECTRON_VERSION = '13.1.2'; -export const DEFAULT_CHROME_VERSION = '89.0.4389.128'; +export const DEFAULT_ELECTRON_VERSION = '13.1.4'; +export const DEFAULT_CHROME_VERSION = '91.0.4472.106'; // Update each of these periodically // https://product-details.mozilla.org/1.0/firefox_versions.json -export const DEFAULT_FIREFOX_VERSION = '89.0'; +export const DEFAULT_FIREFOX_VERSION = '89.0.2'; // https://en.wikipedia.org/wiki/Safari_version_history export const DEFAULT_SAFARI_VERSION = { majorVersion: 14, - version: '14.0.3', + version: '14.1.1', webkitVersion: '610.4.3.1.7', }; From ba2ea4d1647341ff2b3e3e6131e4a71438cda4b4 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Sat, 26 Jun 2021 20:45:47 -0400 Subject: [PATCH 24/28] Update Webkit version for Safari --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 776ea8da3f..4aadbd84d6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,7 +15,7 @@ export const DEFAULT_FIREFOX_VERSION = '89.0.2'; export const DEFAULT_SAFARI_VERSION = { majorVersion: 14, version: '14.1.1', - webkitVersion: '610.4.3.1.7', + webkitVersion: '611.2.7.1.4', }; export const ELECTRON_MAJOR_VERSION = parseInt( From eec69bf3bb2aebdc7dfa2ecbaf602a752c140a1f Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Thu, 15 Jul 2021 09:56:56 -0400 Subject: [PATCH 25/28] 13.1.6 -> NO CRASH! --- app/package.json | 2 +- src/constants.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/package.json b/app/package.json index d97dba021f..44c9152f22 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,6 @@ "source-map-support": "^0.5.19" }, "devDependencies": { - "electron": "^13.1.4" + "electron": "^13.1.6" } } diff --git a/src/constants.ts b/src/constants.ts index 4aadbd84d6..1ca16b36ae 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,8 +4,8 @@ export const DEFAULT_APP_NAME = 'APP'; // Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, // and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION -export const DEFAULT_ELECTRON_VERSION = '13.1.4'; -export const DEFAULT_CHROME_VERSION = '91.0.4472.106'; +export const DEFAULT_ELECTRON_VERSION = '13.1.6'; +export const DEFAULT_CHROME_VERSION = '91.0.4472.124'; // Update each of these periodically // https://product-details.mozilla.org/1.0/firefox_versions.json From 64ae857c6a2ae3ddfdce3d024957ebed8c8cb81f Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Thu, 15 Jul 2021 09:59:19 -0400 Subject: [PATCH 26/28] Fix types/debug build error on Ubuntu --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d22f964181..ed3a3a0b8a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "yargs": "^17.0.1" }, "devDependencies": { + "@types/debug": "^4.1.6", "@types/electron-packager": "^15.0.1", "@types/hasbin": "^1.2.0", "@types/jest": "^26.0.23", From d0383bfb508843c86acf3dcf08e68f562b14b7f3 Mon Sep 17 00:00:00 2001 From: Adam Weeden Date: Thu, 15 Jul 2021 17:00:00 -0400 Subject: [PATCH 27/28] 13 -> 13.1.7 --- app/package.json | 2 +- src/constants.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index 44c9152f22..eba66032ec 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,6 @@ "source-map-support": "^0.5.19" }, "devDependencies": { - "electron": "^13.1.6" + "electron": "^13.1.7" } } diff --git a/src/constants.ts b/src/constants.ts index 1ca16b36ae..38dc9fc26d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,7 +4,7 @@ export const DEFAULT_APP_NAME = 'APP'; // Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, // and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION -export const DEFAULT_ELECTRON_VERSION = '13.1.6'; +export const DEFAULT_ELECTRON_VERSION = '13.1.7'; export const DEFAULT_CHROME_VERSION = '91.0.4472.124'; // Update each of these periodically From 6fb98ba8f9bd4aa3d28538b343c4704c14175e94 Mon Sep 17 00:00:00 2001 From: Ronan Jouchet Date: Fri, 16 Jul 2021 19:03:50 -0400 Subject: [PATCH 28/28] Bump default Firefox version --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 38dc9fc26d..73e3b3891d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,7 +9,7 @@ export const DEFAULT_CHROME_VERSION = '91.0.4472.124'; // Update each of these periodically // https://product-details.mozilla.org/1.0/firefox_versions.json -export const DEFAULT_FIREFOX_VERSION = '89.0.2'; +export const DEFAULT_FIREFOX_VERSION = '90.0'; // https://en.wikipedia.org/wiki/Safari_version_history export const DEFAULT_SAFARI_VERSION = {