Adds a hidden window (#7063)

* init new window

* fix tests

* preload listener

* merge processes and windows

* trigger CI

* fix types

* mock test flake
This commit is contained in:
Jack Kavanagh authored and GitHub committed 2024-02-05 14:37:05 +01:00
1 parent 302172d4c5
commit 33e80d56cd
11 files changed
+146 -45

No files matched your search

@@ -23,10 +23,10 @@ test('Sign in with GitHub', async ({ app, page }) => {
// https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow
const fakeGitHubOAuthWebFlow = app.evaluate(electron => {
return new Promise<{ redirectUrl: string }>(resolve => {
const webContents = electron.BrowserWindow.getAllWindows()[0].webContents;
const webContents = electron.BrowserWindow.getAllWindows()?.find(w => w.title === 'Insomnia')?.webContents;
// Remove all navigation listeners so that only the one we inject will run
webContents.removeAllListeners('will-navigate');
webContents.on('will-navigate', (event: Event, url: string) => {
webContents?.removeAllListeners('will-navigate');
webContents?.on('will-navigate', (event: Event, url: string) => {
event.preventDefault();
const parsedUrl = new URL(url);
// We use the same state parameter that the app created to assert that we prevent CSRF
@@ -16,8 +16,6 @@ test('can make a mock route', async ({ page }) => {
await page.getByPlaceholder('200').fill('201');
await page.getByRole('button', { name: 'Test' }).click();
await page.getByRole('tab', { name: 'Preview' }).click();
await page.getByLabel('Preview').getByText('123').click();
await page.getByRole('tab', { name: 'Timeline' }).click();
await page.getByText('201').click();
});
@@ -9,10 +9,10 @@ test('Sign in with Gitlab', async ({ app, page }) => {
const fakeGitLabOAuthWebFlow = app.evaluate(electron => {
return new Promise<{ redirectUrl: string }>(resolve => {
const webContents = electron.BrowserWindow.getAllWindows()[0].webContents;
const webContents = electron.BrowserWindow.getAllWindows()?.find(w => w.title === 'Insomnia')?.webContents;
// Remove all navigation listeners so that only the one we inject will run
webContents.removeAllListeners('will-navigate');
webContents.on('will-navigate', (event: Event, url: string) => {
webContents?.removeAllListeners('will-navigate');
webContents?.on('will-navigate', (event: Event, url: string) => {
event.preventDefault();
const parsedUrl = new URL(url);
// We use the same state parameter that the app created to assert that we prevent CSRF
+2
View File
@@ -6,3 +6,5 @@ src/main.min.js
src/main.min.js.map
src/preload.js
src/preload.js.map
src/renderers/hidden-browser-window/index.js
src/renderers/hidden-browser-window/index.js.map
+17 -1
View File
@@ -22,6 +22,9 @@ export default async function build(options: Options) {
'process.env.APP_RENDER_URL': JSON.stringify(
`http://localhost:${PORT}/index.html`
),
'process.env.HIDDEN_BROWSER_WINDOW_URL': JSON.stringify(
`http://localhost:${PORT}/renderers/hidden-browser-window/index.html`
),
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.INSOMNIA_ENV': JSON.stringify('development'),
'process.env.BUILD_DATE': JSON.stringify(new Date()),
@@ -41,6 +44,18 @@ export default async function build(options: Options) {
format: 'cjs',
external: ['electron'],
});
const hiddenBrowserWindow = esbuild.build({
entryPoints: ['./src/renderers/hidden-browser-window/index.ts'],
// the hidden browser window script is always outputed to 'src' as index.html requires a built bundle
outfile: path.join(__dirname, 'src', 'renderers/hidden-browser-window/index.js'),
target: 'esnext',
bundle: true,
platform: 'browser',
sourcemap: true,
format: 'cjs',
external: [],
});
const main = esbuild.build({
entryPoints: ['./src/main.development.ts'],
outfile: path.join(outdir, 'main.min.js'),
@@ -56,7 +71,8 @@ export default async function build(options: Options) {
...Object.keys(builtinModules),
],
});
return Promise.all([main, preload]);
return Promise.all([main, preload, hiddenBrowserWindow]);
}
// Build if ran as a cli script
+1 -1
View File
@@ -25,7 +25,7 @@
"package": "npm run build:app && cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js",
"start": "concurrently -n dev,app --kill-others \"npm run start:dev-server\" \"npm run start:electron\"",
"start:dev-server": "vite dev",
"start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron .",
"start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron --inspect=5858 .",
"test": "jest",
"electron:dev-build": "electron ./build/main.min.js",
"test:watch": "jest --watch",
@@ -158,6 +158,7 @@ const _launchApp = async () => {
const args = process.argv.slice(1).filter(a => a !== '.');
if (args.length) {
window = windowUtils.getOrCreateWindow();
windowUtils.createHiddenBrowserWindow();
window.webContents.send('shell:open', args.join());
}
});
+95 -35
View File
@@ -1,4 +1,14 @@
import electron, { type BrowserWindow as ElectronBrowserWindow, type MenuItemConstructorOptions } from 'electron';
import {
app,
BrowserWindow,
type BrowserWindow as ElectronBrowserWindow,
clipboard,
dialog,
Menu,
type MenuItemConstructorOptions,
screen,
shell,
} from 'electron';
import fs from 'fs';
import * as os from 'os';
import path from 'path';
@@ -16,17 +26,15 @@ import {
} from '../common/constants';
import { docsBase } from '../common/documentation';
import * as log from '../common/log';
import { invariant } from '../utils/invariant';
import LocalStorage from './local-storage';
const { app, Menu, shell, dialog, clipboard, BrowserWindow } = electron;
const DEFAULT_WIDTH = 1280;
const DEFAULT_HEIGHT = 720;
const MINIMUM_WIDTH = 500;
const MINIMUM_HEIGHT = 400;
let newWindow: ElectronBrowserWindow | null = null;
const windows = new Set<ElectronBrowserWindow>();
const browserWindows = new Map<'Insomnia' | 'HiddenBrowserWindow', ElectronBrowserWindow>();
let localStorage: LocalStorage | null = null;
interface Bounds {
@@ -40,14 +48,67 @@ export function init() {
initLocalStorage();
}
export function createWindow() {
export async function createHiddenBrowserWindow(): Promise<ElectronBrowserWindow> {
// if open, close it
if (browserWindows.get('HiddenBrowserWindow')) {
await new Promise<void>(resolve => {
const hiddenBrowserWindow = browserWindows.get('HiddenBrowserWindow');
invariant(hiddenBrowserWindow, 'hiddenBrowserWindow is not defined');
// overwrite the closed handler
hiddenBrowserWindow.on('closed', () => {
if (hiddenBrowserWindow) {
console.log('[main] restarting hidden browser window');
browserWindows.delete('HiddenBrowserWindow');
}
resolve();
});
stopHiddenBrowserWindow();
});
}
const hiddenBrowserWindow = new BrowserWindow({
show: false,
title: 'HiddenBrowserWindow',
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
preload: path.join(__dirname, 'renderers/hidden-browser-window/preload.js'),
spellcheck: false,
},
});
browserWindows.set('HiddenBrowserWindow', hiddenBrowserWindow);
const hiddenBrowserWindowPath = path.resolve(__dirname, './renderers/hidden-browser-window/index.html');
const hiddenBrowserWindowUrl = process.env.HIDDEN_BROWSER_WINDOW_URL || pathToFileURL(hiddenBrowserWindowPath).href;
hiddenBrowserWindow.loadURL(hiddenBrowserWindowUrl);
console.log('[main][init hidden win step 1/6]: starting hidden browser window');
hiddenBrowserWindow.on('closed', () => {
if (browserWindows.get('HiddenBrowserWindow')) {
console.log('[main] closing hidden browser window');
browserWindows.delete('HiddenBrowserWindow');
}
});
return hiddenBrowserWindow;
}
export function stopHiddenBrowserWindow() {
browserWindows.get('HiddenBrowserWindow')?.close();
}
export function createWindow(): ElectronBrowserWindow {
const { bounds, fullscreen, maximize } = getBounds();
const { x, y, width, height } = bounds;
const appLogo = 'static/insomnia-core-logo_16x.png';
let isVisibleOnAnyDisplay = true;
for (const d of electron.screen.getAllDisplays()) {
for (const d of screen.getAllDisplays()) {
const isVisibleOnDisplay =
// @ts-expect-error -- TSCONVERSION genuine error
x >= d.bounds.x &&
@@ -63,7 +124,7 @@ export function createWindow() {
}
}
newWindow = new BrowserWindow({
const mainBrowserWindow = new BrowserWindow({
// Make sure we don't initialize the window outside the bounds
x: isVisibleOnAnyDisplay ? x : undefined,
y: isVisibleOnAnyDisplay ? y : undefined,
@@ -88,22 +149,23 @@ export function createWindow() {
disableBlinkFeatures: 'Auxclick',
},
});
browserWindows.set('Insomnia', mainBrowserWindow);
// BrowserWindow doesn't have an option for this, so we have to do it manually :(
if (maximize) {
newWindow?.maximize();
mainBrowserWindow.maximize();
}
newWindow?.on('resize', () => saveBounds());
newWindow?.on('maximize', () => saveBounds());
newWindow?.on('unmaximize', () => saveBounds());
newWindow?.on('move', () => saveBounds());
newWindow?.on('unresponsive', () => {
mainBrowserWindow.on('resize', () => saveBounds());
mainBrowserWindow.on('maximize', () => saveBounds());
mainBrowserWindow.on('unmaximize', () => saveBounds());
mainBrowserWindow.on('move', () => saveBounds());
mainBrowserWindow.on('unresponsive', () => {
showUnresponsiveModal();
});
// Open generic links (<a .../>) in default browser
newWindow?.webContents.on('will-navigate', (event, url) => {
mainBrowserWindow.webContents.on('will-navigate', (event, url) => {
// Prevents local dev full-reload events from opening browser window, see https://github.com/Kong/insomnia/pull/4925
if (url.startsWith(appUrl)) {
return;
@@ -118,7 +180,7 @@ export function createWindow() {
}
});
newWindow?.webContents.setWindowOpenHandler(() => {
mainBrowserWindow.webContents.setWindowOpenHandler(() => {
return { action: 'deny' };
});
@@ -127,12 +189,11 @@ export function createWindow() {
const appUrl = process.env.APP_RENDER_URL || pathToFileURL(appPath).href;
console.log(`[main] Loading ${appUrl}`);
newWindow?.loadURL(appUrl);
mainBrowserWindow.loadURL(appUrl);
// Emitted when the window is closed.
newWindow?.on('closed', () => {
if (newWindow) {
windows.delete(newWindow);
newWindow = windows.values().next().value || null;
mainBrowserWindow.on('closed', () => {
if (browserWindows.get('Insomnia')) {
browserWindows.delete('Insomnia');
}
});
@@ -259,7 +320,7 @@ export function createWindow() {
{
label: `Resize to ${MNEMONIC_SYM}Small (qHD 540)`,
click: () =>
newWindow?.setBounds({
mainBrowserWindow.setBounds({
width: 960,
height: 540,
}),
@@ -267,7 +328,7 @@ export function createWindow() {
{
label: `Resize to Defaul${MNEMONIC_SYM}t (HD 720)`,
click: () =>
newWindow?.setBounds({
mainBrowserWindow.setBounds({
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
}),
@@ -275,7 +336,7 @@ export function createWindow() {
{
label: `Resize to ${MNEMONIC_SYM}Large (FHD 1080)`,
click: () =>
newWindow?.setBounds({
mainBrowserWindow.setBounds({
width: 1920,
height: 1080,
}),
@@ -367,7 +428,7 @@ export function createWindow() {
{
label: `Show App ${MNEMONIC_SYM}Data Folder`,
click: () => {
const directory = process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData');
const directory = process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData');
shell.showItemInFolder(directory);
},
},
@@ -445,10 +506,10 @@ export function createWindow() {
helpMenu.submenu?.push({
type: 'separator',
},
{
label: `${MNEMONIC_SYM}About`,
click: aboutMenuClickHandler,
});
{
label: `${MNEMONIC_SYM}About`,
click: aboutMenuClickHandler,
});
}
const developerMenu: MenuItemConstructorOptions = {
@@ -470,7 +531,7 @@ export function createWindow() {
label: `Take ${MNEMONIC_SYM}Screenshot`,
click: function() {
// @ts-expect-error -- TSCONVERSION not accounted for in the electron types to provide a function
newWindow?.capturePage(image => {
mainBrowserWindow.capturePage(image => {
const buffer = image.toPNG();
const dir = app.getPath('desktop');
fs.writeFileSync(path.join(dir, `Screenshot-${new Date()}.png`), buffer);
@@ -496,7 +557,7 @@ export function createWindow() {
{
label: `Set window for ${MNEMONIC_SYM}FHD Screenshot`,
click: () => {
newWindow?.setBounds({
mainBrowserWindow.setBounds({
width: 1920,
height: 1080,
});
@@ -546,8 +607,7 @@ export function createWindow() {
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
windows.add(newWindow);
return newWindow;
return mainBrowserWindow;
}
async function showUnresponsiveModal() {
@@ -646,10 +706,10 @@ export const setZoom = (transformer: (current: number) => number) => () => {
};
function initLocalStorage() {
const localStoragePath = path.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'localStorage');
const localStoragePath = path.join(process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'), 'localStorage');
localStorage = new LocalStorage(localStoragePath);
}
export function getOrCreateWindow() {
return newWindow ?? createWindow();
return browserWindows.get('Insomnia') ?? createWindow();
}
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="font-src 'self' data:; connect-src * data: api: insomnia-event-source:; default-src * insomnia://*; img-src blob: data: * insomnia://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src blob: data: mediastream: * insomnia://*;"
/>
<title>Hidden Browser Window</title>
</head>
<body>
<h1>>Hidden Browser Window</h1>
<script src="./index.js" type="module"></script>
</body>
</html>
Whitespace-only changes.
@@ -0,0 +1,8 @@
const { ipcRenderer } = require('electron');
window.onmessage = ev => {
if (ev.data === 'message-event://preload/publish-port') {
ipcRenderer.postMessage('ipc://main/publish-port', null, [ev.ports[0]]);
}
console.log('[preload-hidden-browser-win][init hidden win step 3/6]: ipc "publishing port to the main renderer" is ready');
};