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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Detects EMFILE errors from `fs.watch` / watcher backends (e.g. chokidar `error`).
3
+ * Fireworm uses a dedicated `emfile` event; other engines surface this via `error`.
4
+ *
5
+ * @param {unknown} err
6
+ * @returns {boolean}
7
+ */
8
+ function isEmfileError(err) {
9
+ return (
10
+ err !== null &&
11
+ typeof err === 'object' &&
12
+ /** @type {{ code?: string }} */ (err).code === 'EMFILE'
13
+ );
14
+ }
15
+
16
+ module.exports = isEmfileError;
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  const browserArgs = require('./browser-args');
4
2
  const path = require('path');
5
3
  const fs = require('fs');
@@ -8,17 +6,20 @@ const os = require('os');
8
6
 
9
7
  function chromeWinPaths(homeDir, name) {
10
8
  return [
11
- homeDir + '\\Local Settings\\Application Data\\Google\\' + name + '\\Application\\chrome.exe',
9
+ homeDir +
10
+ '\\Local Settings\\Application Data\\Google\\' +
11
+ name +
12
+ '\\Application\\chrome.exe',
12
13
  homeDir + '\\AppData\\Local\\Google\\' + name + '\\Application\\chrome.exe',
13
14
  'C:\\Program Files\\Google\\' + name + '\\Application\\Chrome.exe',
14
- 'C:\\Program Files (x86)\\Google\\' + name + '\\Application\\Chrome.exe'
15
+ 'C:\\Program Files (x86)\\Google\\' + name + '\\Application\\Chrome.exe',
15
16
  ];
16
17
  }
17
18
 
18
19
  function chromeOSXPaths(name) {
19
20
  return [
20
21
  process.env.HOME + '/Applications/' + name + '.app/Contents/MacOS/' + name,
21
- '/Applications/' + name + '.app/Contents/MacOS/' + name
22
+ '/Applications/' + name + '.app/Contents/MacOS/' + name,
22
23
  ];
23
24
  }
24
25
 
@@ -31,7 +32,9 @@ function chromeArgs(browserTmpDir, url) {
31
32
  '--test-type',
32
33
  '--disable-renderer-backgrounding',
33
34
  '--disable-background-timer-throttling',
34
- url
35
+ '--disable-infobars',
36
+ '--disable-session-crashed-bubble',
37
+ url,
35
38
  ];
36
39
  }
37
40
 
@@ -42,15 +45,24 @@ function headlessChromeArgs(browserTmpDir, url) {
42
45
  '--disable-software-rasterizer',
43
46
  '--mute-audio',
44
47
  '--remote-debugging-port=0',
45
- '--window-size=1440,900'].concat(chromeArgs(browserTmpDir, url));
48
+ '--window-size=1440,900',
49
+ ].concat(chromeArgs(browserTmpDir, url));
46
50
  }
47
51
 
48
52
  function braveWinPaths(homeDir, name) {
49
53
  return [
50
- homeDir + '\\Local Settings\\Application Data\\BraveSoftware\\' + name + '\\Application\\brave.exe',
51
- homeDir + '\\AppData\\Local\\BraveSoftware\\' + name + '\\Application\\brave.exe',
54
+ homeDir +
55
+ '\\Local Settings\\Application Data\\BraveSoftware\\' +
56
+ name +
57
+ '\\Application\\brave.exe',
58
+ homeDir +
59
+ '\\AppData\\Local\\BraveSoftware\\' +
60
+ name +
61
+ '\\Application\\brave.exe',
52
62
  'C:\\Program Files\\BraveSoftware\\' + name + '\\Application\\brave.exe',
53
- 'C:\\Program Files (x86)\\BraveSoftware\\' + name + '\\Application\\brave.exe'
63
+ 'C:\\Program Files (x86)\\BraveSoftware\\' +
64
+ name +
65
+ '\\Application\\brave.exe',
54
66
  ];
55
67
  }
56
68
 
@@ -68,15 +80,17 @@ function headlessBraveArgs(browserTmpDir, url) {
68
80
  '--disable-gpu',
69
81
  '--mute-audio',
70
82
  '--remote-debugging-port=0',
71
- '--window-size=1440,900'].concat(braveArgs(browserTmpDir, url));
83
+ '--window-size=1440,900',
84
+ ].concat(braveArgs(browserTmpDir, url));
72
85
  }
73
86
 
74
87
  function edgePaths() {
75
88
  return [
76
89
  'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
77
90
  'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
78
- process.env.HOME + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
79
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
91
+ process.env.HOME +
92
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
93
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
80
94
  ];
81
95
  }
82
96
 
@@ -84,9 +98,11 @@ function firefoxPaths() {
84
98
  return [
85
99
  'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
86
100
  'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe',
87
- 'C:\\Users\\' + process.env.USERNAME + '\\AppData\\Local\\Mozilla Firefox\\firefox.exe',
101
+ 'C:\\Users\\' +
102
+ process.env.USERNAME +
103
+ '\\AppData\\Local\\Mozilla Firefox\\firefox.exe',
88
104
  process.env.HOME + '/Applications/Firefox.app/Contents/MacOS/firefox',
89
- '/Applications/Firefox.app/Contents/MacOS/firefox'
105
+ '/Applications/Firefox.app/Contents/MacOS/firefox',
90
106
  ];
91
107
  }
92
108
 
@@ -95,13 +111,14 @@ function firefoxSetup(browserTmpDir, config, done) {
95
111
  let perfJS = path.join(browserTmpDir, 'user.js');
96
112
 
97
113
  if (config.get('firefox_user_js')) {
98
- return fs.promises.readFile(config.get('firefox_user_js'))
99
- .then(content => fs.promises.writeFile(perfJS, content))
114
+ return fs.promises
115
+ .readFile(config.get('firefox_user_js'))
116
+ .then((content) => fs.promises.writeFile(perfJS, content))
100
117
  .then(() => done(null), done);
101
118
  }
102
119
 
103
- // using user.js to suppress the check default browser popup,
104
- // the welcome start page, and first-run helper installation prompts
120
+ // user.js: legacy Testem defaults plus prefs aligned with common Firefox --pref automation
121
+ // sets (welcome, onboarding, messaging, telemetry) and fewer prompts / background noise.
105
122
  let prefs = [
106
123
  'user_pref("browser.shell.checkDefaultBrowser", false);',
107
124
  'user_pref("browser.cache.disk.smart_size.first_run", false);',
@@ -110,9 +127,34 @@ function firefoxSetup(browserTmpDir, config, done) {
110
127
  'user_pref("app.update.auto", false);',
111
128
  'user_pref("app.update.enabled", false);',
112
129
  'user_pref("browser.EULA.override", true);',
113
- 'user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);'
130
+ 'user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);',
131
+ 'user_pref("browser.aboutwelcome.enabled", false);',
132
+ 'user_pref("browser.startup.homepage", "about:blank");',
133
+ 'user_pref("browser.startup.page", 0);',
134
+ 'user_pref("browser.startup.firstrunSkipsHomepage", true);',
135
+ 'user_pref("browser.startup.homepage_override.mstone", "ignore");',
136
+ 'user_pref("browser.startup.homepage_welcome_url", "");',
137
+ 'user_pref("browser.startup.homepage_welcome_url.additional", "");',
138
+ 'user_pref("browser.startup.cohort", "ignore");',
139
+ 'user_pref("browser.messaging-system.prompts.enabled", false);',
140
+ 'user_pref("browser.onboarding.enabled", false);',
141
+ 'user_pref("browser.tour.enabled", false);',
142
+ 'user_pref("browser.startup.upgradeDialog.enabled", false);',
143
+ 'user_pref("browser.uiCustomization.skipDefaultState", true);',
144
+ 'user_pref("toolkit.telemetry.enabled", false);',
145
+ 'user_pref("toolkit.telemetry.unified", false);',
146
+ 'user_pref("browser.ping-centre.telemetry", false);',
147
+ 'user_pref("browser.sessionstore.resume_from_crash", false);',
148
+ 'user_pref("browser.tabs.warnOnClose", false);',
149
+ 'user_pref("browser.tabs.warnOnCloseOtherTabs", false);',
150
+ 'user_pref("browser.tabs.warnOnOpen", false);',
151
+ 'user_pref("browser.download.manager.showWhenStarting", false);',
152
+ 'user_pref("extensions.update.enabled", false);',
153
+ 'user_pref("dom.webnotifications.enabled", false);',
154
+ 'user_pref("gfx.direct2d.disabled", true);',
114
155
  ];
115
- fs.promises.writeFile(perfJS, prefs.join(os.EOL))
156
+ fs.promises
157
+ .writeFile(perfJS, prefs.join(os.EOL))
116
158
  .then(() => done(null), done);
117
159
  }
118
160
 
@@ -138,7 +180,7 @@ module.exports = function knownBrowsers(platform, config) {
138
180
  },
139
181
  setup(config, done) {
140
182
  firefoxSetup(this.browserTmpDir(), config, done);
141
- }
183
+ },
142
184
  },
143
185
  {
144
186
  name: 'Headless Firefox',
@@ -150,156 +192,156 @@ module.exports = function knownBrowsers(platform, config) {
150
192
  },
151
193
  setup(config, done) {
152
194
  firefoxSetup(this.browserTmpDir(), config, done);
153
- }
195
+ },
154
196
  },
155
197
  {
156
198
  name: 'Brave',
157
- possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(braveOSXPaths('Brave Browser')),
158
- possibleExe: [
159
- 'brave'
160
- ],
199
+ possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(
200
+ braveOSXPaths('Brave Browser'),
201
+ ),
202
+ possibleExe: ['brave'],
161
203
  args(config, url) {
162
204
  return braveArgs(this.browserTmpDir(), url);
163
- }
205
+ },
164
206
  },
165
207
  {
166
208
  name: 'Headless Brave',
167
- possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(braveOSXPaths('Brave Browser')),
168
- possibleExe: [
169
- 'brave'
170
- ],
209
+ possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(
210
+ braveOSXPaths('Brave Browser'),
211
+ ),
212
+ possibleExe: ['brave'],
171
213
  args(config, url) {
172
214
  return headlessBraveArgs(this.browserTmpDir(), url);
173
- }
215
+ },
174
216
  },
175
217
  {
176
218
  name: 'Chrome',
177
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(chromeOSXPaths('Google Chrome')),
178
- possibleExe: [
179
- 'google-chrome',
180
- 'google-chrome-stable',
181
- 'chrome'
182
- ],
219
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(
220
+ chromeOSXPaths('Google Chrome'),
221
+ ),
222
+ possibleExe: ['google-chrome', 'google-chrome-stable', 'chrome'],
183
223
  args(config, url) {
184
224
  return chromeArgs(this.browserTmpDir(), url);
185
- }
225
+ },
186
226
  },
187
227
  {
188
228
  name: 'Headless Chrome',
189
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(chromeOSXPaths('Google Chrome')),
190
- possibleExe: [
191
- 'google-chrome',
192
- 'google-chrome-stable',
193
- 'chrome'
194
- ],
229
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(
230
+ chromeOSXPaths('Google Chrome'),
231
+ ),
232
+ possibleExe: ['google-chrome', 'google-chrome-stable', 'chrome'],
195
233
  args(config, url) {
196
234
  return headlessChromeArgs(this.browserTmpDir(), url);
197
- }
235
+ },
198
236
  },
199
237
 
200
238
  // On Windows, Chrome Stable/Beta/Dev are installed on the same path
201
239
  {
202
240
  name: 'Chrome Beta',
203
241
  possiblePath: chromeOSXPaths('Google Chrome Beta'),
204
- possibleExe: [
205
- 'google-chrome-beta'
206
- ],
242
+ possibleExe: ['google-chrome-beta'],
207
243
  args(config, url) {
208
244
  return chromeArgs(this.browserTmpDir(), url);
209
- }
245
+ },
210
246
  },
211
247
  {
212
248
  name: 'Headless Chrome Beta',
213
249
  possiblePath: chromeOSXPaths('Google Chrome Beta'),
214
- possibleExe: [
215
- 'google-chrome-beta'
216
- ],
250
+ possibleExe: ['google-chrome-beta'],
217
251
  args(config, url) {
218
252
  return headlessChromeArgs(this.browserTmpDir(), url);
219
- }
253
+ },
220
254
  },
221
255
  {
222
256
  name: 'Chrome Dev',
223
257
  possiblePath: chromeOSXPaths('Google Chrome Dev'),
224
- possibleExe: [
225
- 'google-chrome-unstable'
226
- ],
258
+ possibleExe: ['google-chrome-unstable'],
227
259
  args(config, url) {
228
260
  return chromeArgs(this.browserTmpDir(), url);
229
- }
261
+ },
230
262
  },
231
263
 
232
264
  // Chrome Canary is not available on Linux
233
265
  {
234
266
  name: 'Chrome Canary',
235
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome SxS').concat(chromeOSXPaths('Google Chrome Canary')),
267
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome SxS').concat(
268
+ chromeOSXPaths('Google Chrome Canary'),
269
+ ),
236
270
  args(config, url) {
237
271
  return chromeArgs(this.browserTmpDir(), url);
238
- }
272
+ },
239
273
  },
240
274
  {
241
275
  name: 'Chromium',
242
- possiblePath: chromeWinPaths(userHomeDir, 'Chromium').concat(chromeOSXPaths('Chromium')),
243
- possibleExe: [
244
- 'chromium-browser',
245
- 'chromium'
246
- ],
276
+ possiblePath: chromeWinPaths(userHomeDir, 'Chromium').concat(
277
+ chromeOSXPaths('Chromium'),
278
+ ),
279
+ possibleExe: ['chromium-browser', 'chromium'],
247
280
  args(config, url) {
248
281
  return chromeArgs(this.browserTmpDir(), url);
249
- }
282
+ },
250
283
  },
251
284
  {
252
285
  name: 'Edge',
253
286
  possiblePath: edgePaths(),
254
- possibleExe: [
255
- 'microsoft-edge'
256
- ],
287
+ possibleExe: ['microsoft-edge'],
257
288
  args(config, url) {
258
289
  return chromeArgs(this.browserTmpDir(), url);
259
- }
290
+ },
260
291
  },
261
292
  {
262
293
  name: 'Headless Edge',
263
294
  possiblePath: edgePaths(),
264
- possibleExe: [
265
- 'microsoft-edge'
266
- ],
295
+ possibleExe: ['microsoft-edge'],
267
296
  args(config, url) {
268
297
  return headlessChromeArgs(this.browserTmpDir(), url);
269
- }
270
- },
271
- {
272
- name: 'Safari',
273
- possiblePath: [
274
- 'C:\\Program Files\\Safari\\safari.exe',
275
- 'C:\\Program Files (x86)\\Safari\\safari.exe',
276
- process.env.HOME + '/Applications/Safari.app/Contents/MacOS/Safari',
277
- '/Applications/Safari.app/Contents/MacOS/Safari'
278
- ],
279
- setup(config, done) {
280
- let url = this.getUrl();
281
- fs.writeFile(
282
- path.join(this.browserTmpDir(), 'start.html'),
283
- '<script>window.location = \'' + url + '\'</script>',
284
- done
285
- );
286
298
  },
287
- args() {
288
- return [path.join(this.browserTmpDir(), 'start.html')];
289
- }
290
299
  },
300
+ platform === 'darwin'
301
+ ? {
302
+ // Use Launch Services so Safari opens the http(s) URL directly. Passing a local
303
+ // start.html file makes Safari 13+ prompt for confirmation (#1387) and breaks CI.
304
+ name: 'Safari',
305
+ possiblePath: '/usr/bin/open',
306
+ // `open` exits right after handing off to Safari; Testem must not treat that as the
307
+ // browser process dying (browser_test_runner ignoreProcessExit).
308
+ ignoreProcessExit: true,
309
+ args(config, url) {
310
+ return ['-a', 'Safari', url];
311
+ },
312
+ }
313
+ : {
314
+ name: 'Safari',
315
+ possiblePath: [
316
+ 'C:\\Program Files\\Safari\\safari.exe',
317
+ 'C:\\Program Files (x86)\\Safari\\safari.exe',
318
+ process.env.HOME + '/Applications/Safari.app/Contents/MacOS/Safari',
319
+ '/Applications/Safari.app/Contents/MacOS/Safari',
320
+ ],
321
+ setup(config, done) {
322
+ let url = this.getUrl();
323
+ fs.writeFile(
324
+ path.join(this.browserTmpDir(), 'start.html'),
325
+ "<script>window.location = '" + url + "'</script>",
326
+ done,
327
+ );
328
+ },
329
+ args() {
330
+ return [path.join(this.browserTmpDir(), 'start.html')];
331
+ },
332
+ },
291
333
  {
292
334
  name: 'Opera',
293
335
  possiblePath: [
294
336
  'C:\\Program Files\\Opera\\opera.exe',
295
337
  'C:\\Program Files (x86)\\Opera\\opera.exe',
296
338
  process.env.HOME + '/Applications/Opera.app/Contents/MacOS/Opera',
297
- '/Applications/Opera.app/Contents/MacOS/Opera'
339
+ '/Applications/Opera.app/Contents/MacOS/Opera',
298
340
  ],
299
341
  args(config, url) {
300
342
  let operaDataDir = this.browserTmpDir();
301
343
  return ['--user-data-dir=' + operaDataDir, '-pd', operaDataDir, url];
302
- }
344
+ },
303
345
  },
304
346
  {
305
347
  name: 'PhantomJS',
@@ -320,43 +362,38 @@ module.exports = function knownBrowsers(platform, config) {
320
362
  options = phantom_args.concat(options);
321
363
  }
322
364
  return options;
323
- }
324
- }
365
+ },
366
+ },
325
367
  ];
326
368
 
327
369
  if (isWin(platform)) {
328
370
  browsers.push({
329
371
  name: 'IE',
330
- possiblePath: 'C:\\Program Files\\Internet Explorer\\iexplore.exe'
372
+ possiblePath: 'C:\\Program Files\\Internet Explorer\\iexplore.exe',
331
373
  });
332
- } else {
374
+ } else if (platform === 'darwin') {
333
375
  browsers.push({
376
+ // Same Launch Services approach as Safari on macOS (#1387).
334
377
  name: 'Safari Technology Preview',
335
- possiblePath: [
336
- process.env.HOME + '/Applications/Safari Technology Preview.app/Contents/MacOS/Safari Technology Preview',
337
- '/Applications/Safari Technology Preview.app/Contents/MacOS/Safari Technology Preview'
338
- ],
339
- setup(config, done) {
340
- let url = this.getUrl();
341
- fs.writeFile(
342
- path.join(this.browserTmpDir(), 'start.html'),
343
- '<script>window.location = \'' + url + '\'</script>',
344
- done
345
- );
378
+ possiblePath: '/usr/bin/open',
379
+ ignoreProcessExit: true,
380
+ args(config, url) {
381
+ return ['-a', 'Safari Technology Preview', url];
346
382
  },
347
- args() {
348
- return [path.join(this.browserTmpDir(), 'start.html')];
349
- }
350
383
  });
351
384
  }
352
385
 
353
386
  // Add user-defined configs to browser objects
354
387
  let browserPaths = config.get('browser_paths'); // Add config defined 'path' for binary
355
- let browserExes = config.get('browser_exes'); // Add config defined 'exe' for binary
388
+ let browserExes = config.get('browser_exes'); // Add config defined 'exe' for binary
356
389
  if (browserPaths || browserExes) {
357
- browsers.forEach(browserObject => {
358
- let browserPath = browserPaths ? browserPaths[browserObject.name] : undefined;
359
- let browserExe = browserExes ? browserExes[browserObject.name] : undefined;
390
+ browsers.forEach((browserObject) => {
391
+ let browserPath = browserPaths
392
+ ? browserPaths[browserObject.name]
393
+ : undefined;
394
+ let browserExe = browserExes
395
+ ? browserExes[browserObject.name]
396
+ : undefined;
360
397
  if (browserPath) {
361
398
  browserObject.possiblePath = browserPath;
362
399
  }
@@ -0,0 +1,39 @@
1
+ const { minimatch } = require('minimatch');
2
+
3
+ /**
4
+ * Match a path against a single glob pattern. Thin wrapper so the matcher
5
+ * implementation (minimatch today, possibly picomatch later) lives in one place.
6
+ *
7
+ * @param {string} filePath - Path to test (forward slashes recommended; callers often use `convertToPosix`).
8
+ * @param {string} pattern - A single glob pattern.
9
+ * @param {import('minimatch').MinimatchOptions} [options] - Passed through to the matcher.
10
+ * @returns {boolean}
11
+ */
12
+ function pathMatchesPattern(filePath, pattern, options) {
13
+ if (pattern === null || pattern === undefined || pattern === '') {
14
+ return false;
15
+ }
16
+ return minimatch(filePath, pattern, options);
17
+ }
18
+
19
+ /**
20
+ * True if `filePath` matches **any** non-empty pattern in the list.
21
+ *
22
+ * @param {string} filePath
23
+ * @param {string[]} patterns
24
+ * @param {import('minimatch').MinimatchOptions} [options]
25
+ * @returns {boolean}
26
+ */
27
+ function pathMatchesAny(filePath, patterns, options) {
28
+ if (!patterns || patterns.length === 0) {
29
+ return false;
30
+ }
31
+ return patterns.some(function(p) {
32
+ return p && pathMatchesPattern(filePath, p, options);
33
+ });
34
+ }
35
+
36
+ module.exports = {
37
+ pathMatchesPattern,
38
+ pathMatchesAny,
39
+ };
@@ -3,6 +3,7 @@ const log = require('../log');
3
3
  const EventEmitter = require('events').EventEmitter;
4
4
 
5
5
  const isWin = require('./is-win')();
6
+ const closeTimeoutAfterExit = 1000;
6
7
 
7
8
  module.exports = class Process extends EventEmitter {
8
9
  constructor(name, options, process) {
@@ -13,6 +14,9 @@ module.exports = class Process extends EventEmitter {
13
14
  this.process = process;
14
15
  this.stdout = '';
15
16
  this.stderr = '';
17
+ this.exitAt = null;
18
+ this._killResolve = null;
19
+ this._closeFallbackTimer = null;
16
20
  this.process.stdout.on('data', chunk => {
17
21
  this.stdout += chunk;
18
22
  this.emit('out');
@@ -25,6 +29,11 @@ module.exports = class Process extends EventEmitter {
25
29
  });
26
30
 
27
31
  this.process.on('close', this.onClose.bind(this));
32
+ this.process.on('exit', code => {
33
+ this.exitAt = Date.now();
34
+ this.exitCode = code;
35
+ this.scheduleCloseFallback();
36
+ });
28
37
  this.process.on('error', this.onError.bind(this));
29
38
 
30
39
  this.process.then(result => {
@@ -37,9 +46,35 @@ module.exports = class Process extends EventEmitter {
37
46
  });
38
47
  }
39
48
 
49
+ scheduleCloseFallback() {
50
+ if (!this._killResolve || !this.exitAt || this._closeFallbackTimer || !this._killTimer) {
51
+ return;
52
+ }
53
+
54
+ this._closeFallbackTimer = setTimeout(() => {
55
+ this._closeFallbackTimer = null;
56
+
57
+ if (!this._killResolve || !this.exitAt) {
58
+ return;
59
+ }
60
+
61
+ if (this._killTimer) {
62
+ clearTimeout(this._killTimer);
63
+ this._killTimer = null;
64
+ }
65
+
66
+ this.process = null;
67
+
68
+ const resolve = this._killResolve;
69
+ this._killResolve = null;
70
+ resolve(this.exitCode);
71
+ }, closeTimeoutAfterExit);
72
+ }
73
+
40
74
  onKillTimeout() {
41
75
  log.warn('Process ' + this.name + ' not terminated in ' + this.killTimeout + 'ms.');
42
76
  kill(this.process, 'SIGKILL');
77
+ this.scheduleCloseFallback();
43
78
  }
44
79
 
45
80
  onClose(code) {
@@ -102,7 +137,13 @@ module.exports = class Process extends EventEmitter {
102
137
  let self = this;
103
138
 
104
139
  return new Promise(resolve => {
140
+ self._killResolve = resolve;
105
141
  self.process.once('close', (code, sig) => {
142
+ if (self._closeFallbackTimer) {
143
+ clearTimeout(self._closeFallbackTimer);
144
+ self._closeFallbackTimer = null;
145
+ }
146
+ self._killResolve = null;
106
147
  self.process = null;
107
148
  if (self._killTimer) {
108
149
  clearTimeout(self._killTimer);
@@ -122,6 +163,7 @@ module.exports = class Process extends EventEmitter {
122
163
  // See https://github.com/testem/testem/pull/1335 for more information.
123
164
  if (self.process) {
124
165
  kill(self.process, sig);
166
+ self.scheduleCloseFallback();
125
167
  } else {
126
168
  log.info('Process ' + this.name + ' already killed.');
127
169
  }
@@ -42,7 +42,26 @@ function disposer(promise, cleanup) {
42
42
 
43
43
  async function using(resource, fn) {
44
44
  const isDisposer = resource instanceof Disposer;
45
- const value = await (isDisposer ? resource.promise : resource);
45
+ let value;
46
+
47
+ if (isDisposer) {
48
+ try {
49
+ value = await resource.promise;
50
+ } catch (setupErr) {
51
+ const inspection = {
52
+ isRejected: () => true,
53
+ reason: () => setupErr,
54
+ };
55
+ try {
56
+ await resource.cleanup(undefined, inspection);
57
+ } catch {
58
+ // Prefer the setup error below
59
+ }
60
+ throw setupErr;
61
+ }
62
+ } else {
63
+ value = await resource;
64
+ }
46
65
 
47
66
  let result, rejected = false, error;
48
67
 
@@ -111,7 +111,7 @@ Reporter.with = (app, stdout, path) => disposer(Promise.resolve(new Reporter(app
111
111
  if (promise.isRejected()) {
112
112
  let err = promise.reason();
113
113
 
114
- if (!err.hideFromReporter) {
114
+ if (reporter && !err.hideFromReporter) {
115
115
  reporter.report(null, {
116
116
  passed: false,
117
117
  name: err.name || 'unknown error',
@@ -122,7 +122,7 @@ Reporter.with = (app, stdout, path) => disposer(Promise.resolve(new Reporter(app
122
122
  }
123
123
  }
124
124
 
125
- return reporter.close();
125
+ return reporter ? reporter.close() : Promise.resolve();
126
126
  });
127
127
 
128
128
  function forwardToReporters(fn) {
@@ -0,0 +1,26 @@
1
+
2
+
3
+ const fs = require('fs');
4
+
5
+ const dirsToClean = new Set();
6
+
7
+ process.on('exit', () => {
8
+ for (const dir of dirsToClean) {
9
+ try {
10
+ fs.rmSync(dir, { recursive: true, force: true });
11
+ } catch {
12
+ // Best-effort cleanup: ignore errors during process exit
13
+ }
14
+ }
15
+ });
16
+
17
+ // Allow graceful cleanup when the process is terminated by a signal.
18
+ // Calling process.exit() ensures the 'exit' event fires and cleanup runs.
19
+ process.on('SIGTERM', () => process.exit(0));
20
+ process.on('SIGINT', () => process.exit(130));
21
+
22
+ function registerCleanup(dir) {
23
+ dirsToClean.add(dir);
24
+ }
25
+
26
+ module.exports = { registerCleanup, _registeredDirs: dirsToClean };