UNPKG

15 kBJavaScriptView Raw
1/*
2
3config.js
4=========
5
6This object returns all config info for the app. It handles reading the `testem.yml`
7or `testem.json` config file.
8
9*/
10'use strict';
11
12const os = require('os');
13const fs = require('fs');
14const yaml = require('js-yaml');
15const log = require('npmlog');
16const path = require('path');
17const glob = require('glob');
18const url = require('url');
19const querystring = require('querystring');
20const Bluebird = require('bluebird');
21
22const browser_launcher = require('./browser_launcher');
23const LauncherFactory = require('./launcher-factory');
24const Chars = require('./utils/chars');
25const pad = require('./utils/strutils').pad;
26const isa = require('./utils/isa');
27const fileExists = require('./utils/fileutils').fileExists;
28const uniqBy = require('lodash.uniqby');
29
30const knownBrowsers = require('./utils/known-browsers');
31const globAsync = Bluebird.promisify(glob);
32
33class Config {
34 constructor(appMode, progOptions, config) {
35 this.appMode = appMode;
36 this.progOptions = progOptions || {};
37 this.defaultOptions = {};
38 this.fileOptions = {};
39 this.config = config || {};
40 this.getters = {
41 test_page: 'getTestPage'
42 };
43
44 if (appMode === 'dev') {
45 this.progOptions.reporter = 'dev';
46 this.progOptions.parallel = -1;
47 }
48
49 if (this.progOptions.debug === true) {
50 this.progOptions.debug = 'testem.log';
51 }
52
53 if (appMode === 'ci') {
54 this.progOptions.disable_watching = true;
55 this.progOptions.single_run = true;
56 }
57 }
58
59 setDefaultOptions(defaultOptions) {
60 this.defaultOptions = defaultOptions;
61 }
62
63 read(callback) {
64 let configFile = this.progOptions.file;
65
66 if (configFile) {
67 this.readConfigFile(configFile, callback);
68 } else {
69 log.info('Seeking for config file...');
70
71 // Try all testem.json, testem.yml and testem.js
72 // testem.json gets precedence
73 let files = ['testem.json', '.testem.json', '.testem.yml', 'testem.yml', 'testem.js', '.testem.js'];
74 return Bluebird.filter(files.map(this.resolveConfigPath.bind(this)), fileExists).then(matched => {
75 let configFile = matched[0];
76 if (matched.length > 1) {
77 let baseNames = matched.map(fileName => path.basename(fileName));
78 console.warn('Found ' + matched.length + ' config files (' + baseNames + '), using ' + baseNames[0]);
79 }
80 if (configFile) {
81 this.readConfigFile(configFile, callback);
82 } else {
83 if (callback) {
84 callback.call(this);
85 }
86 }
87 });
88 }
89 }
90
91 resolvePath(filepath) {
92 if (filepath[0] === '/') {
93 return filepath;
94 }
95
96 return path.resolve(this.cwd(), filepath);
97 }
98
99 client() {
100 return {
101 decycle_depth: this.get('client_decycle_depth')
102 };
103 }
104
105 resolveConfigPath(filepath) {
106 if (this.progOptions.config_dir) {
107 return path.resolve(this.progOptions.config_dir, filepath);
108 } else if (this.defaultOptions && this.defaultOptions.config_dir) {
109 return path.resolve(this.defaultOptions.config_dir, filepath);
110 } else {
111 return this.resolvePath(filepath);
112 }
113 }
114
115 reverseResolvePath(filepath) {
116 return path.relative(this.cwd(), filepath);
117 }
118
119 cwd() {
120 return this.get('cwd') || process.cwd();
121 }
122
123 readConfigFile(configFile, callback) {
124 if (!configFile) { // allow empty configFile for programmatic setups
125 if (callback) {
126 callback.call(this);
127 }
128 } else if (configFile.match(/\.c?js$/)) {
129 this.readJS(configFile, callback);
130 } else if (configFile.match(/\.json$/)) {
131 this.readJSON(configFile, callback);
132 } else if (configFile.match(/\.yml$/)) {
133 this.readYAML(configFile, callback);
134 } else {
135 log.error('Unrecognized config file format for ' + configFile);
136 if (callback) {
137 callback.call(this);
138 }
139 }
140 }
141
142 readJS(configFile, callback) {
143 this.fileOptions = require(this.resolveConfigPath(configFile));
144 if (callback) {
145 callback.call(this);
146 }
147 }
148
149 readYAML(configFile, callback) {
150 fs.readFile(configFile, (err, data) => {
151 if (!err) {
152 let cfg = yaml.load(String(data));
153 this.fileOptions = cfg;
154 }
155 if (callback) {
156 callback.call(this);
157 }
158 });
159 }
160
161 readJSON(configFile, callback) {
162 fs.readFile(configFile, (err, data) => {
163 if (!err) {
164 let cfg = JSON.parse(data.toString());
165 this.fileOptions = cfg;
166 this.progOptions.file = configFile;
167 }
168 if (callback) {
169 callback.call(this);
170 }
171 });
172 }
173
174 mergeUrlAndQueryParams(urlString, queryParamsObj) {
175 if (!queryParamsObj) {
176 return urlString;
177 }
178
179 if (typeof queryParamsObj === 'string') {
180 if (queryParamsObj[0] === '?') {
181 queryParamsObj = queryParamsObj.substr(1);
182 }
183 queryParamsObj = querystring.parse(queryParamsObj);
184 }
185
186 let urlObj = url.parse(urlString);
187 let outputQueryParams = querystring.parse(urlObj.query) || {};
188 Object.keys(queryParamsObj).forEach(param => {
189 outputQueryParams[param] = queryParamsObj[param];
190 });
191 urlObj.query = outputQueryParams;
192 urlObj.search = querystring.stringify(outputQueryParams)
193 .replace(/=&/g, '&')
194 .replace(/=$/, '');
195 urlObj.path = urlObj.pathname + urlObj.search;
196 return url.format(urlObj);
197 }
198
199 getTestPage() {
200 let testPage = this.getConfigProperty('test_page');
201 let queryParams = this.getConfigProperty('query_params');
202
203 if (!Array.isArray(testPage)) {
204 testPage = [testPage];
205 }
206
207 return testPage.map(page => this.mergeUrlAndQueryParams(page, queryParams));
208 }
209
210 getConfigProperty(key) {
211 if (this.config && key in this.config) {
212 return this.config[key];
213 }
214 if (key in this.progOptions && typeof this.progOptions[key] !== 'undefined') {
215 return this.progOptions[key];
216 }
217 if (key in this.fileOptions && typeof this.fileOptions[key] !== 'undefined') {
218 return this.fileOptions[key];
219 }
220 if (this.defaultOptions && key in this.defaultOptions && typeof this.defaultOptions[key] !== 'undefined') {
221 return this.defaultOptions[key];
222 }
223 if (key in this.defaults) {
224 let defaultVal = this.defaults[key];
225 if (typeof defaultVal === 'function') {
226 return defaultVal.call(this);
227 } else {
228 return defaultVal;
229 }
230 }
231 }
232
233 get(key) {
234 let getterKey = this.getters[key];
235 let getter = getterKey && this[getterKey];
236 if (getter) {
237 return getter.call(this, key);
238 } else {
239 return this.getConfigProperty(key);
240 }
241 }
242
243 set(key, value) {
244 if (!this.config) {
245 this.config = {};
246 }
247 this.config[key] = value;
248 }
249
250 isCwdMode() {
251 return !this.get('src_files') && !this.get('test_page');
252 }
253
254 getAvailableLaunchers(cb) {
255 let browsers = knownBrowsers(process.platform, this);
256 browser_launcher.getAvailableBrowsers(this, browsers, (err, availableBrowsers) => {
257 if (err) {
258 return cb(err);
259 }
260
261 let availableLaunchers = {};
262 availableBrowsers.forEach(browser => {
263 let newLauncher = new LauncherFactory(browser.name, browser, this);
264 availableLaunchers[browser.name.toLowerCase()] = newLauncher;
265 });
266
267 // add custom launchers
268 let customLaunchers = this.get('launchers');
269 if (customLaunchers) {
270 for (let name in customLaunchers) {
271 let newLauncher = new LauncherFactory(name, customLaunchers[name], this);
272 availableLaunchers[name.toLowerCase()] = newLauncher;
273 }
274 }
275 cb(null, availableLaunchers);
276 });
277 }
278
279 getLaunchers(cb) {
280 this.getAvailableLaunchers((err, availableLaunchers) => {
281 if (err) {
282 return cb(err);
283 }
284
285 this.getWantedLaunchers(availableLaunchers, cb);
286 });
287 }
288
289 getWantedLauncherNames(available) {
290 let launchers = this.get('launch');
291 if (launchers) {
292 launchers = launchers.toLowerCase().split(',');
293 } else if (this.appMode === 'dev') {
294 launchers = this.get('launch_in_dev') || [];
295 } else {
296 launchers = this.get('launch_in_ci') || Object.keys(available);
297 }
298
299 let skip = this.get('skip');
300 if (skip) {
301 skip = skip.toLowerCase().split(',');
302 launchers = launchers.filter(name => skip.indexOf(name) === -1);
303 }
304 return launchers;
305 }
306
307 getWantedLaunchers(available, cb) {
308 let launchers = [];
309 let wanted = this.getWantedLauncherNames(available);
310 let err = null;
311
312 wanted.forEach(name => {
313 let launcher = available[name.toLowerCase()];
314 if (!launcher) {
315 if (this.appMode === 'dev' || this.get('ignore_missing_launchers')) {
316 log.warn('Launcher "' + name + '" is not recognized.');
317 } else {
318 err = new Error('Launcher ' + name + ' not found. Not installed?');
319 }
320 } else {
321 launchers.push(launcher);
322 }
323 });
324 cb(err, launchers);
325 }
326
327 printLauncherInfo() {
328 this.getAvailableLaunchers((err, launchers) => {
329 let launch_in_dev = (this.get('launch_in_dev') || [])
330 .map(s => s.toLowerCase());
331 let launch_in_ci = this.get('launch_in_ci');
332 if (launch_in_ci) {
333 launch_in_ci = launch_in_ci.map(s => s.toLowerCase());
334 }
335 launchers = Object.keys(launchers).map(k => launchers[k]);
336
337 const launcherColumnWidth = launchers.reduce((acc, current) => acc > current.name.length ? acc : current.name.length, 0);
338 const launcherTitle = 'Launcher'.padEnd(launcherColumnWidth, ' ');
339 const launcherTitleDivider = ''.padEnd(launcherColumnWidth, '-');
340
341 console.log('Have ' + launchers.length + ' launchers available; auto-launch info displayed on the right.');
342 console.log(); // newline
343 console.log(`${launcherTitle} Type CI Dev`);
344 console.log(`${launcherTitleDivider} ------------ -- ---`);
345 console.log(launchers.map(launcher => {
346 let protocol = launcher.settings.protocol;
347 let kind = protocol === 'browser' ?
348 'browser' : (
349 protocol === 'tap' ?
350 'process(TAP)' : 'process');
351 let dev = launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1 ?
352 Chars.mark :
353 ' ';
354 let ci = !launch_in_ci || launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1 ?
355 Chars.mark :
356 ' ';
357 return (pad(launcher.name, launcherColumnWidth + 2, ' ', 1) +
358 pad(kind, 12, ' ', 1) +
359 ' ' + ci + ' ' + dev + ' ');
360 }).join('\n'));
361 });
362 }
363
364 getFileSet(want, dontWant, callback) {
365 if (isa(want, String)) {
366 want = [want]; // want is an Array
367 }
368 if (isa(dontWant, String)) {
369 dontWant = [dontWant]; // dontWant is an Array
370 }
371
372 // Filter glob < 6 negation patterns to still support them
373 // See https://github.com/isaacs/node-glob/tree/3f883c43#comments-and-negation
374 let positiveWants = [];
375 want.forEach(patternEntry => {
376 let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
377 if (pattern.indexOf('!') === 0) {
378 return dontWant.push(pattern.substring(1));
379 }
380
381 positiveWants.push(patternEntry);
382 });
383
384 dontWant = dontWant.map(p => p ? this.resolvePath(p) : p);
385 Bluebird.reduce(positiveWants, (allThatIWant, patternEntry) => {
386 let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
387 let attrs = patternEntry.attrs || [];
388 let patternUrl = url.parse(pattern);
389
390 if (patternUrl.protocol === 'file:') {
391 pattern = patternUrl.hostname + patternUrl.path;
392 } else if (patternUrl.protocol) {
393 return allThatIWant.concat({src: pattern, attrs: attrs});
394 }
395
396 return globAsync(this.resolvePath(pattern), { ignore: dontWant }).then(files => allThatIWant.concat(files.map(f => {
397 f = this.reverseResolvePath(f);
398 return {src: f, attrs: attrs};
399 })));
400 }, [])
401 .then(result => uniqBy(result, 'src'))
402 .asCallback(callback);
403 }
404
405 getSrcFiles(callback) {
406 let srcFiles = this.get('src_files') || '*.js';
407 let srcFilesIgnore = this.get('src_files_ignore') || '';
408 this.getFileSet(srcFiles, srcFilesIgnore, callback);
409 }
410
411 getFooterScripts(callback) {
412 var want = this.get('footer_scripts') || [];
413 var dontWant = this.get('src_files_ignore') || '';
414
415 this.getFileSet(want, dontWant, callback);
416 }
417
418
419 getServeFiles(callback) {
420 let want = this.get('serve_files') || this.get('src_files') || '*.js';
421 let dontWant = this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
422 this.getFileSet(want, dontWant, callback);
423 }
424
425 getUserDataDir() {
426 if (this.get('user_data_dir')) {
427 return path.resolve(this.cwd(), this.get('user_data_dir'));
428 }
429
430 return os.tmpdir();
431 }
432
433 getHomeDir() {
434 return process.env.HOME || process.env.USERPROFILE;
435 }
436
437 getCSSFiles(callback) {
438 let want = this.get('css_files') || '';
439 this.getFileSet(want, '', callback);
440 }
441
442 getAllOptions() {
443 let options = [];
444 function getOptions(o) {
445 if (!o) {
446 return;
447 }
448 if (o.options) {
449 o.options.forEach(o => {
450 options.push(o.name());
451 });
452 }
453 getOptions(o.parent);
454 }
455 getOptions(this.progOptions);
456 return options;
457 }
458
459 getTemplateData(cb) {
460 let ret = {};
461 let options = this.getAllOptions();
462 let key;
463 for (key in this.progOptions) {
464 if (options.indexOf(key) !== -1) {
465 ret[key] = this.progOptions[key];
466 }
467 }
468 for (key in this.fileOptions) {
469 ret[key] = this.fileOptions[key];
470 }
471 for (key in this.config) {
472 ret[key] = this.config[key];
473 }
474 this.getServeFiles((err, files) => {
475 let replaceSlashes = f => ({
476 src: f.src.replace(/\\/g, '/'),
477 attrs: f.attrs
478 });
479
480 ret.serve_files = files.map(replaceSlashes);
481
482 this.getCSSFiles((err, files) => {
483 ret.css_files = files.map(replaceSlashes);
484 this.getFooterScripts((err, files) => {
485 ret.footer_scripts = files;
486 if (cb) {
487 cb(err, ret);
488 }
489 });
490 });
491 });
492 }
493}
494
495Config.prototype.defaults = {
496 host: 'localhost',
497 port: 7357,
498 url() {
499 let scheme = 'http';
500 if (this.get('key') || this.get('pfx')) {
501 scheme = 'https';
502 }
503 return scheme + '://' + this.get('host') + ':' + this.get('port') + '/';
504 },
505 parallel: 1,
506 reporter: 'tap',
507 bail_on_uncaught_error: true,
508 browser_start_timeout: 30,
509 browser_disconnect_timeout: 10,
510 browser_reconnect_limit: 3,
511 client_decycle_depth: 5,
512 socket_heartbeat_timeout() {
513 let browserDisconnectTimeout = this.get('browser_disconnect_timeout');
514 let defaultBrowserDisconnectTimeout = this.defaults.browser_disconnect_timeout;
515
516 return (browserDisconnectTimeout !== defaultBrowserDisconnectTimeout) ? browserDisconnectTimeout : 5;
517 }
518};
519
520module.exports = Config;