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