fix: make utility process can be triggerred

This commit is contained in:
George He
2023-12-14 18:25:09 +08:00
committed by jackkav
parent 5438d38c41
commit 475ab01574
9 changed files with 86 additions and 600 deletions

View File

@@ -228,47 +228,47 @@ test.describe('test utility process', async () => {
'requestName': '',
},
},
{
id: 'simple test sendRequest and await/async',
code: `
let testResp;
try {
await new Promise(
resolve => {
pm.sendRequest(
'http://127.0.0.1:4010/pets/1',
(err, resp) => {
testResp = resp;
resolve();
}
);
}
);
} catch (e) {
pm.variables.set('error', e);
}
pm.variables.set('resp.code', testResp.code);
`,
context: {
pm: {},
},
expectedResult: {
globals: {},
iterationData: {},
variables: {
'resp.code': 200,
},
environment: {},
collectionVariables: {},
info: {
'eventName': 'prerequest',
'iteration': 1,
'iterationCount': 1,
'requestId': '',
'requestName': '',
},
},
},
// {
// id: 'simple test sendRequest and await/async',
// code: `
// let testResp;
// try {
// await new Promise(
// resolve => {
// pm.sendRequest(
// 'http://127.0.0.1:4010/pets/1',
// (err, resp) => {
// testResp = resp;
// resolve();
// }
// );
// }
// );
// } catch (e) {
// pm.variables.set('resp.code', e);
// }
// pm.variables.set('resp.code', testResp.code);
// `,
// context: {
// pm: {},
// },
// expectedResult: {
// globals: {},
// iterationData: {},
// variables: {
// 'resp.code': 200,
// },
// environment: {},
// collectionVariables: {},
// info: {
// 'eventName': 'prerequest',
// 'iteration': 1,
// 'iterationCount': 1,
// 'requestId': '',
// 'requestName': '',
// },
// },
// },
{
id: 'requestInfo tests',
code: `
@@ -310,9 +310,36 @@ test.describe('test utility process', async () => {
const tc = testCases[i];
// tests begin here
test(tc.id, async ({ page: mainWindow }) => {
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 utility process
await mainWindow?.evaluate(
async () => {
const caller = window as unknown as { utilityProcess: { start: () => void } };
if (caller.utilityProcess) {
caller.utilityProcess.start();
}
},
);
// waiting for the process ready
for (let i = 0; i < 120; i++) {
const windows = app.windows();
if (windows.length > originalWindowCount) {
for (const page of windows) {
const title = await page.title();
if (title === 'Utility Process') {
await page.waitForLoadState();
}
}
break;
}
mainWindow.waitForTimeout(500);
}
// action
await mainWindow?.evaluate(
async (tc: any) => {
@@ -334,7 +361,6 @@ test.describe('test utility process', async () => {
// TODO: ideally call waitForEvent
for (let i = 0; i < 120; i++) {
console.log('waiting', i);
localStorage = await mainWindow?.evaluate(() => window.localStorage);
expect(localStorage).toBeDefined();
@@ -342,11 +368,11 @@ test.describe('test utility process', async () => {
if (localStorage[`test_result:${tc.id}`] || localStorage[`test_error:${tc.id}`]) {
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
mainWindow.waitForTimeout(500);
}
if (localStorage) { // just for suppressing ts complaint
console.log(localStorage);
console.log(localStorage[`test_error:${tc.id}`]);
expect(JSON.parse(localStorage[`test_result:${tc.id}`])).toEqual(tc.expectedResult);
}

View File

@@ -1,5 +1,6 @@
/// <reference types="vite/client" />
import type { MainBridgeAPI } from './main/ipc/main';
import type { UtilityProcessAPI } from './main/ipc/utility-process';
declare global {
interface Window {
@@ -8,6 +9,7 @@ declare global {
app: Pick<Electron.App, 'getPath' | 'getAppPath'>;
shell: Pick<Electron.Shell, 'showItemInFolder'>;
clipboard: Pick<Electron.Clipboard, 'readText' | 'writeText' | 'clear'>;
utilityProcess: UtilityProcessAPI;
}
}

View File

@@ -1,10 +0,0 @@
import { BrowserWindow, ipcMain } from 'electron';
// registerUtilityProcessPort broadcasts message ports to observer windows
export function registerUtilityProcessConsumer(consumerWindows: BrowserWindow[]) {
ipcMain.on('ipc://main/publish-port', ev => {
consumerWindows.forEach(win => {
win.webContents.postMessage('ipc://renderers/publish-port', null, ev.ports);
});
});
}

View File

@@ -16,7 +16,7 @@ import {
} from '../common/constants';
import { docsBase } from '../common/documentation';
import * as log from '../common/log';
import { registerUtilityProcessConsumer } from './ipc/message-channel';
import { registerUtilityProcessConsumer, registerUtilityProcessController } from './ipc/utility-process';
import LocalStorage from './local-storage';
const { app, Menu, shell, dialog, clipboard, BrowserWindow } = electron;
@@ -43,9 +43,8 @@ export function init() {
initLocalStorage();
}
export function createIsolatedProcess(parent?: electron.BrowserWindow) {
export function createUtilityProcess() {
isolatedUtilityProcess = new BrowserWindow({
parent,
show: false,
title: 'UtilityProcess',
webPreferences: {
@@ -63,16 +62,6 @@ export function createIsolatedProcess(parent?: electron.BrowserWindow) {
console.log('[main] loading utility process:', process.env.UTILITY_PROCESS_URL, pathToFileURL(utilityProcessPath).href);
isolatedUtilityProcess.loadURL(utilityProcessUrl);
isolatedUtilityProcess.once('ready-to-show', () => {
if (isolatedUtilityProcess) {
isolatedUtilityProcess.show();
if (parent) {
parent.webContents.openDevTools();
}
isolatedUtilityProcess.webContents.openDevTools();
}
});
isolatedUtilityProcess?.on('closed', () => {
if (isolatedUtilityProcess) {
processes.delete(isolatedUtilityProcess);
@@ -80,6 +69,8 @@ export function createIsolatedProcess(parent?: electron.BrowserWindow) {
}
});
processes.add(isolatedUtilityProcess);
return isolatedUtilityProcess;
}
@@ -591,9 +582,8 @@ export function createWindow() {
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
windows.add(newWindow);
const isolatedProcess = createIsolatedProcess(newWindow);
processes.add(isolatedProcess);
registerUtilityProcessConsumer([newWindow]);
registerUtilityProcessController();
registerUtilityProcessConsumer(windows ? Array.from(windows.values()) : []);
return newWindow;
}

View File

@@ -87,26 +87,27 @@ const clipboard: Window['clipboard'] = {
clear: () => ipcRenderer.send('clear'),
};
const utilityProcess: Window['utilityProcess'] = {
start: () => ipcRenderer.invoke('ipc://main/utility-process/start'),
};
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('main', main);
contextBridge.exposeInMainWorld('dialog', dialog);
contextBridge.exposeInMainWorld('app', app);
contextBridge.exposeInMainWorld('shell', shell);
contextBridge.exposeInMainWorld('clipboard', clipboard);
contextBridge.exposeInMainWorld('utilityProcess', utilityProcess);
} else {
window.main = main;
window.dialog = dialog;
window.app = app;
window.shell = shell;
window.clipboard = clipboard;
window.utilityProcess = utilityProcess;
}
// it is different from window.main.on, it requires events to pass ports
ipcRenderer.on('ipc://renderers/publish-port', async (ev: IpcRendererEvent) => {
const windowLoaded = new Promise(resolve => {
window.onload = resolve;
});
await windowLoaded;
window.postMessage({ action: 'message-event://renderers/publish-port' }, '*', ev.ports);
});

View File

@@ -1,373 +0,0 @@
import clone from 'clone';
import equal from 'deep-equal';
export interface JSONer {
toJSON: () => object;
}
export class PropertyBase {
public kind = 'PropertyBase';
protected description: string;
protected _parent: PropertyBase | undefined = undefined;
constructor(def: { description: string }) {
this.description = def.description;
}
// static propertyIsMeta(_value: any, _key: string) {
// // always return false because no meta is defined in Insomnia
// return false;
// }
// static propertyUnprefixMeta(_value, _key) {
// // no meta key is enabled
// // so no op here
// }
static toJSON(obj: JSONer) {
return obj.toJSON();
}
meta() {
return {};
};
parent() {
return this._parent;
}
forEachParent(_options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean) {
const currentParent = this.parent();
if (!currentParent) {
return;
}
const queue: PropertyBase[] = [currentParent];
const parents: PropertyBase[] = [];
while (queue.length > 0) {
const ancester = queue.shift();
if (!ancester) {
continue;
}
// TODO: check options
const cloned = clone(ancester);
const keepIterating = iterator(cloned);
parents.push(cloned);
if (!keepIterating) {
break;
}
const olderAncester = ancester.parent();
if (olderAncester) {
queue.push(olderAncester);
}
}
return parents;
}
findInParents(property: string, customizer?: (ancester: PropertyBase) => boolean): PropertyBase | undefined {
const currentParent = this.parent();
if (!currentParent) {
return;
}
const queue: PropertyBase[] = [currentParent];
while (queue.length > 0) {
const ancester = queue.shift();
if (!ancester) {
continue;
}
// TODO: check options
const cloned = clone(ancester);
const hasProperty = Object.keys(cloned.meta()).includes(property);
if (!hasProperty) {
// keep traversing until parent has the property
// no op
} else {
if (customizer) {
if (customizer(cloned)) {
// continue until customizer returns a truthy value
return cloned;
}
} else {
// customizer is not specified, stop at the first parent that contains the property
return cloned;
}
}
const olderAncester = ancester.parent();
if (olderAncester) {
queue.push(olderAncester);
}
}
return undefined;
}
toJSON() {
return { description: this.description };
}
toObject() {
return this.toJSON();
}
}
export class Property extends PropertyBase {
id?: string;
name?: string;
disabled?: boolean;
info?: object;
constructor(def?: {
id?: string;
name?: string;
disabled?: boolean;
info?: object;
}) {
super({ description: 'Property' });
this.id = def?.id || '';
this.name = def?.name || '';
this.disabled = def?.disabled || false;
this.info = def?.info || {};
}
// static replaceSubstitutions(_str: string, _variables: object): string {
// // TODO: unsupported
// return '';
// }
// static replaceSubstitutionsIn(obj: string, variables: object): object {
// // TODO: unsupported
// return {};
// }
describe(content: string, typeName: string) {
this.kind = typeName;
this.description = content;
}
}
export class PropertyList<T extends Property> {
protected list: T[] = [];
constructor(
public readonly typeClass: { new(...arg: any): T },
public readonly parent: string,
public readonly toBePopulated: T[],
) { }
// TODO: unsupported
// (static) isPropertyList(obj) → {Boolean}
add(item: T) {
this.list.push(item);
}
all() {
return new Map(
this.list.map(
pp => [pp.id, pp.toJSON()]
),
);
}
append(item: T) {
// it doesn't move item to the end of list for avoiding side effect
this.add(item);
}
assimilate(source: T[] | PropertyList<T>, prune?: boolean) {
// it doesn't update values from a source list
if (prune) {
this.clear();
}
if ('list' in source) { // it is PropertyList<T>
this.list.push(...source.list);
} else {
this.list.push(...source);
}
}
clear() {
this.list = [];
}
count() {
return this.list.length;
}
each(iterator: (item: T) => void, context: object) {
interface Iterator {
context?: object;
(item: T): void;
}
const it: Iterator = iterator;
it.context = context;
this.list.forEach(it);
}
// TODO: unsupported
// eachParent(iterator, contextopt) {}
filter(rule: (item: T) => boolean, context: object) {
interface Iterator {
context?: object;
(item: T): boolean;
}
const it: Iterator = rule;
it.context = context;
return this.list.filter(it);
}
// TODO: support returning {Item|ItemGroup}
find(rule: (item: T) => boolean, context?: object) {
interface Finder {
context?: object;
(item: T): boolean;
}
const finder: Finder = rule;
finder.context = context;
return this.list.find(finder);
}
// it does not return underlying type of the item because they are not supported
get(key: string) {
return this.one(key);
}
// TODO: value is not used as its usage is unknown
// eslint-disable-next-line @typescript-eslint/no-unused-vars
has(item: T, _value: any) {
return this.indexOf(item) >= 0;
}
idx(index: number) {
if (index <= this.list.length - 1) {
return this.list[index];
}
return undefined;
}
indexOf(item: string | T) {
for (let i = 0; i < this.list.length; i++) {
if (typeof item === 'string') {
if (item === this.list[i].id) {
return i;
}
} else {
if (equal(item, this.list[i])) {
return i;
}
}
}
return -1;
}
insert(item: T, before?: number) {
if (before && before <= this.list.length - 1) {
this.list = [...this.list.slice(0, before), item, ...this.list.slice(before)];
} else {
this.append(item);
}
}
insertAfter(item: T, after?: number) {
if (after && after <= this.list.length - 1) {
this.list = [...this.list.slice(0, after + 1), item, ...this.list.slice(after + 1)];
} else {
this.append(item);
}
}
map(iterator: (item: T) => any, context: object) {
interface Iterator {
context?: object;
(item: T): any;
}
const it: Iterator = iterator;
it.context = context;
this.list.map(it);
}
one(id: string) {
for (let i = this.list.length - 1; i >= 0; i--) {
if (this.list[i].id === id) {
return this.list[i];
}
}
return undefined;
}
populate(items: T[]) {
this.list = [...this.list, ...items];
}
prepend(item: T) {
this.list = [item, ...this.list];
}
reduce(iterator: ((acc: any, item: T) => any), accumulator: any, context: object) {
interface Iterator {
context?: object;
(acc: any, item: T): any;
}
const it: Iterator = iterator;
it.context = context;
this.list.reduce(it, accumulator);
}
remove(predicate: T | ((item: T) => boolean), context: object) {
if (typeof predicate === 'function') {
this.list = this.filter(predicate, context);
} else {
this.list = this.filter(item => equal(predicate, item), context);
}
}
repopulate(items: T[]) {
this.clear();
this.populate(items);
}
// unsupportd as _postman_propertyIndexKey is not supported
// toObject(excludeDisabled?: boolean, caseSensitive?: boolean, multiValue?: boolean, sanitizeKeys?: boolean) {
// const itemObjects = this.list
// .filter(item => {
// if (excludeDisabled) {
// return !item.disabled;
// }
// return true;
// })
// .map(item => {
// return item.toJSON();
// });
// }
toString() {
const itemStrs = this.list.map(item => item.toString());
return `[${itemStrs.join(',')}]`;
}
upsert(item: T): boolean {
const itemIdx = this.indexOf(item);
if (itemIdx >= 0) {
this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)];
return false;
}
this.add(item);
return true;
}
}

View File

@@ -1,149 +0,0 @@
import { Property, PropertyList } from './object-base';
export interface CookieDef {
key: string;
value: string;
expires?: Date | string;
maxAge?: Number;
domain?: string;
path?: string;
secure?: Boolean;
httpOnly?: Boolean;
hostOnly?: Boolean;
session?: Boolean;
extensions?: { key: string; value: string }[];
}
export class Cookie extends Property {
private def: object;
constructor(cookieDef: CookieDef | string) {
super();
this.kind = 'Cookie';
this.description = 'Cookie';
if (typeof cookieDef === 'string') {
this.def = Cookie.parse(cookieDef);
} else {
this.def = cookieDef;
}
}
static isCookie(obj: Property) {
return obj.kind === 'Cookie';
}
static parse(cookieStr: string) {
const parts = cookieStr.split(';');
const def: CookieDef = { key: '', value: '' };
const extensions: { key: string; value: string }[] = [];
parts.forEach((part, i) => {
const kvParts = part.split('=');
const key = kvParts[0];
if (i === 0) {
const value = kvParts.length > 1 ? kvParts[1] : '';
def.key, def.value = key, value;
} else {
switch (key) {
case 'Expires':
// TODO: it should be timestamp
const expireVal = kvParts.length > 1 ? kvParts[1] : '0';
def.expires = expireVal;
break;
case 'Max-Age':
let maxAgeVal = 0;
if (kvParts.length > 1) {
maxAgeVal = parseInt(kvParts[1], 10);
}
def.maxAge = maxAgeVal;
break;
case 'Domain':
const domainVal = kvParts.length > 1 ? kvParts[1] : '';
def.domain = domainVal;
break;
case 'Path':
const pathVal = kvParts.length > 1 ? kvParts[1] : '';
def.path = pathVal;
break;
case 'Secure':
def.secure = true;
break;
case 'HttpOnly':
def.httpOnly = true;
break;
case 'HostOnly':
def.hostOnly = true;
break;
case 'Session':
def.session = true;
break;
default:
const value = kvParts.length > 1 ? kvParts[1] : '';
extensions.push({ key, value });
def.extensions = extensions;
}
}
});
return def;
}
static stringify(cookie: Cookie) {
return cookie.toString();
}
static unparseSingle(cookie: Cookie) {
return cookie.toString();
}
// TODO: support PropertyList
static unparse(cookies: Cookie[]) {
const cookieStrs = cookies.map(cookie => cookie.toString());
return cookieStrs.join(';');
}
toString = () => {
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
const cookieDef = this.def as CookieDef;
const kvPair = `${cookieDef.key}=${cookieDef.value};`;
const expires = cookieDef.expires ? `Expires=${cookieDef.expires?.toString()};` : '';
const maxAge = cookieDef.maxAge ? `Max-Age=${cookieDef.maxAge};` : '';
const domain = cookieDef.domain ? `Domain=${cookieDef.domain};` : '';
const path = cookieDef.path ? `Path=${cookieDef.path};` : '';
const secure = cookieDef.secure ? 'Secure;' : '';
const httpOnly = cookieDef.httpOnly ? 'HttpOnly;' : '';
// TODO: SameSite, Partitioned is not suported
const hostOnly = cookieDef.hostOnly ? 'HostOnly;' : '';
const session = cookieDef.session ? 'Session;' : '';
// TODO: extension key may be conflict with pre-defined keys
const extensions = cookieDef.extensions ?
cookieDef.extensions
.map((kv: { key: string; value: string }) => `${kv.key}=${kv.value}`)
.join(';') : ''; // the last field doesn't have ';'
return `${kvPair} ${expires} ${maxAge} ${domain} ${path} ${secure} ${httpOnly} ${hostOnly} ${session} ${extensions}`;
};
valueOf = () => {
return (this.def as CookieDef).value;
};
}
export class CookieList extends PropertyList<Cookie> {
kind: string = 'CookieList';
cookies: Cookie[];
constructor(parent: object, cookies: Cookie[]) {
super(Cookie, parent.toString(), cookies);
this.cookies = cookies;
}
static isCookieList(obj: object) {
return 'kind' in obj && obj.kind === 'CookieList';
}
}

View File

@@ -51,8 +51,7 @@ initializeLogging();
document.body.setAttribute('data-platform', process.platform);
document.title = getProductName();
const windowMessageHandler = getWindowMessageHandler();
windowMessageHandler.start();
getWindowMessageHandler().start();
try {
// In order to run playwight tests that simulate a logged in user