* Catch promise errors better * Move subFunctions to bottom of createNewWindow * Use parents when creating child BrowserWindow instances * Some about:blank pages have an anchor (for some reason) * Inject browserWindowOptions better * Interim refactor to MainWindow object * Split up the window functions/helpers/events some * Further separate out window functions + tests * Add a mock for unit testing functions that access electron * Add unit tests for onWillPreventUnload * Improve windowEvents tests * Add the first test for windowHelpers * Move WebRequest event handling to node * insertCSS completely under test * clearAppData completely under test * Fix contextMenu require bug * More tests + fixes * Fix + add to createNewTab tests * Convert createMainWindow back to func + work out gremlins * Move setupWindow away from main since its shared * Make sure contextMenu is handling promises
This commit is contained in:
parent
ec12702359
commit
72de7b3fca
|
@ -1,29 +1,51 @@
|
|||
import { shell } from 'electron';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
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(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 });
|
||||
|
||||
export function initContextMenu(createNewWindow, createNewTab): void {
|
||||
contextMenu({
|
||||
prepend: (actions, params) => {
|
||||
log.debug('contextMenu.prepend', { actions, params });
|
||||
const items = [];
|
||||
if (params.linkURL) {
|
||||
items.push({
|
||||
label: 'Open Link in Default Browser',
|
||||
click: () => {
|
||||
shell.openExternal(params.linkURL); // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
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);
|
||||
},
|
||||
click: () =>
|
||||
createNewWindow(
|
||||
options,
|
||||
setupNativefierWindow,
|
||||
params.linkURL,
|
||||
window,
|
||||
),
|
||||
});
|
||||
if (createNewTab) {
|
||||
if (nativeTabsSupported()) {
|
||||
items.push({
|
||||
label: 'Open Link in New Tab',
|
||||
click: () => {
|
||||
createNewTab(params.linkURL, false);
|
||||
},
|
||||
click: () =>
|
||||
createNewTab(
|
||||
options,
|
||||
setupNativefierWindow,
|
||||
params.linkURL,
|
||||
true,
|
||||
window,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
export function createLoginWindow(loginCallback): BrowserWindow {
|
||||
export async function createLoginWindow(
|
||||
loginCallback,
|
||||
parent?: BrowserWindow,
|
||||
): Promise<BrowserWindow> {
|
||||
log.debug('createLoginWindow', loginCallback, parent);
|
||||
|
||||
const loginWindow = new BrowserWindow({
|
||||
parent,
|
||||
width: 300,
|
||||
height: 400,
|
||||
frame: false,
|
||||
|
@ -12,8 +20,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]);
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import {
|
||||
BrowserWindow,
|
||||
shell,
|
||||
ipcMain,
|
||||
dialog,
|
||||
Event,
|
||||
HeadersReceivedResponse,
|
||||
OnHeadersReceivedListenerDetails,
|
||||
WebContents,
|
||||
} from 'electron';
|
||||
import { ipcMain, BrowserWindow, IpcMainEvent } from 'electron';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
import log from 'loglevel';
|
||||
|
||||
import {
|
||||
isOSX,
|
||||
linkIsInternal,
|
||||
getCssToInject,
|
||||
shouldInjectCss,
|
||||
getAppIcon,
|
||||
nativeTabsSupported,
|
||||
getCounterValue,
|
||||
isOSX,
|
||||
nativeTabsSupported,
|
||||
openExternal,
|
||||
} from '../helpers/helpers';
|
||||
import { setupNativefierWindow } from '../helpers/windowEvents';
|
||||
import {
|
||||
clearAppData,
|
||||
clearCache,
|
||||
getCurrentURL,
|
||||
getDefaultWindowOptions,
|
||||
goBack,
|
||||
goForward,
|
||||
goToURL,
|
||||
hideWindow,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomReset,
|
||||
} from '../helpers/windowHelpers';
|
||||
import { initContextMenu } from './contextMenu';
|
||||
import { onNewWindowHelper } from './mainWindowHelpers';
|
||||
import { createMenu } from './menu';
|
||||
|
||||
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||
const ZOOM_INTERVAL = 0.1;
|
||||
|
||||
type SessionInteractionRequest = {
|
||||
id?: string;
|
||||
|
@ -44,119 +45,23 @@ 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();
|
||||
}
|
||||
// will close the window on other platforms
|
||||
}
|
||||
|
||||
function injectCss(browserWindow: BrowserWindow): void {
|
||||
if (!shouldInjectCss()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssToInject = getCssToInject();
|
||||
|
||||
browserWindow.webContents.on('did-navigate', () => {
|
||||
log.debug('browserWindow.webContents.did-navigate');
|
||||
// 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) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
details.webContents.insertCSS(cssToInject);
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function clearCache(browserWindow: BrowserWindow): Promise<void> {
|
||||
const { session } = browserWindow.webContents;
|
||||
await session.clearStorageData();
|
||||
await session.clearCache();
|
||||
}
|
||||
|
||||
function setProxyRules(browserWindow: BrowserWindow, proxyRules): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
browserWindow.webContents.session.setProxy({
|
||||
proxyRules,
|
||||
pacScript: '',
|
||||
proxyBypassRules: '',
|
||||
});
|
||||
}
|
||||
|
||||
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()})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type createWindowResult = {
|
||||
window: BrowserWindow;
|
||||
setupWindow: (window: BrowserWindow) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{}} nativefierOptions AppArgs from nativefier.json
|
||||
* @param {function} onAppQuit
|
||||
* @param {function} setDockBadge
|
||||
*/
|
||||
export function createMainWindow(
|
||||
export async function createMainWindow(
|
||||
nativefierOptions,
|
||||
onAppQuit,
|
||||
setDockBadge,
|
||||
): createWindowResult {
|
||||
onAppQuit: () => void,
|
||||
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||
): Promise<BrowserWindow> {
|
||||
const options = { ...nativefierOptions };
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: options.width || 1280,
|
||||
defaultHeight: options.height || 800,
|
||||
});
|
||||
|
||||
const DEFAULT_WINDOW_OPTIONS = {
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
|
||||
const browserwindowOptions = { ...options.browserwindowOptions };
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
frame: !options.hideWindowFrame,
|
||||
width: mainWindowState.width,
|
||||
|
@ -170,14 +75,13 @@ export function createMainWindow(
|
|||
autoHideMenuBar: !options.showMenuBar,
|
||||
icon: getAppIcon(),
|
||||
// set to undefined and not false because explicitly setting to false will disable full screen
|
||||
fullscreen: options.fullScreen || undefined,
|
||||
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,
|
||||
...browserwindowOptions,
|
||||
...getDefaultWindowOptions(options),
|
||||
});
|
||||
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
@ -193,274 +97,14 @@ export 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<void> => {
|
||||
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,
|
||||
zoomIn: onZoomIn,
|
||||
zoomOut: onZoomOut,
|
||||
zoomReset: onZoomReset,
|
||||
zoomBuildTimeValue: options.zoom,
|
||||
goBack: onGoBack,
|
||||
goForward: onGoForward,
|
||||
getCurrentUrl,
|
||||
gotoUrl,
|
||||
clearAppData,
|
||||
disableDevTools: options.disableDevTools,
|
||||
};
|
||||
|
||||
createMenu(menuOptions);
|
||||
if (!options.disableContextMenu) {
|
||||
initContextMenu(
|
||||
createNewWindow,
|
||||
nativeTabsSupported() ? createNewTab : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
mainWindow.webContents.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
if (options.proxyRules) {
|
||||
setProxyRules(mainWindow, options.proxyRules);
|
||||
}
|
||||
|
||||
injectCss(mainWindow);
|
||||
sendParamsOnDidFinishLoad(mainWindow);
|
||||
createMainMenu(options, mainWindow, onAppQuit);
|
||||
createContextMenu(options, mainWindow);
|
||||
setupNativefierWindow(options, mainWindow);
|
||||
|
||||
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('');
|
||||
}
|
||||
});
|
||||
setupCounter(options, mainWindow, 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('');
|
||||
});
|
||||
setupNotificationBadge(options, mainWindow, setDockBadge);
|
||||
}
|
||||
|
||||
ipcMain.on('notification-click', () => {
|
||||
|
@ -468,6 +112,117 @@ export function createMainWindow(
|
|||
mainWindow.show();
|
||||
});
|
||||
|
||||
setupSessionInteraction(options, mainWindow);
|
||||
|
||||
if (options.clearCache) {
|
||||
await clearCache(mainWindow);
|
||||
}
|
||||
|
||||
await mainWindow.loadURL(options.targetUrl);
|
||||
|
||||
setupCloseEvent(options, mainWindow);
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function createContextMenu(options, window: BrowserWindow): void {
|
||||
if (!options.disableContextMenu) {
|
||||
initContextMenu(options, 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()})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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('');
|
||||
});
|
||||
}
|
||||
|
||||
function setupSessionInteraction(options, window: BrowserWindow): void {
|
||||
// See API.md / "Accessing The Electron Session"
|
||||
ipcMain.on(
|
||||
'session-interaction',
|
||||
|
@ -489,7 +244,7 @@ export function createMainWindow(
|
|||
}
|
||||
|
||||
// Call func with funcArgs
|
||||
result.value = mainWindow.webContents.session[request.func](
|
||||
result.value = window.webContents.session[request.func](
|
||||
...request.funcArgs,
|
||||
);
|
||||
|
||||
|
@ -498,22 +253,26 @@ 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) {
|
||||
if (request.propertyValue !== undefined) {
|
||||
// Set the property
|
||||
mainWindow.webContents.session[request.property] =
|
||||
window.webContents.session[request.property] =
|
||||
request.propertyValue;
|
||||
}
|
||||
|
||||
// Get the property value
|
||||
result.value = mainWindow.webContents.session[request.property];
|
||||
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(
|
||||
|
@ -534,52 +293,4 @@ export function createMainWindow(
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
mainWindow.webContents.setVisualZoomLevelLimits(1, 3);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
mainWindow.loadURL(options.targetUrl);
|
||||
|
||||
// @ts-ignore
|
||||
mainWindow.on('new-tab', () => createNewTab(options.targetUrl, true));
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
hideWindow(mainWindow, event, options.fastQuit, options.tray);
|
||||
|
||||
if (options.clearCache) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
clearCache(mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
return { window: mainWindow, setupWindow };
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
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,
|
||||
): void {
|
||||
if (!linkIsInternal(targetUrl, urlToGo, internalUrls)) {
|
||||
preventDefault();
|
||||
if (blockExternal) {
|
||||
onBlockedExternalUrl(urlToGo);
|
||||
} else {
|
||||
openExternal(urlToGo);
|
||||
}
|
||||
} else if (urlToGo === 'about:blank') {
|
||||
const newWindow = createAboutBlankWindow();
|
||||
preventDefault(newWindow);
|
||||
} else if (nativeTabsSupported()) {
|
||||
if (disposition === 'background-tab') {
|
||||
const newTab = createNewTab(urlToGo, false);
|
||||
preventDefault(newTab);
|
||||
} else if (disposition === 'foreground-tab') {
|
||||
const newTab = createNewTab(urlToGo, true);
|
||||
preventDefault(newTab);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
@ -28,10 +28,11 @@ export function createMenu({
|
|||
zoomBuildTimeValue,
|
||||
goBack,
|
||||
goForward,
|
||||
getCurrentUrl,
|
||||
gotoUrl,
|
||||
getCurrentURL,
|
||||
goToURL,
|
||||
clearAppData,
|
||||
disableDevTools,
|
||||
openExternal,
|
||||
}): void {
|
||||
const zoomResetLabel =
|
||||
zoomBuildTimeValue === 1.0
|
||||
|
@ -68,7 +69,7 @@ export function createMenu({
|
|||
label: 'Copy Current URL',
|
||||
accelerator: 'CmdOrCtrl+L',
|
||||
click: () => {
|
||||
const currentURL = getCurrentUrl();
|
||||
const currentURL = getCurrentURL();
|
||||
clipboard.writeText(currentURL);
|
||||
},
|
||||
},
|
||||
|
@ -240,15 +241,13 @@ 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');
|
||||
openExternal('https://github.com/nativefier/nativefier');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue',
|
||||
click: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
shell.openExternal('https://github.com/nativefier/nativefier/issues');
|
||||
openExternal('https://github.com/nativefier/nativefier/issues');
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -333,7 +332,7 @@ export function createMenu({
|
|||
return {
|
||||
label: bookmark.title,
|
||||
click: () => {
|
||||
gotoUrl(bookmark.url);
|
||||
goToURL(bookmark.url);
|
||||
},
|
||||
accelerator: accelerator,
|
||||
};
|
||||
|
|
|
@ -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.debug(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';
|
||||
}
|
||||
|
@ -44,7 +116,8 @@ export function linkIsInternal(
|
|||
newUrl: string,
|
||||
internalUrlRegex: string | RegExp,
|
||||
): boolean {
|
||||
if (newUrl === 'about:blank') {
|
||||
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
|
||||
if (newUrl.split('#')[0] === 'about:blank') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -80,87 +153,16 @@ 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 shouldInjectCss(): boolean {
|
||||
try {
|
||||
return fs.existsSync(INJECT_DIR);
|
||||
} catch (e) {
|
||||
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;
|
||||
export function openExternal(
|
||||
url: string,
|
||||
options?: OpenExternalOptions,
|
||||
): Promise<void> {
|
||||
log.debug('openExternal', { url, options });
|
||||
return shell.openExternal(url, options);
|
||||
}
|
||||
|
||||
export function removeUserAgentSpecifics(
|
||||
|
@ -177,3 +179,11 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,357 @@
|
|||
jest.mock('./helpers');
|
||||
jest.mock('./windowEvents');
|
||||
jest.mock('./windowHelpers');
|
||||
|
||||
import { dialog, BrowserWindow, WebContents } from 'electron';
|
||||
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
|
||||
const { onNewWindowHelper, onWillNavigate, onWillPreventUnload } =
|
||||
jest.requireActual('./windowEvents');
|
||||
import {
|
||||
blockExternalURL,
|
||||
createAboutBlankWindow,
|
||||
createNewTab,
|
||||
} 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 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;
|
||||
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(() => {
|
||||
mockBlockExternalURL
|
||||
.mockReset()
|
||||
.mockReturnValue(Promise.resolve(undefined));
|
||||
mockCreateAboutBlank.mockReset();
|
||||
mockCreateNewTab.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();
|
||||
mockOpenExternal.mockRestore();
|
||||
});
|
||||
|
||||
test('internal urls should not be handled', () => {
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('external urls should be opened externally', () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('external urls should be ignored if blockExternalUrls is true', () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
const options = {
|
||||
blockExternalUrls: true,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
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 = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('tab disposition should be ignored if url is external', () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('foreground tabs with internal urls should be opened in the foreground', () => {
|
||||
mockNativeTabsSupported.mockReturnValue(true);
|
||||
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
true,
|
||||
undefined,
|
||||
);
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('background tabs with internal urls should be opened in background tabs', () => {
|
||||
mockNativeTabsSupported.mockReturnValue(true);
|
||||
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
backgroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
||||
options,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('about:blank urls should be handled', () => {
|
||||
const options = {
|
||||
blockExternalUrls: false,
|
||||
targetUrl: originalURL,
|
||||
};
|
||||
onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
'about:blank',
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
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()
|
||||
.mockReturnValue(Promise.resolve(undefined));
|
||||
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')
|
||||
.mockImplementation(() => new BrowserWindow());
|
||||
const mockShowDialog: jest.SpyInstance = jest.spyOn(
|
||||
dialog,
|
||||
'showMessageBoxSync',
|
||||
);
|
||||
const preventDefault: jest.SpyInstance = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockFromWebContents.mockReset();
|
||||
mockShowDialog.mockReset().mockReturnValue(undefined);
|
||||
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.mockReturnValue(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.mockReturnValue(1);
|
||||
|
||||
const event = { preventDefault, sender: new WebContents() };
|
||||
onWillPreventUnload(event);
|
||||
|
||||
expect(mockFromWebContents).toHaveBeenCalledWith(event.sender);
|
||||
expect(mockShowDialog).toHaveBeenCalled();
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,194 @@
|
|||
import { dialog, BrowserWindow, IpcMainEvent, WebContents } from 'electron';
|
||||
import log from 'loglevel';
|
||||
|
||||
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
||||
import {
|
||||
blockExternalURL,
|
||||
createAboutBlankWindow,
|
||||
createNewTab,
|
||||
injectCSS,
|
||||
sendParamsOnDidFinishLoad,
|
||||
setProxyRules,
|
||||
} from './windowHelpers';
|
||||
|
||||
export function onNewWindow(
|
||||
options,
|
||||
setupWindow: (...args) => void,
|
||||
event: Event & { newGuest?: any },
|
||||
urlToGo: string,
|
||||
frameName: string,
|
||||
disposition:
|
||||
| 'default'
|
||||
| 'foreground-tab'
|
||||
| 'background-tab'
|
||||
| 'new-window'
|
||||
| 'save-to-disk'
|
||||
| 'other',
|
||||
parent?: BrowserWindow,
|
||||
): Promise<void> {
|
||||
log.debug('onNewWindow', {
|
||||
event,
|
||||
urlToGo,
|
||||
frameName,
|
||||
disposition,
|
||||
parent,
|
||||
});
|
||||
const preventDefault = (newGuest: any): void => {
|
||||
event.preventDefault();
|
||||
if (newGuest) {
|
||||
event.newGuest = newGuest;
|
||||
}
|
||||
};
|
||||
return onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
urlToGo,
|
||||
disposition,
|
||||
preventDefault,
|
||||
parent,
|
||||
);
|
||||
}
|
||||
|
||||
export function onNewWindowHelper(
|
||||
options,
|
||||
setupWindow: (...args) => void,
|
||||
urlToGo: string,
|
||||
disposition: string,
|
||||
preventDefault,
|
||||
parent?: BrowserWindow,
|
||||
): Promise<void> {
|
||||
log.debug('onNewWindowHelper', {
|
||||
urlToGo,
|
||||
disposition,
|
||||
preventDefault,
|
||||
parent,
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function onWillNavigate(
|
||||
options,
|
||||
event: IpcMainEvent,
|
||||
urlToGo: string,
|
||||
): Promise<void> {
|
||||
log.debug('onWillNavigate', { options, event, urlToGo });
|
||||
if (!linkIsInternal(options.targetUrl, urlToGo, options.internalUrls)) {
|
||||
event.preventDefault();
|
||||
if (options.blockExternalUrls) {
|
||||
return blockExternalURL(urlToGo).then(() => null);
|
||||
} else {
|
||||
return openExternal(urlToGo);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
export function onWillPreventUnload(event: IpcMainEvent): void {
|
||||
log.debug('onWillPreventUnload', event);
|
||||
|
||||
const webContents: WebContents = event.sender;
|
||||
if (webContents === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
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 { clearAppData, createNewTab, 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('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;
|
||||
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',
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test('will not inject if shouldInjectCSS is false', () => {
|
||||
mockShouldInjectCSS.mockReturnValue(false);
|
||||
|
||||
const window = new BrowserWindow();
|
||||
|
||||
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 catch errors inserting CSS', (done) => {
|
||||
mockGetCSSToInject.mockReturnValue(css);
|
||||
|
||||
mockWebContentsInsertCSS.mockReturnValue(
|
||||
Promise.reject('css insertion error'),
|
||||
);
|
||||
|
||||
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(mockLogError).toHaveBeenCalledWith(
|
||||
'webContents.insertCSS ERROR',
|
||||
'css insertion error',
|
||||
);
|
||||
expect(result.cancel).toBe(false);
|
||||
expect(result.responseHeaders).toBe(responseHeaders);
|
||||
done();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,302 @@
|
|||
import {
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
dialog,
|
||||
HeadersReceivedResponse,
|
||||
IpcMainEvent,
|
||||
MessageBoxReturnValue,
|
||||
OnHeadersReceivedListenerDetails,
|
||||
} from 'electron';
|
||||
|
||||
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((focusedWindow: BrowserWindow) => {
|
||||
focusedWindow.webContents.zoomFactor =
|
||||
focusedWindow.webContents.zoomFactor + adjustment;
|
||||
});
|
||||
}
|
||||
|
||||
export function blockExternalURL(url: string): Promise<MessageBoxReturnValue> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearAppData(window: BrowserWindow): Promise<void> {
|
||||
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<void> {
|
||||
const { session } = window.webContents;
|
||||
await session.clearStorageData();
|
||||
await session.clearCache();
|
||||
}
|
||||
|
||||
export function createAboutBlankWindow(
|
||||
options,
|
||||
setupWindow: (...args) => void,
|
||||
parent?: BrowserWindow,
|
||||
): BrowserWindow {
|
||||
const window = createNewWindow(options, setupWindow, 'about:blank', parent);
|
||||
window.hide();
|
||||
window.webContents.once('did-stop-loading', () => {
|
||||
if (window.webContents.getURL() === 'about:blank') {
|
||||
window.close();
|
||||
} else {
|
||||
window.show();
|
||||
}
|
||||
});
|
||||
return window;
|
||||
}
|
||||
|
||||
export function createNewTab(
|
||||
options,
|
||||
setupWindow,
|
||||
url: string,
|
||||
foreground: boolean,
|
||||
parent?: BrowserWindow,
|
||||
): Promise<BrowserWindow> {
|
||||
log.debug('createNewTab', { url, foreground, parent });
|
||||
return withFocusedWindow((focusedWindow) => {
|
||||
const newTab = createNewWindow(options, setupWindow, url, parent);
|
||||
focusedWindow.addTabbedWindow(newTab);
|
||||
if (!foreground) {
|
||||
focusedWindow.focus();
|
||||
}
|
||||
return newTab;
|
||||
});
|
||||
}
|
||||
|
||||
export function createNewWindow(
|
||||
options,
|
||||
setupWindow: (...args) => void,
|
||||
url: string,
|
||||
parent?: BrowserWindow,
|
||||
): BrowserWindow {
|
||||
log.debug('createNewWindow', { url, parent });
|
||||
const window = new BrowserWindow({
|
||||
parent,
|
||||
...getDefaultWindowOptions(options),
|
||||
});
|
||||
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 getDefaultWindowOptions(
|
||||
options,
|
||||
): BrowserWindowConstructorOptions {
|
||||
const browserwindowOptions: BrowserWindowConstructorOptions = {
|
||||
...options.browserwindowOptions,
|
||||
};
|
||||
// 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;
|
||||
|
||||
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,
|
||||
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,
|
||||
...webPreferences,
|
||||
},
|
||||
...browserwindowOptions,
|
||||
};
|
||||
|
||||
log.debug('getDefaultWindowOptions', {
|
||||
options,
|
||||
webPreferences,
|
||||
defaultOptions,
|
||||
});
|
||||
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
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): Promise<void> {
|
||||
return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url));
|
||||
}
|
||||
|
||||
export function hideWindow(
|
||||
window: BrowserWindow,
|
||||
event: IpcMainEvent,
|
||||
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` 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
|
||||
(
|
||||
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);
|
||||
})
|
||||
.finally(() =>
|
||||
callback({
|
||||
cancel: false,
|
||||
responseHeaders: details.responseHeaders,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
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) => any): any {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
return block(focusedWindow);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function zoomOut(): void {
|
||||
log.debug('zoomOut');
|
||||
adjustWindowZoom(-ZOOM_INTERVAL);
|
||||
}
|
||||
|
||||
export function zoomReset(options): void {
|
||||
log.debug('zoomReset');
|
||||
withFocusedWindow((focusedWindow) => {
|
||||
focusedWindow.webContents.zoomFactor = options.zoom;
|
||||
});
|
||||
}
|
||||
|
||||
export function zoomIn(): void {
|
||||
log.debug('zoomIn');
|
||||
adjustWindowZoom(ZOOM_INTERVAL);
|
||||
}
|
|
@ -10,19 +10,21 @@ import {
|
|||
globalShortcut,
|
||||
systemPreferences,
|
||||
BrowserWindow,
|
||||
IpcMainEvent,
|
||||
} from 'electron';
|
||||
import electronDownload from 'electron-dl';
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import { createLoginWindow } from './components/loginWindow';
|
||||
import {
|
||||
createMainWindow,
|
||||
saveAppArgs,
|
||||
APP_ARGS_FILE_PATH,
|
||||
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')) {
|
||||
|
@ -31,6 +33,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'));
|
||||
|
@ -95,7 +99,6 @@ if (appArgs.processEnvs) {
|
|||
}
|
||||
|
||||
let mainWindow: BrowserWindow;
|
||||
let setupWindow: (BrowserWindow) => void;
|
||||
|
||||
if (typeof appArgs.flashPluginDir === 'string') {
|
||||
app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir);
|
||||
|
@ -233,7 +236,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(
|
||||
|
@ -254,20 +257,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<void> {
|
||||
const mainWindow = await createMainWindow(
|
||||
appArgs,
|
||||
app.quit.bind(this),
|
||||
setDockBadge,
|
||||
);
|
||||
log.debug('onReady', createWindowResult);
|
||||
mainWindow = createWindowResult.window;
|
||||
setupWindow = createWindowResult.setupWindow;
|
||||
|
||||
createTrayIcon(appArgs, mainWindow);
|
||||
|
||||
// Register global shortcuts
|
||||
|
@ -333,17 +334,20 @@ 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', () => {
|
||||
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) => {
|
||||
|
@ -357,13 +361,15 @@ app.on('login', (event, webContents, request, authInfo, callback) => {
|
|||
) {
|
||||
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
||||
} else {
|
||||
createLoginWindow(callback);
|
||||
createLoginWindow(callback, mainWindow).catch((err) =>
|
||||
log.error('createLoginWindow ERROR', err),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
app.on(
|
||||
'accessibility-support-changed',
|
||||
(event: Event, accessibilitySupportEnabled: boolean) => {
|
||||
(event: IpcMainEvent, accessibilitySupportEnabled: boolean) => {
|
||||
log.debug('app.accessibility-support-changed', {
|
||||
event,
|
||||
accessibilitySupportEnabled,
|
||||
|
@ -373,22 +379,23 @@ 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 (setupWindow !== undefined) {
|
||||
setupWindow(window);
|
||||
}
|
||||
});
|
||||
app.on(
|
||||
'browser-window-created',
|
||||
(event: IpcMainEvent, window: BrowserWindow) => {
|
||||
log.debug('app.browser-window-created', { event, window });
|
||||
setupNativefierWindow(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 });
|
||||
});
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/* 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 {
|
||||
webContents: MockWebContents;
|
||||
|
||||
constructor(options?: any) {
|
||||
super(options);
|
||||
this.webContents = new MockWebContents();
|
||||
}
|
||||
|
||||
addTabbedWindow(tab: MockBrowserWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
static fromWebContents(webContents: MockWebContents): MockBrowserWindow {
|
||||
return new MockBrowserWindow();
|
||||
}
|
||||
|
||||
static getFocusedWindow(window: MockBrowserWindow): MockBrowserWindow {
|
||||
return window ?? new MockBrowserWindow();
|
||||
}
|
||||
|
||||
loadURL(url: string, options?: any): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class MockDialog {
|
||||
static showMessageBox(
|
||||
browserWindow: MockBrowserWindow,
|
||||
options: any,
|
||||
): Promise<number> {
|
||||
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<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
clearStorageData(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
class MockWebContents extends EventEmitter {
|
||||
session: MockSession;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.session = new MockSession();
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
insertCSS(css: string, options?: any): Promise<string> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class MockWebRequest {
|
||||
emitter: InternalEmitter;
|
||||
|
||||
constructor() {
|
||||
this.emitter = new InternalEmitter();
|
||||
}
|
||||
|
||||
onHeadersReceived(
|
||||
filter: any,
|
||||
listener:
|
||||
| ((
|
||||
details: any,
|
||||
callback: (headersReceivedResponse: any) => void,
|
||||
) => void)
|
||||
| null,
|
||||
): void {
|
||||
this.emitter.addListener(
|
||||
'onHeadersReceived',
|
||||
(details: any, callback: (headersReceivedResponse: any) => void) =>
|
||||
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,
|
||||
MockSession as Session,
|
||||
MockWebContents as WebContents,
|
||||
MockWebRequest as WebRequest,
|
||||
};
|
|
@ -90,6 +90,9 @@
|
|||
},
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"moduleNameMapper": {
|
||||
"^electron$": "<rootDir>/app/dist/mocks/electron.js"
|
||||
},
|
||||
"setupFiles": [
|
||||
"./lib/jestSetupFiles"
|
||||
],
|
||||
|
|
|
@ -66,7 +66,7 @@ export async function copyFileOrDir(
|
|||
});
|
||||
}
|
||||
|
||||
export async function downloadFile(fileUrl: string): Promise<DownloadResult> {
|
||||
export function downloadFile(fileUrl: string): Promise<DownloadResult> {
|
||||
log.debug(`Downloading ${fileUrl}`);
|
||||
return axios
|
||||
.get(fileUrl, {
|
||||
|
|
Loading…
Reference in New Issue