testem 3.19.1 → 3.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/app.js CHANGED
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  const EventEmitter = require('events').EventEmitter;
4
2
  const { fromCallback, using, disposer, mapLimit } = require('./utils/promises');
5
3
  const Path = require('path');
@@ -40,7 +38,7 @@ module.exports = class App extends EventEmitter {
40
38
 
41
39
  let alreadyExit = false;
42
40
 
43
- this.cleanExit = err => {
41
+ this.cleanExit = (err) => {
44
42
  if (!alreadyExit) {
45
43
  alreadyExit = true;
46
44
 
@@ -52,7 +50,9 @@ module.exports = class App extends EventEmitter {
52
50
 
53
51
  if (this.testSuiteTimedOut === true) {
54
52
  let timeoutSeconds = this.testSuiteTimeout.timeout;
55
- err = new Error(`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminated all test runners.`);
53
+ err = new Error(
54
+ `Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminated all test runners.`,
55
+ );
56
56
  exitCode = 1;
57
57
  }
58
58
 
@@ -64,53 +64,63 @@ module.exports = class App extends EventEmitter {
64
64
  start(cb) {
65
65
  log.info('Starting ' + this.config.appMode);
66
66
 
67
- return using(SignalListeners.with(), signalListeners => {
68
- signalListeners.on('signal', err => this.exit(err));
67
+ return using(SignalListeners.with(), (signalListeners) => {
68
+ signalListeners.on('signal', (err) => this.exit(err));
69
69
 
70
- return using(Reporter.with(this, this.stdoutStream, this.reportFileName), reporter => {
71
- this.reporter = reporter;
70
+ return using(
71
+ Reporter.with(this, this.stdoutStream, this.reportFileName),
72
+ (reporter) => {
73
+ this.reporter = reporter;
72
74
 
73
- return using(this.fileWatch(), () => {
74
- return using(this.getServer(), () => {
75
- return using(this.getRunners(), () => {
76
- return using(this.runHook('on_start'), () => {
77
- let w = this.waitForTests();
75
+ return using(this.fileWatch(), () => {
76
+ return using(this.getServer(), () => {
77
+ return using(this.getRunners(), () => {
78
+ return using(this.runHook('on_start'), () => {
79
+ let w = this.waitForTests();
78
80
 
79
- if (cb) {
80
- cb();
81
- }
81
+ if (cb) {
82
+ cb();
83
+ }
82
84
 
83
- return w;
84
- }).then(() => {
85
- log.info('Stopping ' + this.config.appMode);
85
+ return w;
86
+ })
87
+ .then(() => {
88
+ log.info('Stopping ' + this.config.appMode);
86
89
 
87
- this.emit('tests-finish');
90
+ this.emit('tests-finish');
88
91
 
89
- return using(this.runHook('on_exit'), () => {});
90
- }).catch(error => {
91
- log.error(error);
92
- log.info('Stopping ' + this.config.appMode);
92
+ return using(this.runHook('on_exit'), () => {});
93
+ })
94
+ .catch((error) => {
95
+ log.error(error);
96
+ log.info('Stopping ' + this.config.appMode);
93
97
 
94
- this.emit('tests-error');
98
+ this.emit('tests-error');
95
99
 
96
- return new Promise((resolve, reject) => {
97
- using(this.runHook('on_exit'), () => {}).then(() => {
98
- reject(error);
100
+ return new Promise((resolve, reject) => {
101
+ using(this.runHook('on_exit'), () => {}).then(() => {
102
+ reject(error);
103
+ });
104
+ });
99
105
  });
100
- });
101
106
  });
102
107
  });
103
108
  });
104
- });
105
- });
106
- }).then(() => this.cleanExit(), err => this.cleanExit(err));
109
+ },
110
+ );
111
+ }).then(
112
+ () => this.cleanExit(),
113
+ (err) => this.cleanExit(err),
114
+ );
107
115
  }
108
116
 
109
117
  waitForTests() {
110
118
  log.info('Waiting for tests.');
111
119
 
112
120
  if (this.exited) {
113
- return Promise.reject(this.exitErr || new Error('Testem exited before running any tests.'));
121
+ return Promise.reject(
122
+ this.exitErr || new Error('Testem exited before running any tests.'),
123
+ );
114
124
  }
115
125
 
116
126
  let run = this.triggerRun('Start');
@@ -133,11 +143,13 @@ module.exports = class App extends EventEmitter {
133
143
  }
134
144
  this.restarting = true;
135
145
 
136
- return this.stopCurrentRun().catch(this.exit.bind(this)).then(() => {
137
- this.restarting = false;
146
+ return this.stopCurrentRun()
147
+ .catch(this.exit.bind(this))
148
+ .then(() => {
149
+ this.restarting = false;
138
150
 
139
- return this.runTests();
140
- });
151
+ return this.runTests();
152
+ });
141
153
  }
142
154
 
143
155
  stopCurrentRun() {
@@ -145,7 +157,7 @@ module.exports = class App extends EventEmitter {
145
157
  return Promise.resolve();
146
158
  }
147
159
 
148
- return Promise.all([ this.stopRunners(), this.currentRun ]);
160
+ return Promise.all([this.stopRunners(), this.currentRun]);
149
161
  }
150
162
 
151
163
  runTests() {
@@ -158,13 +170,15 @@ module.exports = class App extends EventEmitter {
158
170
  this.reporter.onStart('testem', { launcherId: 0 });
159
171
 
160
172
  return using(this.runHook('before_tests'), () => {
161
- return using(RunTimeout.with(this.config.get('timeout')), timeout => {
173
+ return using(RunTimeout.with(this.config.get('timeout')), (timeout) => {
162
174
  this.testSuiteTimeout = timeout;
163
175
 
164
176
  timeout.on('timeout', () => {
165
177
  let timeoutSeconds = timeout.timeout;
166
178
 
167
- log.info(`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminating all test runners`);
179
+ log.info(
180
+ `Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminating all test runners`,
181
+ );
168
182
  this.testSuiteTimedOut = true;
169
183
  this.killRunners();
170
184
  });
@@ -178,23 +192,25 @@ module.exports = class App extends EventEmitter {
178
192
  }).then(() => {
179
193
  return using(this.runHook('after_tests'), () => {});
180
194
  });
181
- }).catch(err => {
182
- if (err.hideFromReporter) {
183
- return;
184
- }
185
-
186
- let result = {
187
- failed: 1,
188
- passed: 0,
189
- name: 'testem',
190
- launcherId: 0,
191
- error: {
192
- message: err.toString()
195
+ })
196
+ .catch((err) => {
197
+ if (err.hideFromReporter) {
198
+ return;
193
199
  }
194
- };
195
200
 
196
- this.reporter.report('testem', result);
197
- }).finally(() => this.reporter.onEnd('testem', { launcherId: 0 }));
201
+ let result = {
202
+ failed: 1,
203
+ passed: 0,
204
+ name: 'testem',
205
+ launcherId: 0,
206
+ error: {
207
+ message: err.toString(),
208
+ },
209
+ };
210
+
211
+ this.reporter.report('testem', result);
212
+ })
213
+ .finally(() => this.reporter.onEnd('testem', { launcherId: 0 }));
198
214
  }
199
215
 
200
216
  exit(err, cb) {
@@ -233,12 +249,25 @@ module.exports = class App extends EventEmitter {
233
249
  }
234
250
 
235
251
  getServer() {
236
- return disposer(Promise.resolve(this.startServer()), () => this.stopServer());
252
+ return disposer(Promise.resolve(this.startServer()), () =>
253
+ this.stopServer(),
254
+ );
237
255
  }
238
256
 
239
- onFileRequested(filepath) {
257
+ /**
258
+ * When the server requests a file, add it to the file watcher if it is not already being watched.
259
+ * If the file watcher is disabled, do nothing.
260
+ *
261
+ * @param {string} filepath
262
+ * @returns {Promise<void>}
263
+ */
264
+ async onFileRequested(filepath) {
240
265
  if (this.fileWatcher && !this.config.get('serve_files')) {
241
- this.fileWatcher.add(filepath);
266
+ try {
267
+ await this.fileWatcher.add(filepath);
268
+ } catch (err) {
269
+ log.error(err);
270
+ }
242
271
  }
243
272
  }
244
273
 
@@ -251,18 +280,30 @@ module.exports = class App extends EventEmitter {
251
280
  }
252
281
 
253
282
  onBrowserLogin(browserName, id, socket) {
254
- let browser = _.find(this.runners, runner => {
255
- return runner.launcherId === id && (!runner.socket || !runner.socket.connected);
283
+ let browser = _.find(this.runners, (runner) => {
284
+ return (
285
+ runner.launcherId === id && (!runner.socket || !runner.socket.connected)
286
+ );
256
287
  });
257
288
 
258
289
  if (!browser) {
259
- let launcher = new LauncherFactory(browserName, {
260
- id: id,
261
- protocol: 'browser'
262
- }, this.config).create();
290
+ let launcher = new LauncherFactory(
291
+ browserName,
292
+ {
293
+ id: id,
294
+ protocol: 'browser',
295
+ },
296
+ this.config,
297
+ ).create();
263
298
  const singleRun = this.config.get('single_run');
264
299
 
265
- browser = new BrowserTestRunner(launcher, this.reporter, this.runnerIndex++, singleRun, this.config);
300
+ browser = new BrowserTestRunner(
301
+ launcher,
302
+ this.reporter,
303
+ this.runnerIndex++,
304
+ singleRun,
305
+ this.config,
306
+ );
266
307
  this.addRunner(browser);
267
308
  }
268
309
 
@@ -270,10 +311,12 @@ module.exports = class App extends EventEmitter {
270
311
  }
271
312
 
272
313
  onBrowserRelogin(browserName, id, socket) {
273
- let browser = _.find(this.runners, runner => {
314
+ let browser = _.find(this.runners, (runner) => {
274
315
  // a browser relogin can happen if a client socket was disconnected, which may not be reflected in runner.socket's connected state
275
316
  // or if the socket was nulled by 'onDisconnect'
276
- return runner.launcherId === id && (runner.socket || runner.socket === null);
317
+ return (
318
+ runner.launcherId === id && (runner.socket || runner.socket === null)
319
+ );
277
320
  });
278
321
 
279
322
  if (!browser) {
@@ -293,8 +336,15 @@ module.exports = class App extends EventEmitter {
293
336
  this.emit('runnerAdded', runner);
294
337
  }
295
338
 
339
+ // Watcher teardown is this disposer's cleanup (using() awaits it on success and on setup failure).
296
340
  fileWatch() {
297
- return disposer(Promise.resolve(this.configureFileWatch()), () => {});
341
+ return disposer(Promise.resolve(this.configureFileWatch()), () => {
342
+ if (this.fileWatcher) {
343
+ const w = this.fileWatcher;
344
+ this.fileWatcher = undefined;
345
+ return w.close();
346
+ }
347
+ });
298
348
  }
299
349
 
300
350
  configureFileWatch() {
@@ -302,46 +352,58 @@ module.exports = class App extends EventEmitter {
302
352
  return Promise.resolve();
303
353
  }
304
354
 
305
- this.fileWatcher = new FileWatcher(this.config);
306
- this.fileWatcher.on('fileChanged', filepath => {
307
- log.info(filepath + ' changed (' + (this.disableFileWatch ? 'disabled' : 'enabled') + ').');
308
- if (this.disableFileWatch || this.paused) {
309
- return;
310
- }
311
- let configFile = this.config.get('file');
312
- if ((configFile && filepath === Path.resolve(configFile)) ||
313
- (this.config.isCwdMode() && filepath === process.cwd())) {
314
- // config changed
315
- this.configure(() => {
316
- this.triggerRun('Config changed');
317
- });
318
- } else {
319
- using(this.runHook('on_change', {file: filepath}), () => {
320
- this.triggerRun('File changed: ' + filepath);
321
- });
322
- }
323
- });
324
- this.fileWatcher.on('EMFILE', () => {
325
- let view = this.view;
326
- let text = [
327
- 'The file watcher received a EMFILE system error, which means that ',
328
- 'it has hit the maximum number of files that can be open at a time. ',
329
- 'Luckily, you can increase this limit as a workaround. See the directions below \n \n',
330
- 'Linux: http://stackoverflow.com/a/34645/5304\n',
331
- 'Mac OS: http://serverfault.com/a/15575/47234'
332
- ].join('');
333
- view.setErrorPopupMessage(new StyledString(text + '\n ').foreground('megenta'));
355
+ return FileWatcher.create(this.config).then((fw) => {
356
+ this.fileWatcher = fw;
357
+ this.fileWatcher.on('fileChanged', (filepath) => {
358
+ log.info(
359
+ filepath +
360
+ ' changed (' +
361
+ (this.disableFileWatch ? 'disabled' : 'enabled') +
362
+ ').',
363
+ );
364
+ if (this.disableFileWatch || this.paused) {
365
+ return;
366
+ }
367
+ let configFile = this.config.get('file');
368
+ if (
369
+ (configFile && filepath === Path.resolve(configFile)) ||
370
+ (this.config.isCwdMode() && filepath === process.cwd())
371
+ ) {
372
+ // config changed
373
+ this.configure(() => {
374
+ this.triggerRun('Config changed');
375
+ });
376
+ } else {
377
+ using(this.runHook('on_change', { file: filepath }), () => {
378
+ this.triggerRun('File changed: ' + filepath);
379
+ });
380
+ }
381
+ });
382
+ this.fileWatcher.on('EMFILE', () => {
383
+ let view = this.view;
384
+ let text = [
385
+ 'The file watcher received a EMFILE system error, which means that ',
386
+ 'it has hit the maximum number of files that can be open at a time. ',
387
+ 'Luckily, you can increase this limit as a workaround. See the directions below \n \n',
388
+ 'Linux: http://stackoverflow.com/a/34645/5304\n',
389
+ 'Mac OS: http://serverfault.com/a/15575/47234',
390
+ ].join('');
391
+ view.setErrorPopupMessage(
392
+ new StyledString(text + '\n ').foreground('megenta'),
393
+ );
394
+ });
334
395
  });
335
-
336
- return Promise.resolve();
337
396
  }
338
397
 
339
398
  getRunners() {
340
- return disposer(fromCallback(callback => {
341
- this.createRunners(callback);
342
- }), () => {
343
- return this.killRunners();
344
- });
399
+ return disposer(
400
+ fromCallback((callback) => {
401
+ this.createRunners(callback);
402
+ }),
403
+ () => {
404
+ return this.killRunners();
405
+ },
406
+ );
345
407
  }
346
408
 
347
409
  createRunners(callback) {
@@ -374,14 +436,20 @@ module.exports = class App extends EventEmitter {
374
436
  case 'tap':
375
437
  return TapProcessTestRunner;
376
438
  default:
377
- throw new Error('Don\'t know about ' + protocol + ' protocol.');
439
+ throw new Error('Unknown protocol: ' + protocol);
378
440
  }
379
441
  }
380
442
 
381
443
  createTestRunner(launcher, reporter) {
382
444
  let singleRun = this.config.get('single_run');
383
445
 
384
- return new (this.getRunnerFactory(launcher))(launcher, reporter, this.runnerIndex++, singleRun, this.config);
446
+ return new (this.getRunnerFactory(launcher))(
447
+ launcher,
448
+ reporter,
449
+ this.runnerIndex++,
450
+ singleRun,
451
+ this.config,
452
+ );
385
453
  }
386
454
 
387
455
  withTestTimeout() {
@@ -392,9 +460,9 @@ module.exports = class App extends EventEmitter {
392
460
 
393
461
  singleRun(timeout) {
394
462
  let limit = this.config.get('parallel');
395
- let concurrency = (limit && limit >= 1) ? parseInt(limit) : Infinity;
463
+ let concurrency = limit && limit >= 1 ? parseInt(limit) : Infinity;
396
464
 
397
- return mapLimit(this.runners, concurrency, runner => {
465
+ return mapLimit(this.runners, concurrency, (runner) => {
398
466
  if (this.exited) {
399
467
  let e = new Error('Run canceled.');
400
468
  e.hideFromReporter = true;
@@ -435,20 +503,22 @@ module.exports = class App extends EventEmitter {
435
503
  }
436
504
 
437
505
  stopRunners() {
438
- return Promise.all(this.runners.map(runner => {
439
- if (typeof runner.stop === 'function') {
440
- return runner.stop();
441
- }
506
+ return Promise.all(
507
+ this.runners.map((runner) => {
508
+ if (typeof runner.stop === 'function') {
509
+ return runner.stop();
510
+ }
442
511
 
443
- return runner.exit();
444
- }));
512
+ return runner.exit();
513
+ }),
514
+ );
445
515
  }
446
516
 
447
517
  killRunners() {
448
- return Promise.all(this.runners.map(runner => runner.exit()));
518
+ return Promise.all(this.runners.map((runner) => runner.exit()));
449
519
  }
450
520
 
451
521
  launchers() {
452
- return this.runners.map(runner => runner.launcher);
522
+ return this.runners.map((runner) => runner.launcher);
453
523
  }
454
524
  };
@@ -0,0 +1,93 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ /**
6
+ * Walks upward from `startDir` looking for `node_modules/testem/testem.js`.
7
+ *
8
+ * @param {string} startDir
9
+ * @returns {string | null}
10
+ */
11
+ function findLocalTestemScript(startDir) {
12
+ let dir = path.resolve(startDir);
13
+ const { root } = path.parse(dir);
14
+ while (true) {
15
+ const candidate = path.join(dir, 'node_modules', 'testem', 'testem.js');
16
+ if (fs.existsSync(candidate)) {
17
+ return candidate;
18
+ }
19
+ if (dir === root) {
20
+ break;
21
+ }
22
+ dir = path.dirname(dir);
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * If a local install exists and is not the same file as the running CLI (e.g. global),
29
+ * returns the path to the local `testem.js`. Otherwise returns null.
30
+ *
31
+ * @param {string} currentFile - `__filename` of the running CLI
32
+ * @param {string} cwd
33
+ * @param {NodeJS.ProcessEnv} [env]
34
+ * @returns {string | null}
35
+ */
36
+ function resolveLocalTestemCliOrNull(currentFile, cwd, env = process.env) {
37
+ if (env.TESTEM_USE_GLOBAL === '1') {
38
+ return null;
39
+ }
40
+ const local = findLocalTestemScript(cwd);
41
+ if (!local) {
42
+ return null;
43
+ }
44
+ let cur;
45
+ let loc;
46
+ try {
47
+ cur = fs.realpathSync(currentFile);
48
+ loc = fs.realpathSync(local);
49
+ } catch {
50
+ return null;
51
+ }
52
+ if (cur === loc) {
53
+ return null;
54
+ }
55
+ return local;
56
+ }
57
+
58
+ /**
59
+ * When the globally installed `testem` is run inside a project that has a local
60
+ * `testem` dependency, re-exec that local copy so CLI and project stay in sync.
61
+ *
62
+ * Exits the process when delegation runs; otherwise returns normally.
63
+ *
64
+ * @param {string} currentFile
65
+ * @param {string[]} argv
66
+ * @param {string} cwd
67
+ * @param {NodeJS.ProcessEnv} env
68
+ */
69
+ function tryDelegateToLocalTestem(currentFile, argv, cwd, env) {
70
+ const local = resolveLocalTestemCliOrNull(currentFile, cwd, env);
71
+ if (!local) {
72
+ return;
73
+ }
74
+ const result = spawnSync(process.execPath, [local, ...argv.slice(2)], {
75
+ stdio: 'inherit',
76
+ env,
77
+ windowsHide: true,
78
+ });
79
+ if (result.error) {
80
+ console.error(result.error);
81
+ process.exit(1);
82
+ }
83
+ if (result.signal) {
84
+ process.exit(1);
85
+ }
86
+ process.exit(result.status === null ? 0 : result.status);
87
+ }
88
+
89
+ module.exports = {
90
+ findLocalTestemScript,
91
+ resolveLocalTestemCliOrNull,
92
+ tryDelegateToLocalTestem,
93
+ };