testem 3.19.0 → 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.
@@ -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
+ };
@@ -2,15 +2,15 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const execa = require('execa').execa;
5
- const Bluebird = require('bluebird');
6
- const log = require('npmlog');
5
+ const { promisify } = require('util');
6
+ const log = require('../log');
7
7
 
8
- const fsStatAsync = Bluebird.promisify(fs.stat);
8
+ const fsStatAsync = promisify(fs.stat);
9
9
 
10
10
  const isWin = require('./is-win')();
11
11
 
12
12
  exports.fileExists = function fileExists(path) {
13
- return fsStatAsync(path).then(stat => stat.isFile()).catchReturn(false);
13
+ return fsStatAsync(path).then(stat => stat.isFile()).catch(() => false);
14
14
  };
15
15
 
16
16
  exports.executableExists = function executableExists(exe, options) {
@@ -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 + '\\Local Settings\\Application Data\\Google\\' + name + '\\Application\\chrome.exe',
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
- url
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'].concat(chromeArgs(browserTmpDir, url));
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 + '\\Local Settings\\Application Data\\BraveSoftware\\' + name + '\\Application\\brave.exe',
51
- homeDir + '\\AppData\\Local\\BraveSoftware\\' + name + '\\Application\\brave.exe',
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\\' + name + '\\Application\\brave.exe'
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'].concat(braveArgs(browserTmpDir, url));
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 + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
79
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
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\\' + process.env.USERNAME + '\\AppData\\Local\\Mozilla Firefox\\firefox.exe',
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,24 +111,51 @@ 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.readFile(config.get('firefox_user_js'), (err, content) => {
99
- if (err) {
100
- return done(err);
101
- }
102
-
103
- fs.writeFile(perfJS, content, done);
104
- });
114
+ return fs.promises
115
+ .readFile(config.get('firefox_user_js'))
116
+ .then((content) => fs.promises.writeFile(perfJS, content))
117
+ .then(() => done(null), done);
105
118
  }
106
119
 
107
- // using user.js to suppress the check default browser popup
108
- // and the welcome start page
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.
109
122
  let prefs = [
110
123
  'user_pref("browser.shell.checkDefaultBrowser", false);',
111
124
  'user_pref("browser.cache.disk.smart_size.first_run", false);',
112
125
  'user_pref("datareporting.policy.dataSubmissionEnabled", false);',
113
- 'user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1481830156314");'
126
+ 'user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1481830156314");',
127
+ 'user_pref("app.update.auto", false);',
128
+ 'user_pref("app.update.enabled", false);',
129
+ 'user_pref("browser.EULA.override", true);',
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.writeFile(perfJS, prefs.join(os.EOL), done);
156
+ fs.promises
157
+ .writeFile(perfJS, prefs.join(os.EOL))
158
+ .then(() => done(null), done);
116
159
  }
117
160
 
118
161
  // Return the catalogue of the browsers that Testem supports for the platform. Each 'browser object'
@@ -137,7 +180,7 @@ module.exports = function knownBrowsers(platform, config) {
137
180
  },
138
181
  setup(config, done) {
139
182
  firefoxSetup(this.browserTmpDir(), config, done);
140
- }
183
+ },
141
184
  },
142
185
  {
143
186
  name: 'Headless Firefox',
@@ -149,156 +192,156 @@ module.exports = function knownBrowsers(platform, config) {
149
192
  },
150
193
  setup(config, done) {
151
194
  firefoxSetup(this.browserTmpDir(), config, done);
152
- }
195
+ },
153
196
  },
154
197
  {
155
198
  name: 'Brave',
156
- possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(braveOSXPaths('Brave Browser')),
157
- possibleExe: [
158
- 'brave'
159
- ],
199
+ possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(
200
+ braveOSXPaths('Brave Browser'),
201
+ ),
202
+ possibleExe: ['brave'],
160
203
  args(config, url) {
161
204
  return braveArgs(this.browserTmpDir(), url);
162
- }
205
+ },
163
206
  },
164
207
  {
165
208
  name: 'Headless Brave',
166
- possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(braveOSXPaths('Brave Browser')),
167
- possibleExe: [
168
- 'brave'
169
- ],
209
+ possiblePath: braveWinPaths(userHomeDir, 'Brave-Browser').concat(
210
+ braveOSXPaths('Brave Browser'),
211
+ ),
212
+ possibleExe: ['brave'],
170
213
  args(config, url) {
171
214
  return headlessBraveArgs(this.browserTmpDir(), url);
172
- }
215
+ },
173
216
  },
174
217
  {
175
218
  name: 'Chrome',
176
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(chromeOSXPaths('Google Chrome')),
177
- possibleExe: [
178
- 'google-chrome',
179
- 'google-chrome-stable',
180
- 'chrome'
181
- ],
219
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(
220
+ chromeOSXPaths('Google Chrome'),
221
+ ),
222
+ possibleExe: ['google-chrome', 'google-chrome-stable', 'chrome'],
182
223
  args(config, url) {
183
224
  return chromeArgs(this.browserTmpDir(), url);
184
- }
225
+ },
185
226
  },
186
227
  {
187
228
  name: 'Headless Chrome',
188
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(chromeOSXPaths('Google Chrome')),
189
- possibleExe: [
190
- 'google-chrome',
191
- 'google-chrome-stable',
192
- 'chrome'
193
- ],
229
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome').concat(
230
+ chromeOSXPaths('Google Chrome'),
231
+ ),
232
+ possibleExe: ['google-chrome', 'google-chrome-stable', 'chrome'],
194
233
  args(config, url) {
195
234
  return headlessChromeArgs(this.browserTmpDir(), url);
196
- }
235
+ },
197
236
  },
198
237
 
199
238
  // On Windows, Chrome Stable/Beta/Dev are installed on the same path
200
239
  {
201
240
  name: 'Chrome Beta',
202
241
  possiblePath: chromeOSXPaths('Google Chrome Beta'),
203
- possibleExe: [
204
- 'google-chrome-beta'
205
- ],
242
+ possibleExe: ['google-chrome-beta'],
206
243
  args(config, url) {
207
244
  return chromeArgs(this.browserTmpDir(), url);
208
- }
245
+ },
209
246
  },
210
247
  {
211
248
  name: 'Headless Chrome Beta',
212
249
  possiblePath: chromeOSXPaths('Google Chrome Beta'),
213
- possibleExe: [
214
- 'google-chrome-beta'
215
- ],
250
+ possibleExe: ['google-chrome-beta'],
216
251
  args(config, url) {
217
252
  return headlessChromeArgs(this.browserTmpDir(), url);
218
- }
253
+ },
219
254
  },
220
255
  {
221
256
  name: 'Chrome Dev',
222
257
  possiblePath: chromeOSXPaths('Google Chrome Dev'),
223
- possibleExe: [
224
- 'google-chrome-unstable'
225
- ],
258
+ possibleExe: ['google-chrome-unstable'],
226
259
  args(config, url) {
227
260
  return chromeArgs(this.browserTmpDir(), url);
228
- }
261
+ },
229
262
  },
230
263
 
231
264
  // Chrome Canary is not available on Linux
232
265
  {
233
266
  name: 'Chrome Canary',
234
- possiblePath: chromeWinPaths(userHomeDir, 'Chrome SxS').concat(chromeOSXPaths('Google Chrome Canary')),
267
+ possiblePath: chromeWinPaths(userHomeDir, 'Chrome SxS').concat(
268
+ chromeOSXPaths('Google Chrome Canary'),
269
+ ),
235
270
  args(config, url) {
236
271
  return chromeArgs(this.browserTmpDir(), url);
237
- }
272
+ },
238
273
  },
239
274
  {
240
275
  name: 'Chromium',
241
- possiblePath: chromeWinPaths(userHomeDir, 'Chromium').concat(chromeOSXPaths('Chromium')),
242
- possibleExe: [
243
- 'chromium-browser',
244
- 'chromium'
245
- ],
276
+ possiblePath: chromeWinPaths(userHomeDir, 'Chromium').concat(
277
+ chromeOSXPaths('Chromium'),
278
+ ),
279
+ possibleExe: ['chromium-browser', 'chromium'],
246
280
  args(config, url) {
247
281
  return chromeArgs(this.browserTmpDir(), url);
248
- }
282
+ },
249
283
  },
250
284
  {
251
285
  name: 'Edge',
252
286
  possiblePath: edgePaths(),
253
- possibleExe: [
254
- 'microsoft-edge'
255
- ],
287
+ possibleExe: ['microsoft-edge'],
256
288
  args(config, url) {
257
289
  return chromeArgs(this.browserTmpDir(), url);
258
- }
290
+ },
259
291
  },
260
292
  {
261
293
  name: 'Headless Edge',
262
294
  possiblePath: edgePaths(),
263
- possibleExe: [
264
- 'microsoft-edge'
265
- ],
295
+ possibleExe: ['microsoft-edge'],
266
296
  args(config, url) {
267
297
  return headlessChromeArgs(this.browserTmpDir(), url);
268
- }
269
- },
270
- {
271
- name: 'Safari',
272
- possiblePath: [
273
- 'C:\\Program Files\\Safari\\safari.exe',
274
- 'C:\\Program Files (x86)\\Safari\\safari.exe',
275
- process.env.HOME + '/Applications/Safari.app/Contents/MacOS/Safari',
276
- '/Applications/Safari.app/Contents/MacOS/Safari'
277
- ],
278
- setup(config, done) {
279
- let url = this.getUrl();
280
- fs.writeFile(
281
- path.join(this.browserTmpDir(), 'start.html'),
282
- '<script>window.location = \'' + url + '\'</script>',
283
- done
284
- );
285
298
  },
286
- args() {
287
- return [path.join(this.browserTmpDir(), 'start.html')];
288
- }
289
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
+ },
290
333
  {
291
334
  name: 'Opera',
292
335
  possiblePath: [
293
336
  'C:\\Program Files\\Opera\\opera.exe',
294
337
  'C:\\Program Files (x86)\\Opera\\opera.exe',
295
338
  process.env.HOME + '/Applications/Opera.app/Contents/MacOS/Opera',
296
- '/Applications/Opera.app/Contents/MacOS/Opera'
339
+ '/Applications/Opera.app/Contents/MacOS/Opera',
297
340
  ],
298
341
  args(config, url) {
299
342
  let operaDataDir = this.browserTmpDir();
300
343
  return ['--user-data-dir=' + operaDataDir, '-pd', operaDataDir, url];
301
- }
344
+ },
302
345
  },
303
346
  {
304
347
  name: 'PhantomJS',
@@ -319,43 +362,38 @@ module.exports = function knownBrowsers(platform, config) {
319
362
  options = phantom_args.concat(options);
320
363
  }
321
364
  return options;
322
- }
323
- }
365
+ },
366
+ },
324
367
  ];
325
368
 
326
369
  if (isWin(platform)) {
327
370
  browsers.push({
328
371
  name: 'IE',
329
- possiblePath: 'C:\\Program Files\\Internet Explorer\\iexplore.exe'
372
+ possiblePath: 'C:\\Program Files\\Internet Explorer\\iexplore.exe',
330
373
  });
331
- } else {
374
+ } else if (platform === 'darwin') {
332
375
  browsers.push({
376
+ // Same Launch Services approach as Safari on macOS (#1387).
333
377
  name: 'Safari Technology Preview',
334
- possiblePath: [
335
- process.env.HOME + '/Applications/Safari Technology Preview.app/Contents/MacOS/Safari Technology Preview',
336
- '/Applications/Safari Technology Preview.app/Contents/MacOS/Safari Technology Preview'
337
- ],
338
- setup(config, done) {
339
- let url = this.getUrl();
340
- fs.writeFile(
341
- path.join(this.browserTmpDir(), 'start.html'),
342
- '<script>window.location = \'' + url + '\'</script>',
343
- done
344
- );
378
+ possiblePath: '/usr/bin/open',
379
+ ignoreProcessExit: true,
380
+ args(config, url) {
381
+ return ['-a', 'Safari Technology Preview', url];
345
382
  },
346
- args() {
347
- return [path.join(this.browserTmpDir(), 'start.html')];
348
- }
349
383
  });
350
384
  }
351
385
 
352
386
  // Add user-defined configs to browser objects
353
387
  let browserPaths = config.get('browser_paths'); // Add config defined 'path' for binary
354
- let browserExes = config.get('browser_exes'); // Add config defined 'exe' for binary
388
+ let browserExes = config.get('browser_exes'); // Add config defined 'exe' for binary
355
389
  if (browserPaths || browserExes) {
356
- browsers.forEach(browserObject => {
357
- let browserPath = browserPaths ? browserPaths[browserObject.name] : undefined;
358
- let browserExe = browserExes ? browserExes[browserObject.name] : undefined;
390
+ browsers.forEach((browserObject) => {
391
+ let browserPath = browserPaths
392
+ ? browserPaths[browserObject.name]
393
+ : undefined;
394
+ let browserExe = browserExes
395
+ ? browserExes[browserObject.name]
396
+ : undefined;
359
397
  if (browserPath) {
360
398
  browserObject.possiblePath = browserPath;
361
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
+ };