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/README.md +175 -134
- package/lib/api.js +1 -1
- package/lib/app.js +186 -116
- package/lib/cli-local-delegation.js +93 -0
- package/lib/config.js +260 -119
- 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/runner_tabs.js +52 -47
- package/lib/reporters/dev/split_log_panel.js +1 -1
- package/lib/reporters/dev/toast_notify.js +5 -0
- package/lib/runners/browser_test_runner.js +3 -1
- package/lib/runners/hook_runner.js +0 -1
- package/lib/server/index.js +39 -10
- 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 +40 -31
- package/testem.js +174 -78
|
@@ -8,22 +8,21 @@ When the tab is not selected, it hides the associated log panel.
|
|
|
8
8
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
const SplitLogPanel = require('./split_log_panel');
|
|
13
12
|
const View = require('./view');
|
|
14
13
|
const Backbone = require('backbone');
|
|
15
14
|
const pad = require('../../utils/strutils').pad;
|
|
16
15
|
const Chars = require('../../utils/chars');
|
|
17
16
|
const Screen = require('./screen');
|
|
18
|
-
const
|
|
17
|
+
const toastNotify = require('./toast_notify');
|
|
19
18
|
const constants = require('./constants');
|
|
20
19
|
const TabWidth = constants.TabWidth;
|
|
21
20
|
const TabStartLine = constants.TabStartLine;
|
|
22
21
|
const TabHeight = constants.TabHeight;
|
|
23
22
|
const TabStartCol = constants.TabStartCol;
|
|
24
|
-
const RunnerTab = exports.RunnerTab = View.extend({
|
|
23
|
+
const RunnerTab = (exports.RunnerTab = View.extend({
|
|
25
24
|
defaults: {
|
|
26
|
-
allPassed: true
|
|
25
|
+
allPassed: true,
|
|
27
26
|
},
|
|
28
27
|
col: TabStartCol,
|
|
29
28
|
line: TabStartLine,
|
|
@@ -46,7 +45,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
46
45
|
runner: runner,
|
|
47
46
|
appview: appview,
|
|
48
47
|
visible: visible,
|
|
49
|
-
screen: this.get('screen')
|
|
48
|
+
screen: this.get('screen'),
|
|
50
49
|
});
|
|
51
50
|
|
|
52
51
|
this.spinnerIdx = 0;
|
|
@@ -56,7 +55,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
this.observe(appview, {
|
|
59
|
-
'change:currentTab': handleCurrentTab
|
|
58
|
+
'change:currentTab': handleCurrentTab,
|
|
60
59
|
});
|
|
61
60
|
this.observe(runner, {
|
|
62
61
|
'change:name'() {
|
|
@@ -77,7 +76,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
77
76
|
},
|
|
78
77
|
'change:allPassed'(model, value) {
|
|
79
78
|
self.set('allPassed', value);
|
|
80
|
-
}
|
|
79
|
+
},
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
if (results) {
|
|
@@ -91,19 +90,19 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
91
90
|
let total = results.get('total');
|
|
92
91
|
let pending = results.get('pending');
|
|
93
92
|
|
|
94
|
-
let allPassed =
|
|
93
|
+
let allPassed = passed + pending === total;
|
|
95
94
|
let hasTests = total > 0;
|
|
96
95
|
let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
|
|
97
|
-
let hasError =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
let hasError =
|
|
97
|
+
runner.get('messages').filter(function (m) {
|
|
98
|
+
return m.get('type') === 'error';
|
|
99
|
+
}).length > 0;
|
|
100
|
+
self.set('allPassed', allPassed && !failCuzNoTests && !hasError);
|
|
102
101
|
}
|
|
103
102
|
},
|
|
104
103
|
'change:all'() {
|
|
105
104
|
self.renderResults();
|
|
106
|
-
}
|
|
105
|
+
},
|
|
107
106
|
});
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -123,7 +122,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
123
122
|
self.renderRunnerName();
|
|
124
123
|
self.renderResults();
|
|
125
124
|
});
|
|
126
|
-
}
|
|
125
|
+
},
|
|
127
126
|
});
|
|
128
127
|
this.render();
|
|
129
128
|
|
|
@@ -132,7 +131,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
132
131
|
|
|
133
132
|
updateSplitPanelVisibility() {
|
|
134
133
|
let appview = this.get('appview');
|
|
135
|
-
this.splitPanel.set(
|
|
134
|
+
this.splitPanel.set(
|
|
135
|
+
'visible',
|
|
136
|
+
this.get('selected') && !appview.isPopupVisible(),
|
|
137
|
+
);
|
|
136
138
|
},
|
|
137
139
|
|
|
138
140
|
color() {
|
|
@@ -147,7 +149,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
147
149
|
let passed = results.get('passed');
|
|
148
150
|
pending = results.get('pending');
|
|
149
151
|
let total = results.get('total');
|
|
150
|
-
equal =
|
|
152
|
+
equal = passed + pending === total;
|
|
151
153
|
hasTests = total > 0;
|
|
152
154
|
}
|
|
153
155
|
let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
|
|
@@ -199,8 +201,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
199
201
|
let runnerName = runner.get('name');
|
|
200
202
|
|
|
201
203
|
// write line 1
|
|
202
|
-
screen
|
|
203
|
-
.foreground(this.color());
|
|
204
|
+
screen.foreground(this.color());
|
|
204
205
|
|
|
205
206
|
if (this.get('selected')) {
|
|
206
207
|
screen.display('bright');
|
|
@@ -233,11 +234,12 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
233
234
|
let passed = results.get('passed');
|
|
234
235
|
let pending = results.get('pending');
|
|
235
236
|
resultsDisplay = passed + '/' + total;
|
|
236
|
-
equal =
|
|
237
|
+
equal = passed + pending === total;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
if (results && results.get('all')) {
|
|
240
|
-
resultsDisplay +=
|
|
241
|
+
resultsDisplay +=
|
|
242
|
+
' ' + (this.get('allPassed') && equal ? Chars.success : Chars.fail);
|
|
241
243
|
} else if (!results && runner.get('allPassed') !== undefined) {
|
|
242
244
|
resultsDisplay = runner.get('allPassed') ? Chars.success : Chars.fail;
|
|
243
245
|
} else {
|
|
@@ -250,8 +252,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
250
252
|
resultsDisplay = pad(resultsDisplay, width - 4, ' ', 2);
|
|
251
253
|
|
|
252
254
|
// write line 1
|
|
253
|
-
screen
|
|
254
|
-
.foreground(this.color());
|
|
255
|
+
screen.foreground(this.color());
|
|
255
256
|
|
|
256
257
|
if (this.get('selected')) {
|
|
257
258
|
screen.display('bright');
|
|
@@ -267,12 +268,13 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
267
268
|
let runner = this.get('runner');
|
|
268
269
|
let results = runner.get('results');
|
|
269
270
|
let name = runner.get('name');
|
|
270
|
-
let resultsDisplay = results
|
|
271
|
-
|
|
271
|
+
let resultsDisplay = results
|
|
272
|
+
? results.get('passed') + '/' + results.get('total')
|
|
273
|
+
: 'finished';
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
title:
|
|
275
|
-
message: name + ' : ' + resultsDisplay
|
|
275
|
+
toastNotify.notify({
|
|
276
|
+
title: "Test'em",
|
|
277
|
+
message: name + ' : ' + resultsDisplay,
|
|
276
278
|
});
|
|
277
279
|
},
|
|
278
280
|
|
|
@@ -329,9 +331,11 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
329
331
|
let firstCol = index === 0;
|
|
330
332
|
screen.position(col, line);
|
|
331
333
|
|
|
332
|
-
screen.write(
|
|
333
|
-
|
|
334
|
-
Chars.
|
|
334
|
+
screen.write(
|
|
335
|
+
(firstCol ? Chars.horizontal : Chars.topLeft) +
|
|
336
|
+
new Array(width - 1).join(Chars.horizontal) +
|
|
337
|
+
Chars.topRight,
|
|
338
|
+
);
|
|
335
339
|
for (let i = 1; i < height - 1; i++) {
|
|
336
340
|
if (!firstCol) {
|
|
337
341
|
screen.position(col, line + i);
|
|
@@ -341,8 +345,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
341
345
|
screen.write(Chars.vertical);
|
|
342
346
|
}
|
|
343
347
|
|
|
344
|
-
let bottomLine =
|
|
345
|
-
|
|
348
|
+
let bottomLine =
|
|
349
|
+
(firstCol ? ' ' : Chars.bottomRight) +
|
|
350
|
+
new Array(width - 1).join(' ') +
|
|
351
|
+
Chars.bottomLeft;
|
|
346
352
|
screen.position(col, line + height - 1);
|
|
347
353
|
screen.write(bottomLine);
|
|
348
354
|
},
|
|
@@ -351,8 +357,8 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
351
357
|
this.stopSpinner();
|
|
352
358
|
this.splitPanel.destroy();
|
|
353
359
|
View.prototype.destroy.call(this);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
356
362
|
|
|
357
363
|
// View container for all the tabs. It'll handle clean up of removed tabs and draw
|
|
358
364
|
// the edge for where there are no tabs.
|
|
@@ -366,12 +372,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
366
372
|
let currentTab = self.appview.get('currentTab');
|
|
367
373
|
if (currentTab >= self.length) {
|
|
368
374
|
currentTab--;
|
|
369
|
-
self.appview.set('currentTab', currentTab, {silent: true});
|
|
375
|
+
self.appview.set('currentTab', currentTab, { silent: true });
|
|
370
376
|
}
|
|
371
377
|
self.forEach((runner, idx) => {
|
|
372
378
|
runner.set({
|
|
373
379
|
index: idx,
|
|
374
|
-
selected: idx === currentTab
|
|
380
|
+
selected: idx === currentTab,
|
|
375
381
|
});
|
|
376
382
|
});
|
|
377
383
|
self.eraseLast();
|
|
@@ -380,9 +386,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
380
386
|
self.blankOutBackground();
|
|
381
387
|
}
|
|
382
388
|
});
|
|
383
|
-
this.appview.on(
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
this.appview.on(
|
|
390
|
+
'change:isPopupVisible change:lines change:cols',
|
|
391
|
+
function () {
|
|
392
|
+
self.reRenderAll();
|
|
393
|
+
},
|
|
394
|
+
);
|
|
386
395
|
},
|
|
387
396
|
|
|
388
397
|
reRenderAll() {
|
|
@@ -397,9 +406,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
397
406
|
let screen = this.screen;
|
|
398
407
|
let cols = this.appview.get('cols');
|
|
399
408
|
for (let i = 0; i < TabHeight; i++) {
|
|
400
|
-
screen
|
|
401
|
-
.position(0, TabStartLine + i)
|
|
402
|
-
.write(pad('', cols, ' ', 1));
|
|
409
|
+
screen.position(0, TabStartLine + i).write(pad('', cols, ' ', 1));
|
|
403
410
|
}
|
|
404
411
|
},
|
|
405
412
|
|
|
@@ -439,9 +446,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
439
446
|
let col = TabStartCol + index * width;
|
|
440
447
|
|
|
441
448
|
for (let i = 0; i < height - 1; i++) {
|
|
442
|
-
screen
|
|
443
|
-
.position(col, line + i)
|
|
444
|
-
.write(new Array(width + 1).join(' '));
|
|
449
|
+
screen.position(col, line + i).write(new Array(width + 1).join(' '));
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
let bottomLine = new Array(width + 1).join(Chars.horizontal);
|
|
@@ -452,5 +457,5 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
452
457
|
isPopupVisible() {
|
|
453
458
|
let appview = this.appview;
|
|
454
459
|
return appview && appview.isPopupVisible();
|
|
455
|
-
}
|
|
460
|
+
},
|
|
456
461
|
});
|
|
@@ -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) {
|
|
@@ -376,6 +399,12 @@ class Server extends EventEmitter {
|
|
|
376
399
|
this.emit('file-requested', filePath);
|
|
377
400
|
});
|
|
378
401
|
} else {
|
|
402
|
+
// `dotfiles: 'allow'` restores the Express 4 (send@0.19) behavior of
|
|
403
|
+
// serving paths that contain intermediate dot-prefixed segments — common
|
|
404
|
+
// in pnpm projects, whose entire dependency tree lives under
|
|
405
|
+
// `node_modules/.pnpm/...`. send@1.x defaults to `'ignore'` and 404s any
|
|
406
|
+
// such path.
|
|
407
|
+
var sendOptions = { dotfiles: 'allow' };
|
|
379
408
|
fs.promises.stat(filePath)
|
|
380
409
|
.then(stat => {
|
|
381
410
|
this.emit('file-requested', filePath);
|
|
@@ -385,11 +414,11 @@ class Server extends EventEmitter {
|
|
|
385
414
|
res.render(dirListingPage, { files: files });
|
|
386
415
|
});
|
|
387
416
|
}
|
|
388
|
-
res.sendFile(filePath);
|
|
417
|
+
res.sendFile(filePath, sendOptions);
|
|
389
418
|
})
|
|
390
419
|
.catch(() => {
|
|
391
420
|
this.emit('file-requested', filePath);
|
|
392
|
-
res.sendFile(filePath);
|
|
421
|
+
res.sendFile(filePath, sendOptions);
|
|
393
422
|
});
|
|
394
423
|
}
|
|
395
424
|
}
|
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;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const url = require('url');
|
|
2
|
+
|
|
3
|
+
const expandGlobPattern = require('./expand_glob_pattern');
|
|
4
|
+
const { convertToPosix } = require('./posix');
|
|
5
|
+
const { pathMatchesAny } = require('./path_pattern_match');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize config `src_files`-style values to a list of glob strings.
|
|
9
|
+
* @param {string|string[]|Array<{src: string}>|undefined|null} value
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
function flattenPatternList(value) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map(function(entry) {
|
|
18
|
+
if (typeof entry === 'string') {
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
if (entry && typeof entry.src === 'string') {
|
|
22
|
+
return entry.src;
|
|
23
|
+
}
|
|
24
|
+
return String(entry);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return [String(value)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the include / ignore pattern lists used by `FileWatcher` with the watch engine
|
|
32
|
+
* (policy filtering; the engine watches cwd and applies these patterns). Single place for glob +
|
|
33
|
+
* ignore semantics.
|
|
34
|
+
*
|
|
35
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
|
|
36
|
+
* @returns {{ includePatterns: string[], ignorePatterns: string[] }}
|
|
37
|
+
*/
|
|
38
|
+
function buildWatchGlobPolicy(config) {
|
|
39
|
+
const includePatterns = [];
|
|
40
|
+
|
|
41
|
+
const confFile = config.get('file');
|
|
42
|
+
if (confFile) {
|
|
43
|
+
includePatterns.push(String(confFile));
|
|
44
|
+
}
|
|
45
|
+
if (config.isCwdMode()) {
|
|
46
|
+
includePatterns.push('*.js');
|
|
47
|
+
}
|
|
48
|
+
const watchFiles = config.get('watch_files');
|
|
49
|
+
if (watchFiles) {
|
|
50
|
+
includePatterns.push(...flattenPatternList(watchFiles));
|
|
51
|
+
}
|
|
52
|
+
const srcFiles = config.get('src_files');
|
|
53
|
+
const srcList = srcFiles !== undefined && srcFiles !== null ? srcFiles : '*.js';
|
|
54
|
+
includePatterns.push(...flattenPatternList(srcList));
|
|
55
|
+
|
|
56
|
+
const ignoreRaw = config.get('src_files_ignore');
|
|
57
|
+
const ignorePatterns = ignoreRaw ? flattenPatternList(ignoreRaw) : [];
|
|
58
|
+
|
|
59
|
+
return { includePatterns, ignorePatterns };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a path would be treated as a watched file (matches an include pattern and
|
|
64
|
+
* no ignore pattern). Paths are normalized with {@link convertToPosix} for matching
|
|
65
|
+
* on Windows vs POSIX.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath - Absolute or relative path as emitted by the filesystem / watcher.
|
|
68
|
+
* @param {{ includePatterns: string[], ignorePatterns: string[] }} policy
|
|
69
|
+
* @param {object} [matchOptions] - forwarded to minimatch (via path_pattern_match).
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
function pathMatchesWatchTarget(filePath, policy, matchOptions) {
|
|
73
|
+
const posix = convertToPosix(filePath);
|
|
74
|
+
if (pathMatchesAny(posix, policy.ignorePatterns, matchOptions)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return pathMatchesAny(posix, policy.includePatterns, matchOptions);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Expand all on-disk files matching the watch policy (same idea as `getFileSet` for
|
|
82
|
+
* `src_files`, but driven by the watch policy). Skips `http:` / `https:` URLs; keeps
|
|
83
|
+
* them as literal entries. Uses {@link expandGlobPattern} with `config.resolvePath`.
|
|
84
|
+
*
|
|
85
|
+
* @param {{ get: function(string): *, cwd: function(): string, resolvePath: function(string): string }} config
|
|
86
|
+
* @returns {Promise<string[]>} Sorted unique absolute paths (and any literal URLs).
|
|
87
|
+
*/
|
|
88
|
+
async function expandWatchableFilePaths(config) {
|
|
89
|
+
const policy = buildWatchGlobPolicy(config);
|
|
90
|
+
const ignorePosix = policy.ignorePatterns.map(function(p) {
|
|
91
|
+
return convertToPosix(config.resolvePath(p));
|
|
92
|
+
});
|
|
93
|
+
const out = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < policy.includePatterns.length; i++) {
|
|
96
|
+
const pattern = policy.includePatterns[i];
|
|
97
|
+
const parsed = url.parse(pattern);
|
|
98
|
+
if (parsed.protocol && parsed.protocol !== 'file:' && parsed.protocol !== '') {
|
|
99
|
+
out.push(pattern);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let fsPattern = pattern;
|
|
103
|
+
if (parsed.protocol === 'file:') {
|
|
104
|
+
fsPattern = parsed.hostname + parsed.path;
|
|
105
|
+
}
|
|
106
|
+
const resolved = convertToPosix(config.resolvePath(fsPattern));
|
|
107
|
+
const files = await expandGlobPattern(resolved, ignorePosix);
|
|
108
|
+
out.push(...files);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [...new Set(out)].sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
buildWatchGlobPolicy,
|
|
116
|
+
flattenPatternList,
|
|
117
|
+
pathMatchesWatchTarget,
|
|
118
|
+
expandWatchableFilePaths,
|
|
119
|
+
};
|