spec-helpers.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import * as childProcess from 'child_process';
  2. import * as path from 'path';
  3. import * as http from 'http';
  4. import * as v8 from 'v8';
  5. import { SuiteFunction, TestFunction } from 'mocha';
  6. import { BrowserWindow } from 'electron/main';
  7. import { AssertionError } from 'chai';
  8. const addOnly = <T>(fn: Function): T => {
  9. const wrapped = (...args: any[]) => {
  10. return fn(...args);
  11. };
  12. (wrapped as any).only = wrapped;
  13. (wrapped as any).skip = wrapped;
  14. return wrapped as any;
  15. };
  16. export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
  17. export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
  18. export const delay = (time: number = 0) => new Promise(resolve => setTimeout(resolve, time));
  19. type CleanupFunction = (() => void) | (() => Promise<void>)
  20. const cleanupFunctions: CleanupFunction[] = [];
  21. export async function runCleanupFunctions () {
  22. for (const cleanup of cleanupFunctions) {
  23. const r = cleanup();
  24. if (r instanceof Promise) { await r; }
  25. }
  26. cleanupFunctions.length = 0;
  27. }
  28. export function defer (f: CleanupFunction) {
  29. cleanupFunctions.unshift(f);
  30. }
  31. class RemoteControlApp {
  32. process: childProcess.ChildProcess;
  33. port: number;
  34. constructor (proc: childProcess.ChildProcess, port: number) {
  35. this.process = proc;
  36. this.port = port;
  37. }
  38. remoteEval = (js: string): Promise<any> => {
  39. return new Promise((resolve, reject) => {
  40. const req = http.request({
  41. host: '127.0.0.1',
  42. port: this.port,
  43. method: 'POST'
  44. }, res => {
  45. const chunks = [] as Buffer[];
  46. res.on('data', chunk => { chunks.push(chunk); });
  47. res.on('end', () => {
  48. const ret = v8.deserialize(Buffer.concat(chunks));
  49. if (Object.prototype.hasOwnProperty.call(ret, 'error')) {
  50. reject(new Error(`remote error: ${ret.error}\n\nTriggered at:`));
  51. } else {
  52. resolve(ret.result);
  53. }
  54. });
  55. });
  56. req.write(js);
  57. req.end();
  58. });
  59. }
  60. remotely = (script: Function, ...args: any[]): Promise<any> => {
  61. return this.remoteEval(`(${script})(...${JSON.stringify(args)})`);
  62. }
  63. }
  64. export async function startRemoteControlApp (extraArgs: string[] = [], options?: childProcess.SpawnOptionsWithoutStdio) {
  65. const appPath = path.join(__dirname, 'fixtures', 'apps', 'remote-control');
  66. const appProcess = childProcess.spawn(process.execPath, [appPath, ...extraArgs], options);
  67. appProcess.stderr.on('data', d => {
  68. process.stderr.write(d);
  69. });
  70. const port = await new Promise<number>(resolve => {
  71. appProcess.stdout.on('data', d => {
  72. const m = /Listening: (\d+)/.exec(d.toString());
  73. if (m && m[1] != null) {
  74. resolve(Number(m[1]));
  75. }
  76. });
  77. });
  78. defer(() => { appProcess.kill('SIGINT'); });
  79. return new RemoteControlApp(appProcess, port);
  80. }
  81. export function waitUntil (
  82. callback: () => boolean,
  83. opts: { rate?: number, timeout?: number } = {}
  84. ) {
  85. const { rate = 10, timeout = 10000 } = opts;
  86. return new Promise<void>((resolve, reject) => {
  87. let intervalId: NodeJS.Timeout | undefined; // eslint-disable-line prefer-const
  88. let timeoutId: NodeJS.Timeout | undefined;
  89. const cleanup = () => {
  90. if (intervalId) clearInterval(intervalId);
  91. if (timeoutId) clearTimeout(timeoutId);
  92. };
  93. const check = () => {
  94. let result;
  95. try {
  96. result = callback();
  97. } catch (e) {
  98. cleanup();
  99. reject(e);
  100. return;
  101. }
  102. if (result === true) {
  103. cleanup();
  104. resolve();
  105. return true;
  106. }
  107. };
  108. if (check()) {
  109. return;
  110. }
  111. intervalId = setInterval(check, rate);
  112. timeoutId = setTimeout(() => {
  113. timeoutId = undefined;
  114. cleanup();
  115. reject(new Error(`waitUntil timed out after ${timeout}ms`));
  116. }, timeout);
  117. });
  118. }
  119. export async function repeatedly<T> (
  120. fn: () => Promise<T>,
  121. opts?: { until?: (x: T) => boolean, timeLimit?: number }
  122. ) {
  123. const { until = (x: T) => !!x, timeLimit = 10000 } = opts ?? {};
  124. const begin = +new Date();
  125. while (true) {
  126. const ret = await fn();
  127. if (until(ret)) { return ret; }
  128. if (+new Date() - begin > timeLimit) { throw new Error(`repeatedly timed out (limit=${timeLimit})`); }
  129. }
  130. }
  131. async function makeRemoteContext (opts?: any) {
  132. const { webPreferences, setup, url = 'about:blank', ...rest } = opts ?? {};
  133. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, ...webPreferences }, ...rest });
  134. await w.loadURL(url.toString());
  135. if (setup) await w.webContents.executeJavaScript(setup);
  136. return w;
  137. }
  138. const remoteContext: BrowserWindow[] = [];
  139. export async function getRemoteContext () {
  140. if (remoteContext.length) { return remoteContext[0]; }
  141. const w = await makeRemoteContext();
  142. defer(() => w.close());
  143. return w;
  144. }
  145. export function useRemoteContext (opts?: any) {
  146. before(async () => {
  147. remoteContext.unshift(await makeRemoteContext(opts));
  148. });
  149. after(() => {
  150. const w = remoteContext.shift();
  151. w!.close();
  152. });
  153. }
  154. export async function itremote (name: string, fn: Function, args?: any[]) {
  155. it(name, async () => {
  156. const w = await getRemoteContext();
  157. const { ok, message } = await w.webContents.executeJavaScript(`(async () => {
  158. try {
  159. const chai_1 = require('chai')
  160. chai_1.use(require('chai-as-promised'))
  161. chai_1.use(require('dirty-chai'))
  162. await (${fn})(...${JSON.stringify(args ?? [])})
  163. return {ok: true};
  164. } catch (e) {
  165. return {ok: false, message: e.message}
  166. }
  167. })()`);
  168. if (!ok) { throw new AssertionError(message); }
  169. });
  170. }