mirror of https://github.com/jiahaog/Nativefier
* 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 { BrowserWindow } from 'electron';
|
||||||
import contextMenu from 'electron-context-menu';
|
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({
|
contextMenu({
|
||||||
prepend: (actions, params) => {
|
prepend: (actions, params) => {
|
||||||
|
log.debug('contextMenu.prepend', { actions, params });
|
||||||
const items = [];
|
const items = [];
|
||||||
if (params.linkURL) {
|
if (params.linkURL) {
|
||||||
items.push({
|
items.push({
|
||||||
label: 'Open Link in Default Browser',
|
label: 'Open Link in Default Browser',
|
||||||
click: () => {
|
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({
|
items.push({
|
||||||
label: 'Open Link in New Window',
|
label: 'Open Link in New Window',
|
||||||
click: () => {
|
click: () =>
|
||||||
createNewWindow(params.linkURL);
|
createNewWindow(
|
||||||
},
|
options,
|
||||||
|
setupNativefierWindow,
|
||||||
|
params.linkURL,
|
||||||
|
window,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (createNewTab) {
|
if (nativeTabsSupported()) {
|
||||||
items.push({
|
items.push({
|
||||||
label: 'Open Link in New Tab',
|
label: 'Open Link in New Tab',
|
||||||
click: () => {
|
click: () =>
|
||||||
createNewTab(params.linkURL, false);
|
createNewTab(
|
||||||
},
|
options,
|
||||||
|
setupNativefierWindow,
|
||||||
|
params.linkURL,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { BrowserWindow, ipcMain } from 'electron';
|
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({
|
const loginWindow = new BrowserWindow({
|
||||||
|
parent,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 400,
|
height: 400,
|
||||||
frame: false,
|
frame: false,
|
||||||
|
@ -12,8 +20,9 @@ export function createLoginWindow(loginCallback): BrowserWindow {
|
||||||
nodeIntegration: true, // TODO work around this; insecure
|
nodeIntegration: true, // TODO work around this; insecure
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await loginWindow.loadURL(
|
||||||
loginWindow.loadURL(`file://${path.join(__dirname, 'static/login.html')}`);
|
`file://${path.join(__dirname, 'static/login.html')}`,
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.once('login-message', (event, usernameAndPassword) => {
|
ipcMain.once('login-message', (event, usernameAndPassword) => {
|
||||||
loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
|
loginCallback(usernameAndPassword[0], usernameAndPassword[1]);
|
||||||
|
|
|
@ -1,34 +1,35 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import {
|
import { ipcMain, BrowserWindow, IpcMainEvent } from 'electron';
|
||||||
BrowserWindow,
|
|
||||||
shell,
|
|
||||||
ipcMain,
|
|
||||||
dialog,
|
|
||||||
Event,
|
|
||||||
HeadersReceivedResponse,
|
|
||||||
OnHeadersReceivedListenerDetails,
|
|
||||||
WebContents,
|
|
||||||
} from 'electron';
|
|
||||||
import windowStateKeeper from 'electron-window-state';
|
import windowStateKeeper from 'electron-window-state';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isOSX,
|
|
||||||
linkIsInternal,
|
|
||||||
getCssToInject,
|
|
||||||
shouldInjectCss,
|
|
||||||
getAppIcon,
|
getAppIcon,
|
||||||
nativeTabsSupported,
|
|
||||||
getCounterValue,
|
getCounterValue,
|
||||||
|
isOSX,
|
||||||
|
nativeTabsSupported,
|
||||||
|
openExternal,
|
||||||
} from '../helpers/helpers';
|
} 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 { initContextMenu } from './contextMenu';
|
||||||
import { onNewWindowHelper } from './mainWindowHelpers';
|
|
||||||
import { createMenu } from './menu';
|
import { createMenu } from './menu';
|
||||||
|
|
||||||
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||||
const ZOOM_INTERVAL = 0.1;
|
|
||||||
|
|
||||||
type SessionInteractionRequest = {
|
type SessionInteractionRequest = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -44,119 +45,23 @@ type SessionInteractionResult = {
|
||||||
error?: Error;
|
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 {{}} nativefierOptions AppArgs from nativefier.json
|
||||||
* @param {function} onAppQuit
|
* @param {function} onAppQuit
|
||||||
* @param {function} setDockBadge
|
* @param {function} setDockBadge
|
||||||
*/
|
*/
|
||||||
export function createMainWindow(
|
export async function createMainWindow(
|
||||||
nativefierOptions,
|
nativefierOptions,
|
||||||
onAppQuit,
|
onAppQuit: () => void,
|
||||||
setDockBadge,
|
setDockBadge: (value: number | string, bounce?: boolean) => void,
|
||||||
): createWindowResult {
|
): Promise<BrowserWindow> {
|
||||||
const options = { ...nativefierOptions };
|
const options = { ...nativefierOptions };
|
||||||
|
|
||||||
const mainWindowState = windowStateKeeper({
|
const mainWindowState = windowStateKeeper({
|
||||||
defaultWidth: options.width || 1280,
|
defaultWidth: options.width || 1280,
|
||||||
defaultHeight: options.height || 800,
|
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({
|
const mainWindow = new BrowserWindow({
|
||||||
frame: !options.hideWindowFrame,
|
frame: !options.hideWindowFrame,
|
||||||
width: mainWindowState.width,
|
width: mainWindowState.width,
|
||||||
|
@ -170,14 +75,13 @@ export function createMainWindow(
|
||||||
autoHideMenuBar: !options.showMenuBar,
|
autoHideMenuBar: !options.showMenuBar,
|
||||||
icon: getAppIcon(),
|
icon: getAppIcon(),
|
||||||
// set to undefined and not false because explicitly setting to false will disable full screen
|
// 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.
|
// Whether the window should always stay on top of other windows. Default is false.
|
||||||
alwaysOnTop: options.alwaysOnTop,
|
alwaysOnTop: options.alwaysOnTop,
|
||||||
titleBarStyle: options.titleBarStyle,
|
titleBarStyle: options.titleBarStyle,
|
||||||
show: options.tray !== 'start-in-tray',
|
show: options.tray !== 'start-in-tray',
|
||||||
backgroundColor: options.backgroundColor,
|
backgroundColor: options.backgroundColor,
|
||||||
...DEFAULT_WINDOW_OPTIONS,
|
...getDefaultWindowOptions(options),
|
||||||
...browserwindowOptions,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindowState.manage(mainWindow);
|
mainWindowState.manage(mainWindow);
|
||||||
|
@ -193,274 +97,14 @@ export function createMainWindow(
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
const withFocusedWindow = (block: (window: BrowserWindow) => void): void => {
|
createMainMenu(options, mainWindow, onAppQuit);
|
||||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
createContextMenu(options, mainWindow);
|
||||||
if (focusedWindow) {
|
setupNativefierWindow(options, mainWindow);
|
||||||
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);
|
|
||||||
|
|
||||||
if (options.counter) {
|
if (options.counter) {
|
||||||
mainWindow.on('page-title-updated', (event, title) => {
|
setupCounter(options, mainWindow, setDockBadge);
|
||||||
log.debug('mainWindow.page-title-updated', { event, title });
|
|
||||||
const counterValue = getCounterValue(title);
|
|
||||||
if (counterValue) {
|
|
||||||
setDockBadge(counterValue, options.bounce);
|
|
||||||
} else {
|
|
||||||
setDockBadge('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
ipcMain.on('notification', () => {
|
setupNotificationBadge(options, mainWindow, setDockBadge);
|
||||||
log.debug('ipcMain.notification');
|
|
||||||
if (!isOSX() || mainWindow.isFocused()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDockBadge('•', options.bounce);
|
|
||||||
});
|
|
||||||
mainWindow.on('focus', () => {
|
|
||||||
log.debug('mainWindow.focus');
|
|
||||||
setDockBadge('');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('notification-click', () => {
|
ipcMain.on('notification-click', () => {
|
||||||
|
@ -468,6 +112,117 @@ export function createMainWindow(
|
||||||
mainWindow.show();
|
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"
|
// See API.md / "Accessing The Electron Session"
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
'session-interaction',
|
'session-interaction',
|
||||||
|
@ -489,7 +244,7 @@ export function createMainWindow(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call func with funcArgs
|
// Call func with funcArgs
|
||||||
result.value = mainWindow.webContents.session[request.func](
|
result.value = window.webContents.session[request.func](
|
||||||
...request.funcArgs,
|
...request.funcArgs,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -498,22 +253,26 @@ export function createMainWindow(
|
||||||
typeof result.value['then'] === 'function'
|
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
|
// 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
|
||||||
result.value = trueResultValue;
|
.then((trueResultValue) => {
|
||||||
log.debug('ipcMain.session-interaction:result', result);
|
result.value = trueResultValue;
|
||||||
event.reply('session-interaction-reply', result);
|
log.debug('ipcMain.session-interaction:result', result);
|
||||||
});
|
event.reply('session-interaction-reply', result);
|
||||||
|
})
|
||||||
|
.catch((err) =>
|
||||||
|
log.error('session-interaction ERROR', request, err),
|
||||||
|
);
|
||||||
awaitingPromise = true;
|
awaitingPromise = true;
|
||||||
}
|
}
|
||||||
} else if (request.property !== undefined) {
|
} else if (request.property !== undefined) {
|
||||||
if (request.propertyValue !== undefined) {
|
if (request.propertyValue !== undefined) {
|
||||||
// Set the property
|
// Set the property
|
||||||
mainWindow.webContents.session[request.property] =
|
window.webContents.session[request.property] =
|
||||||
request.propertyValue;
|
request.propertyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the property value
|
// Get the property value
|
||||||
result.value = mainWindow.webContents.session[request.property];
|
result.value = window.webContents.session[request.property];
|
||||||
} else {
|
} else {
|
||||||
// Why even send the event if you're going to do this? You're just wasting time! ;)
|
// Why even send the event if you're going to do this? You're just wasting time! ;)
|
||||||
throw Error(
|
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 * as fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Menu, clipboard, shell, MenuItemConstructorOptions } from 'electron';
|
import { clipboard, Menu, MenuItemConstructorOptions } from 'electron';
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
type BookmarksLink = {
|
type BookmarksLink = {
|
||||||
|
@ -28,10 +28,11 @@ export function createMenu({
|
||||||
zoomBuildTimeValue,
|
zoomBuildTimeValue,
|
||||||
goBack,
|
goBack,
|
||||||
goForward,
|
goForward,
|
||||||
getCurrentUrl,
|
getCurrentURL,
|
||||||
gotoUrl,
|
goToURL,
|
||||||
clearAppData,
|
clearAppData,
|
||||||
disableDevTools,
|
disableDevTools,
|
||||||
|
openExternal,
|
||||||
}): void {
|
}): void {
|
||||||
const zoomResetLabel =
|
const zoomResetLabel =
|
||||||
zoomBuildTimeValue === 1.0
|
zoomBuildTimeValue === 1.0
|
||||||
|
@ -68,7 +69,7 @@ export function createMenu({
|
||||||
label: 'Copy Current URL',
|
label: 'Copy Current URL',
|
||||||
accelerator: 'CmdOrCtrl+L',
|
accelerator: 'CmdOrCtrl+L',
|
||||||
click: () => {
|
click: () => {
|
||||||
const currentURL = getCurrentUrl();
|
const currentURL = getCurrentURL();
|
||||||
clipboard.writeText(currentURL);
|
clipboard.writeText(currentURL);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -240,15 +241,13 @@ export function createMenu({
|
||||||
{
|
{
|
||||||
label: `Built with Nativefier v${nativefierVersion}`,
|
label: `Built with Nativefier v${nativefierVersion}`,
|
||||||
click: () => {
|
click: () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
openExternal('https://github.com/nativefier/nativefier');
|
||||||
shell.openExternal('https://github.com/nativefier/nativefier');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue',
|
label: 'Report an Issue',
|
||||||
click: () => {
|
click: () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
openExternal('https://github.com/nativefier/nativefier/issues');
|
||||||
shell.openExternal('https://github.com/nativefier/nativefier/issues');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -333,7 +332,7 @@ export function createMenu({
|
||||||
return {
|
return {
|
||||||
label: bookmark.title,
|
label: bookmark.title,
|
||||||
click: () => {
|
click: () => {
|
||||||
gotoUrl(bookmark.url);
|
goToURL(bookmark.url);
|
||||||
},
|
},
|
||||||
accelerator: accelerator,
|
accelerator: accelerator,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,83 @@ import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, OpenExternalOptions, shell } from 'electron';
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
export const INJECT_DIR = path.join(__dirname, '..', 'inject');
|
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 {
|
export function isOSX(): boolean {
|
||||||
return os.platform() === 'darwin';
|
return os.platform() === 'darwin';
|
||||||
}
|
}
|
||||||
|
@ -44,7 +116,8 @@ export function linkIsInternal(
|
||||||
newUrl: string,
|
newUrl: string,
|
||||||
internalUrlRegex: string | RegExp,
|
internalUrlRegex: string | RegExp,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (newUrl === 'about:blank') {
|
log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex });
|
||||||
|
if (newUrl.split('#')[0] === 'about:blank') {
|
||||||
return true;
|
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 {
|
export function nativeTabsSupported(): boolean {
|
||||||
return isOSX();
|
return isOSX();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCounterValue(title: string): string {
|
export function openExternal(
|
||||||
const itemCountRegex = /[([{]([\d.,]*)\+?[}\])]/;
|
url: string,
|
||||||
const match = itemCountRegex.exec(title);
|
options?: OpenExternalOptions,
|
||||||
return match ? match[1] : undefined;
|
): Promise<void> {
|
||||||
|
log.debug('openExternal', { url, options });
|
||||||
|
return shell.openExternal(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeUserAgentSpecifics(
|
export function removeUserAgentSpecifics(
|
||||||
|
@ -177,3 +179,11 @@ export function removeUserAgentSpecifics(
|
||||||
.replace(`Electron/${process.versions.electron} `, '')
|
.replace(`Electron/${process.versions.electron} `, '')
|
||||||
.replace(`${appName}/${appVersion} `, ' ');
|
.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,
|
globalShortcut,
|
||||||
systemPreferences,
|
systemPreferences,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
|
IpcMainEvent,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronDownload from 'electron-dl';
|
import electronDownload from 'electron-dl';
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import { createLoginWindow } from './components/loginWindow';
|
import { createLoginWindow } from './components/loginWindow';
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
|
||||||
saveAppArgs,
|
saveAppArgs,
|
||||||
APP_ARGS_FILE_PATH,
|
APP_ARGS_FILE_PATH,
|
||||||
|
createMainWindow,
|
||||||
} from './components/mainWindow';
|
} from './components/mainWindow';
|
||||||
import { createTrayIcon } from './components/trayIcon';
|
import { createTrayIcon } from './components/trayIcon';
|
||||||
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
|
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
|
||||||
import { inferFlashPath } from './helpers/inferFlash';
|
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
|
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
|
||||||
if (require('electron-squirrel-startup')) {
|
if (require('electron-squirrel-startup')) {
|
||||||
|
@ -31,6 +33,8 @@ if (require('electron-squirrel-startup')) {
|
||||||
|
|
||||||
if (process.argv.indexOf('--verbose') > -1) {
|
if (process.argv.indexOf('--verbose') > -1) {
|
||||||
log.setLevel('DEBUG');
|
log.setLevel('DEBUG');
|
||||||
|
process.traceDeprecation = true;
|
||||||
|
process.traceProcessWarnings = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
|
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
|
||||||
|
@ -95,7 +99,6 @@ if (appArgs.processEnvs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow;
|
let mainWindow: BrowserWindow;
|
||||||
let setupWindow: (BrowserWindow) => void;
|
|
||||||
|
|
||||||
if (typeof appArgs.flashPluginDir === 'string') {
|
if (typeof appArgs.flashPluginDir === 'string') {
|
||||||
app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir);
|
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
|
// @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) => {
|
app.on('widevine-ready', (version: string, lastVersion: string) => {
|
||||||
log.debug('app.widevine-ready', { version, lastVersion });
|
log.debug('app.widevine-ready', { version, lastVersion });
|
||||||
onReady();
|
onReady().catch((err) => log.error('onReady ERROR', err));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on(
|
app.on(
|
||||||
|
@ -254,20 +257,18 @@ if (shouldQuit) {
|
||||||
} else {
|
} else {
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
log.debug('ready');
|
log.debug('ready');
|
||||||
onReady();
|
onReady().catch((err) => log.error('onReady ERROR', err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReady(): void {
|
async function onReady(): Promise<void> {
|
||||||
const createWindowResult = createMainWindow(
|
const mainWindow = await createMainWindow(
|
||||||
appArgs,
|
appArgs,
|
||||||
app.quit.bind(this),
|
app.quit.bind(this),
|
||||||
setDockBadge,
|
setDockBadge,
|
||||||
);
|
);
|
||||||
log.debug('onReady', createWindowResult);
|
|
||||||
mainWindow = createWindowResult.window;
|
|
||||||
setupWindow = createWindowResult.setupWindow;
|
|
||||||
createTrayIcon(appArgs, mainWindow);
|
createTrayIcon(appArgs, mainWindow);
|
||||||
|
|
||||||
// Register global shortcuts
|
// Register global shortcuts
|
||||||
|
@ -333,17 +334,20 @@ function onReady(): void {
|
||||||
const oldBuildWarningText =
|
const oldBuildWarningText =
|
||||||
appArgs.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.';
|
'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
|
||||||
dialog.showMessageBox(null, {
|
.showMessageBox(null, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Old build detected',
|
message: 'Old build detected',
|
||||||
detail: oldBuildWarningText,
|
detail: oldBuildWarningText,
|
||||||
});
|
})
|
||||||
|
.catch((err) => log.error('dialog.showMessageBox ERROR', err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.on('new-window-for-tab', () => {
|
app.on('new-window-for-tab', () => {
|
||||||
log.debug('app.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) => {
|
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);
|
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
||||||
} else {
|
} else {
|
||||||
createLoginWindow(callback);
|
createLoginWindow(callback, mainWindow).catch((err) =>
|
||||||
|
log.error('createLoginWindow ERROR', err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on(
|
app.on(
|
||||||
'accessibility-support-changed',
|
'accessibility-support-changed',
|
||||||
(event: Event, accessibilitySupportEnabled: boolean) => {
|
(event: IpcMainEvent, accessibilitySupportEnabled: boolean) => {
|
||||||
log.debug('app.accessibility-support-changed', {
|
log.debug('app.accessibility-support-changed', {
|
||||||
event,
|
event,
|
||||||
accessibilitySupportEnabled,
|
accessibilitySupportEnabled,
|
||||||
|
@ -373,22 +379,23 @@ app.on(
|
||||||
|
|
||||||
app.on(
|
app.on(
|
||||||
'activity-was-continued',
|
'activity-was-continued',
|
||||||
(event: Event, type: string, userInfo: any) => {
|
(event: IpcMainEvent, type: string, userInfo: any) => {
|
||||||
log.debug('app.activity-was-continued', { event, type, userInfo });
|
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 });
|
log.debug('app.browser-window-blur', { event, window });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('browser-window-created', (event: Event, window: BrowserWindow) => {
|
app.on(
|
||||||
log.debug('app.browser-window-created', { event, window });
|
'browser-window-created',
|
||||||
if (setupWindow !== undefined) {
|
(event: IpcMainEvent, window: BrowserWindow) => {
|
||||||
setupWindow(window);
|
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 });
|
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": {
|
"jest": {
|
||||||
"collectCoverage": true,
|
"collectCoverage": true,
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^electron$": "<rootDir>/app/dist/mocks/electron.js"
|
||||||
|
},
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"./lib/jestSetupFiles"
|
"./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}`);
|
log.debug(`Downloading ${fileUrl}`);
|
||||||
return axios
|
return axios
|
||||||
.get(fileUrl, {
|
.get(fileUrl, {
|
||||||
|
|
Loading…
Reference in New Issue