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
package/lib/config.js
CHANGED
|
@@ -8,13 +8,11 @@ or `testem.json` config file.
|
|
|
8
8
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
const os = require('os');
|
|
13
12
|
const fs = require('fs');
|
|
14
13
|
const yaml = require('js-yaml');
|
|
15
14
|
const log = require('./log');
|
|
16
15
|
const path = require('path');
|
|
17
|
-
const glob = require('glob').glob;
|
|
18
16
|
const url = require('url');
|
|
19
17
|
const querystring = require('querystring');
|
|
20
18
|
const { filter, reduce } = require('./utils/promises');
|
|
@@ -29,7 +27,7 @@ const _ = require('lodash');
|
|
|
29
27
|
|
|
30
28
|
const knownBrowsers = require('./utils/known-browsers');
|
|
31
29
|
const { convertToPosix } = require('./utils/posix');
|
|
32
|
-
const
|
|
30
|
+
const expandGlobPattern = require('./utils/expand_glob_pattern');
|
|
33
31
|
|
|
34
32
|
class Config {
|
|
35
33
|
constructor(appMode, progOptions, config) {
|
|
@@ -39,7 +37,7 @@ class Config {
|
|
|
39
37
|
this.fileOptions = {};
|
|
40
38
|
this.config = config || {};
|
|
41
39
|
this.getters = {
|
|
42
|
-
test_page: 'getTestPage'
|
|
40
|
+
test_page: 'getTestPage',
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
if (appMode === 'dev') {
|
|
@@ -71,12 +69,31 @@ class Config {
|
|
|
71
69
|
|
|
72
70
|
// Try all testem.json, testem.yml and testem.js
|
|
73
71
|
// testem.json gets precedence
|
|
74
|
-
let files = [
|
|
75
|
-
|
|
72
|
+
let files = [
|
|
73
|
+
'testem.json',
|
|
74
|
+
'.testem.json',
|
|
75
|
+
'.testem.yml',
|
|
76
|
+
'testem.yml',
|
|
77
|
+
'testem.js',
|
|
78
|
+
'.testem.js',
|
|
79
|
+
'testem.cjs',
|
|
80
|
+
'.testem.cjs',
|
|
81
|
+
];
|
|
82
|
+
return filter(
|
|
83
|
+
files.map(this.resolveConfigPath.bind(this)),
|
|
84
|
+
fileExists,
|
|
85
|
+
).then((matched) => {
|
|
76
86
|
let configFile = matched[0];
|
|
77
87
|
if (matched.length > 1) {
|
|
78
|
-
let baseNames = matched.map(fileName => path.basename(fileName));
|
|
79
|
-
console.warn(
|
|
88
|
+
let baseNames = matched.map((fileName) => path.basename(fileName));
|
|
89
|
+
console.warn(
|
|
90
|
+
'Found ' +
|
|
91
|
+
matched.length +
|
|
92
|
+
' config files (' +
|
|
93
|
+
baseNames +
|
|
94
|
+
'), using ' +
|
|
95
|
+
baseNames[0],
|
|
96
|
+
);
|
|
80
97
|
}
|
|
81
98
|
if (configFile) {
|
|
82
99
|
this.readConfigFile(configFile, callback);
|
|
@@ -99,7 +116,7 @@ class Config {
|
|
|
99
116
|
|
|
100
117
|
client() {
|
|
101
118
|
return {
|
|
102
|
-
decycle_depth: this.get('client_decycle_depth')
|
|
119
|
+
decycle_depth: this.get('client_decycle_depth'),
|
|
103
120
|
};
|
|
104
121
|
}
|
|
105
122
|
|
|
@@ -122,7 +139,8 @@ class Config {
|
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
readConfigFile(configFile, callback) {
|
|
125
|
-
if (!configFile) {
|
|
142
|
+
if (!configFile) {
|
|
143
|
+
// allow empty configFile for programmatic setups
|
|
126
144
|
if (callback) {
|
|
127
145
|
callback.call(this);
|
|
128
146
|
}
|
|
@@ -143,7 +161,7 @@ class Config {
|
|
|
143
161
|
readJS(configFile, callback) {
|
|
144
162
|
let exportedConfig = require(this.resolveConfigPath(configFile));
|
|
145
163
|
if (typeof exportedConfig === 'function') {
|
|
146
|
-
Promise.resolve(exportedConfig()).then(obj => {
|
|
164
|
+
Promise.resolve(exportedConfig()).then((obj) => {
|
|
147
165
|
this.fileOptions = obj;
|
|
148
166
|
if (callback) {
|
|
149
167
|
callback.call(this);
|
|
@@ -158,10 +176,14 @@ class Config {
|
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
readYAML(configFile, callback) {
|
|
161
|
-
return fs.promises
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
return fs.promises
|
|
180
|
+
.readFile(configFile)
|
|
181
|
+
.then(
|
|
182
|
+
(data) => {
|
|
183
|
+
this.fileOptions = yaml.load(String(data));
|
|
184
|
+
},
|
|
185
|
+
() => {},
|
|
186
|
+
)
|
|
165
187
|
.then(() => {
|
|
166
188
|
if (callback) {
|
|
167
189
|
callback.call(this);
|
|
@@ -170,11 +192,15 @@ class Config {
|
|
|
170
192
|
}
|
|
171
193
|
|
|
172
194
|
readJSON(configFile, callback) {
|
|
173
|
-
return fs.promises
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
return fs.promises
|
|
196
|
+
.readFile(configFile)
|
|
197
|
+
.then(
|
|
198
|
+
(data) => {
|
|
199
|
+
this.fileOptions = JSON.parse(data.toString());
|
|
200
|
+
this.progOptions.file = configFile;
|
|
201
|
+
},
|
|
202
|
+
() => {},
|
|
203
|
+
)
|
|
178
204
|
.then(() => {
|
|
179
205
|
if (callback) {
|
|
180
206
|
callback.call(this);
|
|
@@ -196,11 +222,12 @@ class Config {
|
|
|
196
222
|
|
|
197
223
|
let urlObj = url.parse(urlString);
|
|
198
224
|
let outputQueryParams = querystring.parse(urlObj.query) || {};
|
|
199
|
-
Object.keys(queryParamsObj).forEach(param => {
|
|
225
|
+
Object.keys(queryParamsObj).forEach((param) => {
|
|
200
226
|
outputQueryParams[param] = queryParamsObj[param];
|
|
201
227
|
});
|
|
202
228
|
urlObj.query = outputQueryParams;
|
|
203
|
-
urlObj.search = querystring
|
|
229
|
+
urlObj.search = querystring
|
|
230
|
+
.stringify(outputQueryParams)
|
|
204
231
|
.replace(/=&/g, '&')
|
|
205
232
|
.replace(/=$/, '');
|
|
206
233
|
urlObj.path = urlObj.pathname + urlObj.search;
|
|
@@ -215,20 +242,32 @@ class Config {
|
|
|
215
242
|
testPage = [testPage];
|
|
216
243
|
}
|
|
217
244
|
|
|
218
|
-
return testPage.map(page =>
|
|
245
|
+
return testPage.map((page) =>
|
|
246
|
+
this.mergeUrlAndQueryParams(page, queryParams),
|
|
247
|
+
);
|
|
219
248
|
}
|
|
220
249
|
|
|
221
250
|
getConfigProperty(key) {
|
|
222
251
|
if (this.config && key in this.config) {
|
|
223
252
|
return this.config[key];
|
|
224
253
|
}
|
|
225
|
-
if (
|
|
254
|
+
if (
|
|
255
|
+
key in this.progOptions &&
|
|
256
|
+
typeof this.progOptions[key] !== 'undefined'
|
|
257
|
+
) {
|
|
226
258
|
return this.progOptions[key];
|
|
227
259
|
}
|
|
228
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
key in this.fileOptions &&
|
|
262
|
+
typeof this.fileOptions[key] !== 'undefined'
|
|
263
|
+
) {
|
|
229
264
|
return this.fileOptions[key];
|
|
230
265
|
}
|
|
231
|
-
if (
|
|
266
|
+
if (
|
|
267
|
+
this.defaultOptions &&
|
|
268
|
+
key in this.defaultOptions &&
|
|
269
|
+
typeof this.defaultOptions[key] !== 'undefined'
|
|
270
|
+
) {
|
|
232
271
|
return this.defaultOptions[key];
|
|
233
272
|
}
|
|
234
273
|
if (key in this.defaults) {
|
|
@@ -264,27 +303,35 @@ class Config {
|
|
|
264
303
|
|
|
265
304
|
getAvailableLaunchers(cb) {
|
|
266
305
|
let browsers = knownBrowsers(process.platform, this);
|
|
267
|
-
browser_launcher.getAvailableBrowsers(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
306
|
+
browser_launcher.getAvailableBrowsers(
|
|
307
|
+
this,
|
|
308
|
+
browsers,
|
|
309
|
+
(err, availableBrowsers) => {
|
|
310
|
+
if (err) {
|
|
311
|
+
return cb(err);
|
|
312
|
+
}
|
|
271
313
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
314
|
+
let availableLaunchers = {};
|
|
315
|
+
availableBrowsers.forEach((browser) => {
|
|
316
|
+
let newLauncher = new LauncherFactory(browser.name, browser, this);
|
|
317
|
+
availableLaunchers[browser.name.toLowerCase()] = newLauncher;
|
|
318
|
+
});
|
|
277
319
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
320
|
+
// add custom launchers
|
|
321
|
+
let customLaunchers = this.get('launchers');
|
|
322
|
+
if (customLaunchers) {
|
|
323
|
+
for (let name in customLaunchers) {
|
|
324
|
+
let newLauncher = new LauncherFactory(
|
|
325
|
+
name,
|
|
326
|
+
customLaunchers[name],
|
|
327
|
+
this,
|
|
328
|
+
);
|
|
329
|
+
availableLaunchers[name.toLowerCase()] = newLauncher;
|
|
330
|
+
}
|
|
284
331
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
cb(null, availableLaunchers);
|
|
333
|
+
},
|
|
334
|
+
);
|
|
288
335
|
}
|
|
289
336
|
|
|
290
337
|
getLaunchers(cb) {
|
|
@@ -310,7 +357,7 @@ class Config {
|
|
|
310
357
|
let skip = this.get('skip');
|
|
311
358
|
if (skip) {
|
|
312
359
|
skip = skip.toLowerCase().split(',');
|
|
313
|
-
launchers = launchers.filter(name => skip.indexOf(name) === -1);
|
|
360
|
+
launchers = launchers.filter((name) => skip.indexOf(name) === -1);
|
|
314
361
|
}
|
|
315
362
|
return launchers;
|
|
316
363
|
}
|
|
@@ -320,7 +367,7 @@ class Config {
|
|
|
320
367
|
let wanted = this.getWantedLauncherNames(available);
|
|
321
368
|
let err = null;
|
|
322
369
|
|
|
323
|
-
wanted.forEach(name => {
|
|
370
|
+
wanted.forEach((name) => {
|
|
324
371
|
let launcher = available[name.toLowerCase()];
|
|
325
372
|
if (!launcher) {
|
|
326
373
|
if (this.appMode === 'dev' || this.get('ignore_missing_launchers')) {
|
|
@@ -328,10 +375,12 @@ class Config {
|
|
|
328
375
|
} else {
|
|
329
376
|
err = new Error(
|
|
330
377
|
'Launcher ' +
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
378
|
+
name +
|
|
379
|
+
' not found. Not installed? Available: ' +
|
|
380
|
+
Object.keys(available)
|
|
381
|
+
.map((elm) => `"${elm}"`)
|
|
382
|
+
.join(', ') +
|
|
383
|
+
'.',
|
|
335
384
|
);
|
|
336
385
|
}
|
|
337
386
|
} else {
|
|
@@ -343,38 +392,62 @@ class Config {
|
|
|
343
392
|
|
|
344
393
|
printLauncherInfo() {
|
|
345
394
|
this.getAvailableLaunchers((err, launchers) => {
|
|
346
|
-
let launch_in_dev = (this.get('launch_in_dev') || [])
|
|
347
|
-
|
|
395
|
+
let launch_in_dev = (this.get('launch_in_dev') || []).map((s) =>
|
|
396
|
+
s.toLowerCase(),
|
|
397
|
+
);
|
|
348
398
|
let launch_in_ci = this.get('launch_in_ci');
|
|
349
399
|
if (launch_in_ci) {
|
|
350
|
-
launch_in_ci = launch_in_ci.map(s => s.toLowerCase());
|
|
400
|
+
launch_in_ci = launch_in_ci.map((s) => s.toLowerCase());
|
|
351
401
|
}
|
|
352
|
-
launchers = Object.keys(launchers).map(k => launchers[k]);
|
|
402
|
+
launchers = Object.keys(launchers).map((k) => launchers[k]);
|
|
353
403
|
|
|
354
|
-
const launcherColumnWidth = launchers.reduce(
|
|
404
|
+
const launcherColumnWidth = launchers.reduce(
|
|
405
|
+
(acc, current) =>
|
|
406
|
+
acc > current.name.length ? acc : current.name.length,
|
|
407
|
+
0,
|
|
408
|
+
);
|
|
355
409
|
const launcherTitle = 'Launcher'.padEnd(launcherColumnWidth, ' ');
|
|
356
410
|
const launcherTitleDivider = ''.padEnd(launcherColumnWidth, '-');
|
|
357
411
|
|
|
358
|
-
console.log(
|
|
412
|
+
console.log(
|
|
413
|
+
'Have ' +
|
|
414
|
+
launchers.length +
|
|
415
|
+
' launchers available; auto-launch info displayed on the right.',
|
|
416
|
+
);
|
|
359
417
|
console.log(); // newline
|
|
360
418
|
console.log(`${launcherTitle} Type CI Dev`);
|
|
361
419
|
console.log(`${launcherTitleDivider} ------------ -- ---`);
|
|
362
|
-
console.log(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
420
|
+
console.log(
|
|
421
|
+
launchers
|
|
422
|
+
.map((launcher) => {
|
|
423
|
+
let protocol = launcher.settings.protocol;
|
|
424
|
+
let kind =
|
|
425
|
+
protocol === 'browser'
|
|
426
|
+
? 'browser'
|
|
427
|
+
: protocol === 'tap'
|
|
428
|
+
? 'process(TAP)'
|
|
429
|
+
: 'process';
|
|
430
|
+
let dev =
|
|
431
|
+
launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1
|
|
432
|
+
? Chars.mark
|
|
433
|
+
: ' ';
|
|
434
|
+
let ci =
|
|
435
|
+
!launch_in_ci ||
|
|
436
|
+
launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1
|
|
437
|
+
? Chars.mark
|
|
438
|
+
: ' ';
|
|
439
|
+
return (
|
|
440
|
+
pad(launcher.name, launcherColumnWidth + 2, ' ', 1) +
|
|
441
|
+
pad(kind, 12, ' ', 1) +
|
|
442
|
+
' ' +
|
|
443
|
+
ci +
|
|
444
|
+
' ' +
|
|
445
|
+
dev +
|
|
446
|
+
' '
|
|
447
|
+
);
|
|
448
|
+
})
|
|
449
|
+
.join('\n'),
|
|
450
|
+
);
|
|
378
451
|
});
|
|
379
452
|
}
|
|
380
453
|
|
|
@@ -389,7 +462,7 @@ class Config {
|
|
|
389
462
|
// Filter glob < 6 negation patterns to still support them
|
|
390
463
|
// See https://github.com/isaacs/node-glob/tree/3f883c43#comments-and-negation
|
|
391
464
|
let positiveWants = [];
|
|
392
|
-
want.forEach(patternEntry => {
|
|
465
|
+
want.forEach((patternEntry) => {
|
|
393
466
|
let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
|
|
394
467
|
if (pattern.indexOf('!') === 0) {
|
|
395
468
|
return dontWant.push(pattern.substring(1));
|
|
@@ -398,33 +471,51 @@ class Config {
|
|
|
398
471
|
positiveWants.push(patternEntry);
|
|
399
472
|
});
|
|
400
473
|
|
|
401
|
-
dontWant = dontWant.map(p => p ? this.resolvePath(p) : p);
|
|
402
|
-
return reduce(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
474
|
+
dontWant = dontWant.map((p) => (p ? this.resolvePath(p) : p));
|
|
475
|
+
return reduce(
|
|
476
|
+
positiveWants,
|
|
477
|
+
(allThatIWant, patternEntry) => {
|
|
478
|
+
let pattern = isa(patternEntry, String)
|
|
479
|
+
? patternEntry
|
|
480
|
+
: patternEntry.src;
|
|
481
|
+
let attrs = patternEntry.attrs || [];
|
|
482
|
+
let patternUrl = url.parse(pattern);
|
|
483
|
+
|
|
484
|
+
if (patternUrl.protocol === 'file:') {
|
|
485
|
+
pattern = patternUrl.hostname + patternUrl.path;
|
|
486
|
+
} else if (patternUrl.protocol) {
|
|
487
|
+
return allThatIWant.concat({ src: pattern, attrs: attrs });
|
|
488
|
+
}
|
|
412
489
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
490
|
+
return expandGlobPattern(
|
|
491
|
+
convertToPosix(this.resolvePath(pattern)),
|
|
492
|
+
dontWant.map(convertToPosix),
|
|
493
|
+
).then((files) =>
|
|
494
|
+
allThatIWant.concat(
|
|
495
|
+
files.map((f) => {
|
|
496
|
+
f = this.reverseResolvePath(f);
|
|
497
|
+
return { src: f, attrs: attrs };
|
|
498
|
+
}),
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
},
|
|
502
|
+
[],
|
|
503
|
+
)
|
|
504
|
+
.then((result) => _.uniqBy(result, 'src'))
|
|
505
|
+
.then(
|
|
506
|
+
(result) => {
|
|
507
|
+
if (callback) {
|
|
508
|
+
callback(null, result);
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
},
|
|
512
|
+
(err) => {
|
|
513
|
+
if (callback) {
|
|
514
|
+
callback(err);
|
|
515
|
+
}
|
|
516
|
+
throw err;
|
|
517
|
+
},
|
|
518
|
+
);
|
|
428
519
|
}
|
|
429
520
|
|
|
430
521
|
getSrcFiles(callback) {
|
|
@@ -440,10 +531,10 @@ class Config {
|
|
|
440
531
|
return this.getFileSet(want, dontWant, callback);
|
|
441
532
|
}
|
|
442
533
|
|
|
443
|
-
|
|
444
534
|
getServeFiles(callback) {
|
|
445
535
|
let want = this.get('serve_files') || this.get('src_files') || '*.js';
|
|
446
|
-
let dontWant =
|
|
536
|
+
let dontWant =
|
|
537
|
+
this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
|
|
447
538
|
return this.getFileSet(want, dontWant, callback);
|
|
448
539
|
}
|
|
449
540
|
|
|
@@ -471,13 +562,19 @@ class Config {
|
|
|
471
562
|
return;
|
|
472
563
|
}
|
|
473
564
|
if (o.options) {
|
|
474
|
-
o.options.forEach(o => {
|
|
565
|
+
o.options.forEach((o) => {
|
|
475
566
|
options.push(o.name());
|
|
476
567
|
});
|
|
477
568
|
}
|
|
478
569
|
getOptions(o.parent);
|
|
479
570
|
}
|
|
480
|
-
|
|
571
|
+
const progOpts = this.progOptions;
|
|
572
|
+
// In subcommand modes, progOptions is a plain object from optsWithGlobals() with
|
|
573
|
+
// a _commander reference pointing to the Command instance for option enumeration.
|
|
574
|
+
// In dev mode (no subcommand), progOptions is the Commander program object directly.
|
|
575
|
+
getOptions(
|
|
576
|
+
progOpts && progOpts._commander ? progOpts._commander : progOpts,
|
|
577
|
+
);
|
|
481
578
|
return options;
|
|
482
579
|
}
|
|
483
580
|
|
|
@@ -497,16 +594,16 @@ class Config {
|
|
|
497
594
|
ret[key] = this.config[key];
|
|
498
595
|
}
|
|
499
596
|
|
|
500
|
-
const replaceSlashes = f => ({
|
|
597
|
+
const replaceSlashes = (f) => ({
|
|
501
598
|
src: f.src.replace(/\\/g, '/'),
|
|
502
|
-
attrs: f.attrs
|
|
599
|
+
attrs: f.attrs,
|
|
503
600
|
});
|
|
504
601
|
|
|
505
602
|
try {
|
|
506
603
|
const [serveFiles, cssFiles, footerScripts] = await Promise.all([
|
|
507
604
|
this.getServeFiles(),
|
|
508
605
|
this.getCSSFiles(),
|
|
509
|
-
this.getFooterScripts()
|
|
606
|
+
this.getFooterScripts(),
|
|
510
607
|
]);
|
|
511
608
|
|
|
512
609
|
ret.serve_files = serveFiles.map(replaceSlashes);
|
|
@@ -543,10 +640,13 @@ Config.prototype.defaults = {
|
|
|
543
640
|
client_decycle_depth: 5,
|
|
544
641
|
socket_heartbeat_timeout() {
|
|
545
642
|
let browserDisconnectTimeout = this.get('browser_disconnect_timeout');
|
|
546
|
-
let defaultBrowserDisconnectTimeout =
|
|
643
|
+
let defaultBrowserDisconnectTimeout =
|
|
644
|
+
this.defaults.browser_disconnect_timeout;
|
|
547
645
|
|
|
548
|
-
return
|
|
549
|
-
|
|
646
|
+
return browserDisconnectTimeout !== defaultBrowserDisconnectTimeout
|
|
647
|
+
? browserDisconnectTimeout
|
|
648
|
+
: 5;
|
|
649
|
+
},
|
|
550
650
|
};
|
|
551
651
|
|
|
552
652
|
module.exports = Config;
|
package/lib/file_watcher.js
CHANGED
|
@@ -1,51 +1,125 @@
|
|
|
1
|
+
const EventEmitter = require('events').EventEmitter;
|
|
1
2
|
|
|
3
|
+
const Impl = require('./file_watcher_impl');
|
|
2
4
|
|
|
3
|
-
const
|
|
4
|
-
const EventEmitter = require('events').EventEmitter;
|
|
5
|
+
const kCompose = Symbol('FileWatcher.compose');
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Coordinates file watching for Testem: it observes the current working directory (and any
|
|
9
|
+
* non-glob paths from config that live outside it), applies the same include / ignore rules as
|
|
10
|
+
* the rest of the app (`src_files`, `src_files_ignore`, `watch_files`, etc.), and reports when
|
|
11
|
+
* paths that matter for the test run change.
|
|
12
|
+
*
|
|
13
|
+
* Listen for {@link FileWatcher#event:fileChanged} to react to changes, and optionally for
|
|
14
|
+
* {@link FileWatcher#event:EMFILE} when the process hits open-file limits.
|
|
15
|
+
*
|
|
16
|
+
* ### Construction
|
|
17
|
+
*
|
|
18
|
+
* **Do not call `new FileWatcher(...)`.** The constructor is not part of the public API; it
|
|
19
|
+
* exists only for internal wiring and will always throw if you invoke it from outside this
|
|
20
|
+
* module. Obtain instances exclusively via {@link FileWatcher.create} with a config object that
|
|
21
|
+
* exposes `get(key)` and `isCwdMode()` (the same shape as Testem’s `Config`).
|
|
22
|
+
*
|
|
23
|
+
* ### Async vs synchronous
|
|
24
|
+
*
|
|
25
|
+
* **Return a `Promise` — use `await`, `.then()`, or `.catch()`:**
|
|
26
|
+
* {@link FileWatcher.create}, {@link FileWatcher#add}, {@link FileWatcher#close}.
|
|
27
|
+
*
|
|
28
|
+
* **Synchronous:** {@link FileWatcher#onFileChanged}, {@link FileWatcher#onEMFILE}, and
|
|
29
|
+
* inherited `EventEmitter` methods (`on`, `once`, `emit`, `removeListener`, …).
|
|
30
|
+
*
|
|
31
|
+
* @hideconstructor
|
|
32
|
+
* @fires FileWatcher#event:fileChanged
|
|
33
|
+
* @fires FileWatcher#event:EMFILE
|
|
34
|
+
*/
|
|
35
|
+
class FileWatcher extends EventEmitter {
|
|
36
|
+
/**
|
|
37
|
+
* Not for public use. Application code must use {@link FileWatcher.create} instead; calling
|
|
38
|
+
* `new FileWatcher` directly always throws.
|
|
39
|
+
*
|
|
40
|
+
* @private
|
|
41
|
+
* @param {*} impl Internal implementation instance.
|
|
42
|
+
* @param {symbol} token Internal composition token (not exposed).
|
|
43
|
+
*/
|
|
44
|
+
constructor(impl, token) {
|
|
8
45
|
super();
|
|
46
|
+
if (token !== kCompose) {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
'FileWatcher is not constructable; use FileWatcher.create(config)',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this._impl = impl;
|
|
52
|
+
impl.on('fileChanged', (filePath) => this.emit('fileChanged', filePath));
|
|
53
|
+
impl.on('EMFILE', () => this.emit('EMFILE'));
|
|
54
|
+
}
|
|
9
55
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Create a watcher, wait until the underlying engine has finished its initial setup, and
|
|
58
|
+
* return a ready-to-use `FileWatcher`. This is the **only** supported way to obtain an
|
|
59
|
+
* instance.
|
|
60
|
+
*
|
|
61
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config Testem-style
|
|
62
|
+
* config (typically a `Config` instance): `get` reads keys such as `src_files`,
|
|
63
|
+
* `src_files_ignore`, `watch_files`, `file`, `disable_watching`; `isCwdMode` reflects cwd
|
|
64
|
+
* mode.
|
|
65
|
+
* @returns {Promise<FileWatcher>} Resolves with a connected watcher once startup is complete.
|
|
66
|
+
* **Await** this promise; if the engine fails during startup, the promise rejects.
|
|
67
|
+
*/
|
|
68
|
+
static async create(config) {
|
|
69
|
+
const impl = await Impl.create(config);
|
|
70
|
+
return new FileWatcher(impl, kCompose);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Request that a **concrete** filesystem path be watched in addition to the configured
|
|
75
|
+
* patterns (for example, a file the dev server started serving after startup). Glob or
|
|
76
|
+
* minimatch strings are not accepted here — only real paths; pattern-based includes stay in
|
|
77
|
+
* config.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} file Path relative to the current working directory or absolute, as
|
|
80
|
+
* accepted by the engine.
|
|
81
|
+
* @returns {Promise<void>} Settles when the engine has finished the add request. **Await**
|
|
82
|
+
* this promise (or attach `.catch()`) so validation errors, engine errors, and internal
|
|
83
|
+
* teardown on failure are observed.
|
|
84
|
+
*/
|
|
85
|
+
add(file) {
|
|
86
|
+
return this._impl.add(file);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tear down the watcher: close the engine, release OS watch handles, and remove listeners on
|
|
91
|
+
* this `FileWatcher`. Safe to call more than once; subsequent calls resolve promptly.
|
|
92
|
+
*
|
|
93
|
+
* @returns {Promise<void>} Resolves when shutdown is complete. **Await** if later code must
|
|
94
|
+
* run only after handles are released.
|
|
95
|
+
*/
|
|
96
|
+
close() {
|
|
97
|
+
return Promise.resolve(this._impl.close()).then(() => {
|
|
98
|
+
this.removeAllListeners();
|
|
13
99
|
});
|
|
14
|
-
let onFileChanged = this.onFileChanged.bind(this);
|
|
15
|
-
this.fileWatcher.on('change', onFileChanged);
|
|
16
|
-
this.fileWatcher.on('add', onFileChanged);
|
|
17
|
-
this.fileWatcher.on('remove', onFileChanged);
|
|
18
|
-
this.fileWatcher.on('emfile', this.onEMFILE.bind(this));
|
|
19
|
-
|
|
20
|
-
let watchFiles = config.get('watch_files');
|
|
21
|
-
this.fileWatcher.clear();
|
|
22
|
-
let confFile = config.get('file');
|
|
23
|
-
if (confFile) {
|
|
24
|
-
this.fileWatcher.add(confFile);
|
|
25
|
-
}
|
|
26
|
-
if (config.isCwdMode()) {
|
|
27
|
-
this.fileWatcher.add('*.js');
|
|
28
|
-
}
|
|
29
|
-
if (watchFiles) {
|
|
30
|
-
this.fileWatcher.add(watchFiles);
|
|
31
|
-
}
|
|
32
|
-
let srcFiles = config.get('src_files') || '*.js';
|
|
33
|
-
this.fileWatcher.add(srcFiles);
|
|
34
|
-
let ignoreFiles = config.get('src_files_ignore');
|
|
35
|
-
if (ignoreFiles) {
|
|
36
|
-
this.fileWatcher.ignore(ignoreFiles);
|
|
37
|
-
}
|
|
38
100
|
}
|
|
39
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Manually signal that a path changed, for the same listeners as a real `fileChanged` event.
|
|
104
|
+
* Used in tests and narrow control flows; normal file edits are reported automatically by the
|
|
105
|
+
* engine.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} filePath Path that should be treated as changed.
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
40
110
|
onFileChanged(filePath) {
|
|
41
|
-
this.
|
|
111
|
+
return this._impl.onFileChanged(filePath);
|
|
42
112
|
}
|
|
43
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Manually emit the `EMFILE` condition (too many open files) for listeners that show user
|
|
116
|
+
* guidance. The real engine also surfaces this when appropriate.
|
|
117
|
+
*
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
44
120
|
onEMFILE() {
|
|
45
|
-
this.
|
|
121
|
+
return this._impl.onEMFILE();
|
|
46
122
|
}
|
|
123
|
+
}
|
|
47
124
|
|
|
48
|
-
|
|
49
|
-
this.fileWatcher.add(file);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
125
|
+
module.exports = FileWatcher;
|