123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- import { BrowserWindow } from 'electron/main';
- import { AssertionError } from 'chai';
- import { SuiteFunction, TestFunction } from 'mocha';
- import * as childProcess from 'node:child_process';
- import * as http from 'node:http';
- import * as http2 from 'node:http2';
- import * as https from 'node:https';
- import * as net from 'node:net';
- import * as path from 'node:path';
- import * as url from 'node:url';
- import * as v8 from 'node:v8';
- const addOnly = <T>(fn: Function): T => {
- const wrapped = (...args: any[]) => {
- return fn(...args);
- };
- (wrapped as any).only = wrapped;
- (wrapped as any).skip = wrapped;
- return wrapped as any;
- };
- export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
- export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
- type CleanupFunction = (() => void) | (() => Promise<void>)
- const cleanupFunctions: CleanupFunction[] = [];
- export async function runCleanupFunctions () {
- for (const cleanup of cleanupFunctions) {
- const r = cleanup();
- if (r instanceof Promise) { await r; }
- }
- cleanupFunctions.length = 0;
- }
- export function defer (f: CleanupFunction) {
- cleanupFunctions.unshift(f);
- }
- class RemoteControlApp {
- process: childProcess.ChildProcess;
- port: number;
- constructor (proc: childProcess.ChildProcess, port: number) {
- this.process = proc;
- this.port = port;
- }
- remoteEval = (js: string): Promise<any> => {
- return new Promise((resolve, reject) => {
- const req = http.request({
- host: '127.0.0.1',
- port: this.port,
- method: 'POST'
- }, res => {
- const chunks = [] as Buffer[];
- res.on('data', chunk => { chunks.push(chunk); });
- res.on('end', () => {
- const ret = v8.deserialize(Buffer.concat(chunks));
- if (Object.hasOwn(ret, 'error')) {
- reject(new Error(`remote error: ${ret.error}\n\nTriggered at:`));
- } else {
- resolve(ret.result);
- }
- });
- });
- req.write(js);
- req.end();
- });
- };
- remotely = (script: Function, ...args: any[]): Promise<any> => {
- return this.remoteEval(`(${script})(...${JSON.stringify(args)})`);
- };
- }
- export async function startRemoteControlApp (extraArgs: string[] = [], options?: childProcess.SpawnOptionsWithoutStdio) {
- const appPath = path.join(__dirname, '..', 'fixtures', 'apps', 'remote-control');
- const appProcess = childProcess.spawn(process.execPath, [appPath, ...extraArgs], options);
- appProcess.stderr.on('data', d => {
- process.stderr.write(d);
- });
- const port = await new Promise<number>(resolve => {
- appProcess.stdout.on('data', d => {
- const m = /Listening: (\d+)/.exec(d.toString());
- if (m && m[1] != null) {
- resolve(Number(m[1]));
- }
- });
- });
- defer(() => { appProcess.kill('SIGINT'); });
- return new RemoteControlApp(appProcess, port);
- }
- export function waitUntil (
- callback: () => boolean,
- opts: { rate?: number, timeout?: number } = {}
- ) {
- const { rate = 10, timeout = 10000 } = opts;
- return new Promise<void>((resolve, reject) => {
- let intervalId: NodeJS.Timeout | undefined; // eslint-disable-line prefer-const
- let timeoutId: NodeJS.Timeout | undefined;
- const cleanup = () => {
- if (intervalId) clearInterval(intervalId);
- if (timeoutId) clearTimeout(timeoutId);
- };
- const check = () => {
- let result;
- try {
- result = callback();
- } catch (e) {
- cleanup();
- reject(e);
- return;
- }
- if (result === true) {
- cleanup();
- resolve();
- return true;
- }
- };
- if (check()) {
- return;
- }
- intervalId = setInterval(check, rate);
- timeoutId = setTimeout(() => {
- timeoutId = undefined;
- cleanup();
- reject(new Error(`waitUntil timed out after ${timeout}ms`));
- }, timeout);
- });
- }
- export async function repeatedly<T> (
- fn: () => Promise<T>,
- opts?: { until?: (x: T) => boolean, timeLimit?: number }
- ) {
- const { until = (x: T) => !!x, timeLimit = 10000 } = opts ?? {};
- const begin = Date.now();
- while (true) {
- const ret = await fn();
- if (until(ret)) { return ret; }
- if (Date.now() - begin > timeLimit) { throw new Error(`repeatedly timed out (limit=${timeLimit})`); }
- }
- }
- async function makeRemoteContext (opts?: any) {
- const { webPreferences, setup, url = 'about:blank', ...rest } = opts ?? {};
- const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, ...webPreferences }, ...rest });
- await w.loadURL(url.toString());
- if (setup) await w.webContents.executeJavaScript(setup);
- return w;
- }
- const remoteContext: BrowserWindow[] = [];
- export async function getRemoteContext () {
- if (remoteContext.length) { return remoteContext[0]; }
- const w = await makeRemoteContext();
- defer(() => w.close());
- return w;
- }
- export function useRemoteContext (opts?: any) {
- before(async () => {
- remoteContext.unshift(await makeRemoteContext(opts));
- });
- after(() => {
- const w = remoteContext.shift();
- w!.close();
- });
- }
- export async function itremote (name: string, fn: Function, args?: any[]) {
- it(name, async () => {
- const w = await getRemoteContext();
- const { ok, message } = await w.webContents.executeJavaScript(`(async () => {
- try {
- const chai_1 = require('chai')
- const promises_1 = require('node:timers/promises')
- chai_1.use(require('chai-as-promised'))
- chai_1.use(require('dirty-chai'))
- await (${fn})(...${JSON.stringify(args ?? [])})
- return {ok: true};
- } catch (e) {
- return {ok: false, message: e.message}
- }
- })()`);
- if (!ok) { throw new AssertionError(message); }
- });
- }
- export async function listen (server: http.Server | https.Server | http2.Http2SecureServer) {
- const hostname = '127.0.0.1';
- await new Promise<void>(resolve => server.listen(0, hostname, () => resolve()));
- const { port } = server.address() as net.AddressInfo;
- const protocol = (server instanceof http.Server) ? 'http' : 'https';
- return { port, hostname, url: url.format({ protocol, hostname, port }) };
- }
|