mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 22:57:59 -04:00
fix: pick improvements from the downstream PR
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { ScriptError } from 'insomnia/src/ui/window-message-handlers';
|
||||
|
||||
import { test } from '../../playwright/test';;
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
async function waitForTrue(timeout: number, func: () => Promise<boolean>) {
|
||||
const pollInterval = 500;
|
||||
@@ -16,7 +17,76 @@ async function waitForTrue(timeout: number, func: () => Promise<boolean>) {
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('test pre-request script execution', async () => {
|
||||
async function runTests(testCases: {
|
||||
id: string;
|
||||
code: string;
|
||||
context: object;
|
||||
expectedResult: any;
|
||||
}[]) {
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const tc = testCases[i];
|
||||
|
||||
// tests begin here
|
||||
test(tc.id, async ({ app, page: mainWindow }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
// start the sandbox
|
||||
await mainWindow?.evaluate(
|
||||
async () => {
|
||||
// it suppresses the type checking error
|
||||
const caller = window as unknown as { hiddenBrowserWindow: { start: () => void } };
|
||||
if (caller.hiddenBrowserWindow) {
|
||||
caller.hiddenBrowserWindow.start();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// execute the command
|
||||
await mainWindow?.evaluate(
|
||||
async (tc: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
action: 'message-event://hidden.browser-window/debug',
|
||||
id: tc.id,
|
||||
code: tc.code,
|
||||
context: tc.context,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
},
|
||||
tc,
|
||||
);
|
||||
|
||||
// verify
|
||||
let localStorage;
|
||||
|
||||
await waitForTrue(60000, async () => {
|
||||
localStorage = await mainWindow?.evaluate(() => window.localStorage);
|
||||
expect(localStorage).toBeDefined();
|
||||
|
||||
return localStorage[`test_result:${tc.id}`] || localStorage[`test_error:${tc.id}`];
|
||||
});
|
||||
|
||||
expect(localStorage).toBeDefined(); // or no output is found
|
||||
|
||||
if (localStorage) { // just for suppressing ts complaint
|
||||
const result = localStorage[`test_result:${tc.id}`];
|
||||
const error = localStorage[`test_error:${tc.id}`];
|
||||
|
||||
if (result) {
|
||||
expect(JSON.parse(result)).toEqual(tc.expectedResult);
|
||||
} else {
|
||||
const scriptError = JSON.parse(error) as ScriptError;
|
||||
expect(scriptError.message).toEqual(tc.expectedResult.message);
|
||||
// TODO: stack field is not checked as its content is complex
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('basic operations', async () => {
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
@@ -277,6 +347,13 @@ test.describe('test pre-request script execution', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await runTests(testCases);
|
||||
});
|
||||
|
||||
test.describe('unhappy paths', async () => {
|
||||
const testCases = [
|
||||
{
|
||||
id: 'execution timeout (default timeout 3s)',
|
||||
code: `
|
||||
@@ -292,90 +369,16 @@ test.describe('test pre-request script execution', async () => {
|
||||
{
|
||||
id: 'invalid result is returned',
|
||||
code: `
|
||||
resolve();
|
||||
return;
|
||||
`,
|
||||
context: {
|
||||
insomnia: {},
|
||||
},
|
||||
expectedResult: {
|
||||
message: 'result is invalid, probably custom value is returned',
|
||||
message: 'result is invalid, null or custom value may be returned',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const tc = testCases[i];
|
||||
|
||||
// tests begin here
|
||||
test(tc.id, async ({ app, page: mainWindow }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
const originalWindowCount = app.windows().length;
|
||||
|
||||
// start the sandbox
|
||||
await mainWindow?.evaluate(
|
||||
async () => {
|
||||
// it suppresses the type checking error
|
||||
const caller = window as unknown as { hiddenBrowserWindow: { start: () => void } };
|
||||
if (caller.hiddenBrowserWindow) {
|
||||
caller.hiddenBrowserWindow.start();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// waiting for the hidden browser ready
|
||||
await waitForTrue(60000, async () => {
|
||||
const windows = app.windows();
|
||||
|
||||
if (windows.length > originalWindowCount) {
|
||||
for (const page of windows) {
|
||||
if (await page.title() === 'Hidden Browser Window') {
|
||||
await page.waitForLoadState();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// execute the command
|
||||
await mainWindow?.evaluate(
|
||||
async (tc: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
action: 'message-event://hidden.browser-window/debug',
|
||||
id: tc.id,
|
||||
code: tc.code,
|
||||
context: tc.context,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
},
|
||||
tc,
|
||||
);
|
||||
|
||||
// verify
|
||||
let localStorage;
|
||||
|
||||
await waitForTrue(60000, async () => {
|
||||
localStorage = await mainWindow?.evaluate(() => window.localStorage);
|
||||
expect(localStorage).toBeDefined();
|
||||
|
||||
return localStorage[`test_result:${tc.id}`] || localStorage[`test_error:${tc.id}`];
|
||||
});
|
||||
|
||||
if (localStorage) { // just for suppressing ts complaint
|
||||
const result = localStorage[`test_result:${tc.id}`];
|
||||
const error = localStorage[`test_error:${tc.id}`];
|
||||
|
||||
if (result) {
|
||||
expect(JSON.parse(result)).toEqual(tc.expectedResult);
|
||||
} else {
|
||||
expect(JSON.parse(error)).toEqual(tc.expectedResult);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
await runTests(testCases);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { createHiddenBrowserWindow } from '../window-utils';
|
||||
import { createHiddenBrowserWindow, stopHiddenBrowserWindow } from '../window-utils';
|
||||
|
||||
export interface HiddenBrowserWindowAPI {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
// registerHiddenBrowserWindowConsumer broadcasts message ports to observer windows
|
||||
@@ -16,7 +17,10 @@ export function registerHiddenBrowserWindowConsumer(consumerWindows: BrowserWind
|
||||
}
|
||||
|
||||
export function registerHiddenBrowserWindowController() {
|
||||
ipcMain.handle('ipc://main/hidden-browser-window/start', () => {
|
||||
createHiddenBrowserWindow();
|
||||
ipcMain.handle('ipc://main/hidden-browser-window/start', async () => {
|
||||
await createHiddenBrowserWindow();
|
||||
});
|
||||
ipcMain.handle('ipc://main/hidden-browser-window/stop', () => {
|
||||
stopHiddenBrowserWindow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../common/constants';
|
||||
import { docsBase } from '../common/documentation';
|
||||
import * as log from '../common/log';
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { registerHiddenBrowserWindowConsumer, registerHiddenBrowserWindowController } from './ipc/hidden-browser-window';
|
||||
import LocalStorage from './local-storage';
|
||||
|
||||
@@ -29,7 +30,7 @@ const MINIMUM_HEIGHT = 400;
|
||||
let newWindow: ElectronBrowserWindow | null = null;
|
||||
let hiddenBrowserWindow: ElectronBrowserWindow | null = null;
|
||||
const windows = new Set<ElectronBrowserWindow>();
|
||||
const processes = new Set<ElectronBrowserWindow>();
|
||||
const processes = new Map<number, ElectronBrowserWindow>();
|
||||
let localStorage: LocalStorage | null = null;
|
||||
|
||||
interface Bounds {
|
||||
@@ -43,7 +44,25 @@ export function init() {
|
||||
initLocalStorage();
|
||||
}
|
||||
|
||||
export function createHiddenBrowserWindow() {
|
||||
export async function createHiddenBrowserWindow() {
|
||||
if (hiddenBrowserWindow) {
|
||||
await new Promise<void>(resolve => {
|
||||
invariant(hiddenBrowserWindow, 'hiddenBrowserWindow is running');
|
||||
|
||||
// overwrite the closed handler
|
||||
hiddenBrowserWindow.on('closed', () => {
|
||||
if (hiddenBrowserWindow) {
|
||||
console.log('[hidden window] restarting hidden browser window:', hiddenBrowserWindow.id);
|
||||
processes.delete(hiddenBrowserWindow.id);
|
||||
hiddenBrowserWindow = null;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
stopHiddenBrowserWindow();
|
||||
});
|
||||
}
|
||||
|
||||
hiddenBrowserWindow = new BrowserWindow({
|
||||
show: false,
|
||||
title: 'HiddenBrowserWindow',
|
||||
@@ -60,20 +79,30 @@ export function createHiddenBrowserWindow() {
|
||||
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] loading hidden browser window:', process.env.HIDDEN_BROWSER_WINDOW_URL, pathToFileURL(hiddenBrowserWindowPath).href);
|
||||
|
||||
console.log(
|
||||
`[hidden window] starting hidden browser window: ${hiddenBrowserWindow.id}`,
|
||||
process.env.HIDDEN_BROWSER_WINDOW_URL,
|
||||
pathToFileURL(hiddenBrowserWindowPath).href,
|
||||
);
|
||||
|
||||
hiddenBrowserWindow?.on('closed', () => {
|
||||
if (hiddenBrowserWindow) {
|
||||
processes.delete(hiddenBrowserWindow);
|
||||
hiddenBrowserWindow = processes.values().next().value || null;
|
||||
console.log('[hidden window] closing hidden browser window: ', hiddenBrowserWindow.id);
|
||||
processes.delete(hiddenBrowserWindow.id);
|
||||
hiddenBrowserWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
processes.add(hiddenBrowserWindow);
|
||||
processes.set(hiddenBrowserWindow.id, hiddenBrowserWindow);
|
||||
|
||||
return hiddenBrowserWindow;
|
||||
}
|
||||
|
||||
export function stopHiddenBrowserWindow() {
|
||||
hiddenBrowserWindow?.close();
|
||||
}
|
||||
|
||||
export function createWindow() {
|
||||
const { bounds, fullscreen, maximize } = getBounds();
|
||||
const { x, y, width, height } = bounds;
|
||||
|
||||
@@ -89,6 +89,7 @@ const clipboard: Window['clipboard'] = {
|
||||
|
||||
const hiddenBrowserWindow: Window['hiddenBrowserWindow'] = {
|
||||
start: () => ipcRenderer.invoke('ipc://main/hidden-browser-window/start'),
|
||||
stop: () => ipcRenderer.invoke('ipc://main/hidden-browser-window/stop'),
|
||||
};
|
||||
|
||||
if (process.contextIsolated) {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { initGlobalObject } from './inso-object';
|
||||
import { initGlobalObject, InsomniaObject } from './inso-object';
|
||||
|
||||
const ErrorTimeout = 'executing script timeout';
|
||||
const ErrorInvalidResult = 'result is invalid, probably custom value is returned';
|
||||
const ErrorInvalidResult = 'result is invalid, null or custom value may be returned';
|
||||
|
||||
const executeAction = 'message-channel://hidden.browser-window/execute';
|
||||
|
||||
async function init() {
|
||||
const channel = new MessageChannel();
|
||||
|
||||
channel.port1.onmessage = async (ev: MessageEvent) => {
|
||||
const action = ev.data.action;
|
||||
const timeout = ev.data.timeout ? ev.data.timeout : 3000;
|
||||
|
||||
if (action === executeAction || action === 'message-channel://hidden.browser-window/debug') {
|
||||
try {
|
||||
try {
|
||||
if (action === executeAction || action === 'message-channel://hidden.browser-window/debug') {
|
||||
const getRawGlobalObject = new Function('insomnia', 'return insomnia;');
|
||||
const rawObject = getRawGlobalObject(ev.data.options.context.insomnia);
|
||||
const insomniaObject = initGlobalObject(rawObject);
|
||||
@@ -22,45 +23,59 @@ async function init() {
|
||||
'insomnia',
|
||||
// if possible, avoid adding code to the following part
|
||||
`
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const $ = insomnia;
|
||||
const pm = insomnia;
|
||||
const alertTimeout = () => reject({ message: '${ErrorTimeout}:${timeout}ms' });
|
||||
const timeoutChecker = setTimeout(alertTimeout, ${timeout});
|
||||
const $ = insomnia, pm = insomnia;
|
||||
${ev.data.options.code};
|
||||
|
||||
${ev.data.options.code};
|
||||
|
||||
clearTimeout(timeoutChecker);
|
||||
resolve(insomnia.toObject());
|
||||
});
|
||||
return insomnia;
|
||||
`
|
||||
);
|
||||
|
||||
const result = await executeScript(insomniaObject);
|
||||
if (!result) {
|
||||
throw { message: ErrorInvalidResult };
|
||||
}
|
||||
const result = await new Promise(async (resolve, reject) => {
|
||||
const alertTimeout = () => reject({ message: `${ErrorTimeout}:${timeout}ms` });
|
||||
const timeoutChecker = setTimeout(alertTimeout, timeout);
|
||||
|
||||
try {
|
||||
const insoObject = await executeScript(insomniaObject);
|
||||
clearTimeout(timeoutChecker);
|
||||
if (insoObject instanceof InsomniaObject) {
|
||||
resolve(insoObject.toObject());
|
||||
} else {
|
||||
throw { message: ErrorInvalidResult };
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
channel.port1.postMessage({
|
||||
action: action === executeAction ? 'message-channel://caller/respond' : 'message-channel://caller/debug/respond',
|
||||
id: action === executeAction ? undefined : ev.data.options.id,
|
||||
id: ev.data.options.id,
|
||||
result,
|
||||
});
|
||||
} catch (e) {
|
||||
const message = e.message;
|
||||
|
||||
channel.port1.postMessage({
|
||||
action: action === executeAction ? 'message-channel://caller/respond' : 'message-channel://caller/debug/respond',
|
||||
id: action === executeAction ? undefined : ev.data.options.id,
|
||||
error: { message: message || 'unknown error' },
|
||||
});
|
||||
} else {
|
||||
console.error(`unknown action ${ev.data}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`unknown action ${ev.data}`);
|
||||
} catch (e) {
|
||||
channel.port1.postMessage({
|
||||
action: action === executeAction ? 'message-channel://caller/respond' : 'message-channel://caller/debug/respond',
|
||||
id: ev.data.options.id,
|
||||
error: {
|
||||
message: e.message || 'unknown error',
|
||||
stack: e.stack,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
window.postMessage('message-event://preload/publish-port', '*', [channel.port2]);
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
channel.port1.postMessage({
|
||||
action: 'message-channel://consumers/close',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@@ -154,7 +154,7 @@ class Variables {
|
||||
};
|
||||
}
|
||||
|
||||
class InsomniaObject {
|
||||
export class InsomniaObject {
|
||||
public globals: Environment;
|
||||
public collectionVariables: Environment;
|
||||
public environment: Environment;
|
||||
@@ -190,7 +190,7 @@ class InsomniaObject {
|
||||
};
|
||||
}
|
||||
|
||||
interface RawObject {
|
||||
export interface RawObject {
|
||||
globals?: object;
|
||||
environment?: object;
|
||||
collectionVariables?: object;
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { RawObject } from '../renderers/hidden-browser-window/inso-object';
|
||||
|
||||
type MessageHandler = (ev: MessageEvent) => Promise<void>;
|
||||
|
||||
export interface ScriptError {
|
||||
message: string;
|
||||
stack: string;
|
||||
}
|
||||
|
||||
interface ScriptResultResolver {
|
||||
id: string;
|
||||
resolve: (value: RawObject) => void;
|
||||
reject: (error: ScriptError) => void;
|
||||
}
|
||||
|
||||
// WindowMessageHandler handles entities in followings domains:
|
||||
// - handle window message events
|
||||
// - handle message port events
|
||||
// - trigger message callbacks
|
||||
class WindowMessageHandler {
|
||||
private hiddenBrowserWindowPort: MessagePort | undefined;
|
||||
private actionHandlers: Map<string, MessageHandler> = new Map();
|
||||
private scriptResultResolvers: ScriptResultResolver[] = [];
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -17,8 +34,28 @@ class WindowMessageHandler {
|
||||
|
||||
this.hiddenBrowserWindowPort.onmessage = ev => {
|
||||
if (ev.data.action === 'message-channel://caller/respond') {
|
||||
// TODO: hook to UI and display result
|
||||
console.log('[main] result from hidden browser window:', ev.data.result);
|
||||
if (!ev.data.id) {
|
||||
console.error('id is not specified in the executing script response message');
|
||||
return;
|
||||
}
|
||||
|
||||
const callbackIndex = this.scriptResultResolvers.
|
||||
findIndex(callback => callback.id === ev.data.id);
|
||||
if (callbackIndex < 0) {
|
||||
console.error(`id(${ev.data.id}) is not found in the callback list`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.data.result) {
|
||||
this.scriptResultResolvers[callbackIndex].resolve(ev.data.result);
|
||||
} else if (ev.data.error) {
|
||||
this.scriptResultResolvers[callbackIndex].reject(ev.data.error);
|
||||
} else {
|
||||
console.error('no data found in the message port response');
|
||||
}
|
||||
|
||||
// skip previous ones for keeping it simple
|
||||
this.scriptResultResolvers = this.scriptResultResolvers.slice(callbackIndex + 1);
|
||||
} else if (ev.data.action === 'message-channel://caller/debug/respond') {
|
||||
if (ev.data.result) {
|
||||
window.localStorage.setItem(`test_result:${ev.data.id}`, JSON.stringify(ev.data.result));
|
||||
@@ -27,20 +64,41 @@ class WindowMessageHandler {
|
||||
window.localStorage.setItem(`test_error:${ev.data.id}`, JSON.stringify(ev.data.error));
|
||||
console.error(ev.data.error);
|
||||
}
|
||||
} else if (ev.data.action === 'message-channel://consumers/close') {
|
||||
this.hiddenBrowserWindowPort?.close();
|
||||
this.hiddenBrowserWindowPort = undefined;
|
||||
console.log('[hidden win] hidden browser window port is closed');
|
||||
} else {
|
||||
console.error(`unknown action ${ev}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
waitUntilHiddenBrowserWindowReady = async () => {
|
||||
window.hiddenBrowserWindow.start();
|
||||
|
||||
// TODO: find a better way to wait for hidden browser window ready
|
||||
// the hiddenBrowserWindow may be still in starting
|
||||
// this is relatively simpler than receiving a 'ready' message from hidden browser window
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (this.hiddenBrowserWindowPort) {
|
||||
break;
|
||||
} else {
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
console.error('the hidden window is still not ready');
|
||||
};
|
||||
|
||||
debugEventHandler = async (ev: MessageEvent) => {
|
||||
if (!this.hiddenBrowserWindowPort) {
|
||||
console.error('hidden browser window port is not inited');
|
||||
return;
|
||||
console.error('hidden browser window port is not inited, restarting');
|
||||
await this.waitUntilHiddenBrowserWindowReady();
|
||||
}
|
||||
|
||||
console.info('sending script to hidden browser window');
|
||||
this.hiddenBrowserWindowPort.postMessage({
|
||||
this.hiddenBrowserWindowPort?.postMessage({
|
||||
action: 'message-channel://hidden.browser-window/debug',
|
||||
options: {
|
||||
id: ev.data.id,
|
||||
@@ -55,6 +113,8 @@ class WindowMessageHandler {
|
||||
};
|
||||
|
||||
start = () => {
|
||||
window.hiddenBrowserWindow.start();
|
||||
|
||||
this.register('message-event://renderers/publish-port', this.publishPortHandler);
|
||||
this.register('message-event://hidden.browser-window/debug', this.debugEventHandler);
|
||||
|
||||
@@ -79,8 +139,34 @@ class WindowMessageHandler {
|
||||
};
|
||||
};
|
||||
|
||||
stop = () => {
|
||||
this.actionHandlers.clear();
|
||||
runPreRequestScript = async (
|
||||
id: string,
|
||||
code: string,
|
||||
context: object,
|
||||
): Promise<RawObject | undefined> => {
|
||||
if (!this.hiddenBrowserWindowPort) {
|
||||
console.error('hidden browser window port is not inited, restarting');
|
||||
await this.waitUntilHiddenBrowserWindowReady();
|
||||
}
|
||||
|
||||
const promise = new Promise<RawObject>((resolve, reject) => {
|
||||
this.scriptResultResolvers.push({
|
||||
id,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
|
||||
this.hiddenBrowserWindowPort?.postMessage({
|
||||
action: 'message-channel://hidden.browser-window/execute',
|
||||
options: {
|
||||
id,
|
||||
code,
|
||||
context,
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user