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.
- package/README.md +32 -9
- package/lib/app.js +186 -116
- package/lib/config.js +209 -109
- package/lib/file_watcher.js +111 -37
- package/lib/file_watcher_impl.js +295 -0
- package/lib/launcher.js +7 -12
- package/lib/reporters/dev/split_log_panel.js +1 -1
- package/lib/runners/browser_test_runner.js +3 -1
- package/lib/runners/hook_runner.js +0 -1
- package/lib/server/index.js +31 -8
- package/lib/tap_consumer.js +73 -11
- package/lib/utils/esbuild-browser-polyfills.js +1 -1
- package/lib/utils/esbuild-buffer-inject.js +1 -1
- package/lib/utils/expand_glob_pattern.js +19 -0
- package/lib/utils/file_watch_glob_policy.js +119 -0
- package/lib/utils/is_emfile_error.js +16 -0
- package/lib/utils/known-browsers.js +155 -118
- package/lib/utils/path_pattern_match.js +39 -0
- package/lib/utils/process.js +42 -0
- package/lib/utils/promises.js +20 -1
- package/lib/utils/reporter.js +2 -2
- package/lib/utils/tmp-cleanup.js +26 -0
- package/package.json +37 -26
- package/testem.js +170 -79
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal `FileWatcher` implementation (see {@link module:lib/file_watcher} for the public API).
|
|
3
|
+
*
|
|
4
|
+
* **Third-party pieces in use today:** `chokidar` (filesystem watch engine), `glob` (`hasMagic`
|
|
5
|
+
* for validating concrete `add` paths), and `minimatch` (include-pattern magic checks). Policy
|
|
6
|
+
* lists and matching live largely in {@link module:lib/utils/file_watch_glob_policy}.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here assumes a single vendor forever: the engine is created only through
|
|
9
|
+
* {@link FileWatcherImpl.createWatcher} (with `chokidar` loaded lazily there), and glob/minimatch
|
|
10
|
+
* are confined to small helpers—so swapping the watcher, glob detection, or minimatch usage is
|
|
11
|
+
* meant to be localized work rather than a rewrite.
|
|
12
|
+
*
|
|
13
|
+
* @module lib/file_watcher_impl
|
|
14
|
+
*/
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const EventEmitter = require('events').EventEmitter;
|
|
17
|
+
const { hasMagic } = require('glob');
|
|
18
|
+
const { Minimatch } = require('minimatch');
|
|
19
|
+
|
|
20
|
+
const isEmfileError = require('./utils/is_emfile_error');
|
|
21
|
+
const { convertToPosix } = require('./utils/posix');
|
|
22
|
+
const {
|
|
23
|
+
buildWatchGlobPolicy,
|
|
24
|
+
pathMatchesWatchTarget,
|
|
25
|
+
} = require('./utils/file_watch_glob_policy');
|
|
26
|
+
|
|
27
|
+
/** @private Only {@link FileWatcherImpl.create} may construct instances. */
|
|
28
|
+
const kInternal = Symbol('FileWatcherImpl.internal');
|
|
29
|
+
|
|
30
|
+
function includePatternHasGlobMagic(pattern) {
|
|
31
|
+
return new Minimatch(pattern, { dot: true }).hasMagic();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolvedPathIsUnderCwd(resolved) {
|
|
35
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
36
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether the watch engine's `all` event (add/addDir) refers to the same path as `file` /
|
|
41
|
+
* `absTarget`.
|
|
42
|
+
*/
|
|
43
|
+
function dynamicAddEventMatchesPath(watchedPath, file, absTarget) {
|
|
44
|
+
const absWatched = path.resolve(watchedPath);
|
|
45
|
+
const relWatched = convertToPosix(
|
|
46
|
+
path.relative(process.cwd(), absWatched),
|
|
47
|
+
);
|
|
48
|
+
const relTarget = convertToPosix(
|
|
49
|
+
path.relative(process.cwd(), absTarget),
|
|
50
|
+
);
|
|
51
|
+
return (
|
|
52
|
+
absWatched === absTarget ||
|
|
53
|
+
relWatched === relTarget ||
|
|
54
|
+
watchedPath === file
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class FileWatcherImpl extends EventEmitter {
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
|
|
61
|
+
* @param {symbol} internalToken
|
|
62
|
+
*/
|
|
63
|
+
constructor(config, internalToken) {
|
|
64
|
+
super();
|
|
65
|
+
|
|
66
|
+
if (internalToken !== kInternal) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
'FileWatcherImpl is not constructable; use FileWatcherImpl.create(config)',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const policy = buildWatchGlobPolicy(config);
|
|
73
|
+
this._watchPolicy = policy;
|
|
74
|
+
this._explicitWatchRelPaths = new Set();
|
|
75
|
+
const opts = { ignoreInitial: true };
|
|
76
|
+
if (policy.ignorePatterns.length) {
|
|
77
|
+
opts.ignored = policy.ignorePatterns;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.fileWatcher = FileWatcherImpl.createWatcher('.', opts);
|
|
81
|
+
|
|
82
|
+
let readySettled = false;
|
|
83
|
+
this._readyPromise = new Promise((resolve, reject) => {
|
|
84
|
+
this.fileWatcher.once('ready', () => {
|
|
85
|
+
readySettled = true;
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
this.fileWatcher.once('error', (err) => {
|
|
89
|
+
if (readySettled || isEmfileError(err)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
readySettled = true;
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < policy.includePatterns.length; i++) {
|
|
98
|
+
const pattern = policy.includePatterns[i];
|
|
99
|
+
if (includePatternHasGlobMagic(pattern)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const resolved = path.resolve(process.cwd(), pattern);
|
|
103
|
+
if (!resolvedPathIsUnderCwd(resolved)) {
|
|
104
|
+
this.fileWatcher.add(path.normalize(pattern));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const onEngineFsEvent = this.onEngineFsEvent.bind(this);
|
|
109
|
+
this.fileWatcher.on('change', onEngineFsEvent);
|
|
110
|
+
this.fileWatcher.on('add', onEngineFsEvent);
|
|
111
|
+
this.fileWatcher.on('unlink', onEngineFsEvent);
|
|
112
|
+
this.fileWatcher.on('unlinkDir', onEngineFsEvent);
|
|
113
|
+
this.fileWatcher.on('error', (err) => {
|
|
114
|
+
if (isEmfileError(err)) {
|
|
115
|
+
this.onEMFILE();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
|
|
122
|
+
* @returns {Promise<FileWatcherImpl>}
|
|
123
|
+
*/
|
|
124
|
+
static async create(config) {
|
|
125
|
+
const fw = new FileWatcherImpl(config, kInternal);
|
|
126
|
+
await fw._readyPromise;
|
|
127
|
+
return fw;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onEngineFsEvent(filePath) {
|
|
131
|
+
const resolved = path.resolve(filePath);
|
|
132
|
+
const posixRel = convertToPosix(
|
|
133
|
+
path.relative(process.cwd(), resolved),
|
|
134
|
+
);
|
|
135
|
+
if (
|
|
136
|
+
pathMatchesWatchTarget(posixRel, this._watchPolicy) ||
|
|
137
|
+
this._explicitWatchRelPaths.has(posixRel)
|
|
138
|
+
) {
|
|
139
|
+
this.onFileChanged(filePath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onFileChanged(filePath) {
|
|
144
|
+
this.emit('fileChanged', filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onEMFILE() {
|
|
148
|
+
this.emit('EMFILE');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {Error} err
|
|
153
|
+
* @returns {Promise<never>}
|
|
154
|
+
*/
|
|
155
|
+
async _closeAfterFailedAdd(err) {
|
|
156
|
+
try {
|
|
157
|
+
await this.close();
|
|
158
|
+
} catch {
|
|
159
|
+
// ignore close errors; primary error is `err`
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} file
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
async _validateConcreteAddPath(file) {
|
|
169
|
+
if (typeof file !== 'string') {
|
|
170
|
+
await this._closeAfterFailedAdd(
|
|
171
|
+
new TypeError('FileWatcher.add expects a string path'),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (file.length === 0) {
|
|
175
|
+
await this._closeAfterFailedAdd(
|
|
176
|
+
new TypeError('FileWatcher.add expects a non-empty path'),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (hasMagic(file, { magicalBraces: true })) {
|
|
180
|
+
await this._closeAfterFailedAdd(
|
|
181
|
+
new TypeError(
|
|
182
|
+
'FileWatcher.add does not accept glob patterns; pass a concrete file path: ' +
|
|
183
|
+
file,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wait for dynamic `add` on the engine to settle (`all` add/addDir, `error`, or timeout).
|
|
191
|
+
*
|
|
192
|
+
* @param {string} file
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async _awaitEngineDynamicAdd(file) {
|
|
196
|
+
const engine = this.fileWatcher;
|
|
197
|
+
const absTarget = path.resolve(process.cwd(), file);
|
|
198
|
+
let onError;
|
|
199
|
+
let onAll;
|
|
200
|
+
let timeoutId;
|
|
201
|
+
|
|
202
|
+
const cleanup = () => {
|
|
203
|
+
if (onError) {
|
|
204
|
+
engine.removeListener('error', onError);
|
|
205
|
+
}
|
|
206
|
+
if (onAll) {
|
|
207
|
+
engine.removeListener('all', onAll);
|
|
208
|
+
}
|
|
209
|
+
if (timeoutId !== undefined) {
|
|
210
|
+
clearTimeout(timeoutId);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
let settled = false;
|
|
216
|
+
|
|
217
|
+
const finish = (err) => {
|
|
218
|
+
if (settled) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
settled = true;
|
|
222
|
+
cleanup();
|
|
223
|
+
if (err) {
|
|
224
|
+
reject(err);
|
|
225
|
+
} else {
|
|
226
|
+
resolve();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
onError = (err) => {
|
|
231
|
+
finish(err);
|
|
232
|
+
};
|
|
233
|
+
engine.prependOnceListener('error', onError);
|
|
234
|
+
|
|
235
|
+
onAll = (event, watchedPath) => {
|
|
236
|
+
if (event === 'add' || event === 'addDir') {
|
|
237
|
+
if (dynamicAddEventMatchesPath(watchedPath, file, absTarget)) {
|
|
238
|
+
finish();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
engine.on('all', onAll);
|
|
243
|
+
|
|
244
|
+
timeoutId = setTimeout(() => finish(), 1000);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
engine.add(file);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
finish(err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} file
|
|
256
|
+
* @returns {Promise<void>}
|
|
257
|
+
*/
|
|
258
|
+
async add(file) {
|
|
259
|
+
await this._validateConcreteAddPath(file);
|
|
260
|
+
const rel = convertToPosix(
|
|
261
|
+
path.relative(
|
|
262
|
+
process.cwd(),
|
|
263
|
+
path.resolve(process.cwd(), file),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
this._explicitWatchRelPaths.add(rel);
|
|
267
|
+
try {
|
|
268
|
+
await this._awaitEngineDynamicAdd(file);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
await this._closeAfterFailedAdd(err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @returns {Promise<void>}
|
|
276
|
+
*/
|
|
277
|
+
close() {
|
|
278
|
+
if (this.fileWatcher) {
|
|
279
|
+
const engine = this.fileWatcher;
|
|
280
|
+
this.fileWatcher = null;
|
|
281
|
+
return Promise.resolve(engine.close()).then(() => {
|
|
282
|
+
this.removeAllListeners();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
this.removeAllListeners();
|
|
286
|
+
return Promise.resolve();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Lazy `require` keeps chokidar off the load path until a real watcher is built (e.g. tests stub this). */
|
|
291
|
+
FileWatcherImpl.createWatcher = function createWatcher(rootPath, opts) {
|
|
292
|
+
return require('chokidar').watch(rootPath, opts);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
module.exports = FileWatcherImpl;
|
package/lib/launcher.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
const log = require('./log');
|
|
4
|
-
const
|
|
2
|
+
const fs = require('fs');
|
|
5
3
|
const path = require('path');
|
|
6
4
|
const { fromCallback } = require('./utils/promises');
|
|
5
|
+
const { registerCleanup } = require('./utils/tmp-cleanup');
|
|
7
6
|
|
|
8
7
|
const template = require('./utils/strutils').template;
|
|
9
8
|
const ProcessCtl = require('./process-ctl');
|
|
10
9
|
|
|
11
|
-
// setup graceful cleanup: removes the created directories when an uncaught exception occurs.
|
|
12
|
-
tmp.setGracefulCleanup();
|
|
13
|
-
|
|
14
10
|
module.exports = class Launcher {
|
|
15
11
|
constructor(name, settings, config) {
|
|
16
12
|
this.name = name;
|
|
@@ -126,12 +122,11 @@ module.exports = class Launcher {
|
|
|
126
122
|
|
|
127
123
|
setupBrowserTmpDir() {
|
|
128
124
|
const userDataDir = this.config.getUserDataDir();
|
|
129
|
-
const tmpPath = path.join(userDataDir, 'testem-' + this.id);
|
|
125
|
+
const tmpPath = path.join(userDataDir, 'testem-' + this.id + '-');
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
127
|
+
const dir = fs.mkdtempSync(tmpPath);
|
|
128
|
+
registerCleanup(dir);
|
|
129
|
+
this.browserTmpDirectory = dir;
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
browserTmpDir() {
|
|
@@ -139,6 +134,6 @@ module.exports = class Launcher {
|
|
|
139
134
|
this.setupBrowserTmpDir();
|
|
140
135
|
}
|
|
141
136
|
|
|
142
|
-
return this.browserTmpDirectory
|
|
137
|
+
return this.browserTmpDirectory;
|
|
143
138
|
}
|
|
144
139
|
};
|
|
@@ -205,7 +205,6 @@ module.exports = View.extend({
|
|
|
205
205
|
var runner = this.get('runner');
|
|
206
206
|
var results = runner.get('results');
|
|
207
207
|
var topLevelError = results ? results.get('topLevelError') : null;
|
|
208
|
-
var tests = null;
|
|
209
208
|
var out = '';
|
|
210
209
|
var pendingOut = '';
|
|
211
210
|
|
|
@@ -213,6 +212,7 @@ module.exports = View.extend({
|
|
|
213
212
|
out += 'Top Level:\n' + indent(topLevelError) + '\n\n';
|
|
214
213
|
}
|
|
215
214
|
|
|
215
|
+
var tests;
|
|
216
216
|
if (results && (tests = results.get('tests')) && !topLevelError) {
|
|
217
217
|
var total = results.get('total');
|
|
218
218
|
var pending = results.get('pending');
|
|
@@ -40,7 +40,9 @@ module.exports = class BrowserTestRunner {
|
|
|
40
40
|
} else {
|
|
41
41
|
this.launcher.start().then(browserProcess => {
|
|
42
42
|
this.process = browserProcess;
|
|
43
|
-
this.
|
|
43
|
+
if (!this.launcher.settings.ignoreProcessExit) {
|
|
44
|
+
this.process.on('processExit', this.onProcessExit.bind(this));
|
|
45
|
+
}
|
|
44
46
|
this.process.on('processError', this.onProcessError.bind(this));
|
|
45
47
|
this.setupStartTimer();
|
|
46
48
|
}).catch(err => {
|
|
@@ -55,7 +55,6 @@ module.exports = class HookRunner {
|
|
|
55
55
|
} else if (exe) {
|
|
56
56
|
args = this.varsub(args || []);
|
|
57
57
|
hookProcessPromise = this.processCtl.spawn(exe, args, options);
|
|
58
|
-
command = exe + ' ' + args.join(' ');
|
|
59
58
|
} else {
|
|
60
59
|
return Promise.reject(new Error('No command or exe/args specified for hook ' + hook));
|
|
61
60
|
}
|
package/lib/server/index.js
CHANGED
|
@@ -103,8 +103,10 @@ class Server extends EventEmitter {
|
|
|
103
103
|
})
|
|
104
104
|
);
|
|
105
105
|
|
|
106
|
+
// In Express 5 (path-to-regexp v8), wildcard params like /*file are captured
|
|
107
|
+
// as an array of path segments, so we rejoin them with '/' to reconstruct the path.
|
|
106
108
|
var serveStaticFile = (req, res) => {
|
|
107
|
-
this.serveStaticFile(req.params
|
|
109
|
+
this.serveStaticFile('/' + req.params.file.join('/'), req, res);
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
if (this.config.get('key') || this.config.get('pfx')) {
|
|
@@ -142,16 +144,31 @@ class Server extends EventEmitter {
|
|
|
142
144
|
res.redirect(`/${String(Math.floor(Math.random() * 10000))}`);
|
|
143
145
|
});
|
|
144
146
|
|
|
145
|
-
app.get(/\/(-?[0-9]+)$/, (req, res) => {
|
|
146
|
-
this.serveHomePage(req, res);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
147
|
app.get('/testem.js', (req, res) => {
|
|
150
148
|
this.serveTestemClientJs(req, res);
|
|
151
149
|
});
|
|
152
150
|
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// Express 5 dropped regex-in-param syntax (e.g. /:id(-?\d+)), so we match
|
|
152
|
+
// any single-segment :id and validate numerically in the handler.
|
|
153
|
+
app.get('/:id', (req, res, next) => {
|
|
154
|
+
if (/^-?\d+$/.test(req.params.id)) {
|
|
155
|
+
this.serveHomePage(req, res);
|
|
156
|
+
} else {
|
|
157
|
+
next();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Serve static files prefixed with a numeric browser id (e.g. /1234/web/test.js).
|
|
162
|
+
// The numeric guard prevents /:id from greedily swallowing non-numeric paths.
|
|
163
|
+
app.all('/:id/*file', (req, res, next) => {
|
|
164
|
+
if (/^-?\d+$/.test(req.params.id)) {
|
|
165
|
+
serveStaticFile(req, res);
|
|
166
|
+
} else {
|
|
167
|
+
next();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// Catch-all: serve any other path as a static file.
|
|
171
|
+
app.all('/*file', serveStaticFile);
|
|
155
172
|
|
|
156
173
|
app.use((err, req, res, next) => {
|
|
157
174
|
if (err) {
|
|
@@ -229,7 +246,13 @@ class Server extends EventEmitter {
|
|
|
229
246
|
});
|
|
230
247
|
return;
|
|
231
248
|
}
|
|
232
|
-
|
|
249
|
+
// Express 5 no longer accepts bare wildcard segments (e.g. /api*). We
|
|
250
|
+
// replace each * in the user-supplied proxy URL with a named param, then
|
|
251
|
+
// append an optional catch-all for trailing path segments.
|
|
252
|
+
let wIdx = 0;
|
|
253
|
+
const proxyPath =
|
|
254
|
+
url.replace(/\*/g, () => `:_proxyW${wIdx++}`).replace(/\/$/, '') + '{/*_proxyRest}';
|
|
255
|
+
app.all(proxyPath, (req, res, next) => {
|
|
233
256
|
var opts = proxies[url];
|
|
234
257
|
if (this.shouldProxy(req, opts)) {
|
|
235
258
|
if (opts.host) {
|
package/lib/tap_consumer.js
CHANGED
|
@@ -3,40 +3,92 @@
|
|
|
3
3
|
const yaml = require('js-yaml');
|
|
4
4
|
const extend = Object.assign;
|
|
5
5
|
const EventEmitter = require('events').EventEmitter;
|
|
6
|
-
const TapParser = require('tap-parser');
|
|
6
|
+
const { Parser: TapParser } = require('tap-parser');
|
|
7
7
|
const log = require('./log');
|
|
8
8
|
|
|
9
9
|
module.exports = class TapConsumer extends EventEmitter {
|
|
10
10
|
constructor() {
|
|
11
11
|
super();
|
|
12
12
|
|
|
13
|
+
this.namePrefix = '';
|
|
14
|
+
this.prefixStack = [];
|
|
15
|
+
this.childParsers = [];
|
|
16
|
+
|
|
13
17
|
this.stream = new TapParser();
|
|
14
|
-
this.stream
|
|
15
|
-
|
|
18
|
+
this.attachParser(this.stream, true);
|
|
19
|
+
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
attachParser(parser, isRoot) {
|
|
22
|
+
parser.on('assert', this.onTapAssert.bind(this));
|
|
23
|
+
parser.on('extra', this.onTapExtra.bind(this));
|
|
24
|
+
parser.on('child', this.onChildParser.bind(this));
|
|
25
|
+
parser.on('complete', this.onParserComplete.bind(this, parser));
|
|
26
|
+
if (isRoot) {
|
|
27
|
+
parser.on('bailout', this.onTapError.bind(this));
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
onChildParser(childParser) {
|
|
32
|
+
this.prefixStack.push(this.namePrefix);
|
|
33
|
+
const parentPrefix = this.namePrefix;
|
|
34
|
+
const segment = (childParser.name && childParser.name.trim()) || '';
|
|
35
|
+
if (parentPrefix && segment) {
|
|
36
|
+
this.namePrefix = `${parentPrefix} > ${segment}`;
|
|
37
|
+
} else if (segment) {
|
|
38
|
+
this.namePrefix = segment;
|
|
39
|
+
} else if (parentPrefix) {
|
|
40
|
+
this.namePrefix = parentPrefix;
|
|
41
|
+
} else {
|
|
42
|
+
this.namePrefix = '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.childParsers.push(childParser);
|
|
46
|
+
this.attachParser(childParser, false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onParserComplete(parser) {
|
|
50
|
+
if (parser === this.stream) {
|
|
51
|
+
this.onTapEnd();
|
|
24
52
|
return;
|
|
25
53
|
}
|
|
54
|
+
this.namePrefix = this.prefixStack.pop();
|
|
55
|
+
const idx = this.childParsers.indexOf(parser);
|
|
56
|
+
if (idx !== -1) {
|
|
57
|
+
this.childParsers.splice(idx, 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onTapAssert(data) {
|
|
62
|
+
log.verbose(JSON.stringify(data, null, 2));
|
|
26
63
|
|
|
27
64
|
if (data.id === undefined) {
|
|
28
65
|
return;
|
|
29
66
|
}
|
|
30
67
|
|
|
68
|
+
const baseName = data.name ? data.name.trim() : '';
|
|
69
|
+
const qualifiedName = this.namePrefix
|
|
70
|
+
? `${this.namePrefix} > ${baseName}`
|
|
71
|
+
: baseName;
|
|
72
|
+
|
|
31
73
|
let test = {
|
|
32
74
|
passed: 0,
|
|
33
75
|
failed: 0,
|
|
34
76
|
total: 1,
|
|
35
77
|
id: data.id,
|
|
36
|
-
name:
|
|
78
|
+
name: qualifiedName,
|
|
37
79
|
items: []
|
|
38
80
|
};
|
|
39
81
|
|
|
82
|
+
if (data.skip) {
|
|
83
|
+
test.skipped = true;
|
|
84
|
+
this.emit('test-result', test);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (data.todo) {
|
|
89
|
+
test.todo = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
40
92
|
if (!data.ok) {
|
|
41
93
|
let stack;
|
|
42
94
|
if (data.diag) {
|
|
@@ -71,6 +123,16 @@ module.exports = class TapConsumer extends EventEmitter {
|
|
|
71
123
|
}
|
|
72
124
|
}
|
|
73
125
|
|
|
126
|
+
detachAllListeners() {
|
|
127
|
+
this.stream.removeAllListeners();
|
|
128
|
+
for (const p of this.childParsers) {
|
|
129
|
+
p.removeAllListeners();
|
|
130
|
+
}
|
|
131
|
+
this.childParsers = [];
|
|
132
|
+
this.prefixStack = [];
|
|
133
|
+
this.namePrefix = '';
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
onTapError(reason) {
|
|
75
137
|
let test = {
|
|
76
138
|
failed: 1,
|
|
@@ -81,13 +143,13 @@ module.exports = class TapConsumer extends EventEmitter {
|
|
|
81
143
|
}
|
|
82
144
|
};
|
|
83
145
|
|
|
84
|
-
this.
|
|
146
|
+
this.detachAllListeners();
|
|
85
147
|
this.emit('test-result', test);
|
|
86
148
|
this.emit('all-test-results');
|
|
87
149
|
}
|
|
88
150
|
|
|
89
151
|
onTapEnd() {
|
|
90
|
-
this.
|
|
152
|
+
this.detachAllListeners();
|
|
91
153
|
this.emit('all-test-results');
|
|
92
154
|
}
|
|
93
155
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared esbuild config for bundling Node.js-dependent test code to run in a
|
|
3
3
|
* browser via testem.
|
|
4
4
|
*
|
|
5
|
-
* esbuild
|
|
5
|
+
* `esbuild-plugins-node-modules-polyfill` stores the `process`
|
|
6
6
|
* polyfill as a scoped variable inside the IIFE it emits, but CJS module
|
|
7
7
|
* factories (wrapped in `__commonJS`) reference `process`, `__dirname`, and
|
|
8
8
|
* `setImmediate` as free globals — causing ReferenceErrors at runtime.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// esbuild inject shim: exposes Buffer as a global for browser bundles.
|
|
2
|
-
// The
|
|
2
|
+
// The esbuild-plugins-node-modules-polyfill plugin resolves `require('buffer')`
|
|
3
3
|
// to a browser polyfill, but does not assign it to globalThis automatically.
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
5
|
if (typeof globalThis !== 'undefined') { globalThis.Buffer = Buffer; }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { glob } = require('glob');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expand one resolved pattern to matching file paths using the project glob backend.
|
|
5
|
+
*
|
|
6
|
+
* Callers pass POSIX-style paths (see `convertToPosix`); this keeps options aligned
|
|
7
|
+
* with `glob` today and allows swapping the implementation later without touching Config.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} resolvedPatternPosix - Single pattern (already resolved to absolute or cwd-relative form, forward slashes).
|
|
10
|
+
* @param {string[]} [ignorePatternsPosix] - Ignore globs in POSIX form; empty entries are dropped.
|
|
11
|
+
* @returns {Promise<string[]>} Matching paths, sorted lexicographically (same ordering as before extraction).
|
|
12
|
+
*/
|
|
13
|
+
async function expandGlobPattern(resolvedPatternPosix, ignorePatternsPosix) {
|
|
14
|
+
const ignore = (ignorePatternsPosix || []).filter(Boolean);
|
|
15
|
+
const files = await glob(resolvedPatternPosix, { ignore });
|
|
16
|
+
return files.slice().sort();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = expandGlobPattern;
|