fix: pick improvements from the downstream PR

This commit is contained in:
George He
2024-01-03 16:40:03 +08:00
parent e3705162dd
commit 62750bb044
7 changed files with 264 additions and 126 deletions

View File

@@ -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);
});

View File

@@ -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();
});
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;
};
}