From 33e80d56cd7492006d8b7acd016d1d1efe3957dc Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Mon, 5 Feb 2024 14:37:05 +0100 Subject: [PATCH] Adds a hidden window (#7063) * init new window * fix tests * preload listener * merge processes and windows * trigger CI * fix types * mock test flake --- .../tests/smoke/git-sync.test.ts | 6 +- .../tests/smoke/mock.test.ts | 2 - .../tests/smoke/oauth-gitlab.test.ts | 6 +- packages/insomnia/.gitignore | 2 + packages/insomnia/esbuild.main.ts | 18 ++- packages/insomnia/package.json | 2 +- packages/insomnia/src/main.development.ts | 1 + packages/insomnia/src/main/window-utils.ts | 130 +++++++++++++----- .../hidden-browser-window/index.html | 16 +++ .../renderers/hidden-browser-window/index.ts | 0 .../hidden-browser-window/preload.js | 8 ++ 11 files changed, 146 insertions(+), 45 deletions(-) create mode 100644 packages/insomnia/src/renderers/hidden-browser-window/index.html create mode 100644 packages/insomnia/src/renderers/hidden-browser-window/index.ts create mode 100644 packages/insomnia/src/renderers/hidden-browser-window/preload.js diff --git a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts index 57ae3b4a69..f1ad838f00 100644 --- a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts @@ -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 diff --git a/packages/insomnia-smoke-test/tests/smoke/mock.test.ts b/packages/insomnia-smoke-test/tests/smoke/mock.test.ts index 06e86b345b..91d21ee6bb 100644 --- a/packages/insomnia-smoke-test/tests/smoke/mock.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/mock.test.ts @@ -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(); }); diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts index 588877c493..0ff3876be1 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts @@ -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 diff --git a/packages/insomnia/.gitignore b/packages/insomnia/.gitignore index bc1c552dc8..8207ed6c2a 100644 --- a/packages/insomnia/.gitignore +++ b/packages/insomnia/.gitignore @@ -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 diff --git a/packages/insomnia/esbuild.main.ts b/packages/insomnia/esbuild.main.ts index 8a4595f833..84fddace88 100644 --- a/packages/insomnia/esbuild.main.ts +++ b/packages/insomnia/esbuild.main.ts @@ -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 diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 90f111090b..b77562b930 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -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", diff --git a/packages/insomnia/src/main.development.ts b/packages/insomnia/src/main.development.ts index 91b7debff0..da87fb0be2 100644 --- a/packages/insomnia/src/main.development.ts +++ b/packages/insomnia/src/main.development.ts @@ -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()); } }); diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index ab0aa27c4d..5895fdd321 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -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(); +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 { + // if open, close it + if (browserWindows.get('HiddenBrowserWindow')) { + await new Promise(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 () 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(); } diff --git a/packages/insomnia/src/renderers/hidden-browser-window/index.html b/packages/insomnia/src/renderers/hidden-browser-window/index.html new file mode 100644 index 0000000000..c524f1441c --- /dev/null +++ b/packages/insomnia/src/renderers/hidden-browser-window/index.html @@ -0,0 +1,16 @@ + + + + + + Hidden Browser Window + + + +

>Hidden Browser Window

+ + + diff --git a/packages/insomnia/src/renderers/hidden-browser-window/index.ts b/packages/insomnia/src/renderers/hidden-browser-window/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia/src/renderers/hidden-browser-window/preload.js b/packages/insomnia/src/renderers/hidden-browser-window/preload.js new file mode 100644 index 0000000000..7ccbf181c3 --- /dev/null +++ b/packages/insomnia/src/renderers/hidden-browser-window/preload.js @@ -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'); +};