testem 3.19.1 → 3.20.0

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.
@@ -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 };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "unittest",
8
8
  "browser"
9
9
  ],
10
- "version": "3.19.1",
10
+ "version": "3.20.0",
11
11
  "author": "Toby Ho <airportyh@gmail.com>",
12
12
  "repository": {
13
13
  "type": "git",
@@ -17,34 +17,34 @@
17
17
  "url": "http://github.com/testem/testem/issues"
18
18
  },
19
19
  "engines": {
20
- "node": ">= 7.*"
20
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
21
21
  },
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "@xmldom/xmldom": "^0.8.0",
25
- "backbone": "^1.1.2",
26
- "charm": "^1.0.0",
27
- "commander": "^2.6.0",
28
- "compression": "^1.7.4",
24
+ "@xmldom/xmldom": "^0.9.9",
25
+ "backbone": "^1.6.1",
26
+ "charm": "^1.0.2",
27
+ "chokidar": "^5.0.0",
28
+ "commander": "^14.0.3",
29
+ "compression": "^1.8.1",
29
30
  "consolidate": "^1.0.4",
30
31
  "execa": "^9.6.1",
31
- "express": "^4.10.7",
32
- "fireworm": "^0.7.2",
32
+ "express": "^5.2.1",
33
33
  "glob": "^13.0.6",
34
- "http-proxy": "^1.13.1",
35
- "js-yaml": "^3.2.5",
36
- "lodash": "^4.17.21",
34
+ "http-proxy": "^1.18.1",
35
+ "js-yaml": "^4.1.1",
36
+ "lodash": "^4.18.1",
37
+ "minimatch": "^10.2.5",
37
38
  "mkdirp": "^3.0.1",
38
39
  "mustache": "^4.2.0",
39
- "node-notifier": "^10.0.0",
40
+ "node-notifier": "^10.0.1",
40
41
  "printf": "^0.6.1",
41
- "proc-log": "^5.0.0",
42
+ "proc-log": "^6.1.0",
42
43
  "rimraf": "^6.1.3",
43
- "socket.io": "^4.5.4",
44
+ "socket.io": "^4.8.3",
44
45
  "spawn-args": "^0.2.0",
45
46
  "styled_string": "0.0.1",
46
- "tap-parser": "^7.0.0",
47
- "tmp": "^0.2.5"
47
+ "tap-parser": "^18.3.0"
48
48
  },
49
49
  "files": [
50
50
  "lib",
@@ -61,27 +61,38 @@
61
61
  "test:verbose": "TESTEM_VERBOSE=1 mocha tests/*_tests.js tests/**/*_tests.js",
62
62
  "cover": "cover run ./node_modules/.bin/_mocha tests/*_tests.js tests/**/*_tests.js; cover report html; open cover_html/index.html",
63
63
  "integration": "node ./bin/run-integration.js",
64
+ "ci:safari-smoke": "node ./scripts/safari-ci-smoke.js",
64
65
  "browser-tests": "cd examples/saucelabs/ && ../../testem.js ci -d",
65
66
  "lint": "eslint ."
66
67
  },
67
68
  "devDependencies": {
68
69
  "@cypress/request": "^3.0.10",
69
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
70
+ "@eslint/eslintrc": "^3.3.5",
71
+ "@eslint/js": "^10.0.1",
70
72
  "chai": "^6.2.2",
71
- "chai-files": "^1.2.0",
72
- "chai-shallow-deep-equal": "^1.4.4",
73
- "cheerio": "1.0.0-rc.12",
73
+ "chai-files": "^1.4.0",
74
+ "chai-shallow-deep-equal": "^1.4.6",
75
+ "cheerio": "^1.2.0",
74
76
  "dirty-chai": "^3.0.0",
75
- "esbuild": "^0.25.1",
76
- "eslint": "^9.39.4",
77
- "eslint-plugin-chai-expect": "^3.1.0",
77
+ "esbuild": "^0.27.4",
78
+ "esbuild-plugins-node-modules-polyfill": "^1.8.1",
79
+ "eslint": "^10.2.0",
80
+ "eslint-config-prettier": "^10.1.8",
81
+ "eslint-plugin-chai-expect": "^4.0.0",
78
82
  "eslint-plugin-mocha": "^11.2.0",
79
- "fresh-tape": "^5.5.3",
83
+ "fresh-tape": "^5.9.1",
80
84
  "mocha": "^11.7.5",
81
85
  "saucie": "^4.0.1",
82
86
  "sinon": "^21.0.3",
83
87
  "sinon-chai": "^4.0.1",
84
- "socket.io-client": "^4.1.2"
88
+ "socket.io-client": "^4.8.3"
89
+ },
90
+ "overrides": {
91
+ "mocha": {
92
+ "diff": "^8.0.3",
93
+ "minimatch": "^9.0.7",
94
+ "serialize-javascript": "^7.0.5"
95
+ }
85
96
  },
86
97
  "bin": {
87
98
  "testem": "./testem.js"
package/testem.js CHANGED
@@ -1,76 +1,178 @@
1
1
  #!/usr/bin/env node
2
2
 
3
-
4
- const program = require('commander');
5
- let progOptions = program;
3
+ const { Command } = require('commander');
6
4
  const Config = require('./lib/config');
7
5
  const Api = require('./lib/api');
8
- let appMode = 'dev';
9
-
10
- program
11
- .version(require(__dirname + '/package').version)
12
- .usage('[options]')
13
- .option('-f, --file [file]', 'config file - defaults to testem.json or testem.yml')
14
- .option('-p, --port [num]', 'server port - defaults to 7357', Number)
15
- .option('--host [hostname]', 'host name - defaults to localhost', String)
16
- .option('-l, --launch [list]', 'list of launchers to launch(comma separated)')
17
- .option('-s, --skip [list]', 'list of launchers to skip(comma separated)')
18
- .option('-d, --debug [file]', 'output debug to debug log - defaults to testem.log')
19
- .option('-t, --test_page [page]', 'the html page to drive the tests')
20
- .option('-g, --growl', 'turn on growl / native notifications');
21
-
22
- program
23
- .command('launchers')
24
- .description('Print the list of available launchers (browsers & process launchers)')
25
- .action(act(env => {
26
- env.__proto__ = program;
27
- progOptions = env;
28
- appMode = 'launchers';
29
- }));
30
-
31
- program
32
- .command('ci')
33
- .description('Continuous integration mode')
34
- .option('-T, --timeout [sec]', 'timeout a browser after [sec] seconds', null)
35
- .option('-P, --parallel [num]', 'number of browsers to run in parallel, defaults to 1', Number)
36
- .option('-b, --bail_on_uncaught_error', 'Bail on any uncaught errors')
37
- .option('-R, --reporter [reporter]', 'Test reporter to use [tap|dot|xunit|teamcity]')
38
- .option('--cwd [path]', 'directory to use as root')
39
- .option('--config_dir [path]', 'directory to use as root for resolving configs, if different than cwd')
40
- .action(act(env => {
41
- env.__proto__ = program;
42
- progOptions = env;
43
- appMode = 'ci';
44
- }));
45
-
46
- program
47
- .command('server')
48
- .description('Run just the server')
49
- .action(act(env => {
50
- env.__proto__ = program;
51
- progOptions = env;
52
- appMode = 'server';
53
- }));
54
-
55
- program.on('--help', () => {
56
- console.log(' Keyboard Controls (in dev mode):\n');
57
- console.log(' ENTER run the tests');
58
- console.log(' q quit');
59
- console.log(' LEFT ARROW move to the next browser tab on the left');
60
- console.log(' RIGHT ARROW move to the next browser tab on the right');
61
- console.log(' TAB switch between top and bottom panel (split mode only)');
62
- console.log(' UP ARROW scroll up in the target text panel');
63
- console.log(' DOWN ARROW scroll down in the target text panel');
64
- console.log(' SPACE page down in the target text panel');
65
- console.log(' b page up in the target text panel');
66
- console.log(' d half a page down in the target text panel');
67
- console.log(' u half a page up in the target text panel');
68
- console.log();
69
- });
70
-
71
- main();
6
+
7
+ // this is to workaround the weird behavior in commander where
8
+ // if you provide additional command line arguments that aren't
9
+ // options, it goes in as a string as the 1st argument of the
10
+ // "action" callback, we don't want this
11
+ function act(fun) {
12
+ return function() {
13
+ let options = arguments[arguments.length - 1];
14
+ fun(options);
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Build and parse a fresh Commander program from the given argv array.
20
+ * Returns { progOptions, appMode } where progOptions is a plain object
21
+ * containing all resolved option values (from program.opts() /
22
+ * optsWithGlobals()), so option values are directly accessible as
23
+ * properties rather than requiring access via a Command instance.
24
+ * A _commander reference is attached for internal option enumeration.
25
+ *
26
+ * @param {string[]} argv - process.argv-style array
27
+ * @param {{ exitOverride?: boolean }} [opts]
28
+ * exitOverride: when true, commander throws instead of calling process.exit
29
+ * (useful in tests to prevent the process from terminating)
30
+ */
31
+ function parseArgs(argv, { exitOverride = false } = {}) {
32
+ let progOptions = null;
33
+ let appMode = 'dev';
34
+
35
+ const program = new Command();
36
+
37
+ if (exitOverride) {
38
+ program.exitOverride();
39
+ }
40
+
41
+ program
42
+ .version(require(__dirname + '/package').version)
43
+ .usage('[options]')
44
+ .option(
45
+ '-f, --file [file]',
46
+ 'config file - defaults to testem.json or testem.yml',
47
+ )
48
+ .option('-p, --port [num]', 'server port - defaults to 7357', Number)
49
+ .option('--host [hostname]', 'host name - defaults to localhost', String)
50
+ .option(
51
+ '-l, --launch [list]',
52
+ 'list of launchers to launch(comma separated)',
53
+ )
54
+ .option('-s, --skip [list]', 'list of launchers to skip(comma separated)')
55
+ .option(
56
+ '-d, --debug [file]',
57
+ 'output debug to debug log - defaults to testem.log',
58
+ )
59
+ .option('-t, --test_page [page]', 'the html page to drive the tests')
60
+ .option('-g, --growl', 'turn on growl / native notifications')
61
+ // A root action is required in commander v7+ to prevent the program from
62
+ // automatically displaying help and exiting when no subcommand is given.
63
+ // Without it, running `testem` (dev mode) would show help instead of
64
+ // starting the dev server.
65
+ .action(() => {});
66
+
67
+ program
68
+ .command('launchers')
69
+ .description(
70
+ 'Print the list of available launchers (browsers & process launchers)',
71
+ )
72
+ .action(
73
+ act((env) => {
74
+ progOptions = env.optsWithGlobals();
75
+ progOptions._commander = env;
76
+ appMode = 'launchers';
77
+ }),
78
+ );
79
+
80
+ program
81
+ .command('ci')
82
+ .description('Continuous integration mode')
83
+ .option(
84
+ '-T, --timeout [sec]',
85
+ 'timeout a browser after [sec] seconds',
86
+ null,
87
+ )
88
+ .option(
89
+ '-P, --parallel [num]',
90
+ 'number of browsers to run in parallel, defaults to 1',
91
+ Number,
92
+ )
93
+ .option('-b, --bail_on_uncaught_error', 'Bail on any uncaught errors')
94
+ .option(
95
+ '-R, --reporter [reporter]',
96
+ 'Test reporter to use [tap|dot|xunit|teamcity]',
97
+ )
98
+ .option('--cwd [path]', 'directory to use as root')
99
+ .option(
100
+ '--config_dir [path]',
101
+ 'directory to use as root for resolving configs, if different than cwd',
102
+ )
103
+ .action(
104
+ act((env) => {
105
+ progOptions = env.optsWithGlobals();
106
+ progOptions._commander = env;
107
+ appMode = 'ci';
108
+ }),
109
+ );
110
+
111
+ program
112
+ .command('server')
113
+ .description('Run just the server')
114
+ .action(
115
+ act((env) => {
116
+ progOptions = env.optsWithGlobals();
117
+ progOptions._commander = env;
118
+ appMode = 'server';
119
+ }),
120
+ );
121
+
122
+ program.on('--help', () => {
123
+ console.log(' Keyboard Controls (in dev mode):\n');
124
+ console.log(' ENTER run the tests');
125
+ console.log(' q quit');
126
+ console.log(
127
+ ' LEFT ARROW move to the next browser tab on the left',
128
+ );
129
+ console.log(
130
+ ' RIGHT ARROW move to the next browser tab on the right',
131
+ );
132
+ console.log(
133
+ ' TAB switch between top and bottom panel (split mode only)',
134
+ );
135
+ console.log(
136
+ ' UP ARROW scroll up in the target text panel',
137
+ );
138
+ console.log(
139
+ ' DOWN ARROW scroll down in the target text panel',
140
+ );
141
+ console.log(
142
+ ' SPACE page down in the target text panel',
143
+ );
144
+ console.log(' b page up in the target text panel');
145
+ console.log(
146
+ ' d half a page down in the target text panel',
147
+ );
148
+ console.log(
149
+ ' u half a page up in the target text panel',
150
+ );
151
+ console.log();
152
+ });
153
+
154
+ program.parse(argv);
155
+
156
+ if (progOptions === null) {
157
+ // dev mode: no subcommand was invoked; use opts() so option values are
158
+ // plain properties rather than requiring access via the Command object
159
+ // (commander v14 no longer exposes options as direct Command properties)
160
+ const devOpts = program.opts();
161
+ devOpts._commander = program;
162
+ progOptions = devOpts;
163
+ }
164
+
165
+ return { progOptions, appMode };
166
+ }
167
+
168
+ module.exports = { parseArgs };
169
+
170
+ if (require.main === module) {
171
+ main();
172
+ }
173
+
72
174
  function main() {
73
- program.parse(process.argv);
175
+ const { progOptions, appMode } = parseArgs(process.argv);
74
176
 
75
177
  let config = new Config(appMode, progOptions);
76
178
  if (appMode === 'launchers') {
@@ -86,14 +188,3 @@ function main() {
86
188
  }
87
189
  }
88
190
  }
89
-
90
- // this is to workaround the weird behavior in command where
91
- // if you provide additional command line arguments that aren't
92
- // options, it goes in as a string as the 1st arguments of the
93
- // "action" callback, we don't want this
94
- function act(fun) {
95
- return function() {
96
- let options = arguments[arguments.length - 1];
97
- fun(options);
98
- };
99
- }