mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-26 00:29:35 -04:00
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:
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
Reference in new issue
Block a user